死锁
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环。当事务试图以不同的顺序锁定资源时,就可能产生死锁。
锁的行为和顺序和存储引擎相关。以同样的顺序执行语句,有些存储引擎会产生死锁有些不会——死锁有双重原因:真正的数据冲突;存储引擎的实现方式。
死锁演示
创建表test 的SQL语句
CREATE TABLE `test` (
`a` int NOT NULL,
`b` int DEFAULT NULL,
`c` int DEFAULT NULL,
`d` varchar(45) DEFAULT NULL,
PRIMARY KEY (`a`),
KEY `idx_test_b` (`b`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
会话A | 会话B |
---|---|
begin; | begin; |
select * from test; | |
select * from test where a=4 for update; | select * from test where a=7 for update; |
select * from test where a=7 for update;–阻塞 | select * from test where a=4 for update;–阻塞 |
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
共享锁与排他锁
InnoDB行锁类型
共享锁(S):允许事务去读取一行数据。
排他锁(X):允许事务删除或更新一行数据。
如果一个事务T1已经获得行r的共享锁,那么另一个事务T2可以立即获得行r的共享锁,因为读取并没有改变行r的数据,称这种情况为锁兼容。但若有其他事务T3想获得行r的排他锁,则其必须等待事务T1,T2释放行r上的共享锁----这种情况称为锁不兼容。
X | S | |
---|---|---|
X | 不兼容 | 不兼容 |
S | 不兼容 | 兼容 |
加锁方式
共享锁(S):select * from table_name where id=1 lock in share mode;
排他锁(X):select * from table_name where id=1 for update;
for update是在数据库中上锁用的,可以为数据库中的行上一个排它锁。
InnoDB行锁实现方式
InnoDB行锁是通过给索引项加锁来实现的,如果没有索引,InnoDB将通过隐藏的聚簇索引来对记录加锁。
for update的使用场景
如果遇到存在高并发并且对于数据的准确性很有要求的场景,是需要了解和使用for update的。
比如涉及到金钱、库存等。一般这些操作都是很长一串并且是开启事务的。如果库存刚开始读的时候是1,而立马另一个进程进行了update将库存更新为0了,而事务还没有结束,会将错的数据一直执行下去,就会有问题。所以需要for upate 进行数据加锁防止高并发时候数据出错。
记住一个原则:一锁二判三更新
共享锁演示
会话A | 会话B |
---|---|
begin; | begin; |
select * from test; | select * from test; |
select * from test where a=1 lock in share mode; | |
select * from test where a =1;–非阻塞 | |
update test set b=10 where a=1;–阻塞 | |
commit; | |
排他锁演示
会话A | 会话B |
---|---|
begin; | begin; |
select * from test; | |
select * from test; | |
select * from test where a=1 for update; | |
select * from test where a=1;–非阻塞 | |
select * from test where a=1 lock in share mode;–阻塞 | |
update test set b=20 where a=1;–阻塞 | |
commit; | |
表锁
InnoDB只有在通过索引条件检索数据时使用行级锁,否则使用表锁
会话A | 会话B |
---|---|
begin; | begin; |
select * from test where d=4 for update; | |
select * from test where a=1 lock in share mode; | |
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction | |
insert into test(a,b,c,d) values(12,12,12,12); | |
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction |
记录锁、间隙锁、临键锁
记录锁(Record Locks)
单个行记录上的锁,例如:
select * from test where a=1 for update;
间隙锁
锁定一个范围,但不包含记录本身。
Next-key Lock
间隙锁 + 记录锁,锁定一个范围,并且锁定记录本身。
会话A | 会话B |
---|---|
begin; | begin; |
select * from test where a=5 for update; --查询不存在的记录 | |
Empty set (0.00 sec) | insert into test values(5,5,5,5);–阻塞 |
insert into test values(6,6,6,6);–阻塞 | |
insert into test values(8,8,8,8);–非阻塞 | |
检索条件 a=5 ,向左取得最靠近的值4作为左区间,向右取得最靠近的7作为右区间,因此,session 1的间隙锁的范围(4,5] 和(5,7],所以 a=5 或者 a=6 插入失败,a=8 可以插入成功。
意向锁
- 意向共享锁(IS Lock): 事务想要获得一张表中某几行的共享锁。
- 意向排他锁(IX Lock): 事务想要获得一张表中某几行的排他锁。
由于InnoDB存储引擎支持的是行级别的锁,因此意向锁其实不会阻塞除全表扫以外的任何请求。意向锁的作用就是:
当一个事务在需要获取资源的锁定时,如果该资源已经被排他锁占用,则数据库会自动给该事务申请一个该表的意向锁。如果自己需要一个共享锁定,就申请一个意向共享锁。如果需要的是某行(或者某些行)的排他锁定,则申请一个意向排他锁。
IS | IX | S | X | |
---|---|---|---|---|
IS | 兼容 | 兼容 | 兼容 | 不兼容 |
IX | 兼容 | 兼容 | 不兼容 | 不兼容 |
S | 兼容 | 不兼容 | 兼容 | 不兼容 |
X | 不兼容 | 不兼容 | 不兼容 | 不兼容 |
锁定的资源是什么?
认识了mySQL中的各种锁,我们来看下,mySQl锁,到底锁住的是什么?我们猜想mySQL锁,锁住的资源是主键。看下面的SQL语句:
会话A | 会话B |
---|---|
begin; | begin; |
update test set a=1111 where a =1; | update test set b=111 where b=1;–阻塞等待 |
Query OK, 1 row affected (0.02 sec) | |
Rows matched: 1 Changed: 1 Warnings: 0 |
表test 有2个索引,一个是聚集索引(a),另一个是普通索引(b)。我们知道InnoDB的普通索引的叶子节点保存的是主键Id。所以普通索引的查找需要回表操作。如果这时主键被其他事务加锁,这当前会话则进入阻塞等待。