druid連接泄露故障分析
- 2021 年 11 月 8 日
- 筆記
1、問題的如何發生的
1.1、應用功能介紹
- 系統是一個雙數據源雙寫單獨的服務。(兩個數據源是不同的存儲,所以無法使用主從複製的模式,是一個切換存儲介質的過渡態)。
- 歷史程式碼有個更新邏輯update xx set a=b where m=n。但是這個表中的記錄超10億。遇到需要更新的記錄比較多的場景下存在問題。故對這個進行了sql優化。採用的邏輯是查詢出需要更新的記錄id,然後分頁更新。
1.2、關鍵程式碼
雙數據源操作
private Object runSql(List<String> sqlSessionFactotyBeanNameList, MethodInvocation invocation)
throws InvocationTargetException, IllegalAccessException {
List<SqlSession> sqlSessionList = Lists.newArrayList();
Object result = null;
try {
for (String sessionFactotyBeanName : sqlSessionFactotyBeanNameList) {
SqlSessionFactory sqlSessionFactory =
RgApplicationContextUtil.getBean(
sessionFactotyBeanName, SqlSessionFactory.class);
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
Object mapper = sqlSession.getMapper(invocation.getMethod().getDeclaringClass());
Object[] param = invocation.getArguments();
result = invocation.getMethod().invoke(mapper, param);
sqlSessionList.add(sqlSession);//問題程式碼,注意!!!!
sqlSession.commit();
}
} catch (Exception ex) {
sqlSessionList.stream()
.forEach(
x -> {
x.rollback();
});
} finally {
sqlSessionList.stream()
.forEach(
x -> {
x.close();
});
}
return result;
}
問題的sql
<select id="getBatchIdWithLimit" resultType="java.lang.Long">
SELECT x.id FROM context x WHERE x.oid = #{oid} ORDER BY id ASC
LIMIT #{offset}, #{limit}
</select>
關鍵的配置
maxWait 獲取連接時最大等待時間,單位毫秒。配置了maxWait之後,預設啟用公平鎖,並發效率會有所下降,如果需要可以通過配置useUnfairLock屬性為true使用非公平鎖。
當前系統此參數未進行配置,所以會無限等待,使用的是公平鎖
1.3、問題出現的步驟
- sql中存在問題,部分數據的長度超過Integer的最大值(2147483647),映射存在問題。
- 雙數據源程式碼存在bug。 List
的程式碼結合的 add 位置過於落後,導致反射出現異常的時候。當次的SqlSession未關聯到待處理的集合中,進而也就未rollback和close。造成鏈接泄露。 - 當出現問題的數據的時候,結合雙數據源的程式碼的bug。會造成List
為空,所以未進行釋放操作,(鏈接泄露了) - 當前系統最大的連接數是100,出現了100次這樣的數據,這個服務就回無盡的等待獲取鏈接中的狀態。
1.4、問題的表象
2、如何復現問題
2.1、問題數據復現
- 把資料庫的最大連接數調整成1,maxWaitTime不設置
- 構造一條id大於2147483647的數據
- 使用api 觸發調用到這個邏輯
- 結果是:第一次調用報錯,第二次調用會卡的客戶端設置的超時時間。
2.2、資料庫連接異常復現
還有一種路徑是程式碼都沒問題,但是由於高並發造成資料庫是鎖。mybatis是可以設置sql的執行時長的。一旦出現了這種場景。問題也是會出現的。
但是這種場景比較難以復現,那麼有沒有一種手段可以高效的偽造這個場景。
準備知識
set autocommit=0; //關閉數據的事務自動提交
SELECT * FROM xxx a WHERE a.id='111' for update; //獲取資料庫的行鎖
commit;//提交事務
數據默認是自動提交的,所以前置set autocommit=0;這個操作不要忘記了,踩過幾次坑。完成後執行commit;進行解鎖。
測試完畢記得set autocommit=1;來恢復資料庫的事務自動提交的特性。
- 準備一條介面測試用的數據
- 執行sql select ..for update 進行行記錄鎖定
- 介面調用使用同一個id進行請求。因為記錄鎖定了,所以api的更新是失敗了,成功的偽造了高並發形成了行鎖造成的sql問題
3、問題總結
- 資料庫的保護配置:maxActive、maxWait都配置上,相當於熔斷保護
- mybatis對象映射需要關注數據的範圍
- 利用select for update製造行鎖偽造高並發造成的數據問題