一、数据库事务基本概念
1.1 事务的定义
事务(Transaction)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。事务是数据库并发控制和恢复的基本单位,具有不可分割的特性------事务中的所有操作要么全部执行成功,要么全部不执行。
在关系型数据库中,事务通常由一条或多条SQL语句组成。以银行转账为例:从账户A扣款100元并向账户B增加100元,这两个操作必须作为一个整体------要么都成功,要么都失败回滚,绝不能出现"钱扣了但没到账"的情况。
1.2 ACID特性
事务的可靠性由ACID四个核心特性来保证:
原子性(Atomicity): 事务是一个不可分割的最小工作单元。事务中的所有操作要么全部完成,要么全部不执行。如果在执行过程中发生错误,系统会将已经执行的操作回滚到事务开始前的状态。
一致性(Consistency): 事务执行前后,数据库必须从一个一致性状态转换到另一个一致性状态。一致性是由业务规则和数据库约束(如主键、外键、唯一约束、CHECK约束等)共同定义的。
隔离性(Isolation): 多个事务并发执行时,每个事务都感觉不到其他事务的存在,仿佛自己是数据库中唯一运行的事务。隔离性通过锁机制和多版本并发控制(MVCC)来实现。
持久性(Durability): 一旦事务被提交,其结果就是永久性的,即使系统发生崩溃也不会丢失。InnoDB通过Redo Log(重做日志)和Double Write Buffer(双写缓冲区)来保证持久性。
1.3 事务的生命周期
一个事务从开始到结束经历以下状态:
- ① 活动状态(Active):事务开始执行,数据库操作正在进行中。
- ② 部分提交状态(Partially Committed):最后一条语句执行完毕,但尚未将结果持久化到磁盘。
- ③ 失败状态(Failed):事务执行过程中发生错误,无法继续正常执行。
- ④ 中止状态(Aborted):事务回滚完成,数据库恢复到事务开始前的状态。
- ⑤ 提交状态(Committed):事务成功完成,所有修改已持久化到磁盘。
在MySQL中,事务的控制语句主要包括:
sql
-- 事务控制
START TRANSACTION; -- 或 BEGIN; -- 开始事务
COMMIT; -- 提交事务
ROLLBACK; -- 回滚事务
-- 保存点控制
SAVEPOINT sp_name; -- 设置保存点
ROLLBACK TO sp_name; -- 回滚到保存点
RELEASE SAVEPOINT sp_name; -- 释放保存点
二、MySQL事务使用实例
2.1 环境准备
以下示例基于MySQL 8.0 + InnoDB引擎。
首先创建测试数据库和账户表:
sql
-- 创建数据库(若不存在)
CREATE DATABASE IF NOT EXISTS transaction_demo;
USE transaction_demo;
-- 创建账户表
CREATE TABLE accounts (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
balance DECIMAL(12,2) NOT NULL DEFAULT 0.00,
CHECK (balance >= 0)
) ENGINE=InnoDB
DEFAULT CHARSET = utf8mb4;
-- 初始化测试数据
INSERT INTO accounts (name, balance) VALUES
('Alice', 1000.00),
('Bob', 500.00),
('Charlie', 2000.00);
2.2 基本事务操作
以下示例展示最基本的事务提交与回滚:
ini
-- ============================================
-- 会话 A:开启事务并修改数据
-- ============================================
START TRANSACTION;
UPDATE accounts
SET balance = balance - 200
WHERE name = 'Alice';
-- 当前会话 A 查询结果:Alice 的 balance = 800
-- 在其他未提交事务的会话中仍看到原值(1000)
-- 体现事务的隔离性(Isolation)
-- ============================================
-- 确认无误后提交事务
-- ============================================
COMMIT;
如果执行过程中发现问题,可以回滚:
ini
-- ============================================
-- 会话 A:开启事务并修改数据
-- ============================================
START TRANSACTION;
UPDATE accounts
SET balance = balance - 200
WHERE name = 'Alice';
-- ============================================
-- 发现业务逻辑错误,决定撤销修改
-- ============================================
ROLLBACK;
-- Alice 的余额恢复为 1000.00
-- 本次事务中的所有更改均被取消
2.3 转账场景示例
这是最经典的银行转账场景------Alice向Bob转账200元。该操作必须保证原子性:扣款和入账要么同时成功,要么同时失败。
正确的事务写法:
sql
START TRANSACTION;
-- 只有余额充足时才扣款
UPDATE accounts
SET balance = balance - 200
WHERE name = 'Alice'
AND balance >= 200;
-- 判断是否真的扣款成功
SELECT ROW_COUNT() INTO @rows;
IF @rows = 0 THEN
ROLLBACK;
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '余额不足,转账失败';
ELSE
UPDATE accounts
SET balance = balance + 200
使用保存点(SAVEPOINT)实现更精细的回滚控制:
sql
DELIMITER $$
CREATE PROCEDURE safe_transfer()
BEGIN
DECLARE alice_balance DECIMAL(12,2);
START TRANSACTION;
-- 保存点:扣款前
SAVEPOINT before_deduct;
UPDATE accounts
SET balance = balance - 200
WHERE name = 'Alice';
SELECT balance INTO alice_balance
FROM accounts
WHERE name = 'Alice';
-- 余额不足,仅回滚扣款操作
IF alice_balance < 0 THEN
ROLLBACK TO before_deduct;
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = '余额不足,转账终止';
ELSE
-- 保存点:入账前
SAVEPOINT before_add;
UPDATE accounts
SET balance = balance + 200
WHERE name = 'Bob';
INSERT INTO transfer_log (from_account, to_account, amount, created_at)
VALUES ('Alice', 'Bob', 200.00, NOW());
COMMIT;
END IF;
END
$$
DELIMITER ;
关键要点:
• InnoDB是MySQL默认存储引擎,支持完整的事务ACID特性。MyISAM引擎不支持事务。
• 在自动提交模式(autocommit=1,默认开启)下,每条SQL语句自动成为一个事务。需显式使用START TRANSACTION开启多语句事务。
• DDL语句(如CREATE TABLE、ALTER TABLE)在MySQL中会隐式提交当前事务,无法回滚。
三、事务隔离级别
3.1 并发事务引发的问题
当多个事务同时操作同一数据时,如果不加以控制,会出现以下三类典型问题:
脏读(Dirty Read): 一个事务读取到另一个未提交事务的修改数据。如果那个事务最终回滚,读取到的就是"脏数据"------这些数据从未真正存在于数据库中。
不可重复读(Non-Repeatable Read): 一个事务内两次读取同一行数据,但两次读取的结果不同。这是因为在两次读取之间,另一个事务修改并提交了该行数据。
幻读(Phantom Read): 一个事务按照相同条件两次查询,第二次查询结果的行数发生了变化(增多或减少)。这是因为另一个事务在此期间插入或删除了满足条件的行。幻读与不可重复读的核心区别:幻读关注的是行数的变化(INSERT/DELETE),不可重复读关注的是同一行内容的变化(UPDATE)。
3.2 四种隔离级别
SQL标准定义了四种事务隔离级别,从低到高依次为:

READ UNCOMMITTED(读未提交): 最低级别。事务可以读取其他事务未提交的修改。存在脏读、不可重复读、幻读问题。几乎不用于生产环境,仅在某些需要非阻塞读的日志/监控场景中有零星应用。
READ COMMITTED(读已提交): 事务只能读取其他事务已提交的数据,解决了脏读问题。但不可重复读和幻读仍然存在。这是Oracle和PostgreSQL的默认隔离级别。
REPEATABLE READ(可重复读): 事务在执行期间看到的数据始终保持一致------同一事务内多次读取同一行数据结果相同。解决了脏读和不可重复读问题,但理论上仍存在幻读。这是MySQL InnoDB的默认隔离级别。
SERIALIZABLE(可串行化): 最高级别。事务完全串行执行,通过强制排序彻底杜绝所有并发问题。代价是并发性能极低,通常只在金融等对数据一致性要求极高的场景中使用。
3.3 隔离级别对比
四种隔离级别与并发问题的关系总结如下:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 默认数据库 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | --- |
| READ COMMITTED | 不可能 | 可能 | 可能 | Oracle、PostgreSQL、SQL Server |
| REPEATABLE READ | 不可能 | 不可能 | 可能(InnoDB实际已解决) | MySQL InnoDB |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | --- |
MySQL中查看和设置隔离级别:
sql
-- ============================================================
-- 查看事务隔离级别
-- ============================================================
-- 查看当前会话的事务隔离级别
SELECT @@transaction_isolation;
-- 查看全局事务隔离级别
SELECT @@global.transaction_isolation;
-- ============================================================
-- 设置事务隔离级别
-- ============================================================
-- 设置当前会话的隔离级别(仅对当前连接生效)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局隔离级别(对所有新建连接生效,不影响已有连接)
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
四、InnoDB隔离级别实现机制
4.1 MVCC多版本并发控制
MVCC(Multi-Version Concurrency Control,多版本并发控制)是InnoDB实现事务隔离的核心机制。其基本思想是:不去阻塞写操作来等待读操作,也不阻塞读操作来等待写操作,而是为每个事务提供数据的一个"快照"版本。
InnoDB的MVCC实现依赖以下关键数据结构:
隐藏列: InnoDB为每行数据隐式添加三个列------
① DB_TRX_ID(6字节):最近一次修改该行的事务ID。
② DB_ROLL_PTR(7字节):回滚指针,指向undo log中该行的上一个版本。
③ DB_ROW_ID(6字节):单调递增的行ID(当表没有主键时作为聚簇索引的键)。
Undo Log(回滚日志): 记录了数据的历史版本。每次对数据进行修改时,InnoDB都会将旧版本的数据记录到undo log中,并通过DB_ROLL_PTR形成一条版本链。事务根据自身的隔离级别,沿着这条版本链找到应该看到的那个版本。
Read View(读视图): 事务在执行快照读时生成的"可见性快照",包含以下关键信息:
① m_ids:生成Read View时,系统中所有活跃(未提交)的事务ID列表。
② min_trx_id:m_ids中的最小值。
③ max_trx_id:生成Read View时系统下一个将分配的事务ID。
④ creator_trx_id:创建该Read View的事务ID。
可见性判断规则:
当要读取一行数据时,InnoDB根据该行的DB_TRX_ID与Read View中的信息进行比较:
• 如果DB_TRX_ID < min_trx_id:说明修改该行的事务在Read View生成前已提交,该版本可见。
• 如果DB_TRX_ID >= max_trx_id:说明修改该行的事务在Read View生成后才开始,该版本不可见,需沿undo log向前查找。
• 如果min_trx_id <= DB_TRX_ID < max_trx_id:需判断DB_TRX_ID是否在m_ids中。如果在,说明事务未提交,不可见;如果不在,说明事务已提交,可见。
4.2 READ UNCOMMITTED的实现
READ UNCOMMITTED是最简单的隔离级别------几乎不做任何并发控制:
• 读操作: 始终读取数据的最新版本,不生成Read View,不检查版本可见性。因此可以读到其他事务尚未提交的修改(脏读)。
• 写操作: 仍然使用行锁,写操作之间互斥,保证不会出现两个事务同时修改同一行导致的数据损坏。
• 锁定读: SELECT ... FOR UPDATE / SELECT ... LOCK IN SHARE MODE 仍然会加锁,按正常锁机制处理。
4.3 READ COMMITTED的实现
READ COMMITTED级别下,每次执行快照读(普通SELECT)时都会生成一个新的Read View。这意味着:
• 每次读取都能看到其他事务在当前时刻之前已提交的最新数据。
• 解决了脏读问题------因为只读已提交版本,永远不会看到未提交的数据。
• 但同一事务内的两次读取可能看到不同的数据(不可重复读),因为第二次读取会生成全新的Read View。
具体流程示例(RC级别下的不可重复读):
ini
-- ============================================================
-- 初始数据
-- ============================================================
-- Alice.balance = 1000
-- ============================================================
-- 事务 A
-- ============================================================
START TRANSACTION;
-- 第一次读取(生成 Read View 1)
SELECT balance
FROM accounts
WHERE name = 'Alice';
-- 结果:1000
-- ============================================================
-- 事务 B(并发执行)
-- ============================================================
START TRANSACTION;
UPDATE accounts
SET balance = 800
WHERE name = 'Alice';
COMMIT;
-- ============================================================
-- 事务 A 再次读取
-- ============================================================
-- 第二次读取(生成新的 Read View 2)
SELECT balance
FROM accounts
WHERE name = 'Alice';
-- 结果:800(不可重复读!)
COMMIT;
对于锁定读(SELECT ... FOR UPDATE),READ COMMITTED使用半一致性读(semi-consistent read):当遇到被锁定的行时,先读取其最新提交版本,如果该版本满足WHERE条件才等待锁释放。这样可以减少不必要的锁等待。
4.4 REPEATABLE READ的实现
REPEATABLE READ是InnoDB的默认隔离级别,也是其实现最为精妙的部分。核心特点:事务在执行第一次快照读时生成一个Read View,此后整个事务期间都使用同一个Read View。
这意味着:
• 同一事务内所有快照读都基于同一个"时间点"的数据快照,保证可重复读。
• 其他事务的提交不会影响当前事务已打开的Read View。
• 解决了不可重复读问题。
具体流程示例(RR级别的可重复读保证):
ini
-- ============================================================
-- 初始数据
-- ============================================================
-- Alice.balance = 1000
-- ============================================================
-- 事务 A
-- ============================================================
START TRANSACTION;
-- 第一次读取:生成 Read View(整个事务唯一)
SELECT balance
FROM accounts
WHERE name = 'Alice';
-- 结果:1000
-- ============================================================
-- 事务 B(并发执行)
-- ============================================================
START TRANSACTION;
UPDATE accounts
SET balance = 800
WHERE name = 'Alice';
COMMIT;
-- ============================================================
-- 事务 A 再次读取
-- ============================================================
-- 复用同一个 Read View
SELECT balance
FROM accounts
WHERE name = 'Alice';
-- 结果:1000(可重复读!)
COMMIT;
幻读问题在InnoDB RR级别下的处理------Next-Key Lock(临键锁):
SQL标准认为REPEATABLE READ无法解决幻读,但InnoDB通过Next-Key Lock机制在很大程度上解决了幻读问题。Next-Key Lock = 记录锁(Record Lock)+ 间隙锁(Gap Lock),是行锁与间隙锁的组合。
• 记录锁(Record Lock):锁定索引中的具体记录,防止其他事务对该行进行UPDATE或DELETE。
• 间隙锁(Gap Lock):锁定索引记录之间的间隙,防止其他事务在该间隙中INSERT新行。
• 临键锁(Next-Key Lock):同时锁定记录和它之前的间隙,形成左开右闭区间,从根本上杜绝幻读。
Next-Key Lock示例:
sql
-- ============================================================
-- 假设 accounts 表 id 列已有记录:1, 5, 10, 20
-- ============================================================
-- 事务 A
START TRANSACTION;
SELECT *
FROM accounts
WHERE id > 10
FOR UPDATE;
-- InnoDB 会添加以下锁:
-- 记录锁(Record Lock):锁定 id = 20
-- 间隙锁(Gap Lock):锁定 (10, 20) 区间
-- 临键锁(Next-Key Lock):(10, 20](左开右闭)
-- 事务 B 在此期间:
-- ❌ 无法在 (10, 20] 区间插入新行
-- ❌ 无法在 (20, +∞) 插入新行
-- ✅ 从而避免幻读(Phantom Read)
-- ⚠️ 重要注意:
-- 如果 WHERE 条件无法使用索引(导致全表扫描),
-- InnoDB 会锁定表中所有记录和所有间隙!
4.5 SERIALIZABLE的实现
SERIALIZABLE是最高隔离级别,实现方式最为严格:
• 所有普通SELECT语句自动转换为SELECT ... LOCK IN SHARE MODE(共享锁),即每读取一行都加共享锁。
• 与MVCC的关系: 在SERIALIZABLE级别下,如果autocommit=0(即使用了显式事务),InnoDB会禁用快照读,所有SELECT都变为锁定读。
• 写操作: 同样使用排他锁(X锁),与共享锁互斥。
• 效果: 通过将所有读操作升级为锁定读,SERIALIZABLE强制事务完全串行执行。一个事务读取某行后,其他事务无法修改该行,必须等待第一个事务提交或回滚。
4.6 锁机制补充
除了MVCC,InnoDB还使用多种锁机制来实现不同隔离级别的要求:
共享锁(S锁 / Shared Lock): 允许多个事务同时读取同一行数据,但不允许任何事务修改被共享锁锁定的行。通过SELECT ... LOCK IN SHARE MODE显式获取,或在SERIALIZABLE级别下自动获取。
排他锁(X锁 / Exclusive Lock): 只允许持有锁的事务读取和修改数据,其他事务既不能读也不能写该行。通过SELECT ... FOR UPDATE或UPDATE/DELETE/INSERT语句获取。
意向锁(Intention Lock): 表级锁,用于协调行锁和表锁之间的关系。分为意向共享锁(IS)和意向排他锁(IX)。当事务要获取行级S锁时,必须先获取表的IS锁;要获取行级X锁时,必须先获取表的IX锁。
自增锁(AUTO-INC Lock): 特殊的表级锁,在INSERT到自增列(AUTO_INCREMENT)时使用,确保自增值的唯一性和连续性。
各隔离级别下的锁使用总结:
| 隔离级别 | 快照读 | MVCC Read View | 锁定读行为 | Gap Lock |
|---|---|---|---|---|
| READ UNCOMMITTED | 读最新版本 | 不使用 | 正常加锁 | 不添加 |
| READ COMMITTED | 每次读取生成新Read View | 每次快照读生成 | 仅记录锁 | 不添加 |
| REPEATABLE READ | 首次读取生成Read View,事务内复用 | 事务内唯一 | 记录锁+Gap Lock | 添加 |
| SERIALIZABLE | 普通SELECT自动变为锁定读 | 不使用 | 所有SELECT加S锁 | 添加 |
总结
InnoDB通过MVCC提供高效的非阻塞读,通过多粒度锁(行锁、间隙锁、临键锁)保证写操作的并发安全,两者协同工作,在不同隔离级别下实现不同力度的并发控制。理解MVCC与锁机制的配合,是深入掌握数据库事务的核心。