数据库会并发执行一些事务,多个事务之间可能会对相同的数据进行读写。如果什么都不做,那么可能就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。为了解决并发问题,mysql提供了隔离级别、锁、mvcc等。
在之前我们已经讲解过锁了,通过加锁是可以实现mysql不同的隔离级别并解决上述并发问题的。
而读-读
之间不会产生什么问题,写-写
之间只有用锁才能解决并发问题。那么我们目前需要讨论的只有读-写
或写-读
之间能否有除了加锁以外更高效的方式(应为数据被加了写锁,那么所有读的请求就都要等待。而如果先被加了读锁,写操作可能要等很久才能被执行)。mysql的innodb通过mvcc来实现并发读写
。
在读该文章时,您应该了解mysql事务、锁和undo日志
。
MVCC(Multiversion Concurrency Control),多版本并发控制。MVCC通过数据行的多个版本管理来实现数据库的并发控制。
MVCC没有正式标准,不同的DBMS中MVCC的实现方式可能是不同的,也不是普遍使用的。在mysql中目前只有innodb支持mvcc,其他存储引擎并不支持。
当前读 :
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。如共享锁select for share, 排他锁select for update,update,insert,delete等操作。快照读 :
不加锁的简单select都属于快照读
,即非阻塞读。它是基于mvcc的,所以读取到的不一定是数据的最新版本,可能是之前的历史版本。快照读的前提是隔离级别不是串行级别,在串行级别下的快照会退化成当前读。对于innodb存储引擎中的聚簇索引中的行格式,包含3个隐藏列(其中有一个是当没有主键时,自动生成的主键列)。
trx_id
每次一个事务对该记录进行改动时,会把事务id写到该隐藏列。roll_pointer
每次对某条聚簇索引记录进行改动时,会把旧版本写道undo日志中,然后这个隐藏列相当于一个指针,可以找到它之前版本的信息。对于READ UNCOMMITTED
隔离级别,由于可以读到未提交事务修改过的记录,所以之间读取记录的最新版本就可以。
对于SERIALIZABLE
隔离界别,innodb采用加锁的方式来访问记录。
所以mvcc只要是针对于RC
和RR
隔离级别,这两种隔离级别都必须保证读到的是已提交的事务。
MVCC的实现依赖于:隐藏字段、undo log 、ReadView
ReadView就是事务在使用mvcc进行快照读操作时产生的读视图。该视图记录了一些信息去保证在后续判断中该读到那个版本的数据。
ReadView中主要包含4个比较重要的内容:
creator_trx_id
创建该ReadView的事务id(只有在对表中记录改动时,才会为该事务分配id)trx_ids
在生成ReadView时当前系统中活跃的读写事务id列表up_limit_id
活跃的事务中最小事务idlow_limit_id
系统中最大的事务id+1当生成了ReadView后,在通过mvcc访问某条记录时,按下边步骤来判断记录的那个版本是可见的:
ReadView的creator_trx_id
相同,意味当前事务在访问自己修改过的记录,所以当前版本可以被事务访问。ReadView的up_limit_id
,表明生成该版本的事务在当前事务之前就已经提交了,所以该版本可以看到。ReadView的low_limit_id
,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被访问。up_limit_id
和low_limit_id
之间,那么就要判断在不在trx_ids
中。如果在,就说明该版本的事务还是活跃的不可被访问。如果不在,说明该版本的事务已经被提交,记录可以被访问。当不能访问当前版本的数据时,就会顺着版本链找到历史版本进行判断。如果最后一个版本也不可见,那么查询结果就查不出该记录。
在RC隔离级别下,每次读取数据前都会生成一个ReadView。还未提交的事务属于活跃事务,所以在RC隔离级别在无法看到未提交的数据,解决了脏读。
在RR隔离级别下,只会在第一次执行查询语句的时候生成一个ReadView,之后就不会重复生成了。所以RR级别下只会读到第一次读到的数据,解决了不可重复读。并且感觉上应该也是解决了幻读,应为读不到新插入的数据了(在不可重复度级别下,新增修改删除的列都感知不到)。
但是如果一开始是快照读,第二次是当前读,还是会存在幻读。所以mvcc解决了快照读的幻读问题,对于当前读还是有可能存在幻读的。