MySQL 事务
- [MySQL 事务](#MySQL 事务)
MySQL 事务
MySQL主从复制:https://blog.csdn.net/a18792721831/article/details/146117935
MySQL Binlog:https://blog.csdn.net/a18792721831/article/details/146606305
MySQL General Log:https://blog.csdn.net/a18792721831/article/details/146607343
MySQL Slow Log:https://blog.csdn.net/a18792721831/article/details/147166971
MySQL Error Log:https://blog.csdn.net/a18792721831/article/details/147167038
MySQL Redo Log: https://blog.csdn.net/a18792721831/article/details/149862528
MySQL Undo Log: https://blog.csdn.net/a18792721831/article/details/149880355
MySQL 索引算法: https://blog.csdn.net/a18792721831/article/details/149883014
MySQL 索引类型: https://blog.csdn.net/a18792721831/article/details/150275404
MySQL 索引优化: https://blog.csdn.net/a18792721831/article/details/150282163
MySQL 锁: https://blog.csdn.net/a18792721831/article/details/154197322
MySQL 事务
事务(Transaction) 是数据库操作的最小工作单元,也是一组操作的集合。事务是一个逻辑概念,事务中的操作要么全部成功,要么全部失败。事务即是区分文件系统和数据库的重要特征之一,也是一个关系型数据库的灵魂所在。
事务的特性
事务一般具有(ACID)特性:
- A(Atomicity):原子性,一个事务要么全部成功,要么全部失败。
- C(Consistency):一致性,事务的执行不能破坏数据库的完整性和一致性,事务执行前后的数据库必须处于一致的状态。
- I(Isolation):隔离性,隔离性一般指的是在并发环境下,多个事务之间相互隔离,一个事务的执行不能被其他事务破坏或干扰。
- D(Durability):持久性,事务一旦提交,那么对数据库中的数据的改变就是永久性的,即使发生断点等宕机事故也能恢复数据。
事务的实现
MySQL InnoDB中的事务完全符合 ACID 的特性。
InnoDB中的事务实现需要3个工具,日志文件、锁、MVCC。
- 日志文件:包含 Redo Log 和 Undo Log,记录了数据修改前后的日志。
- 锁:锁在事务中主要用来实现隔离和并发。
- MVCC:中文全称为多版本并发控制,主要通过行数据中两个隐藏的字段(事务ID和回滚指针)来实现。
原子性的实现
原子性是指事务中的所有操作要么全部成功,要么全部失败回滚。
InnoDB通过 Undo Log 来实现原子性。当事务对数据进行修改时,InnoDB会生成对应的 Undo Log。如果事务执行失败或者调用了 ROLLBACK,就可以利用 Undo Log 中的信息将数据回滚到修改之前的状态。
Undo Log 记录了数据修改前的值,回滚时按照 Undo Log 的记录进行逆向操作:
- 对于 INSERT 操作,Undo Log 记录主键信息,回滚时执行 DELETE
- 对于 DELETE 操作,Undo Log 记录整行数据,回滚时执行 INSERT
- 对于 UPDATE 操作,Undo Log 记录修改前的值,回滚时执行反向 UPDATE
举个例子,创建一个账户表:
sql
CREATE DATABASE IF NOT EXISTS test_tx;
USE test_tx;
DROP TABLE IF EXISTS account;
CREATE TABLE account (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
balance INT NOT NULL DEFAULT 0
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO account(name, balance) VALUES('张三', 1000), ('李四', 1000);
执行一个转账事务,然后回滚:
sql
USE test_tx;
BEGIN;
UPDATE account SET balance = balance - 500 WHERE name = '张三';
UPDATE account SET balance = balance + 500 WHERE name = '李四';
SELECT * FROM account;
-- 此时张三余额500,李四余额1500
ROLLBACK;
SELECT * FROM account;
-- 回滚后张三余额1000,李四余额1000
执行结果:
-- 事务中查询
id name balance
1 张三 500
2 李四 1500
-- 回滚后查询
id name balance
1 张三 1000
2 李四 1000
可以看到,回滚后数据恢复到了事务开始前的状态。这就是 Undo Log 保证原子性的体现。
Undo Log 的回滚流程:
- 事务开始时,记录当前事务ID
- 每次修改数据前,先将原数据写入 Undo Log
- 如果事务失败或执行 ROLLBACK,根据 Undo Log 逆向恢复数据
- 如果事务提交,Undo Log 不会立即删除(因为可能被 MVCC 使用)
一致性的实现
一致性是事务的最终目标,是指事务执行前后,数据库从一个一致性状态转换到另一个一致性状态。
说白了,一致性是一个综合性的保障,它依赖于原子性、隔离性和持久性的共同作用:
- 原子性保证事务要么全部成功,要么全部失败,不会出现部分执行的情况
- 隔离性保证并发事务之间互不干扰,避免数据异常
- 持久性保证已提交的事务数据不会丢失
除了 AID 的保障,一致性还依赖于:
- 数据库约束:主键约束、外键约束、唯一约束、非空约束、CHECK约束等
- 触发器:在数据变更时自动执行业务规则检查
- 应用层逻辑:业务代码保证数据的业务一致性
举个例子,银行转账场景:
sql
-- 张三转账500给李四
BEGIN;
UPDATE account SET balance = balance - 500 WHERE name = '张三';
UPDATE account SET balance = balance + 500 WHERE name = '李四';
COMMIT;
一致性要求:转账前后,张三和李四的余额总和不变。
- 如果事务执行成功,两条 UPDATE 都执行,总和不变
- 如果事务执行失败,两条 UPDATE 都回滚,总和不变
- 不会出现只执行一条 UPDATE 的情况
隔离性的实现
隔离性是指在并发环境下,多个事务之间相互隔离,一个事务的执行不能被其他事务干扰。
InnoDB通过 锁 和 MVCC 两种机制来实现隔离性:
- 锁机制:通过共享锁(S锁)、排它锁(X锁)、意向锁、间隙锁、临键锁等实现写-写冲突的隔离
- MVCC机制:通过多版本并发控制实现读-写不冲突,提高并发性能
锁机制在之前的文章中已经详细介绍过,这里重点讲 MVCC。
不同的隔离级别,隔离性的实现方式不同:
| 隔离级别 | 实现方式 |
|---|---|
| 读未提交(READ UNCOMMITTED) | 不加锁,直接读取最新数据 |
| 读已提交(READ COMMITTED) | MVCC,每次读取生成新的 ReadView |
| 可重复读(REPEATABLE READ) | MVCC,事务开始时生成 ReadView,整个事务复用 |
| 串行化(SERIALIZABLE) | 加锁,读加共享锁,写加排它锁 |
持久性的实现
持久性是指事务一旦提交,对数据库的修改就是永久性的,即使发生宕机等故障也能恢复。
InnoDB通过 Redo Log 来实现持久性。
MySQL采用 WAL(Write-Ahead Logging) 技术,先写日志再写磁盘:
- 事务修改数据时,先将修改记录写入 Redo Log Buffer
- 事务提交时,将 Redo Log Buffer 刷新到磁盘的 Redo Log 文件
- 后台线程异步将脏页刷新到数据文件
这样即使在数据页刷盘之前发生宕机,也可以通过 Redo Log 恢复数据。
Redo Log 的落盘策略由 innodb_flush_log_at_trx_commit 参数控制:
- 0:每秒刷新一次,可能丢失1秒数据
- 1:每次事务提交都刷新,最安全但性能最低
- 2:每次提交写入OS缓存,每秒刷新到磁盘
sql
SHOW VARIABLES LIKE 'innodb_flush_log_at_trx_commit';
生产环境建议设置为1,保证数据不丢失。
MVCC实现
什么是MVCC
MVCC(Multi-Version Concurrency Control),多版本并发控制,是一种并发控制的方法。
MVCC的核心思想是:为每一行数据维护多个版本,读操作读取的是数据的快照版本,写操作创建新的版本。这样读写操作就不会相互阻塞,大大提高了并发性能。
MVCC 主要用于实现 读已提交(RC) 和 可重复读(RR) 两种隔离级别。
隐藏字段
InnoDB 在每行数据后面添加了三个隐藏字段:
| 字段名 | 大小 | 说明 |
|---|---|---|
| DB_TRX_ID | 6字节 | 最近修改该行的事务ID |
| DB_ROLL_PTR | 7字节 | 回滚指针,指向 Undo Log 中该行的上一个版本 |
| DB_ROW_ID | 6字节 | 隐藏主键,如果表没有定义主键,则使用该字段 |
其中 DB_TRX_ID 和 DB_ROLL_PTR 是 MVCC 实现的关键。
版本链
每次对数据进行修改时,InnoDB 会:
- 将修改前的数据写入 Undo Log
- 更新数据行的 DB_TRX_ID 为当前事务ID
- 更新数据行的 DB_ROLL_PTR 指向 Undo Log 中的旧版本
这样,通过 DB_ROLL_PTR 就可以找到该行数据的历史版本,形成一条版本链。
举个例子,假设有一行数据:
id=1, name='张三', balance=1000
DB_TRX_ID=100, DB_ROLL_PTR=null
事务200修改 balance 为 800:
当前行:id=1, name='张三', balance=800, DB_TRX_ID=200, DB_ROLL_PTR -> Undo Log
Undo Log:id=1, name='张三', balance=1000, DB_TRX_ID=100, DB_ROLL_PTR=null
事务300修改 balance 为 500:
当前行:id=1, name='张三', balance=500, DB_TRX_ID=300, DB_ROLL_PTR -> Undo Log(200)
Undo Log(200):id=1, name='张三', balance=800, DB_TRX_ID=200, DB_ROLL_PTR -> Undo Log(100)
Undo Log(100):id=1, name='张三', balance=1000, DB_TRX_ID=100, DB_ROLL_PTR=null
这就形成了一条版本链:500(300) -> 800(200) -> 1000(100)
ReadView
ReadView 是 MVCC 实现的核心,用于判断版本链中哪个版本对当前事务可见。
ReadView 包含以下关键信息:
| 字段 | 说明 |
|---|---|
| m_ids | 生成 ReadView 时,当前系统中活跃的事务ID列表 |
| min_trx_id | m_ids 中的最小值 |
| max_trx_id | 生成 ReadView 时,系统应该分配给下一个事务的ID |
| creator_trx_id | 创建该 ReadView 的事务ID |
ReadView 的生成时机:
- 读已提交(RC):每次 SELECT 都生成新的 ReadView
- 可重复读(RR):事务第一次 SELECT 时生成 ReadView,后续复用
可见性判断
当事务读取某行数据时,需要判断版本链中哪个版本对当前事务可见。判断规则如下:
-
如果
DB_TRX_ID == creator_trx_id,说明是当前事务修改的,可见 -
如果
DB_TRX_ID < min_trx_id,说明该版本在 ReadView 生成前已提交,可见 -
如果
DB_TRX_ID >= max_trx_id,说明该版本在 ReadView 生成后才开始,不可见 -
如果
min_trx_id <= DB_TRX_ID < max_trx_id:- 如果 DB_TRX_ID 在 m_ids 中,说明该事务还未提交,不可见
- 如果 DB_TRX_ID 不在 m_ids 中,说明该事务已提交,可见
如果当前版本不可见,则通过 DB_ROLL_PTR 找到上一个版本,继续判断,直到找到可见的版本或者版本链结束。
举个例子:
假设当前有三个事务:
- 事务100:已提交
- 事务200:活跃中
- 事务300:活跃中
事务300执行 SELECT,生成 ReadView:
- m_ids = [200, 300]
- min_trx_id = 200
- max_trx_id = 301
- creator_trx_id = 300
对于版本链 500(300) -> 800(200) -> 1000(100):
- 版本500,DB_TRX_ID=300,等于 creator_trx_id,可见(如果是事务300自己修改的)
- 如果是其他事务读取,DB_TRX_ID=300 在 m_ids 中,不可见,继续找下一个版本
- 版本800,DB_TRX_ID=200 在 m_ids 中,不可见,继续找下一个版本
- 版本1000,DB_TRX_ID=100 < min_trx_id,可见
所以其他事务读取到的是 balance=1000。
普通读和当前读
普通读
普通读也叫快照读 或一致性读,是指不加锁的 SELECT 语句。
sql
SELECT * FROM account WHERE id = 1;
普通读通过 MVCC 机制读取数据的快照版本,不会加任何锁,读写不冲突。
在不同隔离级别下的表现:
- 读已提交(RC):每次读取都生成新的 ReadView,可能读到其他事务已提交的修改
- 可重复读(RR):事务内复用同一个 ReadView,保证多次读取结果一致
当前读
当前读是指读取数据的最新版本,并且会对读取的数据加锁。
以下语句都是当前读:
sql
-- 共享锁
SELECT * FROM account WHERE id = 1 LOCK IN SHARE MODE;
SELECT * FROM account WHERE id = 1 FOR SHARE; -- MySQL 8.0
-- 排它锁
SELECT * FROM account WHERE id = 1 FOR UPDATE;
-- DML语句(隐式当前读)
UPDATE account SET balance = 500 WHERE id = 1;
DELETE FROM account WHERE id = 1;
INSERT INTO account(name, balance) VALUES('王五', 1000);
当前读会读取最新的已提交数据,并根据语句类型加相应的锁:
LOCK IN SHARE MODE/FOR SHARE:加共享锁(S锁)FOR UPDATE/ DML语句:加排它锁(X锁)
当前读和普通读的区别:
| 特性 | 普通读 | 当前读 |
|---|---|---|
| 读取版本 | 快照版本 | 最新版本 |
| 是否加锁 | 不加锁 | 加锁 |
| 实现机制 | MVCC | 锁机制 |
| 并发性能 | 高 | 较低 |
举个例子验证:
sql
-- Session 1
USE test_tx;
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
-- 不提交
-- Session 2
USE test_tx;
BEGIN;
-- 普通读,读取快照版本
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000(读取的是修改前的版本)
-- 当前读,会阻塞等待 Session 1 释放锁
SELECT * FROM account WHERE id = 1 FOR UPDATE;
-- 阻塞中...
隔离级别
MySQL InnoDB 支持四种隔离级别,默认是可重复读(REPEATABLE READ)。
sql
-- 查看当前隔离级别
SHOW VARIABLES LIKE 'transaction_isolation';
-- 或者
SELECT @@transaction_isolation;
读未提交
读未提交(READ UNCOMMITTED) 是最低的隔离级别。
特点:
- 事务可以读取其他事务未提交的数据(脏读)
- 不使用 MVCC,直接读取最新数据
- 并发性能最高,但数据一致性最差
sql
-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
脏读示例:
sql
-- Session 1
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
-- 不提交
-- Session 2 (READ UNCOMMITTED)
BEGIN;
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 500(读到了未提交的数据,脏读)
-- Session 1
ROLLBACK;
-- Session 2
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000(数据又变回去了)
读已提交
读已提交(READ COMMITTED) 是大多数数据库的默认隔离级别(但不是MySQL的默认)。
特点:
- 只能读取其他事务已提交的数据,解决了脏读
- 每次 SELECT 都生成新的 ReadView
- 可能出现不可重复读(同一事务内多次读取结果不同)
sql
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
不可重复读示例:
sql
-- Session 1
BEGIN;
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000
-- Session 2
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
COMMIT;
-- Session 1
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 500(同一事务内,两次读取结果不同)
COMMIT;
可重复读
可重复读(REPEATABLE READ) 是 MySQL InnoDB 的默认隔离级别。
特点:
- 事务内多次读取同一数据结果一致,解决了不可重复读
- 事务开始时生成 ReadView,整个事务复用
- InnoDB 通过间隙锁解决了幻读问题
sql
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
可重复读示例:
sql
-- Session 1
BEGIN;
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000
-- Session 2
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
COMMIT;
-- Session 1
SELECT * FROM account WHERE id = 1;
-- 结果:balance = 1000(仍然是1000,可重复读)
COMMIT;
串行化
串行化(SERIALIZABLE) 是最高的隔离级别。
特点:
- 所有事务串行执行,完全隔离
- 读加共享锁,写加排它锁
- 并发性能最低,但数据一致性最高
sql
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
在串行化级别下,普通的 SELECT 也会加共享锁:
sql
-- Session 1 (SERIALIZABLE)
BEGIN;
SELECT * FROM account WHERE id = 1;
-- 自动加共享锁
-- Session 2
BEGIN;
UPDATE account SET balance = 500 WHERE id = 1;
-- 阻塞,等待 Session 1 释放锁
四种隔离级别对比:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交 | 可能 | 可能 | 可能 |
| 读已提交 | 不可能 | 可能 | 可能 |
| 可重复读 | 不可能 | 不可能 | InnoDB不可能 |
| 串行化 | 不可能 | 不可能 | 不可能 |
总结
MySQL InnoDB 的事务实现是一个非常精妙的设计,通过 Redo Log、Undo Log、锁和 MVCC 四大机制协同工作,实现了完整的 ACID 特性。
- 原子性:通过 Undo Log 实现,事务失败时可以回滚到修改前的状态
- 一致性:是事务的最终目标,由 AID 共同保障,加上数据库约束和业务逻辑
- 隔离性:通过锁和 MVCC 实现,不同隔离级别有不同的实现策略
- 持久性:通过 Redo Log 实现,采用 WAL 技术保证数据不丢失
MVCC 是 InnoDB 实现高并发的关键技术,通过版本链和 ReadView 实现了读写不冲突。理解 MVCC 的工作原理,对于理解 MySQL 的并发控制和排查并发问题非常重要。
在实际开发中,需要根据业务场景选择合适的隔离级别:
- 对数据一致性要求不高,追求性能:读已提交
- 大多数场景:可重复读(MySQL默认)
- 对数据一致性要求极高:串行化
同时也要注意普通读和当前读的区别,在需要读取最新数据或者需要加锁保护的场景,使用当前读(FOR UPDATE / LOCK IN SHARE MODE)。
以后还需要继续努力。加油!