异步化改造:从同步调用到事件驱动

对应简历段落

在保险销售活动和商机流转系统中,负责将投保提交、商机分配、客户通知、CRM 同步、保司状态回写等长链路从同步调用改造为事件驱动架构。通过核心链路瘦身、RocketMQ 事件解耦、线程池隔离、幂等补偿和监控告警,降低接口超时和下游抖动对用户体验的影响,提升高峰期系统稳定性。

这段简历面试时要特别小心。异步化不是把 service.call() 改成 executor.submit(),也不是简单发一条 MQ。真正的异步化改造包括业务边界重划、状态机设计、一致性方案、可观测性和失败恢复。如果说不清这些,面试官会认为你只是做了技术包装。

业务背景

改造前的投保提交链路往往很长:校验活动资格、查询客户信息、计算保费、创建投保意向、分配代理人、发送短信、写运营埋点、同步 CRM、调用保司预核保、生成跟进待办。平时流量低时,这种同步链路看起来简单直接;活动高峰时,任何一个下游变慢都会导致整个接口超时。

同步链路还有一个隐性问题:调用方和被调用方的可用性强绑定。短信通道抖动不应该影响投保意向创建,CRM 同步失败不应该让用户提交失败,运营埋点慢也不应该占用入口线程。但同步代码会天然把它们绑在一起,导致弱依赖拖垮强业务。

异步化的目标不是“所有事情都异步”,而是把用户实时感知链路和后置处理链路拆开。用户请求内必须完成的是:请求合法、业务幂等、核心单据落库、用户能得到明确结果。后置链路可以延迟处理,但必须可靠、可追踪、可补偿。

核心原理

同步调用是命令式思维:A 调 B,B 成功后 A 再调 C。调用关系清晰,但耦合强、延迟累加、故障传播直接。事件驱动是事实式思维:A 完成自己的业务状态变更后发布事件,B、C、D 订阅这个事件并各自处理。发布者不关心订阅者数量和处理耗时,订阅者失败也不直接影响发布者。

事件驱动的关键是事件语义。事件应该表示“已经发生的业务事实”,例如 OpportunityCreatedOpportunityAssignedPolicyStatusChanged,而不是 DoAssignCommandSendSmsCommand 这种远程命令。事实事件更稳定,也更适合多个消费者扩展。

异步化必须接受最终一致性。同步链路中,调用完成即可认为所有动作完成;异步链路中,主单创建成功时,商机可能还未分配,通知可能还未发送,CRM 可能还未同步。业务页面、运营后台和客服工具都要能展示中间状态,并支持重试和补偿。

异步化还要处理失败传播方式的变化。同步调用失败可以直接抛异常;异步消费失败不能让用户重新提交,它需要重试、死信、失败表、人工处理和状态回滚策略。没有这些配套,异步化只会把错误藏起来。

项目落地

第一步是链路梳理。把原同步接口中的动作按重要性拆分为强依赖、弱依赖和后置任务。强依赖包括活动资格、重复提交幂等、核心单据创建;弱依赖包括客户标签、营销权益展示;后置任务包括商机分配、短信通知、CRM 同步、运营埋点。强依赖留在同步链路,弱依赖设置短超时和降级,后置任务事件化。

第二步是状态机设计。投保意向不能只有“成功/失败”,而应有 CREATEDWAIT_ASSIGNASSIGNEDNOTIFIEDSYNCED_CRMFAILED 等状态或子状态。异步消费者每完成一个动作,就推进对应状态。页面可以显示“已提交,正在分配顾问”,运营后台可以看到哪些环节失败。

第三步是事件发布一致性。核心单据落库和事件发布必须绑定。可以用事务消息,也可以用本地消息表。对于团队可控性强的业务系统,本地消息表很常见:业务事务里写主单和消息记录,后台投递任务负责发送 MQ,发送成功后标记状态。

第四步是消费者解耦。一个事件可以有多个消费者,但每个消费者只做自己的事情。商机分配消费者只负责代理人分配和状态推进;通知消费者只负责发送通知;CRM 消费者只负责外部同步。消费者之间不要直接互相调用,否则事件驱动会退化成隐藏的同步链路。

第五步是补偿闭环。异步任务要有清晰的失败表、重试次数、错误原因、最后处理时间和人工触发入口。比如 CRM 同步失败不影响用户提交,但运营人员需要看到失败记录并可重新同步。商机分配失败则影响销售跟进,应设置更高优先级告警。

关键伪代码或流程

同步改造前:

public SubmitResult submit(SubmitCommand command) {
    qualificationService.check(command);
    Opportunity opportunity = opportunityService.create(command);
    Agent agent = assignService.assign(opportunity);
    smsService.send(agent, opportunity);
    crmService.sync(opportunity);
    metricService.record(opportunity);
    return SubmitResult.success(opportunity.id());
}

事件驱动改造后:

@Transactional
public SubmitResult submit(SubmitCommand command) {
    qualificationService.check(command);
    Opportunity opportunity = opportunityService.create(command);

    eventRepository.save(DomainEvent.of(
            "OPPORTUNITY_CREATED",
            opportunity.id(),
            OpportunityCreatedPayload.from(opportunity)));

    return SubmitResult.accepted(opportunity.id(), "SUBMITTED");
}

事件投递任务:

public void dispatchEvents() {
    List<DomainEvent> events = eventRepository.scanPending(100);
    for (DomainEvent event : events) {
        try {
            mqProducer.send(event.topic(), event.bizKey(), event.payload());
            eventRepository.markSent(event.eventId());
        } catch (Exception ex) {
            eventRepository.markRetry(event.eventId(), ex.getMessage());
        }
    }
}

消费者状态推进:

public void onOpportunityCreated(OpportunityCreatedEvent event) {
    if (!consumeLog.tryStart(event.eventId(), "ASSIGN")) {
        return;
    }
    Agent agent = assignPolicy.select(event);
    int updated = opportunityRepository.assignIfWaiting(event.opportunityId(), agent.id());
    if (updated == 1) {
        eventPublisher.publish(new OpportunityAssignedEvent(event.opportunityId(), agent.id()));
    }
    consumeLog.markSuccess(event.eventId(), "ASSIGN");
}

推荐流程:用户提交请求;同步校验与主单落库;写入领域事件;后台可靠投递 MQ;多个消费者独立处理;每个消费者幂等推进状态;失败进入重试或补偿;监控覆盖事件滞留、消息积压、消费失败和状态长时间未推进。

常见坑与排查

第一个坑是异步后接口返回太早,但业务状态不可查。用户看到提交成功,后台却没有商机记录或无法解释处理中状态。改造时必须让主单先落库,并提供状态查询。

第二个坑是事件语义设计成命令。比如发送 AssignAgentCommand,以后要新增通知、标签、CRM 消费者时就很别扭。使用业务事实事件更容易扩展。

第三个坑是消费者之间互相依赖。A 消费者处理完直接调 B 服务,B 慢又把 A 拖住。应通过新事件表达状态变化,让下游订阅。

第四个坑是缺少幂等。事件驱动系统默认会重复投递,消费者必须按事件 ID 和业务动作去重。

第五个坑是没有补偿入口。异步失败如果只写日志,线上就会出现用户提交成功但后续没人跟进的黑洞。失败必须可查询、可告警、可重试。

第六个坑是监控还停留在同步接口维度。异步化后,接口成功率不等于业务完成率,还要看事件发送延迟、消费延迟、失败率、死信数和状态超时数。

面试追问

追问一:哪些逻辑适合同步,哪些适合异步?

追问二:异步化后如何保证用户体验?

追问三:如何保证主单落库和消息发送一致?

追问四:消费者失败怎么办?

追问五:异步链路如何排查问题?

追问六:事件驱动和 RPC 解耦的区别是什么?

推荐回答

我会回答:异步化的边界由业务实时性决定。用户提交必须立刻知道是否成功,所以活动资格、幂等校验、核心单据落库留在同步链路;商机分配、通知、CRM 同步、埋点这些后置动作改成事件驱动。同步接口返回的是“核心受理成功”,不是所有后置动作都完成。

一致性上,我会用事务消息或本地消息表保证主单和事件一致。消费者按事件 ID 做幂等,业务状态用条件更新推进。失败分临时和永久:临时失败走 MQ 延迟重试,永久失败落失败表并告警。页面和运营后台展示中间状态,避免用户和运营误以为所有动作已经完成。

排查时,我会从业务单号出发,串起主单状态、事件表、MQ 投递记录、消费日志、失败表和下游调用日志。异步化后最重要的是可观测性,否则链路断点会很难定位。

延伸学习路线

第一阶段,学习同步调用、异步任务、消息队列、事件驱动的差异。重点理解延迟、耦合、失败传播和一致性模型。

第二阶段,学习领域事件建模。能区分业务事实、命令和数据同步消息,能为事件设计版本、幂等键和业务 key。

第三阶段,学习可靠事件发布方案:本地消息表、事务消息、Outbox、CDC。理解每种方案的复杂度和适用场景。

第四阶段,学习异步链路可观测性。包括 traceId 传递、业务单号日志、消息积压、消费延迟、状态超时、补偿成功率。

第五阶段,做完整复盘:选择一个同步接口,画出改造前调用链、改造后事件流、失败补偿路径和监控指标。能讲清楚这张图,面试表达会非常扎实。