异步化改造:从同步调用到事件驱动
对应简历段落
在保险销售活动和商机流转系统中,负责将投保提交、商机分配、客户通知、CRM 同步、保司状态回写等长链路从同步调用改造为事件驱动架构。通过核心链路瘦身、RocketMQ 事件解耦、线程池隔离、幂等补偿和监控告警,降低接口超时和下游抖动对用户体验的影响,提升高峰期系统稳定性。
这段简历面试时要特别小心。异步化不是把 service.call() 改成 executor.submit(),也不是简单发一条 MQ。真正的异步化改造包括业务边界重划、状态机设计、一致性方案、可观测性和失败恢复。如果说不清这些,面试官会认为你只是做了技术包装。
业务背景
改造前的投保提交链路往往很长:校验活动资格、查询客户信息、计算保费、创建投保意向、分配代理人、发送短信、写运营埋点、同步 CRM、调用保司预核保、生成跟进待办。平时流量低时,这种同步链路看起来简单直接;活动高峰时,任何一个下游变慢都会导致整个接口超时。
同步链路还有一个隐性问题:调用方和被调用方的可用性强绑定。短信通道抖动不应该影响投保意向创建,CRM 同步失败不应该让用户提交失败,运营埋点慢也不应该占用入口线程。但同步代码会天然把它们绑在一起,导致弱依赖拖垮强业务。
异步化的目标不是“所有事情都异步”,而是把用户实时感知链路和后置处理链路拆开。用户请求内必须完成的是:请求合法、业务幂等、核心单据落库、用户能得到明确结果。后置链路可以延迟处理,但必须可靠、可追踪、可补偿。
核心原理
同步调用是命令式思维:A 调 B,B 成功后 A 再调 C。调用关系清晰,但耦合强、延迟累加、故障传播直接。事件驱动是事实式思维:A 完成自己的业务状态变更后发布事件,B、C、D 订阅这个事件并各自处理。发布者不关心订阅者数量和处理耗时,订阅者失败也不直接影响发布者。
事件驱动的关键是事件语义。事件应该表示“已经发生的业务事实”,例如 OpportunityCreated、OpportunityAssigned、PolicyStatusChanged,而不是 DoAssignCommand、SendSmsCommand 这种远程命令。事实事件更稳定,也更适合多个消费者扩展。
异步化必须接受最终一致性。同步链路中,调用完成即可认为所有动作完成;异步链路中,主单创建成功时,商机可能还未分配,通知可能还未发送,CRM 可能还未同步。业务页面、运营后台和客服工具都要能展示中间状态,并支持重试和补偿。
异步化还要处理失败传播方式的变化。同步调用失败可以直接抛异常;异步消费失败不能让用户重新提交,它需要重试、死信、失败表、人工处理和状态回滚策略。没有这些配套,异步化只会把错误藏起来。
项目落地
第一步是链路梳理。把原同步接口中的动作按重要性拆分为强依赖、弱依赖和后置任务。强依赖包括活动资格、重复提交幂等、核心单据创建;弱依赖包括客户标签、营销权益展示;后置任务包括商机分配、短信通知、CRM 同步、运营埋点。强依赖留在同步链路,弱依赖设置短超时和降级,后置任务事件化。
第二步是状态机设计。投保意向不能只有“成功/失败”,而应有 CREATED、WAIT_ASSIGN、ASSIGNED、NOTIFIED、SYNCED_CRM、FAILED 等状态或子状态。异步消费者每完成一个动作,就推进对应状态。页面可以显示“已提交,正在分配顾问”,运营后台可以看到哪些环节失败。
第三步是事件发布一致性。核心单据落库和事件发布必须绑定。可以用事务消息,也可以用本地消息表。对于团队可控性强的业务系统,本地消息表很常见:业务事务里写主单和消息记录,后台投递任务负责发送 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 传递、业务单号日志、消息积压、消费延迟、状态超时、补偿成功率。
第五阶段,做完整复盘:选择一个同步接口,画出改造前调用链、改造后事件流、失败补偿路径和监控指标。能讲清楚这张图,面试表达会非常扎实。