ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java AI应用异步化与高并发实战:从阻塞到扛住千万流量

Java AI应用异步化与高并发实战:从阻塞到扛住千万流量 说个真实场景吧你开了一家餐厅生意火爆但坚持让客人站着等饭做好才落座——菜还没上座位已经被站着等的人全占完了。这是我接手一个Java AI应用时脑子里蹦出来的第一幅画面。项目不复杂就是给业务方做一个基于大模型的智能问答接口但线上表现很割裂Tomcat线程池动不动被打满AI接口平均耗时2秒QPS只要一过30就开始大量超时日志里全是“Connection pool exhausted”和“Task rejected”。这篇文章就围绕Java AI应用的异步化与高并发设计来讲。你未必在写大模型应用只要是Java后端、要调用慢外部依赖、想扛住高并发思路都通用。内容包括四块为什么AI场景必须异步化Java生态里几种异步方案怎么选高并发设计的几个关键环节以及一个能直接参考的实操案例。结尾我会把踩过的坑列一份速查表帮你有针对性地排查问题。1. 为什么AI应用比传统Web应用更需要异步化1.1 AI应用的延迟特征从毫秒级到秒级甚至分钟级传统业务接口讲究低延迟100毫秒以内是常态超过1秒用户就开始焦虑。AI应用完全不同。一次模型推理动辄几百毫秒到几秒如果涉及RAG检索增强生成要先做向量检索、再拼提示词、再调模型走完一轮轻松5到10秒。要是做成Agent形态大模型要多次调用工具、来回规划耗时到几十秒也正常。这里有一个核心矛盾HTTP连接和线程是稀缺资源而AI服务天然“慢”。同步处理模式里一个请求占住一个Tomcat线程不放手线程池只有200个意味着一瞬间只能同时处理200个请求。AI接口慢线程就大规模阻塞。用户侧看到的是请求排队、超时、5xx而服务端CPU使用率可能并不高——因为线程都在等根本没在算。我做过一个小实验把一个内部AI接口的耗时从800ms调到8秒其他条件不变系统能承受的QPS直接降了大概9倍。这就是为什么AI应用必须把“等待”和“计算”解耦不能让链路里最慢的那一环决定整个吞吐量。1.2 同步阻塞的本质线程不是等不起是太贵要理解异步化的价值先得算一笔账。JVM里每个线程默认栈大小1MB注意这是虚拟内存但它承载的并发能力确实有限。一个4核8G的机器JVM堆设置4G线程数开到500到800通常就已经偏大继续加线程会导致上下文切换开销暴涨GC也跟着恶化。而且线程不是免费工人它是“有编制的员工”——创建、销毁、调度都有成本。Tomcat通过线程池复用线程本质上就是为了摊薄这些成本。但复用的前提是线程能很快空出来接下一个任务。AI应用把线程按在那里等外部HTTP响应相当于员工站在快递柜前等包裹一等等半天活全积压了。异步化的目标恰恰是让线程只做“发起调用”和“处理结果”两件短活把漫长的等待时间交给底层的非阻塞IO和回调机制让线程去服务其他请求。1.3 异步化要解决什么线程利用率、响应速度和削峰具体来说异步化在AI场景下能带来三方面收益。第一是线程利用率。同样200个线程同步模式只能同时处理200个在途请求异步模式下可以同时挂着几千个在途请求因为线程在等待结果时已经被释放了。JVM本身不感知HTTP连接是否活跃但它感知线程所以提高线程复用就是提高并发承载量。第二是响应速度。异步模型允许一个请求拆分多个子任务并行执行。比如Agent场景需要同时调两个工具同步写就是串行等两次异步可以并行等一次耗时从T1加T2变成max(T1, T2)。这对AI链路总延迟的改善立竿见影。第三是削峰填谷。异步化常常配合消息队列一起用。用户点击提问后请求先返回“已受理”后台异步任务慢慢执行查询结果通过轮询或SSE推送返回。这样业务高峰期的突发流量不会直接压垮模型服务而是进入队列排队系统吞吐量反而更稳。2. Java生态异步方案横向对比CompletableFuture、虚拟线程、消息队列到底怎么选2.1 CompletableFutureJDK原生的异步组合方案CompletableFuture是Java 8引入的它弥补了Future的短板——不能手动编排任务、不能回调、不能组合。我在AI应用里最常用的几个方法supplyAsync()提交一个异步任务返回CompletableFuturethenApplyAsync()上一个任务完成后继续处理支持异步执行allOf()等待多个异步任务全部完成常用于并行调用多个模型exceptionally()捕获异常并返回一个默认值做降级示例代码如下public CompletableFutureAnswerResult parallelAnswer(String question) { CompletableFutureRetrievalResult retrievalFuture CompletableFuture.supplyAsync(() - retrievalService.search(question), searchExecutor); CompletableFutureLlmResult llmFuture CompletableFuture.supplyAsync(() - llmService.generate(question), llmExecutor); return retrievalFuture .thenCombine(llmFuture, (retrieval, llm) - new AnswerResult(retrieval.getContexts(), llm.getAnswer())) .exceptionally(ex - { log.error(parallel answer failed, ex); return AnswerResult.fallback(); }); }这段代码的作用是向量检索和大模型生成这两个独立步骤可以并行执行而不是检索完再生成。对RAG场景来说时间从两个步骤相加缩短为两者中的较大值。使用CompletableFuture有两点必须注意。默认使用的ForkJoinPool.commonPool非常坑它的并行度是CPU核数减1AI场景下大批量任务会把它挤爆所以务必自建线程池传入。再有就是异常处理不能遗漏异步链路里的异常不会像同步代码那样自动向上抛要在exceptionally()或者handle()里显式兜底否则用户请求会“永久悬挂”。2.2 Spring的Async和线程池配置异步落地的第一步如果你的项目是Spring Boot最省事的异步入口就是Async。但这里有个高频误区随便贴一个Async注解就完事了结果方法没异步执行。原因是Spring的异步基于AOP代理而代理对this内部调用不生效只有通过Spring容器调用bean方法时才会走代理。正确写法是把异步逻辑放到独立的Service里通过注入调用Service public class QaTaskService { Async(aiTaskExecutor) public CompletableFutureString submitLlmTask(String question) { // 实际的模型调用逻辑 return CompletableFuture.completedFuture(llmClient.call(question)); } }这里还有个细节如果方法返回值是void或者普通对象Async会把返回值丢弃想要拿到异步结果并支持扩展一定要声明为CompletableFutureT。线程池配置是最容易被忽略的。Spring Boot如果不开任何配置Async默认用的是SimpleAsyncTaskExecutor它是不复用线程的每个任务都新建线程高并发下能直接创建出几万个线程堆内存和GC一起崩。实际项目里要显式定义一个线程池BeanBean(aiTaskExecutor) public ThreadPoolTaskExecutor aiTaskExecutor() { ThreadPoolTaskExecutor executor new ThreadPoolTaskExecutor(); executor.setCorePoolSize(10); executor.setMaxPoolSize(50); executor.setQueueCapacity(1000); executor.setThreadNamePrefix(ai-task-); executor.setWaitForTasksToCompleteOnShutdown(true); executor.initialize(); return executor; }参数不是抄来的要根据下游接口的吞吐和Redis、数据库等资源综合定。核心线程数建议按“单AO接口QPS乘平均耗时”估算比如目标并发处理200个在途请求平均耗时5秒那么至少需要40到50个工作线程才可能满足。2.3 响应式编程WebFlux适合SSE流式和大规模IO密集场景WebFlux是另一条路线。它基于Netty的Reactor全链路非阻塞用Mono和Flux表示异步流。AI应用里最常见的SSEServer-Sent Events流式输出用WebFlux做得非常自然——模型生成的token一个接一个推给前端传统的Servlet模型做流式反而别扭。PostMapping(value /v1/chat, produces MediaType.TEXT_EVENT_STREAM_VALUE) public FluxServerSentEventString chat(RequestBody ChatRequest request) { return llmService.streamResponse(request.getPrompt()) .map(token - ServerSentEvent.builder(token).build()); }但WebFlux不是银弹它要求整个调用链都非阻塞否则还是会卡线程。如果项目组对响应式编程不熟WebClient、R2DBC、非阻塞Redis这些中间件都要适配学习成本和排查成本比异步Servlet高得多。我的看法是团队里有Reactor经验、或者确实需要长连接流式服务可以考虑如果只是为了“异步化”这一个目标优先选CompletableFuture加虚拟线程或消息队列。2.4 虚拟线程JDK 21带来的高并发新解法虚拟线程是JDK 21正式引入的。它的核心思路是不再让操作系统线程承载Java线程的所有生命周期虚拟线程挂起时只占几十KB内存平台线程空闲后立刻去执行别的虚拟线程。通俗说以前一个并发请求占一个1MB栈空间的线程现在只占一点轻量级调度单元阻塞不再是罪过。虚拟线程在AI场景确实非常舒坦。以前同步代码要改异步逻辑被拆得七零八落。虚拟线程允许你继续写同步风格代码底层却把阻塞挂起释放给平台线程。比如下面的代码按理说是个同步阻塞调用但在虚拟线程里模型等待期间平台线程并没有被占着ExecutorService virtualExecutor Executors.newVirtualThreadPerTaskExecutor(); public void handleRequest(String question) { virtualExecutor.submit(() - { String context retrievalService.search(question); // 阻塞但轻量 String answer llmService.generate(context); // 阻塞但轻量 notifyClient(answer); }); }要注意虚拟线程也不是万能的。它不适合CPU密集任务也不适合带Synchronized块内执行阻塞IO的情况因为虚拟线程遇到synchronized会锁定载体线程出现“固定”现象阻塞照样占住平台线程。此外虚拟线程不应该池化它是轻量到“用完即扔”的搞一个虚拟线程池反而没意义。2.5 消息队列异步生产消费模式扛流量利器异步化不只是线程模型改造还包含任务队列。我强烈建议AI应用中凡是需要长时间处理的任务都考虑消息队列比如RocketMQ或Kafka。用消息队列处理AI请求的模式大概是用户请求 - Controller快速响应“已提交” - 消息发到队列 - AI消费组拉取消息 - 调模型 - 结果写入结果表/推送SSE这种模式天然削峰填谷。模型服务能力有限队列不用担心消费者处理不过来消息都会积压不会直接丢弃。而且多个消费者实例可以水平扩展后端加的机器越多处理能力越强这个在云原生环境下很友好。代价是链路变长了带来额外的消息中间件运维成本和至少几十毫秒的投递延迟。只有业务能接受异步化的比如工单处理、资料分析、报告生成这类非实时场景再用队列。如果用户必须立刻拿到完整响应那还是用同步加异步线程配合限流。2.6 方案选型速查一张表把场景对应清楚方案适用场景运维成本推荐度CompletableFuture单请求内多个慢调用并行编排低高Async单个异步子任务配合业务线程池低中高WebFluxSSE流式输出、异步网关非阻塞链路中中虚拟线程保持同步代码风格提升阻塞型高并发承载低高消息队列超大流量削峰、任务解耦、异步结果回写高中选型没有标准答案。我的习惯是默认用虚拟线程处理阻塞调用用CompletableFuture编排并行的子步骤如果场景确实需要流式再用WebFlux或者Servlet异步加SSE最后把重任务放到队列去削峰。整套组合下来既兼顾开发效率也扛得住实际流量。3. 高并发设计的四个关键环节3.1 限流保护自己更是保护下游AI服务高并发系统第一个要求是“活下来”不是“接得下”。限流就是给系统装刹车。尤其AI场景下游模型服务往往按Token计费有QPS配额你不做限流模型供应商先把你的建联断掉整个服务直接雪崩。限流可以放在网关层用Sentinel或自研拦截器实现令牌桶算法。令牌桶的好处是允许突发流量只要桶里有令牌就能放行而不是死板地平均分配。Component public class AiRateLimiter { private final RateLimiter rateLimiter RateLimiter.create(100); public boolean tryAcquire() { return rateLimiter.tryAcquire(); } }真正上线前要估算清楚限流阈值。比如目标QPS 100平均一次模型调用消耗5秒那么同时挂在系统中的请求就是500个加上超时和重试的放大实际要预留至少1.5倍余量。阈值设太低了业务投诉太高了下游被打挂。所以限流要结合压测数据动态调整。另一个细节是区分“普通用户”和“VIP用户”的配额。用同一个限流器会有饥饿问题大流量用户把令牌抢光付费用户进不来。实际做法是分桶限流每个用户组独立配额整体并发再设一个总闸。3.2 熔断与降级AI服务挂了你的服务不能跟着挂熔断和限流是两回事。限流是防止流量过大熔断是防止下游故障向上游蔓延。我在实际项目里遇到过一个典型模型供应商服务不稳定响应偶尔超时到30秒结果我的服务线程全被这些慢调用占住后续正常请求也跟着超时。这就是故障放大效应。用Resilience4j做熔断非常直接。它支持基于失败率、慢调用率来打开熔断器打开后直接走降级逻辑不再实际调用下游。Bean public CircuitBreaker llmCircuitBreaker() { CircuitBreakerConfig config CircuitBreakerConfig.custom() .failureRateThreshold(50) .waitDurationInOpenState(Duration.ofSeconds(30)) .permittedNumberOfCallsInHalfOpenState(10) .build(); return CircuitBreaker.of(llm, config); }熔断后的降级方案要提前想好。能做缓存就返回缓存结果没有缓存就返回一个通用的“服务暂时繁忙”响应绝不能让用户请求一路挂着等到超时。这里有个经验之谈熔断器和线程池隔离要配合使用每个下游依赖一个独立的线程池A服务慢了占满A的线程池最多影响A不会拖垮主线程池。3.3 缓存LLM输出也有大量可复用的部分很多人觉得AI结果是“随机生成”的不能缓存。这个想法不全对。实际业务里FAQ类问题、政策咨询、产品介绍等大量答案是高度相似甚至重复的。我在一个客服项目里做过统计两周内的高频问题重复率达四成缓存命中能显著降低模型调用量和用户等待时间。LLM缓存分两层。一是语义缓存通过向量相似度判断两个问题是否表达同一个意思命中后直接返回之前的答案二是精确匹配缓存针对完全相同的输入直接走Redis。public AnswerResult getCachedAnswer(String question) { String md5Key DigestUtils.md5DigestAsHex(question.getBytes(StandardCharsets.UTF_8)); String cached redisTemplate.opsForValue().get(ai:answer: md5Key); if (cached ! null) { return JsonUtils.parse(cached, AnswerResult.class); } return null; }但缓存不是全无代价。模型输出可能有随机性业务方如果要求答案要带时效性比如价格、政策必须控制缓存TTL设置成5分钟或更短。另外敏感问题不建议缓存涉及用户个人信息或者合规风险的一律直连模型防止缓存穿透或者数据泄露扩散。还有缓存击穿风险——某个高频问题过期瞬间大量回源解决办法是加互斥锁只让一个请求去重新生成答案其余请求等锁后读取新缓存。3.4 超时与重试把“坏请求”的影响半径缩到最小高并发设计里超时是最容易被低估的参数。很多人不设置HTTP客户端超时或者设置了一个很大的值比如60秒然后抱怨线程不够用。事实上下游模型服务一旦异常响应时间会从正常的5秒变成几十秒你要是不设超时线程就被烂请求无限期占住。我见过一份代码里Feign的配置readTimeout给的是10秒看起来也不算离谱但下游偶尔抖动到30秒Feign不会自动放弃线程照样干等。正确的做法是先设保守超时——比如模型调用3秒无响应就中断——然后通过重试去弥补偶发失败。超时时间要根据实际响应分布来定取P99耗时再上浮一小段不是拍脑袋。spring: cloud: openfeign: client: config: llm-client: connectTimeout: 2000 readTimeout: 5000重试也不是越多越好。每次重试都会重新调用下游如果你的超时是5秒一次请求最多重试3次最坏情况一个请求要花15秒等待。高并发下这些重试流量叠加保护性限流就失效了。重试策略要配合幂等设计模型调用不幂等恢复结果会发生重复计费、重复推送。我的经验是只有明确知道上次失败发生在“网络层”而不是“业务层”时才重试而且要做退避第一次等500毫秒第二次等2秒减轻下游压力。4. 实操记录AI问答平台的高并发后端改造全流程4.1 业务场景和初始状态项目背景是给一个企业内部知识库做AI问答平台。员工提问后端要完成三件事向量检索相关知识片段、把片段拼接进提示词、调用大模型生成回答。初始实现是全同步的Controller里直接串行调用三个服务测试环境一个人用没问题全网推广当天就崩了。崩溃现场很典型Tomcat线程池被打满、模型服务日志全是超时、Redis连接池报错、前端页面大面积“服务暂时不可用”。我从监控里抓到的数据是Tomcat最大线程200高峰期在途请求超过230线程池出现拒绝执行应用因为OOM差点挂掉。4.2 改造方案同步改异步任务入列结果回调我们最后采用的是“同步接受 异步处理 SSE推送”的组合没有用WebFlux重写整个项目主要是因为团队对Reactor不熟迁移成本太高。具体链路是用户POST问题 - Nginx - Spring Boot Controller快速返回请求ID - 问题写入RocketMQ业务队列 - AI消费组拉取消息 - 向量检索并行于大模型生成 - 结果写入Redis并使用SSE推送给前端。整套改造花了不到两周但效果非常明显。在线人数翻三倍QPS从30支撑到了180模型调用成功率也从88%提升到了接近99%。4.3 核心代码片段从Controller到消费端Controller只负责接收和快速返回PostMapping(/api/qa) public ResponseEntityQaSubmitResponse submit(RequestBody QaQuestion request) { String requestId IdGenerator.generate(); qaMessageProducer.send(requestId, request); return ResponseEntity.accepted() .body(new QaSubmitResponse(requestId, 已受理)); }消息生产者把问题投递到队列Component public class QaMessageProducer { private final RocketMQTemplate rocketMQTemplate; public void send(String requestId, QaQuestion question) { QaMessage message new QaMessage(requestId, question); rocketMQTemplate.convertAndSend(qa-task-topic, message); } }消费端拉取消息执行异步链路Component public class QaTaskConsumer { private final ExampleDataService searchService; private final LlmClient llmClient; private final SsePublisher ssePublisher; public void handle(QaMessage message) { RequestContextHolder.setRequestId(message.getRequestId()); try { // 并行执行检索和生成缩短总耗时 CompletableFutureListString searchFuture CompletableFuture.supplyAsync( () - searchService.search(message.getQuestion()), searchExecutor); CompletableFutureString generateFuture CompletableFuture.supplyAsync( () - llmClient.generate(message.getQuestion()), llmExecutor); CompletableFutureVoid combined searchFuture .thenAcceptBoth(generateFuture, (contexts, answer) - { AnswerResult result new AnswerResult(contexts, answer); ssePublisher.publish(message.getRequestId(), result); }); combined.orTimeout(30, TimeUnit.SECONDS) .exceptionally(ex - { log.error(qa task failed, ex); ssePublisher.publishError(message.getRequestId(), 生成失败请稍后重试); return null; }); } finally { RequestContextHolder.clear(); } } }注意我在这里用了orTimeout这个API从Java 9开始提供它能在指定时间后主动完成Future并抛异常是防止任务无限卡死的利器。异步链路上不设超时等同于同步代码里不设readTimeout早晚出事。4.4 线程池与队列参数怎么定这是改造里最需要抠细节的部分。我们最后给检索和大模型分别建了线程池。检索线程池核心线程15最大线程30队列容量200因为向量检索平均耗时80msP99大概200ms扛住每秒钟几百次检索绰绰有余。模型线程池核心线程20最大线程40队列容量500大模型平均耗时3秒P99可能要8秒所以线程数反而要多。估算逻辑是目标在线处理500个模型任务平均耗时3秒至少需要500除以1约等于500个线程才能保证每秒处理166个任务这个算错了我想想。准确计算如果每秒进入500个模型调用每个耗时3秒那么系统同时处于在途状态的调用数量大约是500乘以3等于1500个。线程数不可能配1500那意味着要控制下游接受能力同时要依赖限流把入口的QPS降下来。实际业务里我们不追求单个节点吃下所有流量而是QPS控制到模型能力之内然后横向扩容节点数。线程池的队列容量也要算。QueueCapacity填500意味着线程池最多接受40加500共540个任务多出来的直接拒绝。拒绝策略我选了CallerRunsPolicy让提交任务的线程自己执行一个兜底逻辑把任务再由生产者发回队列防止数据丢失。4.5 限流、熔断和数据一致性收尾在网关层加了Sentinel限流按用户ID分桶普通用户每人每秒钟最多3个问题VIP用户可以放宽到10个。全局最大QPS设置为150超过后返回友好的“系统繁忙请稍候再试”。下游模型服务专门加了一个Resilience4j熔断60秒内失败率达到50%熔断30秒。数据一致性这块常被忽略。异步结果写回Redis时如果用户已经关闭浏览器SSE连接断了消息不能默默丢弃我选择把结果持久化到一张结果表前端下次刷新时可以按requestId拉取历史答案。这样即使某个环节失败用户再点一次也能拿到结果。压测结果比较理想。使用JMeter模拟200并发用户每用户连续提问5轮平均响应时间SSE首包时间从原来的4000ms降到了700ms请求成功率从88%提升到99.6%。整体QPS在4台4核8G的节点上跑到180再往上提升就得加节点了。5. 实战常见的6个问题和排查技巧5.1 线程池满了报“Task rejected”该查什么这个报错是最常见的。先看线程池核心线程、最大线程、队列容量三个参数匹配不匹配。很多人只调大了最大线程数但队列容量设了100000等于永远触达不到最大线程任务全积压队列里表面线程没满实际响应延迟已经拉垮了。Tomcat的线程池队列默认是无限长这是个大坑。Tomcat最大线程200如果队列无限积压请求不会返回报错但每个请求的等待时间不断增长用户点击后要卡几十秒才响应。这种情况体现出来的问题不是“线程满了”而是“线程没有被释放”。排查时要同时看活跃线程数、队列积压数和下游响应时间三个指标放一起才能定位是哪里慢。5.2 CompletableFuture用了默认线程池全链路拖垮项目初期有人直接用CompletableFuture.runAsync()没传线程池结果用的是ForkJoinPool.commonPool。这个池的线程数等于CPU核数减1AI场景下几十个并发任务就排队等线程性能还不如同步执行。排查方法很简单看线程Dump里有没有大量名为ForkJoinPool.commonPool-worker的线程如果有代码里八成漏传了线程池。修复方式是把所有supplyAsync调用改成显式传线程池并且和业务线程池隔离各管各的。5.3 主线程关闭时异步任务被“吞掉”Spring Boot应用如果直接重启正在执行的异步任务会被中断消息队列里还没消费的任务也原地消失。我第一次上线就踩了这个坑发布新版本前没有任何告警结果发布完用户发现少了一批回答。解决办法有两条路。线程池要配置优雅停机setWaitForTasksToCompleteOnShutdown(true)让任务在关闭前尽量完成再给一个setAwaitTerminationSeconds(30)上限避免停机卡死消息队列消费组要开启断点续传消费成功后确认位移重启后从记录点继续消费。5.4 traceId在异步链路里丢失排查问题无从下手同步代码里用日志追踪非常方便一个请求的日志是一串日志串起来的。但异步化之后线程发生了变化ThreadLocal里的traceId不会自动传递日志就成了碎片定位一个用户的问题要翻半天。我的做法是使用TransmittableThreadLocal或者用Spring Cloud Sleuth这类链路追踪组件从HTTP入口生成traceId往消息体里塞一份消费端再重新绑定上下文。这样从用户提问到模型生成、SSE推送的全过程日志都能串起来。别小看这一步等线上出了问题你会感谢当初这十分钟的配置。5.5 慢消费导致消息积压下游被压垮消息队列解耦了流量但也带来了新的风险如果消费速度跟不上生产速度消息积压会越来越大积压到一定量下游模型服务可能会被“追债式”流量引爆。这时候光加消费实例不够要先看看消费端每个任务的平均耗时是不是变长了可能是模型接口变慢也可能是下游依赖的服务有抖动。稳妥做法是对消费组单独做限流配置每秒钟最大消费数量让消费速度平缓不要猛灌。监控上要注意积压条数这个指标超过阈值就告警。积压不是问题无感知的积压才是大问题。5.6 数据竞态异步结果写到同一个Key互相覆盖多个子任务并行后如果它们同时更新同一个缓存Key或同一个数据库行就有竞态问题。模型场景里不太明显但RAG检索片段拼接时如果多线程往同一个StringBuilder里追加内容顺序就会错乱。处理策略是每个异步子任务只负责处理自己的中间结果最终拼接放到下一个阶段单独做如果要并发更新同一个记录用数据库乐观锁版本号控制Redis里则用setIfAbsent加锁键来串行化写操作。问题典型症状优先排查点线程池拒绝Task rejected报错核心/最大线程数与队列容量默认ForkJoinPool过载commonPool-worker线程爆炸是否显式传线程池重启丢任务用户答案丢失优雅停机与消费位移traceId丢失日志搜不到关联ThreadLocal的异步传递消息积压消费延迟增大消费线程数与下游耗时数据竞态答案内容错乱共享对象与并发写控制写到最后我想分享点体会。刚开始接触AI应用的时候我总想着把异步化和高并发做成一套很“炫”的架构WebFlux、响应式、消息队列全上。后来被线上问题教育了几次才明白架构是为稳定性服务的不是为简历服务的。很多项目根本不需要全链路响应式一套“虚拟线程加CompletableFuture加限流熔断缓存”的组合已经能覆盖绝大多数场景。如果在座各位刚接手类似项目我建议不要一上来就改架构。先打好监控把线程数、队列积压、下游耗时、限流阈值这些数据捞出来看清楚瓶颈在哪里再决定改哪里。每一步改动都做压测对比用数据说话。这是个慢功夫但也是最靠谱的路。
返回列表