同时修改事务内的 NDB 实体未按预期工作

Concurrently modifying NDB entities inside transactions not working as expected

我的理解是 ndb.transactional 用于确保函数正在处理最新数据。我在本地 Google App Engine 开发服务器的交互式控制台中测试了以下代码:

from google.appengine.ext import ndb

class UserModel(ndb.Model):

    level = ndb.IntegerProperty(default=0)

@ndb.transactional(retries=0)
def inc_user_lvl(user_key, recurse=True):
    user = user_key.get()
    print(user.level)
    user.level += 1
    if recurse:
        inc_user_lvl(user_key, recurse=False)
    user.put()

user_key = UserModel().put()
inc_user_lvl(user_key)
user = user_key.get()
print(user.level)

docs say

There is a limit (default 3) to the number of retries attempted; if the transaction still does not succeed, NDB raises TransactionFailedError.

在这种情况下,重试次数为 0,所以我希望用户的级别增加到 1,并提高 TransactionFailedError

相反,函数调用都成功了,并且对 inc_user_lvl 的第二次调用对级别 1 的用户进行了操作(在第一次调用放置实体之前)。两次通话结束后用户等级为2。为什么会这样?

在事务中编写的代码将使用上下文缓存(特定于线程)

来自the docs

Transaction behavior and NDB's caching behavior can combine to confuse you if you don't know what's going on. If you modify an entity inside a transaction but have not yet committed the transaction, then NDB's context cache has the modified value but the underlying datastore still has the unmodified value.

这意味着第二次调用 inc_user_lvl 将从上下文缓存中提取 UserModel 实体,而不是对数据存储执行 ping 操作。您可以通过在 ndb 模型上设置 _use_cache = False 来解决这个问题。例如

class UserModel(ndb.Model):
    _use_cache = False
    level = ndb.IntegerProperty(default=0)

所以现在函数调用后用户级别为 1 但没有引发异常...

事务中的写入不影响后续读取

出于某种原因,ndb 文档中没有提到这一点。您必须查看已取代的版本 (db) docs:

This consistent snapshot view also extends to reads after writes inside transactions. Unlike with most databases, queries and gets inside a Cloud Datastore transaction do not see the results of previous writes inside that transaction. Specifically, if an entity is modified or deleted within a transaction, a query or get returns the original version of the entity as of the beginning of the transaction, or nothing if the entity did not exist then.

这意味着,因为对 inc_user_lvl 的第二次调用是在第一次调用的事务中,所以获取用户实体将 return 用户在事务开始时的状态。

您可以使用 ndb.transactional 中的 kwarg propagation=ndb.TransactionOptions.INDEPENDENT 来启动单独的事务。有关交易选项的完整列表,请参阅 the docs

@ndb.transactional(retries=0, propagation=ndb.TransactionOptions.INDEPENDENT)
def inc_user_lvl(user_key, recurse=True):
    user = user_key.get()
    user.level += 1
    if recurse:
        inc_user_lvl(user_key, recurse=False)
    user.put()

这现在提高了预期 TransactionFailedError