
目录一、并发执行的代价正确性与性能的两难二、丢失更新被无声覆盖的写入三、脏读窥见未提交的中间状态四、不可重复读同一查询的双面结果五、幻读变化的行集合六、四种隔离级别的防护矩阵七、结语隔离级别的选择哲学一、并发执行的代价正确性与性能的两难在上一篇中我们定义了事务的ACID属性其中隔离性的理想形态是可串行化——并发事务的执行效果等同于它们按某种顺序串行执行。如果所有事务都串行执行不存在任何并发异常但系统的吞吐量将被限制在单CPU的处理能力之内。在现代多核处理器和高并发连接的服务器环境中强制串行执行等于放弃了硬件提供的大部分计算能力。并发执行的核心矛盾由此产生允许事务并发执行以提升吞吐量就必然允许事务的操作在时间线上交错——而这种交错可能破坏隔离性产生串行执行中不可能出现的数据不一致。数据库管理系统并非简单地在“完全串行”与“完全无序”之间二选一而是提供了一组分级的隔离级别——每一级以更强的并发限制为代价关闭一类或多类并发异常的窗口。理解这些隔离级别的设计逻辑必须首先理解它们旨在防范的异常现象本身。这正是本文的任务在严格控制的时序场景中逐一实证四种并发异常的触发机制。二、丢失更新被无声覆盖的写入丢失更新是最直观的并发异常也是最容易被忽视的一种。它发生在两个并发事务读取同一行数据基于读取的值进行修改然后将修改结果写回——后写回的事务覆盖了先写回事务的修改导致先写回事务的更新如同从未发生过。考虑一个经典的库存扣减场景。某商品当前库存为100件。事务T₁意图扣减30件事务T₂意图扣减20件。正确的串行执行——无论T₁先于T₂还是T₂先于T₁——最终库存应为50件100 - 30 - 20。但在不加控制的并发交织中以下时序悄然发生时刻1T₁读取库存得到值100。时刻2T₂读取库存得到值100。时刻3T₁计算新库存70100-30写回数据库。T₁提交。时刻4T₂计算新库存80100-20写回数据库。T₂提交。T₁的修改在时刻4被T₂的写入无声覆盖。最终库存为80而非正确的50。T₁扣减的30件商品凭空消失——丢失更新的名称由此而来。丢失更新的触发条件是严格的两个事务必须都先读取同一数据项再基于读取的值进行修改并写回。如果T₂在T₁提交之后才读取库存即串行执行T₂将读到70而非100计算结果将是50。丢失更新是典型的写-写冲突——两个写入操作竞争同一数据项而第二个写入不知晓第一个写入的存在。防范丢失更新的标准手段是写锁。如果T₁在读取库存时对该行加上写锁T₂在试图读取同一行时将被阻塞直到T₁提交并释放锁。T₂随后读到的是T₁修改后的值70计算正确结果50。在SQL标准的隔离级别体系中即使是相对宽松的“读已提交”级别也要求写操作持有写锁直到事务结束因此丢失更新在大多数主流数据库的默认隔离级别下已被有效防范。三、脏读窥见未提交的中间状态脏读发生在事务读取了另一个事务尚未提交的修改而该修改随后被回滚——导致读取的数据如同从未存在过的“脏”数据。考虑一个银行转账场景。事务T₁从账户A向账户B转账100元操作为从A扣款100向B加款100。事务T₂在T₁的两个操作之间读取了A和B的余额。时刻1T₁从账户A扣款100A余额变为400尚未提交。时刻2T₂读取账户A余额得到400。时刻3T₂读取账户B余额得到200尚未加款。时刻4T₁向账户B加款100B余额变为300T₁提交。在这个时序中T₂看到的是T₁部分执行后的状态——A已扣款B尚未加款。两个账户余额之和为600400200而非转账前后应保持的700。T₂基于这个临时的不一致状态做出的决策如判断总资产为600元可能是错误的。脏读的另一种后果更为严重。如果T₁在时刻4选择回滚而非提交则T₂在时刻2和时刻3读取的所有数据——扣款400、未加款200——都是最终从未被提交过的“脏”数据。T₂可能在毫不知情的情况下将基于这些不存在的数据的决策持久化到数据库中造成永久性的逻辑错误。脏读的触发条件是一个事务读取了另一事务的未提交修改。防范手段是禁止事务读取未提交的数据——这要求读操作也需要获取读锁或在MVCC机制下读取事务开始时的快照版本。“读已提交”隔离级别正是以此命名事务只能读取已提交的数据从而从根本上杜绝了脏读。四、不可重复读同一查询的双面结果不可重复读是读-写冲突的一种表现。在一个事务的生命周期内如果它两次读取同一行数据而两次读取之间该行被另一个已提交的事务修改则两次读取的值不同——第一次读到的值无法被“重复读取”。考虑事务T₁需要两次读取账户A的余额以进行某种校验。事务T₂在T₁的两次读取之间修改了账户A的余额并提交。时刻1T₁读取账户A余额得到500。时刻2T₂将账户A余额更新为600T₂提交。时刻3T₁再次读取账户A余额得到600。T₁在时刻1和时刻3读到了同一行的两个不同值——不可重复读。T₁在时刻1读取的500已经在时刻2被T₂的提交所“过时”。如果T₁基于时刻1的500进行了某些计算而后续读取的600与这些计算不一致T₁的内部逻辑可能产生矛盾。不可重复读与脏读的本质区别在于脏读读到的是未提交的、可能被回滚的数据不可重复读两次读到的都是已提交的数据——第一次读到的是T₂提交前的旧值第二次读到的是T₂提交后的新值。从“只读已提交数据”的角度两次读取都没有读到“脏”数据因此不可重复读在“读已提交”隔离级别下是被允许的。防范不可重复读要求更高的隔离级别——“可重复读”。在该级别下事务在第一次读取某行时锁定该行或锁定其版本直到事务结束其他事务无法修改该行。这样T₁的两次读取必然返回相同的值。五、幻读变化的行集合幻读是不可重复读的变体和升级。不可重复读针对的是同一行的值发生变化而幻读针对的是查询结果的行集合发生变化——在同一个事务内两次执行相同的范围查询第二次返回了第一次未出现的行幻影行或第一次出现的行在第二次消失了。考虑事务T₁需要两次查询“部门编号为D1的所有员工”以执行某种汇总。事务T₂在T₁的两次查询之间向D1部门插入了一名新员工并提交。时刻1T₁查询部门D1的所有员工返回10行。时刻2T₂向部门D1插入一名新员工T₂提交。时刻3T₁再次查询部门D1的所有员工返回11行——多出一个“幻影”行。T₁在时刻1基于10行做出的汇总计算与时刻3查询到的11行事实产生了矛盾。幻读的破坏性不在于单个行的值变化而在于行集合本身的边界发生了变化——这对涉及聚合查询、约束检查和报表生成的业务逻辑尤为致命。幻读的触发条件是一个事务在执行范围查询后另一个事务在该范围内插入了符合条件的新行或删除了符合条件的行。防范幻读是四种隔离级别中最高级别——“可串行化”——的核心使命。标准的手段是间隙锁——不仅锁定已存在的行还锁定索引中符合条件的键值范围之间的间隙阻止其他事务在间隙中插入新行。六、四种隔离级别的防护矩阵上述四种并发异常构成了隔离级别分级的逻辑依据。SQL标准定义了四种隔离级别每一级逐层关闭异常窗口读未提交是最低的隔离级别。它允许事务读取未提交的数据因此无法防范任何并发异常——丢失更新、脏读、不可重复读和幻读均可能发生。实践中极少使用仅在允许读取近似值的极少数场景如监控仪表盘中有其应用价值。读已提交要求事务只能读取已提交的数据从而杜绝了脏读。但不可重复读和幻读仍然可能发生。这是许多数据库系统的默认隔离级别如PostgreSQL、Oracle它在并发性能和数据正确性之间取得了适度的平衡。可重复读要求事务在执行期间已读取的行在事务结束前不会被其他事务修改。这杜绝了不可重复读但幻读仍然可能发生。MySQL的InnoDB引擎通过间隙锁在可重复读级别下也防范了幻读因此InnoDB的可重复读实际上达到了可串行化的部分防护能力。可串行化是最高隔离级别。它完全杜绝了上述所有并发异常保证并发事务的执行效果等同于某种串行顺序。其实现手段最为严格——基于严格两阶段锁协议或可串行化快照隔离——并发性能也最低。这四种级别的划分可以用一个防护矩阵来总结读未提交无任何防护读已提交防护脏读可重复读防护脏读不可重复读可串行化防护全部四种异常。每一个级别的提升都以更严格的并发控制和更高的锁竞争为代价选择哪一级别取决于业务场景对数据正确性的要求与对系统吞吐量的期望。七、结语隔离级别的选择哲学并发异常不是数据库理论中的边缘案例而是在高并发生产环境中每天都在发生的事情。一个在功能测试中完美运行的应用可能在并发负载下暴露出丢失更新或不可重复读导致的逻辑错误——这些错误往往难以复现、难以调试因为它们的触发依赖于精确的并发时序。隔离级别的选择本质上是一个风险管理决策。金融交易系统通常要求可串行化以确保账户余额的绝对正确。社交媒体平台的信息流查询可能接受可重复读或读已提交因为偶尔读到稍旧的数据对用户体验影响有限。电商库存系统则往往在读已提交的基础上通过应用层的乐观锁如版本号检查来补偿丢失更新的防护。下一篇我们将深入并发控制的核心机制——基于锁的并发控制。两阶段锁协议如何从理论上保证可串行化锁的粒度如何从表级到行级逐步细化以及锁的广泛使用必然带来的死锁问题如何被检测和化解。