Mybatis的parameterType造成线程阻塞问题分析

 更新时间:2023年06月08日 10:58:40   作者:京东云开发者  
这篇文章主要详细分析了Mybatis的parameterType造成线程阻塞问题,文中有详细的解决方法,及相关的代码示例,具有一定的参考价值,感兴趣的朋友可以借鉴阅读

一、前言

最近在新发布某个项目上线时,每次重启都会收到机器的 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,直接升级会存在一定风险,所以在不升级情况下,按照总结规范使用也可以降低阻塞风险。

以上就是Mybatis的parameterType造成线程阻塞问题分析的详细内容,更多关于Mybatis parameterType 线程阻塞的资料请关注脚本之家其它相关文章!

相关文章

  • 详解Java Unsafe如何花式操作内存

    详解Java Unsafe如何花式操作内存

    C++可以动态的分类内存,而java并不能这样,是不是java就不能操作内存呢,其实是有其他办法可以操作内存的,下面就一起看看Unsafe是如何花式操作内存的吧
    2023-08-08
  • Spring根据XML配置文件注入属性的方法

    Spring根据XML配置文件注入属性的方法

    下面小编就为大家带来一篇Spring根据XML配置文件注入属性的方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-11-11
  • Spring实现动态切换多数据源的解决方案

    Spring实现动态切换多数据源的解决方案

    这篇文章主要给大家介绍了Spring实现动态切换多数据源的解决方案,文中给出了详细的介绍和示例代码,相信对大家的理解和学习具有一定的参考借鉴价值,有需要的朋友可以参考学习,下面来一起看看吧。
    2017-01-01
  • java设计模式Ctrl C和Ctrl V的原型模式详解

    java设计模式Ctrl C和Ctrl V的原型模式详解

    这篇文章主要为大家介绍了java设计模式Ctrl C和Ctrl V的原型模式详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-02-02
  • idea在plugins中搜不到插件的解决方法

    idea在plugins中搜不到插件的解决方法

    本文主要介绍了idea在plugins中搜不到插件的解决方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-06-06
  • java动态添加外部jar包到classpath的实例详解

    java动态添加外部jar包到classpath的实例详解

    这篇文章主要介绍了java动态添加外部jar包到classpath的实例详解的相关资料,希望通过本文能帮助到大家,需要的朋友可以参考下
    2017-09-09
  • 详解Spring Boot 目录文件结构

    详解Spring Boot 目录文件结构

    这篇文章主要介绍了Spring Boot 目录文件结构的相关资料,文中示例代码非常详细,帮助大家更好的理解和学习,感兴趣的朋友可以了解下
    2020-07-07
  • 在Java中轻松使用工厂设计模式介绍

    在Java中轻松使用工厂设计模式介绍

    这篇文章主要介绍了在Java中轻松使用工厂设计模式介绍,工厂设计模式或工厂方法设计模式是一种广泛使用且易于理解的设计模式,文章通过围绕主题展开详细的内容介绍,感兴趣的朋友可以参考一下
    2022-09-09
  • Java 线程池全面总结与详解

    Java 线程池全面总结与详解

    在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念
    2021-10-10
  • 一篇文章带你了解SpringMVC数据绑定

    一篇文章带你了解SpringMVC数据绑定

    这篇文章主要给大家介绍了关于如何通过一篇文章弄懂Spring MVC的参数绑定,文中通过示例代码以及图文介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2021-08-08

最新评论