锁机制

事务与锁是不同的。事务具有ACID( 原子性、一致性、隔离性和持久性),锁是用于解决隔离性的一种机制。事务的隔离级别通过锁的机制来实现。

为了保证数据并发访问时的一致性和有效性,任何一个数据库都存在锁机制。锁机制的优劣直接影响到数据库的并发处理能力和系统性能,所以锁机制也就成为了各种数据库的核心技术之一。

锁机制是为了解决数据库的并发控制问题而产生的。如在同一时刻,客户端对同一个表做更新或查询操作,为了保证数据的一致性,必须对并发操作进行控制。同时,锁机制也为实现 MySQL 的各个隔离级别提供了保证。

可以将锁机制理解为使各种资源在被并发访问时变得有序所设计的一种规则

如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题锁冲突也是影响数据库并发访问性能的一个重要因素。从这个角度来说,锁对数据库显得尤其重要,也更加复杂。

image-20220303140824038

锁级别分类,可分为共享锁、排他锁和意向锁。

  • 共享锁
  • 共享锁的代号是 S,是 Share 的缩写,也可称为读锁。是一种可以查看但无法修改和删除的数据锁

    共享锁的锁粒度是行或者元组(多个行)一个事务获取了共享锁之后,可以对锁定范围内的数据执行读操作。会阻止其它事务获得相同数据集的排他锁。

  • 排他锁
  • 排他锁的代号是 X,是 eXclusive 的缩写,也可称为写锁,是基本的锁类型。

    排他锁的粒度与共享锁相同,也是行或者元组一个事务获取了排他锁之后,可以对锁定范围内的数据执行写操作允许获得排他锁的事务更新数据,阻止其它事务取得相同数据集的共享锁和排他锁

    如有两个事务 A 和 B,如果事务 A 获取了一个元组的共享锁,事务 B 还可以立即获取这个元组的共享锁,但不能立即获取这个元组的排他锁,必须等到事务 A 释放共享锁之后才可以。

    如果事务 A 获取了一个元组的排他锁,事务 B 不能立即获取这个元组的共享锁,也不能立即获取这个元组的排他锁,必须等到 A 释放排他锁之后才可以。

  • 意向锁
  • 为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB 还有两种内部使用的意向锁

    意向锁是一种表锁,锁定的粒度是整张表,分为意向共享锁(IS)和意向排他锁(IX)两类。

    意向共享锁表示一个事务有意对数据上共享锁或者排他锁。“有意表示事务想执行操作但还没有真正执行

    锁和锁之间的关系,要么是相容的,要么是互斥的。

  • 锁 a 和锁 b 相容是指:操作同样一组数据时,如果事务 t1 获取了锁 a,另一个事务 t2 还可以获取锁 b;
  • 锁 a 和锁 b 互斥是指:操作同样一组数据时,如果事务 t1 获取了锁 a,另一个事务 t2 在 t1 释放锁 a 之前无法释放锁 b。
  • 其中共享锁排他锁意向共享锁、意向排他锁相互之间的兼容/互斥关系如下表所示,其中 Y 表示相容,N 表示互斥。

    image-20220303132033771

    如果一个事务请求的锁模式与当前的锁兼容InnoDB 就将请求的锁授予该事务;反之,如果两者不兼容,该事务就要等待锁释放。

    为了尽可能提高数据库的并发量,需每次锁定的数据范围越小越好,越小的锁其耗费的系统资源越多,系统性能下降。为在高并发响应和系统性能两方面进行平衡,这样就产生了“锁粒度”的概念。

    也可以按锁粒度分类,可分为行级锁、表级锁和页级锁

  • 行级锁
  • 行级锁的锁定颗粒度在 MySQL 中是最小的只针对操作的当前行进行加锁,所以行级锁发生锁定资源争用的概率也最小。

    行级锁能够给予应用程序尽可能大的并发处理能力,从而提高需要高并发应用系统的整体性能。虽然行级锁在并发处理能力上面有较大的优势,但也因此带来了不少弊端。

    由于锁定资源的颗粒度很小,所以每次获取锁和释放锁需要做的事情也就更多,带来的消耗自然也就更大。此外,行级锁也最容易发生死锁。所以说行级锁最大程度地支持并发处理的同时,也带来了最大的锁开销。

    行级锁主要应用于 InnoDB 存储引擎。

    随着锁定资源颗粒度的减小,锁定相同数据量的数据所需要消耗的内存数量也越来越多,实现算法也会越来越复杂。不过,随着锁定资源颗粒度的减小,应用程序的访问请求遇到锁等待的可能性也会随之降低,系统整体并发度也会随之提升。

  • 表级锁
  • 表级锁为表级别的锁定,会锁定整张表,可以很好的避免死锁,是 MySQL 中最大颗粒度的锁定机制。

    一个用户在对表进行写操作(插入、删除、更新等)时,需要先获得写锁(也叫排斥锁),这会阻塞其它用户对该表的所有读写操作。没有写锁时,其它读取的用户才能获得读锁,读锁之间是不相互阻塞的。

    表级锁最大的特点就是实现逻辑非常简单,带来的系统负面影响最小。所以获取锁和释放锁的速度很快。当然,锁定颗粒度大带来最大的负面影响就是出现锁定资源争用的概率会很高,致使并发度大打折扣。

    不过在某些特定的场景中,表级锁也可以有良好的性能。例如,READ LOCAL 表级锁支持某些类型的并发写操作。另外,写锁也比读锁有更高的优先级,因此一个写锁请求可能会被插入到读锁队列的前面(写锁可以插入到锁队列中读锁的前面,反之读锁则不能插入到写锁的前面)。

    使用表级锁的主要是 MyISAMMEMORYCSV 等一些非事务性存储引擎。

    尽管存储引擎可以管理自己的锁,MySQL 本身还是会使用各种有效的表级锁来实现不同的目的。例如,服务器会为诸如 ALTER TABLE 之类的语句使用表级锁,而忽略存储引擎的锁机制。

  • 页级锁
  • 页级锁是 MySQL 中比较独特的一种锁定级别,在其他数据库管理软件中并不常见。

    页级锁的颗粒度介于行级锁与表级锁之间,所以获取锁定所需要的资源开销,以及所能提供的并发处理能力同样也是介于上面二者之间。另外,页级锁和行级锁一样,会发生死锁。

    页级锁主要应用于 BDB 存储引擎。

    image-20220303134128774

    从锁的角度来说,表级锁适合以查询为主只有少量按索引条件更新数据的应用,如 Web 应用。而行级锁更适合于有大量按索引条件,同时又有并发查询的应用,如一些在线事务处理(OLTP)系统。

    InnoDB锁算法 中的三种行锁

    在 MySQL 中,InnoDB 行锁通过给索引上的索引项加锁来实现,如果没有索引,InnoDB 将通过隐藏的聚簇索引来对记录加锁。

    InnoDB 支持 3 种行锁定方式:

  • 行锁(Record Lock):直接对索引项加锁。也叫记录锁
  • 间隙锁(Gap Lock):锁加在索引项之间的间隙,也可以是第一条记录前的“间隙”或最后一条记录后的“间隙”。
  • 临键锁(Next-Key Lock):行锁与间隙锁组合起来用就叫做 Next-Key Lock。 前两种的组合,对记录及其前面的间隙加锁。
  • 默认情况下,InnoDB 工作在可重复读(默认隔离级别)下,并且以 Next-Key Lock 的方式对数据行进行加锁,这样可以有效防止幻读的发生。

    Next-Key Lock 是行锁与间隙锁的组合,这样,当 InnoDB 扫描索引项的时候,会首先对选中的索引项加上行锁(Record Lock),再对索引项两边的间隙(向左扫描扫到第一个比给定参数小的值, 向右扫描扫到第一个比给定参数大的值, 然后以此为界,构建一个区间)加上间隙锁(Gap Lock)。如果一个间隙被事务 T1 加了锁,其它事务不能在这个间隙插入记录。

    要禁止间隙锁的话,可以把隔离级别降为读已提交(READ COMMITTED),或者开启参数 innodb_locks_unsafe_for_binlog。

    注意:以上语句描述的情况,与 MySQL 所设置的事务隔离级别有较大的关系。

    开启一个事务时,InnoDB 存储引擎会在更新的记录上加行级锁,此时其它事务不可以更新被锁定的记录。

    1. innodb对于⾏的查询使⽤next-key lock

    2. Next-locking keying为了解决Phantom Problem幻读问题

    3. 当查询的索引含有唯⼀属性时,将next-key lock降级为record key

    4. Gap锁设计的⽬的是为了阻⽌多个事务将记录插⼊到同⼀范围内,⽽这会导致幻读问题的产⽣

    5. 有两种⽅式显式关闭gap锁:(除了外键约束和唯⼀性检查外,其余情况仅使⽤record lock) A. 将事务隔离级别设置为RC B. 将参数innodb_locks_unsafe_for_binlog设置为1

    实例:

  • 行锁(Record Lock)
  • 分别在 A 窗口和 B 窗口中查看事务隔离级别,A 窗口和 B 窗口的事务隔离级别需要保持一致。

    # A 窗口查看隔离级别的 SQL 语句和运行结果
    show variables like 'tx_isolation';

    image-20220303141013025

    # B 窗口查看隔离级别 SQL 语句和运行结果
    show variables like 'tx_isolation';

    image-20220303141204472

    A窗口和 B窗口的事务隔离级别都为可重复读 REPEATABLE-READ

    # 在 A窗口中开启一个事务,并修改 tb_student 表
    begin;
    update test.tb_student set age ='30' where id = 1;
    # 在 B窗口中也开启一个事务,并修改 tb_student 表
    begin;
    update test.tb_student set age ='30' where id = 1;
    # 会发现 update 语句一直在执行
    # 这时我们在 A 窗口中提交事务。
    commit;
    # B 窗口中的 UPDATE 语句执行成功。
    # 查询 tb_student 表中的数据
    select * from test.tb_student;

    image-20220303141629968

    当有不同的事务同时更新同一条记录时,另外一个事务需要等待另一个事务把锁释放。

    # 查看 MySQL 中 InnoDB 存储引擎的状态
    show engine innodb status;

    image-20220303141815968

    从上面运行结果可以看出,SQL 语句 UPDATE test.tb_student SET age ='30' WHERE id = 1 在等待,RECORD LOCKS space id 197 page no 3 n bits 80 index PRIMARY of table test.tb_student trx id 19555 lock_mode X locks rec but not gap 表示锁住的资源,locks rec but not gap 代表锁住的是一个索引,不是一个范围。

    MySQL thread id 14, OS thread handle 4568, query id 886 localhost ::1 root updating”表示第 2 个事务连接的 ID 为 14,当前状态为正在更新,同时正在更新的记录需要等待其它事务将锁释放。当超过事务等待锁允许的最大时间,此时会提示“ERROR 1205(HY000):Lock wait timeout exceeded; try restarting transaction" 及当前事务执行失败,则自动执行回滚操作。

    MySQL 数据库采用 InnoDB 模式,默认参数 innodb_lock_wait_timeout 设置锁等待的时间是 50s,一旦数据库锁超过这个时间就会报错。

    # 命令查看当前数据库锁等待的时间
    show global variables like 'innodb_lock_wait_timeout';

    image-20220303142251934

  • 间隙锁(Gap Lock)
  • # 在保证 A 窗口和 B 窗口的前提下,将 tb_student 表中的 id 字段设为外键,并开启一个事务,修改 tb_student 表中 id 为 1 的 age。
    alter table test.tb_student add unique key idx_id(id);
    
    begin;
    update test.tb_student set age ='31' where id = 1;
    # B 窗口中开启一个事务,修改 tb_student 表中 id 为 2 的 age
    begin;
    update test.tb_student set age ='28' where id = 2;
    # 分别提交 A窗口和 B窗口的事务
    commit;
    # 查询 tb_student 表的数据
    select  * from test.tb_student;

    image-20220303142633667

    由于 InnoDB 行级锁为间隙锁,只锁定需要的记录,因此 B窗口中的事务可以更新其它记录,两个事务之间互不影响。

    来源:BearBrick0

    物联沃分享整理
    物联沃-IOTWORD物联网 » MySQL锁

    发表评论