最近的项目,为了保障绝对的一致性,使用SERIALIZABLE作为隔离级别。
然后就爆出了很诡异的死锁。
报错log如下:
org.springframework.dao.DeadlockLoserDataAccessException: PreparedStatementCallback; SQL [xxxxx]; Deadlock found when trying to get lock;
try restarting transaction; nested exception is com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
1461 2017-04-17 21:58:26.550 ERROR pool-7-thread-2
我们的sql大概是这样的:
begin;
select * from table_a where id = 6;
INSERT INTO xxx (xxxxx) ON DUPLICATE KEY UPDATE xxxxx;
UPDATE table_a SET column_a = GREATEST(column_a, 0) WHERE id = 6;
commit;
报错的概率并不高,在我们的系统中,大概只有2%的概率左右出现。
单独执行这段sql并不会有任何问题,因此定位了许久。后来灵光一下,同时开启两个事务来模拟这个操作。
方式如下:
//准备工作
1 set session transaction isolation level SERIALIZABLE;
2 create table user (id int PRIMARY KEY, age int) engine = innodb;
3 insert into user values(1, 2);
分别开启两个事务
//事务A
begin;
select * from user wherer id = 1;
//A level_1
update user set age = 3 where id = 1;
//A level_2
commit;
//事务B
begin;
select * from user wherer id = 1;
//B level_1
update user set age = 3 where id = 1;
//B level_2
commit;
两个事务同时开启,当执行到level_1的时候停住,注意观察数据库的输出
这个时候一切正常
我们继续往下执行事务A,如图
我们发现事务A卡住了。
再继续执行事务B,死锁出现了。
原因:
究其原因,是SERIALIZABLE隔离级别读写锁竞争导致的。
在SERIALIZABLE级别下,不会使用mysql的mvcc机制,而是在每一个select请求下获得读锁,在每一个update操作下尝试获得写锁。
在我们的例子中,在level_1中,事务A获得了id = 1的读锁A。
而在同时,事务B获得id = 1的读锁B。
在事务A level_2时,事务A尝试获得id = 1的写锁,这个时候,由于id = 1处不仅有事务A的读锁,还有事务B的读锁,因此事务A的update操作获取锁被阻塞。
此时,当事务B继续执行update操作时,由于事务A又拥有id = 1的读锁A,因此进入互相等待状态,造成死锁。
解决方案:
1 将select操作改为select for update,直接加写锁。
2 在业务层将此种类事务串行化。