学习总结与分享-MySQL的MVCC(多版本并发控制)浅析

我们在平时编码操作数据库时,通常会有一个事务A读取一行数据,而又有另一个事务B需要写同一条数据,而为了保证事务A在每次读取时保持一致性和原子性,在传统的事务的锁机制下,这肯定是不允许的,事务B需要等待事务A执行结束后才可以执行,而当类似的需求并发过多的时候,就很容易产生锁阻塞、死锁或者事务一直在等待执行过慢导致报错等等情况,所以数据库就使用了MVCC这个概念来解决事务在并发下的效率问题。
什么是MVCC?
摘自百度百科:Multi-Version Concurrency Control 多版本并发控制,MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程语言中实现事务内存。
也可将MVCC看成行级别锁的一种妥协,它在许多情况下避免了使用锁,同时可以提供更小的开销。根据实现的不同,它可以允许非阻塞式读,在写操作进行时只锁定必要的记录。所以这也意味着MVCC都是针对于行级锁来进行的。
但是MySQL数据库的InnoDB引擎所用MVCC概念并不是真正的MVCC,InnoDB只是借了MVCC这个名字,提供了读的非阻塞而已
理想MVCC与InnoDB的MVCC:
MVCC有下面几个特点:
每行数据都存在一个版本,每次数据更新时都更新该版本
修改时Copy出当前版本随意修改,各个事务之间无干扰
保存时比较版本号,如果成功(commit),则覆盖原记录;失败则放弃copy(rollback)
就是每行都有版本号,保存时根据版本号决定是否成功,听起来含有乐观锁的味道,而InnoDB的实现方式是:
事务以排他锁的形式修改原始数据
把修改前的数据存放于undo log,通过回滚指针与主数据关联
修改成功(commit)啥都不做,失败则恢复undo log中的数据(rollback)
二者最本质的区别就在于在修改时是否使用排他锁。而说到了InnoDB的MVCC,就必然要提到上面说的undo log,这就又涉及到了MySQL的存储机制,其中不仅仅有undo log,还有redo log来控制数据的commit。

undo log 与 redo log:
redo log:
事务中所有操作会先写到redo log中,然后再同步到数据库文件中。所以数据库文件进行事务操作修改时,redo log肯定已经记录了所有事务操作,此时即使数据库挂掉,事务操作也都已经持久化到redo log中了,数据库恢复后可以继续执行剩下操作。
redo log有两部分组成,redo log buffer与redo log file。如果每个事务的redo log都实时写到file中,再写到数据文件中,那么性能会比较差,所以可以先把一定时间间隔中的事务操作记录到buffer中,然后统一刷新到file中(此时数据库文件的刷新不一定晚于重做日志文件的刷新)。
redo log使用buffer缓存,丢失了数据持久性,数据库宕机时,没有持久化到redo log file中的事务操作也会丢失。此时数据库数据需要回滚到这些丢失事务之前的状态,undo log正好记录了事务之前的状态。
也就是说在一个事务中的每一次SQL操作之后都会写入一个redo log到buffer中,在最后COMMIT的时候,必须先将该事务的所有日志写入到redo log file进行持久化(这里的写入是顺序写的),待事务的COMMIT操作完成才算完成。
在这里插入图片描述
undo log:
undo log记录了事务提交之前的数据状态。所以当事务操作同步到数据文件仅仅执行了一半就失败了,恢复后无法找到剩余事务操作,那就只好回滚到事务执行前了。这是就可以使用undo log了。
不同于redo log存放在单独文件中,undo log存放在数据库内部特殊的段中(undo segment),这个段位于共享表空间中。可以知道,undo log必然发生在事务执行之前,所以事务操作执行开始了,undo log必然已经存在了。
那么每个事务如果需要回滚,数据库如何确定undo log的数据存储在哪里呢,又是如何准确的回滚呢。这里就需要说到一些隐藏的字段:
DB_TRX_ID:最近更改该行数据的事务ID。
DB_ROLL_PTR:undo log的指针,用于记录之前历史数据在undo log中的位置。
在这里插入图片描述
大体知道了MySQL的存储机制和过程了之后,那又是怎么根据undo log来实现mvcc的呢,简单来说就是根据每个事务的ID也就是版本号存入undo log对应的位置中,每次读的时候都会读取自己版本相应数据,而当其他事务在修改该条记录或新增区间的记录后,读取的事务再次读取时也会根据活跃的事务的版本数组来计算,从而还是只读自己当前事务的数据,所以MySQL的事务在重复读的隔离级别下,由于MVCC的关系,也已经解决了幻读的问题。下面介绍一下具体的实现原理
MySQL的MVCC实现原理:
可见性比较算法(这里每个比较算法后面的描述是建立在rr级别下,rc级别也是使用该比较算法,此处未做描述)
设要读取的行的最后提交事务id(即当前数据行的稳定事务id)为 trx_id_current
当前新开事务id为 new_id
当前新开事务创建的快照read view 中最早的事务id为up_limit_id, 最迟的事务id为low_limit_id(注意这个low_limit_id=未开启的事务id=当前最大事务id+1)
比较:
1.trx_id_current < up_limit_id, 这种情况比较好理解, 表示, 新事务在读取该行记录时, 该行记录的稳定事务ID是小于, 系统当前所有活跃的事务, 所以当前行稳定数据对新事务可见, 跳到步骤5.
2.trx_id_current >= trx_id_last, 这种情况也比较好理解, 表示, 该行记录的稳定事务id是在本次新事务创建之后才开启的, 但是却在本次新事务执行第二个select前就commit了,所以该行记录的当前值不可见, 跳到步骤4。
3.trx_id_current <= trx_id_current <= trx_id_last, 表示: 该行记录所在事务在本次新事务创建的时候处于活动状态,从up_limit_id到low_limit_id进行遍历,如果trx_id_current等于他们之中的某个事务id的话,那么不可见, 调到步骤4,否则表示可见。
4.从该行记录的 DB_ROLL_PTR 指针所指向的回滚段中取出最新的undo-log的版本号, 将它赋值该 trx_id_current,然后跳到步骤1重新开始判断。
5.将该可见行的值返回。
(摘自 https://segmentfault.com/a/1190000012650596?utm_source=tag-newest
在这里插入图片描述
由于这样的一种机制,让MySQL在RR的隔离级别下就可以部分的防止了幻读,而为什么是部分防止呢,是因为我们大部分读的sql都属于快照读,即select column from table…而这样的SQL是通过MVCC和undo log的机制产生的一种快照读,被读到的记录实际上并没有加锁,而是根据undo log来读到的历史记录,这样其他事务如果在记录表中insert了一条,虽然在事务里还是读到的是当前的记录,但是我们还是可以修改那条被insert进去的数据,而只有在当前读,即select column from table … for update 这种SQL下(被加了行级锁)才会真正的实现了再RR的隔离级别下就防止了幻读。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值