假设我们有一个应用程序必须将资金从账户 A 转移到账户 B。 让我们假设数据库是 MySQL(尽管我也很欣赏 Postgres 的答案)。
Account A balance: 20
Account B balance: 0
Transaction 1 (TX1): transfer 10 from A to B
Transaction 2 (TX2): transfer 15 from A to B
TX1 和 TX2 同时发生。
现在,这是程序逻辑步骤:
update BALANCES set balance = balance - 10 where id = A
update BALANCES set balance = balance + 10 where id = B
问题:
1。该事务应该使用什么事务隔离级别?
2。事务隔离级别足够还是我必须显式使用悲观锁定?
让我详细说明一下。以 MySQL 中的 SERIALIZABLE 级别为例,当您在事务中读取记录时,记录会被共享锁锁定。 共享锁不会阻止其他事务放置共享锁并读取数据。
实际上,两个事务都获得了 A 和 B 上的共享锁,然后 TX1 尝试进行更新并接收排它锁。它不能这样做,因为 TX2 已经持有共享锁。 所以我们遇到了死锁:其中一个事务将被回滚,我们将不得不挂起一段时间(默认为 30 秒或更多秒)。
据我所知,仅通过可串行化隔离无法实现这样的转账。相反,看起来我们可以使用 READ_COMMITTED,但在读取数据时显式请求独占锁:“select * from BALANCES where id = A FOR UPDATE NOWAIT;”。因此,没有其他事务可以获得记录上的共享锁,从而防止死锁。
请帮助我理解这一点。
你的结论是正确的,你需要显式地加锁来防止出现问题。换句话说,悲观锁定。
您用
FOR UPDATE
显示的语句实际上获取了独占锁,而不是共享锁。 MySQL 语法使用 FOR SHARE
来获取共享锁(这在 MySQL 5.x 中称为 LOCK IN SHARE MODE
)。
事实上,您还应该同时锁定 B 的目标行,以防止事务同时从 B 向 A 转账时出现死锁。
对于此任务来说,锁定比选择事务隔离级别更重要。这就是为什么当你问“我应该使用哪种事务隔离级别?”时没有明确的答案