Python SQLAlchemy with_for_update读取陈旧数据

Python SQLAlchemy with_for_update读取陈旧数据,python,sql,postgresql,sqlalchemy,Python,Sql,Postgresql,Sqlalchemy,我正在编写一个函数,负责更新帐户余额。为了防止并发更新,我首先使用with_for_update()锁定帐户,计算金额,更新余额,然后提交会话。为了模拟并发请求,我生成了两个进程,并在每个进程中运行该函数一次。以下是计算和更新余额的代码: session = create_db_session(db_engine)() session.connection(execution_options={'isolation_level': 'SERIALIZABLE'}) print("&a

我正在编写一个函数,负责更新帐户余额。为了防止并发更新,我首先使用
with_for_update()
锁定帐户,计算金额,更新余额,然后提交会话。为了模拟并发请求,我生成了两个进程,并在每个进程中运行该函数一次。以下是计算和更新余额的代码:

session = create_db_session(db_engine)()
session.connection(execution_options={'isolation_level': 'SERIALIZABLE'})

print("&" * 80)
print(f"{process_number} entering!")
print("&" * 80)

accounts = (
    session.query(Account)
    .filter(Account.id == [some account IDs])
    .with_for_update()
    .populate_existing()
    .all()
)

print("*" * 80)
print(f"{process_number} got here!")
for account in accounts:
    print(
        f"Account version: {account.version}. Name: {account.name}. Balance: {account.balance}"
    )
    print(hex(id(session)))
    print("*" * 80)

# Calculate the total amount outstanding by account.
for account in accounts:
    total_amount = _calculate_total_amount()
    if account.balance >= total_amount:
        # For accounts with sufficient balance, deduct the amount from the balance.
        account.balance -= total_amount
    else:
        # Otherwise, save them for notification. Code omitted.

print("-" * 80)
print(f"{process_number} committing!")
for li, account in line_items_accounts:
    print(
        f"Account version: {account.version}. Name: {account.name}. Balance: {account.balance}"
    )
    print("-" * 80)
session.commit()
以下是输出:

&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
0 entering!
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
1 entering!
&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
********************************************************************************
0 got here!
Account version: 1. Name: Phi's Account. Balance: 20000.000000
0x7fcb65d7e0d0
********************************************************************************
--------------------------------------------------------------------------------
0 committing!
Account version: 1. Name: Phi's Account. Balance: 19930.010000
--------------------------------------------------------------------------------
********************************************************************************
1 got here!
Account version: 1. Name: Phi's Account. Balance: 20000.000000
0x7fcb65f930a0
********************************************************************************
--------------------------------------------------------------------------------
1 committing!
Account version: 1. Name: Phi's Account. Balance: 19930.010000
--------------------------------------------------------------------------------
0和1是进程号,十六进制数是会话的id。您可以看到锁起作用了(进程0阻止了1,直到0提交),但1读取了过期数据:余额应该是
19930.01
,而不是
20000
,并且在进程1的输出中,“帐户版本”应该是2,而不是1

我尝试过使用
populate_existing()
,但运气不佳,尽管我怀疑这不会有任何帮助,因为这两个会话是不同的,进程1的会话在进程0释放锁之前不应该填充任何内容。我还尝试了“可重复读取”和“可序列化”隔离级别,并期望在进程1中引发异常,因为事务之间存在并发更新/读/写依赖关系,但什么也没有发生

值得注意的是,这种行为并不一致。当我在本地运行上面的代码块时,一切正常,但当我构建一个包含所有代码的Docker容器并在那里运行它时,几乎不起作用。软件包版本没有区别。我用的是Postgres和psycopg2


我现在正拼命想弄清楚到底发生了什么事。我觉得也许我忽略了一些简单的事情。有什么想法吗?

更新就行了

FOR UPDATE
导致
SELECT
语句检索到的行 已锁定,好像要更新。这样可以防止它们被锁定, 被其他交易记录修改或删除,直到当前 交易结束。也就是说,尝试更新的其他事务,
删除
选择更新
选择无密钥更新
选择共享
选择密钥共享
这些行将被阻止 直到当前交易结束

我的

这正是SQLAlchemy的
和\u for\u update()的作用

在没有参数的情况下调用时,生成的
SELECT
语句将附加一个
FOR UPDATE
子句

但是,在像您一样使用
可序列化的
快照隔离进行操作时,这是多余的工作

此级别模拟所有已提交事务的串行事务执行;好像事务是一个接一个地连续执行的,而不是并发执行的

因此,您的代码在竞争条件下是安全的,冗余的使用
更新
(推荐!),使用
可序列化事务。后者通常要贵得多。您需要为序列化失败做好准备(而不是在显示的代码中)

。。。与可重复读取级别一样,使用此级别的应用程序 由于序列化失败,必须准备重试事务

房间里的大象:你真的给数据库写过信吗?
会话。提交()
可能在过早打印“任务完成”后失败


检查数据库日志中的序列化失败或任何其他异常。如果(毫不奇怪)发现序列化失败,简单的解决方案是切换到(默认!)
readcommitted
隔离级别。您的手动锁定已经完成了这项工作。

这可能是因为您没有启动或刷新会话吗?@YaakovBressler我在块的末尾有session.commit(),所以我怀疑是这样吗?看来您遇到这个问题是因为您的进程共享同一线程和同一会话池。但是我不完全确定。。。在我的专业知识之外…我不认为他们共享同一个线程?这是两个过程。如果两个进程共享同一个线程,那么Python就有严重的错误。您能否澄清一下“会话池”是什么意思?像连接池一样?在最初的问题中添加了一条注释-我尝试了所有三个事务隔离级别,但都不起作用。另外,是否真的值得努力进行手动锁定,而不是使用更严格的隔离级别?对于手动锁定,我必须担心的一件事是死锁预防,这对于大型应用程序来说可能非常困难。我考虑在任何地方都使用SERIALIZABLE并在失败时重试,而不必担心抓取锁。@ljiatu:好吧,你已经有了手动锁,可以用
进行更新了。是的,这通常比
SERIALIZABLE
事务要便宜得多。除此之外,如果您已经尝试了所有隔离级别,那么您的设置中还有另一个问题,我无法确定。(但您是否检查了数据库日志中的错误消息?)