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<String> unmappedColumnNames 长度为 0,表示所有字段都命中了 <resultMap> 标签配置,符合预期。
2、实验组(resultType=JavaBean)
List<String> unmappedColumnNames 长度为 11,表示所有字段都在 <resultMap> 标签配置中未找到。这是因为 SQL 执行后的 resultMap 对应的 id 并不等于<resultMap>标签的 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
作者:京东物流 张凯
来源:京东云开发者社区

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
主动发现系统稳定性缺陷:混沌工程 | 京东云技术团队
这是一篇较为详细的混沌工程调研报告,包含了背景,现状,京东混沌工程实践,希望帮助大家更好的了解到混沌工程技术,通过混沌工程实验,更好的为系统保驾护航。 一、概述 1.1 研究背景 Netflix公司最早系统化地提出了混沌工程的概念。2008年8月,Netflix公司由于数据库发生故障,导致了三天时间的停机,使得DVD在线租赁业务中断,造成了巨大的经济损失。于是Netflix公司开始尝试利用混沌工程优化稳定性保障体系。2010年,Netflix公司开发了混沌工程程序Chaos Monkey,于2012年在Simain Army项目中开源,该程序的主要功能是随机终止在生产环境中运行的虚拟机实例和容器,模拟系统基础设施遭到破坏的场景,从而检验系统服务的健壮性。2019年,阿里巴巴开源了ChaosBlade混沌工程程序,该程序可以结合阿里云进行混沌工程实验。2020年,PingCap 作为国内优秀的数据库领域开源公司,开源了其公司内部的混沌工程实践平台ChaosMesh。 基于以上开源项目,混沌工程项目在国内受到了多家公司的关注,越来越多的公司开始引入混沌工程进行系统稳定性保障工作,通过混沌工...
- 下一篇
百度离线资源治理
作者 |百度MEG离线优化团队 导读 近些年移动互联网的高速发展驱动了数据爆发式的增长,各大公司之间都在通过竞争获得更大的增长空间,大数据计算的效果直接影响到公司的发展,而这背后其实依赖庞大的算力及数据作为支撑,因此在满足业务迭代的前提下如何控制成本是公司非常重要的一环。 本文将介绍百度MEG(移动生态事业群组)在离线资源降本增效方面用到的一些技术以及取得的一些成果。 全文4478字,预计阅读时间12分钟。 01 业务背景 随着百度App的日活用户的持续增长,为了满足广大用户对信息资讯更加精准的需求,MEG的各个业务模块对于离线算力和存储的需求也不断增加通过其驱动上层模型获得更好的效果,因此离线成本也逐年增加,如何满足业务增长的情况下最小化机器资源成本是本文重点关注的问题。就拿百度App后端推荐服务(后简称Feed)举例,拥有离线大数据计算数百万核、分布式存储数百PB,成本以亿为单位,而且还在持续增长,因此我们希望能够在满足推荐效果的前提下优化降低离线的成本。整体离线计算主要分为两大类,即数据挖掘类和数据分析类,其中挖掘类场景主要是通过python脚本提交的MapReduce任务为主,...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- MySQL8.0.19开启GTID主从同步CentOS8
- Mario游戏-低调大师作品
- Linux系统CentOS6、CentOS7手动修改IP地址
- Docker安装Oracle12C,快速搭建Oracle学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- CentOS7安装Docker,走上虚拟化容器引擎之路
- Docker快速安装Oracle11G,搭建oracle11g学习环境