找回密码
 立即注册
首页 业界区 业界 MySQL事务与锁机制实战——从ACID到并发控制 ...

MySQL事务与锁机制实战——从ACID到并发控制

宛蛲 2025-11-20 21:35:01
一、引言:小明的图书馆新挑战

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 事务的生命周期与状态转换
  1. 【活跃状态】→【部分提交状态】→【提交完成】
  2.      ↓              ↓
  3.      └──→【失败状态】→【中止状态】
复制代码

  • 活跃状态:事务正在执行中,可以正常操作
  • 部分提交状态:最后一条语句执行完毕,等待刷盘确认
  • 提交完成:事务成功结束,所有修改永久生效
  • 失败状态:事务遇到错误,无法正常继续
  • 中止状态:事务已回滚,所有修改被撤销
2.1.4 SQL实例:基本事务操作
  1. -- 开启事务(MySQL自动提交模式下需显式开启)
  2. START TRANSACTION;
  3. -- 或者
  4. BEGIN;
  5. -- 执行业务操作
  6. UPDATE books SET stock = stock - 1 WHERE book_id = 1 AND stock > 0;
  7. INSERT INTO borrow_records (book_id, reader_name, borrow_date)
  8. VALUES (1, '张三', CURDATE());
  9. -- 检查操作结果
  10. SELECT ROW_COUNT();  -- 检查受影响的行数
  11. -- 提交事务(全部成功)
  12. COMMIT;
  13. -- 如果出错,回滚事务(全部撤销)
  14. -- ROLLBACK;
复制代码
关键命令解释

  • START TRANSACTION / BEGIN:显式开启事务(默认自动提交模式下每条SQL都是一个事务)
  • COMMIT:提交事务,使所有修改永久生效
  • ROLLBACK:回滚事务,撤销所有未提交的修改
  • ROW_COUNT():返回上一条SQL影响的行数(用于判断操作是否成功)
2.2 第二阶段:事务回滚与提交机制深度剖析

2.2.1 事务提交机制(COMMIT的内部工作)

提交过程的三阶段

  • 执行阶段:应用程序执行SQL语句,修改内存中的数据页
  • 日志写入阶段:先将修改写入Redo Log Buffer,再刷写到磁盘Redo Log文件
  • 数据刷盘阶段:后台线程将脏页(修改过的数据页)异步刷写到磁盘
提交的关键命令执行流程
  1. -- 示例:小明处理借书事务的提交过程
  2. START TRANSACTION;  -- ① 创建事务ID,分配回滚段
  3. UPDATE books SET stock = stock - 1 WHERE book_id = 1;  -- ② 修改内存数据,写Undo Log
  4. --    - 记录旧值(stock=5)到Undo Log
  5. --    - 修改内存中数据为stock=4
  6. --    - 记录修改到Redo Log Buffer
  7. INSERT INTO borrow_records VALUES (1, '张三', '2023-09-01');  -- ③ 同上处理
  8. --    - 记录Undo Log(插入操作的逆操作)
  9. --    - 修改内存数据
  10. --    - 记录Redo Log
  11. COMMIT;  -- ④ 提交事务
  12. --    - 将Redo Log Buffer中的内容刷写到磁盘(fsync)
  13. --    - 写入Commit标记到Redo Log
  14. --    - 释放事务持有的锁
  15. --    - 清理Undo Log(如果是只读事务可立即清理)
复制代码
核心知识点:MySQL采用WAL(Write-Ahead Logging)机制,即"先写日志,再写数据"。提交时首先确保Redo Log写入磁盘,数据页可以异步刷写。
2.2.2 事务回滚机制(ROLLBACK的内部工作)

回滚的两种场景

  • 主动回滚:应用程序调用ROLLBACK命令
  • 被动回滚:事务执行中出现错误(如违反约束、死锁检测)
回滚的执行流程
  1. -- 示例:借书过程中发现库存不足,执行回滚
  2. START TRANSACTION;
  3. -- 第一步:检查库存
  4. SELECT stock FROM books WHERE book_id = 1;  -- 返回stock=0
  5. -- 第二步:尝试扣减库存(条件不满足,实际不会执行)
  6. UPDATE books SET stock = stock - 1 WHERE book_id = 1 AND stock > 0;  -- 影响行数=0
  7. -- 第三步:判断是否成功
  8. IF ROW_COUNT() = 0 THEN
  9.     ROLLBACK;  -- ④ 执行回滚
  10.     -- 回滚过程:
  11.     -- - 从Undo Log中读取修改前的旧值
  12.     -- - 将数据恢复到修改前的状态
  13.     -- - 释放事务持有的所有锁
  14.     -- - 清理事务相关的临时数据
  15.     SELECT '库存不足,借书失败' AS result;
  16. ELSE
  17.     -- 继续后续操作...
  18.     COMMIT;
  19. END IF;
复制代码
回滚的关键技术

  • Undo Log回放:按照与原始操作相反的顺序,依次执行逆操作
  • 多级回滚:支持嵌套事务的部分回滚
  • 回滚段管理:Undo Log存储在专门的回滚段中,支持并发访问
2.2.3 保存点(Savepoint)机制

保存点允许在事务中设置"检查点",可以回滚到指定位置而不影响之前的操作:
  1. START TRANSACTION;
  2. -- 操作1:更新库存
  3. UPDATE books SET stock = stock - 1 WHERE book_id = 1;
  4. SAVEPOINT sp1;  -- 设置保存点1
  5. -- 操作2:添加借阅记录
  6. INSERT INTO borrow_records (book_id, reader_name) VALUES (1, '张三');
  7. SAVEPOINT sp2;  -- 设置保存点2
  8. -- 操作3:更新读者积分(可能失败)
  9. UPDATE readers SET points = points + 10 WHERE reader_id = 100;  -- 假设reader_id=100不存在
  10. -- 发现错误,回滚到保存点sp2(保留操作1和操作2)
  11. ROLLBACK TO SAVEPOINT sp2;
  12. -- 继续执行其他操作...
  13. UPDATE readers SET points = points + 10 WHERE reader_id = 101;  -- 正确的读者ID
  14. COMMIT;  -- 最终提交:包含操作1、2和最后的UPDATE
复制代码
<blockquote>

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册