1、Mybatis的parameterType造成线程阻塞

一、场景

最近在新发布某个项目上线时,每次重启都会收到机器的 CPU 使用率告警,查看对应监控,持续时长达 5 分钟CPU使用率过高。

二、分析过程

1、初步分析

首先cpu过高的情况一般是程序在执行某段代码执行过长,且一直在计算,要么进入了死循环,要么计算的量太大,导致占用cpu过高。

  • 大量计算
  • 死锁或者死循环
  • 频繁的IO读写
  • 频繁GC等

2、定位问题

1、使用top命令检查进程的cpu占用,进一步确认具体的线程,然后将线程的堆栈信息导出

top 
top -Hp 33861 
jstack 33861 >/data/jstack/jstack.log

2、通过对服务连续间隔 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 解析某些参数导致大量线程被阻塞,还需继续往下分析。

"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)
        ......

在 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>

初步看没发现问题,但是我们在入 TypeHandler 缓存时 debug 下,分析下哪种类型在缓存中缺失

从 debug 信息中可以看出,TypeHandler 缓存中存在的是 interface java.util.Map,而 SQL 执行时传入的是 class java.util.HashMap,导致并没有命中缓存。

在 Mybatis 社区已经优化了 TypeHandler 入缓存的逻辑,可以解决重复计算 TypeHandler 问题,一定程度上缓解以上问题。但是 Mybatis 修复最低版本为 3.5.8,依赖 spring5.x,而我们项目使用的 Mybatis3.4.4,spring4.x,直接升级会存在一定风险,所以在不升级情况下,按照总结规范使用也可以降低阻塞风险。

TypeHandler 相关issue:Avoid unnecessary `ConcurrentHashMap#put()` calls in `TypeHandlerRegistry` by harawata · Pull Request #2300 · mybatis/mybatis-3 · GitHub

四、总结

1、在使用 paramType 时,xml 配置的类型需要与 Java 代码中传入的一致,使用 Mybatis 预加载时的类型缓存。

2、在使用 paramType 时,避免使用 java.util.HashMap 类型,避免 SQL 执行时解析 TypeHandler。

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值