• Официальный сайт SDK
  • Сайт с примерами кода

Изолированность транзакций в App Engine

Макс Росс, инженер по программному обеспечению
13 мая, 2008

Введение

Согласно соответствующей статье в Википедии, уровень изолированности транзакций - это "значение, определяющее уровень, при котором в транзакции допускаются несогласованные данные", то есть как и когда изменения, выполненные одной операцией, станут видимы для других. Целью этой статьи является объяснить механизм изолированности транзакций в хранилище Google App Engine. После прочтения этой статьи вы будете глукобо понимать, как ведут себя конкурирующие операции чтения и записи данных.

Чтение фиксированных данных

В стандарте SQL-92 определены четыре уровня изолированности транзакций, поддерживаемых базами данных: упорядочиваемость (Serializable), повторяемость чтения (Repeatable Read), чтение фиксированных данных (Read Committed) и чтение незафиксированных данных (Read Uncommitted). Уровень, используемый для работы с хранилищем наиболее близко приближен к типу чтение фиксированных данных (Read Committed). Все объекты, загруженные из хранилища при помощи запросов или операций get(), будут содержать только данные, зафиксированные транзакциями (commit). Загруженные объекты никогда не будут иметь частично сохраненных данных. Взаимодействие между запросами и транзакциями довольно нетривиальное, и чтобы понять это давайте глубже рассмотрим процесс commit().

Процесс commit()

Операция commit() проходит через два состояния: момент времени, в котором изменения были применены к объекту, и момент, когда они же были записаны в соответствующие индексы. Давайте обозначим первый как Момент A, и второй (когда операция commit() завершается) - Момент B. При достижении Момента A, все изменения в объект хранилища уже применены. При достижении Момента B, применены все изменения к индексам, в которых участвует этот объект.



Запрос к данным, который извлекает объект из хранилища с помощью его ключа после Момента А, гарантированно получит последние изменения этого объекта. Однако, если приложение выполняет запрос к данным (с условием типа 'where'), который удовлетворяет только свойствам объектов, после их обновления, результирующая выборка будет содержать данные только после того, когда операция commit() достигнет Момента B. Другими словами в некоторые моменты времени возможны ситуации, когда при извлечении объектов с помощью ключей, изменения будут видимы, а при выполнении запросов - нет. Обратите внимание, что при извлечении таких данных в период между Моментом A и Моментом B, запрос получит объекты с уже обновленными свойствами по состоянию на Момент A.

Примеры

Ниже мы опишем на примере как взаимодействуют конкурентные обновления данных и запросы к ним, и хотя вам может показаться это объяснение слишком тривиальным, его достаточно чтобы понять основную концепцию работы хранилища. Давайте попробуем. Мы начнем с нескольких простых примеров и закончим более сложными.

Допустим наше приложение работает с данными персонала - объектами типа Person. Объект Person содержит следующие данные:

  • Имя
  • Рост

Наше приложение выполняет следующие операции:

  • updatePerson()
  • getTallPeople(), которая возвращает список всех людей выше 72 дюймов

Сейчас мы имеем в хранилище 2 объекта Person:

  • Адам с ростом 68 дюймов.
  • Боб с ростом 73 дюймов.

Пример 1 - делаем Адама выше

Представим, что приложение обрабатывает в один момент времени два запроса и производит соответственно две операции с данными. Первый из них изменяет рост Адама с 68 дюймов до 74. Неплохо подрос! Второй выполняет запрос getTallPeople(). Что возвращает getTallPeople()?

Ответ зависит от времени, когда произошла операция commit() Запроса 1 и операция getTallPeople() Запроса 2. Представим, что это происходит в следующей последовательности:

  • Запрос 1, put()
  • Запрос 2, getTallPeople()
  • Запрос 1, put()-->commit()
  • Запрос 1, put()-->commit()-->Момент A
  • Запрос 1, put()-->commit()-->Момент B

В этом случае функция getTallPeople() вернет только объект с данными Боба. Почему? Так как изменение данных Адама, которые увеличивают его рост еще не были зафиксированы на тот момент времени, они не были видимы операции извлечения данных, которая выполнялась в Запросе 2.

Теперь представим другую ситуацию:

  • Запрос 1, put()
  • Запрос 1, put()-->commit()
  • Запрос 1, put()-->commit()-->Момент A
  • Запрос 2, getTallPeople()
  • Запрос 1, put()-->commit()-->Момент B

В этом случае формируется запрос к данным в тот момент, до того как Запрос 1 достигнет Момента B, таким образом изменения в индексы, соответствующие объекту Person, еще не будут применены. В результате функция getTallPeople() также вернет только одного Боба. В этом примере мы увидели, что результат запроса к данным не содержит объекта, который удовлетворяет условиям.

Пример 2 - Делаем Боба ниже (прости нас Боб!)

В этом примере мы изменим работу Запроса 1. Вместо увеличения роста Адама с 68 дюймов до 74, мы произведем уменьшение роста Боба с 73 до 65 дюймов. И снова попробуем получить данные с помощью функции getTallPeople()

  • Запрос 1, put()
  • Запрос 2, getTallPeople()
  • Запрос 1, put()-->commit()
  • Запрос 1, put()-->commit()-->Момент A
  • Запрос 1, put()-->commit()-->Момент B

В этом случае функция getTallPeople() вернет только объект с данными Боба. Почему? Так как изменение данных Боба, которые уменьшают его рост, еще не были зафиксированы в тот момент времени, они не были видимы операции извлечения данных, которая выполнялась в Запросе 2.

Теперь представим другую ситуацию:

  • Запрос 1, put()
  • Запрос 1, put()-->commit()
  • Запрос 1, put()-->commit()-->Момент A
  • Запрос 1, put()-->commit()-->Момент B
  • Запрос 2, getTallPeople()

В этом случае функция getTallPeople() вообще не вернет ни одного объекта. Почему? Так как изменения данных Боба уже были зафиксированы, когда мы выполнили операцию в Запросе 2.

Теперь представим другую ситуацию:

  • Запрос 1, put()
  • Запрос 1, put()-->commit()
  • Запрос 1, put()-->commit()-->Момент A
  • Запрос 2, getTallPeople()
  • Запрос 1, put()-->commit()-->Момент B

В этом случае запрос к данным выполняется до Момента B, таким образом в это время изменения к индексам еще не были применены. В результате функция getTallPeople() также вернет Боба, но свойство его объекта будет содержать уже обновленное значение: 65. В этом примере результат содержит объект, свойства которого не удовлетворяют условию запроса.

Заключение

Как вы видели из предыдущих примеров, уровень изолированности транзакций платформы Google App Engine очень близок к Read committed (чтение фиксированных данных). Конечно, отличия от стандартов достаточно существенные, но теперь вы можете понять их и причины такого поведения и сможете в дальнейшем правильно проектировать работу с данными в ваших приложениях.