分布式系统中接口时序不确定性处理

分布式系统中接口时序不确定性处理

一、核心问题

在分布式系统中,多个独立接口由外部系统分别调用,无法保证到达顺序。当接口 B 的处理依赖接口 A 的结果时,如果 B 先到、A 后到,B 的处理必然失败。

复制代码
期望时序:                           实际可能:
  A(创建订单) → B(补充条码)            B(补充条码) → A(创建订单)
       ↓              ↓                    ↓              ↓
    订单存在 → 条码关联成功            订单不存在 → 条码关联失败 ❌

产生原因

  • 外部系统多个模块独立回调,彼此不感知时序
  • 网络延迟不均,先发的请求可能后到
  • 外部系统内部有队列/异步处理,不同类型消息的处理速度不同
  • 多个微服务分别推送,无全局编排

不处理的后果

  • 依赖方处理失败,需等定时任务补偿(延迟从秒级变为分钟级甚至小时级)
  • 失败日志堆积,告警噪音
  • 定时任务负载增大
  • 用户体验下降(数据延迟可见)

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、解决思路

核心原则:谁后到,谁负责串联

复制代码
方案一:后到方主动触发先到方
┌─────────────────────────────────────────────┐
│  B 先到 → 标记待处理(等待 A)                │
│  A 后到 → 完成自身逻辑 → 检查 B 是否在等 → 触发 B │
└─────────────────────────────────────────────┘

方案二:先到方轮询等待
┌─────────────────────────────────────────────┐
│  B 先到 → 检查 A 是否完成 → 未完成 → 进入重试   │
│  A 后到 → 完成自身逻辑                         │
│  定时重试 → B 再次检查 → A 已完成 → B 处理成功   │
└─────────────────────────────────────────────┘

方案三:聚合后统一处理
┌─────────────────────────────────────────────┐
│  A 到达 → 写入聚合表                           │
│  B 到达 → 写入聚合表 → 检查聚合完整性 → 完整则处理 │
└─────────────────────────────────────────────┘

三、模式分类

模式 1:后到方主动触发(推荐)

特点:时延最短(秒级),不依赖定时任务,响应及时。

复制代码
A 后到时:完成自身 → 反查 B 的状态 → B 待处理则投递 MQ 触发 B
B 后到时:正常执行(A 已完成,依赖条件满足)

适用场景:两个接口有明确的依赖关系,且后到方能够通过数据关联找到先到方的记录。

模式 2:失败重试 + 定时补偿

特点:实现简单,但延迟较高。

复制代码
B 先到 → 尝试执行 → 依赖不满足 → 标记失败
定时任务 → 扫描失败记录 → 重新投递 → 此时 A 已完成 → B 成功

适用场景:时效性要求不高,或改造成本较高时的兜底方案。

模式 3:聚合等待模式

特点:所有条件到齐后才处理,不存在失败重试。

复制代码
A 到达 → 写片段到聚合表
B 到达 → 写片段到聚合表 → 检查是否全部到齐 → 到齐则执行

适用场景:多方数据需要全部到齐才能处理(如多物流节点聚合、分片数据汇总)。

模式 4:延迟消费

特点:通过延迟队列给依赖方留出到达时间。

复制代码
B 先到 → 放入延迟队列(等待 N 秒)→ N 秒后消费 → 此时 A 大概率已到

适用场景:两个接口的时间差通常很小(几秒内),用固定延迟覆盖绝大多数情况。


四、通用示例代码

4.1 状态表设计

sql 复制代码
CREATE TABLE event_dependency_log (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    event_type VARCHAR(32) NOT NULL COMMENT '事件类型',
    biz_key VARCHAR(128) NOT NULL COMMENT '业务唯一键',
    payload TEXT NOT NULL COMMENT '原始数据(JSON)',
    status CHAR(1) NOT NULL DEFAULT 'O' COMMENT 'O-待处理 P-失败 Y-成功',
    depend_on VARCHAR(32) COMMENT '依赖的事件类型',
    error_msg VARCHAR(512),
    retry_count INT NOT NULL DEFAULT 0,
    create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE INDEX uk_event_biz (event_type, biz_key),
    INDEX idx_status (status, event_type)
) COMMENT '事件依赖日志表';

4.2 模式 1 完整实现:后到方主动触发

接口 A:被依赖方(如"创建订单")
java 复制代码
@Service
@Slf4j
public class EventAService {

    @Resource
    private EventDependencyLogRepository logRepository;
    @Resource
    private EventAProcessor processor;
    @Resource
    private DependencyTriggerService triggerService;

    /**
     * 接收事件 A(被依赖方).
     * 完成自身逻辑后,主动检查并触发依赖自己的事件 B。
     */
    @Transactional(rollbackFor = Exception.class)
    public void handleEventA(String bizKey, Object payload) {
        // 1. 落日志
        Long logId = saveLog("EVENT_A", bizKey, payload);
        if (logId == null) return; // 已成功,幂等返回

        // 2. 执行核心业务逻辑(如创建订单、生成出库单)
        ProcessResult result = processor.process(payload);

        // 3. 标记自身成功
        markSuccess(logId);

        // 4. 【关键】主动触发依赖自己的事件 B
        triggerService.triggerDependentEvent(bizKey, "EVENT_B");
    }
}
接口 B:依赖方(如"补充条码")
java 复制代码
@Service
@Slf4j
public class EventBService {

    @Resource
    private EventDependencyLogRepository logRepository;
    @Resource
    private EventBMqSender mqSender;

    /**
     * 接收事件 B(依赖方).
     * 只落日志并投递 MQ,不关心依赖是否满足(由消费者判断)。
     */
    @Transactional(rollbackFor = Exception.class)
    public void handleEventB(String bizKey, Object payload) {
        Long logId = saveLog("EVENT_B", bizKey, payload);
        if (logId == null) return;

        // 事务提交后投递 MQ
        final Long finalLogId = logId;
        TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronizationAdapter() {
                @Override
                public void afterCommit() {
                    mqSender.send(finalLogId);
                }
            });
    }
}
事件 B 的消费者
java 复制代码
@Component
@Slf4j
public class EventBConsumer {

    @Resource
    private EventDependencyLogRepository logRepository;
    @Resource
    private EventBProcessor processor;

    @RabbitListener(queues = "${mq.queue.event-b}")
    public void consume(Long logId) {
        EventDependencyLog eventLog = logRepository.findById(logId).orElse(null);
        if (eventLog == null || "Y".equals(eventLog.getStatus())) {
            return;
        }

        try {
            // 执行业务逻辑(内部会查询事件A的结果)
            // 如果事件 A 尚未完成,这里会抛异常
            processor.process(eventLog.getPayload());

            eventLog.setStatus("Y");
            logRepository.saveAndFlush(eventLog);

        } catch (DependencyNotReadyException e) {
            // 依赖未就绪,标记 P,等待事件 A 完成后主动触发
            log.info("事件B依赖未就绪, bizKey={}, 等待事件A触发", eventLog.getBizKey());
            eventLog.setStatus("P");
            eventLog.setErrorMsg(e.getMessage());
            logRepository.saveAndFlush(eventLog);

        } catch (Exception e) {
            log.warn("事件B处理失败, logId={}", logId, e);
            eventLog.setStatus("P");
            eventLog.setRetryCount(eventLog.getRetryCount() + 1);
            eventLog.setErrorMsg(e.getMessage());
            logRepository.saveAndFlush(eventLog);
            throw e;
        }
    }
}
主动触发服务(核心组件)
java 复制代码
@Service
@Slf4j
public class DependencyTriggerService {

    @Resource
    private EventDependencyLogRepository logRepository;
    @Resource
    private EventBMqSender eventBMqSender;

    /**
     * 当事件 A 完成时,检查是否有依赖它的事件 B 在等待.
     * 如果存在且未完成,事务提交后投递 MQ 触发重新消费。
     *
     * @param bizKey 关联的业务键(两个事件通过此键关联)
     * @param dependentEventType 要检查的依赖事件类型
     */
    public void triggerDependentEvent(String bizKey, String dependentEventType) {
        // 通过业务键关联查找依赖事件
        String dependentBizKey = resolveDependentBizKey(bizKey);
        if (dependentBizKey == null) {
            return; // 无关联关系(如非该业务场景)
        }

        EventDependencyLog dependentLog = logRepository
            .findByBizKeyAndEventType(dependentBizKey, dependentEventType);

        // 不存在(尚未到达)或已成功,无需触发
        if (dependentLog == null || "Y".equals(dependentLog.getStatus())) {
            return;
        }

        Long targetLogId = dependentLog.getId();
        log.info("主动触发依赖事件, eventType={}, bizKey={}, logId={}",
            dependentEventType, dependentBizKey, targetLogId);

        // 事务提交后投递 MQ(保证当前事务数据对消费者可见)
        TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronizationAdapter() {
                @Override
                public void afterCommit() {
                    eventBMqSender.send(targetLogId);
                }
            });
    }

    /**
     * 从事件 A 的业务键推导出事件 B 的业务键.
     * 两个事件可能用不同的标识,需要通过数据关联。
     */
    private String resolveDependentBizKey(String eventABizKey) {
        // 示例:事件A用订单号,事件B用子单号+后缀
        // 通过关联表查出子单号
        SubOrder subOrder = subOrderRepository.findByOrderNo(eventABizKey);
        if (subOrder == null || subOrder.getSubOrderNo() == null) {
            return null;
        }
        return subOrder.getSubOrderNo() + "D";
    }
}

4.3 模式 3 实现:聚合等待

java 复制代码
@Service
@Slf4j
public class AggregationService {

    @Resource
    private EventFragmentRepository fragmentRepository;
    @Resource
    private AggregatedProcessor aggregatedProcessor;

    /**
     * 接收事件片段.
     * 每个片段到达时检查是否所有片段都已到齐,到齐则触发处理。
     */
    @Transactional(rollbackFor = Exception.class)
    public void receiveFragment(String aggregateKey, String fragmentType, Object payload) {
        // 1. 存储片段
        EventFragment fragment = new EventFragment();
        fragment.setAggregateKey(aggregateKey);
        fragment.setFragmentType(fragmentType);
        fragment.setPayload(JsonUtil.toJson(payload));
        fragment.setArrivedTime(new Date());
        fragmentRepository.saveAndFlush(fragment);

        // 2. 检查是否所有必需片段都已到齐
        Set<String> requiredTypes = getRequiredFragmentTypes(aggregateKey);
        Set<String> arrivedTypes = fragmentRepository.findArrivedTypes(aggregateKey);

        if (arrivedTypes.containsAll(requiredTypes)) {
            log.info("所有片段已到齐, aggregateKey={}", aggregateKey);
            // 3. 聚合处理
            List<EventFragment> allFragments = fragmentRepository
                .findByAggregateKey(aggregateKey);
            aggregatedProcessor.processAll(aggregateKey, allFragments);
        } else {
            Set<String> missing = new HashSet<>(requiredTypes);
            missing.removeAll(arrivedTypes);
            log.info("等待片段到达, aggregateKey={}, missing={}", aggregateKey, missing);
        }
    }

    private Set<String> getRequiredFragmentTypes(String aggregateKey) {
        // 根据业务规则确定需要哪些片段
        return Sets.newHashSet("ORDER_INFO", "BARCODE_INFO", "LOGISTICS_INFO");
    }
}

4.4 模式 4 实现:延迟消费

java 复制代码
@Service
@Slf4j
public class DelayedEventService {

    @Resource
    private DelayedMqSender delayedMqSender;

    /**
     * 接收依赖方事件,延迟 N 秒后再消费.
     * 给被依赖方留出到达时间。
     */
    @Transactional(rollbackFor = Exception.class)
    public void handleWithDelay(String bizKey, Object payload, int delaySeconds) {
        Long logId = saveLog("DELAYED_EVENT", bizKey, payload);
        if (logId == null) return;

        final Long finalLogId = logId;
        TransactionSynchronizationManager.registerSynchronization(
            new TransactionSynchronizationAdapter() {
                @Override
                public void afterCommit() {
                    // 投递到延迟队列
                    delayedMqSender.sendWithDelay(finalLogId, delaySeconds);
                }
            });
    }
}

@Component
@Slf4j
public class DelayedMqSender {

    @Resource
    private RabbitTemplate rabbitTemplate;

    /**
     * 投递延迟消息.
     * RabbitMQ 通过 x-delayed-message 插件或 TTL + 死信队列实现。
     */
    public void sendWithDelay(Long logId, int delaySeconds) {
        rabbitTemplate.convertAndSend("delayed-exchange", "event.delayed", logId,
            message -> {
                message.getMessageProperties().setDelay(delaySeconds * 1000);
                return message;
            });
    }
}

五、业务键关联策略

两个独立接口通常使用不同的业务标识,需要通过某种方式建立关联:

5.1 直接关联

两个接口入参中有共同字段:

java 复制代码
// 接口 A 入参:orderId = "ORDER_001"
// 接口 B 入参:orderId = "ORDER_001"
// 直接用 orderId 关联

5.2 间接关联(通过中间表)

java 复制代码
// 接口 A 入参:deliveryCode = "SO.001"
// 接口 B 入参:subOrderNo = "8900001D"
// 关联方式:
//   delivery_master.id → sub_table.delivery_id (取 sub_order_no)
//   sub_order_no + "D" = 接口 B 的 biz_key

private String resolveBizKey(String deliveryCode) {
    SubTable sub = subTableRepo.findByDeliveryCode(deliveryCode);
    if (sub == null || sub.getSubOrderNo() == null) {
        return null; // 非该业务场景
    }
    return sub.getSubOrderNo() + "D";
}

5.3 规则推导

java 复制代码
// 接口 A 入参:orderNo = "8900001"
// 接口 B 入参:bizKey = "8900001D"(固定后缀 D)
// 关联方式:字符串拼接规则

private String deriveBizKey(String orderNo) {
    return orderNo + "D";
}

六、并发安全设计

时序不确定还带来并发问题:两个接口可能几乎同时到达。

6.1 分布式锁隔离

java 复制代码
@Component
@Slf4j
public class ConcurrencySafeConsumer {

    @Resource
    private DistributedLockProvider lockProvider;

    public void consumeWithLock(String bizKey, Long logId) {
        // 同一业务键的两个事件用同一把锁
        String lockKey = "event:process:" + bizKey;
        DistributedLock lock = lockProvider.getLock(lockKey, 60, TimeUnit.SECONDS);

        if (!lock.tryLock(30, TimeUnit.SECONDS)) {
            log.warn("获取锁失败, bizKey={}", bizKey);
            return;
        }
        try {
            doProcess(logId);
        } finally {
            lock.unlock();
        }
    }
}

6.2 不同锁粒度避免互斥

如果两个事件用不同的锁,需要确保不会死锁:

java 复制代码
// 事件 A 的锁:按操作维度
String lockKeyA = "order:create:" + memberId;

// 事件 B 的锁:按单据维度
String lockKeyB = "barcode:process:" + subOrderNo;

// 两把锁互不影响,A 完成后触发 B 时不会被 B 的锁阻塞
// 因为触发动作是"投递 MQ"而非"直接调用 B 的逻辑"

6.3 事务提交后才发消息

java 复制代码
// A 的事务:创建订单 → 提交 → 释放锁 → 发送触发 B 的 MQ
// B 的消费:收到 MQ → 获取 B 的锁 → 查到 A 的数据(已提交可见)→ 处理

// 时序保证:
// 1. A 的数据已提交 → B 能查到
// 2. A 的锁已释放 → B 获取自己的锁不会被阻塞
// 3. MQ 在提交后才发 → 不存在数据未提交就消费的问题

七、多级保障组合

生产环境通常组合使用多种策略:

java 复制代码
@Service
@Slf4j
public class RobustEventHandler {

    /**
     * 完整的时序不确定性处理流程.
     *
     * 第一级:B先到,尝试处理
     *         → 依赖不满足,标记P
     *
     * 第二级:A后到,主动触发B(秒级)
     *         → 立即投递MQ,B重新消费
     *
     * 第三级:定时任务兜底(分钟级)
     *         → 扫描P状态记录重新投递
     *
     * 幂等保证:无论哪一级触发,多次执行结果一致
     */

    // 事件 A 处理完成
    @Transactional(rollbackFor = Exception.class)
    public void onEventACompleted(String bizKey) {
        // 核心逻辑...
        doBusinessLogic(bizKey);

        // 【第二级】主动触发
        triggerDependentEvent(bizKey);
    }

    // 事件 B MQ 消费
    public void onEventBConsumed(Long logId) {
        try {
            process(logId); // 可能因 A 未到而失败
        } catch (DependencyNotReadyException e) {
            markFailed(logId, e); // 【第一级】标记失败,等待触发
        }
    }

    // 【第三级】定时补偿
    @Scheduled(cron = "0 */5 * * * ?")
    public void compensate() {
        findFailedLogs().forEach(log -> mqSender.send(log.getId()));
    }
}

八、适用场景与选型建议

场景特征 推荐模式 理由
时效性要求高(秒级) 后到方主动触发 最快响应,不等调度周期
时效性一般(分钟级可接受) 失败重试 + 定时补偿 实现简单,改造量小
两接口时间差极小(<5秒) 延迟消费 固定延迟覆盖大多数情况
多方数据缺一不可 聚合等待 数据完整性优先
外部系统不可控(可能长时间不回调) 主动触发 + 定时 + 人工 多级保障

九、注意事项

  1. 关联查询要轻量:主动触发时的反查操作应走索引,避免全表扫描
  2. 触发失败不能影响主流程 :MQ 发送放在 afterCommit 中,且 sender 内部 try-catch,不回滚主事务
  3. 避免循环触发:A 触发 B,B 不应反过来触发 A(通过事件类型区分)
  4. 状态过滤 :只触发 O/P 状态的记录,Y 状态不重复触发
  5. 监控延迟分布:统计从"B 先到标记 P"到"A 触发 B 成功变 Y"的时间差,验证优化效果
  6. 降级方案:主动触发机制异常时,定时任务仍能兜底,不会造成数据永久不一致
相关推荐
深入云栈1 小时前
Netty 4.2.x 源码深度解析 (四):HashedWheelTimer —— 时间轮定时调度算法
java
带刺的坐椅1 小时前
别再把 Coding Agent 当智能补全了:SolonCode 想做的是数字员工
java·solon·codex·claudecode·opencode·soloncode
库克克1 小时前
【C++】C++11 包装器function 与 绑定器 bind
开发语言·c++
大模型码小白2 小时前
在 Windows 下 Codex 安装、配置与使用详细指南
java·人工智能·windows
多加点辣也没关系2 小时前
JavaScript|第30章:事件机制
开发语言·javascript
小大宇2 小时前
python milvus 案例
开发语言·python·milvus
Gauss松鼠会2 小时前
【GaussDB】GaussDB锁阻塞源头查询
java·开发语言·前端·数据库·算法·gaussdb·经验总结
mingo_敏2 小时前
DeepAgents : 后端(Backends)
java·开发语言
霸道流氓气质2 小时前
SpringBoot中事务内同步处理 + 事务后异步调用外部系统的通用模式示例
java·spring boot·后端