InnoDB存储引擎中的锁
InnoDB存储引擎中的锁
概要
人们认为行级锁总会增加开销。实际上,只有当实现本身会增加开销时,行级锁才会增加开销。InnoDB存储引擎不需要锁升级,因为一个锁和多个锁的开销是相同的。
一、什么是锁 ?
锁是数据库系统区别于文件系统的一个关键特性。锁机制用于管理对共享资源的并发访问。
二、锁类型
在 InnoDB 中实现了两个标准的行级锁,可以简单的看为两个读写锁。
1. S 共享锁
又叫读锁,其他事务可以继续加共享锁,但是不能继续加排他锁。
2. X 排他锁
又叫写锁,一旦加了写锁之后,其他事务就不能加锁了。
3. 锁兼容
如果一个事务T1已经获得了行r的共享锁,那么另外的事务T2可以立即获得行r的共享锁,因为读取并没有改变行r的数据,称这种情况为锁兼容(Lock Compatible)。
4. 锁不兼容
但若有其他的事务T3想获得行r的排他锁,则其必须等待事务T1、T2释放行r上的共享锁——这种情况称为锁不兼容。
如下图:
说明:从上图中可以看出X锁与任何的锁都不兼容,而S锁仅和S锁兼容。需要特别注意的是,S和X锁都是行锁,兼容是指对同一记录 (row) 锁的兼容性情况。
三、意向锁 (Intention Lock)
MySQL 自身就提供了表锁的能力,行锁是 InnoDB 存储引擎提供的,MySQL 本身并不提供行级锁的能力。
1. 锁共存问题
既有表锁又有行锁,我们来考虑下这两种类型的锁共存的问题。看下面这个例子:
事务 A 加了行级读锁,锁住了表中的一行,让这一行只能读,不能写。之后,事务 B 尝试申请整个表的写锁。
如下图:
如果事务 B 申请成功,那么理论上它就能修改表中的任意一行,这与 A 持有的行级读锁是冲突的。
数据库需要避免这种冲突,就势必要让 B 的申请被阻塞,直到 A 释放行级读锁。
那数据库要怎么判断这个冲突呢?
- 步骤 1:判断表是否已被其他事务用表级锁锁住了整张表
- 步骤 2:判断表中的每一行是否已被行级锁锁住
看起来没有什么困难的,但请注意步骤 2,判断表中的每一行,各位,如何判断?
显然,需要遍历!遍历表中的每一行。但是,这样的判断方法效率实在太过于低下了。于是就有了意向锁!
2. 意向锁
InnoDB存储引擎支持多粒度(granular)锁定,这种锁定允许事务在行级上的锁和表级上的锁同时存在。
为了支持在不同粒度上进行加锁操作,InnoDB存储引擎支持一种额外的锁方式,称之为意向锁(Intention Lock)。意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度(fine granularity)上进行加锁。
InnoDB存储引擎支持意向锁设计比较简练,其意向锁即为表级别的锁。设计目的主要是为了在一个事务中揭示下一行将被请求的锁类型。
其支持两种意向锁为:
1)意向共享锁 (Intention Shared Lock, IS Lock):表达一个事务想要获取一张表中某几行的共享锁(行级读锁)时,InnoDB 存储引擎会自动地先获取该表的意向读锁(表级锁)。
2)意向排他锁(Intention Exclusive Lock, IX Lock):表达一个事务想要获取一张表中某几行的排他锁(行级写锁)时,InnoDB 存储引擎会自动地先获取该表的意向写锁(表级锁)。
注意这里的自动:申请意向锁的动作是数据库完成的,就是说,事务 A 申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。
在意向锁存在的情况下,事务 A 如果想申请行级读锁,就必须先申请该表的意向读锁,申请成功后才能继续申请某行记录的行级读锁。
2. 如何解决锁并存问题?
在意向锁存在的情况下,上面的判断可以改成:
- 步骤 1(不变):判断表是否已被其他事务用表级锁锁住了整张表
- 步骤 2:发现表上有意向读锁(说明表中有些行被行级读锁锁住了),意向读锁和表级写锁互斥,因此,事务 2 申请表的写锁会被阻塞。
也就是说原先步骤 2 的遍历表中每一行的操作,简化成了判断下整张表上有无表级意向锁就行了,效率大幅提升。
如下图:
3. 意向锁的工作原理
1)当一个事务尝试对表中的某些行加锁时,它首先会在表级别加一个意向锁(IS 或 IX)。
2)其他事务在尝试加表级锁(如表级共享锁 S 或表级排他锁 X)时,会检查表级的意向锁情况,决定是否可以加锁。
4. 意向锁的具体案例
假设有一个事务 T1 想要在表 orders
中对某些行加排他锁(X),而另一个事务 T2 想要对整个表加共享锁(S)。
- T1 先对表
orders
加意向排他锁(IX),然后对某些行加排他锁(X)。 - T2 尝试对表
orders
加共享锁(S),此时发现表上已经有 IX 锁,因此不能加 S 锁,T2 需要等待 T1 释放锁。
通过这个机制,InnoDB 可以在不同粒度的锁之间进行协调,确保锁定操作的高效性和一致性。
5. InnoDB存储引擎中锁的兼容性
如下图:
兼容性:是指事务 A 获得一个某行某种锁之后,事务 B 同样的在这个行上尝试获取某种锁,如果能立即获取,则称锁兼容,反之叫冲突。
纵轴是代表已有的锁,横轴是代表尝试获取的锁。
6. 意向锁的作用
1)提高锁定效率
通过在表级别设置意向锁,可以避免每次在行级别加锁时都要检查整个表的锁情况。这种机制提高了锁定效率,特别是在大表上的操作。
2)保证多粒度锁定一致性
意向锁允许在表级别和行级别之间进行协调,从而确保锁定机制的一致性和正确性。例如,在表级别加排他锁前,意向锁会确保表中没有其他事务持有行级锁。
3)避免死锁
意向锁通过提前声明锁定意图,帮助 InnoDB 预防和检测死锁情况,提高系统的并发处理能力和可靠性。
7. 总结
1)InnoDB 支持多粒度锁,特定场景下,行级锁可以与表级锁共存。
2)IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
3)意向锁在保证并发性的前提下,实现了行锁和表锁共存且满足事务隔离性的要求。
四、锁的算法
InnoDB存储引擎有3种行锁的算法,其分别是:
- Record Lock:单个行记录上的锁
- Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
- Next-Key Lock∶Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身。
具体在下面分别介绍:
1. 记录锁 (Record Lock)
Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表在建立的时候没有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。
需要注意以下几点:
1)记录锁是锁住记录的,锁住的是索引记录,而不是我们真正的数据记录:
2)如果锁的是非主键索引,会在自己的索引上面加锁之后然后再去主键上面加锁锁住。
3)如果表上没有索引(包括没有主键),则会使用隐藏的主键索引进行加锁。
4)如果要锁的没有索引,则会进行全表记录加锁。
2. 间隙锁 (Gap Lock)
间隙锁顾名思义锁间隙,不锁记录。锁间隙的意思就是锁定某一个范围,间隙锁又叫 gap 锁,其不会阻塞其他的 gap 锁,但是会阻塞插入间隙锁,这也是用来防止幻读的关键。
如下图:
3. Next-Key Lock
Next-Key Lock是结合了Gap Lock和Record Lock的一种锁定算法,在Next-Key Lock算法下,InnoDB对于行的查询都是采用这种锁定算法。
在InnoDB 默认的事务隔离级别下,即REPEATABLE READ下,InnoDB存储引擎采用Next-Key Locking这种锁定算法。例如一个索引有10,11,13和20这四个值,那么该索引可能被Next-Key Locking的区间为:
(-∞,10] 、 (10,11] 、 (11,13] 、(13,20] 、(20,+∞)
当查询的索引含有唯一属性(unique字段)时,InnoDB存储引擎会对Next-Key Lock进行优化,将其降级为Record Lock,即仅锁住索引本身,而不是范围。
五、死锁
1. 死锁的常见原因
在MySQL数据库管理系统中,死锁是并发控制中一个非常关键的问题。当两个或更多的事务在执行过程中,因争夺资源而造成的一种相互等待的现象,若无外力作用,这些事务都将无法继续执行,这种情况称为死锁。
1)资源竞争:多个事务同时请求同一资源,并相互等待对方释放资源。
2)顺序不一致:事务A锁定了资源1,然后请求资源2;同时事务B锁定了资源2,然后请求资源1。这样就形成了循环等待,导致死锁。
3)长时间事务:如果一个事务执行时间过长,其他等待该事务释放资源的事务也可能因等待时间过长而进入死锁状态。
4)索引使用不当:如果没有使用适当的索引,可能导致数据库表的全扫描,进而增加死锁的概率。
2. 死锁的解决方法
1) 优化查询语句:确保SQL查询尽可能高效,使用适当的索引,避免全表扫描。
2) 调整事务设计:尽量将事务设计得简短且高效,尽快提交或者回滚,减少事务持有锁的时间。
3) 设置锁超时时间:可以通过设置 innodb_lock_wait_timeout 参数来指定一个事务等待锁的最长时间,超过这个时间后,事务将自动回滚,从而避免死锁。
4) 死锁检测与回滚:MySQL的InnoDB存储引擎会自动检测死锁,并回滚其中一个事务,从而解决死锁。但这种方式仍然可能导致性能问题,因为它涉及到事务的回滚。
5) 分析和监控:使用 SHOW ENGINE INNODB STATUS 命令来分析死锁的原因,并根据分析结果进行相应的优化。
6) 重试机制:在应用程序中实现重试机制,当检测到死锁时,可以让事务稍后再试。
7) 避免用户交互事务:尽量避免在事务中等待用户输入,因为这会延长事务的执行时间,增加死锁的风险。
8) 合理设计数据库结构:避免在多个表中持有锁,尽量通过JOIN操作来一次性获取所需数据。
3. 总结
MySQL死锁是数据库并发控制中的一个常见问题,但通过合理的查询优化、事务设计、死锁检测与回滚、分析和监控等手段,可以有效地减少死锁的发生,提高数据库的性能和稳定性。
参考链接:
https://www.cnblogs.com/wade-luffy/p/9689975.html
https://www.51cto.com/article/743293.html