电商库存系统的分布式一致性方案:基于 TCC 的扣减、预留与回滚的工程实现

发布时间:2026/7/25 1:37:33
电商库存系统的分布式一致性方案:基于 TCC 的扣减、预留与回滚的工程实现 电商库存系统的分布式一致性方案基于 TCC 的扣减、预留与回滚的工程实现一、库存一致性——电商系统的基石电商库存系统面临一个经典分布式难题用户下单扣减库存如果后续支付失败库存必须归还——即预留→扣减→回滚的完整生命周期。在高并发秒杀场景下库存数量的最终一致性与实时准确性之间存在着尖锐矛盾。传统方案是使用数据库行锁UPDATE inventory SET count count - 1 WHERE sku_id ? AND count 0。这在工作量不大的场景中可行但在 TPS 10K 的电商场景中行锁成为性能瓶颈——每个 SKU 的更新排队导致 P99 延迟飙升到数百毫秒。TCCTry-Confirm-Cancel是分布式事务的一种补偿模式。Try 阶段预留资源库存锁定Confirm 阶段确认操作库存正式扣减Cancel 阶段释放预留库存恢复。与传统两阶段提交2PC不同TCC 不依赖于资源管理器RM的锁持有期间——资源在 Try 阶段已被修改为已预留状态后续 Confirm 或 Cancel 是幂等的。二、TCC 模式的协议原理与幂等性设计TCC 的核心在于幂等性设计。Confirm 和 Cancel 操作可能因网络重试被多次调用——如果 Cancel 被调用两次而第二次无预留可退不应报错。实现幂等的标准做法是在 Try 时为每笔预留分配唯一的reserve_idConfirm/Cancel 时先检查该reserve_id的状态。已处理的预留直接返回成功不重复执行业务操作。状态机驱动是另一关键设计。每笔预留经历INITTry 创建→ CONFIRMEDConfirm 后或 CANCELLEDCancel 后→ EXPIRED超时自动 Cancel。每个状态转换是单向且原子性的——使用 CAS 操作确保并发安全。超时自动 Cancel 是 TCC 的安全网。如果订单服务在 Try 后崩溃预留将永久占用库存。定时任务扫描超过 N 分钟的 INIT 状态记录触发自动 Cancel——确保库存资源的最终可回收性。三、Rust 实现的 TCC 库存服务use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; use anyhow::{Context, Result, bail}; use chrono::{Utc, DateTime}; use uuid::Uuid; /// 库存记录 #[derive(Debug, Clone)] pub struct InventoryRecord { pub sku_id: String, /// 可用库存可被 Try 预留的数量 pub available: i64, /// 已预留库存Try 成功但尚未 Confirm/Cancel pub reserved: i64, /// 已售出库存Confirm 后的数量 pub sold: i64, } /// TCC 预留的状态 #[derive(Debug, Clone, PartialEq)] pub enum ReserveStatus { /// Try 创建后的初始状态 Init, /// Confirm 后 Confirmed, /// Cancel 后或超时自动取消 Cancelled, } /// 预留记录 #[derive(Debug, Clone)] pub struct Reservation { pub reserve_id: String, pub sku_id: String, pub quantity: i64, pub status: ReserveStatus, pub created_at: DateTimeUtc, pub timeout_at: DateTimeUtc, } /// TCC 库存服务 /// 设计原因Try-Confirm-Cancel 是分布式事务的标准模式。 /// 通过预留机制将扣减与确认解耦 /// 库存的锁定与支付流程异步化。 pub struct TccInventoryService { /// SKU → 库存记录 inventory: ArcRwLockstd::collections::HashMapString, InventoryRecord, /// reserve_id → 预留记录 reservations: ArcRwLockstd::collections::HashMapString, Reservation, /// 预留超时时间秒 timeout_secs: i64, } impl TccInventoryService { pub fn new(timeout_secs: i64) - Self { Self { inventory: Arc::new(RwLock::new(std::collections::HashMap::new())), reservations: Arc::new(RwLock::new(std::collections::HashMap::new())), timeout_secs, } } /// Try: 预留库存 /// 原子操作available - qty, reserved qty /// 设计原因使用 CAS 语义——先检查可用量再扣减 /// 虽然在内存中操作但保留关系数据模型的设计意图 pub async fn try_reserve( self, sku_id: str, quantity: i64, ) - ResultOptionString { let mut inv self.inventory.write().await; let mut res self.reservations.write().await; let record inv.get_mut(sku_id) .context(SKU 不存在)?; // 检查可用库存是否充足 if record.available quantity { return Ok(None); // 库存不足——返回 None 而非错误 } // 预留减少可用增加已预留 record.available - quantity; record.reserved quantity; // 创建预留记录 let reserve_id Uuid::new_v4().to_string(); let now Utc::now(); let reservation Reservation { reserve_id: reserve_id.clone(), sku_id: sku_id.to_string(), quantity, status: ReserveStatus::Init, created_at: now, timeout_at: now chrono::Duration::seconds(self.timeout_secs), }; res.insert(reserve_id.clone(), reservation); tracing::info!( sku_id, quantity, reserve_id %reserve_id, 库存预留成功 ); Ok(Some(reserve_id)) } /// Confirm: 确认预留 /// 幂等操作——多次调用不会重复扣减 /// 设计原因网络超时重试导致重复调用 /// 幂等性保证系统在部分失败后仍可恢复 pub async fn confirm(self, reserve_id: str) - Result() { let mut res self.reservations.write().await; let reservation res.get_mut(reserve_id) .context(预留记录不存在)?; match reservation.status { ReserveStatus::Confirmed { // 幂等已经确认的预留直接返回成功 tracing::info!(reserve_id, 预留已确认幂等重试); return Ok(()); } ReserveStatus::Cancelled { bail!(预留 {} 已被取消无法确认, reserve_id); } ReserveStatus::Init { // 从已预留转为已售出 let mut inv self.inventory.write().await; let record inv.get_mut(reservation.sku_id) .context(SKU 记录不存在)?; record.reserved - reservation.quantity; record.sold reservation.quantity; reservation.status ReserveStatus::Confirmed; tracing::info!(reserve_id, 预留确认成功); } } Ok(()) } /// Cancel: 取消预留 /// 幂等操作 pub async fn cancel(self, reserve_id: str) - Result() { let mut res self.reservations.write().await; let reservation res.get_mut(reserve_id) .context(预留记录不存在)?; match reservation.status { ReserveStatus::Cancelled { tracing::info!(reserve_id, 预留已取消幂等重试); return Ok(()); } ReserveStatus::Confirmed { bail!(预留 {} 已确认无法取消, reserve_id); } ReserveStatus::Init { // 归还库存 let mut inv self.inventory.write().await; let record inv.get_mut(reservation.sku_id) .context(SKU 记录不存在)?; record.reserved - reservation.quantity; record.available reservation.quantity; reservation.status ReserveStatus::Cancelled; tracing::info!(reserve_id, 预留取消成功库存已归还); } } Ok(()) } /// 后台任务扫描超时预留并自动 Cancel /// 设计原因订单服务崩溃后无人调用 Cancel /// 超时自动回收确保库存不被永久锁定 pub async fn run_timeout_scanner(self) { loop { tokio::time::sleep(Duration::from_secs(10)).await; let now Utc::now(); let res self.reservations.read().await; let expired_ids: VecString res .iter() .filter(|(_, r)| { r.status ReserveStatus::Init r.timeout_at now }) .map(|(id, _)| id.clone()) .collect(); drop(res); // 释放读锁 for id in expired_ids { if let Err(e) self.cancel(id).await { tracing::error!(reserve_id %id, error %e, 超时取消失败); } } } } }代码的幂等性设计体现在 Confirm/Cancel 方法中重复调用已处理的预留直接返回成功而非报错。超时扫描器作为后台任务独立运行——与请求处理异步解耦。所有状态变更使用读写锁保护——库存更新的竞争集中在单个 SKU 上。四、方案边界与适用场景分析适用场景库存、优惠券、限购名额等需预留-确认的资源管理支付与库存分离的异步电商系统——支付由支付服务负责库存服务独立管理多 SKU 组合订单——每个 SKU 独立 Try/Cancel灵活性高。不适用场景需要 ACID 强一致性的金融转账——TCC 是最终一致性商品库存量极大 100 万/ SKU且从不缺货——TCC 的预留开销不必要调用链超过 5 个服务的超复杂事务——TCC 的 Cancel 路径复杂度指数增长。Trade-offsTCC 将一致性保证从数据库层转移到应用层。好处是消除了行锁瓶颈代价是代码复杂度增加。Confirm 和 Cancel 的实现需要仔细处理幂等性和并发安全。超时时间的选择是平衡点太短30s导致正常支付被 Cancel太长30min导致库存被无效占用。五、总结TCC 通过预留-确认-取消的三阶段模型在分布式系统中实现资源的最终一致性Confirm 和 Cancel 的幂等性设计是 TCC 的核心——通过状态机驱动和 reserve_id 唯一性保证超时自动 Cancel 是容错的关键——防止预留因服务崩溃而永久占用资源TCC 将锁从数据库层提升到应用层消除了行锁瓶颈但增加了代码复杂度库存服务与订单、支付服务通过事务日志实现异步解耦确保最终一致