MySQL事务与锁机制详解:保障数据一致性
做了这么多年数据库相关工作,MySQL的事务和锁机制一直是被问得最多、也最容易翻车的一块。不管是开发写业务代码时遇到死锁报错,还是DBA排查线上锁等待导致的慢查询,归根结底都在跟这两个概念打交道。很多面试题喜欢问“MySQL怎么保证数据一致性”,说白了,答案就藏在这套事务隔离级别、MVCC快照读和各类锁的协同运作里。
这篇文章我从实际使用角度出发,把MySQL事务与锁机制完整梳理一遍。适合刚接触MySQL的开发者建立整体认知,也适合有一定经验的老手查漏补缺,特别是那些平时只在应用层写CRUD、没怎么关注过底层隔离和并发控制的同学,看完应该能少踩不少坑。
1. 事务机制:一致性从哪来
1.1 ACID到底在说什么
事务这概念,理论上讲就是一组SQL操作要么全部成功、要么全部失败,不能只执行一半。MySQL里通过ACID四个特性来保障这件事,但很多人背了这四个字母却不知道它们各自靠什么机制实现。
先拆开看:
- 原子性(Atomicity):一个事务里的所有操作,要么全部提交,要么全部回滚。靠的是undo log,事务执行过程中如果出错,MySQL会根据undo log里记录的反向操作把数据恢复到事务开始前的样子。
- 一致性(Consistency):事务执行前后,数据库的完整性约束不能被破坏。这个其实是终极目标,由其他三个特性共同支撑,再加上约束、触发器这些数据库自身规则。
- 隔离性(Isolation):多个事务并发执行时,互相之间不能产生干扰。靠的是锁机制和MVCC多版本并发控制。
- 持久性(Durability):事务一旦提交,结果就永久保存,即使数据库崩溃也不能丢。靠的是redo log,事务提交时先把日志刷到磁盘,数据页随后再落盘。
市面上常见的误区是把InnoDB的“双写缓冲”“redo log刷盘策略”当成事务机制本体,其实它们只是保障持久性的具体工程手段。理解ACID时心里要有一条线:每个特性都有对应的底层组件在支撑,而不是一个抽象概念悬在空中。
1.2 事务的提交与回滚流程
一个事务从begin到commit,中间发生了什么,这个流程很多人没仔细看过。我画个简化版描述:
开启事务后,每次数据修改操作都会产生undo log和redo log。undo log记录的是“如果回滚,该怎么改回去”;redo log记录的是“这次改了哪个页、改成了什么”。这两份日志各有各的用途,数据真正写进磁盘数据页之前,redo log会先落盘。
接下来是commit的关键点:MySQL采用两阶段提交来协调redo log和binlog的一致性。prepare阶段写redo log并标记状态,然后写binlog,最后commit阶段把redo log状态改为提交。这中间任何一个环节崩溃,MySQL都能通过日志判断是回滚还是继续提交,确保主从复制场景下数据不会出现主库和从库各执一词的情况。
实际工作中最常见的坑是:一个事务里执行了大量更新操作却不提交,导致undo log膨胀、锁持有时间过长,后续会话全部堵在锁等待上。我自己就处理过一次事故,开发在测试环境跑了一个批量更新脚本,循环几十万次,每次都开事务但不提交,结果数据库的连接池直接被耗尽,所有业务请求全部超时。所以事务不是开得越久越好,小而快才是原则。
1.3 隔离级别的默认值由谁决定
MySQL InnoDB默认的隔离级别是Repeatable Read(可重复读),和Oracle、PostgreSQL默认的Read Committed不一样。这个选择有历史原因:MySQL的binlog在statement格式下,只有在Repeatable Read级别才能保证主从复制的一致性。
不过要注意,MySQL的Repeatable Read在特定的加锁读场景下仍然可能出现幻读(后面会细说)。网上很多文章说“MySQL的RR级别完全解决了幻读”,这话只对了一半——快照读不会幻读,但如果用了SELECT ... FOR UPDATE或LOCK IN SHARE MODE这类当前读,在RR级别下如果不走唯一索引,幻读依然存在。
隔离级别的设置方式有两种,一种是set session/global transaction isolation level,另一种是在配置文件中写transaction-isolation = READ-COMMITTED。生产环境如果需要调整,运维上一般建议在配置文件中统一改,避免某个连接自己设置了不同级别导致行为不一致。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 并发问题与隔离级别:脏读、不可重复读、幻读的真实场景
2.1 三种异常读现象逐个拆解
理论研究隔离级别时,教材上都会提脏读、不可重复读、幻读这三种现象。只背定义容易忘,我直接说几个实战中的例子。
脏读:事务A修改了一条数据但还没提交,事务B读到了这个修改后的值。然后事务A回滚了,事务B拿着一个不存在的值继续做业务逻辑。这个发生在Read Uncommitted级别下。现实中极少有人用这个级别,但面试时它是很好的引子。
不可重复读:事务A先查了一条记录,值还是100,事务B把它改成200并提交,事务A再查一次发现变成200了。同一个事务内两次查询结果不一致。这个发生在Read Committed级别下。
幻读:事务A查询某个范围的数据,比如SELECT * FROM orders WHERE amount > 100,第一次查出5条;事务B插入了一条新记录并提交;事务A再次执行同样查询,发现变成了6条。多出来的这一条就是“幻影行”。幻读和不可重复读的区别在于:不可重复读是同一行记录的内容变了,幻读是结果集的行数变了。
理解这三个现象后,隔离级别就不难梳理了:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read Uncommitted | 可能 | 可能 | 可能 |
| Read Committed | 不会 | 可能 | 可能 |
| Repeatable Read | 不会 | 不会 | 可能(加锁读场景 |
