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
事务要便宜得多。除此之外,如果您已经尝试了所有隔离级别,那么您的设置中还有另一个问题,我无法确定。(但您是否检查了数据库日志中的错误消息?)