MYSQL MVCC实现原理

MySQL是现在最流行的关系型数据库(RDB)的选择,创建一个应用时,无论是用户数据还是订单数据,使用关系型数据库存储是最可靠稳定的选择,借助RDB提供的可靠性、事务等功能,为应用提供完善的支持。MySQL是开源软件,可以免费使用,MySQL在发展多年后越来越成熟,成为大部分公司的数据库首选。MySQL采用插件式的存储引擎架构,5.5版本后默认使用InnoDB存储引擎。

MySQL架构

MySQL从概念上可以分为四层:

  • 第一层是接入层: 不同语言的客户端通过mysql的协议与mysql服务器进行连接通信,接入层进行权限验证、连接池管理、线程管理等。
  • 第二层是服务层: 包括sql解析器、sql优化器、数据缓冲、缓存等。
  • 第三层是存储引擎层: mysql中存储引擎是基于表的。
  • 第四层是系统文件层: 保存数据、索引、日志等。 http://static.cyblogs.com/mysql-arch.png

MVCC

MVCC是Multi Version Concurrency Control的简称,代表多版本并发控制。为什么需要MVCC,还要从数据库事务的ACID特性说起。
相信很多朋友都了解ACID,它们分别代表了

  • Atomicity(原子性)
  • Consistency(一致性)
  • Isolation(隔离性)
  • Durability(持久性)

原子性表示一个事务的操作结果要么全部执行要么全部不执行。

一致性表示事务总是从一个一致的状态转换到另一个一致的状态。

隔离性表示一个事务的修改结果在什么时间能够被其他事务看到,SQL1992规范中对隔离性定义了不同的隔离级别,分为读未提交(READ UNCOMMITED),事务能够看到其他事务没有提及的修改,当另一个事务又回滚了修改后的情况又被称为脏读dirty read。 读已提交(READ COMMITTED),事务能够看到其他事务提交后的修改,这时会出现一个事务内两次读取数据可能因为其他事务提交的修改导致不一致的情况,称为不可重复读。 可重复读(REPEATABLE READ),在两次读取时读取到的数据的状态是一致的,和序列化(SERIALIZABLE)可重复读中可能出现第二次读读到第一次没有读到的数据,也就是被其他事务插入的数据,这种情况称为幻读phantom read,序列化级别中不能出现幻读。 隔离级别依次增强,但是导致的问题是并发能力的减弱。各种数据库厂商会对各个隔离级别进行实现。和Java中的多线程问题相同,数据库通常使用锁来实现隔离性。 最原生的锁,锁住一个资源后会禁止其他任何线程访问同一个资源。但是很多应用的一个特点都是读多写少的场景,很多数据的读取次数远大于修改的次数,而读取数据间互相排斥显得不是很必要。所以就使用了一种读写锁的方法,读锁和读锁之间不互斥,而写锁和写锁、读锁都互斥。这样就很大提升了系统的并发能力。之后人们发现并发读还是不够,又提出了能不能让读写之间也不冲突的方法,就是读取数据时通过一种类似快照的方式将数据保存下来,这样读锁就和写锁不冲突了,不同的事务session会看到自己特定版本的数据。当然快照是一种概念模型,不同的数据库可能用不同的方式来实现这种功能。

InnoDB与MVCC

MySQL中的InnoDB存储引擎的特性有,默认隔离级别REPEATABLE READ,行级锁,实现了MVCC,Consistent nonlocking read(默认读不加锁,一致性非锁定读),Insert Buffer,Adaptive Hash Index,DoubleWrite,Cluster Index。
上面列举了这么多,表示InnoDB有很多特性、很快。 InnoDB中通过UndoLog实现了数据的多版本,而并发控制通过锁来实现。
Undo Log除了实现MVCC外,还用于事务的回滚。

Redo log,bin log,Undo log

MySQL Innodb中存在多种日志,除了错误日志、查询日志外,还有很多和数据持久性、一致性有关的日志。
binlog,是mysql服务层产生的日志,常用来进行数据恢复、数据库复制,常见的mysql主从架构,就是采用slave同步master的binlog实现的,另外通过解析binlog能够实现mysql到其他数据源(如ElasticSearch)的数据复制。
redo log记录了数据操作在物理层面的修改,mysql中使用了大量缓存,缓存存在于内存中,修改操作时会直接修改内存,而不是立刻修改磁盘,当内存和磁盘的数据不一致时,称内存中的数据为脏页(dirty page)。为了保证数据的安全性,事务进行中时会不断的产生redo log,在事务提交时进行一次flush操作,保存到磁盘中,redo log是按照顺序写入的,磁盘的顺序读写的速度远大于随机读写。当数据库或主机失效重启时,会根据redo log进行数据的恢复,如果redo log中有事务提交,则进行事务提交修改数据。这样实现了事务的原子性、一致性和持久性。

Undo Log: 除了记录redo log外,当进行数据修改时还会记录undo log,undo log用于数据的撤回操作,它记录了修改的反向操作,比如,插入对应删除,修改对应修改为原来的数据,通过undo log可以实现事务回滚,并且可以根据undo log回溯到某个特定的版本的数据,实现MVCC。

redo log 和binlog的一致性,为了防止写完binlog但是redo log的事务还没提交导致的不一致,innodb 使用了两阶段提交
大致执行序列为

InnoDB prepare  (持有prepare_commit_mutex);  
write/sync Binlog;  
InnoDB commit (写入COMMIT标记后释放prepare_commit_mutex)。  

MVCC实现

innodb中通过B+树作为索引的数据结构,并且主键所在的索引为ClusterIndex(聚簇索引),ClusterIndex中的叶子节点中保存了对应的数据内容。一个表只能有一个主键,所以只能有一个聚簇索引,如果表没有定义主键,则选择第一个非NULL唯一索引作为聚簇索引,如果还没有则生成一个隐藏id列作为聚簇索引。 除了Cluster Index外的索引是Secondary Index(辅助索引)。辅助索引中的叶子节点保存的是聚簇索引的叶子节点的值。 InnoDB行记录中除了刚才提到的rowid外,还有trx_iddb_roll_ptrtrx_id表示最近修改的事务的id,db_roll_ptr指向undo segment中的undo log。 新增一个事务时事务id会增加,trx_id能够表示事务开始的先后顺序。

Undo log分为InsertUpdate两种,delete可以看做是一种特殊的update,即在记录上修改删除标记。 update undo log记录了数据之前的数据信息,通过这些信息可以还原到之前版本的状态。 当进行插入操作时,生成的Insert undo log在事务提交后即可删除,因为其他事务不需要这个undo log。 进行删除修改操作时,会生成对应的undo log,并将当前数据记录中的db_roll_ptr指向新的undo log undolog

数据可见性判断

CREATE TABLE `testunique` (  
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `uid` int(11) DEFAULT NULL,
  `ukey` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `id_uid` (`uid`),
  KEY `index_key` (`ukey`)
) ENGINE=InnoDB AUTO_INCREMENT=70 DEFAULT CHARSET=utf8;

隔离级别REPEATABLE READ

| session1 | session2 | | :-------------------------------------------- | :------------------------ | | begin; | | | | select * from testunique; | | insert into testunique values(NULL,NULL,1); | | | | select * from testunique; | | commit | | | | select * from testunique; | | | commit | | | select * from testunique; |

只有当session2 commit之后的查询才能查到session1插入的数据

事务可见性的处理过程: undo-view RR级别下一个事务开始后第一个snapshot read的时候,会将当期活动的事务id记录下来,记录到read view中。RC级别则是每次snapshot read都会创建一个新的read view
假设当前,read view中最大的事务id为tmax,最小为tmin。则判断一个数据是否可见以及对应的版本的方法为。 如果该行中的trx_id,赋值给tid,如果tid和当前事务id相等或小于tmin,说明是事务内发生的或开启前的修改,则直接返回该版本数据; 如果 trx_id大于tmax,则查看该版本的db_roll_ptr中的trx_id,赋值给tid并从头开始判断。如果tid小于tmax并且不在read view中,则返回,否则中回滚段中找出undo logtrx_id,赋值给tid从头判断。

所以可见性是,只有当第一次读之前提交的修改和自己的修改可见,其他的均不可见。

参考地址:

如果大家喜欢我的文章,可以关注个人订阅号。欢迎随时留言、交流。如果想加入微信群的话一起讨论的话,请加管理员简栈文化-小助手(lastpass4u),他会拉你们进群。

简栈文化服务订阅号