不合理的執行順序引發的死鎖

冰雪女娲發表於2024-04-22

問題現象

程式上線後執行反饋總會提示死鎖,日誌大量出現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);
    }

解決辦法

  1. 非得要拼接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);
    }
  1. 使用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或所有正整數都屬於正常返回)

if(counts[i]==0) throw new BusException("XXXX異常了");

相關文章