项目描述:定时任务扫描300万条数据库记录,并对每条记录执行检查操作(调用其他服务接口,发送短信等)。
版本迭代记录:
1)第一版:一次查询全部300万条数据放在JVM内存中,没有使用线程池,使用固定20个线程,每个线程循环不停的从内存中取一条数据执行,直到所有数据全部执行完为止。
这种方式耗时大约3个半小时,代码如下:
// 实现runnable接口的线程,infos:全部300条任务 CheckTask checkTask = new CheckTask(infos); // 固定20个线程执行,线程不能回收再利用(线程池可以) for (int i = 0; i < 20; i++) { Thread t = new Thread(checkTask, name + "_" + i); t.start(); } private class CheckTask implements Runnable { private List<Info> infos; private volatile int count = 0; public CheckTask(List<Info> infos) { this.infos= infos; } @Override public void run() { boolean done = false; while (!done) { try { Info info = null; // 使用同步锁 synchronized (infos) { if (count < infos.size()) { info = infos.get(count); count++; } else { done = true; break; } } if (info != null) { // 对取到的一个任务执行业务逻辑 ... } } catch (Exception e) { // 异常处理 } } // while } }
2)第二版:翻车的版本,使用线程池,线程池核心线程数为1,最大线程数为50,等待队列为1000。分页查询300万条任务数据,每次执行1000条任务,使用CountDownLatch控制当前页1000条执行完毕再执行下一页。
翻车原因:核心线程数为1,等待队列为1000,每次执行的任务数为1000;根据线程池的执行规则,线程池中的线程数达到核心线程数1之后,将剩余的任务全部放在了队列中,因为队列很大,所以线程池中根本没有
再创建新的线程,只有一个核心线程在慢慢地循环反复从队列中取任务去执行。根据日志估算一个线程执行完一个任务需要0.25秒,执行完300万个任务需要208小时,比之前的三个半小时增加了近7倍,失败。
翻车代码:
/** * 创建一个用于发送邮件的线程池,核心线程数为1,最大线程数为100,线程空闲时间为60s, */ private static final ThreadPoolExecutor EXECUTORS = new ThreadPoolExecutor(1, 100, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(1000), new CustomThreadFactory("checkTask"), new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { // 打印日志,并且重启一个线程执行被拒绝的任务 LOGGER.error("Task:{},rejected from:{}", r.toString(), executor.toString()); // 直接执行被拒绝的任务,JVM另起线程执行 r.run(); } }); int pageSize = 1000; for (int i = 1; i <= pageCount; i++) { PageInfo pageInfo = new PageInfo((i-1)*pageSize, pageSize); // 查询到一页然后进行处理 List<Info> infos = infoDao.queryInfoByPage(infoVo, pageInfo); // 同步执行这一页 // 当前页的大小 int size = infos.size(); final CountDownLatch countDownLatch = new CountDownLatch(size); for (final Info info : infos) { // 交给线程池去执行 EXECUTORS.execute(new MDCRunnable(new Runnable() { @Override public void run() { try { // 执行业务逻辑 ... } catch (Exception e) { LOGGER.error( "检查异常" + e.getMessage(), e); } finally { // 无论执行结果如何都要countDown,避免影响后续的检查 countDownLatch.countDown(); } } })); } // 等待线程池执行完一页的自动续费检查 try { countDownLatch.await(); } catch (InterruptedException e) { LOGGER.error("await一页检查异常" + e.getMessage(), e); } }
3)第三版:对线程池进行调优
已知线上服务器cpu为16核,堆内存最大可为64G。第二版得知一个线程执行一个任务大约需要0.25秒,所以在每页1000个任务不变的情况下对线程池进行了优化。
(1)方案一:
1000个任务1秒内会全部丢进线程池,40个线程1秒处理完160个任务,100个线程每秒处理400个任务,所以1000个任务全部到来,核心线程处理不完,将600个任务在队列等待,400个任务在执行,即使被拒绝也会另起线程执行。
在开发环境i7 12核处理器,JVM内存大小为750m 测试,执行100万个任务耗时大约2500秒。
线程池代码:
/** * 创建一个用于发送邮件的线程池,核心线程数为40,最大线程数为100,线程空闲时间为60s * 服务器CPU为16核,堆内存大小为最大64g * 经测试一个任务执行大概耗时250ms,0.25s,40个线程每秒钟执行大约160个任务,100个线程每秒钟执行400个任务 */ private static final ThreadPoolExecutor EXECUTORS = new ThreadPoolExecutor(40, 100, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(600), new CustomThreadFactory("autoRenewCheckTask"), new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { // 打印日志,并且重启一个线程执行被拒绝的任务 LOGGER.error("Task:{},rejected from:{}", r.toString(), executor.toString()); // 直接执行被拒绝的任务,JVM另起线程执行 r.run(); } });
结果:又翻车了,执行1000条任务大概需要9秒左右,远超设想的2秒。
分析:
(2)方案二:在第一版的基础上,将固定线程数调至30,其他不变。
结果:执行完300万条任务,耗时2个小时,比之前提前了一个半小时。