MySQL-不可重复读

MySQL-不可重复读 MySQL 不可重复读是什么、带来什么业务问题、怎么解决一、先明确概念RR 隔离级别默认存在MySQL InnoDB 默认 RR不可重复读同一个事务内多次读取同一行数据中间被其他事务提交更新 / 删除导致前后读到的数据不一样。 场景简化事务 Aselect money from user where id1;→ 余额 1000事务 B更新该行update user set money200 where id1; commit;事务 A 再次查询同一行 → 余额变成 200 同一事务两次读同一行结果不一致 不可重复读。二、不可重复读会造成哪些真实业务问题1. 统计对账、报表数据错乱最常见财务、订单对账、月度统计需要事务内多次查询汇总中途其他事务修改并提交数据会导致第一次求和总销售额 10 万中间其他事务修改订单金额并提交第二次求和总销售额 8 万 同一事务算出两份不一样的汇总数字对账校验直接失败财务数据失真。2. 基于快照做业务逻辑出现逻辑错误举个扣钱业务事务 A 读取用户余额 1000准备判断是否够扣款事务 B 并发把余额改成 200 并提交事务 A 再次读取余额校验数值突变原本的业务判断失效 如果代码只依赖第一次读取的旧值做运算会出现超扣、金额计算错误。3. 分页、列表循环读取数据前后不一致事务内循环遍历同一批数据中途其他事务更新提交循环中同一记录前后数值不同导出 Excel、批量处理时数据前后矛盾。4. 乐观锁版本号判断失效事务 A 读取 version1准备更新 事务 B 先更新并提交 version2 事务 A 再查 version 变成 2原有更新逻辑判断错乱。三、区分不可重复读 vs 幻读容易混淆不可重复读针对已有行数据被修改 / 删除多次读同一行内容变了幻读针对范围查询其他事务新增 / 删除行第二次查询行数变多 / 变少 两者问题场景不同但都会破坏事务内数据一致性。四、MySQL InnoDB 怎么避免不可重复读方案 1提升隔离级别到 SERIALIZABLE串行化最高隔离级别完全杜绝不可重复读、幻读、脏读。 缺点全表 / 行锁并发性能暴跌线上几乎不用。方案 2读的时候加共享锁行锁锁定读取行使用select ... lock in share mode;事务 A 读取时锁住该行其他事务不能更新 / 删除只能读直到 A 事务提交其他事务才能修改 同一事务多次读取该行数据不会变彻底解决不可重复读。 适用金融、库存、对账强一致性场景。方案 3使用排他锁select ... for update读取同时加写锁其他事务既不能读也不能改适合要马上更新的业务。方案 4MySQL 8.0 可重复读基础上开启快照读无不可重复读误区纠正InnoDB RR 下普通select是快照读MVCC读取事务启动瞬间的历史快照不会出现不可重复读 只有当前读update/delete/select for update/lock in share mode才会读到其他事务已提交的最新数据才会触发不可重复读。关键总结核心考点普通快照读不加锁 selectRR 隔离级别不存在不可重复读当前读加锁查询、更新删除RR 下会出现不可重复读带来对账、金额、统计逻辑错误业务危害核心同一事务内多次读取同一行数据不一致破坏事务内部一致性解决手段加共享锁 / 排他锁锁住行或升级隔离级别五、举一个生产故障实例电商对账任务开启事务先查所有订单总金额再循环逐个订单校验 并发支付事务修改订单支付金额并提交 循环里读取到更新后的金额汇总总额和单条明细对不上对账系统判定账实不符触发告警。 根源就是当前读产生的不可重复读。实际情况就应该是不可重复读吧因为这样才能读到实际正确的数据核心矛盾你站在「实时最新数据」视角数据库隔离级别站在「事务一致性」视角先说结论 不可重复读确实能拿到最新真实数据但它破坏了同一个事务内部逻辑自洽很多业务场景不能接受 “事务读到两半新旧混合的数据”。一、先分清两种需求你只看到了其中一种场景 1你想要实时最新数据 → 不可重复读反而是优点业务需求每次查询都要当前数据库最新状态不在乎事务内数据统一。 例子用户点按钮查自己余额单次查询就结束事务立刻提交后台简单单次查询展示实时库存 这种场景下不可重复读完全没问题甚至更好数据实时。场景 2事务要做多步逻辑计算、校验、汇总 → 不可重复读就是 bug事务是一组不可分割的操作逻辑上要求整个事务执行期间数据状态固定不变所有步骤基于同一个版本的数据计算。 举个财务经典例子你就能直观感受到问题 事务 A 逻辑同一个事务内两条查询第一步查询账户余额 1000第二步计算手续费余额 * 0.1100第三步再次查询余额做最终扣款校验中间事务 B 并发执行把余额更新为 100 并提交。 如果允许不可重复读 第一步读到 1000第三步读到 100。 手续费按 1000 算要扣 100但账户只剩 100校验逻辑直接崩溃出现逻辑矛盾。关键痛点事务内部前后数据版本不一致业务逻辑基于两套数据运算计算结果无意义。二、通俗比喻理解把事务比作一次完整算账可重复读算账全程只看账本快照账本中途别人改了你手里的复印件不变加减汇总全程统一不可重复读你算到一半别人直接涂改账本你后面计算用改过的数字前后数字对不上账算错。数据库定义「不可重复读是问题」不是因为数据 “不真实”而是同一事务内数据不稳定导致多步骤业务逻辑出错。 最新数据是真实但和你事务前面读取的数据不属于同一时间切片混合运算会产生错误。三、MySQL InnoDB 巧妙的折中方案完美兼顾两种需求你疑惑的核心其实 MySQL 早就解决了分两种读模式快照读普通 select不加锁RR 隔离级别下全程读事务启动时的快照天然没有不可重复读保证事务内数据统一适合统计、对账、批量计算。当前读select ... for update /lock in share mode /update/delete强制读取磁盘最新已提交数据会出现不可重复读适合需要立刻拿到最新数据、马上更新的场景。也就是说你想要 “实时最新数据”用当前读主动接受不可重复读你想要 “事务内部数据统一不混乱”普通快照读规避不可重复读两种模式由开发者自主选择不是一刀切禁止不可重复读。四、什么时候我们主动要不可重复读下单扣库存必须读到实时库存不能用旧快照否则超卖支付扣款必须读取最新余额基于最新数值判断单次查询展示实时数据无复杂事务计算这类场景我们主动触发当前读主动利用 “可读到最新数据” 这个特性不可重复读在这里是有用的。五、总结回答你的疑问你的想法没错单纯看单次查询不可重复读能拿到当下真实数据但事务的设计目标是保证一组操作逻辑一致当事务有多步查询、计算、汇总时前后数据不一致会引发业务错误这才是数据库把它归类为 “不一致问题” 的原因MySQL 没有强制消除不可重复读而是区分快照读 / 当前读让开发者根据业务需求自由选择多步骤计算对账 → 快照读规避不可重复读实时扣减、强更新场景 → 当前读接受不可重复读读取最新数据。可见可重复读有的情况下也是有问题的结论是的可重复读RR确实有它自己的缺陷不能万能分两类问题一、业务层面RR 快照读拿不到最新数据会引发业务 bug你刚才疑惑的点RR 的普通 select 是 MVCC 快照只会读取事务启动那一刻的旧快照看不到其他事务已经提交的修改。典型坑场景库存扣减、下单防超卖事务 A 开启快照库存 10 事务 B 同时下单扣到 0 并提交 事务 A 继续用快照里的库存 10 判断以为还有货继续下单 →超卖。 解决这里不能用快照读必须用select ... for update当前读放弃 RR 的快照一致性读最新数据。支付、余额校验查到快照余额充足实际余额已经被其他事务扣光直接造成资金逻辑错误。需要立刻感知外部变更的实时查询后台运营实时看用户余额、商品实时销量快照里是几分钟前的数据页面展示失真。简单说业务需要实时最新状态时RR 快照读就是缺点。二、数据库理论层面RR 没有解决幻读InnoDB RR 只是优化没有彻底消除标准 SQL 隔离级别定义里读已提交 RC存在脏读、不可重复读、幻读可重复读 RR消除脏读、不可重复读但允许幻读串行化 SERIALIZABLE三者全部杜绝幻读带来的问题同一个事务内范围查询两次中间其他事务插入 / 删除数据第二次查到多 / 少了行统计总数前后对不上。 例事务 Aselect * from order where create_time2026-07-13查出 10 条事务 B 插入一条同日订单并提交事务 A 再查快照读依旧 10 条但如果走 update 当前读会更新到这条新增数据 同一事务查询和更新看到的数据集合不一致批量统计、批量更新对账会错乱。三、RR 还有锁层面的隐性问题间隙锁导致并发阻塞、死锁概率上升InnoDB RR 为了抑制幻读范围更新会加临键锁 间隙锁会锁住不存在的区间。 比如更新id 100会锁住整个 id 区间其他事务无法插入 id50 这种不存在的数据并发插入直接阻塞高并发场景吞吐量下降。RC 隔离级别没有间隙锁插入性能更好。长事务持有旧快照占用回滚日志undo log如果一个事务长时间不提交MVCC 需要保留大量 undo 快照磁盘空间暴涨甚至触发数据库清理线程阻塞线上库磁盘打满。四、总结对比帮你理清取舍选 RR默认适合 报表统计、批量导出、对账、多步骤运算要求事务内数据统一不在乎瞬间实时性 代价看不到最新数据、存在幻读风险、间隙锁影响并发、长事务占 undo 日志。选 RC读已提交适合 高并发下单、实时余额查询、大量插入场景 代价同一事务多次查询同一行会出现不可重复读多步骤计算容易账不平。没有完美隔离级别所谓 “问题” 都是业务需求带来的取舍矛盾 想要事务内部数据一致就要牺牲实时性想要实时最新数据就要接受事务内数据前后不一致。脏读、幻读、不可重复读的好处和坏处先给核心结论脏读几乎纯坏处没有任何业务适合主动要脏读幻读有场景能容忍、甚至业务无感但不存在 “主动追求幻读” 的收益和不可重复读完全不一样。分开拆开对比把三者利弊讲清楚一、脏读纯负面没有任何可取之处什么是脏读一个事务读到另一个事务未提交、随时可能回滚的数据。 示例事务 B 更新余额为 0还没 commit事务 A 查询读到余额 0事务 B 发生异常回滚余额恢复 1000 此时 A 拿着一条根本不存在的 “脏数据” 做业务判断直接产生脏数据 bug。为什么完全没有好处脏数据是临时、无效、随时作废的数据哪怕你想要实时数据脏读拿到的也不是真实落地的数据金融、库存、订单任何业务都不能接受基于未提交数据做判断没有任何业务场景会主动开启脏读SQL 标准里最低隔离级别读未提交Read Uncommitted线上几乎没人用。简单说 不可重复读读到的是其他事务已经提交、永久生效的真实数据只是同一事务前后版本冲突 脏读读到的是可能被撤销的临时中间状态数据本身不可信不存在利用价值。二、幻读只有 “能容忍”不存在 “有用、主动想要”幻读定义同一事务内多次范围查询其他事务插入 / 删除数据并提交导致两次查询行数不一致。幻读能不能带来收益完全不能幻读只会带来两类问题统计对账两次统计总数不一样对账失败批量更新查询出来 10 条更新实际悄悄多了 1 条新数据漏处理业务遗漏。但幻读和脏读有一点区别它可以被业务容忍部分简单业务事务很短、没有统计 / 批量更新逻辑哪怕存在幻读也不会出 bug 比如事务只做一次查询、立刻提交不会二次范围统计幻读现象根本不会暴露对业务无影响。 这叫容忍幻读不是幻读本身有好处。举个例子 页面单次查当前用户所有订单查完事务直接结束不再二次查询幻读完全体现不出来业务不受影响。 但你绝不会说 “幻读对我有利”只是它没机会制造故障。三、对比不可重复读天然存在正向使用场景不可重复读 读到其他事务已提交、真实有效的最新数据。 很多业务必须主动利用这个特性扣库存、防超卖必须实时最新库存支付扣款必须读取最新余额运营后台实时刷新数据面板这些场景我们主动用for update当前读主动接受不可重复读它是解决业务问题的工具有明确收益。四、三者极简总结脏读只有坏处无任何业务收益线上禁用幻读只有坏处无收益仅部分短事务场景可以被动容忍不可重复读有坏有好坏处长事务多步统计、对账前后数据不一致好处获取实时已提交最新数据是库存、支付等核心场景刚需。补充一句话区分本质差异脏读读到未提交的虚假临时数据→ 毫无价值幻读范围查询行数变多 / 变少只会破坏批量统计逻辑 → 无价值仅可容忍不可重复读单行数据更新读到真实落地的最新数据→ 业务可主动利用。