我注意到我的应用程序经常将值写入依赖于以前的读取操作的数据库。一个常见的例子是用户可以存钱的银行账户:
void deposit(amount) {
balance = getAccountBalance()
setAccountBalance(balance + amount)
}
如果两个线程/客户端/ATM同时调用此方法,我想避免竞争条件,这样帐户所有者会赔钱:
balance = getAccountBalance() |
| balance = getAccountBalance()
setAccountBalance(balance + amount) |
| // balance2 = getAccountBalance() // theoretical
| setAccountBalance(balance + amount)
V
我经常读到Repeatable Read或Serializable可以解决这个问题。甚至德国维基百科关于丢失更新的文章也指出了这一点。翻译成英文:
隔离级别 RR(可重复读取)经常被提及作为丢失更新问题的解决方案。
这个 SO answer建议Serializable在 SELECT 之后使用 INSERT 解决类似问题。
据我了解这个想法 - 在右侧的过程尝试设置帐户余额时,(理论上)读取操作将不再返回相同的余额。因此不允许写操作。是的 - 如果您阅读了这个流行的 SO 答案,它实际上听起来非常合适:
在 REPEATABLE READ 下,第二个 SELECT 保证至少显示从第一个 SELECT 返回的行不变。在那一分钟内,并发事务可能会添加新行,但不能删除或更改现有行。
但后来我想知道“它们不能被删除或改变”实际上是什么意思。如果您尝试删除/更改它会发生什么?你会收到错误吗?还是您的事务会等到第一个事务完成并最终执行其更新?这一切都不同了。在第二种情况下,您仍然会赔钱。
如果您阅读下面的评论,情况会变得更糟,因为还有其他方法可以满足可重复读取条件。例如快照技术:可以在左侧事务写入其值之前拍摄快照,如果稍后在右侧事务中发生第二次读取,这允许提供原始值。例如,参见MySQL 手册:
同一事务内的一致性读取读取第一次读取建立的快照
我得出的结论是,限制事务隔离级别可能是摆脱竞争条件的错误工具。如果它解决了问题(对于特定的 DBMS),那不是由于可重复读取的定义。而是因为满足可重复读取条件的特定实现。例如锁的使用。
所以,对我来说,它看起来像这样:你真正需要解决这个问题的是一个锁定机制。一些 DBMS 使用锁来实现可重复读取的事实被利用了。
这个假设正确吗?还是我对事务隔离级别的理解有误?
你可能会生气,因为这肯定是关于该主题的第 100 万个问题。问题是:示例银行账户场景是绝对关键的。就在那里,应该绝对清楚发生了什么,在我看来,好像有太多误导和矛盾的信息和误解。