最近开发中遇到了几个典型问题,总结记录一下。
先看一下线程池的定义
// 初始化线程池
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(2000);
ThreadFactory threadFactory = new CustomThreadFactory("statusService");
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
executor = new ThreadPoolExecutor(50, 50, 30L, TimeUnit.SECONDS, queue, threadFactory, handler);
有几个关键信息
线程池的拒绝策略,通常是在线程池已经饱和,没有办法继续执行新任务的时候触发。在我们的例子中,线程数为50,队列长度为2000,那么当第2051个任务提交的时候,就会触发拒绝策略。 JUC中共有四种默认的拒绝策略供选择。
DiscardPolicy,新任务被提交后直接被丢弃掉,也不会给任何的通知,提交任务的一方根本不知道这个任务会被丢弃,可能会影响业务。
DiscardOldestPolicy,会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第DiscardPolicy不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的业务影响。
AbortPolicy,这种拒绝策略在拒绝任务时,会直接抛出异常 RejectedExecutionException (属于RuntimeException),通知到任务提交方,后续可以根据业务逻辑选择重试或者放弃提交等处理。
CallerRunsPolicy,在线程池饱和的情况下,当有新任务提交后,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。这种拒绝策略有两个特点
通过上面的解析我们可以看到,对于CallerRunsPolicy拒绝策略来说,是会影响任务提交的,进而就会影响整体任务的执行。 回到我们遇到的业务问题,前面说的我们原有配置的线程池当提交任务到了2051的时候,就可能会触发CallerRunsPolicy拒绝策略。我们的定时任务按照每2分钟触发一次的频率,单次提交的任务数量已经接近10000,所以容易触发该拒绝策略。一旦触发该拒绝策略,那就会影响定时任务的单次执行,进而无法满足2分钟执行完成的业务需求。 通过性能分析发现,线程池提交的任务实际执行速度很快,可以通过调整核心线程数和最大线程数,以及队列长度,避免CallerRunsPolicy拒绝策略的发生,在预留了业务buffer以后,调整后的线程池参数如下。
// 初始化线程池
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(20000);
ThreadFactory threadFactory = new CustomThreadFactory("statusService");
RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
executor = new ThreadPoolExecutor(200, 200, 30L, TimeUnit.SECONDS, queue, threadFactory, handler);
按照新的配置参数,拒绝策略会在20201个任务提交的时候可能触发。 经验教训:
我们经常会遇到从DO对象转为VO对象给接口返回的情况,通常有很多种实现方式,像现在比较常用的MapStruct也是一种选择。这里偷懒了,考虑到属性名基本一致,所以想直接通过BeanUtils.copyProperties实现。 最开始使用了默认导入了Apache commons的BeanUtils包 随后因为代码规范扫描问题,
不允许使用Apache BeanUtils,于是替换为了Spring的BeanUtils。具体原因可参考文章:https://baijiahao.baidu.com/s?id=1672085660306809424&wfr=spider&for=pc。 这里犯了一个错误,只是修改了import包的导入情况,并未实际查看代码签名。这里可以对比一下两者。 Apache commons BeanUtils包 参数1为dest,参数2为orig
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
BeanUtilsBean.getInstance().copyProperties(dest, orig);
}
Spring BeanUtils.copyProperties 参数1为source,参数2为target。
public static void copyProperties(Object source, Object target) throws BeansException {
copyProperties(source, target, null, (String[]) null);
}
可以看到两者的参数声明是相反的,所以本来是要将DO赋值给VO,结果变成了用VO的值赋值给了DO,那VO属性依旧为空。 经验教训:
该问题是业务开发非常容易忽略的问题。 简化一下场景。 心跳数据表模型
CREATE TABLE `tb_heartbeat` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`last_time` datetime NOT NULL COMMENT '最后时间'
`session_id` varchar(255) DEFAULT NULL COMMENT '会话id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='心跳信息'
;
从旧的建表语句可以看到,该数据表只有主键索引。 本次业务变更,希望根据会话session_id更新last_time,业务变更SQL也很简单。
update tb_heartbeat set gmt_modified= now(),
last_time=#{lastTime}
where session_id= #{sessionId}
表中的数据量大约为1W左右,实际数据量不算多。但是由于心跳信息每30s上传一次,而且会拥有很多设备同时上传心跳的情况发生,所以进而逐步引起了数据库性能问题。 经验教训
由于业务变更,需要给某数据表新增一个字段,由于对应的Mybatis文件最开始是由MybatisGenerator自动生成的,本次由于刚刚更换电脑,暂时未找到对应的文件,再加上想着就只有一个字段,所以就想手动修改一下。 原表信息(已做简化)
CREATE TABLE `tb_history` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`biz_src` varchar(255) DEFAULT NULL COMMENT '业务来源',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='记录表'
;
其中biz_src是本次要新增的字段。 看了一下Mybatis的文件信息,确定了几个要修改的地方。 第一,Mybatis对应的DO文件,需要新增字段biz_src。 代码主要改动点
import java.util.Date;
public class HistoryDo {
private Long id;
private Date gmtCreate;
private Date gmtModified;
private String bizSrc;
public HistoryDo(Long id, Date gmtCreate, Date gmtModified, String bizSrc) {
this.id = id;
this.gmtCreate = gmtCreate;
this.gmtModified = gmtModified;
this.bizSrc = bizSrc;
}
public HistoryDo() {
super();
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Date getGmtCreate() {
return gmtCreate;
}
public void setGmtCreate(Date gmtCreate) {
this.gmtCreate = gmtCreate;
}
public Date getGmtModified() {
return gmtModified;
}
public void setGmtModified(Date gmtModified) {
this.gmtModified = gmtModified;
}
public String getBizSrc() {
return bizSrc;
}
public void setBizSrc(String bizSrc) {
this.bizSrc = bizSrc == null ? null : bizSrc.trim();
}
}
第二,Mybatis Mapper文件,需要修改insert语句,对biz_src赋值。
<insert id="insertSelective" parameterType="HistoryDo">
insert into tb_history
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
if>
<if test="gmtCreate != null">
gmt_create,
if>
<if test="gmtModified != null">
gmt_modified,
if>
<if test="callBizSrc != null">
call_biz_src,
if>
trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id,jdbcType=BIGINT},
if>
<if test="gmtCreate != null">
#{gmtCreate,jdbcType=TIMESTAMP},
if>
<if test="gmtModified != null">
#{gmtModified,jdbcType=TIMESTAMP},
if>
<if test="callBizSrc != null">
#{callBizSrc,jdbcType=VARCHAR},
if>
trim>
insert>
上述修改以后,结果发现在HistoryDO的构造函数地方报异常,说对应的构造函数不匹配,无法完成实例化。 通过堆栈信息发现,原来存在一处select的操作,对应的Mybatis XML如下
<resultMap id="BaseResultMap" type="HistoryDo">
<constructor>
<idArg column="id" javaType="java.lang.Long" jdbcType="BIGINT" />
<arg column="gmt_create" javaType="java.util.Date" jdbcType="TIMESTAMP" />
<arg column="gmt_modified" javaType="java.util.Date" jdbcType="TIMESTAMP" />
<arg column="biz_src" javaType="java.lang.String" jdbcType="VARCHAR" />
constructor>
resultMap>
<select id="queryById" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from tb_history
where id = #{id}
select>
发现select依赖的BaseResultMap中,并未添加bizSrc字段,导致初始化的时候找不到对应的构造函数。 经验教训