Согласно соответствующей статье в Википедии, уровень изолированности транзакций - это "значение, определяющее уровень, при котором в транзакции допускаются несогласованные данные", то есть как и когда изменения, выполненные одной операцией, станут видимы для других. Целью этой статьи является объяснить механизм изолированности транзакций в хранилище Google App Engine. После прочтения этой статьи вы будете глукобо понимать, как ведут себя конкурирующие операции чтения и записи данных.
В стандарте SQL-92 определены четыре уровня изолированности транзакций, поддерживаемых базами данных: упорядочиваемость (Serializable), повторяемость чтения (Repeatable Read), чтение фиксированных данных (Read Committed) и чтение незафиксированных данных (Read Uncommitted). Уровень, используемый для работы с хранилищем наиболее близко приближен к типу чтение фиксированных данных (Read Committed). Все объекты, загруженные из хранилища при помощи запросов или операций get(), будут содержать только данные, зафиксированные транзакциями (commit). Загруженные объекты никогда не будут иметь частично сохраненных данных. Взаимодействие между запросами и транзакциями довольно нетривиальное, и чтобы понять это давайте глубже рассмотрим процесс commit().
Операция commit() проходит через два состояния: момент времени, в котором изменения были применены к объекту, и момент, когда они же были записаны в соответствующие индексы. Давайте обозначим первый как Момент A, и второй (когда операция commit() завершается) - Момент B. При достижении Момента A, все изменения в объект хранилища уже применены. При достижении Момента B, применены все изменения к индексам, в которых участвует этот объект.
Запрос к данным, который извлекает объект из хранилища с помощью его ключа после Момента А, гарантированно получит последние изменения этого объекта. Однако, если приложение выполняет запрос к данным (с условием типа 'where'), который удовлетворяет только свойствам объектов, после их обновления, результирующая выборка будет содержать данные только после того, когда операция commit() достигнет Момента B. Другими словами в некоторые моменты времени возможны ситуации, когда при извлечении объектов с помощью ключей, изменения будут видимы, а при выполнении запросов - нет. Обратите внимание, что при извлечении таких данных в период между Моментом A и Моментом B, запрос получит объекты с уже обновленными свойствами по состоянию на Момент A.
Ниже мы опишем на примере как взаимодействуют конкурентные обновления данных и запросы к ним, и хотя вам может показаться это объяснение слишком тривиальным, его достаточно чтобы понять основную концепцию работы хранилища. Давайте попробуем. Мы начнем с нескольких простых примеров и закончим более сложными.
Допустим наше приложение работает с данными персонала - объектами типа Person. Объект Person содержит следующие данные:
Наше приложение выполняет следующие операции:
updatePerson()getTallPeople(), которая возвращает список всех людей выше 72 дюймовСейчас мы имеем в хранилище 2 объекта Person:
Представим, что приложение обрабатывает в один момент времени два запроса и производит соответственно две операции с данными. Первый из них изменяет рост Адама с 68 дюймов до 74. Неплохо подрос! Второй выполняет запрос getTallPeople(). Что возвращает getTallPeople()?
Ответ зависит от времени, когда произошла операция commit() Запроса 1 и операция getTallPeople() Запроса 2. Представим, что это происходит в следующей последовательности:
put()getTallPeople()put()-->commit()put()-->commit()-->Момент Aput()-->commit()-->Момент BВ этом случае функция getTallPeople() вернет только объект с данными Боба. Почему? Так как изменение данных Адама, которые увеличивают его рост еще не были зафиксированы на тот момент времени, они не были видимы операции извлечения данных, которая выполнялась в Запросе 2.
Теперь представим другую ситуацию:
put()put()-->commit()put()-->commit()-->Момент AgetTallPeople()put()-->commit()-->Момент BВ этом случае формируется запрос к данным в тот момент, до того как Запрос 1 достигнет Момента B, таким образом изменения в индексы, соответствующие объекту Person, еще не будут применены. В результате функция getTallPeople() также вернет только одного Боба. В этом примере мы увидели, что результат запроса к данным не содержит объекта, который удовлетворяет условиям.
В этом примере мы изменим работу Запроса 1. Вместо увеличения роста Адама с 68 дюймов до 74, мы произведем уменьшение роста Боба с 73 до 65 дюймов. И снова попробуем получить данные с помощью функции getTallPeople()
put()getTallPeople()put()-->commit()put()-->commit()-->Момент Aput()-->commit()-->Момент BВ этом случае функция getTallPeople() вернет только объект с данными Боба. Почему? Так как изменение данных Боба, которые уменьшают его рост, еще не были зафиксированы в тот момент времени, они не были видимы операции извлечения данных, которая выполнялась в Запросе 2.
Теперь представим другую ситуацию:
put()put()-->commit()put()-->commit()-->Момент Aput()-->commit()-->Момент BgetTallPeople()В этом случае функция getTallPeople() вообще не вернет ни одного объекта. Почему? Так как изменения данных Боба уже были зафиксированы, когда мы выполнили операцию в Запросе 2.
Теперь представим другую ситуацию:
put()put()-->commit()put()-->commit()-->Момент AgetTallPeople()put()-->commit()-->Момент BВ этом случае запрос к данным выполняется до Момента B, таким образом в это время изменения к индексам еще не были применены. В результате функция getTallPeople() также вернет Боба, но свойство его объекта будет содержать уже обновленное значение: 65. В этом примере результат содержит объект, свойства которого не удовлетворяют условию запроса.
Как вы видели из предыдущих примеров, уровень изолированности транзакций платформы Google App Engine очень близок к Read committed (чтение фиксированных данных). Конечно, отличия от стандартов достаточно существенные, но теперь вы можете понять их и причины такого поведения и сможете в дальнейшем правильно проектировать работу с данными в ваших приложениях.