0
点赞
收藏
分享

微信扫一扫

MySQL事务隔离性的实现原理

金穗_ec4b 2024-07-24 阅读 32

目录

一、数据库的并发场景

二、读写场景

1.知识准备

(1)事务ID

(2)四个隐藏字段

(3)undo日志

(4)Read View

2.模拟MVCC场景

3.Read View详解

三、RR与RC的本质区别


一、数据库的并发场景

数据库只有两种并发场景:

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

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

本文只详解读写场景,写写场景理解为都是当前读即可。

二、读写场景

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

1.知识准备
(1)事务ID

在MVCC下,事务会被分配一个单向增长的事务ID

(2)四个隐藏字段

表结构中还会有四个隐藏字段:

1.DB_TRX_ID:记录创建/最后一次修改这条记录的事务ID

2.DB_ROLL_PTR:回滚指针,指向要修改记录的历史版本(要修改记录的历史版本通常被保存在undo log中)

3.DB_ROW_ID:隐藏的自增ID(隐藏主键),如果表中没有主键,InnoDB会自动以DB_ROW_ID产生一个聚簇索引

4.flag:删除隐藏字段,删除记录并非真正删除数据,而是通过flag标志位标记当前记录是否被删除

创建一张测试表,并向其中插入一条记录

mysql> create table if not exists student(
name varchar(11) not null,
age int not null
);
mysql> insert into student (name, age) values ('张三', 28);
Query OK, 1 row affected (0.05 sec)
mysql> select * from student;
+--------+-----+
| name | age |
+--------+-----+
| 张三 | 28 |
+--------+-----+
1 row in set (0.00 sec)

实际上表中存储的信息如下:

由于当前并不知道创建该记录的事务ID和隐式主键,默认设置为null和1。由于是第一条记录,不存在历史版本,所以回滚指针也为null。

(3)undo日志

undo日志是MySQL中的一段内存缓冲区,用于保存日志数据。记录的历史版本都被保存在undo log中。

(4)Read View

Read View是读视图

2.模拟MVCC场景

假设现在有一个事务ID为10的事务,对student表进行修改:将name(张三)改为name(李四):

  • 事务ID为10的事务需要修改记录,先将改行记录加锁(其他事务无法对该记录进行更改或删除操作)
  • 修改前,先将该记录拷贝到undo log中。undo log中就存在了一行副本数据,即为记录的历史版本(假设其地址为0xaaaa)
  • 开始修改记录,name(张三)改为name(李四)。并将隐藏字段DB_TRX_ID(记录事务ID)改为10,隐藏字段DB_ROLL_PTR(回滚指针)改为0xaaaa
  • 提交事务10,释放锁

此时,又有一个事务11,对student表中的记录进行修改:将age(28)改为age(30)

  • 先将该行记录加锁(其他事务无法再对该记录进行修改、删除操作)
  • 修改前,先将该记录拷贝头插到undo log中,undo log中就又有了一行副本数据(假设其地址为0xbbbb)
  • 修改记录,将age(28)改为age(30)。并将隐藏字段DB_TRX_ID(记录事务ID)改为11,隐藏字段DB_ROLL_PTR(回滚指针)改为0xbbbb
  • 提交事务11,释放锁

以上讲述的都是update更新操作,如果是delete删除操作如何找到历史版本呢?

删除操作并不是真的删除掉记录,而是将记录的flag标志位记为被删除,也需要将数据拷贝到undo log中,也存在自己的历史版本链

如果是insert操作如何找到历史版本呢?

由于insert是插入,也就是没有之前数据,所以insert操作没有历史版本。但是insert的数据还是要拷贝放到undo log中,因为要支持回滚操作,回滚时会自动形成对应的delete操作,同时还要保证在不同隔离级别下,其他事务可以看到insert的记录

如果是select操作呢?

select不会对数据进行任何修改,因此为select维护历史版本没有任何意义。

但是关于select有一个问题:select读取的最新版本的数据,还是读取历史版本的数据呢?

当前读:读取最新的记录,就是当前读。例如增删改操作,都是当前读。

快照读:读取历史版本记录,就是快照读。

对于当前读,当多个事务同时删改查时,就是当前读,需要加锁保护。同时有select,也是当前读,也需要加锁保护,这就是串行化。

对于快照读,读取的是历史版本,无需加锁保护,因此可以并行执行。

那么是什么决定了select是当前读还是快照读?

是隔离级别,隔离级别可以实现不同的事务,看到不同的内容

那么隔离级别是如何实现的?

Read View

3.Read View详解

Read View是事务进行快照读时生成的读视图,记录并维护当前活跃事务的ID。在MySQL源码中就是一个类,用来进行事务对不同版本记录的可见性判断。

Read View中有四个重要的变量:

m_ids:存放系统正在运行的事务ID

up_limit_id:存放m_ids中事务ID最小的ID

low_limit_id:Read View 生成时刻系统尚未分配的的下一个事务ID,即当前已出现过事务ID的最大值+1

creator_trx_id:存放创建该读视图的事务ID

整体流程:

  • 当前存在记录:

  • 多个事务操作:

  • 事务4:修改name(张三)为name(李四)
  • 事务2对记录进行快照读,数据库为该记录生成一个Read View读视图

  • 事务4在事务2快照读前,修改了数据并提交了事务,此时undo log中存在一个修改前的历史版本。需要通过读视图判断事务2快照读时可以读取哪一个版本的记录。一条被修改的记录,七对应的事务ID,如果大于最小的活跃事务ID,小于等于已存在的事务ID,且不是活跃事务,则可以看到该记录的最新记录,否则只能看到历史版本的记录。

所以事务2能读取到事务4提交的最新数据

三、RR与RC的本质区别

事务的快照读结果非常依赖于该事物首次出现快照读的地方。即某个事务在什么时候首次出现快照读,决定该事务后续快照读的结果。

因为在RR隔离级别下,某个事务对某条记录的第一次快照读会生成一个Read View,伺候其他的快照读都使用该Read View。所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是一个Read View,对之后的修改都不可见。

而在RC隔离级别下,每一次快照读都会生成一个新的Read View ,这也就是RC隔离级别下的事务可以看到其他事务提交更新的原因。

举报

相关推荐

0 条评论