Избранное | Русский | Войти

Транзакции

Хранилище данных App Engine поддерживает транзакции. Транзакция представляет собой одну или несколько операций, которые либо полностью выполняются, либо полностью не выполняются. Приложение может выполнить несколько операций и вычислений в единой транзакции.

Использование транзакций

Транзакция представляет собой одну или несколько операций в хранилище данных, которые либо полностью выполняются, либо полностью не выполняются. Если транзакция выполняется успешно, то к хранилищу данных применяются результаты всех ее действий. Если во время выполнения транзакции произошел сбой, не применяется ни один.

Каждая операция записи в хранилище данных является атомарной. Создание, обновление или удаление объекта либо выполняется, либо нет. Операция может не быть выполнена из-за многочисленных конфликтов, возникающих вследствие одновременных попыток изменения одного и того же объекта несколькими пользователями. Она также может не быть выполнена, если приложение достигло назначенной квоты. Кроме того, в хранилище данных может возникнуть внутренняя ошибка. Во всех этих случаях результаты выполнения операции не применяются и API хранилища данных вызывает исключение.

Приложение может выполнить ряд операторов и операций в хранилище данных в одной транзакции так, что если во время выполнения одного оператора или операции возникает исключение, ни одна из операций в хранилище данных не применяется. Приложение определяет действия, которые будут выполнены в транзакции, с помощью функции Python, затем вызывает функцию db.run_in_transaction(), указывая в качестве аргумента эту функцию.

from google.appengine.ext import db

class Accumulator(db.Model):
  counter = db.IntegerProperty()

def increment_counter(key, amount):
  obj = db.get(key)
  obj.counter += amount
  obj.put()

q = db.GqlQuery("SELECT * FROM Accumulator")
acc = q.get()

db.run_in_transaction(increment_counter, acc.key(), 5)

Функция db.run_in_transaction() получает функциональный объект, позиционные и именованные аргументы, которые передаются в функцию. Если функция возвращает значение, то функция db.run_in_transaction() также вернет значение.

Если функция возвращает значение, транзакция выполняется, и все результаты операций в хранилище данных применяются. Если возникает исключение, то транзакция "откатывается", и результаты не применяются.

Если возникает исключение Rollback, то функция db.run_in_transaction() возвращает значение None. В случае другого исключения у функции db.run_in_transaction() возникнет то же исключение.

Возможные действия в транзакциях

Хранилище данных налагает ряд ограничений на действия, которые можно выполнить в одной транзакции.

Все операции в хранилище данных, включенные в транзакцию, должны работать с объектами из одной группы. Это относится к получению объектов по ключу, обновлению и удалению объектов. Обратите внимание, что все корневые объекты входят в разные группы, поэтому одна транзакция не может создать или выполнить какие-либо иные действия более чем с одним корневым объектом. О группах объектов рассказано в разделе Ключи и группы объектов.

Приложение не может выполнять запросы во время транзакции. Однако во время транзакции приложение может получать объекты из хранилища данных с помощью ключей и гарантированно получит объект, согласованный с остальными действиями транзакции. Ключи можно подготовить перед выполнением транзакции или создать внутри транзакции с помощью названий ключей или идентификаторов.

Приложение не может создать или обновить объект более одного раза в одной транзакции.

Любой другой код Python допустим внутри функции транзакции. Функция транзакции не должна иметь побочных эффектов, отличных от результатов выполнения операций. Функцию транзакции можно вызывать несколько раз, если не удается выполнить операцию в хранилище данных из-за обновления объектов в группе другим пользователем в одно и то же время. Когда это происходит, API хранилища данных пытается повторить транзакцию определенное количество раз. Если ни один из них не завершается успешно, во время выполнения функции db.run_in_transaction() возникает TransactionFailedError. Можно регулировать количество повторных попыток выполнения транзакции, используя метод db.run_in_transaction_custom_retries() вместо метода db.run_in_transaction().

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

Применение транзакций

В следующем примере показан один из вариантов применения транзакций: обновление объекта путем присвоения его свойству нового значения.

def increment_counter(key, amount):
  obj = db.get(key)
  obj.counter += amount
  obj.put()

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

Другое распространенное применение транзакций – обновление объекта с помощью именованного ключа либо создание объекта, если он не существует:

class SalesAccount(db.Model):
  address = db.PostalAddressProperty()
  phone_number = db.PhoneNumberProperty()

def create_or_update(parent_obj, account_id, address, phone_number):
  obj = db.get(Key.from_path("SalesAccount", account_id, parent=parent_obj))
  if not obj:
    obj = SalesAccount(key_name=account_id,
                       parent=parent_obj,
                       address=address,
                       phone_number=phone_number)
  else:
    obj.address = address
    obj.phone_number = phone_number

  obj.put()

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

Функция "создать или обновить" настолько полезна, что для ее выполнения существует встроенный метод Model.get_or_insert(), который получает название ключа, родителя (необязательно) и аргументы, передаваемые конструктору модели, если объект с таким названием и путем не существует. Попытка получения и создание осуществляются в одной транзакции, поэтому при успешном выполнении транзакции метод всегда возвращает экземпляр модели, который представляет существующий объект.

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