MySQL笔记---事务
1. 什么是事务
在 MySQL 中,事务(Transaction)是一组不可分割的 SQL 操作序列,这些操作要么全部成功执行,要么全部失败回滚,以此保证数据的一致性和完整性。事务是数据库并发控制的基础,广泛用于金融交易、订单处理等需要确保数据准确性的场景。
1.1 事务的 ACID 特性
- 原子性(Atomicity):事务是一个 “原子” 操作单元,所有 SQL 语句要么全部执行成功,要么全部失败(回滚),不存在部分执行的情况。 例:银行转账时,“A 账户扣款” 和 “B 账户到账” 必须同时成功,若任一环节失败,整个操作需回滚到初始状态。
- 一致性(Consistency):事务执行前后,数据库的状态必须从一个 “一致状态” 切换到另一个 “一致状态”(即数据满足预设的约束,如主键唯一、外键关联等)。 例:转账前 A 和 B 的总余额为 1000 元,转账后总余额仍需为 1000 元,不能因事务执行导致总额变化。
- 隔离性(Isolation):多个并发事务之间相互隔离,一个事务的操作不会被其他事务干扰,避免因并发导致的数据错误(如脏读、不可重复读等)。 MySQL 通过 “隔离级别” 控制隔离性的严格程度(见下文)。
- 持久性(Durability):事务一旦提交(COMMIT),其对数据的修改会被永久保存到磁盘,即使数据库崩溃,重启后也能恢复该修改。
1.2 引擎对事务的支持
在MySQL中,只有InnoDB引擎支持事务:
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 |
+--------------------+---------+----------------------------------------------------------------+--------------+------+------------+
9 rows in set (0.01 sec)
2. 事务的操作命令
命令 | 作用 |
BEGIN 或 START TRANSACTION | 显式开启一个事务 |
COMMIT | 提交事务:将事务中所有修改永久保存到数据库 |
ROLLBACK | 回滚事务:放弃事务中所有未提交的修改,恢复到事务开始前的状态 |
SAVEPOINT 保存点名称 | 在事务中创建一个 “保存点”,后续可回滚到该点(而非整个事务) |
ROLLBACK TO 保存点名称 | 回滚到指定保存点(保存点之后的操作被撤销,之前的仍有效) |
RELEASE SAVEPOINT 保存点名称 | 删除指定保存点 |
我们使用如下一张简单的表来作为示例:
CREATE TABLE user(id INT PRIMARY KEY,name VARCHAR(20),balance DECIMAL(6, 2)
);
2.1 直接提交
-- 终端1
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)mysql> INSERT INTO user VALUES (4, '赵六', 4561.23);
Query OK, 1 row affected (0.00 sec)mysql> COMMIT;
Query OK, 0 rows affected (0.01 sec)-- 终端2
-- 提交前
mysql> SELECT * FROM user;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 1234.56 |
| 2 | 李四 | 2345.61 |
| 3 | 王五 | 3456.12 |
+----+--------+---------+
3 rows in set (0.00 sec)-- 提交后
mysql> SELECT * FROM user;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 1234.56 |
| 2 | 李四 | 2345.61 |
| 3 | 王五 | 3456.12 |
| 4 | 赵六 | 4561.23 |
+----+--------+---------+
4 rows in set (0.00 sec)
2.2 全部回滚
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)mysql> INSERT INTO user VALUES (5, '田七', 5612.34);
Query OK, 1 row affected (0.00 sec)mysql> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)
2.3 部分回滚
-- 终端1
mysql> BEGIN;
Query OK, 0 rows affected (0.00 sec)mysql> INSERT INTO user VALUES (4, '赵六', 4561.23);
Query OK, 1 row affected (0.00 sec)mysql> SAVEPOINT id4;
Query OK, 0 rows affected (0.00 sec)mysql> INSERT INTO user VALUES (5, '田七', 5612.34);
Query OK, 1 row affected (0.00 sec)mysql> ROLLBACK TO id4;
Query OK, 0 rows affected (0.00 sec)mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)-- 终端2
-- 提交前
mysql> SELECT * FROM user;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 1234.56 |
| 2 | 李四 | 2345.61 |
| 3 | 王五 | 3456.12 |
+----+--------+---------+
3 rows in set (0.00 sec)-- 提交后
mysql> SELECT * FROM user;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 1234.56 |
| 2 | 李四 | 2345.61 |
| 3 | 王五 | 3456.12 |
| 4 | 赵六 | 4561.23 |
+----+--------+---------+
4 rows in set (0.00 sec)
3. 事务的自动提交
MySQL 默认开启AUTOCOMMIT=1(自动提交),即单条 SQL 语句会被视为独立事务自动提交。
mysql> SHOW VARIABLES LIKE 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.01 sec)
若需显式控制事务,需先执行SET AUTOCOMMIT=0关闭自动提交,或用BEGIN手动开启。
mysql> SET AUTOCOMMIT = 0;
Query OK, 0 rows affected (0.00 sec)mysql> SHOW VARIABLES LIKE 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
1 row in set (0.00 sec)
如果我们将AUTOCOMMIT关闭,此时我们执行的单条SQL语句就不会被自动提交,而需要我们显式执行COMMIT:
-- 终端2
mysql> SELECT * FROM user;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 1234.56 |
| 2 | 李四 | 2345.61 |
| 3 | 王五 | 3456.12 |
| 4 | 赵六 | 4561.23 |
+----+--------+---------+
4 rows in set (0.00 sec)-- 终端1
mysql> INSERT INTO user VALUES (5, '田七', 5612.34);
Query OK, 1 row affected (0.00 sec)-- 终端2
mysql> SELECT * FROM user;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 1234.56 |
| 2 | 李四 | 2345.61 |
| 3 | 王五 | 3456.12 |
| 4 | 赵六 | 4561.23 |
+----+--------+---------+
4 rows in set (0.00 sec)-- 终端1
mysql> COMMIT;
Query OK, 0 rows affected (0.01 sec)-- 终端2
mysql> SELECT * FROM user;
+----+--------+---------+
| id | name | balance |
+----+--------+---------+
| 1 | 张三 | 1234.56 |
| 2 | 李四 | 2345.61 |
| 3 | 王五 | 3456.12 |
| 4 | 赵六 | 4561.23 |
| 5 | 田七 | 5612.34 |
+----+--------+---------+
5 rows in set (0.00 sec)
4. 事务的隔离级别
4.1 并发事务可能导致的问题
- 脏读:事务 A 读取了事务 B 未提交的修改(若 B 后续回滚,A 读取的数据是 “无效” 的)。
- 不可重复读:事务 A 两次读取同一数据,期间事务 B 修改并提交了该数据,导致 A 两次结果不一致。
- 幻读:事务 A 两次执行同一查询,期间事务 B 插入 / 删除了符合条件的记录,导致 A 两次结果集不同。
不可重复读与幻读的区别 对比维度 不可重复读(Non-repeatable Read) 幻读(Phantom Read) 定义 同一事务内,两次读取同一行数据时,因其他事务对该行数据修改并提交,导致两次读取结果不一致。 同一事务内,两次执行相同范围的查询(如 WHERE 条件)时,因其他事务插入 / 删除了符合条件的新记录,导致两次结果集的行数或内容不一致。 数据变化类型 针对已有数据的更新(UPDATE)(如修改某行的字段值)。 针对新数据的插入(INSERT)或已有数据的删除(DELETE)(影响符合查询条件的记录数量)。 产生场景 聚焦于单行数据的内容变化。 聚焦于符合条件的记录范围变化(新增或减少了行)。
4.2 隔离级别
MySQL 定义了 4 种隔离级别(从上到下,隔离性越强,并发性能越差):
隔离级别 | 解决脏读? | 解决不可重复读? | 解决幻读? | 说明 |
读未提交(READ UNCOMMITTED) | ❌ | ❌ | ❌ | 允许读取其他事务未提交的修改,性能最高但安全性最差。 |
读已提交(READ COMMITTED) | ✅ | ❌ | ❌ | 只能读取其他事务已提交的修改,避免脏读(Oracle 默认级别)。 |
可重复读(REPEATABLE READ) | ✅ | ✅ | ✅(InnoDB) | 事务中多次读取同一数据结果一致,InnoDB 通过 MVCC + 间隙锁解决幻读(MySQL 默认级别)。 |
串行化(SERIALIZABLE) | ✅ | ✅ | ✅ | 强制事务串行执行(加表级锁),完全避免并发问题,但性能极低。 |
4.3 隔离级别的查看与设置
在MySQL中,隔离级别由两个变量进行控制:
- 会话隔离级别:决定当前会话的隔离级别。
- 全局隔离级别:用户登录后默认使用全局隔离级别作为会话的隔离级别;
4.3.1 查看隔离级别
(1)MySQL 8.0+
-- 查看当前会话隔离级别
SELECT @@transaction_isolation;
-- 或
SELECT @@session.transaction_isolation;-- 查看全局隔离级别
SELECT @@global.transaction_isolation;
(2)MySQL 5.7及以下
-- 查看当前会话隔离级别
SELECT @@tx_isolation;
-- 或
SELECT @@session.tx_isolation;-- 查看全局隔离级别
SELECT @@global.tx_isolation;
4.3.2 设置隔离级别
-- (1)设置会话隔离级别
SET TRANSACTION ISOLATION LEVEL 隔离级别名称(上表中的英文名称);
-- 例:设置为读已提交
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;-- (2)设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL 隔离级别名称;
5. MVCC
MVCC(Multi-Version Concurrency Control,多版本并发控制)是 InnoDB 存储引擎实现高并发读写的核心机制。它通过保存数据的多个历史版本,让读写操作互不阻塞(读不阻塞写,写不阻塞读),同时保证事务隔离性,是解决 “不可重复读” 等并发问题的关键技术。
MVCC 的核心目标是在并发场景下,解决两大矛盾:
- 读写冲突:避免 “读操作等待写操作释放锁” 或 “写操作等待读操作释放锁”,提升并发效率。
- 隔离性保证:在不加锁的情况下,让不同事务看到对应隔离级别允许的数据版本(如 “可重复读” 级别下,事务内多次读取数据一致)。
例如,在“可重复读”的隔离级别下,即使其他事务对数据进行了修改并提交,我们也希望在当前事务的视角下看到的数据是不变的(看到历史版本的数据)。
这就需要保存并管理历史版本的数据,而InnoDB使用的技术就是MVCC。
5.1 MVCC 的核心组成
MVCC 的实现依赖 3 个关键组件:隐藏列、undo 日志、Read View。
(1)隐藏列(数据行的版本标识)
InnoDB 会为表中的每一行数据添加 3 个隐藏列(默认不显示,用于版本管理):
- DB_TRX_ID:记录最后一次修改该行数据的事务 ID(6 字节)。 (事务 ID 是自增的,新事务的 ID 比旧事务大,可用于判断事务的先后顺序。)
- DB_ROLL_PTR:回滚指针(7 字节),指向该行数据的上一个历史版本(存储在 undo 日志中)。
- DB_ROW_ID:行 ID(6 字节),若表没有主键或唯一索引,InnoDB 会用它生成聚簇索引。
(2)undo 日志(历史版本的存储)
当事务修改数据时,InnoDB 不会直接覆盖旧数据,而是将旧数据写入undo 日志(回滚日志),并通过DB_ROLL_PTR将当前行与 undo 日志中的历史版本串联起来,形成一条版本链。
例如:
- 事务 1(ID=10)插入一行数据,DB_TRX_ID=10,DB_ROLL_PTR=NULL(无历史版本)。
- 事务 2(ID=20)修改该行数据,InnoDB 会将修改前的旧数据写入 undo 日志,然后更新当前行的DB_TRX_ID=20,DB_ROLL_PTR指向 undo 日志中的旧版本(ID=10 的版本)。
- 事务 3(ID=30)再次修改,重复上述过程,版本链会不断延长(当前行 → ID=30 的修改 → ID=20 的修改 → ID=10 的初始版本)。
现在我们将历史版本都组织管理起来了,但问题是,对于某个具体的事务,哪些版本是他可见的呢?
(3)Read View(可见性判断规则)
Read View(读视图)是事务在读取数据时生成的一个 “快照”,它定义了当前事务能看到哪些版本的数据(即哪些历史版本对当前事务 “可见”)。
Read View 包含 4 个核心属性:
- m_ids:当前活跃(未提交)的事务 ID 列表。
- min_trx_id:m_ids中最小的事务 ID(当前活跃事务中最早开始的)。
- max_trx_id:下一个将要分配的事务 ID(大于所有已存在的事务 ID)。
- creator_trx_id:生成该 Read View 的事务自身的 ID。
5.2 MVCC 的工作原理(可见性判断逻辑)
当事务读取一行数据时,InnoDB 会通过 Read View 的规则,从该数据的版本链中筛选出 “可见” 的版本:
- 取版本链中某个版本的DB_TRX_ID(修改该版本的事务 ID),与 Read View 的属性对比。
- 若DB_TRX_ID == creator_trx_id: 说明该版本是当前事务自己修改的,可见。
- 若DB_TRX_ID < min_trx_id: 说明修改该版本的事务在当前事务开始前已提交,可见。
- 若DB_TRX_ID >= max_trx_id: 说明修改该版本的事务在当前事务生成 Read View 后才开始,不可见。
- 若min_trx_id <= DB_TRX_ID < max_trx_id:
- 若DB_TRX_ID在m_ids中(该事务仍活跃未提交):不可见。
- 若DB_TRX_ID不在m_ids中(该事务已提交):可见。
若当前版本不可见,则通过DB_ROLL_PTR回溯到上一个历史版本,重复上述判断,直到找到可见版本或版本链结束(返回空)。
注意:这里的判断逻辑不与某个隔离级别绑定,接下来我们会介绍MVCC与隔离级别的关系。
5.3 MVCC 与隔离级别的关系
MVCC 的行为会根据事务隔离级别调整,核心差异在于Read View 的生成时机:
- 读已提交(READ COMMITTED): 每次执行查询时,都会重新生成一个 Read View。
- 因此,同一事务内两次查询可能看到其他事务已提交的修改(解决脏读,但允许不可重复读)。
- 可重复读(REPEATABLE READ): 仅在事务第一次执行查询时生成 Read View,之后的查询复用该 Read View。
- 因此,同一事务内多次查询看到的是同一版本的数据,不受其他事务提交的修改影响(解决不可重复读)。
5.4 MVCC 的优势
- 读写不阻塞:读操作无需加锁(快照读),写操作仅锁定当前行,避免了传统锁机制中 “读等写、写等读” 的性能损耗。
- 隔离性保证:通过版本链和 Read View,高效实现了 “读已提交” 和 “可重复读” 隔离级别,平衡了一致性和并发性能。
- 支持事务回滚:undo 日志不仅用于存储历史版本,也是事务ROLLBACK时恢复数据的依据。