金不三,银不四的高频面试题中,MySQL的业务特性,阻隔等级等问题也是非常经典八股文之一,面对此种问题,估计绝大数小伙伴也是信手拈来的工作:
业务特性(ACID):原子性
(Atomicity
)、阻隔性
(Isolation
)、一致性
(Consistency
)和持久性
阻隔等级:读取未提交
(READ UNCOMMITTED
),读取已提交
(READ COMMITTED
),可重复读
(REPEATABLE READ
),可串行化
(SERIALIZABLE
)
而每一种阻隔等级导致的问题有:
-
READ UNCOMMITTED
阻隔等级下,可能发生脏读
、不可重复读
和幻读
问题 -
READ COMMITTED
阻隔等级下,可能发生不可重复读
和幻读
问题,可是不能够发生脏读
问题 -
REPEATABLE READ
阻隔等级下,可能发生幻读
问题,可是不能够发生脏读
和不可重复读
的问题 -
SERIALIZABLE
阻隔等级下,各种问题都不能够发生
关于MySQL InnoDB 存储引擎的默许支撑的阻隔等级是REPEATABLE-READ(可重读),从上面的SQL规范的四种阻隔等级定义可知,REPEATABLE-READ(可重复读)
是不能够防止幻读的,可是咱们都知道,MySQL InnoDB存储引擎是处理了幻读问题发生的,那他又是如何处理的呢?
1. 行格局
在进入主题之前,咱们先大致了解一下什么是行格局,这样有助于咱们了解下面的MVCC,行格局是表中的行记载在磁盘的寄存方法,Innodb
存储引擎总共有4种不同类型的行格局:compact
、redundant
、dynamic
、compress
;虽然很很多行格局,可是在原理上,大体都相同,如下,为compact
行格局:
从图中能够看出来,一条完好的记载其实能够被分为记载的额定信息
和记载的实在数据
两大部分,记载的额定信息
分别是变长字段长度列表
、NULL值列表
和记载头信息
,而记载的实在数据
除了咱们自己定义的列之外,MySQL会为每个记载添加一些默许列,这些默许列又称为躲藏列
,具体列如下:
列名 | 长度 | 描绘 |
---|---|---|
row_id | 6个字节 | 行ID,唯一标识一条记载 |
transaction_id | 6个字节 | 业务ID |
roll_pointer | 7个字节 | 回滚指针 |
躲藏列的值不用咱们操心,InnoDB
存储引擎会自己帮咱们生成的,画得再具体一点,compact
行格局如下:
- transaction_id :事物id,当事物对行记载进行修正时,都会将本事物的事物id赋值到该列
- roll_pointer:每次在对行记载进行改动的时分,都会把旧版别的数据写入undolog日志,
然后将roll_pointer
指向该undolog
,所以该列相当于一个指针,经过该列,能够找到修正之前的信息
2. MVCC详解
2.1 版别链
假定有一条记载如下:
插入该记载的业务id
为80
,roll_pointer
指针为NULL(为了便于了解,读者可了解为指向为NULL,实际上roll_pointer第一个比特位就标记取它指向的undo日志的类型,假如该比特位的值为1时,就代表着它指向的undo日志类型为insert undo)
假定之后两个业务id
分别为100
、200
的业务对这条记载进行UPDATE
操作:
-- 业务id=100
update person set grade =20 where id =1;
update person set grade =40 where id =1;
-- 业务id=200
update person set grade =70 where id =1;
每次对记载进行改动,都会记载一条undo日志
,每条undo日志
也都有一个roll_pointer
特点(INSERT
操作对应的undo日志
没有该特点,由于该记载并没有更早的版别),能够将这些undo日志
都连起来,串成一个链表,所以现在的情况就像下图相同:
对该记载每次更新后,都会将旧值放到一条undo日志
中,就算是该记载的一个旧版别,随着更新次数的增多,一切的版别都会被roll_pointer
特点连接成一个链表,咱们把这个链表称之为版别链
,版别链的头节点便是当时记载最新的值。另外,每个版别中还包括生成该版别时对应的业务id
2.2 ReadView
关于数据库的四种阻隔等级:1)read uncommitted
;2) read committed
;3) REPEATABLE READ
; 4)SERIALIZABLE
;来说,READ UNCOMMITTED
,每次读取版别链的最新数据即可;SERIALIZABLE
,主要是经过加锁操控;而read committed
和REPEATABLE READ
都是读取现已提交了的事物,所以关于这两个阻隔等级,中心问题是版别链中,哪些事物是对当时事物可见;为了处理这个问题,MySQL提出了read view 概念,其包括四个中心概念:
-
m_ids
:生成read view
时分,活跃的事物id调集 -
min_trx_id
:m_ids的最小值
,既生成read view的时分,活跃事物的最小值 -
max_trx_id
:表示生成read view
的时分,体系应该分配下一个事物id值 -
creator_trx_id
:创立read view
的事物id,即当时事物id。
有了这个ReadView
,这样在拜访某条记载时,只需要依照下边的过程判别记载的某个版别是否可见:
- 当记载的事物id等于
creator_trx_id
的时分,阐明当时事物正在拜访自己修正的记载,所以该版别可见 - 假如被拜访的版别事物id小于
min_trx_id
的时分,则阐明,在创立read view
的时分,该事物现已提交,该版别,对当时事物可读 - 假如被拜访的版别事物id大于或等于
max_trx_id
,则阐明创立该read view
的时分,该阐明生成该版别记载的事物id在生成Read view
之后才开启,所以该版别不能被当时事物可读 - 假如被拜访的版别事物
transaction_id
在m_ids
调集中,阐明生成Read view
的时分,该事物仍是活跃的,还没有被提交,则该版别不能够被拜访;假如不在,则阐明创立ReadView
时生成该版别的业务现已被提交,能够被拜访
注:读事物的事物id为0
在MySQL
中,READ COMMITTED
和REPEATABLE READ
阻隔等级的的一个非常大的差异便是它们生成ReadView的机遇不同:
-
READ COMMITTED
—— 每次读取数据前都生成一个ReadView
-
REPEATABLE READ
—— 在第一次读取数据时生成一个ReadView
下面咱们经过具体比如来阐明,两者有何不同:
时刻编号 | trx 100 | trx 200 |
---|---|---|
① | BEGIN; | |
② | BEGIN; | BEGIN; |
③ | update person set grade =20 where id =1; | |
④ | update person set grade =40 where id =1; | |
⑤ | SELECT * FROM person WHERE id = 1; | |
⑥ | COMMIT; | |
⑦ | update person set grade =70 where id =1; | |
⑧ | SELECT * FROM person WHERE id = 1; | |
⑨ | COMMIT; | |
COMMIT; |
在时刻④中,因业务trx 100
履行了业务的提交,id=1行记载的版别链如下:
在时刻⑥中,因业务trx 200
履行了业务的提交,id=1行记载的版别链如下:
在时刻⑤,业务trx 100
履行select
句子时会先生成一个ReadView
,ReadView
的m_ids
列表的内容便是[100, 200]
,min_trx_id
为100
,max_trx_id
为201
,creator_trx_id
为0
,此刻,从版别链中选可见的记载,版别链从上到下遍历:由于grade=40,trx_id
值为100
,在m_ids
里,所以该记载不可见,同理,grade=20的也不见。继续往下遍历,grade=20,trx_id
值为80
,小于小于ReadView
中的min_trx_id
值100
,所以这个版别符合要求,回来给用户的是等级为10的记载。
在时刻⑧中,假如业务的阻隔等级是READ COMMITTED
,会独自又生成一个ReadView
,该ReadView
的m_ids
列表的内容便是[200]
,min_trx_id
为200
,max_trx_id
为201
,creator_trx_id
为0
,此刻,从版别链中选可见的记载,版别链从上到下遍历:由于grade=70,trx_id
值为200
,在m_ids
里,所以该记载不可见,继续往下遍历,grade=40,trx_id
值为100
,小于ReadView
中的min_trx_id
值200
,所以这个版别是符合要求的,回来给用户的是是等级为40的记载。
在时刻⑧中,假如业务的阻隔等级是 REPEATABLE READ
,在时刻⑧中,不会独自生成一个ReadView
,而是沿用时刻5的ReadView
,所以回来给用户的等级是10。前后两次select得到的是相同的,这便是可重复读
的含义。
3. 总结
经过分析MVCC详解部分,能够得出,基于MVCC,在RR阻隔等级下,很好处理了幻读
问题,可是咱们知道,select for update
是发生当时读,不再是快照读,那么此种情况,MySQL又是怎样处理幻读
问题的呢?基于时刻问题(收拾画图的确需要花比较多的时刻),此处先给定论,后面再分析在当时读的情况下,MySQL是怎样处理幻读
问题:
- 当时读: 运用 Next-Key Lock(空隙锁) 进行加锁来保证不呈现幻读
关于空隙锁是如何在当时读的情况下处理幻读问题的,感兴趣朋友可加个关注,点个赞