总结最近遇到的几个问题

概述

最近开发中遇到了几个典型问题,总结记录一下。

问题1. 线程池参数不正确引发的定时任务执行时间超长问题

先看一下线程池的定义

// 初始化线程池
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
  • 拒绝策略为CallerRunsPolicy

线程池的拒绝策略,通常是在线程池已经饱和,没有办法继续执行新任务的时候触发。在我们的例子中,线程数为50,队列长度为2000,那么当第2051个任务提交的时候,就会触发拒绝策略。 JUC中共有四种默认的拒绝策略供选择。
总结最近遇到的几个问题_第1张图片

  • 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个任务提交的时候可能触发。 经验教训:

  • 线程池的参数配置一定要深入了解和掌握,并结合业务优化配置。
  • 实际上调整线程池参数,对于该业务场景属于缓兵之计,如何将单机执行任务扩展到多机执行是新的优化方向。

问题2.BeanUtils.copyProperties引发的属性为空问题

我们经常会遇到从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属性依旧为空。 经验教训:

  • 对于使用的API,一定要再三确认和验证。

问题3.业务上线未加索引引发的线上数据库问题

该问题是业务开发非常容易忽略的问题。 简化一下场景。 心跳数据表模型

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上传一次,而且会拥有很多设备同时上传心跳的情况发生,所以进而逐步引起了数据库性能问题。 经验教训

  • 业务新上线的功能要检查下是否有索引

问题4.数据库新增字段后,手写Mapper文件引发的构造函数问题

由于业务变更,需要给某数据表新增一个字段,由于对应的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。 代码主要改动点

  • 新增bizSrc属性,并提供对应的getter和setter方法
  • 修改构造函数,通过构造函数给bizSrc赋值。
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字段,导致初始化的时候找不到对应的构造函数。 经验教训

  • 即使有便利的工具,还是需要弄清楚原理,避免手动修改时,引入问题。
  • 对于发布,如果有灰度环境,一定要仔细检查发布后的日志,遇到问题要及时修正,不要忽略。

你可能感兴趣的:(java,代码人生,java)