我为什么把“异步化”从炫技变成了刚需
我为什么把“异步化”从炫技变成了刚需
刚学多线程那会儿,我觉得 CompletableFuture 和消息队列特别酷,写代码总想找个地方“异步化”一下,好像不用就显不出水平。结果有一次,我把用户注册后的“发欢迎邮件”异步化了,但没处理好异常,邮件失败了也没告警,导致一批新用户没收到邮件,运营找了我好几天。那时候我觉得异步是个坑,能不用就不用。
后来系统流量涨起来,一次下单接口要调库存、优惠券、积分、推送四个服务,全同步调用,P99 直接干到 2 秒,大促时线程池打满,整个下单链路雪崩。我才明白:异步不是炫技,是在高并发下活下来的基本功。
一、异步化的本质:把“必须马上做完”和“可以晚点做”分开
我现在的判断标准很简单:用户当前操作是否依赖这个结果? 不依赖的,全部异步。
| 场景 | 是否异步 | 原因 |
|---|---|---|
| 扣库存 | ❌ 同步 | 下单必须知道有没有库存 |
| 创建订单 | ❌ 同步 | 用户要立刻看到订单号 |
| 发短信通知 | ✅ 异步 | 晚几秒收到无所谓 |
| 更新用户积分 | ✅ 异步 | 不影响当前下单结果 |
| 记录操作日志 | ✅ 异步 | 纯粹后台审计用 |
| 调用风控服务 | 视情况 | 如果风控要拦截,就同步;只打分,就异步 |
把发邮件、记日志、推消息这些“旁路逻辑”拆出去后,下单接口从 2 秒降到 400ms,用户体验立竿见影。
二、三种异步手段,我用它们各管一段
1. 线程池:适合轻量、快执行的任务
比如记录日志、更新统计字段。用 CompletableFuture.runAsync() 丢到自定义线程池:
CompletableFuture.runAsync(() -> {
try {
operationLogService.record(userId, action);
} catch (Exception e) {
log.error("record log failed", e);
}
}, bizExecutor);
注意:一定要用自定义线程池,别用 ForkJoinPool.commonPool()。我以前偷懒用默认的,结果被其他地方的并行流把池子占满,日志全丢。
2. 消息队列:适合重操作、需要削峰和重试的
比如订单创建后触发物流调度、积分计算、推荐系统更新。用 MQ 解耦,好处是:
- 削峰:大促时订单洪峰,MQ 把请求攒住,消费者按自己的节奏处理
- 重试:消费失败自动重试,不用自己写重试逻辑
- 解耦:下单服务根本不用知道下游有哪些系统,加新消费者就行
@KafkaListener(topics = "order_created")
public void handleOrderCreated(OrderEvent event) {
pointsService.calculate(event);
recommendationService.updateUserModel(event.getUserId());
}
3. 定时任务 + 状态机:适合最终一致性场景
有些操作不适合实时异步,比如对账、数据修复。我们会在主流程里写一条“待处理”记录,定时任务扫表执行。好处是可控、可观测、可手动触发。
三、异步化的三个坑,我一个没落下
坑一:异常被吞
这是最常见的。异步任务里抛了异常,主线程感知不到,日志里可能就一行 ERROR,甚至什么都没有。我的规矩是:所有异步任务必须有 try-catch + 日志 + 监控告警,关键任务还要写失败表,支持手动重试。
坑二:上下文丢失
异步执行时,MDC 里的 traceId、用户登录信息、国际化上下文全丢了。排查问题时一脸懵:这条日志是哪个请求触发的?解法是用 TaskDecorator 把上下文传递到子线程:
public class MdcTaskDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
Map<String, String> context = MDC.getCopyOfContextMap();
return () -> {
if (context != null) MDC.setContextMap(context);
try { runnable.run(); } finally { MDC.clear(); }
};
}
}
坑三:资源竞争
异步化后,多个线程同时写同一行数据的概率大增。以前同步时串行执行,现在并发了,乐观锁、分布式锁得跟上。我们有一张积分表,异步更新时没加版本号,结果两个任务同时读、同时算、同时写,用户少了 50 积分。后来全加上 UPDATE ... SET points = points + ? WHERE version = ?,问题解决。
四、一个判断框架:该不该异步?
我现在做技术评审时,会问三个问题:
- 失败了对主流程有影响吗? 有 → 同步;没有 → 可以异步
- 执行时间超过 100ms 吗? 是 → 考虑异步;否 → 同步也行
- 调用方需要实时结果吗? 需要 → 同步;不需要 → 异步
三个问题一问,该不该异步基本就清楚了。
一个心态的转变
以前我觉得异步化是“让系统更快”,现在觉得是让系统更稳。同步调用链越长,一个环节抖,整个链路就抖。异步化把强依赖变成弱依赖,把串行变成并行,把实时变成最终一致——系统的韧性就出来了。
如果你现在的接口里还有“下单 → 发邮件 → 记日志 → 推推荐系统”这种串行调用,今晚就试试把非核心的拆出去。不用一步到位,先拆一个,测一下响应时间,你会有体感的。毕竟,用户不关心你后台做了多少事,他们只关心点完按钮多久能看到结果。
- 点赞
- 收藏
- 关注作者
评论(0)