我为什么把“异步化”从炫技变成了刚需

举报
逆行时光 发表于 2026/11/06 08:50:03 2026/11/06
【摘要】 我为什么把“异步化”从炫技变成了刚需刚学多线程那会儿,我觉得 CompletableFuture 和消息队列特别酷,写代码总想找个地方“异步化”一下,好像不用就显不出水平。结果有一次,我把用户注册后的“发欢迎邮件”异步化了,但没处理好异常,邮件失败了也没告警,导致一批新用户没收到邮件,运营找了我好几天。那时候我觉得异步是个坑,能不用就不用。后来系统流量涨起来,一次下单接口要调库存、优惠券、...

我为什么把“异步化”从炫技变成了刚需

刚学多线程那会儿,我觉得 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 = ?,问题解决。


四、一个判断框架:该不该异步?

我现在做技术评审时,会问三个问题:

  1. 失败了对主流程有影响吗? 有 → 同步;没有 → 可以异步
  2. 执行时间超过 100ms 吗? 是 → 考虑异步;否 → 同步也行
  3. 调用方需要实时结果吗? 需要 → 同步;不需要 → 异步

三个问题一问,该不该异步基本就清楚了。


一个心态的转变

以前我觉得异步化是“让系统更快”,现在觉得是让系统更稳。同步调用链越长,一个环节抖,整个链路就抖。异步化把强依赖变成弱依赖,把串行变成并行,把实时变成最终一致——系统的韧性就出来了。

如果你现在的接口里还有“下单 → 发邮件 → 记日志 → 推推荐系统”这种串行调用,今晚就试试把非核心的拆出去。不用一步到位,先拆一个,测一下响应时间,你会有体感的。毕竟,用户不关心你后台做了多少事,他们只关心点完按钮多久能看到结果。

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。