MVCC

数据库中的MVCC多版本并发控制(快照隔离)可以避免事务并发时的脏读、不可重复读以及幻读的问题,但是却无法避免丢失更新以及写入偏差的问题。

丢失更新和写入偏差

  • 丢失更新:两个事务并发读取同一记录,并在此基础上修改记录,并将其写回数据库,第二次写入的结果会覆盖第一次写入的结果,导致数据库状态不一致。(丢失更新不仅出现在数据库中,在应用程序多线程并发修改变量、分布式系统多主复制和无主复制中都会遇见)
    例如:以下两个事务,对A变量进行读取10,T1事务将其减5,T2事务将其减8,两事务提交之后,A的状态为2,即T1事务的更新丢失了。如果A代表账户余额,这种错误是不可接受的。
  • java数据并发入库没有全部入库的问题 数据库并发写入_java数据并发入库没有全部入库的问题

  • 写入偏差:写入偏差可视为丢失更新问题的一般化,如果两个事务读取相同的记录集,然后更新记录集,不同的事务可能更新(插入、删除)不同的记录,则可能发生写入偏差,导致数据库状态不一致或者不符合约束条件。
    比如:1.医院规定必须至少有一名医生在值班。而两个值班医生同时进行请假事务,由于快照隔离,两个医生在检查当前值班医生数量时结果都为2,所以他们都可能请假。但是两个事务提交之后,却没有医生值班了,违反了医院的规定。2.两个用户同时修改唯一用户名,由于快照隔离,两人都发现数据库中无重复用户名,则修改成功。两个修改事务提交后,却出现用户名冲突。
  • java数据并发入库没有全部入库的问题 数据库并发写入_提交事务_02

  • 如果并发事务没有先读取记录集的值,而是直接写入新的记录集值,个人认为不属于丢失更新或者写入偏差,因为这导致的数据库不一致,是用户的错误逻辑所导致的,不是事务并发的原因。即使时事务串行,第二个写入还是会直接覆盖第一个写入。

丢失更新和写入偏差的原因是相同的:并发事务首先查询得到了相同的记录集,然后根据此记录集进行更新、插入、删除等动作。由于快照隔离,事务感受不到其他事务对记录集的更改,所以可以成功提交事务。但是并发事务的修改却出现了矛盾,导致数据库不一致。

《设计数据密集型应用》书中这样说:

java数据并发入库没有全部入库的问题 数据库并发写入_java数据并发入库没有全部入库的问题_03

解决方法

CAS

比较并设置(CAS, Compare And Set)是一种原子操作,此操作的目的是为了避免丢失更新:只有当前值从上次读取时一直未改变,才允许更新发生。如果当前值与先前读取的值不匹配,则更新不起作用,且必须重试读取-修改-写入序列。但是在提供快照隔离的事务中不起作用

显示锁定

由于并发事务的更改都是基于读取到的记录集,所以只要事务对读取到的记录集加排他锁,然后再对记录集更新,提交事务之后才释放排他锁。这样并发事务就只能读取到其他事务以及提交后的数据,则不会出现不一致的状态。

比如对于医生请假事务,读取并锁定所有医生的值班状态,修改自己的值班状态为请假,提交事务后释放锁。则第二个医生在请假事务中读取所有医生值班状态会阻塞,直到上一个事务提交,发现只剩自己一个人值班,则无法请假。

序列化隔离级别

使用事务的序列化隔离级别可以避免丢失更新和写入偏差。

幻读与写入偏差

从《设计数据密集型应用》看到,快照隔离避免了只读查询中幻读,但是对于读写事务中,幻读可能会导致特别棘手的写入偏差情况。

这里的意思表示在快照隔离的情况下,对于读写事务依然有可能出现幻读。这其实是与我之前的认知有所不同的,后来仔细查看了之后发现这里对幻读的定义似乎有所不同:
- 我之前对幻读的认知是,事务中进行两次范围查询,第二次查询到第一次查询没有的记录,称之为幻读。这样快照隔离是可以解决幻读的。
- 书中表示的读写事务幻读,是将事务中的写操作也当成一次查询操作,由于写操作在数据库内部是需要进行当前读的(当前读可以读取最新提交的数据,而不是快照的数据),所以写操作是可以感受到新插入的数据,导致幻读。这种幻读是快照隔离无法解决的,MySQL RR隔离级别中可使用间隙锁解决,即还是显示锁定方法。但是我不明白为什么书里说幻读导致了写入偏差,因为我觉得在写入时能够感受到其他事务的插入,反而导致本事务不能进行不一致的修改,从而事务回滚,避免了写入偏差。

但是我觉得只要明白其中的原理就可:
对于快照隔离(MVCC),事务中的快照读只能看到快照数据和当前事务的修改。而对于当前读则能看到最近提交事务后的数据。