• 总结最近遇到的几个问题


    概述

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

    问题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);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    有几个关键信息

    • 核心线程数和最大线程数都为50
    • 队列长度为2000
    • 拒绝策略为CallerRunsPolicy

    线程池的拒绝策略,通常是在线程池已经饱和,没有办法继续执行新任务的时候触发。在我们的例子中,线程数为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);
    
    • 1
    • 2
    • 3
    • 4
    • 5

    按照新的配置参数,拒绝策略会在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);
    }
    
    • 1
    • 2
    • 3

    Spring BeanUtils.copyProperties 参数1为source,参数2为target。

    public static void copyProperties(Object source, Object target) throws BeansException {
        copyProperties(source, target, null, (String[]) null);
    }
    
    • 1
    • 2
    • 3

    可以看到两者的参数声明是相反的,所以本来是要将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='心跳信息'
    ;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    从旧的建表语句可以看到,该数据表只有主键索引。 本次业务变更,希望根据会话session_id更新last_time,业务变更SQL也很简单。

      update tb_heartbeat set gmt_modified= now(),
      last_time=#{lastTime}
      where session_id= #{sessionId}
    
    • 1
    • 2
    • 3

    表中的数据量大约为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='记录表'
    ;
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    其中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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    第二,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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    上述修改以后,结果发现在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>
    ​
    ​
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    发现select依赖的BaseResultMap中,并未添加bizSrc字段,导致初始化的时候找不到对应的构造函数。 经验教训

    • 即使有便利的工具,还是需要弄清楚原理,避免手动修改时,引入问题。
    • 对于发布,如果有灰度环境,一定要仔细检查发布后的日志,遇到问题要及时修正,不要忽略。
  • 相关阅读:
    python 为 网易云下载的 本地音乐文件增加 序号
    数组的声明和使用
    Weblogic管理控制台未授权远程命令执行漏洞(CVE-2020-14882,CVE-2020-14883)
    LeetCode中等题之旋转图像
    企业应用身份中台,让登录变得更加高效
    MySQL向自增列插入0失败问题
    Graalvm 安装和静态编译
    java设计模式之组合设计模式
    Java BufferedWriter.write()具有什么功能呢?
    智能指针学习笔记
  • 原文地址:https://blog.csdn.net/yi_chao_jiang/article/details/127821922