《MySQL是怎么运行的:从根儿上理解MySQL》(24)学习总结

说明

文章的图片来源《MySQL是怎么运行的:从根儿上理解MySQL》,本篇文章只是个人学习总结,欢迎大家买一本正版小册看看,对于mysql是由浅入深的讲解非常细致

24.事务隔离级别和MVCC

CREATE TABLE hero (
number INT,
name VARCHAR(100),
country varchar(100),
PRIMARY KEY (number)
) Engine=InnoDB CHARSET=utf8;

事务隔离级别

  • 事务中有隔离性,但是如果要保证完全的隔离性,那么就要让不同会话的事务但是使用的是同一个数据,那么就要进行排队,但是这样的排队会导致性能很慢,所以策略是放弃一部分隔离性来增强性能。

事务并发执行遇到的问题

  • 脏写:一个事务修改了另一个事务未提交的数据

image-20211107104752447

上面的B修改number为1的数据name为关羽,A修改为张飞,但是B回滚导致A的修改无效。问题是这里的B并没有提交。A提交了应当就显示A修改成功的数据,但是现在却是变成了B回滚之后的数据。这种就是语句交替导致的脏写,覆盖问题。这里相当于就是A修改B对应的数据,最后B回滚把A的修改给覆盖掉了。

  • 脏读:一个事务读到另一个事务未提交修改的数据

image-20211107105122012

上面这种就是B修改,A读到B修改的数据,但是B回滚导致A读取的数据是错误的。

  • 不可重复读

一个事务只能读取到一个已经提交事务修改的数据,并且提交之后另一个事务可以得到最新的值。

image-20211107105421817

  • 幻读

一个事务按照某个条件查询多条记录,另一个事务插入数据,上一次的事务再次查询就会导致查询多了几条数据。

image-20211107105537244

SQL标准中的四种隔离级别

  • READ UNCOMMITED:读未提交
  • READ COMMITTED: 读已提交
  • REPEATABLE READ:可重复读
  • SERIALIZABLE:可串行化

image-20211107105814142

  • 但是仍然可能会有脏写的存在。

MySQL中支持的四种隔离级别

  • mysql在REPEATABLE READ的情况下是可以解决幻读问题的。
  • 默认隔离级别是REPEATABLE READ

如何设置事务的隔离级别

  • SET [GLOBAL|SESSION] TRANSACTION ISOLATION LEVEL level;

    • global对于全局的,下一次开启的会话就是这种隔离级别,但是当前已经开启的会话并不会改变
    • session只是针对当前会话的。会话后续事务都可以使用这个隔离级别
  • SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

    • 这种只对下一个事务有效。
  • SHOW VARIABLES LIKE ‘transaction_isolation’;这个可以查询当前会话的一个隔离级别

  • SELECT @@transaction_isolation;

MVCC原理

版本链

每行记录两个必要的列

  • trx_id:事务id,每次事务对聚簇索引的记录修改都会把事务id赋值给这个记录
  • roll_pointer:每次对记录进行改动都会把旧的版本写到undo日志上面,并且这个可以找到undo日志修改前的信息

如果插入一条记录

image-20211107110709597

  • roll_pointer有7个字节,并且第一个bit位用于标识指向undo日志的类型(1是插入,0是更新)。
  • 如果事务提交之后那么这条undo日志就无效了,因为undo日志的目的就是为了给事务回滚,redo日志是为了让系统崩溃的时候恢复数据。这个时候undo log segment(对于这个事务的所有链表)也会被回收。但是roll_pointer的值没有变

image-20211107111027268

  • 下面的图就是上面的修改的版本链,版本链的头结点就是最新的记录值

image-20211107111231283

ReadView

  • 对于read uncommited隔离级别来说可以读到未提交事务修改的数据,所以直接去链表头的数据就可以了。
  • serializable隔离级别会通过加锁来访问记录
  • read commiterd和repeatable read必须保证读到的是已经提交的数据,如果一条记录是修改但是没有提交那么是不可以直接读取的,所以需要判断版本链哪个版本对当前事务是可见的。

readview就是这样提出来的,4个重要内容

  • m_ids:生成readview时系统中活跃读写事务的事务id列表
  • min_trx_id:生成readview活跃读写事务列表中最小的事务id
  • max_trx_id:生成readview时,系统应该分配给下一个事务的id值。(它针对的最大的那个事务id,比如1、2、3而且3事务提交,那么新的事务m_ids就是1和2,max_trx_id是4,min_trx_id就是1)
  • creator_trx_id:生成该readview的事务的事务id

有了readview之后的判断某个记录的版本是否可见

  • 被访问的版本的trx_id是否和creator_trx_id相同,意味着这条记录就是当前事务修改的记录,是可见的
  • 被访问版本的trx_id是否小于min_trx_id,如果是说明事务已经提交可见
  • 如果被访问版本的trx_id是大于max_trx_id那么说明生成这个版本的记录是当前事务生成readview之后才开启的。
  • 如果被访问版本trx_id处于min和max之间,需要判断trx_id是否存在于m_ids里面,如果存在那么说明就是活跃事务,不可见,如果不存在说明就是已经提交了的事务,可以访问。

其实就是顺着版本链,并且按照上面的规则来对应。

对于read commited和repeatable read

  • 他们生成readview的时机是不同的

对于read commited来说

  • 每次读取数据都会生成一个readview

比如下面的这个例子

# Transaction 100
BEGIN;
UPDATE hero SET name = '关羽' WHERE number = 1;
UPDATE hero SET name = '张飞' WHERE number = 1;
# Transaction 200
BEGIN;
# 更新了一些别的表的记录

image-20211107113322127

  • 上面这个是hero表的number1的记录的一个版本链

现在假设有一个有一个read commited级别的事务

  • 生成自己的readview,m_ids=[100,200],min=100,max=201,creator_trx_id=0
  • 所以这个时候看不到张飞和关羽因为事务id刚好就在m_ids里面,活跃未提交的事务
  • 但是刘备的事务id=80<100所以这个事务是可见的
# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'
  • 现在如果把事务进行提交
# Transaction 100
BEGIN;
UPDATE hero SET name = '关羽' WHERE number = 1;
UPDATE hero SET name = '张飞' WHERE number = 1;
COMMIT;
然后再到事务id为200的事务中更新一下表hero中number为1的记录:
# Transaction 200
BEGIN;
# 更新了一些别的表的记录
...
UPDATE hero SET name = '赵云' WHERE number = 1;
UPDATE hero SET name = '诸葛亮' WHERE number = 1;

image-20211107113900490

然后再次使用read commited事务级别来进行访问

  • 对于select2来说这个时候事务id=100已经提交了,再次生成readview的时候数据是m_ids=200,min=200,max=201,creator_trx_id=0所以这个张飞很明显trx_id=100小于200是可见的。
  • 也就是说read commited生成readview的时机是在每次读取数据的时候
# 使用READ COMMITTED隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'
# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'张飞'

对于repeatable read来说

  • repeatable read在第一次读取数据的时候就会生成readview。

仍然是这两个事务

# Transaction 100
BEGIN;
UPDATE hero SET name = '关羽' WHERE number = 1;
UPDATE hero SET name = '张飞' WHERE number = 1;
# Transaction 200
BEGIN;

image-20211107114714051

现在使用一个repeatable read隔离事务级别读

# 使用REPEATABLE READ隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'
  • 对于select1来说还是和之前的read commited的数据一样m_ids=[100,200],min=100,max=201,creator_trx_id=0
  • 所以还是只能看到刘备
# Transaction 100
BEGIN;
UPDATE hero SET name = '关羽' WHERE number = 1;
UPDATE hero SET name = '张飞' WHERE number = 1;
COMMIT;
然后再到事务id为200的事务中更新一下表hero中number为1的记录:
# Transaction 200
BEGIN;
# 更新了一些别的表的记录
...
UPDATE hero SET name = '赵云' WHERE number = 1;
UPDATE hero SET name = '诸葛亮' WHERE number = 1;

image-20211107115908575

# 使用REPEATABLE READ隔离级别的事务
BEGIN;
# SELECT1:Transaction 100、200均未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值为'刘备'
# SELECT2:Transaction 100提交,Transaction 200未提交
SELECT * FROM hero WHERE number = 1; # 得到的列name的值仍为'刘备'

  • 但是对于可重复读的select2来说,这里并不会重新生成视图,而是使用第一次读生成的视图。关于这里creator_trx_id为什么是0原因是这个事务还没有修改记录所以并不会分配事务id。

总结

  • MVCC(Muti-Vesion Concurrency Control多版本并发控制)使用在read commited和repeatable read这两种隔离级别。主要就是根据readview判断对于当前事务可读的版本记录。两个事务级别生成readview时机不同导致最后读取数据的版本也是不同的
  • insert undo可以在事务提交之后覆盖,但是update undo仍然需要支持mvcc所以不会那么快覆盖和删除
  • 对于delete来说为了支持mvcc所以只是做了一个标记,并没有真正删除
  • 事务四种并发问题(脏读、脏写、幻读、不可重复读),四种隔离级别(读未提交,读已提交、可重复读、串行化)
  • mvcc的本质就是一条undo日志链,并且通过readview规则来确定本事务可以读取的对应的版本

录。两个事务级别生成readview时机不同导致最后读取数据的版本也是不同的

  • insert undo可以在事务提交之后覆盖,但是update undo仍然需要支持mvcc所以不会那么快覆盖和删除
  • 对于delete来说为了支持mvcc所以只是做了一个标记,并没有真正删除
  • 事务四种并发问题(脏读、脏写、幻读、不可重复读),四种隔离级别(读未提交,读已提交、可重复读、串行化)
  • mvcc的本质就是一条undo日志链,并且通过readview规则来确定本事务可以读取的对应的版本

おすすめ

転載: blog.csdn.net/m0_46388866/article/details/121203014