7.1 异步请求与WebSocket


7.1 异步请求与 WebSocket

本节摘要:处理耗时几秒的请求,同步等待等于扣住一根容器线程干等——线程池两百根,五十个并发慢请求就能让全站排队。本节讲两种改法:把容器线程提前归还的异步请求模式,以及升级为双向长连接后连"请求—响应"这个概念都消失的 WebSocket。

异步请求:先还线程,后补答案

生成报表要八秒,同步写法占用线程整整八秒。异步写法把方法返回值改成 CompletableFuture,分发器立刻归还容器线程,业务在另一条线程上继续,完成后响应再写出:

@RestController public class ReportController { private final ReportService service; public ReportController(ReportService service) { this.service = service; } @GetMapping("/reports") public CompletableFuture<ReportVO> generate() { return service.generateAsync(); // 方法一返回,容器线程即归还 } } @Service class ReportService { @Async("reportPool") public CompletableFuture<ReportVO> generateAsync() { ReportVO r = heavyCompute(); return CompletableFuture.completedFuture(r); } }

对浏览器来说体验不变(还是要等八秒),对服务端来说天差地别:等答案的成本从"一根宝贵的容器线程"变成"一个轻量的未来对象加一条业务线程"。容量账很直白——容器线程池两百根扛请求接收,业务线程池按慢操作的真实并发配置,两池解耦后慢接口不再拖累快接口。

⚠️ @Async 依赖代理,自调用不生效(第 4 章的规矩第三次出现);且必须显式配置线程池与拒绝策略,默认的池容量有限且排队无上限提示,高峰期会静默堆积任务。

WebSocket:连"一来一回"都不要了

行情推送、协同编辑、在线客服——这些场景的特征是服务端也要主动说话,请求响应模型装不下。WebSocket 通过一次协议升级把连接变成双向通道:

@Component @EnableWebSocket public class PriceSocket implements WebSocketConfigurer, WebSocketHandler { private final Set<WebSocketSession> sessions = ConcurrentHashMap.newKeySet(); @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(this, "/ws/price").setAllowedOriginPatterns("*"); } @Override public void afterConnectionEstablished(WebSocketSession s) { sessions.add(s); // 连接即登记 } @Override protected void handleTextMessage(WebSocketSession s, TextMessage msg) { } public void push(String json) { // 服务端主动推送 sessions.forEach(s -> s.sendMessage(new TextMessage(json))); } }

生命周期完全不同于 HTTP:一次连接建立后长期存活,双方随时互发消息帧,直到任一方关闭。它住在旅程的哪个位置?协议升级请求以普通请求的身份进入容器,握手过滤器完成升级后,后续帧的处理绕过分发器,由专门的处理器接管——严格说,WebSocket 之后请求旅程的九站地图不再适用,这也是本章把它放在"延伸"里的原因。

图 7-1 同步、异步与长连接的线程占用对比

图 7-1 同步、异步与长连接的线程占用对比

实验:用压测对比同步与异步的吞吐差

背景:报表接口耗时约两秒,同步版本在并发五十时全站响应劣化,需要数据验证异步改造的收益。操作:第一步,给异步业务线程池做显式配置(拒绝策略选 caller 之外的明确拒绝并记日志):

@Configuration @EnableAsync public class AsyncConfig { @Bean("reportPool") public ThreadPoolTaskExecutor reportPool() { var pool = new ThreadPoolTaskExecutor(); pool.setCorePoolSize(8); // 按慢操作真实并发定 pool.setMaxPoolSize(16); pool.setQueueCapacity(100); pool.setThreadNamePrefix("report-"); pool.setRejectedExecutionHandler( // 拒绝要有声响,不能静默 (r, e) -> { throw new RejectedExecutionException("报表池已满"); }); return pool; } }

第二步,先压同步版本:五十并发下,容器线程被报表占满,其它快接口也排队超时。第三步,压异步版本:容器线程即刻归还,快接口响应恢复原速,报表在业务池上排队完成。结果:全站九十九分位延迟恢复正常,报表吞吐由池容量决定、可控可调。解读:两池解耦的本质是把"不可控的等待"从公共资源(容器线程)挪到专用资源(业务池),专用资源满了只会拒绝报表任务,不再殃及全局。变式:故意把队列容量调小触发拒绝,观察客户端收到明确错误而不是无限等待——拒绝快比排队死更体面。

本节要点回顾

  • 异步请求把等待成本从容器线程转移到业务线程池,两池解耦
  • @Async 走代理且需自配线程池,默认池不可托付生产
  • WebSocket 适合服务端主动的场景,握手后脱离分发器管辖
  • 升级手段按代价排序:缓存、拆分、异步、长连接

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U