”真正学会,如你般自由~“
MVCC机制简介
MVCC(Multi-Version-Concurrency-Control)多版本并发控制,MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程中实现事务内存。
取自
MVCC存在被使用于多个大型数据库软件,诸如Mysql、Oracle、PostgreSQL等。其中,在Mysql存储引擎中,MVCC机制的运用,对于Innodb支持事务,更好的处理读-写并发,提高并发性能等具有支撑作用。
MVCC带来的好处?
数据并发的三种场景:
🥊 写-写: 有线程安全问题,只能串行化执行。
🥊 读-写: 有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读。
🥊 读-读: 不存在任何问题,也不需要并发控制。
MVCC主要为 读-写情况提供无锁并发控制:
- 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
- 同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
概念辨析
💎 如何理解事务?
说起事务的概念,其本质并不复杂。我们可以将其理解为 一组“操作逻辑”,而这套逻辑天然需要支持四大特性: 原子性、持久性、隔离性、一致性 —— ACID。
在Mysql中,分别采用不同的措施来保证 事务这四大特性:
🎫 通过回滚机制,保证sql在执行的过程中要么都被执行成功,要么都执行失败 —— 原子性
🎫 事务一旦提交,数据就是永久的 —— 持久性
🎫 通过隔离级别,保证各个开启事务、执行SQL不会互相干扰 —— 隔离性
Mysql唯独没有对保证 一致性做任何操作,但为保障其他三个特点所做的技术,最终就是为了保证事务的一致性!
💎 当前读 vs 快照读
当前读: 就是它读取的是记录的最新版本
快照读: 读取为历史版本
我们可以先看一个实验,我们先将Mysql中的隔离界别调整为RC(Mysql默认的隔离级别为RR)
重启客户端后,就可以查看到修改字段:
创建一个新表,并在其中插入一些数据:
RC隔离级别下的事务:
这很符合预期,毕竟咱们设置的隔离级别就是 “RC”,即读可提交。
RR默认隔离级别下的事务:
我们现在按照统一的操作,在RR隔离级别下会发生什么呢?
是的,我们再也无法看到另一端启动的事务 对表做的任何修改,即便它执行了“commit”!
直观地,在RR下两个事务进行select的表一定是不同的!而这两个不同的表,一张记录的是当前数据信息,而另一张表记录的则是历史数据!
当我们使用,像"select lock in share mode"在锁机制下的查询sql,就能读取到对端事务提交修改的新数据。
由此,所谓当前读 与 快照读的本质,就是查询的表信息版本的不同~通过控制、管理 不同的版本信息,从而实现隔离性~ 对于,事务能看到的版本范围,则是由 “隔离级别“ 决定!隔离级别是怎样实现,Mysql是如何管理这个过程的,则是依赖MVCC机制从根本上提供技术支持。
MVCC机制实现原理
MVCC目的在于 —— 多版本并发控制。为了解决读-写场景下产生的问题,MVCC通过3个核心机制完成控制和管理:
🔮 3个记录的隐藏字段
🔮 undo日志
🔮 Read View
3个隐式字段
⛳ DB_TRX_ID:
6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID。
⛳ DB_ROLL_PTR
回滚指针,指向这条记录的上一个版本,通过这个指针,从而形成一个历史版本链。
⛳ DB_ROW_ID
undo日志
undo log有两个作用:提供回滚和多个行版本控制,它是一个逻辑日志。
当sql语句是insert\delete时,记录这条操作的同时,也会在该日志中对应插入delete\insert 语句,用于回滚。同样,如果是遇到 update命令时,也会将原数据进行一份拷贝,再进行修改,并让新表中的 回滚指针,填写原版本的地址(这里的地址,指的时undo天然可以看成一块内存缓冲区,当数据被写入内存时,天然就会携带内存地址)。
上面的一个一个版本,我们可以称之为一个一个的快照。当我们开启快照读,就是到这里面存储的表中读取数据!至于哪些版本是我们应该看到的,就与我们下一小节的read view决定了!
read View
所谓read view,就是事务进行 "快照读"操作时,产生出的读视图。任何一个事务启动时,都会被分配一个事务ID!而这个事务ID是线性递增的~言外之意,事务ID越小,说明该事务到来得越早!
read view是事务进行快照读所产生的,它需不需要记录是哪个事务执行的这个操作?它需不需要保存快照读时刻下,哪些事务是“活跃的”(即未进行commit,仍处于事务状态的ID)?……
一个Mysql服务中,一定不止一个事务启动,一定不止一次快照读产生的不止一个read view,这些东西需不需要管理? 答案是都需要!如何管理呢? 先描述,再组织~ 因此,read view其本质上就是一个结构体(类对象)!
下面是 我们简化一下的ReadView 结构:
class ReadView {
// 省略...
private:
/** 高水位,大于等于这个ID的事务均不可见*/
trx_id_t m_low_limit_id
/** 低水位:小于这个ID的事务均可见 */
trx_id_t m_up_limit_id;
/** 创建该 Read View 的事务ID*/
trx_id_t m_creator_trx_id;
/** 创建视图时的活跃事务id列表*/
ids_t m_ids;
/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
trx_id_t m_low_limit_no;
/** 标记视图是否被关闭*/
bool m_closed;
};
🥎 m_ids: 用来维护Read View生成时刻,系统正活跃的事务ID
🥎 creator_trx_id: 创建该ReadView的事务ID
🥎 up_limit_id: m_ids中,最小的事务ID
🥎 low_limit_id: ReadView生成时刻系统尚未分配的下一个事务ID
当我们获取历史版本链时,每一个事务所能看到的版本都应该是受限制的,比如一个早来到的事务,不可能了解到后到事务对表数据进行的修改,正如 古人先贤不会对当今的智能时代加以评论、享受。
我们read view手头中,正有DB_TRX_ID能够标识事务的先后顺序……
这个可见性过程是怎样的?我们可以参照如下流程:
🏀 此时到来一个 creator_ID事务ID,它现在进行了一次快照读,生产read view,记录下了当前活跃的事务ID,更新了up_limit_id \ low_limit_id。它会去undo日志中,寻找任意一条记录,并获取上面的 DB_TRX_ID。
🏀 DB_TRX_ID < up_limit_id。如果小于,则事务能够看到这条记录。反之,我们继续往下走~
🏀 DB_TRX_ID >= low_limit_id。如果大于,则该事务不能看到这条记录,反之我们继续往下走~
🏀 DB_TRX_ID IN [m_ids]。如果该事务ID不存在活跃事务之中,那么就能看到这条记录,反之就不能。
RR与RC机制的本质区别
总结这两种隔离级别的区别就是一句话,” 形成快照的时机不同 “。
RC: 每一次查询都会创建read view,保证读取当前最新版本。
RR: 只能生产一次read view,之后的一切查询读取都只能按照这个read view读取。
正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。
本篇到此结束,感谢你的阅读。
祝你好运,向阳而生~