锁机制是数据库系统保障数据的完整性和一致性的重要技术,各数据库存储引擎在实现机制上虽各有不同,但整体实现上是有异曲同工之妙。本文首先介绍mysql中锁类型以及死锁检测机制,并结合多并发场景下更新单条记录的死锁分析进行分析讨论,以了解背后的处理机制。
数据库系统使用锁机制用于管理对共享资源的并发访问,提供数据的完整性和一致性。InnoDB存储引擎会在行级别对表数据上锁,同时也会在数据库内部比如缓冲池资源LRU进行加锁以保证一致性。
在数据库中,lock和latch都可以称为锁,但是二者有着截然不同的含义:
MySQL [(none)]> show engine innodb mutex;
+--------+----------------+------------+
| Type | Name | Status |
+--------+----------------+------------+
| InnoDB | log0log.cc:907 | os_waits=2 |
+--------+----------------+------------+
1 row in set (0.68 sec)
在InnoDB存储引擎中实现了以下类型的行锁:
另外,为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁。
上述锁模式的兼容情况具体如下表所示:
需要注意的是,意向锁是InnoDB自动加的,不需用户干预。对于UPDATE、DELETE和INSERT语句,InnoDB会自动给涉及数据集加排他锁(X);对于普通SELECT语句,InnoDB不会加任何锁;事务可以通过以下语句显示给记录集加共享锁或排他锁:
共享锁(S):SELECT * FROM table_name WHERE … LOCK IN SHARE MODE
排他锁(X):SELECT * FROM table_name WHERE … FOR UPDATE
通过命令show engine innodb status;可以查看当前锁请求信息:
------------
TRANSACTIONS
------------
Trx id counter 20748
Purge done for trx's n:o < 20276 undo n:o < 0 state: running but idle
History list length 528
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 0, not started
MySQL thread id 3, OS thread handle 0x7fe8b2180700, query id 34 192.168.112.110 root init
show engine innodb status
---TRANSACTION 20747, ACTIVE 5 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 360, 1 row lock(s)
MySQL thread id 4, OS thread handle 0x7fe8b1f21700, query id 33 localhost root Sending data
select * from test.test01 lock in share mode
------- TRX HAS BEEN WAITING 5 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 8 page no 3 n bits 72 index `GEN_CLUST_INDEX` of table `test`.`test01` trx id 20747 lock mode S waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 6; compact format; info bits 0
0: len 6; hex 000000000411; asc ;;
1: len 6; hex 000000001d76; asc v;;
2: len 7; hex c2000001760110; asc v ;;
3: len 8; hex 7a68616e6773616e; asc zhangsan;;
4: len 20; hex 323031372d31322d31392d32302e303020202020; asc 2017-12-19-20.00 ;;
5: len 12; hex 757365723031202020202020; asc user01 ;;
------------------
---TRANSACTION 20746, ACTIVE 14 sec
2 lock struct(s), heap size 360, 5 row lock(s)
MySQL thread id 5, OS thread handle 0x7fe8b1edf700, query id 31 localhost root cleaning up
InnoDB存储引擎有三种行锁算法:
Next-Key lock是结合了Gap Lock和Record Lock的锁机制,采用的是左开右闭规则,假如一个索引有10、11、13和20这四个值,那么该索引可能被Next-Key Locking的区间为:
(-∞,10]、(10,11]、(11,13]、(13,20]、(20,+∞]
以下为例,向表T1(name primary key,id key)插入id=10,因为next-key lock是左开右闭,id=6本身没有加锁、id=10本身加锁了,所以加锁区间为(6,10]
幻读问题是指在同一事务下,连续执行两次相同的SQL语句可能导致不同的结果,第二次执行的SQL语句可能返回之前不存在的行。在默认的事务隔离级别Repeatable READ下,InnoDB存储引擎采用Next-Key locking机制来避免幻读问题。在隔离级别READ Committed下,采用的是record lock,可以在应用层面指定share mode实现next-key lock机制:
SELECT * FROM table WHERE col=xxx LOCK IN SHARE MODE;
如果通过索引查询一个值,并对该行加上一个S-LOCK,即使查询的值不存在,锁定的也是一个范围。因此如果没有返回任何行,新插入的值一定是唯一的。如果在SELECT … LOCK IN SHARE MODE时候有多个并发操作会导致死锁,只有一个事务的插入操作会成功,其余的事务会抛出死锁的错误。如下所示:
![在这里插入图片描述](https://img-blog.csdnimg.cn/164489e6630e410c9ecab13781de0e52.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBAc29saWhhd2s=,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center = 90%x90)
死锁是指两个或两个以上的事务在执行过程过程中,因争夺锁资源而造成的一种互相等待的现象。解决死锁问题的最简单的一种方法是超时,即当两个事务互相等待时,当其中一个等待时间超过设定的阈值时会进行回滚,另一个等待的事务就能继续执行,在innodb存储引擎中,通过innodb_lock_wait_timeout参数设置超时时间。超时机制简单粗暴,但是如果超时的事务所占的权重较大,执行了很多更新操作,回滚将占用很长时间。除了超时机制,数据库还普遍采用waits-for graph的方式进行死锁检测,waits-for graph机制要求数据库保存两种信息:锁的信息链表和事务等待链表,通过上述链表构造出一张图,如果存在回路,则说明存在死锁。
如图所示,在事务等待列表中有4个事务T1/T2/T3/T4,事务T2对row1占用X-lock、事务T1对row2占用S-lock。事务T1需要等待事务T2中的row1资源,事务T2需要等待T1和T4占用的row2资源,最终waits-for graph图如上图所示。从图中可以发现存在回路(T1,T2),表示存在死锁。waits-for graph是主动的死锁检测机制,在每个事务请求锁并发生等待时都会判断是否存在回路,若存在则有死锁,一般情况下InnoDB存储引擎会选择undo log最小的事务进行回滚。如果发生了死锁,可以使用show engine innodb status命令来确定最后一个死锁产生的原因。
上面介绍了InnoDB存储引擎中的锁类型以及死锁检测机制,下面来看下在实际开发过程中遇到的多并发更新单条记录引发的死锁问题。
1)表结构
CREATE TABLE t2 (
`a` int(11) NOT NULL DEFAULT 0,
`b` int(11) DEFAULT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`a`),
UNIQUE KEY `uk_bc` (`b`,`c`)
);
2)并发执行三个session
序号 | Session A | Session B | Session C |
---|---|---|---|
1 | BEGIN;INSERT INTO t2 VALUES(123,22,12,11); | ||
2 | BEGIN;INSERT INTO t2 VALUES(123,22,12,11); | ||
3 | BEGIN;INSERT INTO t2 VALUES(123,22,12,11); | ||
4 | ROLLBACK; | ||
5 | DEADLOCK; |
上述场景出现的deadlock日志如下:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2022-03-19 22:14:44 7f229eae0700
*** (1) TRANSACTION:
TRANSACTION 21771, ACTIVE 129 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1184, 2 row lock(s)
MySQL thread id 2, OS thread handle 0x7f229eb22700, query id 49 localhost root update
INSERT INTO t2 VALUES(123,22,12,11)
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 21 page no 3 n bits 72 index `PRIMARY` of table `test`.`t2` trx id 21771 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) TRANSACTION:
TRANSACTION 21772, ACTIVE 112 sec inserting
mysql tables in use 1, locked 1
4 lock struct(s), heap size 1184, 2 row lock(s)
MySQL thread id 3, OS thread handle 0x7f229eae0700, query id 50 localhost root update
INSERT INTO t2 VALUES(123,22,12,11)
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 21 page no 3 n bits 72 index `PRIMARY` of table `test`.`t2` trx id 21772 lock mode S
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 21 page no 3 n bits 72 index `PRIMARY` of table `test`.`t2` trx id 21772 lock_mode X insert intention waiting
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
0: len 8; hex 73757072656d756d; asc supremum;;
*** WE ROLL BACK TRANSACTION (2)
MySQL [test]> select * from information_schema.innodb_locks;
+--------------+-------------+-----------+-----------+-------------+------------+------------+-----------+----------+-----------+
| lock_id | lock_trx_id | lock_mode | lock_type | lock_table | lock_index | lock_space | lock_page | lock_rec | lock_data |
+--------------+-------------+-----------+-----------+-------------+------------+------------+-----------+----------+-----------+
| 21772:21:3:2 | 21772 | S | RECORD | `test`.`t2` | PRIMARY | 21 | 3 | 2 | 123 |
| 21770:21:3:2 | 21770 | X | RECORD | `test`.`t2` | PRIMARY | 21 | 3 | 2 | 123 |
| 21771:21:3:2 | 21771 | S | RECORD | `test`.`t2` | PRIMARY | 21 | 3 | 2 | 123 |
+--------------+-------------+-----------+-----------+-------------+------------+------------+-----------+----------+-----------+
3 rows in set (0.00 sec)
多并发场景下更新单条记录引发的死锁问题,背后的处理机制是LOCK_INSERT_INTENTION和LOCK_S之间的出现的锁资源冲突。本文基于基哥分享的死锁场景和死锁分析过程总结,这是一个很有意思的死锁场景。
参考资料
转载请注明原文地址:https://blog.csdn.net/solihawk/article/details/123676871
文章会同步在公众号“牧羊人的方向”更新,感兴趣的可以关注公众号,谢谢!