樓下小黑哥 小黑十一點半前端
小黑十一點半
推薦搜索
支付java開發踩坑java
Hello,你們好,我是樓下小黑哥~sql
2021 年立刻就要來,小黑哥沒文化,就簡單一點:
「就祝你們 2021 年 bug 少點,頭髮多點,績效高點,年年 375~」數據庫
ps:文末有彩蛋,能夠獲取 2020 年 Github 年度報告,看看這一年你在上面幹了些啥~編程
好了,進入今天的正文,今天想跟你們聊聊一次 mybatis 動態 SQL 引起的生產事故。mybatis
事情這樣的,咱們有個訂單相關數據庫服務,專門負責訂單相關的增刪改查。這個服務運行了好久,一直都沒有問題。app
直到某天中午,正想躺下休息一下,就忽然接到系統報警,大量訂單建立失敗。訂單服務能夠說是核心服務,這個服務不可用,整個流程都會被卡主,交易都將會失敗。運維
立刻沒了睡意,馬上起來登上生產運維機,查看訂單服務的系統日誌。編程語言
Caused by: java.util.concurrent.RejectedExecutionException: Thread pool is EXHAUSTED! Thread Name: DubboServerHandler-xxip, Pool Size: 200 (active: 200, core: 200, max: 200, largest: 200), Task: 165633 (completed: 165433), Executor status:(isShutdown:false, isTerminated:false, isTerminating:false), in 1! at com.alibaba.dubbo.common.threadpool.support.AbortPolicyWithReport.rejectedExecution(AbortPolicyWithReport.java:53) at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:768) at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:656) at com.alibaba.dubbo.remoting.transport.dispatcher.all.AllChannelHandler.caught(AllChannelHandler.java:65)
如上所示,日誌中打印大量的 Dubbo 線程池線程耗盡,直接拒絕服務調用的日誌。登上另外一臺機器,好傢伙,除了上述日誌之外,仔細翻看竟然還發生 「OOM」!!!ide
其實發生 「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 物理刪除數據,這時若是條件丟失將會致使全表數據被刪。數據若是都沒了,沒什麼好說了,跑路吧~
能夠看到,更新/刪除這類動態 sql,若是丟失了條件,那致使的危害將會很大,業務可能都會被停擺。
那有沒有什麼辦法解決這些問題?
「很簡單,不要用動態 sql 了,直接手寫吧~」
emm!大家先把刀放下,我開個玩笑的~
雖然上面的問題確實是動態 sql 引發的,可是本質緣由我以爲仍是使用不當引發的。
咱們確定不能因噎廢食,自廢武功,今後退回到「刀耕火種」時代,手寫 sql。
好了,不說廢話了,解決動態 sql 帶來潛在的問題,我以爲能夠從兩方面下手:
第1、改變意識形態,科普動態 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})}) @Slf4j public 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 的過程當中更加當心。
好了,我是樓下小黑哥,一位資深的踩坑工程師。
2020 年即將結束,這是特別的一年,相信每一個人都有本身不平凡的經歷
做爲開發者的咱們,到了年末也迎來了盤點一年收穫的時刻
GitHub 做爲全球最大的開源協做平臺,相信很多開發者都貢獻過本身的代碼,做爲開發者的你是否對如下問題好奇?
本篇文章將給你所有答案!2020 年度 GitHub 代碼報告即將上演!
領取方式:
「關注公衆號「小黑十一點半」,回覆「報告」便可領取,還能夠查看 GitHub 報告排行榜」
嘿嘿,下面是小黑哥的報告,這一年爲了寫文章,搞了幾個開源工程,仍是提交挺多代碼的。
「[吃瓜]提及來,今年最大的成就,給 Dubbo 改過一個bug,修改了兩處開源文檔的。」