能避免覆盖,关键在于用带预期状态的WHERE条件(如status='pending')执行UPDATE,并检查affected_rows是否为0来判断是否被并发修改;原子性仅保证语句执行不中断,不解决业务层竞态。

UPDATE 语句加 WHERE 条件判断是否真能避免覆盖?
不能直接依赖 UPDATE 自身的原子性来解决并发更新冲突。MySQL 的 UPDATE 确实是原子操作,但“原子”只保证单条语句执行不中断,不保证业务逻辑层面的竞态被消除——比如两个线程都读到旧值 status = 'pending',又都执行 UPDATE ... SET status = 'processing',结果仍是覆盖而非拒绝。
真正起作用的是显式条件判断,即把“预期状态”作为 WHERE 子句的一部分:
UPDATE orders SET status = 'processing', updated_at = NOW() WHERE id = 123 AND status = 'pending';
这时返回的 affected_rows 就成了关键信号:如果为 0,说明条件不满足(可能已被其他线程改过),当前更新失败,需重试或报错。
-
affected_rows == 0是正常现象,不是错误,必须在应用层检查并响应 - 不要用
SELECT + UPDATE组合替代带条件的UPDATE,这会引入 TOCTOU(time-of-check to time-of-use)漏洞 - InnoDB 行锁会在
WHERE匹配到的行上加锁,但仅限于索引列;若WHERE用的是非索引字段,可能升级为表锁或锁住大量无关行
为什么不用 SELECT FOR UPDATE?
它确实能加排他锁,防止其他事务读写同一行,但代价高、易阻塞、还容易死锁——尤其当多个线程按不同顺序访问多行时。
CAS 思路的核心是“乐观”:先假设冲突少,失败再处理,而不是“悲观”地提前锁住资源。对高并发低冲突场景,SELECT FOR UPDATE 反而降低吞吐、拖慢响应。
- 除非你明确需要“读取后立即锁定+后续多步修改”,否则别用
SELECT ... FOR UPDATE - 即使用了,也得配合
WAIT N或超时机制,避免无限等待 - 注意隔离级别:
READ COMMITTED下,SELECT FOR UPDATE只锁匹配到的行;REPEATABLE READ下可能锁住间隙(gap lock),扩大锁定范围
如何设计可重试的 CAS 更新逻辑?
数据库层只负责“一次尝试”,应用层要承担重试责任。典型模式是“循环 + 有限次数 + 指数退避”。关键不是重试多少次,而是每次重试前是否重新读取最新状态。
例如更新库存:
UPDATE products SET stock = stock - 1 WHERE id = 456 AND stock >= 1;
如果 affected_rows == 0,说明库存不足或已被扣减,此时应查最新 stock 值,再决定是拒绝下单,还是等库存恢复后重试。
- 重试前必须重新
SELECT当前行,不能复用旧值做下一轮 CAS 判断 - 建议限制最大重试次数(如 3–5 次),避免雪崩式重试压垮 DB
- 对用户可见的操作(如支付扣款),重试应有明确反馈:“正在重试,请勿重复提交”,而非静默卡住
MySQL 的 version 字段真的比状态字段更可靠?
不一定。version 字段本质也是 CAS 的一种实现,但它把“变化预期”从业务状态解耦出来,更适合频繁变更、状态语义复杂的场景。但要注意:它没解决根本问题,只是把冲突检测抽象成数字比较。
例如:
UPDATE users SET name = 'Alice', version = version + 1 WHERE id = 789 AND version = 5;
如果 affected_rows == 0,说明 version 已变,需重新读取再试。
- version 必须是
NOT NULL、有默认值(如 0),且每次更新都自增,不能跳变 - 不要用
TIMESTAMP或DATETIME模拟 version,精度和时钟同步问题会导致误判 - 复合条件(如
status = 'active' AND version = 3)能兼顾业务语义与版本控制,但会让 WHERE 更复杂,影响索引效率
最易被忽略的一点:CAS 不是银弹。当更新涉及多个表、或需要强一致性跨服务操作时,单纯靠 MySQL 的 WHERE 条件无法兜底,得上分布式锁或 Saga 模式。别在单机数据库里硬扛分布式问题。











