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、問題的表象

image-20210601165753462

程式碼的問題點

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製造行鎖偽造高並發造成的數據問題