你能讲下mysql 的mvcc 吗?|MySQL 系列(8)
作者 | 小汪哥写代码
来源 | 小汪哥(ID:xwgcoding)
“ 面试中经常遇到这个问题,那你知道吗?阅读本文,你将了解到:
互联网项目mysql一般用什么隔离级别?
MVCC的原理
MVCC 能不能解决幻读问题?”
前言
我们知道,所谓MVCC (Multi-Version Concurrency Control ,多版本并发控制)可以提高不同事物的读写、写读操作的并发执行的系统性能,那么为什么会有mvcc就要从事物并发执行会有什么问题开始聊了。
事务并发执行遇到的问题
第二部分的正文内容从这里开始。mysql 是一个c/s 的 服务器架构软件,客户端的每一个连接就是一个会话,正常来讲如果会话排队执行,那么就没有问题。但是这样性能也太差了。如果并发执行多个会话,就会舍弃一部分隔离性。一般情况下有以下几种隔离级别:
脏写(Dirty Write)
- 如果一个事务修改了另一个未提交事务修改过的数据,那就意味着发生了脏写
脏读(Dirty Read)
- 如果一个事务读到了另一个未提交事务修改过的数据,那就意味着发生了脏读
不可重复读(Non-Repeatable Read)
- 如果一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,那就意味着发生了不可重复读
幻读(Phantom)
- 如果一个事务先根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,那就意味着发生了幻读
SQL标准中的四种隔离级别
- READ UNCOMMITTED:未提交读。(RU)
- READ COMMITTED:已提交读。(RC)
- REPEATABLE READ:可重复读。(RR)
- SERIALIZABLE:可串行化。
RU 隔离级别下,可能发生脏读、不可重复读和幻读问题。
RC 隔离级别下,可能发生不可重复读和幻读问题,但是不可以发生脏读问题。
RR 隔离级别下,可能发生幻读问题,但是不可以发生脏读和不可重复读的问题。
SERIALIZABLE 隔离级别下,各种问题都不可以发生。性能最差。
数据库必须实现SQL标准吗?
不同的数据库厂商对SQL标准中规定的四种隔离级别支持不一样,比方说Oracle就只支持READ COMMITTED和SERIALIZABLE隔离级别。
MySQL的默认隔离级别为REPEATABLE READ (RR)。
互联网项目mysql一般用什么隔离级别?
在互联网项目中一般用读已提交(Read Commited)这个隔离级别,项目中是不用读未提交(Read UnCommitted)和串行化(Serializable)两个隔离级别,有以下两个原因:
- 采用读未提交(Read UnCommitted),一个事务读到另一个事务未提交读数据,这个不用多说吧,从逻辑上都说不过去!
- 采用串行化(Serializable),每个次读操作都会加锁,快照读失效,一般是使用mysql自带分布式事务功能时才使用该隔离级别!是强一致性事务,性能不佳!互联网的分布式方案,多采用最终一致性的事务解决方案!
为什么选读已提交(Read Commited)作为事务隔离级别?
- 在互联网项目中,读到其他已经提交的事物基本上是可以理解的。
- 在RR隔离级别下,条件列未命中索引会锁表!而在RC隔离级别下,只锁行。
- 因为MySQL的RR需要gap lock来解决幻读问题。而RC隔离级别则是允许存在不可重复读和幻读的。所以RC的并发一般要好于RR。而且导致出现死锁的几率比RC大的多!
- 在RC隔离级别下,半一致性读(semi-consistent)特性增加了update操作的并发性!“ 半一致性读:在 RC 事务隔离级别下,Update 语句可以利用到半一致性读的特性,会多进行一次判断,当 where 条件匹配到的记录与当前持有锁的事务中的记录不冲突时,就会提前释放 InnoDB 锁,虽然这样做违背了二阶段加锁协议,但却可以减少锁冲突,提高事务并发能力,是一种很好的优化行为。”
关于锁的问题我们后面分享
MVCC的原理
终于要讲MVCC了,mysql 应用这么广泛,和它的性能好有一定的关系,性能又和MVCC有关系。上面我们了解的 mysql 的事物隔离级别,其实我觉得MVCC 主要作用于RR 和RC 两种事物隔离级别。
版本链
想要了解MVCC 就必须知道版本链。
对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL的UNIQUE键时都不会包含row_id列):
- trx_id:每次一个事务对某条聚簇索引记录进行改动时,都会把该事务的事务id赋值给trx_id隐藏列。
- roll_pointer:每次对某条聚簇索引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。
每次对记录进行改动,都会记录一条undo日志,每条undo日志也都有一个roll_pointer属性 (INSERT操作对应的undo日志没有该属性,因为该记录并没有更早的版本),可以将这些undo日志都连起来,串成一个链表,所以现在的情况就像下图一样:
ReadView 数据结构
前面说为什么我觉得 MVCC 主要作用于RC 和RR 的隔离级别呢?
对于使用RU隔离级别的事物来说,由于可以读到未提交的事物修改过的的记录,所以直接读取记录的最新版本就行。
对于SERIALIZABLE的隔离级别,Innodb 使用加锁的方式来访问记录。
ReadView 数据结构几个重要的属性:
- m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
- min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
- max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
- creator_trx_id:表示生成该ReadView的事务的事务id。
版本数据是否可见的判断步骤:
- trx_id=creator_trx_id;如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- trx_id<MIN_TRX_ID;如果被访问版本的TRX_ID属性值小于READVIEW中的MIN_TRX_ID值,表明生成该版本的事务在当前事务生成READVIEW前已经提交,所以该版本可以被当前事务访问。< p>
- trx_id>max_trx_id;如果被访问版本的trx_id属性值大于或等于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- min_trx_id<TRX_ID<MAX_TRX_ID;如果被访问版本的TRX_ID属性值在READVIEW的MIN_TRX_ID和MAX_TRX_ID之间,那就需要判断一下TRX_ID属性值是不是在M_IDS列表中,如果在,说明创建READVIEW时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建READVIEW时生成该版本的事务已经被提交,该版本可以被访问。< p>
RR 和 RC 隔离级别的不同:
生成ReadView的时机不同:
- RC 在每一次进行普通SELECT操作前都会生成一个ReadView,每次生成新的m_ids、min_trx_id、max_trx_id、creator_trx_id 属性。所以不能可重复读。
- RR 只在第一次进行普通SELECT操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView。m_ids、min_trx_id、max_trx_id、creator_trx_id 属性是一样的。所以实现了可重复读。
MVCC 不能解决幻读问题
MVCC不能解决幻读问题,不然RR 的隔离级别就可以叫可幻读了。例如
begin;
#假设users表为空,下面查出来的数据为空
select * from users; #没有加锁
#此时另一个事务提交了,且插入了一条id=1的数据
select * from users; #读快照,查出来的数据为空
update users set name='mysql' where id=1;#update是当前读,所以更新成功,并生成一个更新的快照
select * from users; #读快照,查出来id为1的一条记录,因为MVCC可以查到当前事务生成的快照
commit;
可以看到前后查出来的数据行不一致,发生了幻读。所以说只有MVCC是不能解决幻读问题的,解决幻读问题靠的是间隙锁。关于锁的问题我们下一次分享。所以RR级别下想解决幻读问题,需要我们显式加锁。
可以看到,InnoDB通过MVCC很好的解决了读写冲突的问题,大大提升了数据库的并发能力。这样的MVCC 你了解了吗?