一文掌握MySQL锁机制(共享锁/排他锁/意向锁/间隙锁/临键锁等)
作者:互联网
文章目录
1、数据库为什么要有锁机制
对于系统来说,数据是最核心的资产,需要提供强一致性和有效性。
当“数据库 + 并发读写访问”场景时,为保持强一致性和有效性,那么数据库中细分的数据区域,就是一个个的临界区。
为具备数据临界区的能力,就引入了数据库的锁机制。
(锁的理念和本质思想都是共性的,数据库锁只是将锁的运用做了场景化的适配优化)
2、锁的分类
MySQL官方文档:https://dev.mysql.com/doc/refman/5.6/en/innodb-locking.html#innodb-intention-locks
数据库中是以“表”为一个数据体的单位,那么“表锁”是自然存在的一类锁
表锁可分为:
- 表 共享锁(S)
- 表 排他锁(X)
- 表 意向共享锁(IS)
- 表 意向排他锁(IX)
- 自增锁
行锁可分为:
- 行 共享锁 + 行 排他锁
- 实现方式:记录锁 / 间隙锁 / 临键锁
- 行 插入意向锁
核心助读: 《《《《《《 重点!重点!重点!
1、先存在:表锁
2、然后为更好的并发需要,做了细粒度的锁:行锁
3、为了表锁和行锁间更高效的配合,出现了:意向锁
4、为了针对范围型数据操作的有效性,出现了:间隙锁、临键锁
5、为了插入查询并发的优化,出现了:插入意向锁
3、锁详细介绍
3.1、表-共享锁(S)
用途: 表共享锁之间不互斥,读读可以并行。
原理: N/A
操作方式:
- 自动(默认)
- MySQL的执行优化判断(锁升级):1、需要更新大部分数据时,不走索引的全表扫;2、事务涉及多个表,比较复杂,很可能引起死锁的;
- 手动
- lock table tableName read;
- unlock table tableName;
- select * from XX lock in share mode的锁升级
3.2、表-排他锁(X)
用途: 表排他锁与任何锁都互斥,写读和谢谢都不可并行。
原理: N/A
操作方式:
- 自动(默认)
- insert / update / delete 的锁升级
- 手动
- lock table tableName write;
- unlock table tableName;
- select * from XX for update的锁升级
3.3、表-自增锁(Auto-Inc Lock)
自增锁是一种特殊的表级锁。
专门针对事务插入AUTO_INCREMENT类型的列。
最简单的情况,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。
MySQL官方解释:
AUTO-INC lock is a special table-level lock taken by transactions inserting into tables with AUTO_INCREMENT columns.In the simplest case, if one transaction is inserting values into the table, any other transactions must wait to do their own inserts into that table, so that rows inserted by the first transaction receive consecutive primary key values.
举个例子:表中有如下三条数据,id为AUTO_INCREMENT:
1, zhangsan
2, lisi
3, wangwu
事务A先执行,但还未提交: insert into t(name) values(‘lindan’);
事务B后执行: insert into t(name) values(‘lizongwei’);
此时事务B插入操作会阻塞,直到事务A提交。
3.4、行 - 记录锁(Record Lock)
(同时支持“共享锁和排他锁”,根据SQL语义区分)
行记录锁是作用在索引记录(Key)上的锁。(B+树上的Key节点)
语义上是“锁定一行数据”,做并发操作的保护。
注意点:
1、记录锁是作用在索引上的锁,如果建表时候没有设置任何一个索引,InnoDB引擎会使用隐式的主键来进行锁定。
2、InnoDB行锁是作用在索引上的锁,只有查询走索引时才会用到。如果SQL查询时没有使用任何一个索引,则会升级为“表锁”。
3、InnoDB行锁是作用在索引上的锁,即使是访问不同行的数据记录,但如果使用的是同一个索引键(KEY),会发生锁冲突。
4、如果数据表建有多个索引,可以通过不同的索引锁定不同的行。
常用SQL:
select ... for update;
or
select ... lock in share mode;
- 注意:
select * from t id = 1;
不加锁的哦。因为他是走MVCC的快照读的。
思考题:SQL中用 IN(x,y,z)操作,是加行记录锁吗?
3.5、表 - 意向共享锁(IS)/ 意向排他锁(IX)
核心助读:为了表锁和行锁间更高效的配合,出现了:意向锁
为什么?
自从锁粒度细分到了:表锁和行锁,就顺带引入了表锁和行锁的配合关系:
- 同一张表下,有行排他锁存在的时候,此表不能同时加表锁(共享锁和排他锁都不行)
- 同一张表下,有行共享锁存在的时候,只允许加表共享锁,不允许加表排他锁。
那么,在每次加锁时往往免不了 查询&判断 表上表锁情况和行锁情况。
为优化遍历表下所有索引,查询行锁情况的费时操作,【意向锁】出现了。
意向锁原理:
- 意向排他锁(IX):一个事务想给一个数据行加排他锁之前,必须先获得该表的IX锁。
- 意向共享锁(IS):一个事务给一个数据行加共享锁之前,必须先获得该表的IS锁。
- 当另外事务想给该表加表锁(S锁 或 X锁)时,只需查看该表上的IS锁和IX锁的加锁情况即可,不用再做遍历行锁的行为。
表共享/排他锁 和 表意向共享/意向排他锁 的兼容关系:
常用SQL:
select ... for update;
or
select ... lock in share mode;
3.6、行 - 间隙锁(Gap Lock)
核心助读:为了针对范围型数据操作的有效性,出现了:间隙锁
从幻读讲起:
- 幻读
- 一个事务中,前后多次读同一个范围的数据,读到的结果不一致。
- 注,若某个column不是唯一键,即使where条件是此column的等值查询,也可能会幻读。因为可以插入/删除column = A的重复记录。
- 怎么解决的
- 快照读的场景:(select … 范围)
- MVCC解决了此问题。
- 当前读的场景:(select … 范围 for update / update 范围 / delete 范围)
- (假设:范围 = (100, 300))
- InnoDB先看查询条件是否走索引,若不走索引,则加表锁。若走索引,则加间隙锁。
- InnoDB基于SQL中的范围,在索引上锁住(100, 300)的索引范围。防止其他事务并发的操作(增删改)这个KEY范围内的数据。
- 快照读的场景:(select … 范围)
注:
- 间隙锁只有RR级别有
- 在行锁里,间隙锁是最重的一个锁。阻塞性最大。
常用SQL:
select ... 范围 for update;
or
select ... 范围 lock in share mode;
or
update ... 范围;
delete ... 范围;
insert ... 范围;
3.7、行 - 临键锁(Next-Key Lock)
核心助读:为了针对范围型数据操作的有效性,出现了:临键锁
注:临键锁(Next-Key Lock)是InnoDB RR模式下的默认行锁实现。
临键锁:
- InnoDB中,更新非唯一索引对应的记录(重点),会加上Next-Key Lock,同时锁住记录本身,以及数据相邻上游范围和相邻下游范围。
- 即同时用到了 Record Lock + Gap Lock
- 当更新记录为空时,临键锁就退化成了间隙锁。
再注:临键锁(Next-Key Lock)是InnoDB RR模式下的默认行锁实现。
举例:
- 建表
`create table test(
id int,
v1 int,
v2 int,
primary key(id),
key `idx_v1`(`v1`)
)Engine=InnoDB DEFAULT CHARSET=UTF8;
- 该表下的记录
+----+------+------+
| id | v1 | v2 |
+----+------+------+
| 1 | 1 | 0 |
| 2 | 3 | 1 |
| 3 | 4 | 2 |
| 5 | 5 | 3 |
| 7 | 7 | 4 |
| 10 | 9 | 5 |
- test表中的v1(非唯一索引)字段值可以划分的区间为:
(-∞,1)
(1,3)
(3,4)
(4,5)
(5,7)
(7,9)
(9, +∞)
- 执行多个事务的SQL
事务A | 事务B |
---|---|
BEGIN; | BEGIN; |
UPDATE test SET v2 = 8 WHERE v1 = 7; | |
//do something others | INSERT INTO test VALUES(6,6,5); // 被阻塞 |
COMMIT; | //do something others |
COMMIT; |
为什么事务B的insert操作会被阻塞呢?
- 这是因为事务A操作v1=7的记录时,其临键锁是既锁住v1 = 7的记录,又锁住(5,7)和(7,9)的临近上游范围和下游范围。
- 当事务B插入的v1值为6时,恰好在事务A的临键锁的锁定区间v1(5 , 9)范围内。故,会产生阻塞等待,
- 不仅仅insert操作, 其他事务的update操作也一样会被阻塞。
疑问:操作单条记录,为什么要锁上下游?
(防止相同KEY的插入,破坏事务间的隔离性)
- 若执行
UPDATE
testSET v2 = 8 WHERE v1 = 7;
时,仅以记录锁锁住v1 = 7的记录。 - 假设在事务A的执行过程中,事务C执行了
INSERT INTO
testVALUES(10,7,9);
。那么事务C新插入的记录就会对事务A可见。破坏了事务的隔离性。
注:
- 临键锁只针对非唯一索引。若是唯一索引,则只需记录锁就行了。
3.8、行 - 插入意向锁(Insert Intention Lock)
核心助读:插入意向锁是间隙锁的一种。保障间隙锁的隔离性,又提高并发插入的能力。
以上的锁,基本都是对已存在的数据做select、update、delete。那么,新插入的数据需不需要锁呢?
要的!
插入意向锁:
- 多个事务,在同一个索引,同一个范围区间插入记录时,如果插入的位置不冲突,不会阻塞彼此。
-
插入意向锁 与 间隙锁/临键锁 是会冲突的,并发了会互相阻塞。
-
不加插入意向锁,会有什么问题?
- 破坏间隙锁保护数据的隔离性。因为检测不到插入动作的冲突。
3.9、总结-要点
1、基础是“表锁和行锁”下的读写锁,其他名称的锁都是为了解决/优化特定场景而诞生的,记住每个锁解决的场景。
2、行锁是作用在索引KEY上的。若SQL查询未走索引,则会升级为表锁。
4、对应SQL场景
以MySQL默认的InnoDB以及RR隔离级别为条件
select ...
- 快照读;无锁;
select ... lock in share mode
-
IS锁;S锁;
-
啥时候用?
操作数据时,想同时锁定其他行/表的数据,已保持与本次SQL数据操作的一致性。
- 拿mysql官方文档的例子来说:
- 一个表是child表,一个是parent表。
- 假设child表的某一列child_id映射到parent表的c_child_id列,那么从业务角度讲,此时直接insert一条child_id=100记录到child表是存在风险的,因为刚insert的时候可能在parent表里删除了这条c_child_id=100的记录,那么业务数据就存在不一致的风险。
- 正确的方法是再插入时执行select * from parent where c_child_id=100 lock in share mode,锁定了parent表的这条记录,然后执行insert into child(child_id) values (100)就ok了。
select ... for update
-
IX锁;X锁;
-
啥时候用?
与"lock in share mode"的场景类似,都目的是把其他相关数据【锁定】,只不过"for update"预示着在本事务中会进一步对其他相关的锁定数据进行修改。
update ... [等值 / 范围]
/ delete ... [等值 / 范围]
- 锁分类:IX锁;X锁;
- 锁实现:记录锁;间隙锁;临键锁;
insert ...
- 插入意向锁;IX锁;X锁;
5、锁与事务的关系
5.1、加锁
- 在事务中,随着执行的SQL,按需获取锁。
- 过程中可能会有阻塞等待或死锁
5.2、释放锁
- 事务提交时,自动释放本事务中所有的锁。
6、死锁和死锁检测处理
6.1、死锁
死锁的发生:多个事务,每个事务都已持有部分锁,然后进一步去获取对方事务持有的锁,互相等待。
例子:
- 事务A
start transaction;
update stock_price set close = 45.50 where stock_id = 4 and date = '2017-4-26';
update stock_price set close = 19.80 where stock_id = 3 and date = '2017-4-27';
commit;
- 事务B
start transaction;
update stock_price set high = 20.10 where stock_id = 3 and date = '2017-4-27';
update stock_price set high = 47.20 where stock_id = 4 and date = '2017-4-26';
commit;
如果凑巧,两个事务均执行了第一条update语句,同时锁定了该资源。当尝试执行第二条update语句的时候,去发现资源已经被锁定,两个事务都等待对方释放锁,则陷入死循环,形成死锁。
6.2、死锁检测和处理
MySQL官方文档:https://dev.mysql.com/doc/refman/5.6/en/innodb-deadlock-example.html
MySQL支持了死锁检测和处理机制。
当出现死锁后,有两种策略:
1、直接进入等待,直到超时。这个超时时间可以通过参数 innodb_lock_wait_timeout
来设置
2、发起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数 innodb_deadlock_detect
设置为 on,表示开启这个逻辑
如何选择策略
在 InnoDB 中,innodb_lock_wait_timeout 的默认值是 50s,意味着如果采用第一个策略,当出现死锁以后,第一个被锁住的线程要过 50s 才会超时退出,然后其他线程才有可能继续执行。对于在线服务来说,这个等待时间往往是无法接受的。但是,我们又不可能直接把这个时间设置成一个很小的值,比如 1s。这样当出现死锁的时候,确实很快就可以解开,但如果不是死锁,而是简单的锁等待呢?所以,超时时间设置太短的话,会出现很多误伤。
所以,正常情况下我们还是要采用第二种策略,即:主动死锁检测,而且 innodb_deadlock_detect 的默认值本身就是 on。主动死锁检测在发生死锁的时候,是能够快速发现并进行处理的,但是它也是有额外负担。
故,即使出现死锁,待mysql回滚,业务上做失败处理或重试即可。
6.3、如何减少死锁
(其实,我们日常都在做)
1、业务研发上,一般以乐观锁为主。
2、SQL尽量用索引,因为走索引才能用上行锁,更细粒度的锁,减少锁冲突的概率。
3、减少大事务,用于减少持有锁的时间,进而降低冲突。
6、类比Java的锁场景
其实,MySQL数据的9种锁并不复杂,试着分层、对比Java的锁场景理解下:
以Java ConcurrentHashMap为例。
为了做到线程安全,ConcurrentHashMap要做好多线程并发访问的控制。
第一层:粗粒度
- 方法:对整个ConcurrentHashMap加读锁/写锁,来保证并发安全。
- 类比MySQL:表-共享锁(S) 和 表-排他锁(X)
第二层:细粒度
- 方法:HashMap的本质是“数组+链表”,把锁粒度控制到数组节点(即hash槽位)的粒度。即当前JDK ConcurrentHashMap synchronized同步域控制到Segment(hash槽位)的理念。
- 类比MySQL:行-记录锁
第三层:粗细结合
- 方法:随着数组的使用,ConcurrentHashMap 会做resize。此时需要锁定整个ConcurrentHashMap。此时,怎么快速判断ConcurrentHashMap下是否有细粒度的锁存在呢?
- 类比MySQL:表-意向共享锁、表-意向排他锁
第四层:场景化定制
- 方法:ConcurrentHashMap内的批量操作,会涉及多个hash槽位的联动。需要对范围的节点进行锁定保护。
- 类比MySQL:行-间隙锁;行-临键锁;行-插入意向锁;
就这么简单!
标签:事务,插入,update,临键,索引,死锁,意向锁,MySQL,select 来源: https://blog.csdn.net/weixin_43318367/article/details/113444316