MySQL之事务理论和案例

发布于:2025-04-12 ⋅ 阅读:(30) ⋅ 点赞:(0)

理论

一、事务的四大特性(ACID)

  1. 原子性(Atomicity)

    • 事务中的操作要么全部成功,要么全部失败。

    • 实现机制:通过undo log记录操作前的数据状态,用于回滚。

  2. 一致性(Consistency)

    • 事务执行前后数据库状态必须符合业务规则。

    • 依赖应用层逻辑和数据库约束(如唯一索引、外键)共同维护。

  3. 隔离性(Isolation)

    • 并发事务之间互不干扰。

    • 实现机制:锁机制和MVCC(多版本并发控制)。

  4. 持久性(Durability)

    • 事务提交后数据永久保存。

    • 实现机制redo log记录数据修改,崩溃恢复时重放日志。

二、事务隔离级别与并发问题

  1. 隔离级别

    • 读未提交(Read Uncommitted):可能读到未提交的数据,存在脏读、不可重复读、幻读。

    • 读已提交(Read Committed):解决脏读,存在不可重复读和幻读(Oracle默认)。

    • 可重复读(Repeatable Read):解决脏读和不可重复读,通过MVCC和间隙锁避免幻读(MySQL默认)。

    • 串行化(Serializable):完全隔离,性能最低。

  2. 并发问题

    • 脏读:读取到未提交的数据。

    • 不可重复读:同一事务内多次读取结果不同。

    • 幻读:同一查询条件返回的行数变化。

三、事务控制语句

  • 开启事务BEGIN 或 START TRANSACTION

  • 提交事务COMMIT

  • 回滚事务ROLLBACK

  • 保存点SAVEPOINT point_nameROLLBACK TO point_name

四、锁机制

  1. 锁类型

    • 行锁:锁定单行数据(InnoDB)。

    • 表锁:锁定整张表(MyISAM)。

    • 间隙锁(Gap Lock):锁定索引区间,防止插入。

    • 临键锁(Next-Key Lock):行锁+间隙锁,解决幻读。

  2. 锁与隔离级别

    • 可重复读:使用临键锁和MVCC防止幻读。

    • 读已提交:仅使用行锁,可能产生幻读。

五、MVCC(多版本并发控制)

  1. 核心机制

    • 隐藏列:事务ID(DB_TRX_ID)和回滚指针(DB_ROLL_PTR)。

    • 版本链:通过undo log构建数据的历史版本。

    • ReadView:事务启动时生成,决定可见的数据版本。

  2. 隔离级别的实现差异

    • RC(读已提交):每次读取生成新ReadView,能看到已提交的新事务。

    • RR(可重复读):首次读取生成ReadView,后续沿用,保证一致性视图。

六、事务日志

  • undo log:记录事务前的数据状态,用于回滚和MVCC。

  • redo log:记录数据页修改,确保崩溃恢复时的持久性。

  • 写入流程:先写redo log到磁盘(顺序写,高效),再异步刷盘数据页。

七、实战注意事项

  1. 长事务风险

    • 导致锁持有时间长,undo log堆积。监控工具:information_schema.INNODB_TRX

  2. 死锁处理

    • MySQL自动检测并回滚较小的事务。优化索引、减少事务粒度可降低死锁概率。

  3. 性能优化

    • 合理设置隔离级别,避免不必要的锁竞争。

    • 使用索引减少锁范围,避免全表扫描。

八、分布式事务

  • XA协议:基于两阶段提交(2PC),存在协调者单点问题。

  • 柔性事务:如TCC(Try-Confirm-Cancel)、Saga模式,保证最终一致性。

九、学习资源

  • 官方文档MySQL 8.0 Reference Manual

  • 书籍:《高性能MySQL》《MySQL技术内幕:InnoDB存储引擎》

  • 实践建议:通过实验模拟不同隔离级别下的并发问题,分析锁和MVCC的影响。

通过以上知识点的系统学习,结合实际操作和案例分析,可以深入理解MySQL事务的工作原理与应用场景。遇到问题时,建议结合日志分析和性能监控工具进行调优。

案例

案例1:脏读(Dirty Read)

场景:事务A修改数据但未提交,事务B读取未提交的数据。
操作步骤

-- 会话1(事务A)
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
UPDATE users SET balance = balance - 100 WHERE id = 1;  -- 未提交

-- 会话2(事务B)
SET SESSION TRANSACTION ISLATION LEVEL READ UNCOMMITTED;
START TRANSACTION;
SELECT balance FROM users WHERE id = 1;  -- 读到未提交的balance值(脏读)

结果:事务B读到未提交的修改(balance减少100)。
修复方法:将隔离级别提升至 READ COMMITTED,事务B将无法读到未提交的数据。


案例2:不可重复读(Non-Repeatable Read)

场景:同一事务内两次读取同一数据,结果不一致。
操作步骤

-- 会话1(事务A)
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
START TRANSACTION;
SELECT balance FROM users WHERE id = 1;  -- 第一次读取:1000

-- 会话2(事务B)
START TRANSACTION;
UPDATE users SET balance = 900 WHERE id = 1;
COMMIT;

-- 会话1(事务A继续)
SELECT balance FROM users WHERE id = 1;  -- 第二次读取:900(不可重复读)

结果:事务A的两次读取结果不一致。
修复方法:将隔离级别提升至 REPEATABLE READ,事务A的两次读取结果一致(基于MVCC的版本快照)。


案例3:幻读(Phantom Read)

场景:同一事务内两次查询同一条件,返回的行数不同。
操作步骤

-- 会话1(事务A)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM users WHERE age > 20;  -- 返回2条记录

-- 会话2(事务B)
START TRANSACTION;
INSERT INTO users (name, age) VALUES ('Bob', 25);  -- 插入符合age>20的新数据
COMMIT;

-- 会话1(事务A继续)
SELECT * FROM users WHERE age > 20;  -- 仍然返回2条记录(MVCC快照)
-- 若执行更新操作(如UPDATE users SET ... WHERE age > 20),会触发间隙锁,阻止插入

结果:在可重复读隔离级别下,MVCC快照保证查询结果一致,但更新可能触发间隙锁。
注意:严格幻读需结合范围更新验证。


案例4:死锁(Deadlock)

场景:两个事务互相等待对方持有的锁。
操作步骤

-- 会话1(事务A)
START TRANSACTION;
UPDATE users SET balance = balance - 100 WHERE id = 1;  -- 持有id=1的行锁
UPDATE users SET balance = balance + 100 WHERE id = 2;  -- 尝试获取id=2的锁

-- 会话2(事务B)
START TRANSACTION;
UPDATE users SET balance = balance - 100 WHERE id = 2;  -- 持有id=2的行锁
UPDATE users SET balance = balance + 100 WHERE id = 1;  -- 尝试获取id=1的锁

结果:事务A和事务B互相等待,触发死锁,MySQL自动回滚其中一个事务。
日志提示

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

优化建议:按固定顺序访问资源(如先更新id小的记录)。


案例5:MVCC与ReadView

场景:可重复读隔离级别下,MVCC如何生成一致性视图。
操作步骤

-- 会话1(事务A)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT balance FROM users WHERE id = 1;  -- 生成ReadView,假设balance=1000

-- 会话2(事务B)
START TRANSACTION;
UPDATE users SET balance = 900 WHERE id = 1;
COMMIT;

-- 会话1(事务A继续)
SELECT balance FROM users WHERE id = 1;  -- 仍然读取到1000(基于事务启动时的ReadView)

结果:事务A的两次读取结果一致,因MVCC通过ReadView屏蔽了事务B的提交。
对比:若隔离级别为 READ COMMITTED,第二次读取会看到新提交的900。


案例6:长事务与undo log堆积

场景:长时间未提交的事务导致undo log无法清理。
操作步骤

-- 会话1(事务A)
START TRANSACTION;
UPDATE users SET balance = balance - 100 WHERE id = 1;  -- 生成undo log
-- 长时间未提交(如休眠10分钟)

-- 监控undo log大小
SELECT * FROM information_schema.INNODB_METRICS WHERE NAME LIKE '%undo%';

风险:undo log占用磁盘空间,影响性能。
处理建议:通过 KILL 终止长事务,或设置 innodb_undo_log_truncate 自动清理。


案例7:间隙锁(Gap Lock)

场景:防止幻读的间隙锁机制。
操作步骤

-- 会话1(事务A)
SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
SELECT * FROM users WHERE age BETWEEN 20 AND 30 FOR UPDATE;  -- 锁定age=20~30的间隙

-- 会话2(事务B)
START TRANSACTION;
INSERT INTO users (name, age) VALUES ('Charlie', 25);  -- 阻塞,等待间隙锁释放

结果:事务B的插入操作被阻塞,直到事务A提交或回滚。
用途:通过间隙锁阻止范围内插入新数据,避免幻读。


总结

通过以上案例可直观理解:

  1. 隔离级别如何解决脏读、不可重复读、幻读。

  2. 锁机制(行锁、间隙锁)与 MVCC 的协作原理。

  3. 死锁 的触发条件和优化方法。

  4. 长事务 对系统资源的潜在风险。


网站公告

今日签到

点亮在社区的每一天
去签到