【MySQL锁详解——从结构分类到适用场景

2023年 10月 22日 89.7k 0

我们要学习锁首先要了解下我们想了解的锁到底是什么🤔

而在MySQL中给某个数据加锁的本质其实就是在内存中创建一个锁结构与之关联,而这个锁结构就是我们常提到的MySQL的锁🔒

那么接下来的问题就是,这个锁结构长啥样呢?

锁的内部结构(InnoDB)

一张图详解锁结构( ̄∇ ̄)/


 为了节约资源,并非每个锁都有一个单独的锁结构与之对应,符合如下条件的记录就会放在同一个锁结构中

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

锁的分类

  • 按操作方式

    • 读锁/共享锁/S(Share Lock)
    • 写锁/排他锁/X(Exclusive Lock)
  • 按锁粒度(Lock Granularity)

    • 全局锁

    • 表级锁(Table Lock)

      • 表级别的共享锁和排他锁
      • 意向锁(Intention Lock)
      • 自增锁(AUTO-INC)
      • 元数据锁(MDL)
    • 页级锁

    • 行级锁

      • 记录锁(Record Locks)
      • 间隙锁(Gap Locks)
      • 临键锁(Next-Key Locks)
      • 插入意向锁(Insert Intention Locks)
  • 按思维方式/设计思想

    • 乐观锁(Optimistic Concurrency Control)
    • 悲观锁(Pessimistic Concurrency Control)
  • 按加锁方式

    • 隐式锁
    • 显式锁

共享锁 vs 排他锁

并发事务中"读-读"的情况一般不会引起什么问题,一般需要解决的就是对于"写-写"和"读-写"/"写-读"引起一些问题,主要有两种解决方式:加锁,或者需MVCC

既要允许"读-读"情况不受影响,又要使"写-写"、"读-写"或"写-读"情况中的操作相互阻塞,所以MySQL实现一个由两种类型的锁组成的锁系统来解决

这两种类型的锁通常被称为共享锁(Shared Lock,S Lock)和排他锁(Exclusive Lock,X Lock),也叫读锁(readlock)和写锁(write lock )

  • 共享锁/S(读锁)

    • 针对同一份数据,多个事务的读操作可以同时进行而不会互相影响,相互不阻阻塞
  • 排他锁/X(写锁)

    • 当前写操作没有完成前,它会阻断其他写锁和读锁
    • 这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源

一句话概括就是,不同事务中,只有都是共享锁才互不影响不会阻塞,但凡有一个事务有排他锁,都会阻塞,对于InnoDB引擎来说,共享锁和排他锁可以加在表上,也可以加在行上

如何加锁

  • 加共享锁

    • SQL语句 + "LOCK IN SHARE MODE" / "FOR SHARE";
  • 加排他锁

    • SQL语句 + "FOR UPDATE";

需要注意的是:

  • 读操作(SELECT)可以加共享锁,也可以加排他锁

  • 写操作(DELETE、UPDATE、INSERT)只能加排他锁

    • INSERT

      • MySQL通过"隐式锁"来维护
    • DELETE

      • B+树上定位 -> 获取X锁 -> 执行delete mark操作
    • UPDATE

      • 修改键值:DELETE操作 + INSERT操作

      • 不修改键值:

      • 不修改存储空间:B+树上定位 -> 获取X锁 -> 修改
      • 修改存储空间:B+树上定位 -> 获取X锁 -> 删除原纪录(移入垃圾链表) -> 插入新纪录(INSERT操作)

补充:MySQL 8.0 新特性

  • nowait

    • 查询的数据已经加锁,立刻返回报错
  • skip locked

    • 立刻返回未被锁定的行

全局锁 vs 表级锁 vs 页级锁 vs 行级锁

数据库的并发度和锁的粒度息息相关,锁的粒度越小,并发度越高,但也越耗资源(锁的获取、检查、释放等操作),影响系统性能,因而我们需要根据实际的业务场景,在高并发响应与系统性能之间进行平衡

全局锁

对整个数据库实例进行加锁,加锁后整个数据库处于只读状态,命令如下:

Flush tables with read lock

表锁

  • 开销小,但并发性较差
  • 可以避免死锁

不与行级锁冲突的表级锁:意向锁

主要用于在已经加了行级锁后,会在上一层记录中自动加一个意向锁,用于判断当前数据页或者数据表是否被加过锁,意向共享锁和意向排他锁之间是任意组合都是相互兼容的,但是与普通的共享锁/排他锁之间,只有意向共享锁与普通共享锁兼容,其余都互斥

具体使用意向共享锁,还是意向排他锁,要根据行级锁(与行级锁一致)

  • 意向锁之间不互斥
  • 意向锁与行级锁不互斥,与表级锁互斥
  • 意向锁在保证高并发的前提下,实现了行锁与表锁共存,并且满足事务隔离性的要求

自增锁

建表字段中如果包含 "AUTO_INCREMENT" 关键字,每当插入操作时,就要对自增锁进行竞争,以保证主键唯一且单调递增

首先,插入数据的模式有3种:

  • 简单插入(Simple inserts)

    事先明确插入的数量

  • 批量插入(Bulk inserts)

    基于现有的表进行插入(事先并明确插入的数量)

  • 混合模式插入(Mixed-mode inserts)

    插入的数据有的有主键,有的没主键

  • 主要有3种模式

  • "传统"锁定模式(并发性较差)

    • innodb_autoinc_lock_mode=0
  • "连续"锁定模式(8.0前默认)

    • innodb_autoinc_lock_mode=1
  • "交错"锁定模式(8.0后默认)

    • innodb_autoinc_lock_mode=2
  • 元数据锁(MDL)

    可以通过下面👇代码查看当前执行的process信息

    show processlist;
    

    如果想查看在等待哪把锁,可以重点关注下State字段

    页锁

    • 开销、粒度、并发度 介于表锁和行锁之间

    • 也会出现死锁

    • 每个层级的锁数量是有限制的(锁会占用内存空间),锁空间的大小是有限制的,某个层达到阈值后就会进行锁升级(用更大力度的锁代替多个较小粒度的锁)

      • 比如一张表中出现大量DELETE操作时,会锁表

    行锁(InnoDB)

    行锁又称记录锁,锁的是某一行数据

    MySQL的服务层并没有实现行锁机制,行锁是在存储引擎层实现的

    • 优点

      • 锁粒度小,发生锁冲突概率低,可实现的并发度高
    • 缺点

      • 锁的开销较大,加锁较慢,会出现死锁

    记录锁

    可参照读锁和写锁理解,只不过粒度是行,只有操作相同的行才有影响

    间隙锁(解决幻读问题)

    在MySQL的可重复读的隔离级别下,可以解决幻读问题

    因为在一开始进行读取操作的时候,我们并不知道会产生幻影数据的主键是多少(还不存在),因而无法使用记录锁事先锁定那一行,间隙锁会阻塞当前记录和其前一条记录之间的插入操作。

    对于间隙锁很多文章都有小部分出入,我们就以官网为准

    MySQL :: MySQL 8.0 Reference Manual :: 15.7.1 InnoDB Locking

    这段话中提到的比较重要的部分就是说间隙锁是在索引记录之间的间隙上的锁,或者是在第一条索引记录之前或最后一条索引记录之后的间隙上的锁,对于使用唯一索引锁定行以搜索唯一行的语句,不需要间隙锁定(这不包括搜索条件只包括多列唯一索引的某些列的情况;在这种情况下,确实会发生间隙锁定)

    例如,如果该列具有唯一索引,则以下语句仅使用id一个索引记录锁定值为 100 的行id,其他会话是否在前面的间隙中插入行并不重要:

    SELECT * FROM child WHERE id = 100;
    

    如果 id 未索引或具有非唯一索引,则该语句会锁定前面的间隙

    临键锁

    一句话理解~就是记录锁+间隙锁(不愧是我(*≧ω≦)/),如果还是不太明白这些🔒的锁定范围,后面会出一篇详解记录锁、间隙锁、临键锁。

    插入意向锁

    获取锁失败的事务,开始等待的事务的其锁的类型就是插入意向锁,插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁

    乐观锁 vs 悲观锁

    悲观锁

    悲观锁是通过数据库自身的锁机制来实现,从而保证致据操作的排它性。

    悲观锁总是假设最坏的情况,认为每次去查数据的时候都会被别人修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)比收如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时,都需要阻塞挂起

    如果在SELECT语句上加悲观锁,在这条语句执行过程中所有扫描的行都会被锁上,因此在MySQL中使用悲观锁需要确定必须使用了索引,不然会全表扫描,把整张表都锁上

    乐观锁

    乐观锁以为对同一数据的并发操作不会总发生,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有设有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现的。在程序上,我们可以采用版本号机制或者CAS机制实现,乐观锁适用于多读的应用类型,这样可以提高吞吐量

    ps:在Java中java.util.concurrent.atomic包下的原子变量类就是使用了乐观锁的CAS实现的

    1. 版本号机制

    在表中设计一个饭木字段version,第一次读的时候,会获取version字段的取值。然后对数据进行更新或删除操作时,会执行UPDATE..·SET version=version+1 NHERE version=version。此时如果已经有事务对这条数据进行了更改,修改就不会成功。

    这种方式类以我们展悉的SN、CVS版本管理系统,当我们修改了代玛进行提交时,首先会检查当前版木号与服务器上的版本号昏一致,如果一致就可以直接提交,如果不一致就需要更新服务器上的最新代码,然后再进行提交。

    2. 时间戳机制

    时间戰和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行较,如果两者一致则更新功,否则是版本冲突,基本是通过给数据行增加一个戳(版本号或者时间戳),从而证明当前拿到的数据是否最新

    总结

    • 乐观锁适用于读多的场景;悲观锁适用于写多的场景
    • 乐观锁基于程序实现,不会产生死锁;悲观锁基于MySQL的锁机制实现

    显式锁 vs 隐式锁

    隐式锁

    一个事务在执行INSERT操作时,如果即将插入的侧除已经被其他争务加了间隙锁,那么本次INSERT操作会阻塞,并且当前事务会在该间隙上加一个插入意向锁,否则一股情况下INSERT操作是不会显式加锁的,执行逻辑大致如下:

  • InnoDB的每条记录中都有一个隐藏的trx_id字段,存于聚簇索引的B+树中
  • 在操作一条记录前,首先根据记录中的trx_id检查该事务是否是活动的事务(未提交、回滚),如果是活动的事务,就会现将隐式锁转换为显式锁
  • 检查是否有锁冲突,如果有,创建锁,并设置为waiting的状态,没有就执行步骤5
  • 等待加锁成功,被唤醒,或者超时
  • 写数据,并将自己的trx_id写入trx_id字段
  • 显式锁

    通过特定语句进行添加的锁,一般称为显式锁("FOR UPDATE"等)

    搞定撒个花(。・ω・。)ノ🎉

    相关文章

    Oracle如何使用授予和撤销权限的语法和示例
    Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
    下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
    社区版oceanbase安装
    Oracle 导出CSV工具-sqluldr2
    ETL数据集成丨快速将MySQL数据迁移至Doris数据库

    发布评论