MVCC(Multi-Version Concurrency Control)机制来保证的,对一行数据的读和写两个 *** 作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过将所有 *** 作加锁互斥来实现的。
Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制
一、Undo日志版本链与Read View机制
undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链.
read-view机制 在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
注意:begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改 *** 作InnoDB表的语句,事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的,也就是执行select是不会生成事务id的只有执行update insert delete 才会生成
事务1、2、3开启(通常事务id是连续的)
并且假设数据库有
事务1:update test set c1=‘123’ where id=1;//事务id假设为100
事务2:update test set c1=‘666’ where id=5;//事务id假设为200
事务3:update test set name=‘lilei300’ where id=1;commit//事务id假设为300
这时候开启一个新的session去执行select语句
如 select name from account where id=1
根据read-view的解释未提交事务数组+最大的事务组成如上面上个中所示
事务1、事务2未提交,事务3id最大因此read-view为 [100,200] ,300
且查出来的name为lilei300.
问:查询name为lilei300的原理?
答案:mysql会拿到read-view里面数组的最小事务id去作为min_id
然后再拿到所有事务中最大的id去作为max_id,然后凡是小于min_id的事务id都归到已提交事务区域里,大于max_id归纳到未开始事务(将来可以有的)
单独有一个区域把未提交事务与已提交事务都划分进去,因此三个区域分别为
已经提交事务(里面事务id均小于最小未提交事务id)
未提交事务与已经提交事务(0-最大提交事务id)
未开始事务(最大事务id-正无穷)
当下一次select开始的时候,会根据undo日志链取数据
链为 1 lilei300 300 --------> 1 lilei 60(假设一开始)
先拿到lilei300 以及事务id300 然后根据区域划分去找 发现在
未提交事务与已提交事务区域 拿到read-view [100,200],300
判断300为数组之外的300即为可见,因此name=事务id为300的那条数据
lilei300
假设这时候事务1 在执行sql *** 作
update test set name=‘lilei1’ where id=1;
update test set name=‘lilei2’ where id=1;
问:再执行查询,name值为多少,执行undo日志链如何判断值
答:这时候undo日志链为
1 lilei2 100 ------> 1 lilei1 100-----> 1 lilei300 300—> 1 lilei 60
readView为[100,200],300
按版本链走 发现 事务id100 比对发现在数组里 为不可见,再走发现仍未100,仍在数组里不可见继续走,发现300在数组外为最大事务id取lilei300为name
假设这时候事务id100的事务提交且事务200将name在依次改为lilei3 lilei4
那么同上根据undo日志链去走name仍为lilei300,
每一个事物的readView都有一份,而undo日志只有一份。
二、Innodb引擎SQL执行的BufferPool缓存机制代码如下(示例):
update t set name='666' where id=1
1.根据索引从磁盘中找到id为1的数据,加载到缓存buffer pool中去
2.将这条数据的旧值存入undo日志(失败可以回滚,上面已经讲过)
3.更新缓存中的数据把旧值—>666
4.写入到redo日志的缓存里面( *** 作等等一系列)
5.准备提交事务(执行commit *** 作),redo日志缓存 *** 作写入磁盘.
6.准备提交事务(执行commit *** 作),service层中的binlog日志写入磁盘(所有引擎偶有写binlog日志)。
7.写commit标记写入到redo日志里面去,主要是为了保证redo日志与binlog日志数据一致(binlog是为了恢复磁盘的数据用的,所以是一定要保证写成功的)
8.随机写入磁盘,以page为单位写入(innodb后台会有一个io线程去不定时的随机写磁盘)
问:为什么mysql要有写入redo日志的 *** 作?
答:假设一个场景当1-7步 *** 作都做完之后实际上只有缓存buffer pool中的值被改为666,磁盘中还是旧值,这时候如果数据库挂掉了,但是java已经认为了commit成功了。这样数据就丢失了,因此有了redo日志,当数据库重启的时候mysql会用redo日志里面的数据恢复到buffer poll里面去,再给予后台线程不定时写入磁盘。
问:mysql为什么要设计这么复杂的机制流程?
答:效率问题,这样设计可以让 *** 作直接基于缓存buffer poll *** 作。如果直接基于磁盘进行增上改查的话,由于数据存储在磁盘上是随机存放的(随机IO),而日志里面是顺序存放的(顺序IO媲美内存 *** 作),顺序IO与随机IO的效率差异巨大,2-3个数量积。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)