前言
- 网上有很多文章都有对MySQL的版本并发控制器MVCC的介绍,这次结合个人理解记录一下,加深印象也方便以后查阅。
MySQL事务特性(ACID)
- 原子性:一个事务内的所有操作要么全部执行,要么全部不执行。如果事务执行到中间过程时,出现异常,需要把之前已经执行的数据进行回滚操作
- 一致性:一个事务开始前和结束后中,数据库中数据是具有完整性的。这表示更新的数据符合所有的预计规则
- 持久性:一个事务内数据提交结束后,数据是永久性改变,服务器故障也不会丢失最新数据
- 隔离性:多个事务如果并发执行读写操作,那么事务间操作的数据是隔离的、互不影响的,防止出现脏数据
其中事务隔离又分为四个级别:读未提交、读已提交、可重复读、串行化
MySQL事务隔离级别
- 读未提交(read uncommitted):一个事务内可以读取到其他事务已操作但未提交的数据,会出现脏读、不可重复读、幻读
- 读已提交(read committed):一个事务内可以读取到其他事务已操作且已提交的数据,会出现不可重复读、幻读
- 可重复读(repeatable read):一个事务内查询的数据保持一致,会出现幻读
- 串行化(serializable):一个事务内在所影响的所有数据都加读锁和写锁,等事务最终提交完成后再释放锁,这能解决脏读、不可重复读、幻读,但效率最低
MySQL事务隔离级别所带来的问题
- 脏读:一个事务内读取到其他事务未提交的数据,而其他事务内的数据可能会因为回滚操作,所以导致脏数据的产生
- 不可重复读:一个事务内读取的数据被其他事务更新且提交了,导致当前事务再次读取时发现数据不一致
- 幻读:一个事务内根据条件读取数据,这时其他事务新增或删除符合当前事务查询条件的数据且提交事务,导致当前事务再次根据条件查询发现结果条数和之前的不一致
注:不可重复读和幻读有点相似。不可重复读是对同一数据进行修改操作,幻读是新增或删除数据行。
MVCC版本并发控制器
- MySQL中对事务并发读读情况,不需要加其他额外操作来保证数据的一致性。
- MySQL中对事务并发写写情况,是直接加锁。一个事务更新了某条数据,就会给该数据加锁,其他事务想要更新同一条数据需要等当前事务提交解锁后才能更新。
- MySQL中对事务并发读写情况,使用MVCC多版本并发控制器进行处理,其目的是为了提高数据库在高并发下的性能效率。
MVCC之版本链
- 在MySQL数据库中使用innodb储存引擎,在表的聚集索引记录中会包含两个隐藏列:trx_id和roll_pointer
- trx_id:在对某条数据进行更新时,会把对应的事务id赋值到当前隐藏字段trx_id
- roll_pointer:在对某条数据进行更新时,当前隐藏列会用一个指针指向改动前的历史版本(是指向undo日志记录,如果是新增,当前隐藏列列为null)
- MySQL在对数据进行更新操作(insert、update、delete)时,MySQL会记录redo日志(解决服务宕机重启数据丢失问题),binlog日志(用于数据备份,主从复制等),undo日志文件(用于事务提交或回滚操作)中,
- 而每一条undo日志记录都对应着一个roll_pointer属性字段,把这些undo日志串联起来就是一个链表结构,这就是版本链
版本链的头节点是最新数据,每个节点都包含的有对应事务id
- MySQL隔离级别如果使用RN(读未提交),那就直接使用版本链中最新记录访问数据
- MySQL隔离级别如果使用串行化,那就通过加锁解锁的方式访问数据
- MySQL隔离级别如果RC(读已提交)和RR(可重复读),那就需要通过版本链中不同版本进行访问记录,那么这里就需要知道该用哪种版本来的数据进行访问?
MVCC之ReadView
- ReadView是一个存储活跃事务ID的列表,里面包含四个重要的属性:
- m_ids:表示活跃事务id列表(开启事务,但还未提交)
- creator_trx_id:表示当前事务id
- min_trx_id:表示活跃事务列表中最小事务id
- max_trx_id:表示当前最大事务id + 1(代表下次递增的事务id)
- 当一个事务访问某行数据时,按照官方规则规定读取数据:
- 1、被访问数据的事务id(trx_id)小于当前事务ReadView列表min_trx_id,表示在当前事务开启之前数据就已经被提交,可以访问
- 2、被访问数据的事务id(trx_id)大于当前事务ReadView列表max_trx_id,表示在当前事务开启后数据才创建提交的,不能被访问
- 3、被访问数据的事务id(trx_id)在当前事务ReadView列表的min_trx_id和max_trx_id之间,那就分为两种情况:
- 3.1、被访问数据的事务id(trx_id)在当前事务ReadView列表m_ids(活跃列表)中,表示当前被访问数据是和当前事务同时间段创建,不能被访问
- 3.2、被访问数据的事务id(trx_id)不在当前事务ReadView列表m_ids(活跃列表)中,表示当前被访问数据是在当前事务创建之前提交,可以访问
注:MySQL事务创建后会在第一个执行更新操作(insert、update、delette)时,才会创建一个事务id,后续更新操作继续使用当前事务id
- RC(读已提交)和RR(可重复读)之间很大的一个区别点就是基于ReadView创建时机的不同:
- RC(读已提交)隔离级别:每次读取数据前,都生成一个ReadView
- RR(可重复读)隔离级别:在第一次读取数据前,生成一个ReadView
RC(读已提交)会出现不可重复读
- 例如:表里有新增一条数据dataA,其事务id为10。然后创建事务A和事务B并发执行,其事务A的id为20,其事务B的id为30。
- 这时事务A读取数据dataA会生成一个ReadView列表:m_ids【20,30】,creator_trx_id=20,min_trx_id=20,max_trx_id=31
- 因为被读取的数据dataA事务id为10,小于当前事务min_trx_id,可以访问数据dataA。
- 这时事务B读取数据dataA会生成一个ReadView列表:m_ids【20,30】,creator_trx_id=30,min_trx_id=20,max_trx_id=31
- 这时dataA事务id为10,仍可以读取,然后事务B对dataA做更新操作,并提交事务。这时dataA事务id变为30
- 这时事务A读取数据dataA会再次生成一个ReadView列表:m_ids【20】,creator_trx_id=20,min_trx_id=20,max_trx_id=31
- 这时dataA事务id为30,这时属于上述规则3.2,可以访问dataA。
- 这就是RC隔离级别产生不可重复读的机制
RR(可重复读)不会出现不可重复读
- 例如:表里有新增一条数据dataA,其事务id为10。然后创建事务A和事务B并发执行,其事务A的id为20,其事务B的id为30。
- 这时事务A读取数据dataA会生成一个ReadView列表:m_ids【20,30】,creator_trx_id=20,min_trx_id=20,max_trx_id=31
- 因为被读取的数据dataA事务id为10,小于当前事务min_trx_id,可以访问数据dataA。
- 这时事务B读取数据dataA会生成一个ReadView列表:m_ids【20,30】,creator_trx_id=30,min_trx_id=20,max_trx_id=31
- 这时dataA事务id为10,仍可以读取,然后事务B对dataA做更新操作,并提交事务。这时dataA事务id变为30
- 这时事务A读取数据dataA会使用第一次的ReadView列表:m_ids【20,30】,creator_trx_id=20,min_trx_id=20,max_trx_id=31
- 这时dataA事务id为30,这时属于上述规则3.1,不可以访问。
- 这时会根据dataA事务id为30所在行的roll_pointer字段,找到指向上一个版本中dataA事务id为10的数据。这个数据是可以访问。
- 这就是RR隔离级别解决不可重复读的机制
最后
- 虚心学习,共同进步 -_-