事务隔离级别本质上是数据库引擎在并发访问数据时,为了保证一致性而牺牲一部分并发性能的折中方案。不同的隔离级别对应不同的加锁策略和快照生成规则,理解这些机制比单纯记住四个级别名称更重要。下面这张图展示了并发事务交错执行时可能产生的异常现象,可以先直观感受一下。

四个隔离级别分别解决了哪些并发异常
SQL标准定义了三种典型的并发问题:脏读、不可重复读和幻读。脏读是指一个事务读到了另一个未提交事务修改过的数据;不可重复读是指同一事务内两次读取同一行数据,结果不同,因为期间有其他事务提交了修改;幻读则针对范围查询,同一事务内两次执行相同的范围条件,第二次多出或少了几行,这是其他事务插入或删除了符合范围的数据。
Read Uncommitted(读未提交)级别几乎不做任何隔离,允许脏读。它只在写操作时对数据行加排他锁,读操作完全不加锁,因此性能最高,但数据一致性最差。实际业务中极少采用,除非是一些允许读到未提交中间状态的统计或监控场景。
Read Committed(读已提交)是许多数据库的默认级别。它保证一个事务只能读到其他事务已经提交的数据,因此防止了脏读。但在同一个事务内,如果两次读取同一条记录,中间有其他事务提交了对该记录的更新,第二次读取就会看到新值,发生不可重复读。这个级别在Oracle和PostgreSQL中是默认选项,适合大多数OLTP业务。
Repeatable Read(可重复读)在Read Committed基础上,保证同一个事务内多次读取同一行数据结果一致。它通常通过MVCC的快照机制实现,事务开始时创建一个一致性快照,后续所有普通读都基于这个快照。但MySQL的InnoDB引擎在Repeatable Read级别下还通过间隙锁(Gap Lock)进一步防止了部分幻读,这是MySQL与标准SQL的一个差异点。标准SQL认为Repeatable Read仍然可能出现幻读,但InnoDB实际上解决了大部分幻读场景。
Serializable(串行化)是最严格的隔离级别。它强制事务按顺序执行,等效于所有事务串行化提交。读操作也会加共享锁,写操作加排他锁,因此完全杜绝脏读、不可重复读和幻读,但并发度极低,容易引发大量锁等待和死锁,只在金融对账等强一致场景中使用。
标准控制流程与代码示例
在具体数据库中设置隔离级别,通常有两种方式:全局设置或会话级设置。以MySQL为例,可以通过SET SESSION TRANSACTION ISOLATION LEVEL来修改当前会话的隔离级别,然后开启事务验证效果。下面演示一个Read Committed级别下发生不可重复读的场景。
-- 会话A:设置为读已提交 SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; START TRANSACTION; SELECT balance FROM accounts WHERE id = 1; -- 假设返回 1000 -- 会话B:同时开启事务并更新提交 START TRANSACTION; UPDATE accounts SET balance = 1500 WHERE id = 1; COMMIT; -- 回到会话A,再次读取同一行 SELECT balance FROM accounts WHERE id = 1; -- 此时返回 1500,不可重复读 COMMIT;
如果将会话A的隔离级别改为Repeatable Read,第二次查询仍然返回1000,因为事务启动时建立了快照,后续读都不受其他事务提交影响。在PostgreSQL中,默认隔离级别是Read Committed,但修改方式类似,使用BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;即可在事务开始前指定。
对于Serializable级别,标准做法是在事务开始时明确声明。下面是一个PostgreSQL中检测串行化冲突的例子:两个事务同时读取同一组数据并尝试更新,其中一个会被回滚并抛出serialization failure错误,需要应用程序重试。
-- 事务一 BEGIN ISOLATION LEVEL SERIALIZABLE; SELECT sum(balance) FROM accounts WHERE branch = '北京'; UPDATE accounts SET balance = balance - 100 WHERE id = 1; COMMIT; -- 事务二并发执行 BEGIN ISOLATION LEVEL SERIALIZABLE; SELECT sum(balance) FROM accounts WHERE branch = '北京'; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT;
事务二提交时,PostgreSQL会检测到两个事务读取了相同的谓词范围并尝试写入,从而中止事务二,报错信息类似ERROR: could not serialize access due to read/write dependencies among transactions。此时应用必须捕获异常并执行重试逻辑,这是使用Serializable级别必须配套的标准流程。
另一个常见的控制流程是设置事务自动提交行为。很多开发者习惯在代码中显式提交,但有些框架默认自动提交每条语句,这会破坏事务隔离的预期。推荐在事务开始时禁用自动提交,结束时统一提交或回滚。例如Java的JDBC代码中,connection.setAutoCommit(false)之后执行一系列SQL,最后connection.commit()。这个流程看似基础,但在高并发下如果忘记关闭自动提交,隔离级别设置后可能产生意想不到的部分提交结果。
常见使用误区与避坑指南
第一个误区是认为把隔离级别调到Serializable就能解决所有并发问题,并且不用关心代码逻辑。实际上Serializable不仅严重降低吞吐量,还要求应用层处理序列化失败重试。如果没有重试机制,事务会频繁报错,反而造成用户体验下降。正确的做法是根据业务需求选择最低满足一致性的隔离级别,例如对于金额扣减,Repeatable Read配合乐观锁或悲观行锁通常足够,无需全局串行化。
第二个误区是在长事务中错误地持有锁。假设一个事务在Repeatable Read级别下开启后,先执行了一个范围查询,然后进行大量业务计算,最后才提交。在MySQL InnoDB中,范围查询会持有间隙锁,阻止其他事务插入符合范围的记录,如果计算时间过长,其他插入事务会被阻塞甚至超时。解决思路是尽量缩短事务时间,或者将读操作放到事务外提前完成,只有写操作才放入事务并快速提交。
第三个误区是误以为MVCC快照读完全不阻塞写操作,或者相反的认为写操作也不会阻塞快照读。MVCC通过保存数据行的多个版本,让读操作读取旧版本而不阻塞新版本的写入,这确实减少了读写冲突。但写操作之间仍然会互相阻塞,例如两个事务同时更新同一行,第二个事务必须等待第一个事务提交或回滚。另外,MVCC的快照读虽然不阻塞,但快照本身会消耗存储空间,长事务会导致旧版本长时间无法回收,造成表膨胀和性能下降。所以不要让事务保持打开状态过久,尤其是涉及大量更新后不提交。
第四个常见的坑是在不同数据库之间直接套用隔离级别行为。比如MySQL的Repeatable Read能防止大部分幻读,但PostgreSQL的Repeatable Read不能完全防止幻读,它的快照机制不同。开发者从MySQL迁移到PostgreSQL时,如果仍然假设Repeatable Read能防止幻读,可能会在范围查询后插入数据时产生意外结果。最稳妥的方式是在目标数据库上用实际测试验证预期行为,而不是依赖文档中的理论描述。
最后一个容易忽视的细节是事务隔离级别与锁等待超时的关系。设置过高的隔离级别虽然换来一致性,但可能引入更多的锁等待。当多个事务互相等待对方释放锁时,数据库会检测并选择一个事务作为死锁牺牲品,强制回滚。应用层需要捕获死锁异常,并根据业务决定是否重试。一个常见的重试策略是随机退避后重新执行整个事务,避免立即重试再次冲突。