线程池在业务中的实践

1. 业务背景

  • 场景一: 快速响应用户请求
    场景描述:比如说⽤户要查看⼀个商品的信息,那么我们需要将商品维度的⼀系列信息如商品的价格优惠库存图⽚等等聚合起来,展示给⽤户。
    分析:从用户角度来看,要求响应越快越还,但其实这些面向用户功能的聚合通常会伴随着调用之间的级联、,这种情况下使用线程池,将调用封装成任务并行执行,缩短总体的响应时间,这种场景其实最重要的就是获取最大的响应速度去满足用户,所以应该不设置队列去缓冲并发任务。调高corePoolSize和maxPoolSize去尽可能创造多的线程快速执行任务。
  • 场景二: 快速处理批量任务
    场景描述:比如说有一个离线的计算任务计算量很大,像大型的统计报表,其实我们希望的是来快速生成报表
    分析:这种需要执行大量任务,我们希望任务执行的越快越好,使用多线程。但这种与响应速度的区别在于:这类场景任务量巨大,并不需要瞬时完成,而是关注如何使用有限的资源尽可能在单位时间处理更多的任务,也就是强调吞吐量。这种情况下应该设置队列去缓冲并发任务,调整合适的corePoolSize,这里,设置的线程数过多可能会引发线程上下文切换频繁,也会降低处理任务的速度,减低吞吐量。

2. 实际问题及方案思考

线程池使用面临的核心问题在于:线程池的参数不好配置:线程池执行的情况和任务类型相关性很大,IO、cpu密集型任务执行起来的情况差异非常大。

事故描述一:xxx 页面展示接口大量调用降级:队列设置正常,核心线程过小
事故原因:该服务展示接口内部逻辑使用线程池并行计算,由于没有预估好调用的流量,导致最大核心数设置偏小,大量抛出RejectedExecutionException,触发接口降低。

事故描述二:服务不可用:任务堆积,线程池队列⻓度设置过⻓、corePoolSize设置过⼩导致任务执⾏速度低;线程来不及处理任务造成大量任务堆积,导致任务执行时间过长,会导致下游服务的大量调用超时失败。

思考:那线程池参数有计算呢公式吗?
其实很难有一个准确的公式来计算出核心线程、最大线程数的,往往实际场景是根据压测、tps等来大概估算的,但实际中有时候流量是正常的,有时候流量往往是随机的,其实也不符合实际场景,那既然不能一下子算出来,那可以通过动态线程池参数,实现参数的动态化。
那问题来了:怎样将修改线程池参数的成本降下来?
在成本可控的情况下,在发生故障时可以快速调整从而缩短故障恢复的时间?如何缩短时间?线程池参数的动态感知------> 分布式配置中心,实现线程池参数动态配置即时生效。在这里插入图片描述

线程池参数动态化:分布式配置中心,参数的更新。

3. 动态线程池

动态调参:JDK提供的原生的ThreadPoolExecutor提供了对核心线程数、最大线程数、拒绝策略等的set方法,在运行期使用方调用此方法设置corePoolSize之后,线程池会直接覆盖原来的corePoolSize值,并且会基于当前值和原始值的比较结果采取不同的处理策略。对于当前值小于当前工作线程的情况,说明有多余的工作线程,这时会向工作线程发起中断请求以实现回收;对于当前值大于原始值并且队列中有待执行任务,则线程池会创建新的工作线程来执行队列任务。

那怎样实现动态线程池:获取到当前程序中的ThreadPoolExecutor实例------> 进而获取到当前程序的线程池参数-----> 之后上报到redis中,基于redis发布订阅,实现动态参数更新。

对于JDK提供的这些set方法,线程池内部会处理好当前状态做到平滑处理。只要维护ThreadPoolExecutor实例,在需要修改的时候修改参数即可。

3.1 遇到的问题

问题一:实际投入项目使用时,使用redis作为线程池的注册中心,项目本身是不是还需要额外定义redis客户端实例?要和starter里面的实例 区分开来使用?

  • @ConditionalOnMissingBean。

问题二:为什么redisson 定义的时候 不用默认的配置,而是自己定义的redis的配置类

  • 自己定义可以更好控制和扩展,不会受到升级影响。

问题三:如果想这个starter 既要支持redis 又要支持nacos?

  • 组件中,如果有多个,之后有需要部分不启动。可以用 class.forName 进行检测,如果类不存在就不检测了。还可以使用@ConditionalOnMissingBean的方式,注入的对象也可以控制。

4. 代码

首先要将本地采集到的线程池数据进行上报,采用redis作为注册中心,将采集到的线程池配置信息上传到redis(这是是用spring定时任务,定时采集线程池配置信息进行上报的);动态更新线程池配置参数,是根据redis发布订阅来实现的,将需要动态更新的消息发布到topic,在消费者这一端(订阅了该topic)就会监听到这个消息,然后将变更的消息设置到当前应用的对应的ThreadPoolExecutor实例上(应用本地有一个Map<String, ThreadPoolExecutor>, key为线程池实例bean的名字,value为线程池实例,拿到的消息包含哪个应用啊、哪个线程池实例啊、以及要设置的参数:核心线程、最大线程;用消息中线程池实例bean的名字拿到对应的实例,然后直接更新这个实例中的核心线程、最大线程、队列长度等信息,实现动态更新,不需要重启应用)。

问:spring采集线程池配置信息怎么做的?
Map<String, ThreadPoolExecutor> threadPoolExecutorMap;就拿到了当前应用中的所有线程池实例。

上报到redis中的信息包括什么:包括线程池的名字、标识、配置参数。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值