想象一下两个线程访问一个维护用户余额的数据库,最初是10个单位。
主题1:撤回5单位货币
1.a read balance from DB
1.b decrement balance in memory
1.c write decremented balance in DB
线程2:存入3个单位的货币
2.a read balance from DB
2.b increment balance in memory
2.c write incremented balance in DB
如果步骤是交错的,并且我们以10的余额开始,那么我们最终可能会遇到丢失的更新问题,如下所示:
Real world seq of events:
1.a read balance from DB (10)
1.b decrement balance in memory (10 - 5 = 5)
2.a read balance from DB (10)
1.c write decremented balance in DB (5)
2.b increment balance in memory (10 + 3 = 13)
2.c write incremented balance in DB (13)
这里我们失去了1.c的更新。 setAutoCommit(false)
和commit
如何解决问题?假设代码是:
setAutoCommit(false)
1.a read balance from DB
1.b decrement balance in memory
1.c write decremented balance in DB
commit(), and if error, rollback()
如果在1.c之前更改了数据库,是否会抛出?我找不到任何解释提交/回滚在错误情况下如何工作的示例。
答案 0 :(得分:2)
有三种可能性可以避免像这样的丢失更新:
悲观锁定:
使用SELECT ... FOR UPDATE
从数据库中读取余额。
这将在读取时锁定行。想要在同一行上工作的并发事务将被锁定,直到第一个事务提交为止。你必须使用显式交易;在JDBC中,您将禁用自动提交。
对于短交易来说,这是一个很好的策略,如果行上的锁定时间没有问题。
使用事务隔离进行乐观锁定:
对要执行此类更新的所有事务使用事务隔离级别REPEATABLE READ
。
然后第二个事务在更新时会出现序列化错误。这不是应该传播给用户的错误,而是应该重试事务的标志。
如果您希望尽可能地保持锁定并且愿意重试接收序列化错误的事务,则这是短事务的好方法。你必须使用显式交易;在JDBC中,您将禁用自动提交。
使用应用程序进行乐观锁定:
您更新如下:
UPDATE account SET balance = <new value>
WHERE id = ... AND balance = <value you originally read>;
然后检查更新是否已修改行(“更新计数”)。如果没有,则余额在此期间已更改,您应该重试该操作。
注意:这仅检查自我们读取行以来是否已修改balance
。如果您希望更新失败,如果任何有关该行的更改,您将扩展WHERE
条件。
如果读取余额和更新之间的时间不短,这是最好的方法,例如如果中间存在用户交互。它不需要使用数据库事务。