理论
一、事务的四大特性(ACID)
原子性(Atomicity)
事务中的操作要么全部成功,要么全部失败。
实现机制:通过
undo log
记录操作前的数据状态,用于回滚。
一致性(Consistency)
事务执行前后数据库状态必须符合业务规则。
依赖应用层逻辑和数据库约束(如唯一索引、外键)共同维护。
隔离性(Isolation)
并发事务之间互不干扰。
实现机制:锁机制和MVCC(多版本并发控制)。
持久性(Durability)
事务提交后数据永久保存。
实现机制:
redo log
记录数据修改,崩溃恢复时重放日志。
二、事务隔离级别与并发问题
隔离级别
读未提交(Read Uncommitted):可能读到未提交的数据,存在脏读、不可重复读、幻读。
读已提交(Read Committed):解决脏读,存在不可重复读和幻读(Oracle默认)。
可重复读(Repeatable Read):解决脏读和不可重复读,通过MVCC和间隙锁避免幻读(MySQL默认)。
串行化(Serializable):完全隔离,性能最低。
并发问题
脏读:读取到未提交的数据。
不可重复读:同一事务内多次读取结果不同。
幻读:同一查询条件返回的行数变化。
三、事务控制语句
开启事务:
BEGIN
或START TRANSACTION
。提交事务:
COMMIT
。回滚事务:
ROLLBACK
。保存点:
SAVEPOINT point_name
,ROLLBACK TO point_name
。
四、锁机制
锁类型
行锁:锁定单行数据(InnoDB)。
表锁:锁定整张表(MyISAM)。
间隙锁(Gap Lock):锁定索引区间,防止插入。
临键锁(Next-Key Lock):行锁+间隙锁,解决幻读。
锁与隔离级别
可重复读:使用临键锁和MVCC防止幻读。
读已提交:仅使用行锁,可能产生幻读。
五、MVCC(多版本并发控制)
核心机制
隐藏列:事务ID(DB_TRX_ID)和回滚指针(DB_ROLL_PTR)。
版本链:通过undo log构建数据的历史版本。
ReadView:事务启动时生成,决定可见的数据版本。
隔离级别的实现差异
RC(读已提交):每次读取生成新ReadView,能看到已提交的新事务。
RR(可重复读):首次读取生成ReadView,后续沿用,保证一致性视图。
六、事务日志
undo log:记录事务前的数据状态,用于回滚和MVCC。
redo log:记录数据页修改,确保崩溃恢复时的持久性。
写入流程:先写redo log到磁盘(顺序写,高效),再异步刷盘数据页。
七、实战注意事项
长事务风险
导致锁持有时间长,undo log堆积。监控工具:
information_schema.INNODB_TRX
。
死锁处理
MySQL自动检测并回滚较小的事务。优化索引、减少事务粒度可降低死锁概率。
性能优化
合理设置隔离级别,避免不必要的锁竞争。
使用索引减少锁范围,避免全表扫描。
八、分布式事务
XA协议:基于两阶段提交(2PC),存在协调者单点问题。
柔性事务:如TCC(Try-Confirm-Cancel)、Saga模式,保证最终一致性。
九、学习资源
书籍:《高性能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提交或回滚。
用途:通过间隙锁阻止范围内插入新数据,避免幻读。
总结
通过以上案例可直观理解:
隔离级别如何解决脏读、不可重复读、幻读。
锁机制(行锁、间隙锁)与 MVCC 的协作原理。
死锁 的触发条件和优化方法。
长事务 对系统资源的潜在风险。