JUC核心工具详解:CountDownLatch、Semaphore、CompletableFuture、BlockingQueue

JUC核心工具详解:CountDownLatch、Semaphore、CompletableFuture、BlockingQueue 对应简历段落 在保险销售活动、商机流转、月底结算等高并发链路中,基于 JUC 工具完成批量任务编排、外部接口并发控制、异步结果聚合和任务队列治理。熟悉 CountDownLatch、Semaphore、CompletableFuture、BlockingQueue 等并发工具的底层语义和适用边界,能够结合 RocketMQ、线程池、限流熔断、幂等补偿设计稳定的异步处理方案。 这段简历表面是在说“会用 JUC”,但资深面试官通常会继续追问:为什么这里用 Semaphore 而不是调大线程池?CompletableFuture.allOf 异常怎么处理?BlockingQueue 容量怎么估?CountDownLatch 卡死怎么排查?这些问题的本质不是 API 记忆,而是你是否理解并发工具背后的资源约束、失败传播和业务一致性。 业务背景 保险销售系统里经常出现“一个请求触发多类子任务”的场景。比如代理人进入客户详情页,系统需要并行查询客户画像、历史保单、活动资格、产品推荐、商机跟进记录和权益信息。如果全部串行调用,接口延迟会被所有下游耗时累加;如果无脑并行,又可能把数据库连接池、保司接口或画像服务打满。 商机流转也类似。投保意向创建后,需要完成商机分配、代理人通知、客户标签刷新、运营埋点、待办生成、活动库存预占等动作。不同动作优先级不同,有的必须成功,有的可以延迟,有的失败后要重试,有的可以降级。并发工具的价值,是把这些动作从“代码顺序调用”升级成“有边界、有预算、有补偿的并发编排”。 月底结算的压力更典型。结算任务可能按机构、产品、代理人、月份拆成大量批次。每个批次要读取订单、计算佣金、写结算明细、生成对账记录。这里既需要并发提升吞吐,又要避免同一机构或同一代理人的数据被重复计算,还要在某个批次失败时支持重跑。JUC 工具用得好,可以把复杂批处理做得清晰;用得不好,会留下卡死、线程泄漏、任务丢失和结果不一致。 核心原理 CountDownLatch 是一次性倒计时门闩。它的核心语义是:一个或多个线程等待其他线程完成指定数量的事件。它适合“主线程等待多个子任务完成后再汇总”的场景,例如结算任务拆成 10 个分片,全部处理结束后更新总任务状态。它不能重置,计数归零后就永久打开。如果需要反复使用,应考虑 CyclicBarrier 或重新创建对象。 Semaphore 是信号量,本质是许可证池。线程执行某段逻辑前先获取许可证,执行完释放许可证。它经常用于限制外部依赖并发,例如保司接口最多允许本系统同时 20 个请求,或者某个导出任务最多同时跑 3 个。它和线程池不同:线程池限制的是任务执行线程数量,Semaphore 限制的是某个临界资源的并发访问量。一个线程池内可能访问多个外部资源,每个资源应该有自己的并发预算。 CompletableFuture 是异步编排工具。它既能提交异步任务,也能表达任务之间的依赖关系,比如并行查询后聚合、前一步成功后继续、失败后兜底、任一任务完成后返回。它比裸 Future 更适合业务编排,但也更容易埋坑:默认线程池是 ForkJoinPool.commonPool,异常会被包在 CompletionException 里,allOf 只告诉你整体完成,不直接返回每个子任务结果。 BlockingQueue 是阻塞队列,是生产者消费者模型的核心组件。线程池的任务队列、异步日志队列、批量写入缓冲都依赖它。常见实现包括 ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue、SynchronousQueue。面试时要能说清楚:有界还是无界、公平还是非公平、是否保持优先级、是否支持延迟、入队出队是否可能阻塞,以及阻塞后如何响应中断。 这四类工具的共同点是:它们都不直接解决业务问题,而是提供并发控制原语。真正的工程能力体现在你能把它们和业务状态机、超时控制、监控告警、失败补偿结合起来。 项目落地 在活动资格聚合接口中,可以用 CompletableFuture 并行查询多个下游。同步链路只保留用户最关心的信息:活动资格、产品可售状态、保费试算结果。客户画像、推荐标签、营销权益可以设置更短超时,失败后返回默认值,避免弱依赖拖慢主链路。 在保司接口调用中,可以用 Semaphore 控制并发。即使业务线程池有 64 个线程,也不代表保司接口能承载 64 个并发。对外部系统要按 SLA 设置独立许可证数,并配合 tryAcquire(timeout)。拿不到许可证时快速降级或进入稍后重试,而不是无限等待。 在月底结算中,可以用 CountDownLatch 等待分片任务结束,但要注意不能让主调度线程永久等待。每个分片任务必须在 finally 中 countDown,并且总等待要带超时。更稳妥的方式是任务状态落库,调度线程只负责派发,完成状态由分片任务异步更新;CountDownLatch 适合单进程内短生命周期编排,不适合跨机器、长时间批处理的最终一致性。 在商机流转消费者中,可以用 BlockingQueue 做本地缓冲,但必须有容量和拒绝策略。高峰期 MQ 消费速度大于本地处理速度时,如果本地队列无界,堆内存会被任务对象慢慢撑满。更合理的做法是有界队列配合 MQ 重试:队列满时返回稍后消费,让压力留在 MQ,而不是压在 JVM 堆里。 ...

January 6, 2025 · 2 min · 318 words · WY

高并发场景下的限流、降级、熔断与隔离

高并发场景下的限流、降级、熔断与隔离 对应简历段落 在保险销售活动和月底结算高峰场景中,负责构建稳定性治理方案,包括 Sentinel 限流、接口降级、下游熔断、线程池隔离、MQ 削峰和核心链路监控告警。针对活动秒级流量突增、保司接口抖动、CRM 同步慢、结算任务集中触发等问题,保障核心投保和商机流转链路在资源有限的情况下稳定运行。 这段简历的关键在“治理”而不是“组件”。面试官会追问:限流保护谁?降级牺牲什么?熔断依据是什么?隔离隔的是什么资源?如果只能背 Sentinel 规则,很难通过资深面试。优秀回答必须把业务优先级、资源预算、失败策略和监控闭环讲清楚。 业务背景 保险销售活动高峰具备突发性。活动开始、产品停售、月底冲刺、代理人集中触达都会让投保提交、保费试算、商机查询、客户画像接口瞬间升高。系统资源却是有限的:Tomcat 线程有限,业务线程池有限,数据库连接有限,保司接口也有限。 如果没有治理,流量会按最自然的方式冲进系统。入口线程被占满、数据库连接池等待、下游 RPC 超时、MQ 消费堆积、重试流量反复放大。最后结果往往是所有接口一起变慢,而不是核心业务优先成功。 稳定性治理的目标不是让系统处理无限流量,而是在超过承载能力时有控制地失败。核心投保、商机创建、支付或结算状态推进要优先保障;短信通知、运营埋点、推荐标签、报表刷新可以延迟或降级;外部系统慢时要及时切断影响;批处理任务不能挤占在线交易资源。 核心原理 限流是入口控制,解决“流量进来太多”的问题。常见维度包括 QPS、并发数、热点参数、用户维度、活动维度、渠道维度。限流的本质是把超过系统承载能力的请求挡在便宜的位置,避免它们进入数据库、RPC 和复杂业务逻辑。 降级是功能取舍,解决“资源不够时先保什么”的问题。比如活动页可以不展示个性化推荐,客户画像可以返回基础标签,短信通知可以延迟发送。降级不是异常处理的别名,而是提前定义好的业务兜底方案。 熔断是故障隔离,解决“下游持续失败或慢调用拖垮上游”的问题。当保司接口、CRM、客户画像服务出现高错误率或高慢调用比例时,上游短时间内不再持续请求它,而是快速失败、返回缓存或进入异步重试。熔断保护的是调用方线程和整体系统稳定性。 隔离是资源切分,解决“不同业务互相拖垮”的问题。隔离可以是线程池隔离、连接池隔离、部署隔离、队列隔离、数据库表隔离。月底结算和在线投保不能共用同一批核心线程;短信通知不能占用商机分配线程池;外部保司调用要有独立并发预算。 这四者经常组合出现:入口先限流,弱依赖可降级,下游异常时熔断,不同业务用资源隔离控制影响范围。再配合 MQ 削峰和监控告警,形成完整稳定性方案。 项目落地 活动提交流程中,入口先按活动 ID 和接口维度做限流。比如投保提交比活动页查询更核心,但提交接口成本更高,所以限流阈值要基于数据库写入能力和下游消息投递能力评估。被限流的请求返回明确提示,避免用户反复点击造成更大压力。 活动页查询中,推荐标签、权益推荐、客户画像属于可降级能力。正常情况下并行查询并展示个性化内容;当画像服务超时或线程池排队过长时,返回默认推荐或基础产品信息。这样页面仍可打开,用户仍能完成核心投保路径。 保司接口调用中,熔断非常关键。保司慢调用会占住业务线程,还可能导致重试叠加。可以按保司渠道配置慢调用比例和异常比例阈值。熔断打开后,在线请求不再同步等待保司,而是返回“已受理,稍后更新状态”,并通过 MQ 或补偿任务异步查询。 月底结算中,隔离比限流更重要。结算任务数据量大、运行时间长、数据库写入重。如果和在线活动共用线程池、连接池或部署实例,很容易在月底拖慢用户链路。推荐单独线程池、单独消费组、独立调度窗口,必要时独立部署结算 worker。 商机流转中,MQ 削峰和消费限速配合使用。生产端高峰消息先进入 MQ,消费端根据数据库和代理人分配服务承载能力设置消费线程数。消费端线程池满时返回稍后重试,而不是无限堆积本地队列。 关键伪代码或流程 入口限流示意: public SubmitResult submit(SubmitCommand command) { Entry entry = null; try { entry = SphU.entry("activity-submit-" + command.activityId()); return submitService.doSubmit(command); } catch (BlockException ex) { return SubmitResult.busy("活动参与人数较多,请稍后重试"); } finally { if (entry != null) { entry.exit(); } } } 弱依赖降级: ...

January 5, 2025 · 1 min · 213 words · WY

MQ削峰填谷在商机流转系统中的落地

MQ削峰填谷在商机流转系统中的落地 对应简历段落 在保险销售活动系统中,针对活动高峰期投保意向集中创建、代理人批量跟进、保司状态回写和商机流转压力突增的问题,引入 RocketMQ 完成核心链路异步解耦与削峰填谷。负责消息模型设计、消费端线程池隔离、幂等处理、重试补偿、积压监控和故障恢复,保障商机在高并发下稳定流转。 这段简历的重点不是“用了 MQ”,而是你能否讲清楚:为什么这里需要 MQ?削峰削的是哪个峰?填谷填到哪里?消息失败、重复、乱序、积压时怎么保证业务正确?面试官通常会沿着“可靠性、一致性、可观测性、容量规划”连续追问。 业务背景 商机流转是保险销售链路中的核心后置流程。用户在活动页提交投保意向后,系统要创建商机、分配代理人、生成跟进待办、推送企微或短信、刷新客户标签、同步 CRM、触发后续营销策略。活动高峰时,入口请求具有明显脉冲:一分钟内可能涌入大量投保意向,但代理人通知、标签刷新、CRM 同步并不需要全部在用户请求内完成。 如果同步执行这些动作,用户提交接口会被下游耗时拖慢。短信通道慢、CRM 接口抖动、标签服务超时都会影响用户提交成功率。更严重的是,同步链路中每多一个外部依赖,就多一个高峰期雪崩点。入口线程被慢调用占满后,即使数据库和核心业务还健康,用户也会看到大量超时。 MQ 的价值在于把瞬时高峰变成可控消费。用户请求内只完成必要校验和主单落库,然后发送商机流转消息。后续动作由消费者按自身能力慢慢处理。这样入口系统承接的是写主单和投递消息的压力,商机处理系统承接的是可调速的消费压力。 核心原理 MQ 削峰填谷的核心是生产速度和消费速度解耦。没有 MQ 时,生产者和消费者在同一个调用栈里,消费者慢会直接拖慢生产者。有 MQ 后,生产者把事件写入队列即可返回,消费者按照配置的线程数、批量大小和下游承载能力消费。 削峰不是消灭流量,而是把峰值暂存在消息系统里。填谷是指在业务低峰时继续消费积压,把高峰期没处理完的任务逐步补齐。因此,设计时不能只看入口 QPS,还要看消息积压容量、可接受延迟、消费者吞吐和失败重试成本。 消息系统带来的新问题是可靠性和一致性。投保意向主单落库成功但消息发送失败,商机就不会流转;消息发送成功但消费者处理失败,需要重试;消费者处理成功但提交 offset 失败,消息可能重复;同一客户多条消息并发消费,可能导致状态乱序。因此,业务必须天然接受“至少一次投递”,通过幂等和状态机保证重复消息不会产生错误结果。 常见可靠性方案包括事务消息、本地消息表、消费幂等表、业务唯一索引、失败重试队列和人工补偿入口。对保险销售系统来说,消息可以延迟,但关键商机不能丢;通知可以重复风险低,但佣金、权益、分配结果不能重复。 项目落地 商机流转消息设计时,应避免把所有业务动作塞进一个大消息。更合理的是按领域事件拆分:OpportunityCreatedEvent 表示投保意向创建;OpportunityAssignedEvent 表示商机已分配;FollowTaskCreatedEvent 表示跟进任务已生成;InsurerStatusChangedEvent 表示保司状态变化。事件之间通过业务状态推进,而不是消费者互相调用形成隐式链路。 生产端要保证主单和消息的一致性。可以使用 RocketMQ 事务消息:先发送半消息,再执行本地事务创建投保意向,最后提交或回滚消息。也可以使用本地消息表:在同一个数据库事务中写主单和待发送消息,由后台任务可靠投递。两种方式都可以,选择取决于团队基础设施和运维经验。 消费端要按业务优先级拆分消费组和线程池。商机分配属于核心后置链路,应使用独立消费组和线程池;短信通知、运营埋点可以单独消费,允许更长延迟;CRM 同步依赖外部系统,必须设置并发上限和熔断策略;月底结算消息不能和在线商机消息共用线程池。 幂等设计是落地重点。消费者处理 OpportunityCreatedEvent 时,应以 eventId 或 opportunityId + actionType 作为幂等键。先尝试插入消费记录,插入成功才执行业务;如果唯一键冲突,说明已经处理过或正在处理,需要根据状态返回成功或稍后重试。业务表本身也要有条件更新,例如只有 WAIT_ASSIGN 状态才能分配代理人。 重试策略要区分临时失败和永久失败。数据库死锁、RPC 超时、线程池满属于临时失败,可以让 MQ 延迟重试;参数缺失、业务状态非法、客户不存在属于永久失败,应记录失败原因并进入人工补偿或死信处理。不能所有异常都无限重试,否则会制造重试风暴。 关键伪代码或流程 生产端本地消息表: @Transactional public SubmitResult submitOpportunity(SubmitCommand command) { Opportunity opportunity = opportunityRepository.create(command); DomainEvent event = DomainEvent.builder() .eventId(IdGenerator.next()) .bizKey(opportunity.id()) .eventType("OPPORTUNITY_CREATED") .payload(toJson(opportunity)) .status("INIT") .build(); localMessageRepository.insert(event); return SubmitResult.success(opportunity.id()); } 可靠投递任务: ...

January 4, 2025 · 2 min · 238 words · WY

锁机制详解:synchronized、ReentrantLock、读写锁与分布式锁的边界

锁机制详解:synchronized、ReentrantLock、读写锁与分布式锁的边界 对应简历段落 在保险销售活动、商机流转和月底结算链路中,负责高并发下的共享状态保护、幂等控制和重复处理治理。熟悉 Java 锁机制,包括 synchronized、ReentrantLock、读写锁以及 Redis/ZooKeeper 分布式锁的适用边界,能够结合数据库唯一索引、状态机、事务消息和补偿机制保证业务一致性。 这段简历面试时非常容易被追问。面试官不是想听“synchronized 是关键字、ReentrantLock 是类”,而是想知道你是否明白:单 JVM 锁和分布式锁保护的边界不同;锁不是越大越安全;锁只能保护临界区,不能替代幂等;高并发系统的正确性往往要靠锁、事务、唯一约束和状态流转共同完成。 业务背景 保险销售系统里有大量“不能重复”的动作。一个客户不能在同一活动下重复领取权益;同一投保意向不能被两个代理人同时认领;同一结算批次不能重复生成佣金明细;同一保司回调不能反复推进订单状态。高峰期这些动作会被用户重复点击、MQ 重试、定时任务补偿、运营后台批量操作同时触发。 如果只在代码里加一个本地锁,在单机部署时可能暂时有效;一旦服务多实例部署,同一个业务单号的请求可能打到不同机器,本地锁就失效。如果所有地方都上分布式锁,又会增加 Redis 压力、锁等待时间和故障复杂度。资深工程实践的关键,是先识别资源边界:这个共享状态是在单 JVM 内、单数据库行内、跨进程、跨服务,还是跨外部系统。 比如活动库存扣减。如果库存存在数据库表中,最核心的保护应该是数据库原子更新和唯一流水,而不是单纯 Redis 锁。比如本地缓存刷新,多个线程同时构建同一份缓存,可以用 JVM 内锁避免缓存击穿。比如月底结算批次被多台机器扫描,可以用数据库任务抢占或分布式锁协调。不同场景的锁完全不同。 核心原理 synchronized 是 JVM 内置监视器锁,具备互斥和可见性。进入同步块前会获取对象监视器,退出同步块会释放锁。它是可重入锁,同一线程可以重复进入同一把锁保护的代码。现代 JVM 对它做了很多优化,包括偏向锁历史机制、轻量级锁、自旋、锁消除、锁粗化等。虽然不同 JDK 版本实现细节会变化,但面试表达时抓住语义即可:它简单、可靠、自动释放,适合临界区短、竞争不复杂的场景。 ReentrantLock 是基于 AQS 的显式锁。相比 synchronized,它支持可中断等待、超时获取、公平锁选择、多个条件队列。代价是必须手动释放,通常要写在 finally 中。它适合需要 tryLock、等待可中断、精细条件唤醒的场景。比如批量任务抢占时,拿不到锁就跳过当前批次,而不是阻塞线程。 读写锁的核心思想是读读共享、读写互斥、写写互斥。ReentrantReadWriteLock 适合读多写少、读操作耗时相对明显、共享数据结构较稳定的场景。它不适合写频繁场景,也不能解决跨 JVM 一致性。使用读写锁时要关注锁降级、写锁饥饿、读锁持有期间禁止升级写锁等问题。 分布式锁用于协调多个进程或多台机器访问同一逻辑资源。常见实现是 Redis SET key value NX PX ttl 加 Lua 脚本释放,或者 ZooKeeper 临时顺序节点。Redis 锁性能高但要处理过期、误删、续期、主从切换风险;ZooKeeper 锁一致性更强但性能和运维成本更高。分布式锁不是银弹,它只能降低并发冲突概率,最终业务正确性仍要靠数据库约束、幂等键和状态机兜底。 项目落地 在客户重复提交投保意向场景中,最可靠的方案不是先加锁,而是设计幂等键。比如以 activityId + customerId + productId 作为唯一约束,投保意向创建时先查再插不可靠,因为并发下两个线程都可能查不到。正确方式是在数据库层加唯一索引,插入冲突后查询已有单据返回。锁可以减少冲突,但不能替代唯一约束。 ...

January 3, 2025 · 2 min · 282 words · WY

异步化改造:从同步调用到事件驱动

异步化改造:从同步调用到事件驱动 对应简历段落 在保险销售活动和商机流转系统中,负责将投保提交、商机分配、客户通知、CRM 同步、保司状态回写等长链路从同步调用改造为事件驱动架构。通过核心链路瘦身、RocketMQ 事件解耦、线程池隔离、幂等补偿和监控告警,降低接口超时和下游抖动对用户体验的影响,提升高峰期系统稳定性。 这段简历面试时要特别小心。异步化不是把 service.call() 改成 executor.submit(),也不是简单发一条 MQ。真正的异步化改造包括业务边界重划、状态机设计、一致性方案、可观测性和失败恢复。如果说不清这些,面试官会认为你只是做了技术包装。 业务背景 改造前的投保提交链路往往很长:校验活动资格、查询客户信息、计算保费、创建投保意向、分配代理人、发送短信、写运营埋点、同步 CRM、调用保司预核保、生成跟进待办。平时流量低时,这种同步链路看起来简单直接;活动高峰时,任何一个下游变慢都会导致整个接口超时。 同步链路还有一个隐性问题:调用方和被调用方的可用性强绑定。短信通道抖动不应该影响投保意向创建,CRM 同步失败不应该让用户提交失败,运营埋点慢也不应该占用入口线程。但同步代码会天然把它们绑在一起,导致弱依赖拖垮强业务。 异步化的目标不是“所有事情都异步”,而是把用户实时感知链路和后置处理链路拆开。用户请求内必须完成的是:请求合法、业务幂等、核心单据落库、用户能得到明确结果。后置链路可以延迟处理,但必须可靠、可追踪、可补偿。 核心原理 同步调用是命令式思维:A 调 B,B 成功后 A 再调 C。调用关系清晰,但耦合强、延迟累加、故障传播直接。事件驱动是事实式思维:A 完成自己的业务状态变更后发布事件,B、C、D 订阅这个事件并各自处理。发布者不关心订阅者数量和处理耗时,订阅者失败也不直接影响发布者。 事件驱动的关键是事件语义。事件应该表示“已经发生的业务事实”,例如 OpportunityCreated、OpportunityAssigned、PolicyStatusChanged,而不是 DoAssignCommand、SendSmsCommand 这种远程命令。事实事件更稳定,也更适合多个消费者扩展。 异步化必须接受最终一致性。同步链路中,调用完成即可认为所有动作完成;异步链路中,主单创建成功时,商机可能还未分配,通知可能还未发送,CRM 可能还未同步。业务页面、运营后台和客服工具都要能展示中间状态,并支持重试和补偿。 异步化还要处理失败传播方式的变化。同步调用失败可以直接抛异常;异步消费失败不能让用户重新提交,它需要重试、死信、失败表、人工处理和状态回滚策略。没有这些配套,异步化只会把错误藏起来。 项目落地 第一步是链路梳理。把原同步接口中的动作按重要性拆分为强依赖、弱依赖和后置任务。强依赖包括活动资格、重复提交幂等、核心单据创建;弱依赖包括客户标签、营销权益展示;后置任务包括商机分配、短信通知、CRM 同步、运营埋点。强依赖留在同步链路,弱依赖设置短超时和降级,后置任务事件化。 第二步是状态机设计。投保意向不能只有“成功/失败”,而应有 CREATED、WAIT_ASSIGN、ASSIGNED、NOTIFIED、SYNCED_CRM、FAILED 等状态或子状态。异步消费者每完成一个动作,就推进对应状态。页面可以显示“已提交,正在分配顾问”,运营后台可以看到哪些环节失败。 第三步是事件发布一致性。核心单据落库和事件发布必须绑定。可以用事务消息,也可以用本地消息表。对于团队可控性强的业务系统,本地消息表很常见:业务事务里写主单和消息记录,后台投递任务负责发送 MQ,发送成功后标记状态。 第四步是消费者解耦。一个事件可以有多个消费者,但每个消费者只做自己的事情。商机分配消费者只负责代理人分配和状态推进;通知消费者只负责发送通知;CRM 消费者只负责外部同步。消费者之间不要直接互相调用,否则事件驱动会退化成隐藏的同步链路。 第五步是补偿闭环。异步任务要有清晰的失败表、重试次数、错误原因、最后处理时间和人工触发入口。比如 CRM 同步失败不影响用户提交,但运营人员需要看到失败记录并可重新同步。商机分配失败则影响销售跟进,应设置更高优先级告警。 关键伪代码或流程 同步改造前: public SubmitResult submit(SubmitCommand command) { qualificationService.check(command); Opportunity opportunity = opportunityService.create(command); Agent agent = assignService.assign(opportunity); smsService.send(agent, opportunity); crmService.sync(opportunity); metricService.record(opportunity); return SubmitResult.success(opportunity.id()); } 事件驱动改造后: ...

January 2, 2025 · 1 min · 200 words · WY

线程池在保险销售高峰场景中的设计与调优

线程池在保险销售高峰场景中的设计与调优 对应简历段落 在保险销售活动系统中,参与销售活动、月底结算、商机流转等核心链路建设。针对活动高峰期用户集中投保、代理人批量跟进、保单状态异步回写、结算任务集中触发等场景,设计 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 基本不会生效。因为任务会一直进入队列,很少走到创建非核心线程那一步。所以生产系统里通常不建议使用无界队列,尤其是承载外部流量或批处理任务时。 ...

January 1, 2025 · 4 min · 640 words · WY

AgentScope4J 深度学习指南 - 从狼人杀项目掌握多智能体框架 以狼人杀多智能体游戏为实战案例,系统学习 AgentScope4J 的核心概念,并补充 2026 年最新的 Harness Framework、长记忆、分布式会话与多 Agent 编排能力。 前言 AgentScope4J 是阿里巴巴开源的多智能体框架,提供了完整的 Agent 开发基础设施。本文以 werewolf-hitl 项目为案例,这是一个带人机交互(HITL)的狼人杀多智能体游戏,先从项目里实际出现的通信、结构化输出、人机交互与多智能体协作讲起,再延伸到 AgentScope Java 1.0.10 之后补强的 Nacos、A2A、长记忆、可观测与 1.1 系列 Harness Framework。 学习路径: Msg 消息体系 — 理解通信单元 Agent 体系 — 理解代理机制 工具调用与结构化输出 — 理解 LLM 交互 WebUserInput 人机交互 — 理解异步等待 MsgHub 多智能体通信 — 理解协作机制 Hook 生命周期 — 理解执行拦截 Pipeline 编排 — 理解多 Agent 组合 Formatter、流式输出、Prompt、持久化 — 理解框架生产化基础 A2A、MCP、RAG、多模态 — 理解扩展生态 Harness Framework — 理解最近新增的工作区、记忆、会话、沙箱和子 Agent 运行时 一、Msg 消息体系 1.1 Msg 结构总览 Msg 是 AgentScope 的唯一通信单元。Agent 之间、Agent 与 Model 之间、Agent 与 Tool 之间,传递的都是 Msg。 ...

39 min · 8138 words · WY