Mysql事务隔离级别,MVCC实现原理,当前读,快照读,Next-Key锁

一,事务

1,什么是事务

      数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。

      Mysql数据库中,存储引擎是InnoDB时,支持事务,MyISAM存储引擎不支持事务。

2,事务四大特性 ACID

1、原子性(Atomicity):一个事务开启后,事务中的全部操作是不可分割的,要么全部完成,要么全部不执行。如果事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。
2、一致性(Consistency):几个并行执行的事务,其执行结果必须与按某一顺序串行执行的结果相一致。  
3、隔离性(Isolation):事务的执行不受其他事务的干扰,事务执行的中间结果对其他事务必须是透明的。 
4、持久性(Durability):对于任意已提交事务,系统必须保证该事务对数据库的改变不被丢失,即使数据库出现故障。

二,事务的隔离级别

1,脏读,不可重复读,幻读

了解事务隔离级别之前,首先要了解什么是数据的 脏读,不可重复读,幻读 问题。

脏读:A事务读到B事务未提交的数据。一旦B事务回滚,那么A事务读到的数据就是脏数据,所以称为 "脏读"。

不可重复读:A事务执行第一次查询之后,B事务修改(update)了这部分数据并提交,A事务执行第二次查询,发现同一事物内对相同数据的前后两次查询数据不一致,称为 "不可重复读"。

幻读:A事务执行第一次查询之后,B事务对数据进行了增删(insert和delete)操作并提交,A事务执行第二次查询,发现同一事务内前后两次查询结果件数不一致,A事务仿佛出现了幻觉一样,称为 "幻读"。

简单来说,脏读就是读到其他事务未提交的数据,不可重复读就是其他事务对当前事务读取的数据进行了update,幻读就是其他事务往数据库 insert 或 delete 了满足当前事务查询条件的数据。

2,事务隔离级别

mysql对应解决以上 脏读,不可重复读,幻读 问题,有四种隔离级别,从低到高依次是,

Read uncommitted(读未提交) 、Read committed (读已提交 RC)、Repeatable read (可重复读 RR)、Serializable(串行化)。

事务隔离级别脏读不可重复读幻读
Read uncommitted(读未提交)
Read committed (读已提交)
Repeatable read (可重复读)
Serializable(串行化)

✕:不会出现;✓:可能会出现

Mysql默认使用的事务隔离级别是Repeatable read (可重复读 RR ),而Oracle,Sql Server 默认的是 Read committed(读已提交)。

脏读是读到其他事务未提交的数据嘛,所以使用RC隔离级别就可以避免脏读问题,这个好理解。

那么,RR下又是如何解决不可重复读的问题呢?

而且,从上表来看,RR隔离级别下,并没办法解决幻读的问题,Mysql选择RR为默认的隔离级别,又是如何解决幻读的问题呢?

三,MVCC

1,当前读和快照读

当前读:select lock in share mode (共享锁), select for update; update; insert; delete (排他锁)这些操作都是一种当前读,这些操作读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁

快照读(非阻塞读):不加锁的 select 操作就是快照读,即不加锁的非阻塞读。快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC ,可以认为 MVCC 是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销。在RR隔离级别下,事务首次调用快照读的地方很关键,创建快照的时机决定了数据的版本。

RC与RR隔离级别下,InnoDB快照读的实现原理:

1,数据行里的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID隐式字段

  • DB_TRX_ID 记录最近创建或者更新这条行记录的事务 ID,大小为6个字节
  • DB_ROLL_PTR:表示指向该行回滚段(rollback segment)的指针,大小为7个字节,InnoDB便是通过这个指针找到之前版本的数据。
  • DB_ROW_ID:行标识(单调自增ID),大小为6字节,如果表没有主键,InnoDB会自动生成一个隐藏主键,因此会出现这个列。
  • deleted_bit :删除flg,一条数据被更新或者删除时,优先更新该数据记录删除flg为true,而不是真正删除数据。

2,undolog

  • insert undo log:事务insert新数据时产生的log,只在事务回滚时需要,在事务提交后可立即丢弃。
  • update undo log:事务对数据进行delete或者update参生的log,不仅在事务回滚时需要,快照读也需要,所以不能随便删除,只有在快照读或者事务回滚不涉及该日志时,才会删除改日志记录,mysql的innoDB有专门的purge线程来删除上面提到删除flg为true的数据记录。

通过以上的隐式字段以及undolog,事务更新数据时,流程大致如下

  1. 数据库对被修改行加排他锁;
  2. 先将修改行数据拷贝至undolog,即旧记录备份在undolog,这也是undolog被叫做回滚日志的原因;
  3. 旧记录备份完成,修改数据行,DB_TRX_ID事务ID递增,DB_ROLL_PTR回滚指针指向旧记录备份地址;
  4. 事务提交,排他锁释放;

 

3,ReadView

      ReadView就是事务进行快照读时产生的读视图,某一事务进行快照读时,对该记录创建一个ReadView,然后ReadView遵循一个可见性算法,会先取出更新对象数据的最新记录中的DB_TRX_ID,与数据库中当前活跃事务的DB_TRX_ID去比较,如果修改对象的事务ID大于等于活跃事务ID,则通过上面提及的DB_ROLL_PTR去取出undolog中上一层的DB_TRX_ID,直到取出的DB_TRX_ID小于当前活跃事务的DB_TRX_ID,那么这个DB_TRX_ID所在的记录就是当前事务能看到的最稳点的数据版本,可能是最新的数据,也可能旧版本的数据。

Mysql源码实现ReadView的一些关键属性,如下图

RC与RR隔离级别下,InnoDB快照读的区别:

RC隔离级别下,每一次快照读都会生成一个ReadView,即能看到最新的数据,

而RR下,一个事务的第一次快照读才会生成一个ReadView,后续快照读都是读取的同一个ReadView,所以RR下的快照读读到的并不一定是数据的最新版本,而有可能是之前的历史版本。

所以,至此,RR下又是如何解决不可重复读,这个问题可以总结回答为,RR通过 快照读 + ReadView ,保证在同一个事务的快照读都读取的是同一个ReadView,即同一事务的多次快照读读到的数据一致,从而解决了不可重复读。

2,Next-Key锁(行锁+gap锁)

  • Record Lock 行锁:对索引记录的锁,如果一个表没有定义索引,InnoDB创建一个隐藏的聚集索引(DB_ROW_ID)并使用该索引进行记录锁定。
  • Gap Lock      间隙锁:间隙锁是对索引记录之间的间隙的锁,或者是对第一个索引记录之前或最后一个索引记录之后的间隙的锁。防止同一事务两次当前读出现幻读的情况,只有在RR以及串行化隔离级别下会有Gap锁。
  • Next-Key锁:next-key 锁是索引记录上的行锁和索引记录之前的间隙上的间隙锁的组合。

Gap锁会用在非唯一索引或者不走索引的当前读中

  1. 如果where条件全部命中,则不会用Gap锁,只会加行锁。
  2. 如果where条件部分命中或者全都不命中,则会加Gap锁。

关于Gap锁上锁的范围,这里直接贴上Mysql官方文档的例子 ,传送门:  Next-Key 锁

在一个事务已经对某些区间加上Gap锁的情况下,其他事务就无法往数据库插入或者删除这些区间的数据,这就一定程度上防止了幻读的产生。

四,模拟

1,模拟表结构  

 默认事务隔离级别是RR

 

2, 读未提交隔离级别 模拟 脏读

 3,读已经提交隔离级别 模拟 不发生脏读

4,读已经提交隔离级别 模拟 不可重复读

初始状态 money = 800 ,事务2 update money为1000 ,事务1同一事物内前后两次查询money不一致

 

 

 

5,可重复读隔离级别 模拟  不发生不可重复读

步骤和上次 读已经提交隔离级别模拟 不可重复读 几乎一样,只不过一开始将事务隔离级别指定为 RR (REPEATABLE READ),所以就不贴分步的图了,贴个结果吧。

6,可重复读隔离级别模拟  不发生幻读

表结构如下

 

-- ----------------------------
-- Table structure for test_user
-- ----------------------------
DROP TABLE IF EXISTS `test_user`;
CREATE TABLE `test_user`  (
  `id` int(0) NOT NULL,
  `name` varchar(10) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
  UNIQUE INDEX `index_id`(`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

初始数据

1, 如果where条件全部命中,则不会用Gap锁。

2,如果where条件部分命中或者全都不命中,则会加Gap锁。

全都不命中

 部分命中

 

 

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值