背景
很多同学应该都会遇到复杂,涉及到N张表组装的数据接口,尤其是首页的某某列表(比如 美团外卖的商家列表,淘宝订单明细列表等).
如果按照通常的线性流程执行所有代码逻辑,那么接口的响应时间可以说直接爆炸(动辄几秒),产品不打死你,用户也要喷你.
为此,我结合一个实际栗子,来分享下优化的思路及成果.
业务描述
这是首页的赛程列表,该列表需要展示最近5日有比赛且关联游戏的列表数据.
数据如下:

这个列表接口需要2大业务域数据组成:
- (比赛域) 赛事,赛程,赛果,对阵关联队伍,队伍
- (游戏域) 赛程关联游戏,游戏内容(比如包含多少关卡)
共计7张表.
业务流程图

业务分析
该业务描述虽然只有一句话,但是实现很复杂.
最近5日有比赛且关联游戏的列表数据,拆解(不使用join的情况下):
- 查询有赛程的日期,并缓存.
- 查询有游戏的日期,并缓存.
- 按照查询条件的开始时间,循环直到查询出有交集的5天.
- 查询流程3计算得到的实际有赛程且有游戏的时间范围作为条件,查询出赛程
- 按日组装赛程,构造2维数组,并准备公用数据
- 按日遍历赛程列表
- 遍历赛程列表
- 查询并拼装7个表的数据,生成单条详情数据
- 输出比赛列表
流程3 是需求的难点,没法事先知道需要取多少数据才能得想要的结果.
基本实现
按照上述流程,不做其他优化,以线性流程去实现,在准备赛程1000条,游戏10000条的情况下,得到如下结果:

(笑哭)单次请求超过3秒,这个数据量上生产,分分钟给你爆掉.
当然,这个数据量其实超过正常情况,但怕就怕不正常的时候.
优化
源码包含业务敏感信息,就不黏贴了,形式参考下面的测试栗子.
优化思路
- 从取数逻辑入手,提高数据检索效率,介绍无用数据返回
- 优化算法,减少数据库访问
- 尽可能多的缓存少变化的数据
- 耗时任务以线程池方式执行
- 有缓存时,可能以循环而不是批量方式方式查询数据更高效
parallelStream并发流优化(有风险)
其实就是简单的以并发流的方式遍历赛程日.

单IO线程池优化
此处构造了专门的IO线程池处理赛程日的数据,以单独一天的赛程列表作为一个任务,并发处理.

主从IO线程池优化
此处构造了2个专门的IO线程池(主,从)分别处理赛程日的数据和对应日中的赛程列表数据,以单独一天的赛程列表作为主任务池的一个任务,以一天中的赛程作为子任务池的任务,并发处理.
![在这里插入图片描述]()
终极杀招
如果数据变化不频繁,直接缓存比赛列表接口,然后通过赛程和游戏变动事件异步更新比赛列表.
这样接口响应时间直接能干到100ms以下. 这个就不测试了,因为该方案是最外层优化.
如果内层实现不够好,当赛程和游戏变化频繁时,单次构造列表花费3秒这样依然会导致服务down掉.
当然,此处我们还可以 同时检查时间和变化事件双条件满足才异步更新列表.
优化工具的源码
import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;import javax.annotation.Nonnull;
import java.util.*;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;/*** IO密集任务工具* Warning: {@link #asyncTaskQueue}, {@link #getAsyncTaskQueueResult} 依赖 ThreadLocal, 注意多线程下的问题* Warning: 警惕嵌套使用该工具,可能会导致死锁. 如果真需要嵌套,请使用master和worker模式.** @Description* @Author LionZhou* @Date 2023/2/21* @Version 1.0*/
@Slf4j
public class IOUtil {static final ThreadPoolTaskExecutor masterThreadPoolExecutor;static final ThreadLocal>> masterTaskQueue;static final ThreadPoolTaskExecutor workerThreadPoolExecutor;static final ThreadLocal>> workerTaskQueue;public static Future async(Callable func) {return workerThreadPoolExecutor.submit(func);}public static List> async(Collection> funcs) {ArrayList> list = new ArrayList<>(funcs.size());for (Callable func : funcs) {list.add(workerThreadPoolExecutor.submit(func));}return list;}public static void asyncTaskQueue(Callable
测试结果
仅worker模式, master 0, worker 3

private static void onlyWorker() {Callable runnable = () -> {try {for (int i = 0; i < 10; i++) {// 提交到worker线程池asyncTaskQueue(callB);}List async = getAsyncTaskQueueResult(Boolean.class);Thread thread = Thread.currentThread();log.info("master {},{}", thread.getThreadGroup().getName(), thread.getId());} catch (Exception e) {throw new RuntimeException(e);}return true;};for (int i = 0; i < 8; i++) {try {runnable.call();} catch (Exception e) {throw new RuntimeException(e);}}}public static void main(String[] args) {long st = System.currentTimeMillis();onlyWorker();long et = System.currentTimeMillis();log.info("{} ms", et - st);masterThreadPoolExecutor.shutdown();workerThreadPoolExecutor.shutdown();}
主从模式,master 1,worker 2
private static void mixed() {Callable runnable = () -> {try {for (int i = 0; i < 10; i++) {// 提交到worker线程池asyncTaskQueue(callB);}List async = getAsyncTaskQueueResult(Boolean.class);Thread thread = Thread.currentThread();log.info("master {},{}", thread.getThreadGroup().getName(), thread.getId());} catch (Exception e) {throw new RuntimeException(e);}return true;};for (int i = 0; i < 8; i++) {// 提交到master线程池调用asyncTaskQueue(runnable, true);}List async = getAsyncTaskQueueResult(Boolean.class, true);}public static void main(String[] args) {long st = System.currentTimeMillis();mixed();long et = System.currentTimeMillis();log.info("{} ms", et - st);masterThreadPoolExecutor.shutdown();workerThreadPoolExecutor.shutdown();}

仅master,master 3,worker 0
private static void onlyMaster() {Callable runnable = () -> {try {for (int i = 0; i < 10; i++) {// 提交到worker线程池callB.call();}Thread thread = Thread.currentThread();log.info("master {},{}", thread.getThreadGroup().getName(), thread.getId());} catch (Exception e) {throw new RuntimeException(e);}return true;};for (int i = 0; i < 8; i++) {// 提交到master线程池调用asyncTaskQueue(runnable, true);}List async = getAsyncTaskQueueResult(Boolean.class, true);}public static void main(String[] args) {long st = System.currentTimeMillis();onlyMaster();long et = System.currentTimeMillis();log.info("{} ms", et - st);masterThreadPoolExecutor.shutdown();workerThreadPoolExecutor.shutdown();}
![在这里插入图片描述]()
结论
- 对于IO密集型业务,使用线程池和不使用响应时间差距巨大.
- 主从模式不一定就好于同等数量的worker模式,尤其是线程数较少且master任务不耗时的情况.
正确使用线程池,对于服务的响应有极大的提升.
思考: 为啥我要分master和worker分别处理2种任务? 放一起不行吗?