1. 同步与异步接口:先搞清楚差异再谈选型
做后端开发这些年,被问得最多的一个问题就是:“这个接口到底该写成同步还是异步?”每次我都不急着给答案,先反问对方三个问题:调用方能接受多长的等待时间?这个请求在服务端是快任务还是慢任务?如果一个任务失败了,你要不要立刻告诉调用方?这三个问题问完,大部分同学自己心里就有眉目了。但实际落到项目里,情况远比这三个问题复杂,今天就把我这几年在接口选型上的经验和踩过的坑一次性说清楚。
所谓同步接口,就是发请求后一直等待服务端处理完并返回结果。而异步接口,是服务端先收下请求,返回一个“我已收到,正在处理”的凭证,处理完了再通过回调、轮询或者通知把结果给调用方。两者最本质的区别不在于“快”和“慢”,而在于“等待”与“解耦”。理解了这一点,选型就有了根基。
1.1 从响应时间模型看同步接口的本质
同步接口的黄金准则是“你的响应时间等于上游的全部处理时间之和”。如果接口内部要调三个下游服务,每个下游耗时200毫秒,串行调用时你的接口就是600毫秒起步,再算上自身逻辑和数据库IO,一秒出结果是常有的事。在用户感知层面,超过1秒的接口就已经让体验大打折扣了,超过3秒基本可以宣告这个同步接口设计得有问题。
我在实际的压测中发现,同步接口的吞吐量其实取决于两个因素:平均响应时间和线程池大小。Tomcat默认线程池200,公式上最大吞吐量约等于线程数除以平均响应时间。如果接口平均响应时间500毫秒,200个线程的理论吞吐大约每秒400个请求。看起来还行对吧?但一旦下游服务抖动,响应时间涨到2秒,吞吐就直接掉到每秒100左右,同时线程池迅速被打满,后续请求全部排队等待,这就是典型的“一个慢接口拖垮整个应用”。
所以我在设计同步接口时,一定会在代码里加上两层保护:第一层是RestTemplate或OpenFeign的超时设置,一般连接超时给1秒,读取超时给2到3秒;第二层是对整个Controller方法做信号量隔离或线程池隔离,避免慢调用耗尽全局线程。这两层保护不加上,同步接口在流量稍微上来一点的时候就是定时炸弹。
1.2 异步接口的核心场景与代价
异步接口的出发点很简单:当用户等不起、也不需要立刻拿到结果的时候,就先给一个“请求已受理”的响应,后台慢慢处理。最典型的例子是批量导出数据、大量数据的清洗任务、发送营销短信或邮件,以及和第三方系统的数据对账。这类场景的共同特征是处理时间呈长尾分布,偶尔会有几十秒甚至分钟级的任务,用户不可能一直傻等。
但是异步是有代价的,而且这个代价往往被新手低估。首先,你需要一个可靠的存储来记录任务状态,数据库里要多一张任务表。其次,你需要一套回调或查询机制,让调用方能在之后拿到处理结果。再次,你还需要考虑任务的失败重试、死信处理和监控告警。换句话说,同步方案把复杂度集中在调用链条上,异步方案把复杂度转移到了系统的各个角落里。
说到异步接口,我见过不少团队一上来就上消息队列,用的还是Kafka这种重型组件。对于绝大多数内部管理系统的场景,这完全是杀鸡用牛刀。一个可落地的做法是先用数据库任务表加上定时任务扫描,等单量真正上来了,再平滑迁移到MQ。我在多个项目里用这个方案扛过每天百万级的异步任务,稳定性一点不差。
1.3 大模型接口场景下的同步与异步选型
最近做AI应用相关的后端是热点话题,大模型接口的调用让同步异步选型又多了个特殊维度。市面上大部分大模型API的响应时间都很长,流式输出一个完整回答可能要几十秒,用同步方式显然不现实。所以现在主流做法是:用户请求进来,后端立刻启动一个异步任务去调用大模型API,同时返回一个任务ID,前端拿到这个ID后通过SSE(Server-Sent Events)或轮询去拉取流式结果。
这里的核心难点在于大模型接口的流式响应如何透传给前端。我的做法是后端用WebClient发起流式请求,逐块接收大模型返回的数据,然后通过SseEmitter推送给前端。这个过程中,整个请求链路其实是异步的,但用户体验是实时的。这种“异步处理+流式推送”的组合,是现在AI应用后端的主流架构,也最能体现同步异步选型在实际业务中的灵
