文章目录
-
- 什么是事务
- ACID特性详解
- 事务隔离级别
- 并发问题与解决方案
-
- [1. 脏读(Dirty Read)](#1. 脏读(Dirty Read))
- [2. 不可重复读(Non-repeatable Read)](#2. 不可重复读(Non-repeatable Read))
- [3. 幻读(Phantom Read)](#3. 幻读(Phantom Read))
- InnoDB事务实现机制
-
- [1. 重做日志(Redo Log)](#1. 重做日志(Redo Log))
- [2. 回滚日志(Undo Log)](#2. 回滚日志(Undo Log))
- [3.二进制日志(Bin Log)](#3.二进制日志(Bin Log))
- [4. 锁机制](#4. 锁机制)
- 5.具体流程
在现代Web开发中,数据一致性是至关重要的。想象一下,用户在电商平台下单时,如果库存扣减了但订单没有生成,或者支付成功了但积分没有增加,这将是灾难性的。MySQL事务机制正是为了解决这类问题而存在的核心技术。
什么是事务
定义
事务(Transaction)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。这些操作要么全部执行成功,要么全部不执行,不存在部分执行的情况。
经典案例:银行转账
sql
-- 转账事务示例
START TRANSACTION;
-- 减少转出账户余额
UPDATE accounts SET balance = balance - 1000 WHERE account_id = 'A001';
-- 增加转入账户余额
UPDATE accounts SET balance = balance + 1000 WHERE account_id = 'B002';
-- 记录转账日志
INSERT INTO transfer_logs (from_account, to_account, amount, transfer_time)
VALUES ('A001', 'B002', 1000, NOW());
COMMIT; -- 提交事务
如果在执行过程中任何一步出现错误,整个事务将回滚到初始状态:
sql
ROLLBACK; -- 回滚事务,撤销所有操作
ACID特性详解
ACID是事务的四个基本特性,是数据库事务正确执行的基本要求:
原子性(Atomicity)
定义:事务是一个不可分割的工作单位,要么全部完成,要么全部不做。
实现机制 :通过 undo log(回滚日志)实现
- 记录事务执行前的数据状态
- 事务失败时根据undo log恢复数据
- 保证数据的一致性状态
示例场景:
sql
START TRANSACTION;
UPDATE products SET stock = stock - 1 WHERE id = 1001; -- 减库存
INSERT INTO orders (product_id, user_id) VALUES (1001, 2001); -- 生成订单
-- 如果第二步失败,第一步也会自动回滚
ROLLBACK;
一致性(Consistency)
定义:事务必须使数据库从一个一致性状态变换到另一个一致性状态。
核心要点:
- 事务开始前和结束后,数据库的完整性约束没有被破坏
- 所有的业务规则都得到满足
- 数据库中的数据应该是逻辑上正确合理的
示例:
sql
-- 约束:账户余额不能为负数
-- 转账前:A账户1000元,B账户500元,总计1500元
-- 转账后:A账户500元,B账户1000元,总计仍为1500元
隔离性(Isolation)
定义:多个事务并发执行时,每个事务都感觉不到其他事务在同时执行。
实现机制:
- 锁机制:行锁、表锁、间隙锁
- MVCC:多版本并发控制
- 隔离级别:4种不同级别的隔离策略
持久性(Durability)
定义:事务一旦提交,其所做的修改就会永久保存到数据库中。
实现机制 :通过 redo log(重做日志)实现
- 先写日志,再写磁盘(WAL机制)
- 系统崩溃后可通过redo log恢复数据
- 确保已提交事务的永久性
事务隔离级别
MySQL定义了四种事务隔离级别,用于解决并发访问时的数据一致性问题:
隔离级别对比表
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 说明 |
|---|---|---|---|---|
| READ UNCOMMITTED 读未提交 | ❌ 可能发生 | ❌ 可能发生 | ❌ 可能发生 | 最低级别,性能最好 |
| READ COMMITTED 读已提交 | ✅ 已解决 | ❌ 可能发生 | ❌ 可能发生 | Oracle默认级别 |
| REPEATABLE READ 可重复读 | ✅ 已解决 | ✅ 已解决 | ⚠️ 基本解决* | MySQL默认级别 |
| SERIALIZABLE 串行化 | ✅ 已解决 | ✅ 已解决 | ✅ 已解决 | 最高级别,性能最差 |
*注:MySQL的InnoDB引擎通过Gap Lock(间隙锁)在RR级别下基本解决了幻读问题
当快照读和当前读混合使用时容易出现幻读
设置隔离级别
sql
-- 查看当前隔离级别
SELECT @@transaction_isolation;
-- 设置会话级别隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 设置全局隔离级别
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
-- 为单个事务设置隔离级别
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
START TRANSACTION;
-- ... 事务操作
COMMIT;
并发问题与解决方案
1. 脏读(Dirty Read)
问题描述:读取了未提交事务的数据,该数据可能回滚
场景演示:
sql
-- 时间线 | 事务A | 事务B
-- T1 | START TRANSACTION; |
-- T2 | UPDATE users SET |
-- | balance = 2000 |
-- | WHERE id = 1; |
-- T3 | | START TRANSACTION;
-- T4 | | SELECT balance FROM users
-- | | WHERE id = 1; -- 读到2000
-- T5 | ROLLBACK; |
-- T6 | | -- 此时读到的2000是脏数据
解决方案:使用READ COMMITTED以上的隔离级别
2. 不可重复读(Non-repeatable Read)
问题描述:在同一事务中,多次读取同一数据返回的结果不一致
场景演示:
sql
-- 事务A中的操作序列
START TRANSACTION;
SELECT balance FROM users WHERE id = 1; -- 第一次读取:1000
-- 此时事务B修改并提交了该数据
-- UPDATE users SET balance = 1500 WHERE id = 1; COMMIT;
SELECT balance FROM users WHERE id = 1; -- 第二次读取:1500(不一致!)
COMMIT;
解决方案:使用REPEATABLE READ以上的隔离级别
3. 幻读(Phantom Read)
问题描述:在同一事务中,多次执行相同查询返回的记录数不一致
场景演示:
sql
-- 事务A
START TRANSACTION;
SELECT COUNT(*) FROM orders WHERE user_id = 1001; -- 返回5条
-- 事务B插入新记录并提交
-- INSERT INTO orders (user_id, product_id) VALUES (1001, 2001); COMMIT;
SELECT COUNT(*) FROM orders WHERE user_id = 1001; -- 返回6条(幻读!)
COMMIT;
解决方案:
- 使用SERIALIZABLE隔离级别
- MySQL的REPEATABLE READ + Gap Lock机制
InnoDB事务实现机制
1. 重做日志(Redo Log)
作用:保证事务的持久性
工作原理:
1. 事务开始修改数据
2. 先将修改记录写入Redo Log Buffer
3. 再修改Buffer Pool中的数据页
4. 事务提交时,Redo Log持久化到磁盘
5. 后台线程异步将Buffer Pool中的脏页刷新到磁盘
配置参数:
sql
-- 查看redo log相关配置
SHOW VARIABLES LIKE 'innodb_log%';
-- 重要参数说明
-- innodb_log_file_size: 每个redo log文件的大小
-- innodb_log_files_in_group: redo log文件的数量
-- innodb_flush_log_at_trx_commit: 控制redo log的刷盘策略
2. 回滚日志(Undo Log)
作用:保证事务的原子性,实现MVCC
记录内容:
- INSERT操作:记录主键,回滚时删除该记录
- DELETE操作:记录整行数据,回滚时插入该记录
- UPDATE操作:记录被修改字段的原始值
MVCC实现:
sql
-- 每行数据都有隐藏字段
-- DB_TRX_ID: 最后修改该行的事务ID
-- DB_ROLL_PTR: 指向该行的undo log记录的指针
-- DB_ROW_ID: 行ID(仅当表没有主键时)
-- 读取数据时的版本判断
-- 如果DB_TRX_ID < 当前事务ReadView.min_trx_id,可见
-- 如果DB_TRX_ID > 当前事务ReadView.max_trx_id,不可见
-- 否则根据ReadView.active_trx_ids判断
3.二进制日志(Bin Log)
作用:记录的是执行了什么 SQL / 行变更了什么
两种常见格式:
| 格式 | 内容 | 特点 |
|---|---|---|
| STATEMENT | 原始 SQL | 体积小,可能不一致(now()、UUID() 等) |
| ROW | 行前后镜像 | 体积大,精确可靠 |
作用:
- 主从复制:从库通过重放 binlog 保持与主库一致。
- 数据恢复:基于某个备份点 + 增量 binlog 恢复到任意时间点。
4. 锁机制
行锁类型:
sql
-- 共享锁(S锁)- 读锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 排他锁(X锁)- 写锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;
-- 意向锁(IS/IX锁)- 表级锁,与行锁配合使用
Gap Lock(间隙锁):
sql
-- 防止幻读的关键机制
-- 锁定索引记录之间的间隙,防止插入新记录
-- 示例:如果存在id = 1, 5, 10的记录
-- 执行:SELECT * FROM users WHERE id BETWEEN 1 AND 10 FOR UPDATE;
-- 将锁定:(1,5), (5,10) 等间隙,防止插入id=3,7等记录
5.具体流程
- 执行阶段(语句执行时)
- 加行锁(X Lock) 防止并发修改同一行
- 读取数据页到 Buffer Pool 若不在内存则从磁盘加载
- 写 Undo Log 记录旧值,用于回滚与 MVCC
- 修改 Buffer Pool 中的脏页 数据只在内存修改,不立即落盘
- 写 Redo Log buffer 标记为 prepare 状态,尚未刷盘
- 提交阶段(COMMIT 时)------ 两阶段提交(2PC)
- Prepare
InnoDB 将 redo log buffer 中当前事务的日志写入到磁盘
redo log 条目标记为 prepare 状态
向 MySQL Server 层返回 prepare - Commit
MySQL Server 层将本次事务的变更写入 binlog,并写入到到磁盘
Server 层通知 InnoDB 执行最终提交
InnoDB 将 redo log 中对应条目改写为 commit 状态
释放行锁,事务结束
向客户端返回成功
-
崩溃恢复
InnoDB 重启,扫描 redo log
│
▼
redo log 状态?
┌────┴────┐
commit prepare
│ │
提交 检查 binlog 中是否有该事务的完整记录
┌────┴────┐
有 无
│ │
提交 回滚 -
为什么要使用2PC
当分别redo log或者bin log 先进行写入都会导致主从数据不一致
通过2PC,在prepare阶段让redo log记录获得一个XID,接下来让bin log记录数据和相同XID并写入磁盘,最后将redo log commit。
- 当prepare阶段后崩溃
bin log查不到相同xid回滚事务 - 当bin log记录完后崩溃
bin log查得到相同xid,让redo log commit 数据保持一致 - commit 后崩溃
同上
如果这篇文章对你有帮助,欢迎
点赞分享评论!如有疑问,请在评论区留言讨论。