文章目录
- 1.场景案例讲解:转账
- 2.事务的四大特性(ACID)
- 3.事务带来的问题
- 4.事务的隔离级别
- 5.事务进阶
-
- [5.1 ACID的底层靠什么保证](#5.1 ACID的底层靠什么保证)
- 5.2.MVCC(多版本并发控制)
-
- [5.2.1MVCC是怎么实现的(以 MySQL InnoDB 为例)](#5.2.1MVCC是怎么实现的(以 MySQL InnoDB 为例))
- [5.2.2 快照读VS当前读](#5.2.2 快照读VS当前读)
- [5.2.3 MVCC在不同隔离级别下的表现](#5.2.3 MVCC在不同隔离级别下的表现)
- [5.3 在RR级别下,是否真的完全没有幻读](#5.3 在RR级别下,是否真的完全没有幻读)
在数据库领域简单来说, 事务就是"一组逻辑操作单元",这一组操作要么全部成功,要么全部失败,不能只执行一半。
1.场景案例讲解:转账
为了方便理解,我们用最经典的"A 给 B 转账 100 元"为例:
- 第一步: 从 A 的账户扣除 100 元。
- 第二步: 给 B 的账户增加 100 元。
如果没有事务,如果第一步执行成功(A 扣钱了),但在执行第二步时系统崩溃或报错(B 没收到钱),那么这 100 元就凭空消失了。
事务的作用就是保证这两步是一个整体:如果第二步失败了,系统会自动把第一步的扣款"撤销"回去,就像一切都没发生过一样。
2.事务的四大特性(ACID)
| 特性 | 英文 | 解释 | 实现原理 (底层) |
|---|---|---|---|
| 原子性 | Atomicity | 要么全做,要么全不做。 事务是不可分割的最小单位。如果有操作失败,回滚到事务开始前的状态。 | Undo Log (回滚日志) |
| 一致性 | Consistency | 事务执行前后,数据必须保持逻辑上的合法性(例如:转账前后,A和B的总金额不变)。 | 依赖于原子性、隔离性以及代码逻辑 |
| 隔离性 | Isolation | 多个事务并发执行时,互不干扰。一个事务内部的操作对其他事务是不可见的(取决于隔离级别)。 | 锁 (Locks) 和 MVCC |
| 持久性 | Durability | 事务一旦提交,对数据的修改就是永久的,即使数据库立刻宕机也能恢复。 | Redo Log (重做日志) |
3.事务带来的问题
当很多个事务同时运行时(高并发),如果没有隔离机制,会出现以下奇葩问题:
-
脏读 (Dirty Read): 事务 A 读取到了事务 B 还没提交的数据。如果 B 后来回滚了,A 读到的就是"脏"数据(根本不存在的数据)。
-
不可重复读 (Non-repeatable Read): 事务 A 在自己的过程中读取了两次某行数据,但两次结果不一样。因为中间事务 B 修改并提交了数据。
-
幻读 (Phantom Read): 事务 A 按照条件查询(比如"查所有大于100元的记录"),第一次查到 3 条,第二次却查到了 4 条。因为中间事务 B 插入了一条新数据。
区别: "不可重复读"侧重于数据被修改 (Update) ;"幻读"侧重于数据被新增或删除 。
4.事务的隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| Read Uncommitted (读未提交) | √ | √ | √ | 最不安全。允许读别的事务没提交的数据。实际几乎不用。 |
| Read Committed (RC) (读已提交) | × | √ | √ | Oracle/SQL Server 的默认级别。只能读别人已提交的数据。解决了脏读。 |
| Repeatable Read (RR) (可重复读) | × | × | × (大部分) | MySQL 的默认级别 。保证同一事务内多次读取结果一致。MySQL 通过 MVCC 和 **间隙锁 ** 解决了大部分幻读问题。 |
| Serializable (串行化) | × | × | × | 最慢但最安全。强制事务排队执行,完全没有并发问题。 |
5.事务进阶
5.1 ACID的底层靠什么保证
- 原子性 ->靠
Undo Log(回滚日志)- 原理:在你更新数据之前,MySQL 会先把"旧值"记录下来。如果事务失败要回滚,就读取 Undo Log 把数据"改回去"(例如:如果是 Insert,回滚时就执行 Delete;如果是 Update,就 Update 回旧值)。
- 持久性 ->靠
Redo Log(重做日志)- 原理:MySQL 为了快,修改数据是先改内存,然后写日志,最后才慢慢刷到磁盘数据文件。如果断电,重启后可以通过 Redo Log 恢复内存中还没刷盘的数据。
- 隔离性 ->靠
锁+MVCC- 这是面试最复杂的点,下面详细说。
- 一致性 ->靠以上三者共同保证 + 业务逻辑
- 一致性是最终目的,原子性、持久性、隔离性都是手段。
5.2.MVCC(多版本并发控制)
简单来说,它是数据库为了提高并发性能 而设计的一种技术。它的核心思想是:"你读你的,我写我的,咱俩互不耽误。"
在没有MVCC 之前,数据库为了保证数据安全,通常使用锁。如果有人在写数据(加锁),其他人就不能读,必须排队等待。这大大降低了效率。MVCC 彻底解决了这个问题。
5.2.1MVCC是怎么实现的(以 MySQL InnoDB 为例)
InnoDB 实现 MVCC 靠三个"法宝":隐藏字段 、Undo Log(回滚日志) 和 Read View(读视图)。
- 隐藏字段
- 在你的数据库表中,除了你看见的列,InnoDB 每一行数据背后都默默添加了几个你看不到的字段,
DB_TRX_ID(事务ID): 记录是哪个事务最后修改了这行数据。DB_ROLL_PTR(回滚指针): 指向这行数据的"上一个版本"(存储在 Undo Log 里)。、
- 在你的数据库表中,除了你看见的列,InnoDB 每一行数据背后都默默添加了几个你看不到的字段,
- Undo Log (版本链)
- 当一个事务修改数据时,数据库不会直接覆盖旧数据,而是先把旧数据复制到 Undo Log 中。 通过
DB_ROLL_PTR指针,新数据和旧数据就连成了一条版本链。 - 例如:数据库初始值是name = "张三"。事务A把改为
李四。表里现在是李四。Undo Log 里存着张三。李四身上有个指针指向张三。
- 当一个事务修改数据时,数据库不会直接覆盖旧数据,而是先把旧数据复制到 Undo Log 中。 通过
- Read View (读视图)
- 这是 MVCC 的判官。当一个事务要读取数据(事务刚启动时并不会有读视图)时,数据库会生成一个 Read View。它包含当前活跃(未提交)的事务 ID 列表。
- 通过 Read View,事务在读取每一行数据时,会根据以下逻辑判断可见性:
- 这行数据是我改的吗? -> 是,读最新的。
- 这行数据的修改者已经提交了吗? -> 是,读最新的。
- 这行数据的修改者还在活跃列表中 (未提交)吗? -> 是,不能读,顺着版本链去找上一个旧版本,直到找到一个能读的版本为止。
5.2.2 快照读VS当前读
- 快照读
- 简单的
SELECT * FROM table ... - 不加锁,读取的是历史版本。
- 这是 MVCC 发挥作用的地方。
- 简单的
- 当前读
- SELECT ... FOR UPDATE
SELECT ... LOCK IN SHARE MODEINSERT,UPDATE,DELETE- 加锁,读取的是最新版本,必须保证数据是最新的,否则会覆盖别人的修改。
5.2.3 MVCC在不同隔离级别下的表现
| 隔离级别 | Read View 生成时机 | 效果 |
|---|---|---|
| Read Committed (RC) | 每次执行 Select 语句时都生成一个新的 Read View。 | 能读到别的事务刚提交的数据(不可重复读)。 |
| Repeatable Read (RR) | 仅在事务中第一次执行 Select 时生成一个 Read View,后续复用它。 | 整个事务期间看到的"世界"是一样的(可重复读),哪怕别人提交了修改我也看不见。 |
5.3 在RR级别下,是否真的完全没有幻读
特例 :如果事务 A 先进行了快照读(没查到数据),事务 B 插入了一条数据并提交。此时事务 A 如果直接执行 UPDATE 操作(当前读),是可以更新到这条"本来看不见"的数据的。一旦更新成功,这条数据的 DB_TRX_ID 就变成了事务 A 的 ID,下次事务 A 再快照读就能看见了。这被称为"幻读的一个特殊场景"。
如何理解 :直观比喻:隐形人与泼油漆
想象你在玩一个游戏(开启了事务 A):
- 快照读(拍照片): 游戏开始时,系统给你拍了一张照片。你只能看这张照片里的东西。
- 别人插入数据(隐形人): 此时,玩家 B 偷偷跑进房间(数据库),放了一个箱子(插入数据)并在箱子上贴了 B 的名字,然后走了。
- 你再看照片: 照片是刚开始拍的,里面当然没有那个箱子。在你眼里,房间是空的。
- 特殊操作(Update = 泼油漆):
- 虽然你看不到箱子,但你这时候如果不讲道理,对着房间空气挥舞刷子说:"把所有东西都刷成红色!"(执行
UPDATE table SET color='red')。 - 关键点来了: 刷漆这个动作是物理交互 ,不能对着照片刷,必须对着真实的房间刷。
- 于是,你的刷子碰到了那个"看不见"的箱子,把它刷成了红色。
- 虽然你看不到箱子,但你这时候如果不讲道理,对着房间空气挥舞刷子说:"把所有东西都刷成红色!"(执行
- 奇迹发生:
- 因为是你刷的漆,你顺手把箱子上的名字撕掉,贴上了你(A)的名字。
- 当你再次拿起照片看的时候,根据 MVCC 规则:"凡是贴着我自己名字的东西,我都可见"。
- 于是,那个箱子突然在你的照片里显形了!
下面是在数据库操作上的严格推导
前置条件
- 事务 A (Trx_id = 100):开启,RR 隔离级别。
- 事务 B (Trx_id = 200) :开启,插入一条数据
id=1,然后提交
| 步骤 | 事务 A (Trx_id=100) | 事务 B (Trx_id=200) | 数据行状态 (DB_TRX_ID) | 此时 A 能看见吗? |
|---|---|---|---|---|
| 1 | BEGIN; SELECT * FROM t; |
(空) | 看不见 (生成了 Read View,后续都用这个) | |
| 2 | INSERT INTO t VALUES(1); COMMIT; |
id=1, TRX_ID=200 |
看不见 (因为 200 > A 的 Read View 生成时的最大ID) | |
| 3 | UPDATE t SET col=x WHERE id=1; |
发生变化! 变成 TRX_ID=100 |
看不见 -> 看得见 (这是关键转折点) | |
| 4 | SELECT * FROM t; |
id=1, TRX_ID=100 |
看得见! |
为什么步骤3能成功?
因为 UPDATE 语句是当前读 (Current Read) 。 数据库规定:为了防止数据丢失或冲突,更新操作必须读取这一行"最新"的状态,而不能看"历史"状态。 所以,虽然 A 的眼睛(快照读)看着旧图,但 A 的手(Update)直接摸到了 B 刚提交的最新数据 TRX_ID=200 的记录,并执行了更新。
为什么步骤4能看见?
更新成功后,这行数据的 DB_TRX_ID 被修改成了 100 (也就是事务 A 自己的 ID)。判断结果为 YES。 既然是你自己亲手改过的,你当然应该看见它。这就导致了"幻读"的感觉------明明刚才没有,我改了一下,它就出来了。
为什么MySQL要允许这种"怪事"发生?
你可能会问:"为什么不干脆禁止 A 更新它看不见的数据?"
这是为了保证数据的一致性 。
如果事务 A 看着旧照片(认为 id=1 不存在),然后想插入一条 id=1 的数据。但实际上 B 已经插入并提交了 id=1。
- 如果 A 不去读最新的数据(当前读),直接强制插入,就会报 主键冲突。
- 如果 A 执行
UPDATE ... WHERE id=1,如果不允许它更新最新的数据,那么 B 提交的修改就会被无视,这在逻辑上更不合理。