Mysql(15)——锁机制 + MVCC(全)

前言

事务的隔离级别在之前我们已经学习过,那么事务隔离级别的实现原理是什么呢?锁+MVCC
Mysql(15)——锁机制 + MVCC(全)_第1张图片

下面我们就来分开讲解:

表级锁&行级锁

注意:表锁和行锁说的是锁的粒度,不要以为它与下面讲到的其他锁是单独的概念。因为有表级共享锁等概念的存在。

  • 表级锁:对整张表加锁。开销小,加锁快,不会出现死锁;锁粒度大,发生锁冲突的概率高,并发度低。
  • 行级锁:对某行记录加锁。开销大,加锁慢,会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度高。

排它锁&共享锁

  • 排它锁(Exclusive),又称为X 锁,锁。
  • 共享锁(Shared),又称为S 锁,锁。

X和S锁之间有以下的关系:
SS可以兼容的,XS、SX、XX之间是互斥的

  • 一个事务对数据对象 O 加了 S 锁,可以对 O 进行读取操作但不能进行更新操作。加锁期间其它事务能对O 加 S 锁但不能加 X 锁。
  • 一个事务对数据对象 O 加了 X 锁,就可以对 O 进行读取和更新。加锁期间其它事务不能对 O 加任何锁。

显示加锁:

  • select ... lock in share mode 强制获取共享锁,
  • select ... for update 强制获取排它锁

Mysql(15)——锁机制 + MVCC(全)_第2张图片
从上面的示例中,我们可以看出InnoDB中,是支持行锁的。
以主键为过滤条件时,事务1和事务2可以获取不同行的排它锁

接着看以下的例子,以非索引列作为过滤条件,会出现什么情况呢?
Mysql(15)——锁机制 + MVCC(全)_第3张图片
于是我们得出结论:
InnoDB的行锁是加在索引项上面的,是给索引加锁,并不是单纯的给行记录加锁,所以如果过滤条件没有加索引的话,使用的就是表锁,而不是行锁。 因此,我们得出InnoDB并不是只有行锁,还存在表锁。

行锁使用的条件:加在索引项上。

InnoDB行级锁

1、行级锁

InnoDB存储引擎支持事务处理,表支持行级锁定,并发能力更好。

  1. InnoDB行锁是通过给索引上的索引项加锁来实现的,而不是给表的行记录加锁实现的,这就意味着只有通过索引条件检索数据,InnoDB才使用行级锁,否则InnoDB将使用表锁。 (原理是在二级索引树上搜索了主键索引)
  2. 由于InnoDB的行锁实现是针对索引字段添加的锁,不是针对行记录加的锁,因此虽然访问的是InnoDB引擎下表的不同行,但是如果使用相同的索引字段作为过滤条件,依然会发生锁冲突,只能串行进行,不能并发进行。
  3. 即使SQL中使用了索引,但是经过MySQL的优化器后,如果认为全表扫描比使用索引效率更高,此时会放弃使用索引,因此也不会使用行锁,而是使用表锁(此时需要注意间隙锁与表锁),比如对一些很小的表,MySQL就不会去使用索引。

所以:InnoDB默认情况下是使用表锁,一旦使用索引项,就会使用行锁。

串行化隔离级别的实现原理

串行化的隔离级别之下,不需要手动获取锁,会自动加上表的共享锁和排它锁 , 示例如下:
Mysql(15)——锁机制 + MVCC(全)_第4张图片
我们知道串行化的隔离级别之下,解决了可重复读未解决的幻读问题,那么它是怎么实现的呢?这就是我们接下来要学习的内容。

串行化解决幻读:依靠的就是间隙锁。

2、 间隙锁 : 串行化隔离级别下解决幻读的利器

当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB 会给符合条件的已有数据记录的索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)” ,InnoDB 也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁。

Mysql(15)——锁机制 + MVCC(全)_第5张图片
不仅给表中满足范围查询要求的记录间隙加锁,给在其之后的 “ 空洞 ” 区域也加上了锁。
间隙(gap)(左开右闭)的范围是 : (11,12],(12,22],(22,23],(23,+正无穷)
给这些间隙加锁,就叫做间隙锁。

在串行化的隔离级别下:使用范围查询不仅给满足范围的记录添加了行锁,还添加了间隙锁,从而消除幻读现象。
next-key lock : record-lock + gap-lock

Mysql(15)——锁机制 + MVCC(全)_第6张图片

读者在理解时要注意思考间隙锁的作用范围。
当查询的表是小表时,很可能用不到索引,此时在串行化的模式下就会添加表锁,即使你插入的范围不再间隙锁的范围之内,也无法插入。

综上,这种情况解决了串行化在范围查询下的幻读问题

那么在等值查询的条件下,间隙锁是如何工作的呢?

  • 在等值查询时,使用的过滤条件如果是主键索引或者唯一键索引,由于值不可以重复,添加行锁后,那么在另一个事务中也是可以完成插入操作的。
  • 在等值查询时,使用的过滤条件如果是辅助索引,由于值是可以重复的,为了防止幻读,就会在已过滤的辅助索引列上加上行锁之后,再在其左右两边范围内加上间隙锁,在间隙锁范围内是不能够完成插入获取排它锁。

示例如下:
Mysql(15)——锁机制 + MVCC(全)_第7张图片

InnoDb表级锁

在绝大部分情况下都应该使用行锁,因为事务和行锁往往是选择InnoDb的理由,但个别情况下也会使用表锁:

  1. 事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅仅这个事务的执行效率低,而且会造成其他事物长时间等待和锁冲突。
  2. 事务涉及多个表,比较复杂,很容易引起死锁,造成大量事务回滚。
LOCK TABLE user READ;读锁锁表
LOCK TABLE user WRITE; 写锁锁表
事务执行...
COMMIT/ROLLBACK; 事务提交或者回滚
UNLOCK TABLES; 本身自带提交事务,释放线程占用的所有表锁

在了解了串行化是如何解决幻读之后,那么已提交读和可重复读的隔离级别下的并发控制是如何实现的呢?
——底层实现原理:MVCC(多版本并发控制):并发读取方式:快照读

已提交读和可重复读的底层原理

InnoDb提供了两种读取操作,锁定读(s和x)和非锁定读(MVCC提供的快照读),依赖底层的undolog(回滚日志)

追根溯源,我们一步一步来看:

事务日志:uodo log和redo log

  1. ACD(事务日志)
  2. I(锁【串行化】+ MVCC【已提交读和可重复读】)

undo log的主要作用是:

  1. 事务发生错误时,回滚日志
  2. 提供了MVCC的非锁定读(快照读)的关键技术

undo log的原理

Mysql(15)——锁机制 + MVCC(全)_第8张图片

MVCC的多版本并发控制

  • MVCC是多版本并发控制,(Multi-Version Concurrency Control)是Mysql基于乐观锁理论实现隔离级别的方式,用于实现已提交读和可重复读隔离级别的实现,也经常称为多版本数据库。
  • MVCC机制会生成一个数据请求时间点的一致性数据快照,并用这个快照提供一定级别(语句级或事务级)的一致性读取,好像数据库可以提供同一数据的多个版本(系统版本号和事务版本号)

MVCC的多版本并发控制中,读操作分为以下的两类:

  1. 快照读:读的是记录的可见版本,不用加锁,如select;
  2. 当前读:读取的是记录的最新版本,如insert,delete,update,select …lock in share mode/for update、

快照内容读取原则:(仔细理解,跟下面的内容息息相关)
1、版本未提交无法读取生成快照
2、版本已提交,但是在快照创建后提交的,无法读取
3、版本已提交,但是在快照创建前提交的,可以读取
4、当前事务内自己的更新,可以读到

1、已提交读解决脏读的原理以及为什么没有解决不可重复读和幻读的原因:

  • 每次select的时候会重新生成一次快照(前提是数据已经被另一个事务正确commit过了,此时脏读时数据是处于prepare状态,此时事务生成的快照还是旧数据生成的快照,并没有用未提交的数据照快照),另一个事务一旦提交,此时再次select快照,照到的就是已提交的新数据。
  • 由于每一次select都会重新产生一次数据快照,其他事务更新后而且已提交的数据实时反馈到当前事务的select结果中,因此已提交读并没有解决不可重复读的问题。
  • 由于每一次select都会重新产生一次数据快照,其他事务增加了和当前事务查询条件相同的新数据时并且已经成功commit,导致当前事务再次以同样的条件查询时,数据变多了。
    Mysql(15)——锁机制 + MVCC(全)_第9张图片

2、可重复读解决脏读和不可重复读的原理以及为什么部分解决幻读的原因

  • 解决脏读的原理和之前相同
  • 解决了不可重复读的原因:因为第一次select产生数据快照,而且只产生一次!其他事务虽然修改了最新的数据,但是当前事务select时依然查看的是最初的快照数据。

Mysql(15)——锁机制 + MVCC(全)_第10张图片

  • 部分解决了幻读问题的原因:

①因为第一次select产生数据快照,而且只产生一次!其他事务虽然新增了数据,但是当前事务select时依然查看的是最初的快照数据。
Mysql(15)——锁机制 + MVCC(全)_第11张图片

②当前事务是可以看见自己事务修改、更新的数据的(此时事务ID号会更新,旧数据会到lundo log中),此时不是快照读,而是当前读,再去select,此时就没有解决幻读问题:
Mysql(15)——锁机制 + MVCC(全)_第12张图片

意向共享锁和意向排它锁

注意:这两个概念都是和表锁相关的,存在的目的就是更快速地获取表锁。InnoDb行锁的存在可能会导致获取表锁时效率较低,为了避免大面积的行锁扫描,我们直接通过观察表级的意向锁即可,就可以判断能否获取当前表的X或S锁。

在这里插入图片描述

  • 意向共享锁(IS锁):事务计划给记录加行共享锁,事务在给一行记录加共享锁前,必须先取得该表的IS 锁。
  • 意向排他锁(IX锁):事务计划给记录加行排他锁,事务在给一行记录加排他锁前,必须先取得该表的IX 锁。

Mysql(15)——锁机制 + MVCC(全)_第13张图片

1、意向锁是由InnoDB存储引擎获取行锁之前自己获取的
2、意向锁之间都是兼容的,不会产生冲突
3、意向锁存在的意义是为了更高效的获取表锁(表格中的X和S指的是表锁,不是行锁!!!
4、意向锁是表级锁,协调表锁和行锁的共存关系。主要目的是显示事务正在锁定某行或者试图锁定某行。

死锁

MyISAM 表锁是 deadlock free 的, 这是因为 MyISAM 总是一次获得所需的全部锁,要么全部满足,要么等待,因此不会出现死锁。但在 InnoDB 中,除单个 SQL 组成的事务外,锁是逐步获得的,即锁的粒度比较小,这就决定了在 InnoDB 中发生死锁是可能的。

Mysql(15)——锁机制 + MVCC(全)_第14张图片
Mysql(15)——锁机制 + MVCC(全)_第15张图片
死锁问题一般都是我们自己的应用造成的,和多线程编程的死锁情况相似,大部分都是由于我们多个线程在获取多个锁资源的时候,获取的顺序不同而导致的死锁问题。因此我们应用在对数据库的多个表做更新的时候,不同的代码段,应对这些表按相同的顺序进行更新操作,以防止锁冲突导致死锁问题。

锁的优化建议

1.尽量使用较低的隔离级别
2.设计合理的索引并尽量使用索引访问数据,使加锁更加准确,减少锁冲突的机会提高并发能力
3.选择合理的事务大小,小事务发生锁冲突的概率小
4.不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行。这样可以大大减少死锁的机会
5.尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响(等值查询与范围查询)
6.不要申请超过实际需要的锁级别
7.除非必须,查询时不要显示加锁

你可能感兴趣的:(Mysql,mysql)