🦄 个人主页 : 小米里的大麦-CSDN博客
🎏 所属专栏 : MySQL_小米里的大麦的博客-CSDN博客
🎁 GitHub主页 : 小米里的大麦的 GitHub

文章目录
- [MySQL 事务](#MySQL 事务)
-
- [1. 什么是事务?](#1. 什么是事务?)
- [2. 事务的版本支持](#2. 事务的版本支持)
- [3. 事务的提交方式](#3. 事务的提交方式)
-
- [1. 查看事务的提交方式](#1. 查看事务的提交方式)
- [2. 设置事务的提交方式](#2. 设置事务的提交方式)
- [3. 注意事项](#3. 注意事项)
- [4. 示例](#4. 示例)
-
- [1. Savepoint 和 Rollback](#1. Savepoint 和 Rollback)
- [2. autocommit = 0 不提交就退出](#2. autocommit = 0 不提交就退出)
- [4. 事务隔离级别](#4. 事务隔离级别)
-
- [1. 什么是事务隔离级别](#1. 什么是事务隔离级别)
- [2. 查看当前隔离级别](#2. 查看当前隔离级别)
- [3. 设置事务隔离级别](#3. 设置事务隔离级别)
- [4. 展开讨论](#4. 展开讨论)
-
- [1. 环境准备](#1. 环境准备)
- [2. 读未提交](#2. 读未提交)
- [3. 读已提交](#3. 读已提交)
- [4. 可重复读](#4. 可重复读)
- [5. 串行化](#5. 串行化)
- [6. 小结](#6. 小结)
- [5. 一致性](#5. 一致性)
- [6. 多版本并发控制(MVCC)](#6. 多版本并发控制(MVCC))
-
- [1. 三个隐藏字段(InnoDB 每条记录都有)](#1. 三个隐藏字段(InnoDB 每条记录都有))
- [2. undo log:回滚日志](#2. undo log:回滚日志)
- [3. 快照](#3. 快照)
- [4. Read View:读视图](#4. Read View:读视图)
- [5. RR 和 RC 的本质区别](#5. RR 和 RC 的本质区别)
- 共勉
MySQL 事务
1. 什么是事务?
事务就是"要么全做,要么全不做"的一组数据库操作,可以类比以前学过的原子操作。 打个比方:银行转账。假设要给朋友转账 100 块钱,这在数据库里其实分两步走:
- 自己的账户余额减去 100 元。
- 朋友的账户余额加上 100 元。
如果没有事务会怎样? 万一第一步执行完了(自己扣了钱),突然停电了、断网了,第二步没执行。结果就是:自己的钱没了,朋友也没收到钱,这 100 块凭空消失了。这肯定不行!
有了事务之后: 我们把这两步打包成一个"事务"。
- 如果两步都成功,我们就 提交,转账生效。
- 如果中间出错了,我们就 回滚,数据库自动恢复到转账之前的样子,就像什么都没发生过一样。
事务有四大特性(ACID),我们只需要记住核心两点:
- 原子性: 像原子一样不可分割。要么全成功,要么全失败回滚。
- 一致性: 数据库总是从一个一致状态变到另一个一致状态。转账前后,总金额是不变的。
事务四大特性 ACID:
- A(Atomic)原子性:要么全做,要么全不做。
- C(Consistency)一致性:操作前后数据规则不破坏。
- I(Isolation)隔离性:多个事务同时做,互不影响。
- D(Durability)持久性:提交后的数据,不会丢。
2. 事务的版本支持
在 MySQL 中,并不是所有的存储引擎都支持事务。最常用的两个引擎是:
- InnoDB: 支持事务(这也是 MySQL 8.0 的默认引擎,也是我们最常用的)。
- MyISAM: 不支持事务(以前的老引擎,适合只读场景,现在用得少)。
如何查看当前数据库支持哪些事务引擎?
sql
mysql> show engines;
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| Engine | Support | Comment | Transactions | XA | Savepoints |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
| ARCHIVE | YES | Archive storage engine | NO | NO | NO |
| BLACKHOLE | YES | /dev/null storage engine (anything you write to it disappears) | NO | NO | NO |
| MRG_MYISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
| FEDERATED | NO | Federated MySQL storage engine | NULL | NULL | NULL |
| MyISAM | YES | MyISAM storage engine | NO | NO | NO |
| PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
| InnoDB | DEFAULT | Supports transactions, row-level locking, and foreign keys | YES | YES | YES |
| MEMORY | YES | Hash based, stored in memory, useful for temporary tables | NO | NO | NO |
| CSV | YES | CSV storage engine | NO | NO | NO |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
mysql> show engines\G
*************************** 1. row ***************************
Engine: ARCHIVE
Support: YES
Comment: Archive storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 2. row ***************************
Engine: BLACKHOLE
Support: YES
Comment: /dev/null storage engine (anything you write to it disappears)
Transactions: NO
XA: NO
Savepoints: NO
*************************** 3. row ***************************
Engine: MRG_MYISAM
Support: YES
Comment: Collection of identical MyISAM tables
Transactions: NO
XA: NO
Savepoints: NO
*************************** 4. row ***************************
Engine: FEDERATED
Support: NO
Comment: Federated MySQL storage engine
Transactions: NULL
XA: NULL
Savepoints: NULL
*************************** 5. row ***************************
Engine: MyISAM
Support: YES
Comment: MyISAM storage engine
Transactions: NO
XA: NO
Savepoints: NO
*************************** 6. row ***************************
Engine: PERFORMANCE_SCHEMA
Support: YES
Comment: Performance Schema
Transactions: NO
XA: NO
Savepoints: NO
*************************** 7. row ***************************
Engine: InnoDB
Support: DEFAULT
Comment: Supports transactions, row-level locking, and foreign keys
Transactions: YES
XA: YES
Savepoints: YES
*************************** 8. row ***************************
Engine: MEMORY
Support: YES
Comment: Hash based, stored in memory, useful for temporary tables
Transactions: NO
XA: NO
Savepoints: NO
*************************** 9. row ***************************
Engine: CSV
Support: YES
Comment: CSV storage engine
Transactions: NO
XA: NO
Savepoints: NO
真正能看事务支持的,是 Transactions 列,它显示 YES/NO/NULL。在上面输出里:
- InnoDB → Transactions: YES → 支持事务
- 其他大部分 → Transactions: NO → 不支持事务
- FEDERATED → Transactions: NULL → 不可用
总之:Transactions 列 YES = 支持事务,NO/NULL = 不支持。
其他字段含义(了解):
- Engine: 引擎名。
- Support: 这引擎能不能用。YES 能用,DEFAULT 默认用它,NO 不能用。
- Comment: 备注,简单说说这引擎干嘛的。
- Transactions: 支不支持事务。YES 支持,NO 不支持。
- XA: 支不支持分布式事务。一般开发不用管。
- Savepoints: 支不支持保存点。事务里能回滚到中间某一步。
3. 事务的提交方式
事务的提交方式分为两种:
- 自动提交: 每写一条 SQL(比如
INSERT),数据库立马悄悄帮我们"保存",这是默认模式。 - 手动提交: 每写一条 SQL,必须手动输入"保存"命令,数据才会真正落盘。
1. 查看事务的提交方式
输入以下命令查看当前的状态(一般默认开):
sql
SHOW VARIABLES LIKE 'autocommit'; # 返回 ON 就是自动提交开,OFF 就是关
SELECT @@autocommit; # 返回 1 就是自动提交开,0 是关
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
mysql> select @@autocommit;
+--------------+
| @@autocommit |
+--------------+
| 1 |
+--------------+
结果解读:
- Value 为 ON :表示开启了 自动提交。每一条 SQL 语句都被当成一个独立的事务,执行完立刻生效。
- Value 为 OFF :表示关闭了自动提交。你必须显式地写
COMMIT;才能保存数据。
2. 设置事务的提交方式
场景 A:想临时关闭自动提交(手动事务模式)
有时候我们需要一次性执行多条 SQL,必须成功了一起保存。这时就要关闭自动提交。
-
设置命令:
sqlSET autocommit = 0; # 或者写成 OFF # START TRANSACTION; # 开始事务执行完这条命令后,当前连接的窗口就进入了"手动模式"。
-
操作流程(经典三部曲):
sql# 1. 显式开启事务(虽然关闭了autocommit后默认就在事务中,但为了规范,建议写上) START TRANSACTION; # 2. 执行自己的 SQL 语句 UPDATE account SET balance = balance - 100 WHERE name = '我'; UPDATE account SET balance = balance + 100 WHERE name = '朋友'; # 3. 确认没问题,手动提交(事务) COMMIT; # 如果中间发现问题,想反悔,可以用: # ROLLBACK; # 回滚事务
场景 B:想恢复自动提交
-
设置命令:
sqlSET autocommit = 1; # 或者写成 ON
3. 注意事项
- DDL 语句会隐式提交: 即便开了事务 (
autocommit = 0),只要敲了CREATE、ALTER、DROP、TRUNCATE,MySQL 会 强制 COMMIT 自动提交 当前的事务,之前的操作全被焊死了,回滚不了。 - 退出连接自动提交: 崩了/断网:必然回滚(ROLLBACK);正常退出(exit):不同工具表现不同,可能提交也可能回滚。永远记住:后端开发里,没写
COMMIT的一律当没存成功!
4. 示例
用一个 银行账户转账 的场景来演示,先创建一张表:
sql
mysql> create database transactions;
mysql> use transactions;
mysql> create table test1 (
-> id int primary key,
-> name varchar(20),
-> balance decimal(10,2)
-> );
mysql> insert into test1 values
-> (1, '张三', 1000),
-> (2, '李四', 2000),
-> (3, '王五', 3000);
Query OK, 3 rows affected (0.03 sec)
Records: 3 Duplicates: 0 Warnings: 0
1. Savepoint 和 Rollback
场景:张三给李四转账 100 元,然后李四给王五转账 200 元。但后来发现李四给王五的转账有问题,要撤销这一步,保留第一步。
sql
# 1. 开启事务
mysql> start transaction;
# 2. 张三给李四转账100元
mysql> update test1 set balance = balance - 100 where id = 1;
mysql> update test1 set balance = balance + 100 where id = 2;
# 3. 打个保存点(存档)
mysql> savepoint step1;
# 4. 查看当前状态
mysql> select * from test1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 2100.00 |
| 3 | 王五 | 3000.00 |
+----+--------+---------+
# 结果:张三900,李四2100,王五3000
# 5. 李四给王五转账200元
mysql> update test1 set balance = balance - 200 where id = 2;
mysql> update test1 set balance = balance + 200 where id = 3;
# 6. 再次查看
mysql> select * from test1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 1900.00 |
| 3 | 王五 | 3200.00 |
+----+--------+---------+
# 结果:张三900,李四1900,王五3200
# 7. 发现问题,回滚到保存点(撤销李四给王五的转账)
mysql> rollback to step1;
# 8. 查看回滚后的状态
select * from test1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 2100.00 |
| 3 | 王五 | 3000.00 |
+----+--------+---------+
# 结果:张三900,李四2100,王五3000(李四给王五的转账被撤销了)
# 9. 最后提交事务
mysql> commit;
# 10. 最终确认
mysql> select * from test1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 2100.00 |
| 3 | 王五 | 3000.00 |
+----+--------+---------+
# 结果:张三900,李四2100,王五3000
2. autocommit = 0 不提交就退出
继续上面的账户表,演示关闭自动提交后,不 commit 就退出的后果。第一个终端窗口(会话 1):
sql
# 1. 关闭自动提交
mysql> set autocommit=0;
# 2. 确认已关闭
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
# 结果:Value = OFF
# 3. 查看当前数据
mysql> select * from test1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 2100.00 |
| 3 | 王五 | 3000.00 |
+----+--------+---------+
# 结果:张三900,李四2100,王五3000
# 4. 王五给张三转账500元
mysql> update test1 set balance=balance-500 where id=3;
mysql> update test1 set balance=balance+500 where id=1;
# 5. 查看当前会话的结果
mysql> select * from test1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 1400.00 |
| 2 | 李四 | 2100.00 |
| 3 | 王五 | 2500.00 |
+----+--------+---------+
# 结果:张三1400,李四2100,王五2500
# 注意:这个变化只有当前会话能看到
# 6. 【关键】不执行 COMMIT,直接退出
mysql> quit
Bye
第二个终端窗口(会话 2):
sql
# 在会话1退出的同时或之后,立即查询
mysql> select * from test1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 2100.00 |
| 3 | 王五 | 3000.00 |
+----+--------+---------+
# 结果:张三900,李四2100,王五3000,王五给张三的转账根本没生效!
重新连接后的验证(会话 1 重新登录):
sql
-- 重新登录 MySQL
mysql -u root -p
mysql> use transactions;
# 查询数据
mysql> select * from test1;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 900.00 |
| 2 | 李四 | 2100.00 |
| 3 | 王五 | 3000.00 |
+----+--------+---------+
# 结果:张三900,李四2100,王五3000,刚才的转账操作全部丢失!
正确的做法:
sql
# 1. 关闭自动提交
mysql> set autocommit=0;
# 2. 执行转账
mysql> update test1 set balance=balance-500 where id=3;
mysql> update test1 set balance=balance+500 where id=1;
# 3. 【关键】提交事务
mysql> commit;
# 4. 退出
mysql> quit
重新登录后查询,这次数据会永久保存。所以(保证数据的一致性和安全性):
-
Savepoint:事务中的 "存档点",可以部分回滚。
-
autocommit = 0: 关闭自动提交后,必须手动 COMMIT,否则:退出 MySQL 时自动回滚、其他会话看不到之前的修改、数据不会持久化!
4. 事务隔离级别
1. 什么是事务隔离级别
隔离级别是 MySQL 控制并发事务同时操作同一数据时,是否会出现脏读、不可重复读、幻读等异常的规则,MySQL 有四种隔离级别(从宽松到严格):
| 隔离级别 | 特点 | 说明 |
|---|---|---|
| READ UNCOMMITTED(读未提交) | 最宽松,事务可以看到别的事务未提交的数据 | 脏读、不可重复读、幻读、并发性能最高、安全性最低,基本不用 |
| READ COMMITTED(读提交) | 只能看到别的事务提交后的数据 | 不可重复读、幻读、高并发性能,Oracle、SQL Server 默认,只能读别人已提交的数据 |
| REPEATABLE READ(可重复读) | 同一事务里多次读取结果一样 | 幻读,中并发性能、InnoDB 默认,确保同一事务内读取数据一致 |
| SERIALIZABLE(串行化) | 最严格,事务一个个排队执行 | 安全性最高,性能最差。相当于"排队"执行,并发极低 |
隔离级别越低:并发能力通常越强、出错概率越高。
隔离级别越高:数据通常越稳、等待和锁冲突通常越多。
简记:越严格越安全,越慢;越宽松越快,可能看到脏数据!
- 脏读: 读到了别人"还没提交"的数据。如果别人回滚了,自己读到的就是脏数据。
- 不可重复读: 在同一个事务里,两次读取同一条数据,结果不一样。因为中间被别人 修改并提交 了。
- 幻读: 在同一个事务里,两次查询的数据 条数 不一样。因为中间被别人 插入或删除 了数据。
2. 查看当前隔离级别
查看全局隔离级别(影响新连接):
sql
# MySQL 8.0+ 新写法
mysql> SELECT @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ |
+--------------------------------+
mysql> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+
查看当前会话隔离级别(只影响当前连接):
sql
mysql> SELECT @@transaction_isolation;
+-------------------------+
| @@transaction_isolation |
+-------------------------+
| REPEATABLE-READ |
+-------------------------+
mysql> SELECT @@session.transaction_isolation;
+---------------------------------+
| @@session.transaction_isolation |
+---------------------------------+
| REPEATABLE-READ |
+---------------------------------+
3. 设置事务隔离级别
设置当前会话(临时):
sql
# SET SESSION TRANSACTION ISOLATION LEVEL 隔离级别
mysql> set session transaction isolation level READ COMMITTED;
mysql> select @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| REPEATABLE-READ |
+--------------------------------+
设置全局(影响新连接,老连接不会变):
sql
# SET GLOBAL TRANSACTION ISOLATION LEVEL 隔离级别;
mysql> set global transaction isolation level read committed;
# 验证:需要新开一个窗口查看
mysql> SELECT @@global.transaction_isolation;
+--------------------------------+
| @@global.transaction_isolation |
+--------------------------------+
| READ-COMMITTED |
+--------------------------------+
设置全局隔离级别会影响后续的新会话,当前会话的隔离级别不会发生变化,要让当前会话的隔离级别也改变,需要重启会话。
4. 展开讨论
1. 环境准备
先开 2 个终端,并初始化环境:
sql
# 建库
mysql> CREATE DATABASE IF NOT EXISTS tx_demo;
mysql> USE tx_demo;
# InnoDB 是 MySQL 8 默认事务引擎,这里写出来更直观
mysql> CREATE TABLE demo (
-> id BIGINT PRIMARY KEY,
-> owner_name VARCHAR(20) NOT NULL,
-> balance DECIMAL(10,2) NOT NULL,
-> KEY idx_balance (balance)
-> ) ENGINE=InnoDB;
# 初始数据
mysql> INSERT INTO demo (id, owner_name, balance) VALUES
-> (1, 'Alice', 1000.00),
-> (2, 'Bob', 1000.00);
# 可选:查看mysql版本
mysql> SELECT VERSION();
+-------------------------+
| VERSION() |
+-------------------------+
| 8.0.45-0ubuntu0.24.04.1 |
+-------------------------+
# 初始实验数据
mysql> UPDATE demo
-> SET balance = CASE id
-> WHEN 1 THEN 1000.00
-> WHEN 2 THEN 1000.00
-> END
-> WHERE id IN (1, 2);
mysql> COMMIT;
2. 读未提交
一个事务可以读取到其他事务 尚未提交 的数据,但是会 脏读,也就是说,即使是看到的数据,可能也不会真正落库。
理解: 好比自己写作业,刚写了个草稿还没定稿呢,就被同桌偷看到了,后来自己把草稿擦了,同桌抄的答案全是错的。同桌读到了自己最后回滚了(撤销了)的数据,这就是"脏数据"。
先在 2 个终端都执行:
sql
USE tx_demo;
# 设置当前会话的事务隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
终端 1:
sql
mysql> USE tx_demo;
mysql> START TRANSACTION;
# Alice 扣掉 200 元,但先不提交
mysql> UPDATE demo
-> SET balance = balance - 200.00
-> WHERE id = 1;
# A 自己能看到 800
mysql> SELECT id, owner_name, balance
-> FROM demo
-> WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
| 1 | Alice | 800.00 |
+----+------------+---------+
终端 2:
sql
mysql> USE tx_demo;
mysql> USE tx_demo;
# 按理说 A 还没提交,B 不该看到 800
# 但在 READ UNCOMMITTED 下,B 就是能看到
mysql> SELECT id, owner_name, balance
-> FROM demo
-> WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
| 1 | Alice | 800.00 |
+----+------------+---------+
紧接着终端 1:
sql
mysql> ROLLBACK;
# 此时终端2:
mysql> SELECT id, owner_name, balance FROM demo WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
| 1 | Alice | 1000.00 |
+----+------------+---------+
3. 读已提交
一个事务 只能 读到别的事务 已经提交 的数据,这就解决了脏读。只有别人提交了的数据,你才能看见 ,它解决不了:不可重复读,也就是在同一个事务里,两次查同一行,结果可能不一样。
理解:只看正式版,不看草稿;但正式版可能在查询期间被别人更新。
2 个终端都执行:
sql
USE tx_demo;
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
终端 2 先执行:
sql
mysql> USE tx_demo;
mysql> START TRANSACTION;
# 第一次查 Alice 的余额
mysql> SELECT id, owner_name, balance
-> FROM demo
-> WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
| 1 | Alice | 1000.00 |
+----+------------+---------+
终端 1 执行:
sql
mysql> USE tx_demo;
mysql> START TRANSACTION;
# A 改余额并提交
mysql> UPDATE demo
-> SET balance = balance - 100.00
-> WHERE id = 1;
mysql> COMMIT;
回到终端 2,继续执行:
sql
# B 还在自己的事务里,再查一次同一行
mysql> SELECT id, owner_name, balance
-> FROM demo
-> WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
| 1 | Alice | 900.00 |
+----+------------+---------+
问题来了:
Session B的事务还没结束。- 但它前后两次读同一行,结果变了。
这就是 不可重复读。
4. 可重复读
同一个事务里,多次读取同一行数据,结果要保持一致。事务一开始,数据库像拍了一张快照。只要当前事务没结束,就会一直看这张快照,不管别人怎么改、怎么提交,自己查到的数据永远是事务刚开始时的样子。开始 2 个终端都执行:
sql
USE tx_demo;
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
终端 2 先执行:
sql
mysql> USE tx_demo;
mysql> START TRANSACTION;
# 第一次查
mysql> SELECT id, owner_name, balance
-> FROM demo
-> WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
| 1 | Alice | 1000.00 |
+----+------------+---------+
终端 1 执行:
sql
mysql> USE tx_demo;
mysql> START TRANSACTION;
# A 把 Alice 余额改成 700 并提交
mysql> UPDATE demo
-> SET balance = balance - 300.00
-> WHERE id = 1;
mysql> COMMIT;
终端 2 执行:
sql
mysql> SELECT id, owner_name, balance
-> FROM demo
-> WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
| 1 | Alice | 1000.00 |
+----+------------+---------+
虽然数据库里真实最新值已经变成 700.00,但终端 2 在自己这个事务没结束前,看到的还是事务开始时那份结果,这就是 可重复读 。终端 2:
sql
COMMIT;
# 事务结束后再查,就能看到最新值
mysql> SELECT id, owner_name, balance FROM demo WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
| 1 | Alice | 700.00 |
+----+------------+---------+
理论上该级别会有"幻读"问题(即查询条数不一致),但 MySQL 的 InnoDB 有两套很关键的机制:
MVCC,让普通查询常常读的是一致性快照。Next-Key Lock,在锁定读、更新范围时,会顺手把"间隙"也管起来。已经很好地解决了 大部分 幻读问题,所以课本说 RR 可能幻读,自己用 SELECT 试了半天,也不一定能轻松复现。
5. 串行化
这是最高级别,它强制大家 排队 ,只要有一个人在用这张表(读或写),其他人就得等着。性能差,等待多,锁冲突明显。2 个终端都执行:
sql
USE tx_demo;
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
终端 1 执行:
sql
mysql> USE tx_demo;
mysql> START TRANSACTION;
# A 先查一把
mysql> SELECT id, owner_name, balance
-> FROM demo
-> WHERE id = 1;
+----+------------+---------+
| id | owner_name | balance |
+----+------------+---------+
| 1 | Alice | 600.00 |
+----+------------+---------+
这时先别提交,然后终端 2 执行:
sql
mysql> USE tx_demo;
mysql> START TRANSACTION;
# B 尝试修改同一行
mysql> UPDATE demo
-> SET balance = balance - 50.00
-> WHERE id = 1;
这时会发现:终端 2 卡住了,在等 ,只有当终端 1 执行 COMMIT; 或 ROLLBACK;,终端 2 才会继续。
6. 小结
| 隔离级别 | 英文名称 | 脏读 | 不可重复读 | 幻读 | 性能 | 常用场景 |
|---|---|---|---|---|---|---|
| 读未提交 | Read Uncommitted | ✅ | ✅ | ✅ | 极快 | 几乎不用 |
| 读已提交 | Read Committed | ❌ | ✅ | ✅ | 快 | Oracle/SQL Server 默认 |
| 可重复读 | Repeatable Read | ❌ | ❌ | ❌ (基本解决) | 中 | MySQL 默认,推荐 |
| 串行化 | Serializable | ❌ | ❌ | ❌ | 极慢 | 极度严格的数据一致性要求 |
5. 一致性
一致性就是:事务执行前后,数据库得是 "合法" 的,如何保证?
一致性是事务 ACID 特性中最综合的一个,它依赖于其他三个特性的协同工作,同时也依赖于业务逻辑:
- 原子性保证(要么全做,要么全不做): 如果事务执行中途出错,必须回滚到初始状态,不能留下一半的残缺数据。
- 持久性保证: 一旦提交,数据就 永久保存,系统崩溃也不能丢失结果。
- 隔离性保证: 多个事务并发执行时不乱套,不会互相干扰导致数据错乱(如脏写、脏读)。
- 业务逻辑保证: 数据库只能管技术层面的"对错",管不了业务层面的"逻辑"。例如,转账逻辑写成了"扣款不收款",虽然事务提交成功了,但数据在业务层面上是不一致的。
总之:一致性是原子性、持久性、隔离性加上正确业务逻辑共同作用的结果。
6. 多版本并发控制(MVCC)
- 读-读并发: 没问题,不需要管。
- 写-写并发: 需要加锁(如行锁),保证数据安全。
- 读-写并发: 这是最高频的场景。如果每次读都要加锁,性能会极差。MVCC 就是为了解决这个问题。
传统做法:读的时候加锁,写的时候也加锁。读写互斥,性能差。MVCC 做法:读旧版本,写新版本。读写不互斥,性能高。其 核心思想 是每条数据都存多个 "历史版本",读的时候,根据事务开始时间,挑一个 "能看到" 的版本读,写的时候,创建新版本,不影响别人读旧版本。
实现依赖: 隐藏字段、Undo Log(回滚日志)、Read View(读视图)。
1. 三个隐藏字段(InnoDB 每条记录都有)
虽然用 DESC 看不到,但底层真有:
| 字段 | 大小 | 作用 |
|---|---|---|
DB_TRX_ID |
6 字节 | 事务 ID。记录创建或最近一次修改该行数据的事务 ID,是判断数据可见性的关键 |
DB_ROW_ID |
6 字节 | 隐含主键。如果表没有显式主键,InnoDB 会自动生成这个字段作为聚簇索引的依据 |
DB_ROLL_PTR |
7 字节 | 回滚指针。指向该行记录的上一个版本(存储在 Undo Log 中),通过这个指针能把数据的不同版本串起来 |
2. undo log:回滚日志
MySQL 三大日志:
redo log:崩溃恢复用,保证持久性。bin log:主从同步用,保证一致性。undo log:回滚用,保证原子性 + 隔离性(MVCC)历史版本。
写操作前会把当前数据拷贝到 undo log,这个副本就构成快照,那么版本链啥时候清理?
- 新插入的记录,提交后就能清(没人会读它的历史)
- 修改/删除的记录,要等 所有可能读它历史版本的事务都结束 才能清
- 所以热点数据的 undo log 可能留很久,正常现象
3. 快照
Undo Log 中串联起来的历史数据链表,就是 版本链 。链表中的每一个节点,都是一个 快照。最新数据: 在缓冲池/磁盘的主索引上,历史快照: 在 Undo Log 链表中。
当前读 vs 快照读:
| 类型 | 触发场景 | 加锁吗 | 读哪个版本 |
|---|---|---|---|
| 当前读 | UPDATE/DELETE/SELECT ... FOR UPDATE |
加锁 | 最新版本 |
| 快照读 | 普通 SELECT |
不加锁 | 历史版本(根据 Read View 判断) |
4. Read View:读视图
Read View 是事务进行 快照读 时的"裁判",它决定了当前事务能看到版本链中的哪一个版本。当一个事务执行快照读时,会生成一个 Read View,其中包含生成时刻系统的活跃事务信息。核心字段如下:
m_ids: 生成 Read View 时,当前系统中 活跃(未提交) 的事务 ID 列表。m_up_limit_id(低水位): 活跃事务列表中最小的事务 ID。m_low_limit_id(高水位): 生成 Read View 时,系统预分配给下一个事务的 ID(即当前最大事务 ID + 1)。m_creator_trx_id: 创建该 Read View 的事务 ID。
可见性判断规则(版本匹配算法): 拿着数据行版本中的 DB_TRX_ID,与 Read View 进行比对:
- 如果是自己改的(
DB_TRX_ID == m_creator_trx_id): 可见。 - 如果是很老的事务(
DB_TRX_ID < m_up_limit_id): 说明该版本在 Read View 生成前就已经提交了,可见。 - 如果是未来的事务(
DB_TRX_ID >= m_low_limit_id): 说明该版本是在 Read View 生成后才开启的事务修改的,不可见。 - 如果是在活跃列表范围内(
m_up_limit_id <= DB_TRX_ID < m_low_limit_id):- 再次判断
DB_TRX_ID是否在m_ids列表中:- 在列表中: 说明生成 Read View 时该事务还未提交(活跃),不可见。
- 不在列表中: 说明生成 Read View 时该事务已经提交了,可见。
- 再次判断
如果不满足可见性怎么办? 根据 DB_ROLL_PTR 回滚指针,去找 Undo Log 中的下一个旧版本,再次进行上述判断,直到找到可见版本或链表结束。
5. RR 和 RC 的本质区别
现象:
- RC(读提交):同一个事务里,两次
SELECT可能读到不同结果(别人提交了就能看见) - RR(可重复读):同一个事务里,两次
SELECT结果一样(不管别人提没提交)
本质原因:Read View 创建时机不同!
| 隔离级别 | Read View 创建时机 | 结果 |
|---|---|---|
| RC | 在每条 SELECT 语句开始时生成新的 Read View | 每次都能看见最新已提交的数据 → 不可重复读 |
| RR | 仅第一次快照读创建 Read View,后面复用 | 一直看见第一次读时的快照 → 可重复读 |
举个栗子(RR 级别):
text
T0: 事务 A 开始,第一次 SELECT → 创建 Read View(记录此时活跃事务)
T1: 事务 B 修改并提交数据
T2: 事务 A 第二次 SELECT → 复用 T0 的 Read View → 还是看到旧数据
举个栗子(RC 级别):
text
T0: 事务 A 开始,第一次 SELECT → 创建 Read View1
T1: 事务 B 修改并提交数据
T2: 事务 A 第二次 SELECT → 创建新的 Read View2 → 看到 B 提交的新数据
共勉

