Mysql深入学习 --- 锁机制


往期:

十四、锁

14.1 并发事务一致性问题

并发事务带来的一致性问题:

  • 读–读情况: 并发事务相继读取相同的记录。读取操作本身不会对记录有任何影响,不会引起什么问题,所以允许这种情况的发生
  • 写-写情况: 并发事务相继对相同的记录进行改动。
  • 读-写或写-读情况: 也就是一个事务进行读取操作,另一个事务进行改动操作。

1.1 写-写情况

写-写情况可能会造成脏写问题,这是任何一种隔离级别都不允许发生的。所以在多个未提交事务相继对一条记录进行改动时,需要让他们排队执行 ,这个排队过程其实就是通过加锁来实现的

1.2 读-写或写-读情况

读写或写读情况在不同隔离级别下可能出现脏读、不可重复读、幻读现象

MySQL在可重复读的隔离级别下很大程度避免了幻读

对于脏读、不可重复读、幻读现象,有两种解决方案:

  • 方案一: 读操作使用MVCC,写操作加锁
    • 查询语句只能读到在生成ReadView之前已提交事务的版本,类似于快照
    • 读记录的历史版本和改动记录的最新版本并不冲突,即采用MVCC,读写操作不冲突
  • 方案二: 读写操作都加锁
    • 有一些业务场景不允许读旧版本,而是每次都必须读取记录的最新版本
    • 这就意味着,读操作和写操作一样都得加锁

1.3 一致性读

事务利用MVCC进行的读取操作称为一致性读(Consisten Read),或称一致性无锁读,一致性读并不会对表中任何记录进行加锁操作,其他事务可以自由对表中记录进行改动。所有普通的SELECT语句在READ COMMITTEDREPEATABLE READ隔离级别下都是一致性读

1.4 锁定读

对于读-写情况可以使用MVCC也可以采用加锁读的方式,接下来介绍MySQL的锁

  • 共享锁(Shared Lock/读锁): 简称S锁,事务读取一条记录时,需要先获取该记录的S锁
  • 独占锁(Exclusive Lock/排他锁/写锁): 简称X锁,事务要改动一条记录时,需要获取该记录的X锁

S锁和X锁的兼容关系:

image-20220217225613390

MySQL中锁定读的语句:

  • 对读取的记录加S锁,共享锁

    SELECT ... LOCK IN SHARE MODE;
    
  • 对读取的记录加X锁,排他锁

    SELECT ... FOR UPDATE;
    

1.5 锁定写

  • Delete操作:
    • 删除一条记录的过程是先在B+树上找到这条记录的位置,然后获取这条记录的X锁,然后执行delete mark操作
    • 先获取X锁的锁定读 ,然后执行delete mark操作
  • Update操作: 分为三种情况
    1. 如果没修改主键,同时更新的列所占空间大小没变化
      • 先在B+树上找到这条记录,然后再获取记录的X锁,然后执行修改操作
      • 先获取X锁的锁定读 ,然后再进行修改
    2. 如果没修改主键,但是更新的列所占空间大小发生了变化
      • 先在B+树上找到这条记录,然后获取记录的X锁,之后把它彻底删除掉,然后再插入一条更新后的记录
      • 先获取X锁的锁定读 ,同时被彻底删除的记录的锁会转移到新的记录上来
    3. 如果修改了主键
      • 相当于再原纪录上执行Delete操作再来了一次Insert操作
      • 加锁操作按照DELETE和INSERT规则进行
  • Insert操作:
    • 一般情况下,新插入的一条记录受隐式锁保护 ,不需要在内存中为其生成对应的锁结构

14.2 多粒度锁

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

为了支持不同粒度上进行加锁,InnoDB支持一种额外的锁方式:意向锁

若将上锁的对象看成一棵树,那么对最下层的对象上锁,也就是对最细粒度的对象进行上锁,那么首先需要对粗粒度的对象上锁

如图,如果需要对页上的记录r进行上X锁,那么分别需要对数据库A、表、页上意向锁IX,最后对记录r上X锁。

image-20220217231738767

InnoDB意向锁设计比较简练,其意向锁即为表级别的锁 。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型

  • 意向共享锁(IS Lock) ,事务想要获得一张表中某几行的共享锁,需要先加一个IS锁
  • 意向排他锁(IX Lock) ,事务想要获得一张表中某几行的排他锁,需要先加一个IX锁

image-20220217232034534

14.3 MySQL中的行锁和表锁

MySQL中支持多种引擎,不同引擎对锁的支持也是不一样的,接下来还是重点讨论InnoDB中的锁

3.1 其他引擎中的锁

对于MyISAM、MEMORY、MERGE这些存储引擎来说,它们只支持表级锁 ,而且这些存储引擎并不支持事务

所以当我们为使用这些存储引擎的表加锁时,一般都是针对当前会话 来说的

3.2 InnoDB中的锁

3.2.1 InnoDB中的表级锁
  • 表级别的S锁、X锁
    • 实际上在对某个表进行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB是不会为这个表加 S 锁或 X 锁的
    • 另外,在执行DDL语句,诸如ALTER TABLE、DROP TABLE等语句时,其他事务的 增删改查 操作会发生阻塞。反过来某个事务进行增删改查时,其他会话中DDL语句也会阻塞。这个过程是通过 server层的 Metadata Lock(MDL,元数据锁) 实现的,一般情况下不会用S锁和X锁
    • 所以,实际上表级的 S 锁和 X 锁相当鸡肋 ,只会在特殊情况下如系统崩溃恢复时用到。InnoDB厉害的地方在行级锁
  • 表级别的IS锁、IX锁
    • 前文提到了,不过多赘述了
  • 表级别的AUTO-INC锁
    • 系统会给AUTO_INCREMENT修饰的列进行递增赋值采用的实现方式有两个
      1. 采用AUTO-INC锁,就是执行一条插入语句时就加一个表级别的AUTO-INC锁,然后为带有AUTO_INCREMENT修饰的字段分配递增的值,在语句结束后释放锁
      2. 采用一个轻量级的锁,在插入语句生成自增列的值时获取到这个锁,然后当自增列用完这个值后就立即释放掉,不用等语句完全执行完再释放
3.2.2 InnoDB中的行级锁

行级锁也称记录锁,这是InnoDB锁中的重头戏

①:Record Lock记录锁

记录锁就是仅仅把一条记录锁上,官方类型为:LOCK_REC_NOT_GAP

如图,比如我们给id为8的元素加上Record Lock锁

image-20220217235010406 Record Lock分为S锁和X锁,和表级的S锁与X锁的兼容性规则一致

  • 当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可 以继续获取X型记录锁;
  • 当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不 可以继续获取X型记录锁。
②:Gap Lock间隙锁

间隙锁,顾名思义就是给记录的间隙加上锁,不如记录插入这个间隙,官方类型为LOCK_GAP

如图,我们给区间(3,8)加上间隙锁,即不允许记录插入(3,8)之间

image-20220217235228475

Gap锁的作用是防止插入幻影记录,所以虽然gap锁有共享锁和排他锁之分,但是它们的作用是相同的,且并不会限制其他事务对这条记录加Record锁或者Gap锁。

再强调一遍,Gap锁的作用是防止插入幻影记录

我们都知道,数据页中有两条伪记录,Infimum记录:表示最小记录Supremum记录:表示最大记录

为了防止其他事务插入id在(20,+∞)的新纪录,我们可以在id为20的记录和Supremum记录之间的间隙加上Gap锁

image-20220218000008804

③:Next-Key Lock临键锁

有时候,我们既想锁住某条记录,又想阻止其他事务在该记录前的间隙插入记录。InnoDB中就有这样的锁:Next-Key Lock,官方的类型为:LOCK_ORDINARY

image-20220218000212966

next-key锁的本质就是一个Record锁和一个gap锁的合体 。它既能保护该条记录,又能阻止别的事务将新记录插入到被保护记录前面的间隙中。

④:Insert Intention Lock插入意向锁

一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了 gap锁( next-key锁 也包含 gap锁 )

  • 如果有的话,插入操作需要等待,直到拥有 gap锁 的那个事务提交。
  • 但是InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个 间隙插入新记录,但是现在在等待。
  • InnoDB就把这种类型的锁命名为 Insert Intention Locks ,官方的类型名称为: LOCK_INSERT_INTENTION ,我们称为 插入意向锁
  • 插入意向锁是一种 Gap锁 ,不是意向锁,在insert 操作时产生。
  • 插入意向锁是在插入一条记录行前,由 INSERT 操作产生的一种间隙锁 。
  • 事实上插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁。
⑤:隐式锁

举个例子,一个事务首先插入了一条记录(此时没有与该记录相关联的锁结构),然后另一个事务执行如下操作:

  • 立即获取这条记录的S锁或者X锁
    • 如果允许这种情况发生,可能出现脏读
  • 立即修改这条记录
    • 如果运行这种情况发生,可能出现脏写

这时候事务id又要起作用了

  1. 情景1: 对于聚簇索引记录 来说,有一个trx_id隐藏列记录着最后改动该记录的事务的事务id。

    • 在当前事务中新插入一条聚簇索引记录后,该记录的trx_id隐藏列代表的就是当前事务的事务id
    • 如果其他事务此时想对该记录添加S锁或者X锁,首先会看一下该记录的 trx_id隐藏列代表的事务是否是当前的活跃事务
      • 如果不是的话就可以正常读取;
      • 如果是的话,那么就帮助当前事务创建一个X锁的锁结构 ,该锁结构的is_waiting属性为false;然后为自己也创建一个锁结构 ,该锁结构的is_waiting属性为true,之后自己进入等待状态。
  2. 情景2: 对于二级索引记录 来说,本身并没有trx_id隐藏列,但是在二级索引页面的Page Header部分有一个 PAGE_MAX_TRX_ID属性,该属性代表对该页面做改动的最大的事务id

    • 如果PAGE_MAX_TRX_ID属性值小于当前最小的活跃事务id,那就说明对该页面做修改的事务都已经提交了
    • 否则就需要在页面中定位到对应的二级索引记录,然后通过回表操作找到它对应的聚簇索引记录,然后再重复情景1的做法。

隐式锁起到了延迟生成锁结构的用处。如果别的事务在执行过程中不需要获取与该隐式锁相冲突的锁,就可以避免在内存中生成锁结构

3.3 InnoDB锁的内存结构

先来看一条语句:

SELECT * FROM hero LOCK IN SHARE MODE;

很显然,这条语句需要为hero表中的所有记录进行加锁。那么,是不是需要为每条记录都生成一个锁结构呢?

如果一个事务要获取10,000条记录的锁,要生成10,000个这样的结构就太亏了吧!所以设计InnoDB的大叔本着勤俭节约的美德,决定在对不同记录加锁时,如果符合下面这些条件,这些记录的锁就可以放到一个锁结构中:

  • 在同一个事务中进行加锁操作
  • 被加锁的记录在同一个页面中
  • 加锁的类型是一样的
  • 等待状态是一样的

如图为InnoDB中的锁结构

image-20220218003333847

  • 锁所在的事务信息: 无论是表级锁还是行级锁,一个锁属于一个事务

  • 索引信息: 对于行级锁,需要记录一下加锁的记录属于哪个索引

  • 表锁/行锁信息: 表级锁和行级锁结构在这个位置上是不同的

    • 表级锁记载这是对哪个表加的锁
    • 行级锁记载了下面3个重要信息
      • Space ID: 记录所在的表空间
      • Page Number: 记录所在页号
      • n_bits: 对于行级锁来说,一条记录对于一个比特;一个页面中包含很多条记录,用不同的比特来区分为哪一条记录加了锁。这个属性表示使用了多个比特
  • type_mode: 这是 32 比特的数,被分成 lock_modelock typerec_lock_type 3个部分,如图:

    image-20220218003933661

    • lock_mode:锁的模式,占用低4位
      • LOCK_IS(十进制的 0 ):表示共享意向锁,也就是 IS锁
      • LOCK_IX(十进制的 1 ):表示独占意向锁,也就是 IX锁
      • LOCK_S(十进制的 2 ):表示共享锁,也就是 S锁
      • LOCK_X(十进制的 3 ):表示独占锁,也就是 X锁
      • LOCK_AUTO_INC(十进制的 4 ):表示 AUTO-INC锁
    • lock_type:锁的类型,占用第5~8位,不过现阶段只有第5位和第6位被使用
      • LOCK_TABLE(十进制的 16 ),也就是当第5个比特位置为1时,表示表级锁。
      • LOCK_REC(十进制的 32 ),也就是当第6个比特位置为1时,表示行级锁。
    • rec_lock_type:行锁的具体类型,使用其余位标识。当lock_type为LOCK_REC时才会有
      • LOCK_ORDINARY(十进制的 0 ):表示 next-key锁 。
      • LOCK_GAP(十进制的 512 ):也就是当第10个比特位置为1时,表示 gap锁 。
      • LOCK_REC_NOT_GAP(十进制的 1024 ):也就是当第11个比特位置为1时,表示正经记录锁 。
      • LOCK_INSERT_INTENTION(十进制的 2048 ):也就是当第12个比特位置为1时,表示插入 意向锁。
    • is_waiting 属性呢?基于内存空间的节省,所以把 is_waiting 属性放到了 type_mode这个32 位的数字中:
      • LOCK_WAIT(十进制的 256 ) :当第9个比特位置为 1 时,表示 is_waiting 为 true ,也就是当前事务尚未获取到锁,处在等待状态;当这个比特位为 0 时,表示 is_waiting 为 false ,也就是当前事务获取锁成功。
  • 其他信息:为了更好的管理系统运行过程中生成的各种锁结构而设计了各种哈希表和链表。

  • 一堆比特位:如果是 行锁结构 的话,在该结构末尾还放置了一堆比特位,比特位的数量是由上边提到的 n_bits属性表示的。

    • InnoDB数据页中的每条记录在 记录头信息 中都包含一个 heap_no属性,伪记录 Infimum 的 heap_no 值为 0 , Supremum 的 heap_no 值为 1 ,之后每插入一条记录, heap_no 值就增1。 锁结构最后的一堆比特位就对应着一个页面中的记录,一个比特位映射一个 heap_no,即一个比特位映射到页内的一条记录

14.4 锁相关的其他问题

4.1 怎么监控锁

可以通过information_schema数据库下的INNODB_TRXINNODB_LOCKSINNODBLOCK_WAITS表来查看事务和锁的相关信息,也可以通过SHOW ENGINE INNODB STATUS语句查看事务和锁的相关信息。

4.2 什么是死锁

不同事务由于互相持有对方需要的锁而导致事务都无法继续执行的情况称为死锁。 死锁发生时,InnoDB会选择一个较小的事务进行回滚。可以通过查看死锁日志来分析死锁发生过程。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值