一个服务有多个二进制,每个二进制有对应的编译信息,判断编译信息不存在时,则插入数据。
执行的时候却发现,对一个服务的多个二进制的编译信息进行插入时,发生了死锁。
抛开业务属性,对于表building
CREATE TABLE `building` (
`appid` int(11) DEFAULT 0,
`buildid` int(11) DEFAULT 0,
KEY `idx1` (`appid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
执行以下语句时,发生了死锁:
begin
select * from building where appid = 5 for update;
// 判断结果为空,则插入数据
insert into building values (5, 1);
commit
即,并发执行select for update + insert时导致死锁
首先,使用的MySQL版本为5.7.18
,使用了默认的隔离级别可重复读(Repeatable read)
。 假设执行语句前数据库里有如下数据:
appid(有索引,起外键作用) | Build-id |
---|---|
10 | 111 |
20 | 222 |
30 | 333 |
执行过程如下
transaction1 | transaction2 |
---|---|
begin // 1 | begin // 2 |
select * from building where appid = 5 for up date; // 3, 成功执行 | select * from building where appid = 6 for up date; // 4, 成功执行 |
insert into building values (5, 444) // 5, 卡住 | insert into building values (6, 555) // 6, 死锁 |
3
和4
同时持有了gap
锁,非互斥,都到了下一步5
的插入意向锁与步骤4的gap锁冲突,卡住6
的插入意向锁和步骤3的gap
锁冲突,卡住,但是数据库发现有环,直接报错死锁ps
: 步骤6报错并回滚之后,transaction 1
是可以执行的。(Read Committed)
,就不会有gap lock
的问题【备注:未解决业务问题】insert ignore
,insert ... on duplicate key update
【备注:实际情况复杂一些,不只是appid
单列的唯一索引】3
和4
在where
条件一样的时候就会互斥么?(gap lock)
和插入意向锁(intert intention lock)
作用到底是什么?appid
的并发插入问题(即上述例子中, 步骤4
的 where
语句条件为appid=5
),能正确执行么?隔离级别 | 脏读(Dirty Read) | 不可重复读(NonRepeatable Read) | 幻读(Phanto) |
---|---|---|---|
未提交读(Read Uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read Committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable Read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable) | 不可能 | 不可能 | 不可能 |
一般来讲,上表中从上到下按照隔离的级别由低到高,越高的隔离,效率越差。
select
操作得到的结果所表征的数据状态无法支撑后续的业务操作。更为具体一些select
某记录是否存在,不存在,准备插入此记录,但执行insert时发现此记录已存在,无法插入,此时就发生了幻读。设置隔离级别为Read Committed。
transaction 1 | transaction 2 |
---|---|
begin // 1 | begin // 2 |
select * from building where appid = 5 // 3, 空数据 | insert into building values (5, 555); // 4, 成功执行 |
select * from building where appid = 5 // 5, 空数据 | commit // 6, 成功执行 |
select * from building where appid = 5 // 7, 数据非空 |
设置隔离级别为Repeatable Read
,将appid
改为唯一索引。
transaction 1 | transaction 2 |
---|---|
begin // 1 | begin // 2 |
select * from building where appid = 40 // 3, 没有for update 空数据 | insert into building values (40, 444); // 4 成功执行 |
select * from building where appid = 40 // 5, 空数据 | commit; // 6. 成功 |
select * from building where appid = 40 // 7, 空数据 | |
insert into building values (40, 444) // 8, 报错Duplic ate entry '40' |
表锁:
DDL
(数据定义语言)处理时使用,也可以自己在SQL
中指定。由MySQL
服务器实现。行锁:
InnoDB
。行锁是加在索引上的(MyISAM
存储引擎只能使用表锁)。行锁细分(上面的问题主要就集中在这里介绍的锁上)
记录锁(Record Lock)
间隙锁(Gap Lock)
(-∞,10),(10,20),(20,30),(30,+∞)
。执行期间where
语句的条件落在了同一个间隙中,因此互相锁住。同时,值得注意的是间隙锁之间并不互斥。Next-Key Lock
Locking reads(select ... for update
或lock in share mode),Update和Delete
时,除了对唯一 索引的唯一搜索外都会获取gap锁或next-key锁。即锁住其扫描的范围。"唯一索引的唯一搜 索"表示where条件用了唯一索引且搜索到了记录。插入意向锁(Insert Intention Locks)
Insert
的时候。插入意向锁只会和间隙锁或Next-Key
锁冲突,和插入意向锁并不冲突。事务A想对数据R获取(row)
的锁,但是数据已持有(col)
的锁时的兼容性。
已持有的锁\想获取的锁 | Record | Gap | Next-Key | Insert Intention |
---|---|---|---|---|
Record | 0 | 1 | 0 | 1 |
Gap | 1 | 1 | 1 | 0 |
Next-Key | 0 | 1 | 0 | 0 |
Insert Intention | 1 | 1 | 1 | 1 |
第一列表示已经有的锁,第二列到第五列表示待加的锁。
注意此矩阵并不对称。
主要是一个事务已经获取了插入意向锁,对其他事务是没有任何影响的。而一个事务想要获取插入意向锁,如果有其他事务已经加了间隙锁或
Next-Key
锁,则会阻塞.
读锁(S)
写锁(X)
读意向锁(IS)
写意向锁(IX)
已持有的锁\想获取的锁 | X | IX | S | IS |
---|---|---|---|---|
X | 0 | 0 | 0 | 0 |
IX | 0 | 1 | 0 | 1 |
S | 0 | 0 | 1 | 1 |
IS | 0 | 1 | 1 | 1 |
select @@global.tx_isolation;
select @@session.tx_isolation;
select @@tx_isolation;
set session transaction isolation level repeatable read
show engine innodb status // 结果中的TRANSACTIONS部分
select * from information_schema.innodb_locks;
RR
隔离级别及以上才有Gap lock
, Next-Key lock
。Gap lock
的范围where
条件一样的时候就会互斥么?where
中使用唯一索引且命中记录gap lock
和intert intention lock
作用到底是什么?appid
的并发插入问题(即上述例子中, 步骤4的where
语句条件为appid=5
,能正确执行么?select + insert
的本意是为了先查找某条数据(的 索引),确认其不存在,然后插入数据。没意识到查找会影响其他数据(即上面的appid = 5
和appid = 6
互相影响了)。select ... where ... for update
的初衷应该是希望对同一个索引有相互阻塞作用,讲人话就是想要在一条不存在的记录上加上互斥的读锁(如果已存在的话,insert
不会发生,不会产生额外的数据,也就无所谓了)。那么,这个情况是MySQL
能解决的么?
可这种情况确实存在怎么办呢?可能有如下的解法:
用mutex table
。
select ... for update
操作。对上面的例子来说,select * from app where id = 5 for update
即可(因为用appid=5
查building
的时候,id
为5
的app
记录已经存在了)改用唯一索引,并舍弃掉select...for update
操作。
insert ignore
即可,让MySQL
自身保证不会有重复数据。重试。
GET_LOCK / RELEASE_LOCK
。redis
等其他系统来加锁。上面的例子中方法1,2,3
应该都可以。值得注意的是,部分情况下2
不可行,例如判断如果不存在,则一次插入多条数据的情况。