首页 > 其他分享 >Mybatis的parameterType造成线程阻塞问题分析 | 京东云技术团队

Mybatis的parameterType造成线程阻塞问题分析 | 京东云技术团队

时间:2023-06-08 14:31:37浏览次数:48  
标签:parameterType java record ibatis apache 线程 Mybatis org

一、前言

最近在新发布某个项目上线时,每次重启都会收到机器的 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的parameterType造成线程阻塞问题分析 | 京东云技术团队_CPU

在 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 下,分析下哪种类型在缓存中缺失?

Mybatis的parameterType造成线程阻塞问题分析 | 京东云技术团队_Mybatis_02

从 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)

Mybatis的parameterType造成线程阻塞问题分析 | 京东云技术团队_线程阻塞_03

两个参数的解析类型分别为 StringTypeHandler(红框中灰色的字)与 IntegerTypeHandler(红框中灰色的字),已经是 Mybatis 提供的 TypeHandler,并没有再进行类型的二次解析。说明 JavaBean 中的 businessId、templateId 字段已经在启动时候被预解析了。

2.2.2、实验组为listxxxByMap(paramType=Map)

Mybatis的parameterType造成线程阻塞问题分析 | 京东云技术团队_java_04

Mybatis的parameterType造成线程阻塞问题分析 | 京东云技术团队_Mybatis_05

两个参数的解析都是 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)

Mybatis的parameterType造成线程阻塞问题分析 | 京东云技术团队_Mybatis_06

Mybatis的parameterType造成线程阻塞问题分析 | 京东云技术团队_线程阻塞_07

List 标签配置,符合预期。

2、实验组(resultType=JavaBean)

Mybatis的parameterType造成线程阻塞问题分析 | 京东云技术团队_java_08

Mybatis的parameterType造成线程阻塞问题分析 | 京东云技术团队_java_09

List标签的 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>

作者:京东物流 张凯

来源:京东云开发者社区

标签:parameterType,java,record,ibatis,apache,线程,Mybatis,org
From: https://blog.51cto.com/u_15714439/6439925

相关文章

  • 基于springmvc+spring+mybatis+hibernate 封装的框架
    基于maven  freemarker2.3.20  spirngmvc3.2.9.RELEASE  spring3.2.9.RELEASE  Hiberante3.6.9.Final(自动建表)  mybatis3.2.7数据交互  druid连接池   cxf3.0.0发布webservice  quartz2.2.1定时任务  jquery   jquery-datata......
  • 第四十七天 线程的相关知识
    一、昨日内容回顾如何创建进程"""进程也可以理解成是内存中某一块存储空间"""#针对不同的操作系统代码创建进程的原理有区别windows以模块导入的方式linux\mac以拷贝的方式join方法主进程代码等待子进程代码运行结束之后再往后执行p.join()进程间数据隔离在同一台计......
  • 多线程中的上下文切换
    我们都知道,在并发编程中,并不是线程越多就效率越高,线程数太少可能导致资源不能充分利用,线程数太多可能导致竞争资源激烈,然后上下文切换频繁造成系统的额外开销。大量的超时报警,通过工具分析,cs指标很高,然后分析日志,发现有大量wait()相关的Exception,这个时候我们怀疑是在多线程并发处......
  • mybatis generator 使用与 mybatis 与属性转换
    mybatisgeneratorMyBatisGenerator是MyBatis提供的一个代码生成工具。可以帮我们生成表对应的持久化对象(po)、操作数据库的接口(dao)、CRUDsql的xml(mapper)。MyBatisGenerator是一个独立工具,你可以下载它的jar包来运行、也可以在Ant和maven运行。引入相关依赖......
  • 关于Java中多线程
    基本概念什么是进程-->是操作系统资源分配和调度的最小(基本)单位(操作系统分配给当前进程一个内存区域供其使用)什么是线程-->是程序运行的基本单位(等待操作系统分配时间片让CPU执行该内存区域中的代码)进程和线程的关系-->一个进程可以存在多个线程线程是由进程创建的(寄......
  • 异步调用方法并弹出处理中窗体(转载)---线程池的封装
    原文链接:[C#]非同步呼叫方法並跳出處理中視窗|愛流浪的小風-點部落(dotblogs.com.tw)前言 當我們在Winform進行某些比較花時間的運算時,若沒有使用非同步的方法來呼叫,畫面上的視窗就會顯示沒有回應,這是一種比較差的使用者體驗,可能會讓使用者以為當機了,在這邊為了方便......
  • 线程同步:Lock锁
        ......
  • 线程同步:同步方法
         ......
  • 线程同步:同步代码块
         ......
  • 线程安全问题:模拟取钱案例
       packageKarl.Demo.money;publicclassTest{publicstaticvoidmain(String[]args){Accountacc=newAccount(100000);newDrawThread(acc,"小明").start();newDrawThread(acc,"小红").start();}}......