不合理的执行顺序引发的死锁
问题现象
程序上线后运行反馈总会提示死锁,日志大量出现java.sql.BatchUpdateException: 批处理中出现错误: ORA-00060: 等待资源时检测到死锁
根据具体的异常堆栈信息找到对应的代码行,发现某个Service层有类似如下代码片段:【方法上有切面事务】
public void batchUpdate(List<Map<String,Object>> list){
String[] sqlArray = list.stream().map(map -> "update tb_test set ct = ct +1 where id='" + MapUtils.getString(map, "id") + "'").toArray(String[]::new);
jdbcTemplate.batchUpdate(sqlArray);
}
先不去纠结语法安全性问题,是否会有其他问题呢?
集合list是由外部传入的,然后这里根据外部传入形成sql,之后批量执行。现在偶发就会报上面的异常,提示死锁。因为外部的集合中的顺序是非固定的,也就是说并不是每次按照固定顺序传入的,那么形成的sql顺序也是随机的
比如:
线程T1传入的集合中id顺序为1,2,3;同一时刻
线程T2传入的集合中id顺序为3,1,2。此时就有可能会因为锁争用问题导致相互等待形成死锁,直到oracle自动检测并牺牲掉一个锁,然后另一个执行成功。
另外,根据版本信息查看到早前这里的代码是多了一行的,只不过由于业务调整用不到,注释了,也就是未注释之前是不会产生死锁的【因为串行执行了】
public void batchUpdate(String id,List<Map<String,Object>> list){
//早前是有这一行的。注意id是写死的...也就是无论多少个线程进来,只要当前事务没有完成(提交或者回滚),其他都得排队,其实就是串行了。。。当然不会死锁了。巧合编程。瞎写呗。
jdbcTemplate.update("update tb_test2 set lk=1 where id=10018");
String[] sqlArray = list.stream().map(map -> "update tb_test set ct = ct +1 where ct<100 and id='" + MapUtils.getString(map, "id") + "'").toArray(String[]::new);
jdbcTemplate.batchUpdate(sqlArray);
}
解决办法
- 按照要修改的主键ID进行排序,保证每次顺序都一致
public void batchUpdate(List<Map<String,Object>> list){
String[] sqlArray = list.stream().sorted(Comparator.comparing(m -> MapUtils.getString(m, "id", "")))
.map(map -> "update tb_test set ct = ct +1 where ct<100 and id='" + MapUtils.getString(map, "id") + "'").toArray(String[]::new);
jdbcTemplate.batchUpdate(sqlArray);
}
- 使用NamedJdbcTemplate
public void batchUpdate(List<Map<String,Object>> list){
//TODO 校验集合,且保证传入id,
String sql = "update tb_test set ct = ct +1 where ct<100 and id=:id";
namedjdbcTemplate.batchUpdate(sql,list.stream().sorted(Comparator.comparing(map-> MapUtils.getString(map,"id"))).collect(Collectors.toList()));
}
注意事项
通常一个update语句执行完成,会返回受影响行数,有时候简单业务判断会依赖于这个返回值判断是否执行成功。
这种情况下,推荐使用非0判断,而不是去取具体返回的值。因为在有些数据库中,比如oracle,返回的-2也是表示成功的(也就是说返回-2、1或所有正整数都属于正常返回)
if(counts[i]==0) throw new BusException("XXXX异常了");
其他说明
需要注意的是事务和锁是两个概念,并不是说只要事务开启,那么在当前事务中的所有改动和删除的语句都会加锁,其他事务也开始排队,并不是这样子,事务只是把执行列表放到了同一个执行单元,要么都失败要么都成功。锁是在执行具体语句的时候才去加上,这里说的是数据库层面的。比如同一个事务并发会允许多个线程同时执行,但是未必每个线程都能获取到数据库的锁。