真正能防覆盖的只有三类写法:带条件的原子update、version字段乐观锁、或select...for update;其余如单纯select+update不加锁不校验必然丢数据。

直接说结论:靠 SELECT + UPDATE 两步走,不加锁、不校验,100% 会丢数据;真正能防覆盖的只有三类写法——带条件的原子 UPDATE、version 字段乐观锁、或 SELECT ... FOR UPDATE(但极易踩坑)。
UPDATE WHERE 条件必须精确且可索引
这是最轻量也最容易落地的方案,前提是业务逻辑允许“条件成立才更新”。比如抢座、扣库存、状态流转。
- 错误写法:
UPDATE ticket SET user_id = ? WHERE place = ?—— 没校验当前是否空闲,后写直接覆盖前写 - 正确写法:
UPDATE ticket SET user_id = ?, updated_at = NOW() WHERE place = ? AND user_id IS NULL - WHERE 中的字段(如
place)必须有唯一索引或联合索引支撑,否则可能锁表或性能暴跌 - MySQL 返回
mysql_affected_rows()为 0 时,说明条件不匹配(已被占),不是“执行失败”,而是“业务拒绝”,必须由上层处理(重试/提示/降级)
乐观锁 version 字段必须三要素齐备
漏掉任一环,version 就等于没写。它不是数据库功能,是应用层手动拼出来的防护逻辑。
- SQL 必须同时满足:
WHERE id = ? AND version = ?+SET version = version + 1+ 执行后检查影响行数 - 常见翻车点:
MyBatis手写 XML 时没拼AND version = #{version};JPA用short类型导致溢出变负数,WHERE version = -1意外命中所有旧记录 -
SELECT时必须把version字段查出来,不能SELECT *却忽略映射,否则WHERE version = ?填的是null或默认值 0 - 重试不是拿旧
version循环尝试,而是重新SELECT拿最新version,再合并变更构造新 SQL
SELECT FOR UPDATE 只在严格条件下可用
它不是“加个 FOR UPDATE 就安全了”,而是一整套执行约束:事务包裹、索引命中、无 DDL、顺序一致。
- 必须在
BEGIN和COMMIT之间使用,且查询条件必须命中唯一索引或主键;否则 MySQL 可能升为表锁 - 禁止在
FOR UPDATE后加LIMIT 1期望“只锁一行”——InnoDB 的 next-key lock 会锁住间隙,尤其当 ID 不连续时 - 事务中不能混入
ALTER TABLE或写非事务表(如 MyISAM),否则隐式提交,锁立刻释放 - 批量操作时,
IN (105, 102, 108)必须提前排序为IN (102, 105, 108),否则 A 事务锁 105→102、B 锁 102→105,死锁就来了
真正容易被忽略的,不是该选哪种方案,而是每种方案里那些“看起来像对了,其实已经失效”的细节:比如 UPDATE 漏了 WHERE version = ?,或者 SELECT FOR UPDATE 查询走了全表扫描,又或者重试时没刷新 version 值——这些地方一错,整个并发控制就塌了半边。











