应使用SELECT ... FOR UPDATE加行级写锁防止并发更新丢失,确保WHERE条件命中索引以避免表锁,并配合INSERT ... ON DUPLICATE KEY UPDATE处理“查再插”竞态;READ COMMITTED下需警惕幻读,跨服务场景须依赖应用层幂等或分布式事务。

用 SELECT ... FOR UPDATE 锁住要更新的行
在事务中修改某条记录前,必须先加行级写锁,否则并发读-改-写会导致覆盖或丢失更新。比如两个事务同时读取余额为 100 的账户,各自加 50 后写回,最终变成 150 而不是预期的 200。
正确做法是在 BEGIN 后立即执行带锁查询:
START TRANSACTION; SELECT balance FROM accounts WHERE id = 123 FOR UPDATE; -- 此时其他事务对 id=123 的行执行 SELECT ... FOR UPDATE 或 UPDATE 会被阻塞 UPDATE accounts SET balance = balance + 50 WHERE id = 123; COMMIT;
注意:FOR UPDATE 只在可重复读(REPEATABLE READ)隔离级别下才真正锁定索引覆盖的行;如果 WHERE 条件没走索引,会升级为表锁。
避免长事务 + 显式控制锁范围
锁持有时间越长,阻塞越多,死锁概率越高。常见错误是把耗时操作(如 HTTP 调用、文件读写)放进事务里。
- 只把真正需要原子性的 DB 操作包进事务
- 确保
WHERE条件命中主键或唯一索引,避免锁住不相关行 - 批量更新时,分页或按主键范围拆成多个小事务,不要一次
UPDATE ... LIMIT 10000 - 确认 autocommit 关闭(
SET autocommit = 0),否则每条语句自动提交,FOR UPDATE失效
用 INSERT ... ON DUPLICATE KEY UPDATE 替代“查再插”
“先查是否存在,不存在则插入”这种逻辑在并发下必然产生竞态:两个事务都查到不存在,然后都插入,触发唯一键冲突或重复数据。
直接用原子语句替代:
INSERT INTO user_points (user_id, points) VALUES (123, 10) ON DUPLICATE KEY UPDATE points = points + 10;
前提是 user_id 有唯一约束(主键或 UNIQUE 索引)。该语句内部由 MySQL 自动处理冲突,不会报错也不会丢更新。
注意:如果业务需要区分“本次是插入还是更新”,可通过 ROW_COUNT() 判断影响行数,但不能依赖 LAST_INSERT_ID() —— 它在 ON DUPLICATE KEY UPDATE 场景下行为不可靠。
读已提交(READ COMMITTED)下慎用非锁定读
默认的 REPEATABLE READ 隔离级别能防止不可重复读,但代价是间隙锁(Gap Lock)更重;而 READ COMMITTED 下每次 SELECT 都读最新已提交版本,不加间隙锁,看似轻量,却容易引发幻读问题——尤其在“检查约束+插入”类逻辑中。
例如判断订单号未被使用,然后插入新订单:
- 事务 A 查
SELECT COUNT(*) FROM orders WHERE order_no = 'NO2024001'→ 0 - 事务 B 插入
'NO2024001'并提交 - 事务 A 继续插入 → 唯一键冲突
这种场景不能靠隔离级别解决,必须用 SELECT ... FOR UPDATE 或唯一索引+INSERT ... ON DUPLICATE KEY UPDATE 这类原子机制兜底。
真正难处理的不是单条 SQL 的一致性,而是跨表、跨服务、含外部依赖的业务逻辑。MySQL 的锁和事务只能保它自己那块数据,一旦涉及 Redis 缓存更新、MQ 发消息、第三方支付回调,就得靠应用层补偿、幂等设计或分布式事务框架来兜住——这些已经超出 MySQL 本身能力范围了。










