线程池在保险销售高峰场景中的设计与调优
对应简历段落
在保险销售活动系统中,参与销售活动、月底结算、商机流转等核心链路建设。针对活动高峰期用户集中投保、代理人批量跟进、保单状态异步回写、结算任务集中触发等场景,设计 RocketMQ 削峰、Sentinel 限流、线程池隔离和异步化处理方案,提升系统在流量突增下的稳定性。负责线程池参数调优、任务队列治理、拒绝策略设计、核心链路监控告警和故障排查,降低高峰期接口超时、任务堆积、数据库连接耗尽和跨业务相互拖垮的风险。
这段简历看起来是在讲“用了线程池”,但面试官真正想确认的是:你是否理解线程池背后的资源模型,是否知道业务高峰为什么会把系统打垮,是否能把 RocketMQ、Sentinel、线程池、数据库连接池、下游 RPC 超时、幂等补偿这些能力串成一套完整的稳定性方案。线程池不是为了把代码改成异步那么简单,它本质上是在有限机器资源上做并发预算、排队策略、故障隔离和降级取舍。
业务背景
保险销售活动通常有几个明显特点。
第一,流量具有强脉冲。比如开门红活动、月底冲刺、节假日前促销、产品停售前最后一波投保,用户和代理人会集中进入活动页、测算保费、提交投保意向、上传资料、查询核保进度。平时 QPS 可能只有几十,活动开始后短时间冲到几百甚至上千。
第二,链路长且外部依赖多。一次销售动作可能包含活动资格校验、产品规则查询、客户画像查询、优惠权益计算、投保单创建、商机分配、代理人通知、短信触达、RocketMQ 消息发送、保司接口调用、CRM 状态同步等步骤。任何一个下游慢,都会把同步线程拖住。
第三,业务有明显的优先级差异。活动页面查询和投保提交属于用户实时感知链路,必须优先保障。短信通知、商机打标签、运营报表、结算明细生成可以延迟。月底结算虽然不是用户实时请求,但数据规模大、持续时间长,如果不隔离,很容易抢占在线交易资源。
第四,数据一致性要求高。保险业务不能简单丢任务。投保单、商机、佣金、结算状态都要可追踪、可重试、可补偿。线程池拒绝任务时不能只是抛异常,必须结合业务状态机、MQ 重试、失败表、人工补偿入口来设计。
因此,项目里的线程池调优不是单点优化,而是围绕“哪些任务可以并行、哪些任务必须排队、哪些任务可以丢弃或降级、哪些任务必须持久化后异步执行”来做整体架构设计。
问题为什么出现
高峰期系统出问题,通常不是因为单个接口写得慢,而是多个资源同时进入临界状态。
最常见的问题是 Tomcat 工作线程被慢调用占满。比如活动提交流程里同步调用保司接口,保司在高峰期响应从 200ms 变成 3s。如果入口线程一直阻塞等待,下游稍慢就会导致 Web 容器线程耗尽,后续请求连进入业务代码的机会都没有。
第二类问题是业务线程池被无界队列拖垮。很多项目喜欢用 Executors.newFixedThreadPool 或 newSingleThreadExecutor,默认队列是 LinkedBlockingQueue,容量接近无限。高峰期任务生产速度大于消费速度时,队列持续增长,看似没有拒绝,实际上把压力转成了堆内存压力和延迟膨胀。等到 OOM 或 Full GC,系统已经很难自救。
第三类问题是不同业务共用一个线程池。销售活动、商机流转、短信通知、结算任务如果都丢进同一个 asyncExecutor,月底结算一旦批量提交几十万条任务,就可能把活动提交通知、保单状态回写、商机分配一起堵住。共用线程池会让低优先级任务拖垮高优先级链路。
第四类问题是线程池数量调大后反而更慢。线程数不是越大越好。线程数过大时,CPU 上下文切换增加,数据库连接池被打满,下游服务被并发冲垮,锁竞争变重。比如线程池最大线程数配置为 300,但数据库连接池只有 50,最终 250 个线程只是在等待连接,既占内存又增加调度成本。
第五类问题是 MQ 消费端没有限速。RocketMQ 本身能削峰,把入口流量先存起来,但如果消费者线程池无限拉取、无限并发处理,就只是把峰值从入口转移到消费端。消费端必须结合线程池容量、数据库吞吐、下游接口承载能力设置消费线程数、批量大小、重试策略和积压告警。
第六类问题是缺少可观测性。很多故障排查时只看到接口超时,却不知道是线程池 active 打满、队列堆积、拒绝数飙升、DB 连接池等待、MQ 消费延迟还是 Sentinel 限流触发。没有指标,调参只能凭感觉。
线程池核心原理
Java 线程池的核心类是 ThreadPoolExecutor。理解它要抓住几个参数:corePoolSize、maximumPoolSize、workQueue、keepAliveTime、threadFactory、RejectedExecutionHandler。
提交任务时,线程池的大致流程是:
- 当前运行线程数小于核心线程数,直接创建核心线程执行任务。
- 核心线程已满,尝试把任务放入队列。
- 队列也满,并且当前线程数小于最大线程数,创建非核心线程执行任务。
- 线程数达到最大值且队列也满,执行拒绝策略。
这里有一个很容易被忽略的点:如果使用无界队列,maximumPoolSize 基本不会生效。因为任务会一直进入队列,很少走到创建非核心线程那一步。所以生产系统里通常不建议使用无界队列,尤其是承载外部流量或批处理任务时。
线程池真正管理的是三个东西。
第一是并发度。线程数决定同一时间最多有多少任务在执行。对于 CPU 密集型任务,线程数通常接近 CPU 核数或略高;对于 IO 密集型任务,可以适当放大,但要受数据库连接池、RPC 连接池、下游限流阈值约束。
第二是缓冲能力。队列用于吸收短时间突发流量,但队列不是越大越好。队列越大,系统越不容易立即拒绝,但任务等待时间越长。对于用户实时链路,排队太久等同于失败;对于离线结算,可以接受更长排队,但必须监控积压。
第三是故障边界。不同线程池代表不同资源池。销售活动提交、商机分配、保司回调、月底结算、短信通知最好按业务优先级和依赖类型拆开。一个池打满时,不应该影响另一个池的核心能力。
拒绝策略也不只是 Java 内置四种策略的选择。AbortPolicy 会直接抛异常;CallerRunsPolicy 会让提交任务的线程自己执行,能形成一定反压,但在 Web 请求线程中滥用会拖慢入口;DiscardPolicy 和 DiscardOldestPolicy 容易悄悄丢任务,不适合保险订单、结算这类关键任务。真实项目更常见的是自定义拒绝策略:记录指标、打印关键上下文、落失败表、发送告警、必要时返回“系统繁忙,请稍后重试”。
项目落地方案
项目落地时,可以把整个保险销售高峰链路拆成四层治理。
第一层是入口限流。活动提交流量先经过 Sentinel。按活动 ID、渠道、接口维度配置限流规则,保护核心接口不被瞬时流量打穿。比如投保提交接口按机器维度限制 200 QPS,商机批量导入接口限制更低。对超过阈值的请求,不让它进入复杂业务链路,而是快速返回可理解的提示,或者引导稍后重试。限流的目的不是“少做业务”,而是让系统在承载范围内稳定处理最有价值的请求。
第二层是同步链路瘦身。用户提交投保意向时,同步链路只做必要校验和主状态落库:活动有效性、用户资格、产品基本规则、重复提交幂等、投保意向单创建。短信通知、商机标签、代理人提醒、运营埋点、结算预计算等非关键动作全部通过 RocketMQ 异步化。同步接口返回前必须保证核心单据已持久化,后续异步任务可以根据业务单号重放。
第三层是 RocketMQ 削峰。投保提交成功后发送商机流转消息,月底结算任务发送结算明细消息。MQ 把瞬时流量变成可控的消费流量。消费端不追求“立刻消费完”,而是根据数据库和下游承载能力稳定消费。比如商机分配消费者设置较高优先级和独立消费组;短信消费者可以低优先级;月底结算消费者单独部署或单独线程池,避免它和在线活动链路抢资源。
第四层是线程池隔离。系统至少拆出以下几个线程池:
活动提交辅助线程池:处理用户提交后的轻量异步动作,如埋点、轻量缓存刷新。核心线程数较小,队列较短,拒绝后可以降级。
商机流转线程池:处理商机分配、状态推进、代理人绑定。任务重要,不能丢,拒绝后要让 MQ 稍后重试或落失败表。
保司接口线程池:处理外部保险公司接口调用。需要严格控制最大并发,避免把保司打挂,也避免外部慢调用拖垮本系统。
月底结算线程池:处理佣金试算、结算明细生成、对账数据聚合。允许长时间运行,但要限制并发和批次大小,避免挤占数据库资源。
通知线程池:处理短信、企微、站内信等通知。优先级低,可降级,可延迟。
这些线程池不是越多越好。拆分的依据是:任务优先级是否不同、依赖资源是否不同、耗时模型是否不同、失败处理方式是否不同。如果只是名字不同但参数和依赖都一样,过度拆分会增加运维复杂度。
在线活动提交的推荐链路是:请求进入后先经过 Sentinel 限流,再执行业务幂等校验,核心状态落库,发送 RocketMQ 事务消息或本地消息表,最后返回用户。MQ 消费者收到消息后,进入对应业务线程池执行商机流转、通知和后续补偿。消费者线程数和业务线程池容量要匹配,避免 MQ 拉取速度远大于本地处理速度。
月底结算的推荐链路是:结算任务按机构、产品、月份、代理人维度切分为小批次,写入任务表,然后发送 MQ 或由调度任务分页扫描。每个批次进入结算线程池处理,处理过程中使用游标分页,避免一次性加载过多数据。结算结果落库要幂等,支持失败批次重跑。线程池并发要和数据库写入能力、锁冲突范围配合,不能因为追求速度把结算库打满。
关键伪代码
线程池配置建议显式使用 ThreadPoolExecutor,不要直接使用 Executors 工厂方法。
public class SalesExecutors {
public static ThreadPoolExecutor opportunityExecutor() {
return new ThreadPoolExecutor(
16,
32,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new NamedThreadFactory("opp-flow"),
new BizRejectedHandler("opp-flow", RejectLevel.RETRYABLE));
}
public static ThreadPoolExecutor insurerExecutor() {
return new ThreadPoolExecutor(
8,
16,
30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
new NamedThreadFactory("insurer-rpc"),
new BizRejectedHandler("insurer-rpc", RejectLevel.FAIL_FAST));
}
public static ThreadPoolExecutor settlementExecutor() {
return new ThreadPoolExecutor(
4,
8,
120,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(5000),
new NamedThreadFactory("month-settle"),
new BizRejectedHandler("month-settle", RejectLevel.PERSIST_FAILED));
}
}
自定义拒绝策略要带业务上下文,不能只打印一行日志。
public class BizRejectedHandler implements RejectedExecutionHandler {
private final String poolName;
private final RejectLevel rejectLevel;
@Override
public void rejectedExecution(Runnable task, ThreadPoolExecutor executor) {
ThreadPoolMetrics.recordReject(poolName);
if (task instanceof BizTask bizTask) {
log.warn("thread pool rejected, pool={}, bizKey={}, taskType={}, active={}, queue={}",
poolName,
bizTask.bizKey(),
bizTask.taskType(),
executor.getActiveCount(),
executor.getQueue().size());
if (rejectLevel == RejectLevel.PERSIST_FAILED) {
failedTaskRepository.save(bizTask.toFailedRecord("THREAD_POOL_REJECTED"));
return;
}
if (rejectLevel == RejectLevel.RETRYABLE) {
throw new RetryLaterException("thread pool busy, retry by mq");
}
}
throw new RejectedExecutionException("thread pool busy: " + poolName);
}
}
RocketMQ 消费端要把“本地线程池满了”转成 MQ 重试,而不是在消费线程里无限阻塞。
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages,
ConsumeConcurrentlyContext context) {
for (MessageExt message : messages) {
OpportunityEvent event = parse(message);
try {
opportunityExecutor.execute(new OpportunityFlowTask(event));
} catch (RetryLaterException | RejectedExecutionException ex) {
context.setDelayLevelWhenNextConsume(3);
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
如果任务必须确认处理结果,不能简单把任务丢进线程池后就返回消费成功。更稳妥的做法是控制 MQ 消费线程本身的并发,或者使用带超时的 Future 等待关键任务完成。否则 MQ 已经 ack,但本地线程池里的任务还没执行,进程重启会导致消息丢失。
public ConsumeConcurrentlyStatus consumeSettlement(MessageExt message) {
SettlementBatchEvent event = parse(message);
Future<BatchResult> future;
try {
future = settlementExecutor.submit(() -> settlementService.settleBatch(event));
BatchResult result = future.get(30, TimeUnit.SECONDS);
return result.success()
? ConsumeConcurrentlyStatus.CONSUME_SUCCESS
: ConsumeConcurrentlyStatus.RECONSUME_LATER;
} catch (TimeoutException ex) {
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
} catch (RejectedExecutionException ex) {
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
} catch (Exception ex) {
failedTaskRepository.save(event.toFailedRecord(ex));
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}
Sentinel 可以放在入口接口和关键下游调用处。入口限流保护本服务,下游调用限流保护依赖。
public SubmitResult submitActivity(SubmitCommand command) {
try (Entry entry = SphU.entry("activity-submit")) {
String bizNo = submitService.createIntent(command);
mqProducer.send("OPPORTUNITY_FLOW_TOPIC", new OpportunityEvent(bizNo));
return SubmitResult.success(bizNo);
} catch (BlockException ex) {
return SubmitResult.busy("当前活动访问人数较多,请稍后再试");
}
}
参数调优方法
线程池参数调优的第一步不是算公式,而是明确任务类型和瓶颈资源。
对于 CPU 密集型任务,比如复杂规则计算、佣金公式计算、批量数据聚合,如果计算过程中很少等待 IO,线程数接近 CPU 核数即可。线程过多只会增加上下文切换。
对于 IO 密集型任务,比如调用保司接口、查询客户画像、写数据库、发短信,可以适当增加线程数,但上限要看外部资源。一个常用估算是:
线程数 = CPU 核数 * (1 + 等待时间 / 计算时间)
例如单个保司调用平均 500ms,其中本地计算 20ms,等待远端 480ms,理论上可以配置较多线程。但这只是上限参考,实际还要受保司限流、HTTP 连接池、数据库连接池和机器内存约束。如果保司只允许每个渠道 50 并发,本地线程池配 200 没有意义。
活动提交辅助线程池通常队列不宜过大。用户实时链路的异步辅助任务如果排队超过几秒,很多已经失去价值。可以配置较小队列,触发拒绝后记录降级日志,避免堆积。
商机流转线程池要关注吞吐和可靠性。商机状态推进影响后续代理人跟进,不能静默丢失。队列可以适中,但必须配合 MQ 重试和失败表。如果队列长期超过 70%,说明消费能力不足,需要扩容消费者、优化 SQL、减少单任务耗时或拆分热点活动。
保司接口线程池要保守。外部接口慢时,线程池队列应短一些,最大线程数应低于下游可承载并发。否则慢调用会在本地堆积,导致线程长期阻塞。还要配置连接超时、读取超时、熔断和隔离,不能只依赖线程池。
月底结算线程池要重点控制批大小。很多结算慢不是线程数不够,而是单批数据太大、事务太长、锁范围太大、索引不匹配。调优时应优先把十万级任务拆成几百或几千条一批,保证单批可重试、可观测、可回滚。线程数一般从小开始,比如 4 到 8,然后观察数据库 CPU、慢 SQL、行锁等待和连接池等待时间。
队列容量的设置要看可接受延迟。假设商机任务平均处理耗时 50ms,线程数 20,那么理论吞吐约 400 TPS。如果队列容量是 1000,满队列时等待时间可能达到 2.5 秒。这个延迟可接受,就可以作为初始值。如果队列容量设 10000,满队列等待时间可能超过 25 秒,高峰结束后用户或代理人看到的状态仍然滞后。
拒绝阈值要和告警联动。建议监控以下指标:activeCount、poolSize、queueSize、queueRemainingCapacity、completedTaskCount、rejectCount、taskCostP95、taskCostP99。告警不是等拒绝数很高才触发,而是队列持续增长、活跃线程长时间接近最大线程数、任务耗时 P99 明显上升时就要提示。
压测时要模拟真实业务比例。不能只压一个接口。保险销售活动里,提交、查询、商机分配、短信通知、保司回调、月底结算可能同时发生。压测模型要包含读写比例、下游延迟抖动、MQ 积压恢复、数据库慢 SQL、单活动热点等情况。否则在线上第一次遇到组合拳时,线程池参数很可能失真。
常见坑与排查
第一个坑是使用 Executors.newFixedThreadPool。它的队列无界,高峰期会把任务无限堆在内存里。排查时可以看堆内存、Full GC、线程池队列长度。如果队列没有容量上限,就要改成显式 ThreadPoolExecutor 加有界队列。
第二个坑是 MQ 消费成功和本地异步执行解耦。消费者收到消息后 executor.execute(),然后立刻返回消费成功。如果线程池里的任务还没执行,应用重启或任务执行失败,MQ 不会再投递,业务数据就丢了。关键任务应在消费线程内完成,或确保任务先持久化,再异步处理。
第三个坑是 CallerRunsPolicy 用在 Web 入口。它会让请求线程执行异步任务,短期能降低任务提交速度,但高峰期会把接口响应时间拖长,进一步占满 Tomcat 线程。它更适合内部生产者可被反压的场景,不适合所有入口接口。
第四个坑是线程池和数据库连接池不匹配。比如结算线程池 100 个线程,数据库连接池 30 个连接,大量线程会卡在获取连接。排查时要同时看线程栈和连接池等待指标。如果线程栈大量停在 getConnection,调大线程池没有意义,应优化 SQL、减少并发或增加连接池并评估数据库承载。
第五个坑是没有传递上下文。异步线程里丢失 traceId、用户 ID、活动 ID,排查问题时只能看到线程池报错,无法定位具体业务单。解决方式是在提交任务时封装业务上下文,或者使用支持上下文传递的装饰器,把 MDC、trace 信息复制到工作线程,并在 finally 中清理。
第六个坑是超时时间不一致。入口接口超时 2 秒,下游 RPC 超时 5 秒,线程池任务还在继续跑。用户已经失败,但后台仍然占用资源,甚至重复写状态。超时要分层设计:入口超时、任务超时、RPC 超时、数据库语句超时应该有明确关系,通常下游超时要小于上游等待时间。
第七个坑是只看平均耗时。线程池排队问题往往体现在 P95、P99 和最大耗时。平均 100ms 不代表没有问题,可能 90% 任务 20ms,10% 任务 1s。高峰期要重点看尾延迟,因为尾部慢任务会持续占用线程。
第八个坑是月底结算大事务。一个线程处理几万条结算明细,事务开得很大,一旦失败全部回滚,还会持有锁。正确做法是分批提交,批次状态可追踪,失败批次可重跑,避免长事务和大锁范围。
排查线程池问题时,可以按这个顺序来:先看入口 QPS 和响应时间,再看 Sentinel 是否限流,再看 Tomcat 线程是否打满,然后看业务线程池 active、queue、reject,再看 MQ 消费延迟,接着看数据库连接池、慢 SQL 和下游 RPC 耗时。必要时抓线程栈,确认线程到底阻塞在锁、IO、数据库连接、远程调用还是队列等待。
面试追问
- 为什么不用
Executors.newFixedThreadPool?
因为它默认使用无界队列,高峰期任务生产速度超过消费速度时不会及时拒绝,而是把任务堆在内存里,造成延迟膨胀、Full GC 甚至 OOM。生产环境更推荐显式使用 ThreadPoolExecutor,配置有界队列、命名线程工厂、监控指标和自定义拒绝策略。
- RocketMQ 已经削峰了,为什么还需要线程池隔离?
MQ 解决的是入口峰值和异步解耦,但消费端仍然需要本地并发控制。如果不同业务共用消费线程或共用业务线程池,月底结算积压可能拖慢商机流转,短信通知也可能影响核心状态回写。线程池隔离是把不同优先级、不同依赖、不同耗时模型的任务拆开,限制故障扩散。
- 线程池最大线程数怎么定?
先判断任务是 CPU 密集还是 IO 密集,再看瓶颈资源。CPU 密集接近核数;IO 密集可以放大,但不能超过数据库连接池、HTTP 连接池、下游限流阈值和机器内存能承受的范围。最终通过压测和线上指标校准,重点看吞吐、P99、队列长度、拒绝数和下游资源水位。
- 队列设置大一点是不是更安全?
不一定。队列大可以减少瞬时拒绝,但会增加排队延迟,并隐藏系统过载。对实时链路来说,排队 30 秒后再执行没有意义。队列容量应该根据可接受等待时间和消费吞吐反推,而不是随便设置一个很大的数字。
- 线程池满了怎么办?
要看任务类型。可降级任务可以记录日志后丢弃或延迟;关键业务任务要返回 MQ 重试、落失败表或触发补偿;入口请求可以快速失败并提示稍后重试。不能让关键保险单据在拒绝时静默丢失,也不能无限阻塞导致上游线程被拖垮。
- 如何保证异步任务不丢?
核心是先持久化再异步。可以使用本地消息表、事务消息、任务表状态机或 MQ 重试。消费者处理时要幂等,失败要可重试,超过重试次数要进入死信或失败表,并提供补偿任务。对于关键任务,不能简单 execute() 后立即 ack MQ。
- Sentinel 限流和线程池拒绝有什么区别?
Sentinel 是更靠前的入口保护或资源保护,尽量在请求进入重业务逻辑前快速拦截。线程池拒绝发生在任务提交阶段,说明本地执行资源已经紧张。前者更主动,后者更像最后一道保护。稳定性设计中,两者通常配合使用。
- 月底结算为什么不能开很多线程快速跑完?
结算瓶颈通常在数据库、锁、事务和批量写入,不一定在 CPU。线程过多会打满连接池、增加锁等待、放大慢 SQL 影响,还可能影响在线交易。正确方式是分批、限速、幂等、可恢复,并根据数据库水位逐步调并发。
推荐回答
如果面试官问“你们线程池是怎么设计的”,可以这样回答:
我们当时的保险销售活动有明显高峰,尤其是月底冲刺和活动开始时,投保提交、商机流转、通知、结算任务会叠在一起。最初的问题不是单个接口慢,而是慢调用和批任务把公共线程池、数据库连接池、MQ 消费端一起拖住。所以我们做了三层治理:入口用 Sentinel 按接口和活动维度限流;核心提交流程只保留幂等校验、主单落库和消息发送;非核心动作通过 RocketMQ 异步削峰,再按业务类型进入不同线程池。
线程池没有使用 Executors 默认工厂,而是显式配置 ThreadPoolExecutor。我们把商机流转、保司接口、月底结算、通知任务拆成独立线程池。商机流转要求可靠,拒绝后让 MQ 重试或落失败表;保司接口线程池最大并发比较保守,防止外部慢调用拖垮本地;月底结算线程池并发较小,重点控制批大小和数据库压力;通知任务优先级低,可以延迟。
调参时我们不是单纯把线程数调大,而是结合任务耗时、数据库连接池、下游限流和压测结果来定。比如商机流转看吞吐和积压,保司调用看外部接口 P99 和熔断次数,结算看慢 SQL、连接池等待和锁等待。监控上我们看 active、queue、reject、任务耗时、MQ lag 和 DB 水位。这样做之后,高峰期即使通知或结算积压,也不会影响用户提交和核心商机推进。
如果继续追问“线程池满了怎么处理”,可以补充:
我们的策略是按业务价值区分。实时入口超过 Sentinel 阈值就快速失败;非关键通知可以降级;商机、结算这类关键任务不能丢,线程池拒绝时返回 MQ 重试,超过次数进入失败表,后面有补偿任务按业务单号重跑。所有异步任务都要求幂等,避免 MQ 重试导致重复分配商机或重复生成结算明细。
如果追问“怎么发现问题”,可以回答:
我们给每个线程池做了指标埋点,包括活跃线程、队列长度、拒绝数、任务耗时 P95/P99,并把线程名前缀带上业务含义。排查时结合 traceId、MQ 消费延迟、DB 连接池等待和慢 SQL。比如看到活动提交变慢,会先看 Sentinel block、Tomcat 线程、商机线程池队列,再看下游保司接口和数据库连接池,而不是盲目加线程。
延伸学习路线
第一阶段,掌握 ThreadPoolExecutor 源码和参数语义。重点理解任务提交流程、核心线程和最大线程的关系、有界队列和无界队列的差异、拒绝策略触发条件、线程回收机制,以及 execute 和 submit 异常处理差异。
第二阶段,学习并发资源建模。把线程池和 CPU、内存、数据库连接池、HTTP 连接池、RPC 超时、锁竞争放在一起看。能够解释为什么线程数增加后吞吐可能下降,为什么队列变大后接口超时反而更严重。
第三阶段,学习异步架构可靠性。重点掌握 RocketMQ 消息重试、死信队列、消费幂等、事务消息、本地消息表、失败任务补偿、状态机流转。保险场景尤其要关注“不能丢、不能重、可追踪、可修复”。
第四阶段,学习稳定性治理。掌握 Sentinel 限流、熔断、热点参数限流、系统自适应保护,以及线程池隔离、舱壁模式、降级策略。要能讲清楚限流、熔断、隔离、削峰分别解决什么问题。
第五阶段,学习压测和监控。会设计混合流量压测模型,模拟活动高峰、下游变慢、MQ 积压恢复、数据库慢 SQL、月底结算并行执行等场景。监控上要从平均值思维切换到 P95/P99、队列趋势、拒绝趋势、资源水位和链路追踪。
第六阶段,结合真实项目复盘。准备两三个具体案例:比如保司接口慢导致线程池堆积如何处理,月底结算打满数据库连接池如何降并发和拆批,商机流转消息重复消费如何通过幂等键解决。资深面试不喜欢抽象背诵,更看重你能不能把原理放回业务现场,并说明当时为什么这么取舍。