在上一篇文章中,我们探讨了数据库并发时可能出现的"脏读、不可重复读、幻读"三大问题。很多新手朋友看完后会产生一个疑问:既然并发会带来这么多麻烦,那数据库是怎么解决这些问题的呢?难道要我们自己写代码去加锁吗?
其实,数据库早就为我们准备好了一套完整的防御体系,这就是"事务隔离级别"。
隔离级别的本质,是数据库在并发事务执行时,对"一个事务能看到另一个事务多少修改"所制定的规则。这就好比在图书馆看书,隔离级别就是图书馆的管理规则。规则越严格,你看到的资料越准确(数据一致性越好),但你需要排队等待的时间就越长(并发性能越低)。
今天,我们就从新手的视角,用大白话把数据库的四大隔离级别及其底层的解决机制彻底盘明白。
一、 读未提交(Read Uncommitted):毫无防备的"裸奔"模式
这是最低级别的隔离,基本等于没有隔离。
底层机制:零隔离
在这个级别下,数据库不对数据的读取做任何限制。事务A修改了一行数据,这条修改会立即写入内存缓冲池。此时事务B来读取,数据库直接把内存中最新的值返回给B,完全不管事务A有没有点击"提交"按钮。
导致的结果:
- 脏读:事务A修改后还没提交(随时可能回滚),事务B就读取了这个中间状态。如果A回滚了,B读到的就是根本不存在的"脏数据"。
- 不可重复读与幻读:同样全部存在。
新手建议:
因为连最基本的脏读都无法避免,读到的数据可能是假的,所以在实际生产环境中,基本没有任何业务会使用这个级别。
二、 读提交(Read Committed,简称RC):每次查询都"重新拍照"
这是很多数据库(如Oracle、SQL Server)的默认隔离级别。它解决了脏读问题,但依然存在不可重复读和幻读。
底层机制:语句级别的快照
在RC级别下,数据库引入了MVCC(多版本并发控制)机制的基础版。你可以把MVCC想象成数据库的"时光机",它通过Undo Log(回滚日志)保存了数据的历史版本。
事务B在每一次执行SELECT语句时,都会生成一个全新的Read View(读视图)。你可以把Read View理解为事务B在查询瞬间戴上的一副"特定滤镜眼镜",这副眼镜决定了B能看到哪个版本的数据。
- 如果事务A正在修改某行且未提交,事务B执行SELECT时,通过Read View判断该数据未提交,就不会读最新值,而是去历史版本里找到上一个已提交的旧版本返回给B。这就解决了脏读。
- 等事务A提交后,事务B再次执行SELECT,此时会生成一副"新的眼镜"(新的Read View)。因为事务A已提交,新眼镜能看到最新值,所以B读到了修改后的数据。
为什么还会不可重复读?
因为RC级别是"每次SELECT都重新拍照(生成新视图)"。事务B第一次查询拍了一张照,期间事务A修改并提交了数据,事务B第二次查询时又拍了一张新照片,自然就看到了A修改后的新结果。同一个事务内两次结果不同,这就是不可重复读。
三、 可重复读(Repeatable Read,简称RR):一次拍照,全程复用
这是MySQL InnoDB引擎的默认隔离级别,也是我们在日常开发中最常接触的级别。它解决了脏读和不可重复读。
底层机制:事务级别的快照
RR级别是MVCC机制的完整形态。事务B在第一次执行SELECT时生成一个Read View(拍第一张照片),此后整个事务期间,所有的普通SELECT都复用这个Read View(一直看这第一张照片)。
- 不管其他事务怎么修改数据并提交,只要事务B不结束,B看到的永远是第一次查询时的数据快照。这就完美解决了不可重复读的问题。
深度剖析:RR级别下到底有没有幻读?
这是面试和实际开发中的深水区。在RR级别下,读操作被严格分为了两种:
- 快照读:普通的SELECT。读取历史版本,绝对不产生幻读。
- 当前读:加锁的SELECT(如 SELECT ... FOR UPDATE),以及所有的增删改(UPDATE, DELETE, INSERT)。这些操作必须读取数据库中最新已提交的数据,会绕过快照。
在RR级别下,如果发生"快照读 -> 当前读 -> 快照读"的穿插操作,依然会出现诡异的幻读现象。我们来看一个经典推演:
- 事务B执行普通SELECT,查询id>10的数据,发现0行。(此时生成Read View,记录当前活跃事务)
- 事务A插入了一行id=20的数据,并执行COMMIT。此时表中真实存在了id=20的数据。
- 事务B执行 UPDATE account SET balance = 0 WHERE id > 10;
关键点:UPDATE属于当前读,必须读最新数据。所以它绕过了Read View,直接读磁盘,发现了事务A刚提交的id=20的记录,并成功修改了这行记录。 - 事务B再次执行普通SELECT,查询id>10的数据。
诡异的现象:竟然多了一行id=20的数据。
底层原因:MVCC有一个规则,被本事务自己修改过的数据,对本事务的快照读可见。因为id=20这行数据已经被事务B自己UPDATE过,它的事务ID变成了事务B的ID,事务B的Read View能看到自己修改的记录。所以这行"幽灵数据"就出现了。
虽然MySQL在RR级别下通过间隙锁(Next-Key Lock)在很大程度上了规避了幻读,但在上述特定的穿插操作下,幻读漏洞依然无法完全避免。
四、 串行化(Serializable):排队模式,终极杀器
这是最高级别的隔离,能解决所有并发问题,但代价是极差的并发性能。
底层机制:强制加锁,彻底放弃并发
在这个级别下,MVCC机制退场,数据库退回到最纯粹的基于锁的并发控制。
- 所有的SELECT查询,数据库会在底层自动将其转换为 SELECT ... LOCK IN SHARE MODE(加共享锁/S锁)。
- 所有的修改操作加排他锁/X锁。
- 锁的冲突规则:S锁与S锁兼容(允许并发读),但S锁与X锁互斥,X锁与X锁互斥。
导致的结果:
事务B在查询某范围或某行时,如果事务A正在修改相同范围或行,事务B的查询会被直接阻塞,直到事务A提交释放锁。强制所有事务排队串行执行,从根源上消除了数据不一致的可能。由于性能损耗太大,只在极其严苛的金融核心交易场景下才会考虑使用。
五、 新手实战指南:到底该选哪个?
了解了底层原理,我们在实际项目中该如何选择呢?
- 坚守默认配置:对于99%的互联网业务系统,直接使用MySQL默认的"可重复读(RR)"级别即可。它在一致性和并发性能之间取得了最好的平衡。
- 理解当前读的风险:在RR级别下编写复杂业务逻辑时,尽量避免在同一个事务中混合使用"普通查询"和"加锁查询/更新操作",以防触发上述的幻读漏洞。
- 不要盲目追求串行化:除非你的业务对数据一致性的要求达到了"哪怕系统慢到卡顿也绝对不能错一分钱"的地步,否则千万不要在生产环境使用串行化级别。
总结
数据库的四大隔离级别,本质上是并发性能与数据一致性之间的一场博弈。
从"读未提交"的毫无防备,到"读提交"的每次重新拍照,再到"可重复读"的一次拍照全程复用,最后到"串行化"的强制排队。理解了MVCC和Read View这些底层机制,你就不再是一个只会写CRUD的代码工,而是真正懂得了数据库是如何在幕后默默保护我们的数据的。