返回架构笔记
Arch Day 71

Arch Day 71: 购物车与结算链路

Arch Day 71: 购物车与结算链路

2026-06-09
第三阶段 - 零售域深度
电商购物车结算高并发库存一致性

日期: 2026-06-09 (Day 71) 阶段: 第三阶段 - 零售域深度 标签: #电商 #购物车 #结算 #高并发 #库存一致性


核心概念

一句话定义

购物车是用户"购买意图"的容器,结算链路是将意图转化为订单的关键路径——这条路径的每一步都涉及价格一致性、库存可靠性和支付安全性的多方博弈。

为什么关注

购物车到结算是电商转化的"最后一公里"。行业数据显示,全球电商购物车放弃率高达70%(2025年Baymard Institute)。这意味着10个加入购物车的用户中只有3个最终完成支付。结算链路的每一毫秒延迟、每一个异常报错、每一次库存不足提示,都可能导致用户流失。从架构角度,结算链路是电商系统中并发最高、一致性要求最严、出错代价最大的链路之一。

误区与反模式

误区现实
"购物车就是个列表"购物车承载促销计算、库存校验、价格实时更新等复杂逻辑
"下单就是插一条记录"下单涉及库存预扣、优惠锁定、风控校验、幂等保障等编排
"库存扣减在创建订单时做"应该在支付前预扣、支付后真扣,否则超卖或占库存
"价格以结算页显示为准"需要三级校验:购物车价→下单价→支付价
"同步处理所有逻辑"非核心路径(积分、通知、日志)应异步化

知识点详解

一、购物车设计

1.1 购物车类型

类型存储位置生命周期特点
游客车(Guest Cart)Cookie/LocalStorage浏览器生命周期无需登录,容量有限
会员车(User Cart)服务端(Redis+DB)长期持久化多端同步,可恢复
临时车(Session Cart)服务端Session会话期间登录前缓存
小程序车微信Storage小程序生命周期受平台限制

1.2 游客车→会员车合并策略

class CartMergeStrategy:
    """购物车合并策略"""

    def merge(self, guest_cart, user_cart):
        """
        登录时合并游客车和会员车
        原则:用户体验优先,不丢失任何商品
        """
        merged = copy.deepcopy(user_cart)

        for guest_item in guest_cart.items:
            existing = merged.find_item(guest_item.sku_id)

            if existing:
                # 策略1: 取较大数量
                existing.quantity = max(existing.quantity, guest_item.quantity)
                # 策略2: 数量相加(需限制上限)
                # existing.quantity = min(existing.quantity + guest_item.quantity, MAX_QTY)
            else:
                # 游客车中的新商品加入会员车
                merged.add_item(guest_item)

        # 清空游客车
        guest_cart.clear()

        # 重新计算促销
        merged.recalculate_promotions()

        return merged

1.3 购物车数据模型

┌─────────────────────────────────────────┐
│              Cart (购物车)                │
│  ├── cartId: String                      │
│  ├── userId: String (nullable for guest) │
│  ├── cartType: GUEST | USER              │
│  ├── items: List<CartItem>               │
│  │   ├── skuId: String                   │
│  │   ├── skuName: String                 │
│  │   ├── quantity: Integer               │
│  │   ├── originalPrice: Decimal          │
│  │   ├── currentPrice: Decimal           │
│  │   ├── selected: Boolean               │
│  │   ├── stockStatus: IN_STOCK|LOW|OUT   │
│  │   ├── shopId: String                  │
│  │   └── promotionTags: List<String>     │
│  ├── promotions: List<CartPromotion>     │
│  │   ├── promotionId, discount, type     │
│  │   └── appliedItems: List<skuId>       │
│  ├── totalOriginalPrice: Decimal         │
│  ├── totalDiscount: Decimal              │
│  ├── totalPayable: Decimal               │
│  ├── updatedAt: Timestamp                │
│  └── expireAt: Timestamp                 │
└─────────────────────────────────────────┘

1.4 购物车存储架构

写入路径:
  用户操作(加购/改量/删除)
       │
       ▼
  ┌──────────┐    异步持久化    ┌──────────┐
  │  Redis    │ ─────────────→ │  MySQL    │
  │ (主存储)  │                │ (备份)    │
  └──────────┘                └──────────┘

读取路径:
  打开购物车
       │
       ▼
  ┌──────────┐    Cache Miss    ┌──────────┐
  │  Redis    │ ←─────────────  │  MySQL    │
  │ (热数据)  │                │ (冷数据)   │
  └──────────┘                └──────────┘
       │
       ▼
  实时校验(库存+价格+促销)
       │
       ▼
  返回完整购物车数据

1.5 购物车读写优化

场景:双11前,用户疯狂加购(写场景优化)
  ├── Redis Hash存储:HSET cart:{userId} {skuId} {json}
  ├── 加购只更新单个field,不需要读写整个购物车
  ├── 异步落DB:MQ → Consumer → MySQL
  └── 限流:单用户每秒最多5次加购操作

场景:双11开始,用户打开购物车结算(读场景优化)
  ├── 批量查询:Pipeline批量获取SKU信息
  ├── 并行计算:库存/价格/促销并行查询
  ├── 预计算:大促前预计算促销价缓存
  └── 降级:促销计算超时则返回原价+异步刷新

二、结算页设计

2.1 结算页要素

┌──────────────────────────────────────────┐
│              结算页(Checkout)              │
│                                          │
│  ① 商品确认                               │
│  ┌────────────────────────────────┐      │
│  │ [✓] 商品A × 2    ¥99.00/件    │      │
│  │ [✓] 商品B × 1    ¥199.00/件   │      │
│  └────────────────────────────────┘      │
│                                          │
│  ② 收货地址                               │
│  ┌────────────────────────────────┐      │
│  │ 张三 138****1234               │      │
│  │ 北京市朝阳区xxx路xxx号          │  [>]  │
│  └────────────────────────────────┘      │
│                                          │
│  ③ 配送方式                               │
│  ┌────────────────────────────────┐      │
│  │ ○ 普通快递 (预计3-5天)  免运费   │      │
│  │ ● 次日达            +¥8.00    │      │
│  └────────────────────────────────┘      │
│                                          │
│  ④ 优惠信息                               │
│  ┌────────────────────────────────┐      │
│  │ 满减优惠:满300减50    -¥50.00  │      │
│  │ 优惠券:新人8折券     -¥39.60   │      │
│  └────────────────────────────────┘      │
│                                          │
│  ⑤ 支付方式                               │
│  ┌────────────────────────────────┐      │
│  │ ● 支付宝  ○ 微信  ○ 银行卡     │      │
│  └────────────────────────────────┘      │
│                                          │
│  ⑥ 金额汇总                               │
│  ┌────────────────────────────────┐      │
│  │ 商品总价          ¥397.00      │      │
│  │ 运费              +¥8.00      │      │
│  │ 优惠              -¥89.60     │      │
│  │ ────────────────────────       │      │
│  │ 应付金额          ¥315.40     │      │
│  └────────────────────────────────┘      │
│                                          │
│  ┌────────────────────────────────┐      │
│  │     ¥315.40  [ 提交订单 ]       │      │
│  └────────────────────────────────┘      │
└──────────────────────────────────────────┘

2.2 结算页数据聚合

结算页一次展示需要聚合多个域的数据:

数据来源查询内容延迟要求
商品服务SKU信息、图片、名称<50ms
库存服务实时可用库存<30ms
价格服务当前售价 + 促销价<50ms
地址服务用户地址列表<30ms
物流服务可用配送方式 + 运费<100ms
促销服务可用优惠 + 计算结果<100ms
风控服务用户风险评估<50ms

优化策略:并行调用 + 超时降级

public CheckoutPageDTO buildCheckoutPage(CheckoutRequest request) {
    // 并行调用各服务(CompletableFuture)
    CompletableFuture<SkuInfoList> skuFuture =
        CompletableFuture.supplyAsync(() -> skuService.batchQuery(request.getSkuIds()));

    CompletableFuture<StockResult> stockFuture =
        CompletableFuture.supplyAsync(() -> stockService.batchCheck(request.getSkuIds()));

    CompletableFuture<PriceResult> priceFuture =
        CompletableFuture.supplyAsync(() -> priceService.calculate(request));

    CompletableFuture<AddressList> addressFuture =
        CompletableFuture.supplyAsync(() -> addressService.listByUser(request.getUserId()));

    CompletableFuture<DeliveryOptions> deliveryFuture =
        CompletableFuture.supplyAsync(() -> deliveryService.getOptions(request));

    CompletableFuture<PromotionResult> promoFuture =
        CompletableFuture.supplyAsync(() -> promotionService.calculate(request));

    // 等待所有结果(总超时500ms)
    CompletableFuture.allOf(skuFuture, stockFuture, priceFuture,
                           addressFuture, deliveryFuture, promoFuture)
                     .get(500, TimeUnit.MILLISECONDS);

    // 聚合结果
    return assembleCheckoutPage(...);
}

三、下单链路设计

3.1 下单全流程

用户点击"提交订单"
       │
       ▼
 ① 前端防重(按钮禁用+requestId)
       │
       ▼
 ② 网关层限流(令牌桶)
       │
       ▼
 ③ 幂等校验(requestId去重)
       │
       ▼
 ④ 参数校验(地址/商品/数量)
       │
       ▼
 ⑤ 风控校验(黑名单/异常行为)
       │
       ▼
 ⑥ 库存预扣(Redis原子减)
       │
       ▼
 ⑦ 优惠锁定(锁定优惠券+计算)
       │
       ▼
 ⑧ 价格校验(三级价格对比)
       │
       ▼
 ⑨ 创建订单(写DB)
       │
       ▼
 ⑩ 发送下单成功消息(MQ)
       │
       ├── 异步:通知仓库备货
       ├── 异步:发送短信/推送
       ├── 异步:积分/成长值
       └── 异步:数据统计
       │
       ▼
 返回订单号 + 拉起支付

3.2 库存预扣机制

时间线:
  T0: 用户点击下单
  T1: Redis库存预扣(原子操作) ← 关键步骤
  T2: 创建订单(状态=待支付)
  T3: 用户支付(15分钟超时)
  T4a: 支付成功 → DB库存真扣 → 订单状态→已支付
  T4b: 支付超时 → Redis库存回补 → 订单状态→已取消
class InventoryService:
    """库存预扣服务"""

    # Redis Lua脚本:原子预扣
    DEDUCT_SCRIPT = """
    local stock = tonumber(redis.call('GET', KEYS[1]))
    local quantity = tonumber(ARGV[1])
    if stock >= quantity then
        redis.call('DECRBY', KEYS[1], quantity)
        return 1  -- 扣减成功
    else
        return 0  -- 库存不足
    end
    """

    def pre_deduct(self, sku_id, quantity, order_id):
        """
        库存预扣(Redis原子操作)
        """
        key = f"stock:available:{sku_id}"
        result = self.redis.eval(self.DEDUCT_SCRIPT, 1, key, quantity)

        if result == 1:
            # 记录预扣流水(用于超时回补)
            self.redis.setex(
                f"stock:hold:{order_id}:{sku_id}",
                900,  # 15分钟TTL
                quantity
            )
            return True
        return False

    def confirm_deduct(self, order_id, sku_id, quantity):
        """
        支付成功后,确认扣减(DB持久化)
        """
        # DB真扣
        self.db.execute(
            "UPDATE inventory SET stock = stock - %s "
            "WHERE sku_id = %s AND stock >= %s",
            (quantity, sku_id, quantity)
        )
        # 删除预扣记录
        self.redis.delete(f"stock:hold:{order_id}:{sku_id}")

    def release_hold(self, order_id, sku_id, quantity):
        """
        超时/取消时,释放预扣库存
        """
        key = f"stock:available:{sku_id}"
        self.redis.incrby(key, quantity)
        self.redis.delete(f"stock:hold:{order_id}:{sku_id}")

3.3 价格三级校验

class PriceConsistencyChecker:
    """
    价格一致性三级校验
    防止:前端篡改/缓存过期/促销变更导致的价格不一致
    """

    def check(self, checkout_request, order):
        # Level 1: 购物车价格 vs 下单价格
        cart_total = self.get_cart_total(checkout_request.cart_id)
        order_total = order.total_payable
        if abs(cart_total - order_total) > Decimal('0.01'):
            raise PriceInconsistencyError("购物车价格与下单价格不一致")

        # Level 2: 下单价格 vs 实时计算价格
        realtime_total = self.price_service.calculate_realtime(order.items)
        if abs(order_total - realtime_total) > Decimal('0.01'):
            # 价格变动容忍策略
            if realtime_total < order_total:
                # 实时更便宜→用实时价(用户受益)
                order.update_price(realtime_total)
            else:
                # 实时更贵→提示用户价格变动
                raise PriceChangedError(
                    f"价格已变动: {order_total} → {realtime_total}"
                )

        # Level 3: 下单价格 vs 支付价格(支付回调时校验)
        # 在支付回调handler中执行
        pass

四、高并发下单优化

4.1 性能瓶颈分析

瓶颈点原因影响
库存热点爆款商品并发写Redis热点key
数据库写入订单+订单项+流水MySQL写TPS上限
分布式锁优惠券核销/库存扣减锁竞争
外部调用支付/物流/风控网络延迟
促销计算复杂叠加规则CPU密集

4.2 优化方案矩阵

层级优化手段效果
前端按钮防抖、请求去重、排队等待动画减少无效请求
网关令牌桶限流、用户级QPS控制保护后端
应用异步化非核心逻辑、预计算降低RT
缓存Redis预扣、本地缓存热点数据减少DB压力
队列订单创建异步化(MQ削峰)平滑写入
数据库分库分表、批量写入、延迟落库提升写TPS

4.3 异步下单架构

同步路径(用户感知):
  请求 → 限流 → 幂等 → 风控 → 库存预扣(Redis) → 返回"下单成功,排队中"
  耗时: <200ms

异步路径(后台处理):
  MQ → 创建订单(DB) → 锁定优惠券 → 发通知 → 更新状态为"待支付"
  耗时: 1-5s

用户体验:
  "提交订单" → 立即反馈"排队中" → 轮询/推送 → "下单成功,请支付"
// 同步路径:快速响应
@PostMapping("/submit-order")
public OrderSubmitResult submitOrder(OrderRequest request) {
    // 1. 幂等校验
    if (idempotentService.isDuplicate(request.getRequestId())) {
        return OrderSubmitResult.duplicate();
    }

    // 2. 库存预扣(Redis)
    boolean stockOk = inventoryService.preDeduct(request.getItems());
    if (!stockOk) {
        return OrderSubmitResult.stockInsufficient();
    }

    // 3. 生成订单号
    String orderNo = orderNoGenerator.generate();

    // 4. 发送到MQ(异步创建订单)
    orderMQ.send(new CreateOrderMessage(orderNo, request));

    // 5. 立即返回(用户不需要等DB写入完成)
    return OrderSubmitResult.queued(orderNo);
}

五、幂等设计

public class IdempotentService {
    /**
     * 下单幂等:同一个requestId只创建一次订单
     * 实现:Redis SETNX + 订单表唯一索引双重保障
     */
    public boolean tryAcquire(String requestId) {
        // Redis: SETNX,设置30分钟过期
        Boolean acquired = redis.opsForValue()
            .setIfAbsent("idempotent:" + requestId, "1", 30, TimeUnit.MINUTES);

        if (Boolean.TRUE.equals(acquired)) {
            return true;  // 首次请求
        }

        // 已存在→检查订单是否已创建
        Order existing = orderRepository.findByRequestId(requestId);
        if (existing != null) {
            // 已有订单→直接返回(不重复创建)
            return false;
        }

        // Redis有记录但订单未创建(上次失败了)→允许重试
        return true;
    }
}

对比分析

下单架构方案对比

维度同步下单异步下单预扣+异步排队下单
用户体验即时反馈延迟反馈快速预反馈排队等待
吞吐量低(受DB限制)极高
一致性强一致最终一致最终一致最终一致
复杂度中高
回滚难度简单需补偿需补偿需补偿
适用场景低并发/B2B常规电商大促秒杀
典型RT200-500ms50ms+异步100ms+异步50ms+排队

购物车存储方案对比

方案优点缺点适用
纯Cookie无服务端存储容量小、不安全简单H5
LocalStorage容量大不跨端单端应用
Redis高性能、跨端需持久化备份主流方案
MySQL持久可靠性能低低频场景
Redis+MySQL兼顾性能和可靠一致性管理大型电商

架构设计实操

设计目标

设计高并发下单链路,满足:

  • 大促峰值10万QPS
  • 下单响应时间<200ms
  • 库存准确(不超卖不少卖)
  • 价格一致性保障

方案设计

┌──────────────────────────────────────────────────────────────┐
│                         客户端                                │
│  按钮防抖 + requestId + 排队等待UI                             │
└───────────────────────────┬──────────────────────────────────┘
                            │
┌───────────────────────────▼──────────────────────────────────┐
│                      API Gateway                             │
│  限流(10万QPS) → 用户级限流(5QPS/人) → 黑名单 → 路由            │
└───────────────────────────┬──────────────────────────────────┘
                            │
┌───────────────────────────▼──────────────────────────────────┐
│                    下单编排服务(BFF)                           │
│  ① 幂等校验(Redis SETNX)                                     │
│  ② 风控校验(异步,超时放行)                                    │
│  ③ 库存预扣(Redis Lua原子操作)                                 │
│  ④ 价格校验(本地缓存+实时计算)                                 │
│  ⑤ 生成订单号(Snowflake)                                     │
│  ⑥ 发MQ(异步创建订单)                                        │
│  ⑦ 返回: {orderNo, status:"queued"}                          │
│  耗时: <150ms                                                │
└───────────────────────────┬──────────────────────────────────┘
                            │ MQ
┌───────────────────────────▼──────────────────────────────────┐
│                    订单创建服务(异步)                           │
│  ① 消费MQ消息                                                │
│  ② 锁定优惠券                                                │
│  ③ 创建订单+订单项(DB)                                        │
│  ④ 发送下单成功事件                                           │
│  ⑤ 通知用户(推送/轮询)                                        │
│  耗时: 1-3s                                                  │
└──────────────────────────────────────────────────────────────┘

ADR: 库存扣减采用Redis预扣+DB异步确认

背景: 高并发场景下数据库直接扣库存会成为瓶颈

决策: 采用"Redis预扣→MQ异步→DB确认"三段式库存扣减

理由:

  • Redis单线程原子操作避免超卖
  • 单实例Redis可支持10万+QPS
  • MQ削峰平滑DB写入压力
  • 预扣超时自动回补保证不少卖

权衡:

  • 优势:高吞吐、不超卖
  • 劣势:Redis和DB不一致窗口(秒级)、Redis宕机需要恢复
  • 兜底:定时对账任务,每小时比较Redis和DB库存

AI增强实践

1. AI智能购物车推荐

class AICartRecommender:
    """AI驱动的购物车推荐"""

    def recommend_additions(self, cart):
        """购物车中追加推荐"""
        # 基于购物车商品进行关联推荐
        cart_embeddings = [
            self.item_encoder.encode(item.sku_id)
            for item in cart.items
        ]

        # 向量搜索相似/互补商品
        candidates = self.vector_db.search(
            query=mean(cart_embeddings),
            filter={"not_in": [i.sku_id for i in cart.items]},
            top_k=20
        )

        # 加入促销信号重排
        for c in candidates:
            c.score *= self.get_promo_boost(c, cart)  # 有凑单优惠的加权
            c.score *= self.get_margin_boost(c)        # 高毛利商品加权

        return sorted(candidates, key=lambda x: x.score, reverse=True)[:5]

    def smart_coupon_suggest(self, cart):
        """智能凑单提示"""
        available_promos = self.promo_service.get_available(cart)
        for promo in available_promos:
            gap = promo.threshold - cart.total_amount
            if 0 < gap <= 50:  # 差50元以内可以凑单
                suggest_items = self.find_items_near_price(gap)
                return f"再买¥{gap}即可享受{promo.description},推荐加购:{suggest_items}"

2. AI预测性缓存预热

AI能力说明效果
热点预测预测大促哪些SKU将成为热点提前预热缓存
库存预警预测库存消耗速度提前补货/限购
流量预测预测各时间段QPS动态扩容
转化预测预测购物车转化概率智能催付策略

与Web3/DeFi的关联

结算链路 × Web3

Web2结算Web3结算
支付宝/微信/银行卡稳定币(USDC/USDT)支付
15分钟支付超时区块确认时间(秒级~分钟级)
平台担保智能合约Escrow
中心化对账链上自动清结算
积分奖励Token奖励(可交易)

DeFi DEX交易 vs 电商下单

DEX Swap流程(本质也是"下单链路"):
  ① 用户发起Swap请求
  ② 前端获取报价(类似价格计算)
  ③ 用户确认(Slippage tolerance = 价格容忍度)
  ④ 提交交易(类似"提交订单")
  ⑤ 链上执行(类似"库存扣减")
  ⑥ 确认(类似"支付成功")

共通的设计挑战:
  - 价格一致性(报价→成交的价格滑移)
  - 库存/流动性争抢(多人竞争同一资源)
  - 失败回滚(交易revert = 订单取消)
  - 前端防重(nonce机制 = requestId幂等)

今日思考

1. 购物车的"实时性"和"性能"如何平衡?

每次打开购物车都要校验库存和价格,数据实时但慢;缓存后快但可能过期。最佳实践是"分层实时":商品基础信息缓存(5分钟更新)、价格信息近实时(1分钟更新)、库存信息实时查询但有降级(超时返回上次结果)。关键商品(即将售罄)实时校验,普通商品缓存校验。

2. 分布式事务在下单链路中如何处理?

下单涉及库存、优惠、订单三个服务的数据一致。强一致方案(2PC/XA)性能太差。实践中采用"Saga+补偿"模式:库存预扣→订单创建→优惠锁定,任一步失败触发补偿(回滚已执行的步骤)。最终一致性通过定时对账保障。

3. "先扣库存再创建订单" vs "先创建订单再扣库存"?

先扣库存:不会超卖,但可能占库存(下单后不支付)。先创建订单:体验好,但可能超卖。行业主流方案是"先扣库存"——因为超卖的损失(赔偿/信誉)远大于占库存的损失(超时自动释放)。阿里的方案是Redis预扣(快)+ DB乐观锁确认(准确)。


面试题准备

题目:下单链路如何保证库存和价格一致性?高并发下如何优化?

30秒回答: 库存用"Redis预扣+DB异步确认"保证不超卖;价格用"三级校验"(购物车→下单→支付)保证一致性。高并发优化核心是"同步链路极简化"——只做幂等+风控+库存预扣,其余全部异步。

2分钟回答

库存一致性方案

  1. Redis Lua脚本原子预扣(10万QPS级)
  2. 预扣设15分钟TTL,超时自动回补
  3. 支付成功后通过MQ触发DB真扣(乐观锁防并发)
  4. 定时对账任务兜底(Redis vs DB差异修复)

价格一致性方案

  1. Level1: 结算页展示前实时计算价格
  2. Level2: 提交订单时二次计算,比对差异
  3. Level3: 支付回调时校验支付金额与订单金额
  4. 订单快照:下单时刻的价格、优惠信息完整记录

高并发优化

  1. 前端:防抖+去重+排队UI
  2. 网关:全局限流(10万QPS) + 用户限流(5QPS/人)
  3. 同步链路:只做预扣+生成订单号+发MQ → <150ms
  4. 异步链路:MQ消费→创建订单→通知用户
  5. 热点隔离:爆款SKU独立Redis实例

追问准备

  • Q: Redis挂了怎么办?→ Redis Cluster + 降级到DB直接扣(性能降低但保证可用)
  • Q: MQ消息丢了怎么办?→ 定时扫描"预扣但未创建订单"的记录,补偿处理
  • Q: 高并发下DB写不过来?→ MQ削峰 + 批量写入(100条/批) + 分库分表

学习资源


明日预告

Day 72: 搜索与推荐系统 —— "搜索"是用户带着明确意图找商品,"推荐"是系统猜测用户可能喜欢什么。两者架构看似不同,底层正在融合。AI时代的搜索推荐正在经历从关键词匹配到语义理解、从协同过滤到LLM生成式推荐的范式转变。