【问题描述】
线上商品图片个别为空白图片。
【影响范围】
APP首页、门店列表页、单品页详情
【事故级别】
P0
【处理过程】
11:23 反馈app商品图片很多都为空
11:30 定位后台接口缓存数据返回为空;
11:35 接口打开降级查询数据库开关,补全商品图片至缓存
11:36 问题解决
12:30 review代码,发现问题所在
【故障原因】
- 商品数据由于图片调整大小类型进行数据重刷,在刷新的过程中为了缩短时间开启了多线程并行处理,在处理的过程中,按照商家下门店数开启了线程,并且门店下的sku图片刷新又按照 批量50个开启了子线程处理。
- 主线程、子线程均来源于同一个线程池,并没有进行线程池隔离。
- 主、子线程占用会导致相互等待。
- 随着任务的增加,新生成的子线程,逐渐达到最大线程数,从而进入到等待队列
- 等待队列对着任务的积压逐渐达到最大值
- 走线程策略,此处使用的是拒绝策略,即任务丢失
- 图片刷新的任务开始丢失,导致线上缓存数据缺失
【总结】
先来回顾下线程池的基础知识。
线程池重要参数:
- corePoolSize核心线程数大小,当线程数<corePoolSize ,会创建线程执行runnable
- maximumPoolSize 最大线程数, 当线程数 >= corePoolSize的时候,会把runnable放入workQueue中
- keepAliveTime 保持存活时间,当线程数大于corePoolSize的空闲线程能保持的最大时间。
- unit 时间单位
- workQueue 保存任务的阻塞队列
- threadFactory 创建线程的工厂
- handler 拒绝策略
任务执行顺序:
- 当线程数小于corePoolSize时,创建线程执行任务。
- 当线程数大于等于corePoolSize并且workQueue没有满时,放入workQueue中
- 线程数大于等于corePoolSize并且当workQueue满时,新任务新建线程运行,线程总数要小于maximumPoolSize
- 当线程总数等于maximumPoolSize并且workQueue满了的时候执行handler的rejectedExecution。也就是拒绝策略。
ThreadPoolExecutor默认有四个拒绝策略:
ThreadPoolExecutor.AbortPolicy()直接抛出异常RejectedExecutionExceptionThreadPoolExecutor.CallerRunsPolicy()直接调用run方法并且阻塞执行ThreadPoolExecutor.DiscardPolicy()直接丢弃后来的任务ThreadPoolExecutor.DiscardOldestPolicy()丢弃在队列中队首的任务
接下来我们通过一个demo来复现事故的原因过程
public class FuatureTaskDemo2 {
private static ThreadPoolExecutor mExecutor = new ThreadPoolExecutor(
4,
4,
10L,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
/**
* @return
*/
public void getWorker(String name) throws Exception {
System.out.println("执行"+name+"程任务开始");
for(int i=0;i<10;i++){
int finalI = i;
mExecutor.submit(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(100L);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("执行"+name+"中子线程:"+ finalI);
}
});
}
int getActiveCount = (mExecutor).getActiveCount();
int getCorePoolSize = (mExecutor).getCorePoolSize();
int getMaximumPoolSize = (mExecutor).getMaximumPoolSize();
long getTaskCount = (mExecutor).getTaskCount();
BlockingQueue<Runnable> blockingQueue = (mExecutor).getQueue();
System.out.println("getActiveCount"+getActiveCount);
System.out.println("getCorePoolSize"+getCorePoolSize);
System.out.println("getMaximumPoolSize"+getMaximumPoolSize);
System.out.println("getTaskCount"+getTaskCount);
System.out.println("blockingQueue"+blockingQueue.size());
mExecutor.shutdown();
}
public static void main(String[] args) {
FuatureTaskDemo2 it = new FuatureTaskDemo2();
FuatureTaskDemo2 it3 = new FuatureTaskDemo2();
try {
it3.getWorker("父线程");
it.getWorker("子线程");
} catch (Exception e) {
e.printStackTrace();
}
}
}
执行结果:
给我们的启示:
- 由上述demo可以看到。30个子线程任务 最后只执行了 6个,其余的全部被拒绝。
- 我们在执行核心数据线程的时候,尽量做到主-子线程池分离
- 核心任务 拒绝策略一定是
ThreadPoolExecutor.CallerRunsPolicy()直接调用run方法并且阻塞执行,或者是ThreadPoolExecutor.AbortPolicy()直接抛出异常后进行重试。