并发事务带来哪些问题?(超详细版)

在数据库并发事务处理场景中,若缺乏有效的隔离机制,会引发脏读 、不可重复读 、幻读 三类核心数据一致性问题,部分数据库还会伴随丢失更新的衍生问题。这些问题的根源是多个事务对同一批数据的交叉操作,且操作时序未被合理管控。

下面对每类问题进行超详细拆解,包括定义、核心成因、典型场景、危害及解决思路:

一、 脏读(Dirty Read)

1. 核心定义

一个事务读取到了另一个事务 尚未提交 的数据修改,这部分未提交的、可能被回滚的数据被称为 脏数据。

2. 核心成因

低隔离级别下,数据库允许事务读取其他事务未持久化的内存修改数据,且不做任何锁或版本控制限制。

3. 典型场景(银行转账)

时间顺序 事务 T1(转账扣钱) 事务 T2(查询余额)
T1 读取账户 A 余额 = 1000 元 -
T2 - 开始执行
T3 执行 UPDATE A SET balance = 500 WHERE id=1(未提交) -
T4 - 读取账户 A 余额 = 500 元
T5 发现转账对象错误,执行 ROLLBACK(余额回滚为 1000 元) -
T6 - 基于 500 元的余额,执行了消费 300 元的操作

4. 危害

事务 T2 基于无效的脏数据做业务决策,会导致业务逻辑错乱,比如上述场景中,账户 A 实际余额 1000 元,但 T2 误以为只有 500 元,后续操作会出现数据偏差。

5. 解决思路

提升事务隔离级别至 读已提交(Read Committed) 及以上,数据库会保证事务只能读取其他事务已提交的数据。

二、 不可重复读(Non-repeatable Read)

1. 核心定义

同一个事务内,多次读取同一行数据,得到的结果不一致。因为在两次读取之间,其他事务提交了对该数据的更新操作。

2. 核心成因

隔离级别为读已提交时,虽然解决了脏读,但允许同一事务内的多次读取,感知到其他事务提交的更新,破坏了事务内数据的一致性。

3. 关键区别(与脏读对比)

  • 脏读读取的是 未提交 的数据;
  • 不可重复读读取的是 已提交 的数据。不可重复读的问题不在于数据 "脏",而在于同一事务内数据读取结果不稳定。

4. 典型场景(财务对账)

时间顺序 事务 T1(对账,多次读同一数据) 事务 T2(修改余额)
T1 开始执行,第一次读取账户 A 余额 = 1000 元 -
T2 - 开始执行
T3 - 执行 UPDATE A SET balance = 1500 WHERE id=1
T4 - 提交事务
T5 第二次读取账户 A 余额 = 1500 元 -
T6 T1 内两次读取结果不一致,对账逻辑混乱 -

5. 危害

依赖同一事务内多次读取数据一致性的业务(如对账、统计、计算)会出现结果错误,无法保证业务逻辑的正确性。

6. 解决思路

  1. 提升隔离级别至 可重复读(Repeatable Read) :数据库通过 MVCC(多版本并发控制) 或 行锁,保证同一事务内多次读取同一数据时,只能看到事务启动时的版本。
  2. 手动加锁:对读取的数据加 共享锁,阻止其他事务修改,直到当前事务结束。

三、 幻读(Phantom Read)

1. 核心定义

同一个事务内 ,多次执行 相同的范围查询 ,得到的结果集条数不一致。因为在两次查询之间,其他事务提交了对该范围数据的 插入或删除 操作,就像出现了 "幻觉"。

2. 核心成因

可重复读隔离级别能解决单行数据的不可重复读,但无法管控范围查询下的新增 / 删除操作,因为新增数据的行锁在事务启动时不存在,无法被拦截。

3. 关键区别(与不可重复读对比)

对比维度 不可重复读 幻读
操作类型 针对 单行数据的 UPDATE 针对 范围数据的 INSERT/DELETE
结果变化 数据内容改变 结果集数量改变
解决级别 可重复读即可解决 需串行化级别才能彻底解决

4. 典型场景(库存盘点)

时间顺序 事务 T1(盘点库存 > 0 的商品) 事务 T2(新增商品)
T1 开始执行,查询 SELECT * FROM goods WHERE stock > 0,得到 10 条数据 -
T2 - 开始执行
T3 - 插入一条 stock = 50 的新商品,提交事务
T4 T1 再次执行相同查询,得到 11 条数据 -
T5 T1 基于第一次的 10 条数据生成盘点报告,与实际 11 条不符 -

5. 特殊情况:"写幻读"

更危险的幻读场景是 写操作的幻觉:事务 T1 先查询范围数据,再基于查询结果执行更新,但其他事务插入了新数据,导致 T1 的更新操作意外修改了新增的数据。

sql

复制代码
-- 事务 T1
BEGIN;
-- 步骤1:查询所有库存>0的商品,此时有10条
SELECT * FROM goods WHERE stock > 0;
-- 步骤2:更新这些商品的价格(此时T2插入了1条新商品)
UPDATE goods SET price = price * 1.1 WHERE stock > 0;
COMMIT;
-- 结果:T1 本意是更新10条商品,实际更新了11条,包含了T2新增的商品

6. 解决思路

  1. 串行化(Serializable)隔离级别 :数据库对范围查询加 表锁 或 间隙锁,阻止其他事务在该范围插入 / 删除数据,事务串行执行,彻底解决幻读,但性能损耗极大。
  2. 间隙锁 + 行锁(Next-Key Lock):MySQL InnoDB 存储引擎在可重复读级别下,会自动为范围查询加间隙锁,阻止其他事务在间隙中插入数据,可缓解大部分幻读场景,但无法完全避免。

四、 衍生问题:丢失更新(Lost Update)

1. 核心定义

两个事务同时修改同一行数据,后提交的事务会覆盖先提交事务的修改,导致先提交的修改丢失。

2. 分类

  • 第一类丢失更新:事务 T1 修改数据后未提交,事务 T2 也修改同一数据并提交,随后 T1 回滚,导致 T2 的修改被回滚丢失。
  • 第二类丢失更新:事务 T1 和 T2 都读取同一数据,T1 先修改提交,T2 后修改提交,T2 的修改覆盖了 T1 的修改。

3. 解决思路

  • 加 排他锁:修改数据时,阻止其他事务读取和修改该数据;
  • 使用 乐观锁:基于版本号或时间戳,在提交时校验数据是否被修改过,若被修改则放弃更新。

五、 各类并发问题与隔离级别的对应关系

隔离级别 脏读 不可重复读 幻读 丢失更新
读未提交(Read Uncommitted) ✅ 会出现 ✅ 会出现 ✅ 会出现 ✅ 会出现
读已提交(Read Committed) ❌ 不会出现 ✅ 会出现 ✅ 会出现 ✅ 会出现
可重复读(Repeatable Read) ❌ 不会出现 ❌ 不会出现 ⚠️ 部分出现(InnoDB 缓解) ❌ 不会出现
串行化(Serializable) ❌ 不会出现 ❌ 不会出现 ❌ 不会出现 ❌ 不会出现

注:✅ 代表会出现,❌ 代表不会出现,⚠️ 代表部分场景会出现

相关推荐
bksczm7 小时前
MySQL之存储引擎
数据库·mysql
大模型最新论文速读7 小时前
text-to-sql:从学术玩具到生产落地
数据库·sql
ManageEngineITSM8 小时前
工单分类怎么设计?类别、子类别与自动分派的实用方法
大数据·服务器·数据库·人工智能·分类·数据挖掘
ClouGence8 小时前
MongoDB 双向同步如何防止数据循环?(附实操指南)
数据库·mongodb
TDengine (老段)8 小时前
TDengine IDMP 十大实用功能(7)
数据库·物联网·时序数据库·tdengine·涛思数据·工业数据·idmp
Decay__8 小时前
15_MySQL 数据同步与迁移方案 —— 复制 / DataX / Canal / 异构同步(对标 Oracle GoldenGate)
运维·数据库·sql·mysql·架构
zjx23888 小时前
oracle11g转Gbase8s库SQL常见问题修改
数据库·sql
真上帝的左手8 小时前
30. 技术专题-‌应用框架组件-缓存-Redis
数据库·redis·分布式·缓存·中间件
雪芽蓝域zzs8 小时前
第 2 章:整合 MySQL + MyBatis,搭建数据库基础、实体类、Mapper、测试查询
数据库·mysql·mybatis
江湖中的阿龙9 小时前
Oracle 紧急安全警报:Java 9.1 分漏洞 CVE-2026-91403,撤销对 eMudhra 风险证书的信任
java·安全·oracle