• 【MySQL】根据MVCC和Read View分析事务的四种隔离级别在读写场景分别是如何体现其隔离性的



    需要云服务器等云产品来学习Linux的同学可以移步/-->腾讯云<--/-->阿里云<--/-->华为云<--/官网,轻量型云服务器低至112元/年,新用户首次下单享超低折扣。


     目录

    一、数据库并发的三种场景

    二、读写场景的MVCC 

    1、3个(4个)记录隐藏列字段

    2、undo log(撤销日志)

    3、模拟MVCC场景

    3.1update场景

    3.2delete场景

    3.3insert

    3.4select场景

    4、Read View

    5、RR和RC的区别

    5.1当前读和快照读在RR级别下的区别

    例一:root在jly修改前快照读

    例二:root在jly修改后快照读

    5.2MySQL对四种隔离级别的不同处理方式 

    三、写写场景


    一、数据库并发的三种场景

    读-读 :不存在任何问题,也不需要并发控制

    读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读

    写-写 :有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

    二、读写场景的MVCC 

    多版本并发控制( MVCC )是一种用来解决 读-写冲突 的无锁并发控制。

    为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题:

    在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了并发读写的性能,同时解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。

    1、3个(4个)记录隐藏列字段

    在创建表的时候,MySQL除了创建用户所需的列之外,还会创建3个记录隐藏列字段

    DB_TRX_ID :6 byte,这一列记录了每一行最后一次修改的事务ID( 修改/插入 )。

    DB_ROLL_PTR : 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在 undo log 中)

    DB_ROW_ID : 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以DB_ROW_ID产生一个聚簇索引

    第四个隐藏列字段:实际还有一个标识该行数据是否删除的隐藏字段flag。

    例如创建并插入一条数据,实际的表结构应该是这样的:

    name

    age

    DB_TRX_ID

    DB_ROLL_PTR

    DB_ROW_ID

    张三

    20

    创建该事务的ID

    null

    1(隐式主键)

    2、undo log(撤销日志)

    MySQL是以守护进程的方式,在内存中运行。undo log是MySQL中的一段内存缓冲区,用以保存日志数据。

    3、模拟MVCC场景

    3.1update场景

    现有一个事务ID为10,对上方信息表进行update,将name由张三修改为李四:

    1、因为要修改,所以要先给该记录加上行锁。

    2、在修改之前,先将改行数据拷贝一份到undo log中(写时拷贝,原始数据在表中,拷贝的数据在undo log中,假设拷贝的数据地址是0XAA)

    3、修改原始数据的同时,将隐藏字段DB_TRX_ID修改为10,将DB_ROLL_PTR回滚指针修改为0XAAAAAAAA

    4、事务10commit提交,释放行锁

    name

    age

    DB_TRX_ID

    DB_ROLL_PTR

    DB_ROW_ID

    李四

    20

    10

    0XAAAAAAAA

    1(隐式主键)

    此时又有一个事务11,需要对信息表的记录进行update,将李四那一行的年龄修改为30:

    1、因为要修改,所以要先给该记录加上行锁。

    2、同样的,将当前的表中的对应行拷贝一份到undo log,假设地址0XBBBBBBBB

    3、修改原始数据的同时,将隐藏字段DB_TRX_ID修改为10,将DB_ROLL_PTR回滚指针修改为0XAAAAAAAA

    name

    age

    DB_TRX_ID

    DB_ROLL_PTR

    DB_ROW_ID

    李四

    30

    11

    0XBBBBBBBB

    1(隐式主键)

    undo log中的一个个版本,被称为快照。除了版本链之外,还可以通过记录相反sql的方式,以备数据的回滚(比如delete数据,日志可以保存insert数据)

    3.2delete场景

    删除数据不是清空,只需将隐藏的flag标志位设置为删除即可。也可以形成版本。

    3.3insert

    insert是插入,只需插入时在undo log中记录其对应的delete语句即可,回滚时只需执行这些delete语句。如果当前的事务提交了,undo log将会删除这些备份数据。(update和delete可能有别的事务还在访问,commit之后不会立马就删除undo log的回滚数据)

    3.4select场景

    在MySQL的RR级别下,一个事务的写操作并不会影响另一个事务的读操作。增删改,都是对最新数据进行修改,但是读取,则可能需要读取历史的版本。

    当前读:读取最新的记录,就是当前读。增删改,都叫做当前读,select也有可能当前读,比如select lock in share mode(共享锁), select for update

    快照读:读取历史版本。快照读不会被加锁。

    多个事务同时增删改的时候,是当前读,需要加锁,如果对select也加锁,那么隔离级别就是串行化。如果select是快照读,和增删改的当前读不影响,所以可以不用加锁,并行执行效率高。事务的隔离级别决定了select读取历史数据是当前读还是快照读。(read view是否更新)

    那么如何保证不同的事务,看到不同的内容呢;先来的事务,应不应该看到后来的事务所作的修改呢?Read View进行可见性判断。

    4、Read View

    Read View在事务首次进行快照读的时候由MySQL生成,记录并维护系统当前活跃事务的ID。Read View 在 MySQL 源码中,就是一个类,本质是配合MVCC用来判断哪些快照我能看到,那些快照我看不到。

    当某个事务执行select快照读的时候,MySQL会对其new一个对象,用其内部的条件来判断当前事务能够看到哪个版本的数据,可见的数据既能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据,这由隔离级别决定。

    下面是 ReadView 简化的结构体:

    1. class ReadView {
    2. // 省略...
    3. private:
    4. /** 高水位,大于等于这个ID的事务均不可见*/
    5. trx_id_t m_low_limit_id
    6. /** 低水位:小于这个ID的事务均可见 */
    7. trx_id_t m_up_limit_id;
    8. /** 创建该 Read View 的事务ID*/
    9. trx_id_t m_creator_trx_id;
    10. /** 创建视图时的活跃事务id列表*/
    11. ids_t m_ids;//ids_t集合类型
    12. /** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
    13. * 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
    14. trx_id_t m_low_limit_no;
    15. /** 标记视图是否被关闭*/
    16. bool m_closed;
    17. // 省略...
    18. };
    1. m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
    2. up_limit_id; //记录m_ids列表中事务ID最小的ID
    3. low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1
    4. creator_trx_id //创建该ReadView的事务ID

    那么哪些数据能被事务读到,那些数据事务看不到呢?见下图:

    总结一下:举个例子,我是学弟,我能看到比我早入学的学长的找工作的数据,但是学长看不到后入学的我找工作的数据。同理在形成快照的时候,我能看到:

    已经提交的事务:

    1、creator_trx_id(创建快照的事务ID)==DB_TRX_ID(undo log中最近一次修改该行的事务ID)

    2、DB_TRX_ID(undo log中最近一次修改该行的事务ID)

    创建快照时m_ids中的事务(活跃的事务ID):

    1、快照中的事务ID不一定连续,快照中的事务ID范围为up_limit_id<=ID

    我看不到:

    快照建立之后的新事物:

    1、DB_TRX_ID(undo log中最近一次修改该行的事务ID)>=low_limit_id(快照生成时系统尚未分配的下一个ID)

    如果查到不应该看到当前版本,接下来就是遍历下一个版本,直到符合条件。

    name

    age

    DB_TRX_ID

    DB_ROLL_PTR

    DB_ROW_ID

    张三

    28

    创建该事务的ID

    null

    1(隐式主键)

    此时的undo log中的版本链:

    在事务2对改行进行快照读的时候,按照undo log中快照的先后版本,依次遍历,得出本次我对该行的快照读应该读取到哪一个版本的快照。

    5、RR和RC的区别

    5.1当前读和快照读在RR级别下的区别

    准备工作:

    1. --将全局隔离级别设置为可重复读(需重启)
    2. mysql> set global transaction isolation level REPEATABLE READ;
    3. Query OK, 0 rows affected (0.00 sec)
    4. --创建一张表
    5. mysql> create table if not exists account(
    6. -> id int primary key,
    7. -> age int not null,
    8. -> name varchar(20) not null
    9. -> )ENGINE=InnoDB DEFAULT CHARSET=UTF8;
    10. Query OK, 0 rows affected (0.26 sec)
    11. --插入一条数据
    12. mysql> insert into account values (1,18,'张三');
    13. Query OK, 1 row affected (0.04 sec)
    例一:root在jly修改前快照读

    使用者:jly

    1. --1、启动事务
    2. mysql> begin;
    3. Query OK, 0 rows affected (0.01 sec)
    4. --2、进行快照读
    5. mysql> select* from account;
    6. +----+-----+--------+
    7. | id | age | name |
    8. +----+-----+--------+
    9. | 1 | 18 | 张三 |
    10. +----+-----+--------+
    11. 1 row in set (0.00 sec)
    12. --3、更新数据,修改id为1的字段的年龄为20
    13. mysql> update account set age=20 where id=1;
    14. Query OK, 1 row affected (0.00 sec)
    15. Rows matched: 1 Changed: 1 Warnings: 0
    16. --4、对事务进行提交
    17. mysql> commit;
    18. Query OK, 0 rows affected (0.04 sec)

    使用者:root

    1. --当上方用户执行完第一步时,root同时启动事务
    2. mysql> begin;
    3. Query OK, 0 rows affected (0.00 sec)
    4. --当上方用户执行完第二步时,root同时进行快照读,读取的结果一样
    5. mysql> select* from account;
    6. +----+-----+--------+
    7. | id | age | name |
    8. +----+-----+--------+
    9. | 1 | 18 | 张三 |
    10. +----+-----+--------+
    11. 1 row in set (0.01 sec)
    12. --当上方用户执行完第三步时,root进行快照读,发现年龄的修改并没有被读到
    13. mysql> select* from account;
    14. +----+-----+--------+
    15. | id | age | name |
    16. +----+-----+--------+
    17. | 1 | 18 | 张三 |
    18. +----+-----+--------+
    19. 1 row in set (0.00 sec)
    20. --当上方用户执行完第四步提交事务时,root再次进行快照读,发现年龄的修改还是没有被读到
    21. mysql> select* from account;
    22. +----+-----+--------+
    23. | id | age | name |
    24. +----+-----+--------+
    25. | 1 | 18 | 张三 |
    26. +----+-----+--------+
    27. 1 row in set (0.00 sec)
    28. --但是此时root使用当前读,使能够读到年龄的修改的
    29. mysql> select* from account lock in share mode;
    30. +----+-----+--------+
    31. | id | age | name |
    32. +----+-----+--------+
    33. | 1 | 20 | 张三 |
    34. +----+-----+--------+
    35. 1 row in set (0.01 sec)
    例二:root在jly修改后快照读

    使用者:jly

    1. --1、启动事务
    2. mysql> begin;
    3. Query OK, 0 rows affected (0.01 sec)
    4. --2、进行快照读
    5. mysql> select* from account;
    6. +----+-----+--------+
    7. | id | age | name |
    8. +----+-----+--------+
    9. | 1 | 20 | 张三 |
    10. +----+-----+--------+
    11. 1 row in set (0.00 sec)
    12. --3、更新数据,修改id为1的字段的年龄为30
    13. mysql> update account set age=30 where id=1;
    14. Query OK, 1 row affected (0.00 sec)
    15. Rows matched: 1 Changed: 1 Warnings: 0
    16. --4、提交事务
    17. mysql> commit;
    18. Query OK, 0 rows affected (0.03 sec)

    使用者:root

    1. --1、同时启动事务
    2. mysql> begin;
    3. Query OK, 0 rows affected (0.01 sec)
    4. --当上方用户执行完第四步提交事务时,root进行快照读,发现读到的数据是被修改过的
    5. mysql> select* from account;
    6. +----+-----+--------+
    7. | id | age | name |
    8. +----+-----+--------+
    9. | 1 | 30 | 张三 |
    10. +----+-----+--------+
    11. 1 row in set (0.00 sec)

    通过例一可以发现:root的select在jly提交之前,读到的是修改前的数据;

    通过例二可以发现:root的select在jly提交之后,读到的是修改后的数据。

    这是因为一个事务在读取时,MySQL会生成一个read view对象,上面介绍read view的章节说了,read view本质就是一个类,用来判断哪些快照我能看到,那些快照我看不到。

    read view生成的时机不同,会影响事务的可见性。

    5.2MySQL对四种隔离级别的不同处理方式 

    Read View生成时机的不同,从而造成RC,RR不同隔离级别下快照读的结果的不同:

    可重复读:在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View对象, 将当前系统活跃的其他事务记录起来;后续再次调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改才能看见。

    读提交:RC级别的事务中,每次快照读都会新生成一个快照和Read View, 这就是我们在RC级别下可以看到其他事务所更新内容的原因。正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。

    读未提交:当前读。没有隔离性。

    串行化:当前读,对增删改进行加锁的同时,对select也加锁。

    三、写写场景

    直接理解成当前读。

  • 相关阅读:
    Dart 3.2 更新,Flutter Web 的未来越来越明朗
    PHP:错误
    react useReducer
    计算狗携手成都超算中心和重庆大学,共同助力“碳中和”
    安装MySQL
    基于springboot的宠物商城网站
    [原创]基于Comsol的方形、三角形、椭圆形克拉尼板仿真研究
    国货拟人AI绘图;500+AI岗位合辑;百川x亚马逊AI黑客松;企业级AI行业图谱;100+LLM面试题与答案 | ShowMeAI日报
    Lammps实现纳米孔道内瓦斯驱替过程(包含In文件)
    区块链技术的应用场景和优势
  • 原文地址:https://blog.csdn.net/gfdxx/article/details/131524580