两个并发事务对同一行数据执行"修改-再查询"操作。
在关系型数据库中,结果取决于事务隔离级别。
以 MySQL InnoDB(默认 Repeatable Read)为例,假设表 A 有一行 id=1, value=100。
时间线模拟
| 时间 | 事务1(用户A) | 事务2(用户B) |
|---|---|---|
| T1 | BEGIN; | |
| T2 | BEGIN; | |
| T3 | SELECT * FROM A WHERE id=1; → 看到 value=100 | |
| T4 | SELECT * FROM A WHERE id=1; → 看到 value=100 | |
| T5 | UPDATE A SET value=200 WHERE id=1; | |
| T6 | UPDATE A SET value=300 WHERE id=1; (会被阻塞,等待事务1提交或回滚) | |
| T7 | SELECT * FROM A WHERE id=1; → 看到 value=200 | (仍在等待锁) |
| T8 | COMMIT; | (事务2获取锁,继续执行) |
| T9 | UPDATE 实际执行(覆盖为300) | |
| T10 | SELECT * FROM A WHERE id=1; → 看到 value=300 | |
| T11 | COMMIT; |
最终结果
-
数据库中最终值 = 300(后提交的事务覆盖先提交的)
-
用户A 在他自己的事务内,修改后查询到 200
-
用户B 在他自己的事务内,修改后查询到 300
不同隔离级别下的差异
| 隔离级别 | 事务2更新时是否阻塞 | 事务1提交后,事务2再次查询能否看到事务1的修改? | 可能问题 |
|---|---|---|---|
| 读未提交(Read Uncommitted) | 不阻塞(可能脏写) | 能,但会读到未提交数据 | 脏读、脏写 |
| 读已提交(Read Committed) | 阻塞(行锁) | 能,事务2更新前会读到已提交的最新值(但它的查询在T10看到300,因为已提交) | 不可重复读 |
| 可重复读(Repeatable Read,MySQL默认) | 阻塞 | 不能,事务2内多次查询结果一致(快照读) | 幻读(部分情况) |
| 可串行化(Serializable) | 阻塞 + 间隙锁 | 不能,且性能最差 | 无并发问题 |
关键结论
-
不会出现"丢失更新"(因为行锁阻塞了并发更新)。
-
用户各自看到自己的修改结果,符合预期。
-
最终数据以后提交的事务为准(丢失了用户A的修改),除非应用层实现乐观锁(如版本号)。
-
在 Repeatable Read 下,事务2即使在T9之后重新查询整行,看到的仍是它修改后的300,不会看到200,保证了可重复读。
实际开发建议
-
这种场景下,如果不允许覆盖(比如余额扣减),应该使用:
sqlUPDATE table SET value = new_value WHERE id=1 AND value = old_value; 或加版本号: UPDATE table SET value = new_value, version = version+1 WHERE id=1 AND version = old_version; -
如果必须保证业务顺序,可以考虑 SELECT ... FOR UPDATE(当前读,加行锁)在查询时就锁定。