MySQL MVCC&LBCC

https://blog.csdn.net/qq_35958391/article/details/124390610

 

MySQL MVCC&LBCC

一:概述

数据库并发场景?

  • 读-读:不存在任何问题,也不需要并发控制
  • 读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读
  • 写-写:有线程安全问题,可能会存在更新丢失问题,比如第一类更新丢失,第二类更新丢失

什么是当前读和快照读?

  • 当前读:像select lock in share mode(共享锁), select for update ; update,insert,delete(排他锁)这些操作都是一种当前读,为什么叫当前读?就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
  • 快照读:像不加锁的select操作就是快照读,即不加锁的非阻塞读;快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本

如果要解决读一致性(事务隔离性)的问题,保证一个事务中前后两次读取数据结果一致,实现事务隔离,应该怎么做?
总体上来说,我们有两大类的方案:

  • MVCC,(Multi Version Concurrency Control)多版本并发控制。
  • LBCC

LBCC

第一种,既然要保证前后两次读取数据一致,那么我读取数据的时候,锁定我要操作的数据,不允许其他的事务修改就行了。这种方案我们叫做基于锁的并发控制Lock Based Concurrency Control (LBCC)。
如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那 就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地 影响操作数据的效率。

MVCC

另一种解决方案,如果要让一个事务前后两次读取的数据保持一致, 那么我们可以在修改数据的之前给它建立一个备份或者叫快照,后面再来读取这个快照就行了。这种方案我们叫做多版本的并发控制Multi Version Concurrency Control (MVCC)。

二:MVCC实现

  • 一个事务只能看到第一次查询之前已经提交的事务修改以及本事务的修改
  • 一个事务不能看到当前事务第一次查询之后创建的事务,以及未提交的事务。

MVCC的效果:我可以査到在我这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了。而在我这个事务之后新增的数据,我是查不到的。所以我们才把这个叫做快照,不管别的事务做任何增删改查的操作,它只能看到第 一次查询时看到的数据版本。

2.1、隐式字段
InnoDB的事务都是有编号的,而且会 不断递增。InnoDB为每行记录都实现了三个隐藏字段:

  • DB_ROW_ID, 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键,Inno DB会自动以DB_ROW_ID产生一个聚簇索引
  • DB_TRX_ID 6byte, 最近修改(修改/插入)事务ID:记录创建这条记录/最后一次修改该记录的事务ID
  • DB_ROLL_PTR 7byte, 回滚指针,指向这条记录的上一个版本 (我们把它理解为删除版本号,数据被删除或记录为旧数据的时候,记录当前事务ID,没有修改或者删除的时候是空)

2.2、undolog日志
undo log(撤销日志或回滚日志)记录了事务发生之前的数据信息、状态,分为insert undo log和update undo logo如果修改数据时出现异常,可以用undo log来实现回滚操作 (保持原子性)

对MVCC有帮助的实质是update undo log ,undo log实际上就是存在rollback segment中旧记录链,它的执行流程如下(要加上begin,否则看不到效果):

  • 第一步:第一个事务,初始化数据,此时的数据,创建版本是当前事务ID (假设事务编号是1),删除版本为空。

在这里插入图片描述

  • 第二步:第二个事务,执行第1次查询,读取到两条原始数据,这个时候事务ID是2。
  • 第三步:第三个事务,插入数据,此时的数据,多了一条tom,它的创建版本号是当前事务编号3。

在这里插入图片描述

  • 第四步:第二个事务,执行第2次查询:

MVCC的査找规则:只能査找创建时间小于等于当前事务ID的数据,和删除时间大于当前事务ID的行(或未删除)。也就是不能查到在我的事务开始之后插入的数据,tom的创建ID大于2,所以还是只能査到两条数据。

  • 第五步:第四个事务,修改数据,修改了 id=2 name=cat 这条记录,这个事务事务ID是4。

这样会导致该记录的undo log成为一条记录版链表,undo log的链首就是最新的旧记录,链尾就是最早的旧记录

2.3、Read View(读视图)

用来实现一个事务对另外一个事务的可见性。每次开启一个事务的时候,都会创建一个ReadView

ReadView的判断规则: (事务隔离性的规则):

  • 如果 trx_id 等于 creator_trx_id,说明当前事务在访问它自己修改过的记录(本事务修改),所以这个版本可以被当前事务访问。
  • 如果 trx_id 小于 min_trx_id,说明在Undo版本链中的这个事务在当前事务生成 ReadView 前已经提交,所以这个版本可以被当前事务访问。(当前事务在执行的时候, 这个快照已经生成了)
  • 如果 trx_id 大于或等于max_trx_id,说明在Undo版本链中的这个事务在当前事务生成 ReadView后才开启,所以这个版本不可以被当前事务访问
  • 如果 trx_id 在 min_trx_id 和 max_trx_id 之间,此时再判断一下 trx_id是不是在 m_ids 列表中
    • 如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问
    • 如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问

开启事务时创建ReadView,ReadView维护当前活动的事务id,即未提交的事务id,排序生成一个数组,访问数据获取数据中的事务id,对比ReadView:

  • 如果在ReadView的左边,可以访问(左边意味该事务已经提交)
  • 如果在ReadView的右边,或者在ReadView中,不可以访问,需获取DB_ROLL_PTR 取上一个版本重新对比。

2.4、如何解决RC和RR的问题

取决于生成ReadView的时机

  • RC ,每次查询前都会生成一个独立的ReadView
  • RR,只是在第一次查询前生成一个ReadView,之后的查询都重复使用这个ReadView

三:LBCC实现

除了MVCC,Mysql还提供了LBCC(Lock Based Concurrency Control)的机制来实现事务的隔离特性。
基于锁的方式起始比较简单,就是一个事务在进行数据查询时,不允许其他事务修改。也就是说,基于锁的机制就使得数据库无法支持并发事务的读写操作,这种方案在一定程度上影响了操作数据的效率。

  • 基于锁的属性:共享锁和排它锁
  • 基于锁的状态:意向共享锁和意向排它锁
  • 基于锁的粒度:表锁、页锁、行锁

3.1、锁的粒度

在之前讲MySQL存储引擎的时候,我们知道了 InnoDB和MylSAM支持的锁 的类型是不同的。MylSAM只支持表锁, 而InnoDB同时支持表锁和行锁

表锁和行锁的区别到底在哪?

  • 锁定粒度:表锁 > 行锁
  • 加锁效率:表锁 > 行锁
  • 冲突概率:表锁 > 行锁
  • 并发性能:表锁 < 行锁

3.2、锁的类型

在Mysql中,锁分为8类:

  • 锁的基本模式: (Shared And Exclusive Locks)行级别锁 和 (Intention Locks)表级别锁

  • 三个:Record Locks、Gap Locks、Next-Key Locs 我们称为锁的算法,也就是说在什么情况下锁定什么范围。

  • 插入意向锁(Insert Intention Locks):是一个特殊的间隙锁。间隙锁不允许插入数据,但是插入意向锁允许 多个事务同时插入数据到同一个范围。比如(4,7), —个事务插入5, —个事务插入6,不 会发生锁等待。

  • 自增锁(AUTO-INC Locks):是一种特殊的表锁,用来防止自增字段重复,数据插入以后就会释放,不需要等到事务提交才释放。如果需要选择更快的自增值生成速度或者更加连续的自增值,就要通过修改自增锁的模式改变。

  • 空间索引谓词锁:Predicate Locks for Spatial Indexes是5.7版本里面新增的空间索引的谓词锁。

3.2、InnoDB中LBCC要解决的问题

  • 问题1-幻读问题(InnoDB)
    范围查询的时候,多次查询结果的数据行数一致
select * from table where id >=1 and id<=4 //锁定2,3 [解决幻读问题]
  • 问题二, for update 实现了排他锁(行锁)
--transaction1 
select * from table where id=1 for update; //查询主键id=1 (行锁,只锁定行)

--transaction2
 update table set name='111' where id=1; //阻塞 
 update table set name='222' where name =''; //阻塞

基于索引来决定的,如果where是索引,那么这个时候,直接加行锁.

  • 问题三, 锁定整个表
select * from table for update; //表锁

update table set name='111' where id=1; //阻塞

3.3、锁的算法(行锁中的锁的算法)

  • Record Lock (行锁) [锁定的是索引]
    顾名思义,记录锁就是为某行记录加锁,它封锁该行的索引记录,并不是真正的数据记录。
-- 记录锁:id 列为主键列或唯一索引列
 SELECT * FROM user WHERE id = 1 FOR UPDATE;

那么,意味着id=1的这条记录会被锁住,如下图所示。

  • Gap Lock(锁定索引区间,不包括record lock)
    间隙锁顾名思义锁间隙,不锁记录。
---间隙锁是基于非唯一索引,它锁定一段范围内的索引记录,比如下面这个查询
 SELECT * FROM user WHERE id BETWEN 1 AND 4 FOR UPDATE;

那么意味着所有在(1,4)区间内的记录行都会被锁住,它是一个左右开区间的范围,意味着在这种情况下, 会锁住id为2,3的索引,但是1、4不会被锁定

  • next Key Lock(锁定索引区间,包括record lock)
    Next-Key 临键锁可以理解为一种特殊的间隙锁,也可以理解为一种特殊的算法,每个数据行上的非唯一索引列上都会存在一把临键锁,当某个事务持有该数据行的临键锁时,会锁住一段左开右闭区间的数据。
-- 根据非唯一索引列 UPDATE 某条记录
 UPDATE user SET name = ‘王五’ WHERE age > 18; 
-- 或根据非唯一索引列 锁住某条记录 
SELECT * FROM user WHERE age > 18 FOR UPDATE;

不管执行上面的哪个SQL,当前事务都会锁定(18,21]这个区间。

当我们使用了范围查询,不仅仅命中了Record记录,还包含了Gap间隙,在这种情况下我们使用的就是临键锁,它是MySQL里面默认的行锁算法,相当于记录锁加上间隙锁。

四:总结
在这里插入图片描述

  • RU隔离级别:不加锁。
  • Serializable所有的select语句都会被隐式的转化为select… in share mode,会和 updates delete 互斥。
  • RR隔离级别下,普通的select使用快照读(snapshot read),底层使用MVCC来实 现。加锁的 select(select … in share mode / select … for update)以及更新操作 update, delete等语句使用当前读(current read),底层使用记录锁、间隙锁、 临键锁。
  • RC隔离级别下,普通的select都是快照读,使用MVCC实现。 加锁的select都使用记录锁,因为没有Gap Lock。除了两种特殊情况 外键约束检査(foreign-key constraint checking)以及重复键检査(duplicate-key checking)时会使用间隙锁封锁区间。所以RC会出现幻读的问题。

五:事务隔离级别怎么选?

  • RU和Serializable肯定不能用

RC和RR主要有几个区别:

  • 1、 RR的间隙锁会导致锁定范围的扩大。
  • 2、 条件列未使用到索引, RR锁表,RC锁行。
  • 3、 RC的"半一致性”(semi-consistent)读可以增加update操作的并发性。

实际上,如果能够正确地使用锁(避免不使用索引去枷锁),只锁定需要的数据,用默认的RR级别就可以了

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值