事务隔离级别选错了,数据可能被“吞”掉——从脏读到幻读,一次讲透

简介: 事务隔离级别是数据库并发控制的核心机制,但很多开发者和DBA对脏读、不可重复读、幻读的区别一知半解,遇到问题只能“加锁试试”。本文从四个隔离级别出发,用真实SQL案例讲透三种并发问题的本质差异,对比MySQL与PostgreSQL在默认隔离级别上的不同选择,并结合业务场景给出选型建议,帮助读者写出更可靠的事务代码。

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

你有没有遇到过这种场景:一个事务里查了两次同样的数据,结果不一样;或者查了两次同样的范围条件,结果集多了一行。很多开发遇到这种情况第一反应是“是不是缓存有问题”,但其实大概率是事务隔离级别没选对。

事务隔离级别决定了多个事务同时运行时,彼此能看到多少对方的数据。选得太低,数据可能被“污染”;选得太高,并发性能会急剧下降。今天我们从四个隔离级别出发,把脏读、不可重复读、幻读这三个概念彻底讲透。

一、先搞懂四个隔离级别

SQL标准定义了四种隔离级别,从低到高依次是:

1. READ UNCOMMITTED(读未提交) ——最低级别。事务可以读取其他事务尚未提交的数据。几乎不提供并发控制,实际生产中极少使用。

2. READ COMMITTED(读已提交,简称RC) ——只能读取其他事务已经提交的数据。避免了脏读,但无法避免不可重复读。

3. REPEATABLE READ(可重复读,简称RR) ——保证同一事务内多次读取同一数据结果一致。MySQL InnoDB的默认隔离级别。

4. SERIALIZABLE(可串行化) ——最高级别,事务完全串行执行,避免所有并发问题,但性能开销最大。

不同隔离级别能解决的问题,可以浓缩成一张表:

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED 可能 可能 可能
READ COMMITTED 避免 可能 可能
REPEATABLE READ 避免 避免 InnoDB基本避免
SERIALIZABLE 避免 避免 避免

二、三种并发问题,用真实SQL讲清楚

脏读:读到别人“还没想好”的数据

脏读指的是一个事务读取了另一个事务尚未提交的修改。如果那个事务最终回滚了,读到的就是“脏数据”——从未真正存在过的数据。

用电商库存场景来理解:事务A开始扣减库存,把商品X的库存从100改为99,但还没提交;此时事务B查询库存,读到了99。如果事务A因支付失败回滚了,库存恢复为100,但事务B已经基于99做了后续操作——超卖了。

-- 事务A(未提交)
START TRANSACTION;
UPDATE products SET stock = stock - 1 WHERE id = 1;  -- stock从100变99
-- 此时事务B读取到stock=99(未提交的数据)
-- 事务A回滚
ROLLBACK;  -- stock恢复为100,但事务B已经用了99的数据

解决脏读,至少要用READ COMMITTED级别。

不可重复读:同一事务内,数据“变了”

不可重复读指在同一个事务内,多次读取同一数据时结果不一致。它与脏读的关键区别在于:脏读读的是未提交的数据,不可重复读读的是已提交的数据。

用财务场景理解:财务人员在事务A中查询员工张三的工资,第一次看到5000元;此时HR在事务B中给张三加薪至6000元并提交;事务A再次查询时,工资变成了6000元。同一个事务里两次查询结果不一样——这就是不可重复读。

-- 事务A(隔离级别READ COMMITTED)
START TRANSACTION;
SELECT salary FROM employees WHERE id = 1;  -- 返回5000

-- 事务B(同时执行)
START TRANSACTION;
UPDATE employees SET salary = 6000 WHERE id = 1;
COMMIT;  -- 提交修改

-- 事务A再次查询
SELECT salary FROM employees WHERE id = 1;  -- 返回6000,和第一次不一样
COMMIT;

READ COMMITTED级别无法避免这个问题。需要升级到REPEATABLE READ。

幻读:结果集“凭空多了一行”

幻读指在同一个事务内,多次查询某个范围的数据时,结果集的数量发生了变化——第一次查有5行,第二次查变成了6行。

打个比方:你在图书馆按分类找书,第一次看到书架上有5本书;等你转了一圈回来再数,变成了6本——多出来那本就是你不在的时候别人放上去的。

-- 事务A(隔离级别REPEATABLE READ)
START TRANSACTION;
SELECT * FROM orders WHERE status = 'pending';  -- 返回5行

-- 事务B(同时执行)
START TRANSACTION;
INSERT INTO orders (id, status) VALUES (100, 'pending');
COMMIT;

-- 事务A再次查询
SELECT * FROM orders WHERE status = 'pending';  -- 返回6行,多了一行!
COMMIT;

三、MySQL的RR为什么能“基本避免”幻读?

根据SQL标准,REPEATABLE READ级别应该允许幻读。但MySQL InnoDB通过MVCC(多版本并发控制)+ Gap Lock(间隙锁) 的组合,在大多数场景下实际避免了幻读。

  • MVCC​:事务开始时拍一次快照,后面的普通SELECT都读这个快照,别人提交了也看不见
  • Gap Lock(间隙锁) :锁定索引记录之间的空隙,阻止其他事务在范围内插入新行

但需要注意的是,​DML语句(UPDATE/DELETE)不遵循快照读​,可能看到其他事务刚提交的行。所以在某些边缘场景下,RR级别仍可能出现幻读。

四、MySQL vs PostgreSQL:默认隔离级别为什么不同?

这是一个容易被忽略但很重要的差异。

  • MySQL InnoDB默认使用REPEATABLE READ(RR)
  • PostgreSQL默认使用READ COMMITTED(RC)

为什么不同?因为两者的MVCC实现机制不同:

对比维度 MySQL InnoDB(RR) PostgreSQL(RC)
快照时机 事务开始时拍一次快照 每条SELECT重新生成快照
不可重复读 避免 RC级别会出现
幻读 基本避免(MVCC+间隙锁) RC级别会出现
适用场景 高并发CRUD 复杂查询、数据分析

MySQL选择RR作为默认,是为了在并发性能和数据一致性之间取得平衡,适合大多数互联网业务场景。PostgreSQL选择RC作为默认,则是因为它在RR级别下对幻读的处理更加严格,可能导致更多死锁。

五、业务场景中怎么选隔离级别?

业务场景 推荐级别 理由
金融账务、库存扣减 SERIALIZABLE 或 RR 数据一致性要求极高
电商订单、用户中心 RR(MySQL默认) 平衡性能与一致性
报表查询、数据分析 RC 查询为主,可接受不可重复读
高并发日志写入 RC 或 READ UNCOMMITTED 写入为主,对一致性要求低

选型原则​:不是隔离级别越高越好。隔离级别越高,并发性能越低。应该根据业务对一致性和性能的要求做权衡。

六、实战避坑:两个常见的隔离级别陷阱

陷阱1:在RC级别下做“先查后改”

很多业务逻辑是“先查询状态,再根据状态做更新”。在RC级别下,两次查询之间数据可能被其他事务修改,导致“查的时候是A,改的时候已经变成B了”——数据被“吞”了。解决方案:要么用RR级别,要么在查询时加FOR UPDATE锁住数据。

陷阱2:RR级别下的间隙锁导致死锁

RR级别通过间隙锁避免幻读,但间隙锁也是死锁的常见来源。在RR级别下做范围删除或范围更新,可能锁住大量不存在的“间隙”,多个事务互相等待形成死锁。如果业务场景不需要防止幻读,可以考虑降级到RC级别,牺牲一点一致性换取更高的并发性能。

总结

事务隔离级别是数据库并发控制的核心机制,直接影响数据的正确性和系统的吞吐量。理解脏读、不可重复读、幻读的本质差异,知道MySQL为什么选择RR、PostgreSQL为什么选择RC作为默认,你就能在业务场景中做出更合理的选择,写出更可靠的事务代码。

小耶在手,SQL 不愁

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽……我们下次见~

相关文章
|
1月前
|
存储 人工智能 关系型数据库
湖库一体:2026年数据库架构的“终极答案”还是新瓶装旧酒?
2026年6月,OceanBase发布湖库一体AI数据库,阿里云PolarDB年初已推出AI数据湖库(Lakebase),Databricks也在6月推出了LTAP架构。“湖库一体”成为2026年数据库圈最热的概念之一。本文从湖库一体的概念定义出发,拆解其技术原理,对比“湖仓一体”与“湖库一体”的差异,分析三大厂商的落地路径,并讨论这一趋势对DBA和架构师的现实意义。
|
2月前
|
人工智能 Cloud Native 关系型数据库
MySQL 8.4 LTS来了!从8.0到8.4,DBA必须知道的5个核心变化
MySQL 8.0社区版将于2026年结束生命周期,8.4 LTS作为首个长期支持版本,提供5年超长支持周期(至2031年)。本文从InnoDB并行查询、Redo Log动态容量、默认认证插件变更、参数默认值调整、云原生适配五个维度,梳理DBA升级前必须掌握的核心变化,并提供升级检查清单。
|
2月前
|
SQL 运维 自然语言处理
国产向量数据库有哪些?两大技术流派深度对比与选型指南
向量数据库是2026年数据库领域增长最快的细分赛道之一。本文从RAG应用和企业知识库的实际需求出发,系统梳理国产向量数据库的两大技术流派——独立向量数据库与融合型向量数据库,深入对比两者的架构差异、适用边界和选型逻辑。
|
2月前
|
SQL 人工智能 自然语言处理
Vibe Coding 是什么?当“感觉编程”遇上数据库
Vibe Coding是2026年编程圈最火的概念之一,指开发者通过自然语言描述“感觉”或“意图”,由AI自动生成代码、调试、优化。本文从Vibe Coding的起源讲起,分析它如何改变数据库开发方式:从手写SQL到自然语言查询、从人工调索引到AI推荐、从经验运维到智能诊断。探讨这项趋势对DBA职业的影响,并给出拥抱变化的实用建议。技术会变,但人的判断力、审美和业务理解才是长期竞争力。
|
22天前
|
存储 传感器 监控
时序数据是什么?2026年企业为什么离不开时序数据库
时序数据是2026年增长最快的数据类型之一。据行业预测,工业物联网产生的时序数据量将占企业总数据量的75%以上,年复合增长率超过40%。时序数据已从“技术补充”升级为“核心资产”。本文从时序数据的基本概念出发,讲解时序数据的特征、应用场景,以及为什么传统数据库处理不了时序数据,帮助读者建立对时序数据的完整认知。
|
27天前
|
SQL JavaScript 关系型数据库
递归CTE实战:用SQL搞定树形结构查询,告别“写死”代码
组织架构、商品分类、菜单权限、BOM清单——树形结构查询是日常开发中高频出现的需求。很多开发者的做法是“写死层级”或“循环查库”,代码又臭又长,性能还差。递归CTE是解决这类问题的标准写法,但很多人一看到WITH RECURSIVE就觉得头大。本文从三个真实场景出发,手把手教读者写出能直接用的递归CTE,并讲清楚执行机制和性能陷阱。
|
1月前
|
SQL 存储 运维
SQL Server迁移必看!深度解析SQLServer兼容性三大核心维度与选型指南
SQL Server迁移是国产化替代中最复杂的场景之一。所谓SQLServer兼容性,通俗来说,就是让国产数据库能够“听懂”并“执行”原本运行在微软SQL Server上的指令,同时保持数据不丢失、业务不中断。本文从语法兼容、语义兼容、生态兼容三大维度深度解析SQLServer兼容性的本质,梳理T-SQL差异、存储过程转换、工具链适配等核心挑战,并提供系统化的迁移评估路径和选型建议,帮助读者在迁移启动之前就建立起清晰的认知图谱。
|
20天前
|
缓存 关系型数据库 MySQL
数据库参数调优实战:100个参数里真正影响性能的不到10个
MySQL有上百个系统参数,初看让人望而生畏。但真正对生产环境影响最大的,不到10个。很多DBA要么“默认参数跑天下”,要么“看到参数就想调”,结果往往是越调越糟。本文从生产环境实际经验出发,筛选出8个最关键的数据库参数,讲解其作用原理、影响范围和调优策略,并提供一套“先诊断后调参”的系统化方法,帮助读者从“参数恐慌”走向“精准调优”。
|
21天前
|
SQL 监控 关系型数据库
锁等待比死锁更隐蔽:不报错、不告警、只默默变慢
死锁是最“显眼”的锁问题——它会直接报错,DBA一眼就能看到。但真正让系统“卡住”的,往往是那些不报错、不告警、只默默等待的锁等待问题。一条SQL平时0.1秒,今天突然3秒,执行计划没变、索引没坏、数据量也没暴涨——背后可能是一条长事务在锁着关键行。本文从锁等待的排查方法出发,讲解如何通过系统视图定位锁等待链、如何识别长事务、如何评估锁等待对系统性能的影响,帮助读者在死锁日志之外,建立完整的锁问题排查能力。
|
22天前
|
SQL 关系型数据库 MySQL
批量DML的性能与一致性:不是所有“批量操作”都应该用批量SQL
批量操作是日常开发中提升性能的常用手段,但“批量”不等于“越快越好”。批量大小不当、事务边界不清、缺乏错误处理,都可能让批量操作从“性能优化”变成“性能灾难”。本文从批量DML的执行机制出发,讲解批量大小对性能的影响曲线、事务边界的设计原则、批量操作中的数据一致性保障,以及如何根据业务场景选择合理的批量策略,帮助读者写出既快又稳的批量操作代码。