问题现象
程序上线后运行反馈总会提示死锁,日志大量出现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);
}
解决办法
- 非得要拼接SQL,那就在形成SQL之前,强行按照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);
}
因为底层实现是不一样的(比如oracle的ojdbc驱动中OracleStatement、OraclePreparedStatement对于batchUpdate的实现是有区别的)。前一个是执行第一个语句的时候打开一个物理连接开始执行所有SQL,直到全部执行完成,也就是逐条执行,那这样如果每次传入的SQL顺序不一样,行锁获取和释放的顺序就不会一样。而OraclePreparedStatement是当成一个整体来执行,也就是一开始就加了锁。
注意事项
通常一个update语句执行完成,会返回受影响行数,有时候简单业务判断会依赖于这个返回值判断是否执行成功。
这种情况下,推荐使用非0判断,而不是去取具体返回的值。因为在有些数据库中,比如oracle,返回的-2也是表示成功的(也就是说返回-2、1或所有正整数都属于正常返回)