MySQL隔离级别的底层理解(MVCC+锁)

时间:2022-11-24 07:55:02

MySQL事务的隔离级别和并发的关系

  • mysql是一个C/S架构的软件, 也就意味着, 同一个mysql服务器可能同时存在很多的clients集合来访问. 此时最重要的是什么?   并发性. 并发时候的安全. 
  • 并发和数据库安全性本来就是相互矛盾的。要保证更好的安全性最好的方式是什么? 完全舍弃并发. 同时只允许一个用户访问mysql, 串行化. 但是显然这是不现实的事情. 所以权衡两者,出现了不同的隔离级别. 隔离级别越高, 安全性越高, 并发度自然就越低.

至此,自然就知晓了不同的隔离级别出现的原因了.

四种隔离级别:

read uncommited: 读未提交. 事务还没有提交, 别人就可以看见。 存在脏读,不可重复读, 幻读现象, 完全没有任何安全性, 隔离性最低, 相应并发度最高,but 几乎不用, 实在是不安全.

read commited: 读已提交. 事务提交,别人才能看见, 与上述相比解决了脏读的现象. 安全性有所提升. 也是现在数据库比较常见的隔离级别之一

repeatable read: 可重复读, 相较于上述,通过每次读取第一版历史快照的方式实现重复读前后数据一致性. 解决了不可重复复读的问题. 

serializable: 串行化, 通过加锁的方式来保证了绝对的安全, 不存在任何的安全性问题, 同时也解决了幻读的问题, 针对幻读的解决用到了间隙锁,详见后文阐述。加锁导致了并发度很低. 并发度是最低下的.

查看隔离级别和事务提交:

  • select @@tx_isolation;      查看事务隔离级别
  • select @@autocommit;      查看是否自动提交事务 

MySQL隔离级别的底层理解(MVCC+锁)

MVCC支持的隔离级别

MVCC快照读取是一种非锁定读. 多版本并发读取.

适用于读已提交和可重复读的底层.      

不加锁的不用wait阻塞的并发读取. MVCC是一种并发控制技术

select每一次都是读取的快照数据

update,insert, delete, lock操作读取都叫做当前数据. (后序细说.

实现核心:快照数据, 快照数据就像是照片一样. 每一次读取快照数据,而不是实际的表中数据,然后快照数据按照不同的隔离级别要求规则来跟新。来实现不加锁的一种读取方式.

快照数据来源: 多版本数据链条.   依照不同隔离级别读取不同版本的数据来实现。

首先是已提交隔离级别(RC):

  • 每一次select都重新生成快照数据.
  • 竟然是每一次select都从新生成快照数据. 那快照数据就是最新版本的快照数据, 所谓快照数据就像是卡擦一下拍的一张照片.
  • 快照数据的跟新时期:  存在commit的时候, 或者事务本身自己进行update更新操作.

注意, RC隔离级别下存在不可重复读原因就是上述的每一次select都重新生成快照数据.竟然每一次select都重新拍摄一张快照, 快照自然就是最新版本的。别的事务完成的commit操作自然也会被读取到. 自然在其他事务commit前后的数据读取结果不会一致,也就实现了可重复读取操作了撒.

MySQL隔离级别的底层理解(MVCC+锁)

怎么读取的: 其实就是在版本链的最初版本卡擦一下拍了一张照片而已:

MySQL隔离级别的底层理解(MVCC+锁)

 快照: 也叫做read view: 读视图。肉眼所见不一定为真。所以还是存在幻读嘛

再强调RC核心. 每一次select均重新重新重新重新产生快照数据.

并且每一次commit均会跟新版本数据链. 每一次commit均跟新版本数据链条。

(实现了read commited, commit了就跟新版本数据链条 

  快照数据依据版本数据链条的头部最新数据产生. 卡擦,照最新的)

每一次的select 均从新产生快照数据,自然会出现其他事务commit前后两版截然不同的快照数据,这就是不可重复读取. 

可重复读隔离级别(RR):

好嘛好嘛,既然每一次select都产生快照数据会造成,其他commit前后select结果是截然不同的两张快照数据。      --------》   在本事务中. 我仅第一次commit操作才产生固定的快照数据. 后序每一次select 都直接拿取第一次select的快照数据就OK了塞.    (重复读取操作也不会造成read view 的差异了. )

当然:快照数据还有一个原则,当前事务自己的跟新操作可以照上. 

验证一下RC是不是每一次select操作均产生快照数据以及RR仅第一次select产生快照数据

(不然口说无凭):

RC隔离级别之下的结果

事务1: 先进行update 王妈年龄 = 50操作.  and commit;    

事务2: 在事务1 完成上述操作之前进行select. 和完成操作之前 select。 按照上述快照数据产生的原则是看当前commit的最新数据和本事务更改的数据,自然结果不一致   

MySQL隔离级别的底层理解(MVCC+锁)

RR 隔离级别下的结果. 不演示大家也晓得,竟然是可重复读取操作, 前后select结果必然一致,这个一致,依赖的就是快照数据产生的时机不同。RR仅第一次select产生快照数据,后序select直接读取第一版select的快照数据即可.   (自然重复读取前后保持是一致的)

当前读(Current Read)什么时候是当前读取?  

读取的是记录的最新版本,并且当前读返回的记录。如insert,delete,update,select...lock in share mode/for update

上述这些操作. 说白了都不是基于read view的结果进行的. 而是基于内存中实实在在的存储的数据是什么就是什么。而不是依照快照数据来完成上述操作。都是依照数据库的实实在在的当前最新数据完成上述操作. 

MySQL隔离级别的底层理解(MVCC+锁)

其实undo log也依赖于版本控制。或者说数据版本链条。undo log 依赖的就是一个roll指针,回滚指针, 回滚到之前的版本. 出错回滚操作. 

DB_TRX_ID:事务ID DB_ROLL_PTR:回滚指针

锁支持的隔离级别

锁类型:

表锁&行锁

表级锁:   锁的粒度大, 锁住整张表, 并发度最低

行级锁:   锁的粒度小, 锁住一行, 并发度相较于表锁更高

共享锁与排他锁

shared mutex: S锁,共享锁. 也叫做读锁, 允许多个client 同时获取一把读锁. 可以读 + 读锁不冲突

Exclusive mutex :X锁, 排他锁, 写锁,独占锁。很是霸道. 不允许和仍何锁共存. 一旦加了写锁,便不可以再加写锁甚至读锁.

加锁方式:   select ... lock in share mode; 加读锁    select ... for update; 加写锁.  

Innodb 支持行锁 + 间隙锁(gap lock)解决幻读问题

  • innodb 存储引擎支持行级锁, 拥有更高的并发能力
  • MyISAM 存储引擎支持表锁, 并发量相对低于innodb存储引擎
  • 注意: innodb的行锁是加在索引列上的.而不是给表的行记录加锁实现的. (用于索引项,否则还是表级锁)
  • 索引可能失效, 如果直接整个全表扫描效率还高于使用索引, innodb会采取全表扫描, 加表锁,而非使用行锁.

record lock 记录锁. 锁住某行记录. 锁住索引项对应的所有记录.

gap lock: 间隙锁. 用于锁住记录与记录之间的间隙. 避免间隙插入操作造成的幻读现象. 

Next-key lock = record lock + gap lock.

案例测试:

目前,存在一张student 表结构数据如下:

MySQL隔离级别的底层理解(MVCC+锁)

 首先我先测试一下是表锁还是行锁:

先对where id = 2 加读锁. 再另起一个事务尝试对此列加写锁,肯定是失败的,写锁不与其他锁共存。    当然再另起一个事务加读锁是可以的,因为读锁与读锁是可以共享的.   验证家的是行锁,不是表锁,对其他列加写锁是OK的。

注意,对索引列才能加锁.

MySQL隔离级别的底层理解(MVCC+锁)

MySQL隔离级别的底层理解(MVCC+锁)

 上述实验初步验证了两个点:

  • 首先innodb是对索引列支持加行锁的. 性能更高,锁粒度小
  • 另外读锁(共享锁) 可以多事务同时共享,不冲突。 写锁(排他锁) 叼的很,不与任何锁共事。

场景2: 验证串行化隔离级别的场景下所有操作自动加锁. (串行化底层实现, 加锁)

首先第一步修改隔离级别为: serializable

MySQL隔离级别的底层理解(MVCC+锁)

接下来,我们尝试做一些读取和写操作,看看是否存在等锁的现象.

 首先读取一下id = 3这行记录,再另外一个事务尝试为这条记录的name 修改为丑八怪.

MySQL隔离级别的底层理解(MVCC+锁)

 很是明显,和我们刚刚上述加写锁的现象一毛一样,被阻塞住了,其实也就是暂时获取不了写锁.

MySQL隔离级别的底层理解(MVCC+锁)

至此,可以简单的证明串行化的底层就是加锁. 对读写操作加上对应的读写锁.

接下来, 尝试给区间加锁。看一看间隙锁是如何加的. 同时对于幻读现象的解决做理解。首先先看一个幻读的现象.   当然串行化下是不会发生的。因为加了间隙锁。为了可以看到幻读现象。我就直接画图简单说明一下幻读现象.

MySQL隔离级别的底层理解(MVCC+锁)

 我们尝试一下在串行化隔离级别场景下是否存在如此现象.

MySQL隔离级别的底层理解(MVCC+锁)

啥情况,根本就不让插入. 其实就是对麻花索引枷锁了。而且加的并不是record lock. 而是key-next lock = record lock + gap lock;

(麻花-1, 麻花] (麻花, +oo)  全部给锁住了.

MySQL隔离级别的底层理解(MVCC+锁)

间隙锁,会将索引左右的间隙加锁。gap lock , 也就是将左右记录中间部分也给锁住,如果没有左右记录就直接索住到无穷. 锁住间隙就是为了避免从间隙中插入。造成了幻读现象。有了间隙锁,别的事务在插入的时候会被阻塞。自然也就避免了因为跟新记录而出现的幻读的现象了

串行化隔离级别解决幻读的方式就是通过自动加上间隙锁的方式来避免, 其他事务在刚刚查询的位置及其之后再进行插入并且提交. 从而引发的刚刚的查询结果失效(结果跟新了)。仿佛刚刚的查询结果是虚幻的幻读现象.  解决办法就是加上间隙锁。使得无法在对应查询位置(区间)进行插入造成之前的查询结果虚幻.

MySQL隔离级别的底层理解(MVCC+锁)

测试测试罗: 用另外一个事务对我们的间隙做出插入,看看是否可以完成插入操作

(id, name, age):   先插入一个(10, "王婆", 25); 插入不进来,被(21, 30)之间的gap lock 锁住的.

MySQL隔离级别的底层理解(MVCC+锁)  

再插入 (10, "王婆", 20) 还是插入不进来, 在(20, 21) gap 间隙中, 也被锁住了.

MySQL隔离级别的底层理解(MVCC+锁)  But 神奇来了. 插入(4, "王婆", 20) OK了,可以插入,因为他并不在(20, 21) 之间. 而是在(20, 20) 之间.    why?   辅助索引相等按照主键主键主键递增插入间隙.  当然前提没有间隙锁. 

MySQL隔离级别的底层理解(MVCC+锁)

意向锁

  • 意向锁是为了更快的检测行级锁的存在,以便于判断是否能加表锁而出现的.

简单的举一个栗子, 比如所一张表中存储一万行数据, 现在我们需要为其加表级排他锁, 此时我们需要检测的是表中是否加了行锁. 怎么检测行锁. 是遍历每一行去判断吗? 效率过于低下了,

于是出现了意向锁. 意向锁相当于是一种表级锁.  

每一次获取锁的时候首先都会先获取意向锁. 意向锁和行锁是可以共存的.      

意向锁是mysql内存自己加上的, 要获取行锁就会自动先获取表的意向锁.

这样真的加表锁的时候,看到了存在对应地 意向锁, 就知道这个表存在某一行加上了锁,所以就不能再加排他锁了。

MySQL隔离级别的底层理解(MVCC+锁)

 意向锁,就是为了做行级锁和表级锁之间地一个桥梁地存在. 否则在加表级锁地时候如何感知一张表是否已经存在行级锁,难道要一行一行地轮询吗?数据量大的情况下效率过于低下.

死锁简单理解

  • 对于表一般不太会死锁。因为锁粒度大. 交叉获取锁冲突发生的可能性小. 但是对于Mysql的Innodb存储引擎因为存在索引,而且支持行级锁。并发度高,锁粒度小,所以更容易出现锁的冲突,死锁现象.

MySQL隔离级别的底层理解(MVCC+锁)

 Mysql解决办法, 测试一下看看就晓得了

MySQL隔离级别的底层理解(MVCC+锁)

 Mysql要是存在这种交叉相互获取对方事务持有的锁情况下会自动检测到死锁报错

undo log 和 redo log 日志的简单理解

  • 在上述, 基本算是简单的介绍完了RC, RR, 串行化隔离级别的底层实现. RC和RR 依赖于MVCC非锁定读读取快照数据的方式实现了  读取commit数据, 和仅第一次select生成快照数据来实现的可重复读
  • 也介绍了串行化的底层实现是加锁. 加记录锁, 间隙锁, 意向锁. 自动加.

undo log (逻辑日志/回滚日志)和 redo log (重做日志) 都是物理日志.   当然,日志在内存中存在缓冲区,专门用于临时记录日志内存,肯定存在缓冲区不能说每一次日志操作都直接进行磁盘IO,肯定是在一定的实际将缓冲区中的日志内存落盘刷新到磁盘上面去的.

如果宕机了,断电了,再次启动数据库,内存上的数据如何恢复。打开redo log 物理日志, 将redo log 物理日志上面的操作进行从做, 还原断电前的内存内容.

Mysql 日志 》 MySQL数据.  数据没了可通过日志恢复. 一次登录的数据是临时的. 下一次可以通过日志恢复. 日志才是MySQL持久化的关键.