单条带条件的update语句+影响行数校验是最轻量可靠的防覆盖方案;select for update仅在需读取旧值计算时才用,且易引发死锁;乐观锁需应用层完整控制version字段;唯一约束是终极兜底。

直接结论:单条带条件的UPDATE语句 + 影响行数校验,是避免覆盖最轻量、最可靠的方式;SELECT FOR UPDATE仅在必须读取旧值再计算时才需引入,且极易踩坑。
UPDATE语句本身不防覆盖,除非WHERE条件包含业务约束
执行UPDATE accounts SET balance = 150 WHERE id = 123这种无条件赋值,不管当前值是多少,都会强行覆盖——两个并发事务都这么写,后提交者必然覆盖前者的修改。
真正起作用的是把判断逻辑塞进WHERE里:
-
UPDATE accounts SET balance = balance + 50 WHERE id = 123 AND balance >= 50:余额不足则整条语句不生效,影响行数为0 - 必须确保
id或balance字段有索引,否则可能升级为表锁或全表扫描 - MySQL中该语句在
READ COMMITTED和REPEATABLE READ下都安全;SQL Server同理,但需注意@@ROWCOUNT必须紧跟UPDATE后检查
SELECT FOR UPDATE不是万能钥匙,用错反而更危险
很多人以为“加了FOR UPDATE就万事大吉”,结果线上频繁死锁或卡顿。根本问题在于它只解决“读-改”中间的窗口期,却放大了锁持有时间。
典型错误场景:
- 在事务开头
SELECT * FROM orders WHERE user_id = 123 FOR UPDATE,接着调用外部HTTP接口耗时2秒,再UPDATE——这2秒内所有想操作该用户的请求全被阻塞 -
WHERE user_id = ?没走索引 → MySQL锁整张表,吞吐量断崖下跌 - SQL Server里写成
SELECT ... WITH (UPDLOCK)但漏掉BEGIN TRANSACTION→ 锁语句一结束就释放,形同虚设
乐观锁(version字段)必须由应用层完整控制
触发器或存储过程无法替代WHERE version = ?这个显式条件。数据库不会自动帮你比对版本,也不会拦截漏传version的UPDATE。
正确链条缺一不可:
- 应用层读取当前
id=123的version=5,连同新数据一起发给DB - 执行
UPDATE products SET stock = 99, version = 6 WHERE id = 123 AND version = 5 - 立即检查
@@ROWCOUNT(SQL Server)或mysql_affected_rows()(MySQL),为0即冲突 -
version字段不能设DEFAULT或ON UPDATE,索引需包含(id, version)
最容易被忽略的点:唯一约束才是终极兜底
所有上述方案都依赖开发者写对SQL。但只要业务上存在“同一资源只能被一个用户占用”的语义,就必须加唯一索引。
例如抢座场景,place字段必须建UNIQUE索引。即使UPDATE漏了AND user_id IS NULL,后续插入重复place也会报Duplicate entry错误,而不是静默覆盖。
这个约束不解决并发逻辑,但它让错误可发现、可监控——比任何隔离级别或锁机制都更接近业务本质。











