一、引言:小明的图书馆新挑战
1.1 故事开篇:并发借阅的混乱
小明的"码农图书馆"越办越红火,读者越来越多。但新问题来了:
场景1:读者A要借《MySQL索引优化》,读者B同时要买这本书(库存只有1本)
- 小明先给A办理了借阅,但在更新库存时被B打断,B以为还有库存就买了
- 结果:A借到了书,B也买到了书,但库存变成了-1本!
场景2:读者C查询某本书的借阅记录,刚看了第一条,小明就去更新了这条记录,C看到的数据前后不一致
技术映射:这就是数据库的并发控制问题——多个用户同时操作同一数据时,如何保证数据一致性?
1.2 解决方案:事务与锁机制
小明通过学习,引入了事务(Transaction)和锁(Lock)机制:
- 事务:将一组操作打包成一个"不可分割的工作单元",要么全成功,要么全失败
- 锁:控制并发访问的"交通规则",防止多个用户同时修改同一数据
1.3 本章学习目标
通过小明的图书馆故事,你将掌握:
- 事务基础:ACID特性、原子性、一致性、隔离性、持久性
- 事务回滚与提交机制:深入理解COMMIT/ROLLBACK的内部工作原理
- Redo/Undo日志系统:数据库持久化与回滚的技术基石
- MVCC机制:多版本并发控制的精髓
- 隔离级别:读未提交、读已提交、可重复读、串行化的区别与应用
- 锁机制详解:共享锁、排他锁、意向锁、行锁、表锁的工作原理
- 并发问题解决:脏读、不可重复读、幻读的成因与防范
- 死锁分析与处理:如何预防和解决死锁问题
⭐ 特别提醒:事务和锁是一把"双刃剑"!用得好保证数据一致性,用不好会导致性能下降甚至死锁。本文将告诉你如何在安全性和性能之间找到平衡。
二、故事展开:小明的图书馆事务进化史
2.1 第一阶段:认识事务——借书的"打包服务"
2.1.1 什么是事务?
事务是将一组数据库操作(INSERT、UPDATE、DELETE)打包成一个不可分割的工作单元。就像小明提供的"借书打包服务":
传统方式(无事务):
- 检查库存
- 减少库存
- 添加借阅记录
- 如果第2步失败,第1步已经执行,数据就不一致了
事务方式:
- 开启事务
- 检查库存
- 减少库存
- 添加借阅记录
- 提交事务(全部成功)或回滚事务(任一步失败则全部撤销)
2.1.2 事务的ACID特性(用借书流程比喻)
ACID特性含义借书流程比喻技术实现Atomicity(原子性)不可分割,要么全做,要么全不做借书要么完全成功(检查库存→减库存→加记录),要么完全失败(恢复原状)Undo Log(回滚日志)Consistency(一致性)事务前后数据保持有效状态借书前后,库存+借阅记录的总书数不变约束检查(主键、外键、Check约束)Isolation(隔离性)并发事务互不干扰A借书时,B不能同时修改同一本书的库存锁机制 + MVCCDurability(持久性)事务完成后数据永久保存借书成功后,即使断电,记录也不会丢失Redo Log(重做日志)2.1.3 事务的生命周期与状态转换
- 【活跃状态】→【部分提交状态】→【提交完成】
- ↓ ↓
- └──→【失败状态】→【中止状态】
复制代码
- 活跃状态:事务正在执行中,可以正常操作
- 部分提交状态:最后一条语句执行完毕,等待刷盘确认
- 提交完成:事务成功结束,所有修改永久生效
- 失败状态:事务遇到错误,无法正常继续
- 中止状态:事务已回滚,所有修改被撤销
2.1.4 SQL实例:基本事务操作
- -- 开启事务(MySQL自动提交模式下需显式开启)
- START TRANSACTION;
- -- 或者
- BEGIN;
- -- 执行业务操作
- UPDATE books SET stock = stock - 1 WHERE book_id = 1 AND stock > 0;
- INSERT INTO borrow_records (book_id, reader_name, borrow_date)
- VALUES (1, '张三', CURDATE());
- -- 检查操作结果
- SELECT ROW_COUNT(); -- 检查受影响的行数
- -- 提交事务(全部成功)
- COMMIT;
- -- 如果出错,回滚事务(全部撤销)
- -- ROLLBACK;
复制代码 关键命令解释:
- START TRANSACTION / BEGIN:显式开启事务(默认自动提交模式下每条SQL都是一个事务)
- COMMIT:提交事务,使所有修改永久生效
- ROLLBACK:回滚事务,撤销所有未提交的修改
- ROW_COUNT():返回上一条SQL影响的行数(用于判断操作是否成功)
2.2 第二阶段:事务回滚与提交机制深度剖析
2.2.1 事务提交机制(COMMIT的内部工作)
提交过程的三阶段:
- 执行阶段:应用程序执行SQL语句,修改内存中的数据页
- 日志写入阶段:先将修改写入Redo Log Buffer,再刷写到磁盘Redo Log文件
- 数据刷盘阶段:后台线程将脏页(修改过的数据页)异步刷写到磁盘
提交的关键命令执行流程:- -- 示例:小明处理借书事务的提交过程
- START TRANSACTION; -- ① 创建事务ID,分配回滚段
- UPDATE books SET stock = stock - 1 WHERE book_id = 1; -- ② 修改内存数据,写Undo Log
- -- - 记录旧值(stock=5)到Undo Log
- -- - 修改内存中数据为stock=4
- -- - 记录修改到Redo Log Buffer
- INSERT INTO borrow_records VALUES (1, '张三', '2023-09-01'); -- ③ 同上处理
- -- - 记录Undo Log(插入操作的逆操作)
- -- - 修改内存数据
- -- - 记录Redo Log
- COMMIT; -- ④ 提交事务
- -- - 将Redo Log Buffer中的内容刷写到磁盘(fsync)
- -- - 写入Commit标记到Redo Log
- -- - 释放事务持有的锁
- -- - 清理Undo Log(如果是只读事务可立即清理)
复制代码⭐ 核心知识点:MySQL采用WAL(Write-Ahead Logging)机制,即"先写日志,再写数据"。提交时首先确保Redo Log写入磁盘,数据页可以异步刷写。
2.2.2 事务回滚机制(ROLLBACK的内部工作)
回滚的两种场景:
- 主动回滚:应用程序调用ROLLBACK命令
- 被动回滚:事务执行中出现错误(如违反约束、死锁检测)
回滚的执行流程:- -- 示例:借书过程中发现库存不足,执行回滚
- START TRANSACTION;
- -- 第一步:检查库存
- SELECT stock FROM books WHERE book_id = 1; -- 返回stock=0
- -- 第二步:尝试扣减库存(条件不满足,实际不会执行)
- UPDATE books SET stock = stock - 1 WHERE book_id = 1 AND stock > 0; -- 影响行数=0
- -- 第三步:判断是否成功
- IF ROW_COUNT() = 0 THEN
- ROLLBACK; -- ④ 执行回滚
- -- 回滚过程:
- -- - 从Undo Log中读取修改前的旧值
- -- - 将数据恢复到修改前的状态
- -- - 释放事务持有的所有锁
- -- - 清理事务相关的临时数据
- SELECT '库存不足,借书失败' AS result;
- ELSE
- -- 继续后续操作...
- COMMIT;
- END IF;
复制代码 回滚的关键技术:
- Undo Log回放:按照与原始操作相反的顺序,依次执行逆操作
- 多级回滚:支持嵌套事务的部分回滚
- 回滚段管理:Undo Log存储在专门的回滚段中,支持并发访问
2.2.3 保存点(Savepoint)机制
保存点允许在事务中设置"检查点",可以回滚到指定位置而不影响之前的操作:- START TRANSACTION;
- -- 操作1:更新库存
- UPDATE books SET stock = stock - 1 WHERE book_id = 1;
- SAVEPOINT sp1; -- 设置保存点1
- -- 操作2:添加借阅记录
- INSERT INTO borrow_records (book_id, reader_name) VALUES (1, '张三');
- SAVEPOINT sp2; -- 设置保存点2
- -- 操作3:更新读者积分(可能失败)
- UPDATE readers SET points = points + 10 WHERE reader_id = 100; -- 假设reader_id=100不存在
- -- 发现错误,回滚到保存点sp2(保留操作1和操作2)
- ROLLBACK TO SAVEPOINT sp2;
- -- 继续执行其他操作...
- UPDATE readers SET points = points + 10 WHERE reader_id = 101; -- 正确的读者ID
- COMMIT; -- 最终提交:包含操作1、2和最后的UPDATE
复制代码 <blockquote>
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |