Mybatis的parameterType造成線程阻塞問題分析

来源:https://www.cnblogs.com/jingdongkeji/archive/2023/06/08/17467035.html
-Advertisement-
Play Games

最近在新發佈某個項目上線時,每次重啟都會收到機器的 CPU 使用率告警,查看對應監控,持續時長達 5 分鐘,對於服務重啟有很大風險。而該項目有非常多 Consumer 消費,服務啟動後會有大量線程去拉取消息處理邏輯,通過多次 Jstack 輸出線程快照發現有很多 BLOCKED 狀態線程,此文主要記... ...


一、前言

最近在新發佈某個項目上線時,每次重啟都會收到機器的 CPU 使用率告警,查看對應監控,持續時長達 5 分鐘,對於服務重啟有很大風險。而該項目有非常多 Consumer 消費,服務啟動後會有大量線程去拉取消息處理邏輯,通過多次 Jstack 輸出線程快照發現有很多 BLOCKED 狀態線程,此文主要記錄分析 BLOCKED 原因。

二、分析過程

2.1、初步分析

"consumer_order_status_jmq1714_1684822992337" #3125 daemon prio=5 os_prio=0 tid=0x00007fd9eca34000 nid=0x1ca4f waiting for monitor entry [0x00007fd1f33b5000]
   java.lang.Thread.State: BLOCKED (on object monitor)
    at java.util.concurrent.ConcurrentHashMap.putVal(ConcurrentHashMap.java:1027)
    - waiting to lock <0x000000056e822bc8> (a java.util.concurrent.ConcurrentHashMap$Node)
    at java.util.concurrent.ConcurrentHashMap.put(ConcurrentHashMap.java:1006)
    at org.apache.ibatis.type.TypeHandlerRegistry.getJdbcHandlerMap(TypeHandlerRegistry.java:234)
    at org.apache.ibatis.type.TypeHandlerRegistry.getTypeHandler(TypeHandlerRegistry.java:200)
    at org.apache.ibatis.type.TypeHandlerRegistry.getTypeHandler(TypeHandlerRegistry.java:191)
    at org.apache.ibatis.mapping.ParameterMapping$Builder.resolveTypeHandler(ParameterMapping.java:128)
    at org.apache.ibatis.mapping.ParameterMapping$Builder.build(ParameterMapping.java:103)
    at org.apache.ibatis.builder.SqlSourceBuilder$ParameterMappingTokenHandler.buildParameterMapping(SqlSourceBuilder.java:123)
    at org.apache.ibatis.builder.SqlSourceBuilder$ParameterMappingTokenHandler.handleToken(SqlSourceBuilder.java:67)
    at org.apache.ibatis.parsing.GenericTokenParser.parse(GenericTokenParser.java:78)
    at org.apache.ibatis.builder.SqlSourceBuilder.parse(SqlSourceBuilder.java:45)
    at org.apache.ibatis.scripting.xmltags.DynamicSqlSource.getBoundSql(DynamicSqlSource.java:44)
    at org.apache.ibatis.mapping.MappedStatement.getBoundSql(MappedStatement.java:292)
    at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:83)
	at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:61)
	at com.sun.proxy.$Proxy232.query(Unknown Source)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:148)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:141)
	at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:77)
	at sun.reflect.GeneratedMethodAccessor160.invoke(Unknown Source)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)
	at com.sun.proxy.$Proxy124.selectOne(Unknown Source)
	at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:166)
	at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:82)
	at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
        ......

通過對服務連續間隔 1 分鐘使用 Jstack 抓取線程快照,發現存在部分線程是 BLOCKED 狀態,通過堆棧可以看出,當前線程阻塞在 ConcurrentHashMap.putVal,而 putVal 方法內部使用了 synchronized 導致當前線程被 BLOCKED,而上一級是 Mybaits 的TypeHandlerRegistry,TypeHandlerRegistry 的作用是記錄 Java 類型與 JDBC 類型的相互映射關係,例如 java.lang.String 可以映射 JdbcType.CHAR、JdbcType.VARCHAR 等,更上一級是 Mybaits 的 ParameterMapping,而 ParameterMapping 的作用是記錄請求參數的信息,包括 Java 類型、JDBC 類型,以及兩種類型轉換的操作類 TypeHandler。通過以上信息可以初步定位為在併發情況下 Mybaits 解析某些參數導致大量線程被阻塞,還需繼續往下分析。

我們可以先回想下 Mybatis 啟動載入時的大致流程,查看下流程中哪些地方會操作 TypeHandler,會使用 ConcurrentHashMap.putVal 進行緩存操作?

在 Mybatis 啟動流程中,大致分為以下幾步:

1、XMLConfigBuilder#parseConfiguration() 讀取本地XML文件

2、XMLMapperBuilder#configurationElement() 解析XML文件中的 select|insert|update|delete 標簽

3、XMLMapperBuilder#parseStatementNode() 開始解析單條 SQL,包括請求參數、返回參數、替換占位符等

4、SqlSourceBuilder 組合單條 SQL 的基本信息

5、SqlSourceBuilder#buildParameterMapping() 解析請求參數

6、ParameterMapping#getJdbcHandlerMap() 解析 Java 與 JDBC 類型,並把映射結果放入緩存

而在第 6 步時候(圖中標色),會去獲取 Java 對象類型與 JDBC 類型的映射關係,並把已經處理過的映射關係 TypeHandler 存入本地緩存中。但是堆棧信息顯示,還是觸發了 TypeHandler 入緩存的操作,也就是某個 paramType 並沒有命中緩存,而是在 SQL 查詢的時候實時解析 paramType,在高併發情況下造成了線程阻塞情況。下麵繼續分析下 sql xml 的配置:

<select id="listxxxByMap" parameterType="java.util.Map" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from xxxxx
        where business_id = #{businessId,jdbcType=VARCHAR}
        and template_id = #{templateId,jdbcType=INTEGER}
    </select>

代碼請求:

Map<String, Object> params = new HashMap<>();
params.put("businessId", "11111");
params.put("templateId", "11111");
List<TrackingInfo> result = trackingInfoMapper.listxxxByMap(params);

初步看沒發現問題,但是我們在入 TypeHandler 緩存時 debug 下,分析下哪種類型在緩存中缺失?

從 debug 信息中可以看出,TypeHandler 緩存中存在的是 interface java.util.Map,而 SQL 執行時傳入的是 class java.util.HashMap,導致並沒有命中緩存。那我們修改下 xml 文件為 parameterType="java.util.HashMap" 是不是就解決了?

很遺憾,部署後仍然存在問題。

2.2、進一步分析

為了進一步分析,引入了對照組,而對照組的 paramType 為具體 JavaBean。

<select id="listResultMap" parameterType="com.jdwl.xxx.domain.TrackingInfo" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from xxxx
        where business_id = #{businessId,jdbcType=VARCHAR}
        and template_id = #{templateId,jdbcType=INTEGER}
    </select>

對照組代碼請求

TrackingInfo record = new TrackingInfo();
record.setBusinessId("11111");
record.setTemplateId(11111);
List<TrackingInfo> result = trackingInfoMapper.listResultMap(record);

在裝載參數的 Handler 類 org.apache.ibatis.scripting.defaults.DefaultParameterHandler#setParameters 處進行 debug 分析。

2.2.1、對照組為 listResultMap(paramType=JavaBean)

兩個參數的解析類型分別為 StringTypeHandler(紅框中灰色的字)與 IntegerTypeHandler(紅框中灰色的字),已經是 Mybatis 提供的 TypeHandler,並沒有再進行類型的二次解析。說明 JavaBean 中的 businessId、templateId 欄位已經在啟動時候被預解析了。

2.2.2、實驗組為listxxxByMap(paramType=Map)

兩個參數的解析都是 UnknownTypeHandler(紅框中灰色的字),而在 UnknownTypeHandler 中會再次調用 resolveTypeHandler() 方法,對參數進行類型的二次解析。可以理解為 Map 里的屬性不是固定類型,只能在執行 SQL 時候再解析一次。

最後修改為 paramType=JavaBean 部署測試環境再抓包,並未發現 TypeHandlerRegistry 相關的線程阻塞。

三、引申思考

既然 paramType 傳值會出現阻塞問題,那 resultType 與 resultMap 是不是有相同問題呢?繼續分為兩個實驗組:

1、對照組(resultMap=BaseResultMap)

<resultMap id="BaseResultMap" type="com.jdwl.tracking.domain.TrackingInfo">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="template_id" property="templateId" jdbcType="INTEGER"/>
        <result column="business_id" property="businessId" jdbcType="VARCHAR"/>
        <result column="is_delete" property="isDelete" jdbcType="TINYINT"/>
        <result column="create_time" property="createTime" jdbcType="TIMESTAMP"/>
        <result column="update_time" property="updateTime" jdbcType="TIMESTAMP"/>
        <result column="ts" property="ts" jdbcType="TIMESTAMP"/>
    </resultMap>

<select id="listResultMap" parameterType="com.jdwl.tracking.domain.TrackingInfo" resultMap="BaseResultMap">
        select
        <include refid="Base_Column_List"/>
        from tracking_info
        where business_id = #{businessId,jdbcType=VARCHAR}
        and template_id = #{templateId,jdbcType=INTEGER}
    </select>

對照組代碼請求:

TrackingInfo record = new TrackingInfo();
record.setBusinessId("11111");
record.setTemplateId(11111);
List<TrackingInfo> result1 = trackingInfoMapper.listResultMap(record);

2、實驗組(resultType=JavaBean)

<select id="listResultType" parameterType="com.jdwl.tracking.domain.TrackingInfo" resultType="com.jdwl.tracking.domain.TrackingInfo">
        select
        <include refid="Base_Column_List"/>
        from tracking_info
        where business_id = #{businessId,jdbcType=VARCHAR}
        and template_id = #{templateId,jdbcType=INTEGER}
    </select>

實驗組代碼請求:

TrackingInfo record = new TrackingInfo();
record.setBusinessId("11111");
record.setTemplateId(11111);
List<TrackingInfo> result2 = trackingInfoMapper.listResultType(record);

在對返回結果 Handler 處理類 org.apache.ibatis.executor.resultset.DefaultResultSetHandler#createAutomaticMappings() 進行 debug 分析。

1、對照組(resultMap=BaseResultMap)

List unmappedColumnNames 長度為 0,表示所有欄位都命中了 標簽配置,符合預期。

2、實驗組(resultType=JavaBean)

List unmappedColumnNames 長度為 11,表示所有欄位都在 標簽配置中未找到。這是因為 SQL 執行後的 resultMap 對應的 id 並不等於標簽的 id,所以這些欄位被標識為未解析,又會執行 TypeHandlerRegistry 的類型映射邏輯,引發併發時線程阻塞問題。

四、總結

1、在使用 paramType 時,xml 配置的類型需要與 Java 代碼中傳入的一致,使用 Mybatis 預載入時的類型緩存。

2、在使用 paramType 時,避免使用 java.util.HashMap 類型,避免 SQL 執行時解析 TypeHandler。

3、在接受返回值時,使用 resultMap,提前映射返回值,減少 TypeHandler 解析。

五、後續

在 Mybatis 社區已經優化了 TypeHandler 入緩存的邏輯,可以解決重覆計算 TypeHandler 問題,一定程度上緩解以上問題。但是 Mybatis 修複最低版本為 3.5.8,依賴 spring5.x,而我們項目使用的 Mybatis3.4.4,spring4.x,直接升級會存在一定風險,所以在不升級情況下,按照總結規範使用也可以降低阻塞風險。

TypeHandler 相關issue:https://github.com/mybatis/mybatis-3/pull/2300/commits/8690d60cad1f397102859104fee1f6e6056a0593

作者:京東物流 鐘凱

來源:京東雲開發者社區


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 某日二師兄參加XXX科技公司的C++工程師開發崗位第9面: > 面試官:C++中,設計一個類要註意哪些東西? > > 二師兄:設計一個類主要考慮以下幾個方面:1.面向對象的封裝、繼承及多態。2.`big three`或者`big five`。3.運算符和函數重載、靜態成員、友元、異常處理等相關問題。 ...
  • 哈嘍大家好,我是鹹魚 好久沒更新 python 爬蟲相關的文章了,今天我們使用 selenium 模塊來簡單寫個爬蟲程式——爬取某東網商品信息 網址鏈接:https://www.jd.com/ 完整源碼在文章最後 ## 元素定位 我們需要找到網頁上元素的位置信息(xpth 路徑) ![image]( ...
  • ## 前言 在C語言中,枚舉是一種方便組織和表示一組相關常量的工具。枚舉類型有助於提高代碼的可讀性和可維護性。本文將介紹C語言枚舉的基本概念、語法和用法,以及一些高級技巧。 ## 一、人物簡介 - 第一位閃亮登場,有請今後會一直教我們C語言的老師 —— 自在。 ![](https://img2023 ...
  • | static基本知識 | header | | | | | | | 類名.靜態成員變數(推薦) 同一個類中靜態成員變數的訪問可以省略類名。 1.靜態成員變數(有static修飾,屬於類、載入一次,可以被共用訪問),訪問格式 類名.靜態成員變數(推薦) 對象.靜態成員變數(不推薦)。 2.實例成員 ...
  • rust 的運行速度、安全性、單二進位文件輸出和跨平臺支持使其成為構建命令行程式的最佳選擇。 實現一個命令行搜索工具`grep`,可以在指定文件中搜索指定的字元串。想實現這個功能呢,可以按照以下邏輯流程處理: 1. 獲取輸入文件路徑、需要搜索的字元串 2. 讀取文件; 3. 在文件內容中查找字元串所 ...
  • 現在Austin的文檔我覺得還是比較全的,但到了看代碼的時候,可能有的同學就不知道應該怎麼看,有想知道模塊之間的調用鏈路,有想一點一點把細節給全看了。這時候就很可能在項目里犯迷糊了,繞不出不來了。 > **Java開源項目消息推送平臺🔥推送下發【郵件】【簡訊】【微信服務號】【微信小程式】【企業微信 ...
  • ## 實體類中嵌套Enum類型並想轉換成JSON字元串時遇到的問題。 先說明問題的產生,在自己寫著玩的時候,新建了一個**User**類如下: ```java package com.ma.xdo; import lombok.*; import java.io.Serializable; /** ...
  • # Rust - 介面設計建議之不意外(unsurprising) 書:Rust for Rustaceans ## Rust介面設計的原則(建議) - 四個原則: - 不意外(unsurprising) - 靈活(flexible) - 顯而易見(obvious) - 受約束(constraine ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...