首頁>技術>

前言

好了,進入今天的正文,今天想跟大家聊聊一次 mybatis 動態 SQL 引發的生產事故。

事情這樣的,我們有個訂單相關資料庫服務,專門負責訂單相關的增刪改查。這個服務運行了很久,一直都沒有問題。

直到某天中午,正想躺下休息一下,就突然接到系統報警,大量訂單建立失敗。訂單服務可以說是核心服務,這個服務不可用,整個流程都會被卡主,交易都將會失敗。

馬上沒了睡意,立刻起來登上生產運維機,檢視訂單服務的系統日誌。

Causedby: java.util.concurrent.RejectedExecutionException: ThreadpoolisEXHAUSTED! ThreadName: DubboServerHandler-xxip, PoolSize: 200 (active: 200, core: 200, max: 200, largest: 200), Task: 165633 (completed: 165433), Executorstatus:(isShutdown:false, isTerminated:false, isTerminating:false), in 1!        atcom.alibaba.dubbo.common.threadpool.support.AbortPolicyWithReport.rejectedExecution(AbortPolicyWithReport.java:53)        atjava.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:768)        atjava.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:656)        atcom.alibaba.dubbo.remoting.transport.dispatcher.all.AllChannelHandler.caught(AllChannelHandler.java:65)

如上所示,日誌中列印大量的 Dubbo 執行緒池執行緒耗盡,直接拒絕服務呼叫的日誌。登上另一臺機器,好傢伙,除了上述日誌以外,仔細翻看居然還發生 「OOM」!!!

其實發生 「OOM」 了,問題倒是簡單了,首先 「dump」 一下,然後分析一下生成的日誌,查詢記憶體佔用最大類,然後分析定位具體程式碼塊。

結合系統日誌以及 dump 日誌,我們很快就定位到發生問題的程式碼位置,樣例程式碼如下:

Order order=new Order();log.info("訂單查詢引數資訊:{}",order);// 其他系統邏輯,關鍵資訊資料加密等List<Order> orderList = orderMapper.query(order);// .. 其他查詢邏輯

查詢底層使用 mybatis 動態 sql 功能,樣例如下:

<select id="query" parameterType="order" resultMap="orderResultMap">    select orderId,amt,orderInfo // 還有其他資訊    from    Order    <where>        <if test="orderId != null">            orderId = #{orderId}        </if>        <if test="amt != null">            AND amt = #{amt}        </if>       ..... 其他條件    </where></select>

上面的程式碼很簡單,由於傳入 mybatis 查詢語句引數都未設定,從而導致生成的 sql 缺失了查詢條件了,查詢全表。

而由於訂單表的資料非常多,全表查詢返回的資料將會源源不斷的載入到應用記憶體中,從而引發 「Full GC」,導致應用陷入長時間的 「stop the world」

由於 Dubbo 執行緒也被暫停了,接收到正常的呼叫無法及時返回結果,從而引發服務消費者超時。

另一方面,由於應用不斷接受請求,而大量 Dubbo 執行緒不能及時處理呼叫,從而導致 Dubbo 執行緒池中執行緒資源被耗盡,後續請求將會被直接拒絕。

最後最後,系統應用記憶體實在無法再載入任何資料,於是丟擲上文中 「OOM」 異常。

這張圖真的體現小黑哥當時心態變化

問題本質原因是找到了,那為什麼之前查詢都沒事,而這次突然就沒傳值了呢?

原來是因為前端頁面改動,導致傳入的查詢引數為空!!!

前端頁面遲遲不能顯示查詢的訂單,使用者一般會選擇重試,然後又未傳入查詢引數,再一次加重應用的情況,雪上加霜。

擴充套件思考

上面的問題,我們只要重啟應用,暫時還是能解決問題。想象一下如果使用動態 sql 發生在其他場景,會怎麼樣?

假設使用者的餘額表使用動態 sql 更新,這時如果條件丟失將會導致全部使用者的餘額都會發生了變化。如果是餘額變多,那可能還好。但是如果餘額是變少的,那真的很可能演變成社會事故了~

解決辦法

那有沒有什麼辦法解決這些問題?

「很簡單,不要用動態 sql 了,直接手寫吧~」

emm!你們先把刀放下,我開個玩笑的~

雖然上面的問題確實是動態 sql 引起的,但是本質原因我覺得還是使用不當引起的。

我們肯定不能因噎廢食,自廢武功,從此退回到「刀耕火種」時代,手寫 sql。

好了,不說廢話了,解決動態 sql 帶來潛在的問題,我覺得可以從兩方面下手:

第一、改變意識形態,科普動態 sql 可能引發的問題,讓所有開發對這個問題引起重視。

只有當我們意識動態 sql 可能引發的問題,我們才有可能在開發過程去思考,這麼寫會不會被帶來問題。

「這一點,我覺得真的很重要。」

第二,針對實際的業務場景提供可控的查詢條件,並且對外介面一定要做好必要的引數校驗。

我們要從實際的業務場景出發分析對外需要提供那些條件,原則上主庫表必須按照主鍵或唯一鍵查詢單條,或者使用相關的外來鍵查詢多條。比如說,訂單表查詢支付單號這類主鍵查詢。

另外針對這些查詢條件,介面層一定要做好的必要的引數校驗。如果引數未傳,直接打回,防患於未然。

如果真的有需要查詢多條資料後臺需求,這類查詢不需要很高實時性,那麼我們其實可以與上面應用查詢剝離開來,並且查詢使用從庫。

第三,增加一些工具類預防外掛。

比如我們可以在 mybatis 增加一個外掛,檢查執行的 sql 是否帶有 where 關鍵字,若不存在直接攔截。

mybatis 攔截器如下:

@Intercepts({        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,                RowBounds.class, ResultHandler.class}),        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class,                RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class,                Object.class})})@Slf4jpublic class CheckWhereInterceptor implements Interceptor {    private static final String WHERE = "WHERE";    @Override    public Object intercept(Invocation invocation) throws Throwable {        //獲取方法的第0個引數,也就是MappedStatement。@Signature註解中的args中的順序        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];        //獲取sql命令操作型別        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();        final Object[] queryArgs = invocation.getArgs();        final Object parameter = queryArgs[1];        BoundSql boundSql = mappedStatement.getBoundSql(parameter);        String sql = boundSql.getSql();        if (Objects.equals(SqlCommandType.DELETE, sqlCommandType)                || Objects.equals(SqlCommandType.UPDATE, sqlCommandType)                || Objects.equals(SqlCommandType.SELECT, sqlCommandType)) {            //格式化sql            sql = sql.replace("\n", "");            if (!StringUtils.containsIgnoreCase(sql, WHERE)) {                sql = sql.replace(" ", "");                log.warn("SQL 語句沒有where條件,禁止執行,sql為:{}", sql);                throw new Exception("SQL語句中沒有where條件");            }        }        Object result = invocation.proceed();        return result;    }    @Override    public Object plugin(Object target) {        return Plugin.wrap(target, this);    }    @Override    public void setProperties(Properties properties) {    }}

上面的程式碼其實還是比較粗糙,各位可以根據各自的業務增加相應的預防措施。

小結

今天的文章,從真實的例子出發,引出了動態 sql 潛在的問題,主要想讓大家意識到這方面的問題。從而在今後使用動態 sql 的過程中更加小心。

作者|後端技術漫談|OSCHINA

19
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 使用註解配置AOP