MVCC
数据库中的MVCC多版本并发控制(快照隔离)可以避免事务并发时的脏读、不可重复读以及幻读的问题,但是却无法避免丢失更新以及写入偏差的问题。
丢失更新和写入偏差
- 丢失更新:两个事务并发读取同一记录,并在此基础上修改记录,并将其写回数据库,第二次写入的结果会覆盖第一次写入的结果,导致数据库状态不一致。(丢失更新不仅出现在数据库中,在应用程序多线程并发修改变量、分布式系统多主复制和无主复制中都会遇见)
例如:以下两个事务,对A变量进行读取10,T1事务将其减5,T2事务将其减8,两事务提交之后,A的状态为2,即T1事务的更新丢失了。如果A代表账户余额,这种错误是不可接受的。 - 写入偏差:写入偏差可视为丢失更新问题的一般化,如果两个事务读取相同的记录集,然后更新记录集,不同的事务可能更新(插入、删除)不同的记录,则可能发生写入偏差,导致数据库状态不一致或者不符合约束条件。
比如:1.医院规定必须至少有一名医生在值班。而两个值班医生同时进行请假事务,由于快照隔离,两个医生在检查当前值班医生数量时结果都为2,所以他们都可能请假。但是两个事务提交之后,却没有医生值班了,违反了医院的规定。2.两个用户同时修改唯一用户名,由于快照隔离,两人都发现数据库中无重复用户名,则修改成功。两个修改事务提交后,却出现用户名冲突。 - 如果并发事务没有先读取记录集的值,而是直接写入新的记录集值,个人认为不属于丢失更新或者写入偏差,因为这导致的数据库不一致,是用户的错误逻辑所导致的,不是事务并发的原因。即使时事务串行,第二个写入还是会直接覆盖第一个写入。
丢失更新和写入偏差的原因是相同的:并发事务首先查询得到了相同的记录集,然后根据此记录集进行更新、插入、删除等动作。由于快照隔离,事务感受不到其他事务对记录集的更改,所以可以成功提交事务。但是并发事务的修改却出现了矛盾,导致数据库不一致。
《设计数据密集型应用》书中这样说:
解决方法
CAS
比较并设置(CAS, Compare And Set)是一种原子操作,此操作的目的是为了避免丢失更新:只有当前值从上次读取时一直未改变,才允许更新发生。如果当前值与先前读取的值不匹配,则更新不起作用,且必须重试读取-修改-写入序列。但是在提供快照隔离的事务中不起作用。
显示锁定
由于并发事务的更改都是基于读取到的记录集,所以只要事务对读取到的记录集加排他锁,然后再对记录集更新,提交事务之后才释放排他锁。这样并发事务就只能读取到其他事务以及提交后的数据,则不会出现不一致的状态。
比如对于医生请假事务,读取并锁定所有医生的值班状态,修改自己的值班状态为请假,提交事务后释放锁。则第二个医生在请假事务中读取所有医生值班状态会阻塞,直到上一个事务提交,发现只剩自己一个人值班,则无法请假。
序列化隔离级别
使用事务的序列化隔离级别可以避免丢失更新和写入偏差。
幻读与写入偏差
从《设计数据密集型应用》看到,快照隔离避免了只读查询中幻读,但是对于读写事务中,幻读可能会导致特别棘手的写入偏差情况。
这里的意思表示在快照隔离的情况下,对于读写事务依然有可能出现幻读。这其实是与我之前的认知有所不同的,后来仔细查看了之后发现这里对幻读的定义似乎有所不同:
- 我之前对幻读的认知是,事务中进行两次范围查询,第二次查询到第一次查询没有的记录,称之为幻读。这样快照隔离是可以解决幻读的。
- 书中表示的读写事务幻读,是将事务中的写操作也当成一次查询操作,由于写操作在数据库内部是需要进行当前读的(当前读可以读取最新提交的数据,而不是快照的数据),所以写操作是可以感受到新插入的数据,导致幻读。这种幻读是快照隔离无法解决的,MySQL RR隔离级别中可使用间隙锁解决,即还是显示锁定方法。但是我不明白为什么书里说幻读导致了写入偏差,因为我觉得在写入时能够感受到其他事务的插入,反而导致本事务不能进行不一致的修改,从而事务回滚,避免了写入偏差。
但是我觉得只要明白其中的原理就可:
对于快照隔离(MVCC),事务中的快照读只能看到快照数据和当前事务的修改。而对于当前读则能看到最近提交事务后的数据。