MySQL MVCC实现原理
一、前置内容
1.事务的ACID
2.MySQL的核心日志
在MySQL数据库中有三个非常重要的日志binlog
,undolog
,redolog
.
3.隔离级别
1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据**
2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
- MySQL事务隔离级别
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) RU | 是 | 是 | 是 |
不可重复读(read-committed)RC | 否 | 是 | 是 |
可重复读(repeatable-read)mysql默认 RR | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
二、MVCC
1.什么是MVCC
MVCC(Multi-Version Concurrency Control):多版本并发控制,是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问,在编程语言中实现事务内存。
MVCC 在 MySQL InnoDB 中的实现主要是为了提高数据库并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。
2.什么是当前读和快照读
类型 | 说明 |
---|---|
快照读(普通读) | 普通的 select 语句。执行方式是生成 readview,直接利用 MVCC 机制来读取,并不会对记录进行加锁。它是基于多版本并发控制即 MVCC机制,既然是多版本,那么快照读读到的数据不一定是当前最新的数据,有可能是之前历史版本的数据。 如下的操作是快照读: 1、 不加锁的 select 操作 (前提 :事务级别不是串行化,串行化的是快照读=当前读) |
当前读(锁定读) | 它读取的记录都是数据库中当前的最新版本,会对当前读取的数据进行加锁,防止其他事务修改数据,这种锁是一种悲观锁。 当前读的规则,就是要能读到所有已经提交的记录的最新值。 如下操作都是当前读: 1、 select … lock in share mode 当前读,加读锁 ,也叫共享锁 2、 select … for update 当前读,加写锁,又叫排他锁 3、 innoDB 里面 update (排他锁)、insert (排他锁)、delete (排他锁),都会自动给涉及的语句添加写锁。 4、 串行化事务的隔离级别 实现方式:next-key(行记录锁+间隙锁)即临键锁,是前开后闭区间。 |
3.当前读快照读 与 MVCC的关系
首先三者都是一种概念,
MySQL的InonDB利用 3 个隐式字段,undo 日志 ,Read View 实现了MVCC
MVCC 在不使用锁的前提下 就能解决了 并发环境下 读写冲突的问题,也就实现了快照读(非阻塞读)的功能
而当前读是一种 必须加锁的读,与MVCC无关了
4.MVCC的好处
首先我们要清楚数据库中的并发场景有三种,分别是
然后来看MVCC的好处:
多版本并发控制(MVCC)是一种用来解决读-写冲突的无锁并发控制,也就是为事务分配单向增长的时间戳,为每个修改保存一个版本,版本与事务时间戳关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题:
1、在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能
2、同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题
5.MVCC工作原理
MVCC 的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。
三个隐藏字段
DB_TRX_ID 是当前操作该记录的事务 ID ,
而 DB_ROLL_PTR 是一个回滚指针,用于配合 undo日志,指向上一个旧版本(上一个修改)
举例如:
DB_ROW_ID 是数据库默认为该行记录生成的唯一隐式主键(InnoDB表如果没有主键或者唯一索引会自动生成这种隐式主键的聚簇索引)
undo log 版本链
所谓版本是针对不同事物的每一次修改来的,不是针对事务
我同时开启了如下事务
不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录
Read View
Read View作用:决定快照读时,读取undo log版本链中的哪一个条记录
ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的) id。
生成ReadView的时机
- RC:在事务中每一次执行快照读时生成ReadView。
- RR:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView。
ReadView有四个核心字段
字段 | 含义 |
---|---|
m_ids | 当前活跃事务id集合 |
min_trx_id | 最小活跃事务id |
max_trx_id | 预分配事务id,当前最大事务+1(因为事务id是自增的)也即下一个要分配的事务id |
creator_trx-id | ReadView创建者的事务id |
Read View中规定了版本链数据的访问规则( trx_id 代表当前undo log版本链上节点对应事务ID )
条件 | 条件 | 说明 |
---|---|---|
trx_id==creator_trx_id | 可以访问该版本 | 数据是当前这个事务更改的,自己创建的事务当然可以访问啊 ; |
trx_id < min_trx_id | 可以访问该版本 | 访问的事务已经肯定已经提交了 ,不管是谁创建的事务,只要提交了就是共享的,当然可以访问 |
trx_id > max_trx_id | 不可以访问该版本(只能说明不能访问,不能说明可以访问,作用是否决) | 该事务是在ReadView生成后才开启 ,太早了 |
min_trx_id<= trx_id<=max_trx_id | 如果trx_id不在m_ids中,可以访问该版本 | 如果条件成立,数据已经提交 |
如果undo log版本链的头节点记录(最新记录),四个条件判断后的结果都是不可访问(只要有一个说明可以访问就能访问),则根据回滚指针找到上一个版本的记录,继续判断,直到找到一个可以访问的记录。
RC级别下
第一次快照读
m_ids(活跃事务id):3,4,5 (不包括2,读之前已经提交了)
min_trx = min m_ids
max_trx = max m_ids +1
creator_trx_id = 所在事务的id
第二次快照读
略
对第一次快照读,
把最新的 db_trx_id=4 带入
1、 不满足等于创建者 ,不能访问该版本
2、不满足小于最小事务版本 ,不满足访问该版本
3、不满足 ,
4、满足 3<=4<=6 但是不满足 不在活跃县城里面,所以不可以访问
访问下个版本 db_trx_id=3 带入
都不满足 不成立
访问下个版本 db_trx_id=2 带入
可以,事务2已提交可以访问 把这个版本的记录返回
RR级别下
RR级别下只有第一次执行快照读时生成ReadView,后续复用该ReadView。保证了下次每次读取和前面保持一致
除非commit 重新读取
三、MVCC是否解决了幻读?
严格意义上并没有解决幻读MVCC
利用版本链,undo log,Read View可以在快照读模式下解决幻读问题,并且不用加锁解决读写冲突问题,极大的增加了数据库的并发量。
但在当前读模式下仅仅依靠MVCC不能解决幻读问题,必须依赖next-key锁(行锁+GAP锁)来解决,这是因为当前读必须获取最新数据。