MySQL整理【03】事务隔离级别和MVCC
MySQL整理【03】事务隔离级别和MVCC
- 事务并发执⾏遇到的问题
- SQL标准中的四种隔离级别
- MVCC原理
- 版本链
- ReadView
- MVCC⼩结
事务并发执⾏遇到的问题
MySQL是⼀个客户端/服务器架构的软件
对于同⼀个服务器来说,可以有若⼲个客户端与之连接,每个客户端与服务器连接上之后,就可以称之为⼀个会话(Session)。
每个客户端都可以在⾃⼰的会话中向服务器发出请求语句,⼀个请求语句可能是某个事务的⼀部分,也就是对于服务器来说可能同时处理多个事务。
在事务简介的章节中我们说过事务有⼀个称之为隔离性的特性,理论上在某个事务对某个数据进⾏访问时,其他事务应该进⾏排队,当该事务提交之后,其他事务才可以继续访问这个数据。但是这样⼦的话对性能影响太⼤
我们既想保持事务的隔离性,⼜想让服务器在处理访问同⼀数据的多个事务时性能尽量⾼些,⻥和熊掌不可得兼,舍⼀部分隔离性⽽取性能。
怎么个舍弃法呢?我们先得看⼀下访问相同数据的事务在不保证串⾏执⾏(也就是执⾏完⼀个再执⾏另⼀个)的情况下可能会出现哪些问题:
-
脏写(Dirty Write):如果⼀个事务修改了另⼀个未提交事务修改过的数据,那就意味着发⽣了脏写,示意图如下:
-
脏读(Dirty Read):如果⼀个事务读到了另⼀个未提交事务修改过的数据,那就意味着发⽣了脏读,示意图如下:
-
不可重复读(Non-Repeatable Read):如果⼀个事务能读到另⼀个已经提交的事务修改过的数据,并且其他事务每对该数据进⾏⼀次修改并提交后,该事务都能查询得到最新值,那就意味着发⽣了不可重复读,示意图如下:
-
幻读(Phantom):如果⼀个事务先根据某些条件查询出⼀些记录,之后另⼀个事务⼜向表中插⼊了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另⼀个事务插⼊的记录也读出来,那就意味着发⽣了幻读,示意图如下:
SQL标准中的四种隔离级别
我们给这些问题按照严重性来排⼀下序:
脏写 > 脏读 > 不可重复读 > 幻读
我们上边所说的舍弃⼀部分隔离性来换取⼀部分性能在这⾥就体现在:设⽴⼀些隔离级别,隔离级别越低,越严重的问题就越可能发⽣。
所以制定了⼀个所谓的SQL标准,在标准中设⽴了4个隔离级别:
- READ UNCOMMITTED:未提交读。
- READ COMMITTED:已提交读。
- REPEATABLE READ:可重复读。
- SERIALIZABLE:可串⾏化。
脏写这个问题太严重了,不论是哪种隔离级别,都不允许脏写的情况发⽣。
InnoDB使⽤锁来保证不会有脏写情况的发⽣,也就是在第⼀个事务更新了某条记录后,就会给这条记录加锁,另⼀个事务再次更新时就需要等待第⼀个事务提交了,把锁释放之后才可以继续更新。
MySQL的默认隔离级别为REPEATABLE READ
MVCC原理
版本链
对于使⽤InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者⾮NULL的UNIQUE键时都不会包含row_id列):
- trx_id:每次⼀个事务对某条聚簇索引记录进⾏改动时,都会把该事务的事务id赋值给trx_id隐藏列。
- roll_pointer:每次对某条聚簇索引记录进⾏改动时,都会把旧的版本写⼊到undo⽇志中,然后这个隐藏列就相当于⼀个指针,可以通过它来找到该记录修改前的信息。
每次对记录进⾏改动,都会记录⼀条undo⽇志,每条undo⽇志也都有⼀个roll_pointer属性(INSERT操作对应的undo⽇志没有该
属性,因为该记录并没有更早的版本),可以将这些undo⽇志都连起来,串成⼀个链表,所以现在的情况就像下图⼀样
对该记录每次更新后,都会将旧值放到⼀条undo⽇志中,就算是该记录的⼀个旧版本,随着更新次数的增多,所有的版本都会被
roll_pointer属性连接成⼀个链表,我们把这个链表称之为版本链,版本链的头节点就是当前记录最新的值。另外,每个版本中还包含⽣成该版本时对应的事务id。
ReadView
对于使⽤READ UNCOMMITTED隔离级别的事务来说,由于可以读到未提交事务修改过的记录,所以直接读取记录的最新版本就好了;
对于使⽤SERIALIZABLE隔离级别的事务来说,设计InnoDB的⼤叔规定使⽤加锁的⽅式来访问记录;
对于使⽤READ COMMITTED和REPEATABLE READ隔离级别的事务来说,都必须保证读到已经提交了的事务修改过的记录,也就是说假如另⼀个事务已经修改了记录但是尚未提交,是不能直接读取最新版本的记录的,核⼼问题就是:需要判断⼀下版本链中的哪个版本是当前事务可⻅的。为此,设计InnoDB的⼤叔提出了⼀个ReadView的概念,这个ReadView中主要包含4个⽐较重要的内容:
-
m_ids:表示在⽣成ReadView时当前系统中活跃的读写事务的事务id列表。
-
min_trx_id:表示在⽣成ReadView时当前系统中活跃的读写事务中最⼩的事务id,也就是m_ids中的最⼩值。
-
max_trx_id:表示⽣成ReadView时系统中应该分配给下⼀个事务的id值。
注意max_trx_id并不是m_ids中的最⼤值,事务id是递增分配的。⽐⽅说现在有id为1,2,3这三个事务,之后id为3的事务提交了。那么⼀个新的读事务在⽣成ReadView时,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4。 -
creator_trx_id:表示⽣成该ReadView的事务的事务id。
只有在对表中的记录做改动时(执⾏INSERT、DELETE、UPDATE这些语句时)才会为事务分配事务id,否则在⼀个只读事务中的事务id值都默认为0。
有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可⻅:
-
如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它⾃⼰修改过的记录,所以该版本可以被当前事务访问。
-
如果被访问版本的trx_id属性值⼩于ReadView中的min_trx_id值,表明⽣成该版本的事务在当前事务⽣成ReadView前已经提交,所以该版本可以被当前事务访问。
-
如果被访问版本的trx_id属性值⼤于ReadView中的max_trx_id值,表明⽣成该版本的事务在当前事务⽣成ReadView后才开启,所以该版本不可以被当前事务访问。
-
如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断⼀下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时⽣成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时⽣成该版本的事务已经被提交,该版本可以被访问。
-
如果某个版本的数据对当前事务不可⻅的话,那就顺着版本链找到下⼀个版本的数据,继续按照上边的步骤判断可⻅性,依此类推,直到版本链中的最后⼀个版本。如果最后⼀个版本也不可⻅的话,那么就意味着该条记录对该事务完全不可⻅,查询结果就不包含该记录。
MVCC⼩结
所谓的MVCC(Multi-Version Concurrency Control ,多版本并发控制)指的就是在使⽤READ COMMITTD、REPEATABLE READ这两种隔离级别的事务在执⾏普通的SEELCT操作时访问记录的版本链的过程,这样⼦可以使不同事务的读-写、写-读操作并发执⾏,从⽽提升系统性能。
READ COMMITTD、REPEATABLE READ这两个隔离级别的⼀个很⼤不同就是:⽣成ReadView的时机不同
- 使⽤READ COMMITTED隔离级别的事务在每次查询开始时都会⽣成⼀个独⽴的ReadView。
- 使⽤REPEATABLE READ隔离级别的事务,只会在第⼀次执⾏查询语句时⽣成⼀个ReadView,之后的查询就不会重复⽣成了。
READ COMMITTD在每⼀次进⾏普通SELECT操作前都会⽣成⼀个ReadView;⽽REPEATABLE READ只在第⼀次进⾏普通SELECT操作前⽣成⼀个ReadView,之后的查询操作都重复使⽤这个ReadView就好了
PS:
- insert undo在事务提交之后就可以被释放掉了,⽽update undo由于还需要⽀持MVCC,不能⽴即删除掉。
- 为了⽀持MVCC,对于delete mark操作来说,仅仅是在记录上打⼀个删除标记,并没有真正将它删除掉。