mysql高并发下读旧数据或写丢更新的根本原因是事务并发破坏隔离性,核心机制是mvcc加锁协同,但需正确使用索引、显式加锁及全链路一致性设计。

为什么高并发下 MySQL 会读到旧数据或写丢更新
根本原因不是 MySQL 本身“坏了”,而是多个事务在没有协调的情况下,同时访问同一行数据,破坏了 ACID 中的 Isolation(隔离性)。比如:事务 A 正在扣减库存,事务 B 同时读取该库存值并下单——如果没加锁或隔离级别太低,B 读到的就是 A 修改前的旧值,后续再扣一次,就超卖了。
- 典型现象:
脏读(读到未提交的回滚数据)、不可重复读(同个事务两次 SELECT 结果不同)、幻读(SELECT 出新插入的行) - 关键诱因:默认隔离级别
REPEATABLE READ能防不可重复读,但不防幻读;而READ COMMITTED下,两次 SELECT 可能拿到不同快照,业务逻辑容易出错 - 硬件/配置放大问题:主从延迟大时,读从库看到的是滞后数据;binlog 异步写入 + crash 后恢复不完整,也可能导致主从不一致
MySQL 并发控制的核心机制是什么
MySQL(InnoDB)靠 **MVCC + 锁** 协同实现并发安全。MVCC 负责“读不阻塞写”,锁负责“写阻塞写”。但这两者不是万能的,用错场景照样翻车。
-
SELECT ... FOR UPDATE和SELECT ... LOCK IN SHARE MODE是显式加行锁的唯一可靠方式;普通SELECT永远不加锁,哪怕在事务里 - 行锁生效的前提是走索引;如果
WHERE条件没命中索引,InnoDB 会升级为表锁,并发能力断崖下跌 - MVCC 的版本链依赖
undo log,长事务不提交会拖慢 purge 线程,导致历史版本堆积、CPU 升高、甚至锁等待雪崩
先删 Redis 再更新 MySQL 为什么还会不一致
这不是“操作顺序对了就没问题”,而是并发时序天然不可控。哪怕你严格按“删缓存 → 更新 DB → 再删缓存”走,仍可能被读请求插队写入旧值。
- 典型竞态:A 删缓存 → B 查缓存为空 → B 读 DB 得到旧值 → B 写回 Redis → A 更新 DB 成功 → 最终 Redis 是旧值,DB 是新值
- 延时双删的
Thread.sleep(500)不是银弹:它假设所有读请求耗时 ≤500ms,但网络抖动、慢 SQL、GC 暂停都可能打破这个假设 - 更隐蔽的坑:MySQL 主从同步延迟期间,B 从从库读到旧值,写入 Redis,此时即使 A 更新主库成功,缓存也已污染
真正能落地的数据一致性策略有哪些
没有“一招鲜”,只有根据业务容忍度组合使用。强一致场景必须接受性能折损;高吞吐场景得靠最终一致+兜底校验。
- 写路径强制串行化:
UPDATE stock SET count = count - 1 WHERE id = 123 AND count >= 1—— 用 WHERE 做原子校验,失败直接报错,不靠应用层重试逻辑 - 缓存更新改用「失效+懒加载」:
DEL key后不做任何写缓存动作,等下次读请求触发回源 + 写入,避免双写失败风险 - 异步补偿兜底:更新 DB 后发 MQ 消息,消费者负责刷新缓存;失败则进死信队列,人工介入或定时任务扫描 DB 与 Redis 的 diff
- 关键业务加分布式锁:
SET key "lock" NX PX 10000,确保同一商品 ID 的更新操作绝对串行,代价是锁竞争和超时管理
最常被忽略的一点:**一致性不是单点问题,是读写链路全链路的责任**。从客户端连接池配置(是否自动重连)、到中间件分库分表路由(是否把同一用户请求固定到同一分片)、再到监控告警(能否快速发现缓存命中率骤降),每层都可能成为断裂点。











