MySQL事务

文章目录

在数据库领域简单来说, 事务就是"一组逻辑操作单元",这一组操作要么全部成功,要么全部失败,不能只执行一半。

1.场景案例讲解:转账

为了方便理解,我们用最经典的"A 给 B 转账 100 元"为例:

  1. 第一步: 从 A 的账户扣除 100 元。
  2. 第二步: 给 B 的账户增加 100 元。
    如果没有事务,如果第一步执行成功(A 扣钱了),但在执行第二步时系统崩溃或报错(B 没收到钱),那么这 100 元就凭空消失了。

事务的作用就是保证这两步是一个整体:如果第二步失败了,系统会自动把第一步的扣款"撤销"回去,就像一切都没发生过一样。

2.事务的四大特性(ACID)

特性 英文 解释 实现原理 (底层)
原子性 Atomicity 要么全做,要么全不做。 事务是不可分割的最小单位。如果有操作失败,回滚到事务开始前的状态。 Undo Log (回滚日志)
一致性 Consistency 事务执行前后,数据必须保持逻辑上的合法性(例如:转账前后,A和B的总金额不变)。 依赖于原子性、隔离性以及代码逻辑
隔离性 Isolation 多个事务并发执行时,互不干扰。一个事务内部的操作对其他事务是不可见的(取决于隔离级别)。 锁 (Locks)MVCC
持久性 Durability 事务一旦提交,对数据的修改就是永久的,即使数据库立刻宕机也能恢复。 Redo Log (重做日志)

3.事务带来的问题

当很多个事务同时运行时(高并发),如果没有隔离机制,会出现以下奇葩问题:

  1. 脏读 (Dirty Read): 事务 A 读取到了事务 B 还没提交的数据。如果 B 后来回滚了,A 读到的就是"脏"数据(根本不存在的数据)。

  2. 不可重复读 (Non-repeatable Read): 事务 A 在自己的过程中读取了两次某行数据,但两次结果不一样。因为中间事务 B 修改并提交了数据。

  3. 幻读 (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的底层靠什么保证

  1. 原子性 ->靠 Undo Log (回滚日志)
    • 原理:在你更新数据之前,MySQL 会先把"旧值"记录下来。如果事务失败要回滚,就读取 Undo Log 把数据"改回去"(例如:如果是 Insert,回滚时就执行 Delete;如果是 Update,就 Update 回旧值)。
  2. 持久性 ->靠 Redo Log (重做日志)
    • 原理:MySQL 为了快,修改数据是先改内存,然后写日志,最后才慢慢刷到磁盘数据文件。如果断电,重启后可以通过 Redo Log 恢复内存中还没刷盘的数据。
  3. 隔离性 ->靠 + MVCC
    • 这是面试最复杂的点,下面详细说。
  4. 一致性 ->靠以上三者共同保证 + 业务逻辑
    • 一致性是最终目的,原子性、持久性、隔离性都是手段。

5.2.MVCC(多版本并发控制)

简单来说,它是数据库为了提高并发性能 而设计的一种技术。它的核心思想是:"你读你的,我写我的,咱俩互不耽误。"

在没有MVCC 之前,数据库为了保证数据安全,通常使用。如果有人在写数据(加锁),其他人就不能读,必须排队等待。这大大降低了效率。MVCC 彻底解决了这个问题。

5.2.1MVCC是怎么实现的(以 MySQL InnoDB 为例)

InnoDB 实现 MVCC 靠三个"法宝":隐藏字段Undo Log(回滚日志)Read View(读视图)

  1. 隐藏字段
    • 在你的数据库表中,除了你看见的列,InnoDB 每一行数据背后都默默添加了几个你看不到的字段,DB_TRX_ID (事务ID): 记录是哪个事务最后修改了这行数据。DB_ROLL_PTR (回滚指针): 指向这行数据的"上一个版本"(存储在 Undo Log 里)。、
  2. Undo Log (版本链)
    • 当一个事务修改数据时,数据库不会直接覆盖旧数据,而是先把旧数据复制到 Undo Log 中。 通过 DB_ROLL_PTR 指针,新数据和旧数据就连成了一条版本链
    • 例如:数据库初始值是name = "张三"。事务A把改为 李四。表里现在是 李四。Undo Log 里存着 张三李四 身上有个指针指向 张三
  3. Read View (读视图)
    • 这是 MVCC 的判官。当一个事务要读取数据(事务刚启动时并不会有读视图)时,数据库会生成一个 Read View。它包含当前活跃(未提交)的事务 ID 列表。
    • 通过 Read View,事务在读取每一行数据时,会根据以下逻辑判断可见性:
      • 这行数据是改的吗? -> 是,读最新的。
      • 这行数据的修改者已经提交了吗? -> 是,读最新的。
      • 这行数据的修改者还在活跃列表中 (未提交)吗? -> 是,不能读,顺着版本链去找上一个旧版本,直到找到一个能读的版本为止。

5.2.2 快照读VS当前读

  • 快照读
    • 简单的 SELECT * FROM table ...
    • 不加锁,读取的是历史版本。
    • 这是 MVCC 发挥作用的地方。
  • 当前读
    • SELECT ... FOR UPDATE
    • SELECT ... LOCK IN SHARE MODE
    • INSERT, 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):

  1. 快照读(拍照片): 游戏开始时,系统给你拍了一张照片。你只能看这张照片里的东西。
  2. 别人插入数据(隐形人): 此时,玩家 B 偷偷跑进房间(数据库),放了一个箱子(插入数据)并在箱子上贴了 B 的名字,然后走了。
  3. 你再看照片: 照片是刚开始拍的,里面当然没有那个箱子。在你眼里,房间是空的。
  4. 特殊操作(Update = 泼油漆):
    • 虽然你看不到箱子,但你这时候如果不讲道理,对着房间空气挥舞刷子说:"把所有东西都刷成红色!"(执行 UPDATE table SET color='red')。
    • 关键点来了: 刷漆这个动作是物理交互 ,不能对着照片刷,必须对着真实的房间刷。
    • 于是,你的刷子碰到了那个"看不见"的箱子,把它刷成了红色。
  5. 奇迹发生:
    • 因为是你刷的漆,你顺手把箱子上的名字撕掉,贴上了你(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 提交的修改就会被无视,这在逻辑上更不合理。
相关推荐
小满、17 分钟前
MySQL :存储引擎原理、索引结构与执行计划
数据库·mysql·索引·mysql 存储引擎
大锦终3 小时前
【MySQL】基本查询
数据库·mysql
q***47183 小时前
使用Canal将MySQL数据同步到ES(Linux)
linux·mysql·elasticsearch
避避风港4 小时前
MySQL 从入门到实战
数据库·mysql
q***69778 小时前
Y20030018基于Java+Springboot+mysql+jsp+layui的家政服务系统的设计与实现 源代码 文档
java·spring boot·mysql
q***09808 小时前
MySQL 常用 SQL 语句大全
数据库·sql·mysql
q***49869 小时前
MySQL数据的增删改查(一)
android·javascript·mysql
q***064717 小时前
MySQL的UPDATE(更新数据)详解
数据库·mysql
8***B17 小时前
MySQL性能
数据库·mysql