【MySQL百日打怪升级第19天】脏读、不可重复读、幻读 —— 遇到了怎么定位,怎么修?

简介: 本系列第19天聚焦MySQL事务隔离问题实战诊断:详解脏读、不可重复读、幻读的典型症状、精准定位方法(查隔离级别+观察读现象)及修复方案(调级别/加锁/重构),附AI生成并发复现脚本技巧与面试高频考点,助你从理论走向线上排障。

【第19天】每天一个MySQL知识点,百日打怪升级

脏读、不可重复读、幻读 —— 遇到了怎么定位,怎么修?


大家好,我是一名拥有10年以上经验的DBA老兵。

做这个系列,源于一个朴素的愿望:把踩过的坑、总结的经验系统化输出,希望能帮到刚入行或想进阶的兄弟们。

让我们开始今天的第19天内容。


昨天聊了隔离级别,也讲了三大问题的定义。今天换个角度——线上真的遇到这些问题了,你该怎么判断是哪一种?怎么修?

说白了,面试考你定义,但工作考你"遇到了能不能认出来"。


三个问题,三组"症状"

每个问题都有自己的"指纹"。你看到的异常现象不一样,对应的诊断方向也不一样。

症状一:你读到了本不该存在的数据

典型表现:一条SELECT返回了看起来"不可能"的数据。比如用户刚注册就有一条购买记录,但那个购买记录后来消失了。

诊断:看看那条"幽灵数据"是不是来自另一个尚未提交的事务。

大概率 → 脏读

怎么确认?查一下你数据库的隔离级别是不是被人改成了 READ UNCOMMITTED:

SELECT @@transaction_isolation;

如果是 READ-UNCOMMITTED,恭喜你找到原因了。

修复:改成 READ COMMITTED。

SET GLOBAL transaction_isolation = 'READ-COMMITTED';
-- 注意:已有连接要重连才生效

不过说实话,线上碰到脏读的概率已经很低了。因为RU基本没人用。我在生产环境干了十几年,只遇到过一次——某监控系统的开发图省事把全局隔离级别改成了RU,结果影响了所有连到这个实例的业务。改回来后,世界清净了。

症状二:同一事务里,同一行数据不一样

典型表现:你的代码逻辑是——先查A,用A的值做计算,再查A做核验——结果对不上。

# 伪代码:业务里可能写成这样
def process_order(order_id):
    with transaction():
        status = db.query("SELECT status FROM orders WHERE id = ?", order_id)
        # status = 'pending'
        if status == 'pending':
            do_something()   # 基于 status=pending 做了一些操作
        # 此时另一个事务把 status 改成了 'paid' 并提交了
        status_again = db.query("SELECT status FROM orders WHERE id = ?", order_id)
        # status_again = 'paid'  ← 同一个事务里,同一行数据变了!

大概率 → 不可重复读

怎么确认

SELECT @@transaction_isolation;

如果结果是 READ-COMMITTED,那就对上了。

修复方案(按推荐优先级排序):

方案 操作 代价
方案A:改成RR SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 快照读,看不到别人新提交的数据
方案B:加锁 SELECT ... FOR UPDATE 行锁,并发下降
方案C:一次读完 把两次查询合并成一次,或第一条SELECT用FOR UPDATE 改代码,稍微麻烦

症状三:同一条件,两次COUNT不一样

典型表现:你的分页查询,第一页返回了10条,第二页却看到了第一页的数据。或者统计报表里,SUM/COUNT结果不符合预期。

# 伪代码
def generate_report():
    with transaction():
        total = db.query("SELECT COUNT(*) FROM orders WHERE date = ?", today)
        # total = 100
        # 此时另一个事务新插入了2条今天的订单并提交
        total_again = db.query("SELECT COUNT(*) FROM orders WHERE date = ?", today)
        # total_again = 102  ← 行数变了

大概率 → 幻读

不过要注意:MySQL的RR级别下,普通SELECT不会出现幻读(有MVCC快照保护)。所以如果你用的是RR还遇到这个问题,大概率是你代码里混用了当前读(FOR UPDATE)和快照读。

修复:如果业务可以接受,统一用RR + 快照读。如果有DML操作,记得在事务中对目标数据集先执行 SELECT ... FOR UPDATE 用Gap Lock占住间隙,再基于这个结果做后续DML,才能防止其他事务插入新行。注意 FOR UPDATE 必须在任何快照读之前执行,否则为时已晚——你看到的仍然是快照读的旧数据。


说到这个想起之前一个case。有个哥们跑来跟我说:"报告跑出来的数据和人工核对的不一致,差了3行。"查了半天,发现是他的报表事务里先 SELECT COUNT(*)(快照读)得到100,然后 SELECT ... FOR UPDATE(当前读)又查了一次——这次看到了别人刚提交的3行,变成了103。这不是Gap Lock的锅,而是快照读和当前读混用的结果。 第二次的FOR UPDATE确实加了Next-Key Lock,但它加锁的时候新行已经存在了,锁并不能把已存在的行变回去。这种问题真不好排查,因为你不容易想到"自己的读操作之间还在互相打架"。


故障诊断速查表

你看到的症状 排查方向 大概率是
读到不可能存在的数据 查隔离级别是不是RU 脏读
同一行两次读不一样 查隔离级别是不是RC 不可重复读
同一条件行数不对 查代码里有没有混用当前读和快照读 幻读
DELETE删了比预期多的行 查事务内是否先用快照读估算行数,再直接DML(当前读)导致多删 RR下的边缘情况
什么都不确定 先查 SELECT @@transaction_isolation

🤖 AI实战工具箱:让AI帮你造并发测试

隔离级别的问题最蛋疼的是什么?不好复现。

你在单机单连接上跑一万次都跑不出来,因为需要两个事务在毫秒级的窗口里交错执行。但AI可以替你生成一套"精准卡点"的并发脚本——开两个session,用sleep控制时序,把概率性问题变成确定性复现。

复现不可重复读

直接把下面这段粘给AI(比如Claude、GPT):

帮我写一组MySQL测试SQL,演示RC级别下的不可重复读。需要两个终端的完整操作步骤,事务A先SELECT余额,事务B UPDATE并提交,事务A再SELECT看到不同值。用sleep控制执行时序,每条SQL前用注释标注在哪端执行。包含验证步骤:改成RR级别后再跑一次,确认两次SELECT结果一致。

AI会输出类似这样的脚本:

-- 准备工作(任何一端执行一次)
CREATE TABLE IF NOT EXISTS account (
  id INT PRIMARY KEY,
  balance INT
);
INSERT INTO account VALUES (1, 100);

-- ===== 终端A:事务A =====
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN;
SELECT balance FROM account WHERE id = 1;
-- 结果: 100

-- SLEEP 给时间切换到终端B执行
SELECT SLEEP(5);

-- 切换到终端B执行完再回来
SELECT balance FROM account WHERE id = 1;
-- 结果: 200(跟第一次不一样!不可重复读出现了)
COMMIT;

-- ===== 终端B:事务B(在终端A SLEEP 期间执行)=====
BEGIN;
UPDATE account SET balance = 200 WHERE id = 1;
COMMIT;

注意SLEEP的作用:终端A执行到SELECT SLEEP(5)后会等待5秒,这5秒内你切换到终端B执行UPDATE并提交,再切回终端A就能看到第二次SELECT的结果变了。没有SLEEP的话,两个终端的操作时序全靠手速,容易乱。

改成RR再跑一遍,两次结果就都是100了——修复验证完成

复现脏读

写一组MySQL测试SQL,演示READ UNCOMMITTED级别下的脏读。一个事务修改数据但不提交,另一个事务读到未提交的数据,然后第一个事务回滚。用sleep确保时序清晰。

复现RR下的DML边缘情况

写一组MySQL SQL,演示RR级别下DELETE能看到快照读看不到的行。开两个终端,事务A SELECT查不到数据,事务B INSERT并提交,事务A DELETE居然能删掉。用sleep控制时序,加注释标注端。

没有AI的时候你要手写这些,开两个终端来回切,手忙脚乱。有了AI,30秒生成一套完整的测试用例,跑完就知道自己理解对了没有。

这才是AI在DBA工作里的正确用法——不是问它"什么是幻读",而是让它帮你把课本上的理论变成能亲手跑的通关文牒。


面试加餐

昨天已经讲了RC和RR怎么选。今天补充几个面试官爱追问的:

Q: 不可重复读和幻读,哪个更严重?

从业务影响看,幻读通常更严重。因为不可重复读影响的是"一条数据"的准确性,幻读影响的是"结果集"的完整性。想象一下:你给所有符合条件的人发优惠券,结果因为幻读漏发了三个人——这个比某个人的余额读错了更难修复。

Q: 如果RC下既要防不可重复读、又不想换RR,怎么办?

SELECT ... FOR UPDATE 对关键行加锁,让其他事务的修改阻塞。代价是并发下降(行锁等待)。或者把需要一致性的多次读取合并成一次,比如用JOIN一次性把所需数据都查出来。

Q: 生产中你遇到过最离奇的隔离级别故障是什么?

遇到过一个连接池的问题。应用配置的是RC,但某个连接因为之前跑过一段脚本,会话级别被改成了SERIALIZABLE。这个连接被归还到连接池后,下一个请求拿到它,所有SELECT都被阻塞了——因为SERIALIZABLE下SELECT自动加共享锁,写操作被排队了。排查了很久才发现是"连接污染"。

顺便提醒一下 SET SESSIONSET TRANSACTION 的区别:

  • SET SESSION TRANSACTION ISOLATION LEVEL ... 影响当前会话的所有后续事务
  • SET TRANSACTION ISOLATION LEVEL ... 只影响当前会话的下一个事务,执行完一次就失效

连接池回收时,如果只 SET TRANSACTION 改了下个事务的隔离级别,跑完就恢复默认了,不会"污染"。但如果你用了 SET SESSION 改了一整个会话级别,归还后下一个请求拿到这个连接就会继承这个设置——这就是污染源头。现在我们会在中间件层做隔离级别兜底校验,每个连接归还前用 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ 重置回默认。


总结

🎯 面试考点

  • 三大问题的实战诊断方法:看症状、查隔离级别、定位根因
  • 脏读:线上极少见,确认RU→改RC即可
  • 不可重复读:最常见,RC下必出。方案:改RR / FOR UPDATE / 重构代码逻辑
  • 幻读:MySQL RR下快照读不出现,但当前读要注意Gap Lock范围
  • 连接池污染:会话级隔离级别设置会被复用,容易被忽略
  • IRL原则:先确认现象 → 再查级别 → 最后定修复方案

今天就试一下:登录你的数据库,执行这条SQL:

SELECT @@global.transaction_isolation, @@session.transaction_isolation;

确认一下当前会话和全局的隔离级别是不是一致的。如果不一致——你现在的连接可能已经被"污染"了。


下期预告:锁机制:行锁 vs 表锁 —— 面试必问!


有问题欢迎评论区交流,明天见!

相关文章
|
自然语言处理 Dubbo Java
【面试问题】Dubbo 推荐用什么协议?
【1月更文挑战第27天】【面试问题】Dubbo 推荐用什么协议?
|
关系型数据库 MySQL 数据库
深入探讨MySQL中的幻读现象:原因、影响及解决方案
**导言:** 在数据库领域中,幻读(Phantom Read)是一个常见但容易被忽视的问题。它可能会导致事务的隔离级别无法满足预期,从而引发数据一致性问题。MySQL作为广泛使用的关系型数据库,也不免遇到幻读问题。本文将深入解析MySQL中的幻读现象,探讨其原因、影响以及可能的解决方案。
2668 0
|
2月前
|
运维 安全 Java
【微服务】API网关核心作用、主流网关对比、服务治理、服务容错
本文系统梳理API网关全体系知识,涵盖核心定位、六大作用(路由/安全/流量/协议/可观测/业务增强)、主流选型对比(APISIX/Kong/SCG等)、与服务治理深度融合,以及全链路容错(限流/熔断/降级/舱壁等)五大维度,助力架构师与开发者高效落地微服务流量治理。
|
人工智能 运维 关系型数据库
智能运维+多模型服务能力,阿里云 RDS AI 助手旗舰版正式上线!
RDS AI 助手旗舰版在 RDS AI 助手专业版智能运维能力的基础上,提供灵活模型选择、智能模型路由、多模型灾备、API Key 集成等更自主可控、灵活便捷的模型服务,并支持纳管运维各类环境部署的数据库。
智能运维+多模型服务能力,阿里云 RDS AI 助手旗舰版正式上线!
|
1月前
|
SQL 关系型数据库 MySQL
【MySQL百日打怪升级第14天】 LIMIT 分页的性能优化:深分页到底慢在哪?
本文深入剖析MySQL深分页(如`LIMIT 100000,20`)性能瓶颈:本质是OFFSET导致全量扫描与丢弃,页码越深,扫描行数线性增长。详解三种实战优化方案——游标分页(高效稳定,需有序唯一字段)、延迟关联(兼容OFFSET,索引覆盖减回表)、范围分页(极简但场景受限),并附EXPLAIN对比与避坑指南。(239字)
196 6
|
1月前
|
SQL 算法 关系型数据库
【MySQL百日打怪升级第10天】JOIN的底层原理与优化:NLJ、Hash Join 与 Merge Join
本文系统解析MySQL三大JOIN算法:NLJ(含Simple/Index/Block变体)、8.0.18引入的Hash Join(O(N+M)复杂度,专治无索引大表连接),以及面试常考但MySQL原生不支持的Sort-Merge Join,附实战EXPLAIN识别与优化指南。(239字)
192 5
|
2月前
|
SQL 关系型数据库 MySQL
EXPLAIN 执行计划:一眼看穿你的SQL慢在哪
数据库小学妹带你轻松掌握SQL性能诊断!通过EXPLAIN查看执行计划,精准识别索引失效、全表扫描(ALL)、key为NULL等瓶颈。聚焦type、key、rows等6个关键字段,结合实战案例与避坑指南(如函数滥用、最左前缀破坏),让优化有的放矢。学完即用,告别盲目调优!
|
1月前
|
人工智能 关系型数据库 MySQL
【第3天】每天一个MySQL知识点,百日打怪升级
本系列由10年经验DBA主理,系统讲解MySQL安装(RPM/二进制/源码)与核心配置,涵盖`my.cnf`优先级、`innodb_buffer_pool_size`调优、连接与日志参数设置、四种生效方式对比,并附生产避坑指南与面试高频考点,助力快速入门与实战进阶。(239字)
207 2
|
2月前
|
SQL 存储 关系型数据库
【第1天】每天一个MySQL知识点,百日打怪升级
本系列以“每天一个知识点”形式,系统讲解MySQL核心原理。首日聚焦Client/Server通信流程,详解三层架构(连接层→SQL层→存储引擎层)、连接管理、查询解析优化及执行计划,直击面试高频考点,助你从CRUD进阶到懂原理、能排障。(239字)
234 0
|
存储 网络协议 Java
Java NIO 开发
本文介绍了Java NIO(New IO)及其主要组件,包括Channel、Buffer和Selector,并对比了NIO与传统IO的优势。文章详细讲解了FileChannel、SocketChannel、ServerSocketChannel、DatagramChannel及Pipe.SinkChannel和Pipe.SourceChannel等Channel实现类,并提供了示例代码。通过这些示例,读者可以了解如何使用不同类型的通道进行数据读写操作。
438 0
Java NIO 开发