一文读懂Innodb MVCC实现原理

·  阅读 994

数据库事务一些小知识

数据库隔离级别干嘛用的?

数据库并发会带来脏读、不可重复读、幻读等问题,所以采用了事物的隔离级别来解决。

先来看看脏读、不可重复读、幻读什么意思?

• 脏读:事物A读取了事物B未提交的数据。

• 不可重复读:事物A同样的查询条件,查询多次,读出的数据不一样,不一样的侧重点在于 update和delete

• 幻读:事物A同样的查询条件,查询多次,读出的数据不一样,不一样的侧重点在于insert

事务隔离级别
  • 读未提交

不解决脏读、不可重复读、幻读的问题,对所有的数据都不加任何的锁。

  • 读已提交

不解决不可重复读、幻读问题,但解决了脏读问题,会读取已提交的数据。它读取数据的时候是不加锁的,只有在更新的时候才会加入行锁操作,但如果更新的条件字段没有索引将会锁整张表(实际上MySQL做了一层优化,过滤时发现不满足条件的数据会释放锁)

  • 可重复读

不解决幻读问题,但解决了脏读、不可重复读问题,其实在MySQL中 该隔离级别也已经解决了幻读的问题。

  • 串行化

解决并发事物带来的所有问题。通过读加共享锁,写加排它锁进行控制,读写互斥,悲观锁理论。

什么是MVCC

多版本并发控制(MVCC),是一种用来解决读-写冲突的无锁并发控制,通俗的讲就是MVCC通过保存数据的历史版本,根据比较版本号来处理数据是否显示。从而达到在读操作的时候不会阻塞写操作,写操作不会阻塞读操作,同时也避免脏读和不可重复读。MySQL的可重复读隔离级别就是用这种思想来实现的。

什么是当前读和快照读

  • 当前读

当前读指的就是它读取的记录是最新版本的。由于它要读取记录的版本是最新版本,所以读取时须保证其他事务不能修改当前记录,因此需要对读取的记录进行加锁。说白了可以简单理解为我们多个客户端并发执行update tb set fd = fd + 1 操作,保证数据最新。

  • 快照读

快照读可以理解为不加锁的select操作就是快照读;快照读的前提是隔离级别不是串行级别,因为在串行隔离级别,快照读可以理解为当前读;快照读的出现,主要解决了在不加锁的情况下也可以进行读取,降低了锁开销;它的实现基础就是多版本并发控制,即MVCC;由于它是基于多版本并发控制,所以使用快照读读取的记录并不一定是最新记录。

当前读,快照读和MVCC关系

准确的说,MVCC主要基于"维护一条数据的多个版本,进而保证在读操作的同时不会阻塞写操作,写操作的同时也不会阻塞读操作,来完成可重复读的需求。

快照读其实就是MVCC的一种体现方式,进行非阻塞读。相对而言,当前读就是悲观锁的体现,每次进行查询操作时,mysql都认为其是不安全操作,为其加锁保证安全,但每次读取的数据为最新数据

MVCC实现原理

MVCC模型在mysql中的具体实现主要是由隐藏字段,undolog,read-view等去完成的。

隐藏字段

隐藏字段中除了咱们自定义的字段外,还隐含着其他属性字段,是系统默认给加上去的,比如roll_pointer,trx_id等字段。

  • roll_pointer

回滚指针,指向这条记录的上一个版本

  • trx_id

记录操作该数据事务的事务ID,也可以叫它版本号,用于版本比较,从而找到快照

  • db_row_id

隐藏ID ,当创建表没有合适的索引作为聚集索引时,会用该隐藏ID创建聚集索引,学过mysql索引知识的应该能懂了

Undo log

Undo log 主要用于记录数据被修改之前的日志,在表信息修改之前先会把数据拷贝到undo log 里,当事务进行回滚时可以通过undo log 里的日志进行数据还原。在MVCC多版本控制中,通过读取undo log的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本

Read view

在innodb 中每个SQL语句执行前都会得到一个read_view,也可以叫它一致性视图。 然后我们查询的数据结果跟read-view的几个重要属性做匹配从而得到正确的快照结果。

Read view 的几个重要属性:

trx_ids: 当前系统活跃(未提交)事务版本号集合;

low_limit_id: 创建当前read view 时“当前系统最大事务版本号+1”;

up_limit_id: 创建当前read view 时“系统正处于活跃事务最小版本号”;

creator_trx_id: 创建当前read view的事务版本号;
复制代码

跟 Read view 的匹配规则:

1. 数据事务ID <up_limit_id 则显示

数据事务ID小于read view中的最小活跃事务ID,则可以肯定该数据是在当前事务启之前就已经存在了的,所以可以显示。

2. 数据事务ID>=low_limit_id 则不显示

数据事务ID大于read view 中的当前系统的最大事务ID,则说明该数据是在当前read view 创建之后才产生的,所以数据不予显示。 (没看懂没关系,后面看我案例分析你就茅塞顿开了)

3. up_limit_id <数据事务ID<low_limit_id 则与活跃事务集合trx_ids里匹配

如果数据的事务ID大于最小的活跃事务ID,同时又小于等于系统最大的事务ID,这种情况就说明这个数据有可能是在当前事务开始的时候还没有提交的。 所以这时候我们需要把数据的事务ID与当前read view 中的活跃事务集合trx_ids 匹配:

情况1: 如果事务ID不存在于trx_ids 集合(则说明read view产生的时候事务已经commit了),这种情况数据则可以显示。

情况2: 如果事务ID存在trx_ids则说明read view产生的时候数据还没有提交,但是如果数据的事务ID等于creator_trx_id ,那么说明这个数据就是当前事务自己生成的,自己生成的数据自己当然能看见,所以这种情况下此数据也是可以显示的。

情况3: 如果事务ID既存在trx_ids而且又不等于creator_trx_id那就说明read view产生的时候数据还没有提交,又不是自己生成的,所以这种情况下此数据不能显示。

当数据的事务ID不满足read view以上3个条件时,再根据undo log获取历史版本数据再和read view 条件匹配 ,直到找到一条满足条件的历史数据,或者找不到则返回空结果;

案例分析

  1. 表user插入一条用户数据
+------+-------+-------+
| id | name | trx_id  |
+------+-------+-------+
| 1 | 小菜   | 101 |
复制代码
  1. 事物A、B、C同时进行如下操作

事物A: update user set name = '小菜A';

事物B: select * from user where id = 1;

事物C: update user set name = '小菜C';

执行流程图如下:

流程图说明:

(1),事物A和C先执行update更新操作,undo log日志生成

(2),事物B执行查询SQL时,根据undo log 生成日志read view 视图

(3),不断取undo log 的快照结果和read view 视图的条件进行匹配,直到匹配到数据,然后返回结果。

拓展延伸

事务隔离级别下的Read view 工作方式

读已提交 级别下同一个事务里面的每一次查询都会获得一个新的read view副本。这样就可能造成同一个事务里前后读取数据可能不一致的问题(不能重复读的问题)

重复读 级别下的一个事务里只会获取一次read view副本,从而保证每次查询的数据都是一样的。

原文链接:4m.cn/01SFw

分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改