写在前面
欢迎来到MySQL系列教学第9天!今天我们将深入MySQL最核心的话题之一------事务(Transaction)。事务是保证数据库数据一致性和完整性的基石,理解事务机制对于开发高可靠性的应用系统至关重要。
无论你是刚接触数据库的新手,还是准备面试的求职者,这篇文章都将帮助你全面掌握MySQL事务的核心概念和原理。

文章目录
-
- 写在前面
- 一、什么是事务
-
- [1.1 事务的定义](#1.1 事务的定义)
- [1.2 银行转账的例子](#1.2 银行转账的例子)
- [1.3 事务的四个特性(ACID)](#1.3 事务的四个特性(ACID))
- 二、ACID特性详解
-
- [2.1 原子性(Atomicity)](#2.1 原子性(Atomicity))
- [2.2 一致性(Consistency)](#2.2 一致性(Consistency))
- [2.3 隔离性(Isolation)](#2.3 隔离性(Isolation))
- [2.4 持久性(Durability)](#2.4 持久性(Durability))
- 三、事务控制语句
-
- [3.1 基本语法](#3.1 基本语法)
- [3.2 SAVEPOINT保存点](#3.2 SAVEPOINT保存点)
- [3.3 自动提交模式](#3.3 自动提交模式)
- [3.4 隐式提交](#3.4 隐式提交)
- 四、事务隔离级别
-
- [4.1 四种隔离级别](#4.1 四种隔离级别)
- [4.2 设置隔离级别](#4.2 设置隔离级别)
- [4.3 READ UNCOMMITTED(读未提交)](#4.3 READ UNCOMMITTED(读未提交))
- [4.4 READ COMMITTED(读已提交)](#4.4 READ COMMITTED(读已提交))
- [4.5 REPEATABLE READ(可重复读)](#4.5 REPEATABLE READ(可重复读))
- [4.6 SERIALIZABLE(串行化)](#4.6 SERIALIZABLE(串行化))
- 五、并发问题详解
-
- [5.1 脏读(Dirty Read)](#5.1 脏读(Dirty Read))
- [5.2 不可重复读(Non-repeatable Read)](#5.2 不可重复读(Non-repeatable Read))
- [5.3 幻读(Phantom Read)](#5.3 幻读(Phantom Read))
- [5.4 三种并发问题对比](#5.4 三种并发问题对比)
- 六、MVCC多版本并发控制
-
- [6.1 什么是MVCC](#6.1 什么是MVCC)
- [6.2 核心概念](#6.2 核心概念)
- [6.3 快照读 vs 当前读](#6.3 快照读 vs 当前读)
- [6.4 ReadView的工作原理](#6.4 ReadView的工作原理)
- [6.5 MVCC实现可重复读](#6.5 MVCC实现可重复读)
- 七、实战:电商下单的事务处理
-
- [7.1 业务场景](#7.1 业务场景)
- [7.2 事务设计](#7.2 事务设计)
- [7.3 调用示例](#7.3 调用示例)
- 八、踩坑提醒
-
- [8.1 长事务问题](#8.1 长事务问题)
- [8.2 事务中避免调用外部接口](#8.2 事务中避免调用外部接口)
- [8.3 注意死锁](#8.3 注意死锁)
- 九、面试高频考点
- 十、总结
- 下一步预告
- 参考资料
- 互动话题
一、什么是事务
1.1 事务的定义
事务是一组逻辑上的数据库操作,这些操作要么全部成功执行,要么全部不执行。事务是数据库管理系统执行过程中的一个逻辑单位。
1.2 银行转账的例子
最经典的事务例子就是银行转账:
张三给李四转账1000元,需要两个操作:
1. 张三账户余额减少1000元
2. 李四账户余额增加1000元
这两个操作必须同时成功或同时失败:
- 如果第1步成功,第2步失败,张三的钱少了,李四没收到,钱凭空消失了
- 如果第1步失败,第2步成功,李四凭空多了1000元
这两种情况都是不可接受的!
sql
-- 事务示例
START TRANSACTION;
-- 1. 扣减张三的余额
UPDATE accounts SET balance = balance - 1000 WHERE name = '张三';
-- 2. 增加李四的余额
UPDATE accounts SET balance = balance + 1000 WHERE name = '李四';
-- 检查是否都成功
IF (没有错误) THEN
COMMIT; -- 提交事务,永久保存修改
ELSE
ROLLBACK; -- 回滚事务,撤销所有修改
END IF;
1.3 事务的四个特性(ACID)
| 特性 | 英文 | 含义 | 说明 |
|---|---|---|---|
| 原子性 | Atomicity | 不可再分 | 事务是最小执行单位,要么全做,要么全不做 |
| 一致性 | Consistency | 数据一致 | 事务执行前后,数据库从一个一致状态变为另一个一致状态 |
| 隔离性 | Isolation | 互不干扰 | 并发执行的事务互不干扰 |
| 持久性 | Durability | 永久保存 | 事务一旦提交,修改永久生效 |
二、ACID特性详解
2.1 原子性(Atomicity)
定义:事务是一个不可分割的最小工作单元,事务中的所有操作要么全部成功,要么全部失败回滚。
实现机制:
- undo log(回滚日志):记录事务执行前的数据状态,用于回滚
sql
-- 原子性示例
START TRANSACTION;
INSERT INTO orders (user_id, amount) VALUES (1, 100); -- 成功
INSERT INTO order_items (order_id, product_id) VALUES (LAST_INSERT_ID(), 1); -- 成功
UPDATE products SET stock = stock - 1 WHERE id = 1; -- 假设这里库存不足,失败
-- 由于第3步失败,前面两步自动回滚
ROLLBACK;
2.2 一致性(Consistency)
定义:事务执行前后,数据库必须处于一致性状态。所有数据必须满足预定义的完整性约束(外键、唯一约束、CHECK约束等)。
示例:
sql
-- 转账前后,系统总金额应该不变
-- 转账前:张三5000 + 李四3000 = 8000
-- 转账后:张三4000 + 李四4000 = 8000
-- 一致性检查
CREATE TABLE accounts (
id INT PRIMARY KEY,
name VARCHAR(50),
balance DECIMAL(10,2) CHECK (balance >= 0) -- 余额不能为负
);
-- 如果执行 UPDATE accounts SET balance = balance - 1000 WHERE balance < 1000;
-- 会违反CHECK约束,事务回滚,保证一致性
2.3 隔离性(Isolation)
定义:多个事务并发执行时,一个事务的执行不应影响其他事务的执行。事务之间是相互隔离的。
实现机制:
- 锁机制:排他锁、共享锁
- MVCC:多版本并发控制
sql
-- 事务A
START TRANSACTION;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
-- 此时事务A未提交
-- 事务B(同时执行)
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1;
-- 根据隔离级别不同,可能读到不同的值
2.4 持久性(Durability)
定义:事务一旦提交,其对数据库的修改就是永久性的,即使系统发生故障也不会丢失。
实现机制:
- redo log(重做日志):记录事务对数据的修改,用于崩溃恢复
- binlog(二进制日志):用于数据恢复和主从复制
三、事务控制语句
3.1 基本语法
sql
-- 开启事务
START TRANSACTION;
-- 或
BEGIN;
-- 提交事务
COMMIT;
-- 回滚事务
ROLLBACK;
3.2 SAVEPOINT保存点
sql
START TRANSACTION;
INSERT INTO accounts (name, balance) VALUES ('王五', 1000);
SAVEPOINT sp1; -- 设置保存点
INSERT INTO accounts (name, balance) VALUES ('赵六', 2000);
SAVEPOINT sp2;
-- 发现第二个插入有问题,回滚到sp1
ROLLBACK TO sp1;
-- 继续其他操作
INSERT INTO accounts (name, balance) VALUES ('孙七', 3000);
COMMIT;
-- 最终只有王五和孙七被插入
3.3 自动提交模式
sql
-- 查看自动提交状态
SHOW VARIABLES LIKE 'autocommit';
-- 关闭自动提交
SET autocommit = 0;
-- 此时每个SQL都在事务中,需要手动COMMIT或ROLLBACK
UPDATE accounts SET balance = 1000 WHERE id = 1;
-- 其他会话看不到这个修改,直到执行COMMIT
COMMIT;
-- 开启自动提交(默认)
SET autocommit = 1;
3.4 隐式提交
某些SQL语句会自动提交当前事务:
- DDL语句(CREATE、DROP、ALTER)
- 锁定语句(LOCK TABLES、UNLOCK TABLES)
sql
START TRANSACTION;
INSERT INTO users (name) VALUES ('张三');
CREATE TABLE test (id INT); -- 隐式提交!前面的INSERT被提交了
ROLLBACK; -- 无效,事务已经提交
四、事务隔离级别
4.1 四种隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 |
| READ COMMITTED | 不可能 | 可能 | 可能 |
| REPEATABLE READ | 不可能 | 不可能 | 可能 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 |
4.2 设置隔离级别
sql
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置会话隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
4.3 READ UNCOMMITTED(读未提交)
特点:事务可以读取其他事务未提交的数据。
sql
-- 事务A
START TRANSACTION;
UPDATE accounts SET balance = 900 WHERE id = 1;
-- 未提交
-- 事务B(READ UNCOMMITTED)
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- 读到900(脏读)
-- 事务A回滚
ROLLBACK;
-- 事务B读到的900就是脏数据
问题:脏读(Dirty Read)
4.4 READ COMMITTED(读已提交)
特点:事务只能读取其他事务已提交的数据。
sql
-- 事务A
START TRANSACTION;
UPDATE accounts SET balance = 900 WHERE id = 1;
-- 未提交
-- 事务B(READ COMMITTED)
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- 读到1000(原值)
-- 事务A提交
COMMIT;
-- 事务B再次查询
SELECT balance FROM accounts WHERE id = 1; -- 读到900(新值)
-- 同一事务内两次读取结果不同(不可重复读)
问题:不可重复读(Non-repeatable Read)
4.5 REPEATABLE READ(可重复读)
特点:同一事务内多次读取同一数据,结果一致。MySQL默认隔离级别。
sql
-- 事务A
START TRANSACTION;
SELECT balance FROM accounts WHERE id = 1; -- 读到1000
-- 事务B
START TRANSACTION;
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT;
-- 事务A再次查询
SELECT balance FROM accounts WHERE id = 1; -- 仍然读到1000(可重复读)
COMMIT;
实现原理:MVCC机制,使用事务开始时的数据快照。
4.6 SERIALIZABLE(串行化)
特点:最高隔离级别,强制事务串行执行,完全避免并发问题。
sql
-- 事务A
START TRANSACTION;
SELECT * FROM accounts WHERE balance > 500;
-- 事务B(SERIALIZABLE)
START TRANSACTION;
INSERT INTO accounts (name, balance) VALUES ('新用户', 1000);
-- 阻塞!等待事务A提交
问题:性能最差,并发度最低。
五、并发问题详解
5.1 脏读(Dirty Read)
定义:事务读取了其他事务未提交的数据,如果那个事务回滚,读取到的数据就是无效的。
sql
-- 时间线
-- T1: 事务A开始,修改数据但未提交
-- T2: 事务B读取了A修改后的数据
-- T3: 事务A回滚
-- T4: 事务B使用的数据是无效的
解决方案:使用READ COMMITTED或更高隔离级别。
5.2 不可重复读(Non-repeatable Read)
定义:同一事务内,两次读取同一数据,结果不同(被其他事务修改并提交)。
sql
-- 时间线
-- T1: 事务A读取数据X = 100
-- T2: 事务B修改X = 200并提交
-- T3: 事务A再次读取X = 200(与T1不同)
解决方案:使用REPEATABLE READ或SERIALIZABLE。
5.3 幻读(Phantom Read)
定义:同一事务内,两次执行相同条件的查询,返回的行数不同(被其他事务插入或删除)。
sql
-- 事务A
START TRANSACTION;
SELECT * FROM accounts WHERE balance > 500; -- 返回3条
-- 事务B插入新数据并提交
INSERT INTO accounts (name, balance) VALUES ('新用户', 1000);
-- 事务A再次查询
SELECT * FROM accounts WHERE balance > 500; -- 返回4条(幻读)
解决方案:
- SERIALIZABLE隔离级别
- InnoDB的间隙锁(Gap Lock)和临键锁(Next-Key Lock)
5.4 三种并发问题对比
| 问题 | 描述 | 发生条件 | 解决方案 |
|---|---|---|---|
| 脏读 | 读到未提交数据 | READ UNCOMMITTED | READ COMMITTED+ |
| 不可重复读 | 同一事务两次读取结果不同 | READ COMMITTED | REPEATABLE READ+ |
| 幻读 | 同一事务两次查询行数不同 | REPEATABLE READ | SERIALIZABLE |
六、MVCC多版本并发控制
6.1 什么是MVCC
MVCC(Multi-Version Concurrency Control) 是一种并发控制技术,通过保存数据的历史版本,实现读写不阻塞,提高并发性能。
6.2 核心概念
隐藏字段:
DB_TRX_ID:最后修改该记录的事务IDDB_ROLL_PTR:回滚指针,指向undo logDB_ROW_ID:隐藏主键(如果没有显式主键)
undo log:
- 记录数据修改前的版本
- 形成版本链,用于回滚和MVCC
ReadView:
- 事务快照,记录当前活跃事务ID列表
- 用于判断数据版本对当前事务是否可见
6.3 快照读 vs 当前读
快照读(Snapshot Read):
sql
-- 普通SELECT就是快照读
SELECT * FROM accounts WHERE id = 1;
-- 读取的是历史版本,不加锁
当前读(Current Read):
sql
-- 读取最新版本,需要加锁
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- 排他锁
SELECT * FROM accounts WHERE id = 1 LOCK IN SHARE MODE; -- 共享锁
-- DML语句也是当前读
INSERT INTO accounts ...
UPDATE accounts ...
DELETE FROM accounts ...
6.4 ReadView的工作原理
ReadView包含以下信息:
creator_trx_id:创建该ReadView的事务IDm_ids:生成ReadView时活跃的事务ID列表min_trx_id:m_ids中的最小值max_trx_id:下一个要分配的事务ID
可见性判断规则:
- 如果
DB_TRX_ID等于creator_trx_id:可见(自己修改的) - 如果
DB_TRX_ID小于min_trx_id:可见(事务已提交) - 如果
DB_TRX_ID大于等于max_trx_id:不可见(事务未开始) - 如果
DB_TRX_ID在m_ids中:不可见(事务未提交) - 否则:可见(事务已提交)
6.5 MVCC实现可重复读
sql
-- 事务A(事务ID=100)
START TRANSACTION;
-- 生成ReadView: m_ids=[100], min_trx_id=100, max_trx_id=101
SELECT * FROM accounts WHERE id = 1; -- 读取快照
-- 事务B(事务ID=101)修改并提交
UPDATE accounts SET balance = 900 WHERE id = 1;
COMMIT;
-- 事务A再次查询,使用相同的ReadView
SELECT * FROM accounts WHERE id = 1; -- 仍然读到原值,实现可重复读
COMMIT;
七、实战:电商下单的事务处理
7.1 业务场景
用户下单需要完成以下操作:
- 创建订单记录
- 扣减商品库存
- 扣减用户余额
- 创建订单明细
7.2 事务设计
sql
-- 存储过程:创建订单
DELIMITER $$
CREATE PROCEDURE create_order(
IN p_user_id BIGINT,
IN p_product_id BIGINT,
IN p_quantity INT,
OUT p_order_id BIGINT,
OUT p_result VARCHAR(100)
)
BEGIN
DECLARE v_price DECIMAL(10,2);
DECLARE v_stock INT;
DECLARE v_balance DECIMAL(12,2);
DECLARE v_total DECIMAL(12,2);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET p_result = '订单创建失败,已回滚';
RESIGNAL;
END;
-- 开启事务
START TRANSACTION;
-- 1. 查询商品信息(加排他锁,防止并发修改)
SELECT price, stock INTO v_price, v_stock
FROM products
WHERE id = p_product_id FOR UPDATE;
-- 检查库存
IF v_stock < p_quantity THEN
ROLLBACK;
SET p_result = '库存不足';
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '库存不足';
END IF;
-- 2. 查询用户余额
SELECT balance INTO v_balance
FROM users
WHERE id = p_user_id FOR UPDATE;
SET v_total = v_price * p_quantity;
-- 检查余额
IF v_balance < v_total THEN
ROLLBACK;
SET p_result = '余额不足';
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '余额不足';
END IF;
-- 3. 扣减库存
UPDATE products
SET stock = stock - p_quantity
WHERE id = p_product_id;
-- 4. 扣减余额
UPDATE users
SET balance = balance - v_total
WHERE id = p_user_id;
-- 5. 创建订单
INSERT INTO orders (user_id, total_amount, status, created_at)
VALUES (p_user_id, v_total, 1, NOW());
SET p_order_id = LAST_INSERT_ID();
-- 6. 创建订单明细
INSERT INTO order_items (order_id, product_id, quantity, price)
VALUES (p_order_id, p_product_id, p_quantity, v_price);
-- 提交事务
COMMIT;
SET p_result = '订单创建成功';
END$$
DELIMITER ;
7.3 调用示例
sql
-- 调用存储过程创建订单
SET @order_id = 0;
SET @result = '';
CALL create_order(1, 100, 2, @order_id, @result);
SELECT @order_id, @result;
八、踩坑提醒
8.1 长事务问题
问题:事务执行时间过长,会导致:
- 锁持有时间过长,阻塞其他事务
- undo log堆积,影响性能
- 可能导致死锁
解决方案:
sql
-- 1. 尽量缩短事务范围
START TRANSACTION;
-- 只放必要的操作
COMMIT;
-- 2. 避免在事务中执行耗时操作
-- 错误:事务中调用外部API、发送邮件等
START TRANSACTION;
INSERT INTO orders ...;
-- 发送邮件(耗时操作,不应该在事务中)
send_email();
COMMIT;
-- 正确:先提交事务,再发送邮件
START TRANSACTION;
INSERT INTO orders ...;
COMMIT;
send_email();
8.2 事务中避免调用外部接口
sql
-- 危险操作
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
-- 调用支付接口(可能超时、失败)
CALL external_payment_api();
-- 如果接口超时,事务一直不提交,锁一直不释放
COMMIT;
正确做法:
- 先调用外部接口
- 接口成功后再开启事务更新数据库
- 或者使用本地消息表实现最终一致性
8.3 注意死锁
sql
-- 事务A
START TRANSACTION;
UPDATE accounts SET balance = 900 WHERE id = 1; -- 锁住id=1
UPDATE accounts SET balance = 1900 WHERE id = 2; -- 等待id=2
-- 事务B(同时执行)
START TRANSACTION;
UPDATE accounts SET balance = 800 WHERE id = 2; -- 锁住id=2
UPDATE accounts SET balance = 1800 WHERE id = 1; -- 等待id=1,死锁!
解决方案:
- 按固定顺序访问资源
- 设置锁超时时间
- 捕获死锁异常并重试
九、面试高频考点
Q1:四种隔离级别分别解决什么问题?
答:
- READ UNCOMMITTED:无,性能最好,问题最多
- READ COMMITTED:解决脏读
- REPEATABLE READ:解决脏读、不可重复读(MySQL默认)
- SERIALIZABLE:解决脏读、不可重复读、幻读,性能最差
Q2:MySQL默认的隔离级别是什么?为什么?
答:
- 默认隔离级别是REPEATABLE READ(可重复读)
- 原因:
- 相比READ COMMITTED,避免了不可重复读问题
- InnoDB通过MVCC和间隙锁,在REPEATABLE READ下也能解决幻读问题
- 平衡了数据一致性和并发性能
Q3:幻读是怎么产生的?如何解决?
答:
- 产生原因:一个事务在两次查询同一范围数据时,由于其他事务插入新数据,导致两次查询结果行数不同。
- 解决方案 :
- SERIALIZABLE隔离级别(性能差)
- InnoDB的间隙锁(Gap Lock)和临键锁(Next-Key Lock)
- 查询时加FOR UPDATE锁定范围
Q4:MVCC是如何工作的?
答:
- 每行数据有隐藏字段:
DB_TRX_ID(事务ID)、DB_ROLL_PTR(回滚指针) - 修改数据时,将旧版本存入undo log,形成版本链
- 事务开始时生成ReadView,记录当前活跃事务
- 查询时根据ReadView判断数据版本是否可见
- 实现读写不阻塞,提高并发性能
Q5:事务的ACID是如何保证的?
答:
- 原子性:undo log实现回滚
- 一致性:原子性+隔离性+持久性共同保证,加上约束检查
- 隔离性:锁机制+MVCC
- 持久性:redo log+binlog实现崩溃恢复
十、总结
今天我们深入学习了MySQL事务的核心知识:
- 事务概念:ACID特性是事务的基石
- 事务控制:START TRANSACTION、COMMIT、ROLLBACK、SAVEPOINT
- 隔离级别:四种隔离级别及其解决的问题
- 并发问题:脏读、不可重复读、幻读的产生和解决
- MVCC机制:多版本并发控制实现高效读写分离
- 实战应用:电商下单的事务设计
核心要点:
- 理解ACID是理解事务的基础
- 选择合适的隔离级别,平衡一致性和性能
- MVCC是InnoDB高并发的核心
- 避免长事务和事务中调用外部接口
下一步预告
Day10我们将学习MySQL的锁机制与并发控制,深入探讨InnoDB的各种锁类型(记录锁、间隙锁、临键锁),以及如何处理死锁问题。敬请期待!
参考资料
互动话题
- 你在项目中遇到过哪些事务相关的问题?是如何解决的?
- 你们项目使用的是什么隔离级别?为什么选择这个级别?
- 对于分布式事务,你有什么实践经验或了解?
欢迎在评论区分享你的经验和见解!如果觉得本文有帮助,别忘了点赞收藏哦~