欢迎光临
我们一直在努力

MySQL事务深度解析:从ACID特性到隔离级别实战

文章目录

    • 什么是事务
      • 定义
      • 经典案例:银行转账
    • ACID特性详解
      • 原子性(Atomicity)
      • 一致性(Consistency)
      • 隔离性(Isolation)
      • 持久性(Durability)
    • 事务隔离级别
      • 隔离级别对比表
      • 设置隔离级别
    • 并发问题与解决方案
      • 1. 脏读(Dirty Read)
      • 2. 不可重复读(Non-repeatable Read)
      • 3. 幻读(Phantom Read)
    • InnoDB事务实现机制
      • 1. 重做日志(Redo Log)
      • 2. 回滚日志(Undo Log)
      • 3.二进制日志(Bin Log)
      • 4. 锁机制
      • 5.具体流程

在现代Web开发中,数据一致性是至关重要的。想象一下,用户在电商平台下单时,如果库存扣减了但订单没有生成,或者支付成功了但积分没有增加,这将是灾难性的。MySQL事务机制正是为了解决这类问题而存在的核心技术。

什么是事务

定义

事务(Transaction)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。这些操作要么全部执行成功,要么全部不执行,不存在部分执行的情况。

经典案例:银行转账

— 转账事务示例
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; — 提交事务

如果在执行过程中任何一步出现错误,整个事务将回滚到初始状态:

ROLLBACK; — 回滚事务,撤销所有操作

ACID特性详解

ACID是事务的四个基本特性,是数据库事务正确执行的基本要求:

原子性(Atomicity)

定义:事务是一个不可分割的工作单位,要么全部完成,要么全部不做。

实现机制:通过 undo log(回滚日志)实现

  • 记录事务执行前的数据状态
  • 事务失败时根据undo log恢复数据
  • 保证数据的一致性状态

示例场景:

START TRANSACTION;
UPDATE products SET stock = stock 1 WHERE id = 1001; — 减库存
INSERT INTO orders (product_id, user_id) VALUES (1001, 2001); — 生成订单

— 如果第二步失败,第一步也会自动回滚
ROLLBACK;

一致性(Consistency)

定义:事务必须使数据库从一个一致性状态变换到另一个一致性状态。

核心要点:

  • 事务开始前和结束后,数据库的完整性约束没有被破坏
  • 所有的业务规则都得到满足
  • 数据库中的数据应该是逻辑上正确合理的

示例:

— 约束:账户余额不能为负数
— 转账前: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级别下基本解决了幻读问题 当快照读和当前读混合使用时容易出现幻读

设置隔离级别

— 查看当前隔离级别
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)

问题描述:读取了未提交事务的数据,该数据可能回滚

场景演示:

— 时间线 | 事务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)

问题描述:在同一事务中,多次读取同一数据返回的结果不一致

场景演示:

— 事务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)

问题描述:在同一事务中,多次执行相同查询返回的记录数不一致

场景演示:

— 事务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中的脏页刷新到磁盘

配置参数:

— 查看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实现:

— 每行数据都有隐藏字段
— 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. 锁机制

行锁类型:

— 共享锁(S锁)- 读锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;

— 排他锁(X锁)- 写锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;

— 意向锁(IS/IX锁)- 表级锁,与行锁配合使用

Gap Lock(间隙锁):

— 防止幻读的关键机制
— 锁定索引记录之间的间隙,防止插入新记录

— 示例:如果存在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 后崩溃 同上

    如果这篇文章对你有帮助,欢迎点赞 分享 评论!如有疑问,请在评论区留言讨论。

    赞(0)
    未经允许不得转载:171主机测评 » MySQL事务深度解析:从ACID特性到隔离级别实战
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址