一、概述

数据库需要尽可能的提高并发访问效率,还要能确保每个用户能以一致的方式读取和修改数据,根据此问题诞生了锁机制。

锁是数据库系统区别于文件系统的一个非常重要的特性,它用于管理对共享资源的并发访问,保证各个用户访问数据一致和完整。

Innodb 提供一致性的非锁定读,支持行级锁。

二、lock 与 latch

  • latch 是轻量级的锁,它要求锁定时间必须非常短,latch锁若持续时间长将严重影响性能,Innodb中它一般用来保证并发线程操作临界资源的正确性;latch 锁定的对象是线程,它保护的是内存中的临界资源。

  • lock 的对象是事务,它一般用来锁定数据库中的对象,比如之前文章中提到的 表、页、行等,lock有死锁机制;它保护的是数据库中的内容,它存在于整个事务过程中仅在发生事务回滚或者事务提交的时候进行释放,它也是本文的主角。

三、Innodb存储引擎中的锁

Innodb支持如下两种标准的行级锁

  • 共享锁(S Lock),允许事务读行数据,多个事务可以共享该锁,称为锁兼容。

  • 排他锁(X Lock),允许事务更新或删除行数据,只允许一个事务获得排他锁,必须等持有该锁的事务释放后其它的事务才能获取该锁。

仅S 锁跟 S锁 兼容,X 锁跟任意锁不兼容。

Innodb支持多粒度的锁,这种锁定允许事务在行级上的锁和表级上的锁同时存在。

为了支持不同粒度的锁,Innodb支持一种额外的锁定方式,称之为意向锁,它将数据对象分为多个层次,意向锁的意思是希望再更细的粒度上加锁。

下图为数据库的对象层次:



如上的树形结构中,如果要对树底层的行记录加锁,那么必须依次对它所在的数据库、表、页上加一个意向锁。意向锁是表级别的锁,它预示着即将被请求的锁的类型。因为Innodb支持的是行级别的锁,所以意向锁不会阻塞除全表扫描<表级别非意向锁>以外的任何请求。

下图为事务之间表级意向锁和行级锁的兼容情况:

  • 意向共享锁 (IS Lock),事务想要获取表中的某几行数据的共享锁。

  • 意向排它锁 (IX Lock),事务想要获得表中某几行的排它锁。

事务T1 对表A的编号为k的行,加一个X锁,那么需要先对表和所在页加意向锁,从兼容情况可以得知,任意的意向锁之间相互兼容,最后判断编号为k的行记录上是否存在任意锁,如果不存在,T1对其上X锁成功,否则等待。

可以通过information_schema架构下的三张表简单的分析当前事务可能存在的锁问题

  • INNODB_TRX 当前存在的所有事务,trx_requested_lock_id 表示等待事务的锁Id,若没有则为 null

  • INNODB_LOCKS 记录所有的锁,以及与之相关的事务id、锁类型、需要上锁的资源等,通过它可以查看每张表上的上锁情况。

  • INNODB_LOCL_WAITS 它可以很直观的显示当前等待中的事务,表中记录了:requested_trx_id申请锁资源的事务ID、requested_lock_id申请的锁ID(别的事务占有,所以导致等待)、blocking_trx_id阻塞的事务(当前占有资源的事务)、blocking_lock_id阻塞的锁ID(占有的锁ID,从如下截图可以看出其与requested_lock_id一致);

下图为我模拟的事务等待场景,17270事务占有了锁,事务17272必须等待。

一致性非锁定读

一致性非锁定读是Innodb通过对行记录的多版本控制方式来实现的,它依赖于undo<它用来支持事务的回滚>段来实现。所谓一致性非锁定读,即使需要读取的行上存在X锁,读取的请求也不会进行等待,而是去读取,通过undo段实现的,行记录的一个快照。它不会等待,所以称为 “非锁定”,因为undo段上的快照数据是不会被事务修改的,所以不需要上锁。

另外由于undo段上的行记录的快照可能不止一个,所以这种多版本控制称为行多版本技术,它带来的并发控制称为:多版本并发控制(MVCC);

Innodb在一些事务级别下支持一致性非锁定读,此外即使是在这些事务隔离级别下,对于一致性非锁定读也有着差异;Innodb中,READ COMMITED 和 REPEATABLE READ 事务隔离级别下支持一致性非锁定读。

  • READ COMMITED 事务隔离级别下,行多版本控制默认读取最新一行。

  • REPEATABLE READ 事务隔离级别下,行多版本控制默认读取该事务开始时的行版本数据<该版本之前或者之后都可能存在行记录别的版本>;也就是说当前事务未提交前,不论该行如何改变,当前事务读取到的都是当前事务开始那一刻的行数据。

一致性锁定读

Innodb只是显式的对数据库操作进行加锁以确保数据一致,它支持如下两种形式的一致性锁定读。

  • select c1,c2 from table_1 where xx = xx for update

  • select c1,c2 from table_1 where xx = xx lock in share mode

for update - 它对读取的行加一个X锁。

lock in share mode - 他对读取的行加一个S锁。

通过锁兼容性,我们很清楚能对它们做什么,不能做什么。

自增长与锁

自增长相信大家都很熟,它在主键上的应用非常广泛;在较低版本的MySQL中,自增长通过表锁实现,这样做的缺点是并发性能较差,因为它是表锁的设计,所以当多个事务插入时,别的事务只能等待执行的事务提交后才能继续。

先来了解一下SQL插入语句类型:

  • insert-like 它泛指所有类型的插入语句。

  • simple inserts 它指再插入前就能明确知道插入的数据行数(或者说能明确知道需要执行多少次自增)。

  • bulk inserts 这类sql插入前无法得知需要插入多少条数据,例如:

      	# table_a 和 table_b 拥有相同的表结构,sql语句执行前我们不知道有多少条数据会被插入table_a
    insert into table_a select * from table_b where name like 'prefix%';
  • mixed-mode inserts 这类SQL中既有自增长的,又有确定的,例如:

      	insert into table_t (c1,c2) values (1,false), (2,true),(null, false),(null, true);

从MySQL5.1.22版本开始,Innodb存储引擎引入了一种轻量级的互斥量(同一时刻只能有一个线程能访问它,相较于表锁它需要付出的代价小了太多)来实现自增长;此版本开始Innodb提供了一个参数:innodb_autoinc_lock_mode 来控制自增长的模式。

  • 当 innodb_autoinc_lock_mode = 0 表示使用低版本的MySQL中的表锁方式实现自增长。

  • 当 innodb_autoinc_lock_mode = 1,知道行数的 simple inserts 插入会使用互斥量实现自增长,不知道行数的插入使用表锁方式。需要注意的是,当有事务使用表锁方式自增长时,使用互斥量自增长的事务必须等待。

  • 当 innodb_autoinc_lock_mode = 2,对所有插入都使用互斥量,毫无疑问这是性能最优的选择,但是需要注意如下的问题:

      1.但是正因为是并发的,所以插入的值可能不是连续自增长的(可能部分事务发生回滚,但是已经无法将子增量退回,
    因为使用互斥量,事务间是没有发生阻塞的); 2.已知binlog在 statement 模式下,记录的是执行的SQL;row模式记录的是每一行的变化;当所有插入都使用互斥量时,
    statement模式的binlog无法保证在从库上执行后也能得到同样的主键,所以当所有sql都通过互斥量自增长时,
    主从复制必须使用row模式记录每一行的变化,而不是记录执行的SQL。

外键与锁

与Oracle不同的是,Innodb建立外键时,如果用户没有主动建立索引,它会自动在该列上建一个索引。

当需要对表上的外键值进行更新或者插入时,会通过一致性锁定读的方式去查父表该外键是否存在于父表上:

  • select * from parent_table where foreign_column_name = xx lock in share mode

如果存在一个行记录,会对该行记录加一个S锁,在对子表操作事务提交前,父表上该行不能被任何事务上X锁。

从而保证了外键约束的功能。

四、锁的算法

锁的算法

  1. Record Lock 单个行记录上的锁,它总是用于锁定索引上的一个记录。

  2. Gap Lock 间隙锁,锁定一个范围,但是不包含记录本身,它的作用是阻止多个事务将记录插入到同一个范围内导致幻读问题。

  3. Next-Key Lock 它锁定的不是单个值而是一个范围,且包含它自身,** Innodb 通过它解决大名鼎鼎的 “幻读问题” **。它除了锁定包含该值的范围外,还会锁定该值的下一个值所在的区间。

重点讲一下Next-Key Lock,假如一个索引上有如下值:-100,-30,22,56,那么可以被锁定的范围是:

  • ( -∞,-100 ]
  • ( -100,-30 ]
  • ( -30,22 ]
  • ( 22,56 ]
  • ( 56,+∞ )

    假如需要对值56 上一个锁,那么区间:( 22,56 ]、( 56,+∞ ) 会被锁住。

此外,当上述索引为唯一索引时,锁会降级为行锁,此时还是对行记录 56 上锁,此时被上锁的就是56这个行本身。【例外情况是,当唯一索引由多个列组成,如果事务中只操作其中的部分列时,那么此时的查询类型是range而不是point查询,所以依然需要使用 Next-Key Lock】。

当存在多个索引时,会对聚集索引上的Next-Key Lock 降级为 Record Lock 而对于非唯一的辅助索引则会严格按照Next-Key Lock 上锁,

Phantom Problem 幻读问题

  • Phantom Problem 幻读问题指的是在同一个事务下,同一个SQL两次查询可能得到不同的查询结果。

下面来进行一个模拟,表r中有三行数据,其中 列a 上存在一个聚集索引。

测试1,使用read committed 事务隔离级别:

表中原始数据如下



事务A执行如下语句不提交事务

set transaction isolation level read committed;
start transaction;
select * from r where a >= 10 for update;

此时开启事务B,执行如下语句,插入成功

回到事务A,再次执行相同的查询语句,发现查询结果多了一列:12,很明显它违反了事务的隔离性,因为当前事务能看到别的事务的执行结果,这就是Phantom Problem 问题。

测试2,使用 repeatable read 事务隔离级别

表中数据同样为:10、13、15

事务A执行如下语句不提交事务,由于隔离级别不同,此时关于 行10 的锁不再降级为行锁

set transaction isolation level repeatable read;
start transaction;
select * from r where a >= 10 for update;

事务B 执行如下插入,此时发现,插入任意 大于10 的值都被阻塞。

这时任意可能导致事务A中SQL select * from r where a >= 10 for update; 查询结果变更的SQL都无法成功插入。

  1. 此外测试过程中还发现了一些特殊现象,当被锁定的值是边界的时候可能不一定完全遵循上述规则。

repeatable read 事务隔离级别下,并不一定完全是Next-Key Lock,它锁定的范围是根据SQL条件来决定的。例如:表中a有如下值:10,20,30,40,50

  • a >= 20 锁定 20到正无穷。

  • a = 20 时实际上只对行 20 加了一个行锁。

五、锁的问题

脏读

所谓脏读,是指事务读到了别的事务未提交的数据,被当前事务读到,在事务隔离级别为:read uncommitted 下会出现,它违反了事务隔离性。

在mysql 主从模式下,slave节点可以设置为:read uncommitted 因为slave只负责查询,并不会有需要修改数据的事务由slave去完成。

不可重复读

指同一个事务中,对相同资源的多次读取可能得到不同的结果,这是因为需要读取的数据被别的事务修改了,同样这也违反了事务的隔离性。在 read commited 事务隔离级别下会出现该问题。

它读到的都是已经提交的数据<非锁定读,否则别的事务不可能更改目标资源>,但是当前事务未提交前,目标资源还可能被别的事务读取并修改。

丢失更新

该问题与事务隔离级别无关,它是逻辑上的丢失,实际应用中非常多这样的使用场景,我们需要对某个行进行更新,但是我们不是直接update 去更新,而是先查询,然后在现有指的基础上修改。

  • select 得到余额,展示到页面上

  • 用户操余额进行update 操作更新数据

在并发环境下,如果多个用户执行上述操作就会出现问题。

针对此种情况,需要在读取的时候就对数据加一个 X 锁 (select .... for update ),这样就可以保证该数据不会别别的事务读取并修改了。

六、阻塞

阻塞发生的原因是由锁兼容问题导致的,例如事务A在行 K 上加了一个X 锁,其它任意事务访问行 K 的行为都会被阻塞,如此可以保证事务可以并发且正确的运行。

需要注意的是,一旦事务中发生了异常,必须决定回滚还是提交。

七、死锁

死锁的概念很简单:两个事务互相等待对方互斥占用的资源,或者多个事务间互斥资源的请求形成了回路。

事务死锁判断机制名为:wait-for graph,它是一种主动死锁检测机制,如果它发现某个sql 可能导致死锁会抛出异常。它的基本概念可以参考 JVM 的 GC Root机制判断循环依赖,当然它们具体实现肯定各不相同。

八、锁升级

锁升级指,锁从细粒度升级为粗粒度,多个行锁可能升级为页锁,多个页锁也可能升级为表锁。

  • 单独的SQL中在一个对象上持有锁的数量超过了阈值,这时会自动进行锁升级,默认值为5000.
  • 锁资源占用的内存超过了激活内存的40%时会发生锁升级。

Innodb存储引擎之锁的更多相关文章

  1. InnoDB存储引擎的锁

    InnoDB存储引擎的锁 锁的类型 锁的类型包括: 1.     共享锁(S lock),允许事务读取一行数据 2.     排他锁(X lock),允许事务删除或更新一行数据 锁的兼容性a X S ...

  2. 图文实例解析,InnoDB 存储引擎中行锁的三种算法

    前文提到,对于 InnoDB 来说,随时都可以加锁(关于加锁的 SQL 语句这里就不说了,忘记的小伙伴可以翻一下上篇文章),但是并非随时都可以解锁.具体来说,InnoDB 采用的是两阶段锁定协议(tw ...

  3. InnoDB 存储引擎的锁机制

    测试环境隔离级别:REPEATABLE-READ 行级别的 - Share and Exclusive Locks 共享锁 S:允许持有S锁的事务对行进行读操作 排他锁 X: 允许持有X锁的事务对行进 ...

  4. 《Mysql技术内幕,Innodb存储引擎》——锁

    lock与latch 在数据库中lock与latch分别指不同的所. latch:可分为互斥量(mutex)和读写锁(rwlock),目的在于保证数据库内部的结构中共享资源并发时能够正确操作,其对象主 ...

  5. MySQL内核:InnoDB存储引擎 卷1

    MySQL内核:InnoDB存储引擎卷1(MySQL领域Oracle ACE专家力作,众多MySQL Oracle ACE力捧,深入MySQL数据库内核源码分析,InnoDB内核开发与优化必备宝典) ...

  6. MySQL数据库InnoDB存储引擎中的锁机制

    MySQL数据库InnoDB存储引擎中的锁机制    http://www.uml.org.cn/sjjm/201205302.asp   00 – 基本概念 当并发事务同时访问一个资源的时候,有可能 ...

  7. MySQL InnoDB存储引擎中的锁机制

    1.隔离级别 Read Uncommited(RU):这种隔离级别下,事务间完全不隔离,会产生脏读,可以读取未提交的记录,实际情况下不会使用. Read Committed (RC):仅能读取到已提交 ...

  8. (转)Mysql技术内幕InnoDB存储引擎-表&索引算法和锁

    表 原文:http://yingminxing.com/mysql%E6%8A%80%E6%9C%AF%E5%86%85%E5%B9%95innodb%E5%AD%98%E5%82%A8%E5%BC% ...

  9. mysql中InnoDB存储引擎的行锁和表锁

    Mysql的InnoDB存储引擎支持事务,默认是行锁.因为这个特性,所以数据库支持高并发,但是如果InnoDB更新数据的时候不是行锁,而是表锁的话,那么其并发性会大打折扣,而且也可能导致你的程序出错. ...

  10. MySQL技术内幕InnoDB存储引擎(表&索引算法和锁)

    表 4.1.innodb存储引擎表类型 innodb表类似oracle的IOT表(索引聚集表-indexorganized table),在innodb表中每张表都会有一个主键,如果在创建表时没有显示 ...

随机推荐

  1. [转帖]shell编程之循环语句

    目录 一.循环语句 for循环 for语句的结构 嵌套循环 while语句的结构 while语句应用示例 until语句的结构 until语句示例 二.跳出循环 continue跳出循环 break跳 ...

  2. [转帖]JVM性能提升50%,聊一聊背后的秘密武器Alibaba Dragonwell

    https://zhuanlan.zhihu.com/p/453437019 今年四月五日,阿里云开放了新一代ECS实例的邀测[1],Alibaba Dragonwell也在新ECS上进行了极致的优化 ...

  3. [转帖]实战瓶颈定位-我的MySQL为什么压不上去–写场景

    https://plantegg.github.io/2023/06/30/%E5%AE%9E%E6%88%98%E7%93%B6%E9%A2%88%E5%AE%9A%E4%BD%8D-%E6%88% ...

  4. stress-NG 磁盘测试结果-全国产信创部分验证

    stress-NG 磁盘测试结果 摘要 前几天分别还是用了redis-benchmark还有specjvm2008进行了多种系统的压测 得出了信创CPU的一些简单结论 但是一直还没有压测磁盘, 今天想 ...

  5. [转帖]磁盘负载指标 %iowait, await, %util 的正确理解

    说明 %iowait, await, %util 是用来衡量硬盘负载的三个指标, 但是这几个指标通常容易被误解, 实际上, 这三个指标单纯的高, 并不一定能说明相应的磁盘有问题或者有瓶颈, 而是需要结 ...

  6. IIS 实现autoindex的简单方法 能够下载文件等.

    之前使用nginx 的autoindex on 的参数 能够实现了 nginx的 目录浏览查看文件 但是那是linux上面的 windows 上面很多 使用的 其实是 iis的居多 然后看了下 其实也 ...

  7. Opentelemetry Metrics SDK

    Metrics SDK 目录 Metrics SDK 目标 期望 SDK 术语 数据流图表 要求 SDK MeterProvider Shutdown SDK:Instrument注册 SDK: Re ...

  8. postman中monitor的使用

    monitor就是一个摸鱼的功能,我们把写好的接口部署到postman的web服务器中, 绑定自己的邮箱,运行结果会发送到自己的邮箱中,不用实时监控,是个非常方便 的功能(不安全) 1.crete a ...

  9. 【APP 逆向百例】Frida 初体验,root 检测与加密字符串定位

    声明 本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容.敏感网址.数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关! 本文章未经许 ...

  10. ABP-VNext 用户权限管理系统实战02---用户权限表的创建与迁移

    一.表实体建立 1.菜单表 [Comment("菜单表")] [Table("t_identity_menu")] public class Menu : Au ...