知用网
白蓝主题五 · 清爽阅读
首页  > 网络运维

微服务架构下如何保障数据一致性

服务拆分后,订单和库存怎么对得上?

很多公司一开始用单体架构,订单、用户、库存全在一个系统里,改数据就像在自家厨房炒菜,锅碗瓢盆都归你管。可随着业务变大,团队一拆,服务一分,订单归A组,库存归B组,这时候问题就来了——用户下单成功,库存没减,或者库存减了,订单却没生成,客户以为买到了,实际仓库还在原地踏步。

分布式事务不是万能解药

有人第一反应是上分布式事务,比如XA协议或者Seata这类框架。听起来靠谱,但实际用起来像拖着铁链跑步。一个下单操作要跨订单、库存、支付三个服务,全都得锁住资源,等所有环节确认才能提交。网络抖一下,某个服务响应慢一点,整个流程卡住,用户体验直接掉到底。高并发场景下,系统吞吐量断崖式下跌,运维半夜就得被报警叫醒。

最终一致性更接地气

现实中的做法,往往是接受“短暂不一致”。比如用户提交订单,系统先写入订单表,状态设为“待支付”,然后发一条消息到MQ,通知库存服务扣减。哪怕库存服务暂时挂了,消息也能堆积,等它恢复后再处理。只要保证消息不丢、不重,数据最终会走到一致状态。

这种模式的关键在于补偿机制。比如库存不足,扣减失败,那就再发一条消息回滚订单,或者自动取消。整个过程像快递退货——先签收,发现问题再寄回去,虽然多走几步,但整体顺畅。

本地消息表解决可靠事件投递

直接发MQ有个风险:订单写成功了,还没来得及发消息,服务宕机,消息丢了,库存永远不知道发生了什么。这时候可以用“本地消息表”——把要发的消息和订单数据一起写进数据库同一个事务。

<?php
// 伪代码示例:本地消息表写入
beginTransaction();

createOrder($orderId, $userId, $amount);
insertIntoMessageTable($orderId, 'DECREASE_STOCK', json_encode($items));

commit();
?>

然后起个定时任务,扫描消息表里未发送的消息,逐个推送到MQ。即使服务重启,消息也不会丢。消费方处理完,也要通知生产方或自己标记已处理,避免重复扣库存。

Saga模式:把长流程拆成可逆步骤

对于涉及多个服务的复杂流程,比如下单+扣库存+积分+优惠券,Saga模式更合适。它把整个流程拆成一系列小步骤,每一步都有对应的补偿操作。比如:

  • 创建订单 → 失败则无需补偿
  • 扣减库存 → 失败则调用“释放库存”
  • 扣除优惠券 → 失败则“返还优惠券”
  • 增加积分 → 失败则“扣回积分”

这些步骤可以同步调用,也可以异步通过事件驱动。一旦某一步出错,就按反向顺序执行补偿,像撤销Git提交一样一步步回退。

幂等设计是底线要求

无论用哪种方案,接口必须支持幂等。网络超时很常见,客户端可能重复提交请求。比如库存服务收到两次扣减指令,结果不该是扣两遍,而应该判断该订单是否已经处理过,是就直接返回成功。

<?php
// 伪代码:幂等控制
$lockKey = 'stock_decr_' . $orderId;
if (redis->set($lockKey, 1, ['nx', 'ex' => 3600])) {
    // 执行扣减逻辑
    decreaseStock($items);
} else {
    // 已处理,直接返回
    return ['code' => 0, 'msg' => 'already processed'];
}
?>

用Redis做去重是最常见的手段,也可以基于数据库唯一索引,比如把订单ID和操作类型作为联合主键,重复插入直接报错。

监控和人工干预不能少

再完善的机制也挡不住极端情况。比如消息积压太多,补偿服务挂了几天没人发现,数据就真不一致了。所以得有对账系统,每天跑一次订单和库存的差异,发现异常自动告警,严重时触发人工介入。电商大促前,运维团队盯着数据流水看,就跟家长盯孩子成绩单一样仔细。