最常见并发问题是UPDATE无WHERE导致覆盖式更新;应始终带主键/唯一索引条件,优先用乐观锁(version+条件更新并检查ROW_COUNT),慎用SELECT...FOR UPDATE,REPLACE/ON DUPLICATE不解决业务逻辑冲突。

UPDATE 语句没加 WHERE 条件导致“覆盖式更新”
这是最常见也最容易被忽略的并发问题:多个事务同时执行 UPDATE table SET status = 'done' 这类无条件更新,后提交的事务会直接覆盖前一个事务的修改,且不报错。MySQL 不会主动检测逻辑冲突,只保证行锁的原子性。
实操建议:
- 所有
UPDATE必须带明确的WHERE条件,最好基于主键或唯一索引,例如WHERE id = 123 - 避免用
WHERE status = 'pending'这类可能匹配多行、且在并发下条件已失效的写法 - 如果业务上确实需要“只更新未处理过的记录”,改用
UPDATE ... SET status = 'done' WHERE id = 123 AND status = 'pending',然后检查ROW_COUNT()是否为 1
乐观锁:用 version 字段 + 条件更新防覆盖
适合读多写少、冲突概率低的场景。核心是把“检查+更新”合并为一条带条件的 UPDATE,由数据库原子执行。
示例表结构:
CREATE TABLE order_info ( id BIGINT PRIMARY KEY, status VARCHAR(20), version INT DEFAULT 0 );
更新逻辑(应用层):
- 先查出当前
id = 1001的记录,拿到version = 5 - 执行:
UPDATE order_info SET status = 'shipped', version = 6 WHERE id = 1001 AND version = 5 - 若
ROW_COUNT() == 0,说明已被其他事务更新,当前操作失败,需重试或提示用户
注意:version 字段必须是整数类型,且每次更新都自增;不能用时间戳替代,因为精度和时钟同步问题会导致误判。
悲观锁:SELECT ... FOR UPDATE 要慎用
SELECT ... FOR UPDATE 在事务中加行级写锁,能阻止其他事务修改同一行,但代价是锁持有时间长、容易引发死锁或锁等待超时。
关键限制:
- 必须在
START TRANSACTION内使用,且锁持续到事务结束(COMMIT或ROLLBACK) - 只对有索引(尤其是主键/唯一索引)的查询生效,否则会升级为表锁
- 不要在长事务或含远程调用的事务里用它——比如查完就去调用微信 API,这期间锁一直占着
- 如果业务允许,优先用乐观锁;只有强一致性要求、且冲突频繁时才考虑悲观锁
典型错误写法:SELECT * FROM user WHERE name = 'alice' FOR UPDATE —— 若 name 无索引,实际锁整张表。
REPLACE INTO / INSERT ... ON DUPLICATE KEY UPDATE 不等于并发安全
这两个语法常被误认为能解决并发更新,其实它们只适用于“存在则更新、不存在则插入”的场景,且依赖唯一键冲突触发。对已存在的行做重复更新时,它们不会判断业务状态是否合理。
举例:
- 订单表有唯一索引
(order_no),两个事务同时执行:INSERT ... ON DUPLICATE KEY UPDATE status = 'cancelled' - 结果是最后提交者胜出,但没人知道谁该真正取消——这属于业务逻辑冲突,不是数据库能自动解决的
- 它们也无法防止“先查再更”里的中间态问题(如 A 查到 status=‘pending’,B 同时改为 ‘processing’,A 仍按 pending 更新)
真正要靠的是显式条件判断(乐观锁)或事务隔离级别配合锁机制(如 SELECT ... FOR UPDATE),而不是依赖冲突触发的语法糖。
并发更新的本质不是“怎么让 SQL 更快”,而是“如何让多个操作在业务意义上互斥”。多数情况下,WHERE ... AND version = ? 加上应用层重试,比锁表、升隔离级别或引入分布式锁更轻量、更可控。容易被忽略的是:版本字段的维护是否严格、重试逻辑有没有幂等保护、以及 ROW_COUNT() 是否真被检查了。










