Go 电商中台架构:订单、库存和支付的服务边界划分

一、一个下单请求穿越了 7 个服务的真实案例

电商中台初期,团队将所有业务逻辑塞在一个"订单服务"里。下单时这个服务要:校验库存、计算优惠、冻结积分、调支付网关、发消息通知、写订单表、更新用户统计。单体服务膨胀到 2 万行代码,每次发布都提心吊胆。

重构时最大的争论不是技术选型,而是服务边界到底怎么划。订单、库存、支付、优惠、物流——这些领域看起来独立,但在下单这个动作里环环相扣。边界划错了,拆得再细也没用。

二、电商中台服务边界模型

三、Go 实现核心领域服务

库存服务——核心中的核心

package inventory

import (
    "context"
    "database/sql"
    "fmt"
    "sync"
    "time"
)

// SKU 库存最小单位
type SKU struct {
    ProductID string
    SkuID     string
    WarehouseID string // 仓库 ID(多仓支持)
    Available int64    // 可用库存
    Reserved  int64    // 预占库存(待支付)
    Total     int64    // 总库存
}

// InventoryService 库存服务——独立的数据和业务逻辑
type InventoryService struct {
    db    *sql.DB  // 库存服务专用数据库
    redis *RedisClient
}

// ReserveStock 预占库存——下单时调用
// 预占成功后订单有效期为 15 分钟,超时自动释放
func (is *InventoryService) ReserveStock(
    ctx context.Context,
    orderID string,
    items []ReserveItem, // [{sku_id, quantity}]
) error {
    // 使用事务保证原子性
    tx, err := is.db.BeginTx(ctx, nil)
    if err != nil {
        return fmt.Errorf("开启事务失败: %w", err)
    }
    defer tx.Rollback()

    for _, item := range items {
        // SELECT ... FOR UPDATE 行锁,防止并发超卖
        row := tx.QueryRowContext(ctx,
            `SELECT available, reserved FROM inventory 
             WHERE sku_id = ? AND warehouse_id = ? FOR UPDATE`,
            item.SkuID, item.WarehouseID,
        )

        var available, reserved int64
        if err := row.Scan(&available, &reserved); err != nil {
            return fmt.Errorf("查询库存失败: sku=%s, err=%w", item.SkuID, err)
        }

        // 库存不足
        if available < item.Quantity {
            return fmt.Errorf("库存不足: sku=%s, 需求=%d, 可用=%d",
                item.SkuID, item.Quantity, available)
        }

        // 预占:available - N, reserved + N
        _, err = tx.ExecContext(ctx,
            `UPDATE inventory 
             SET available = available - ?, reserved = reserved + ?
             WHERE sku_id = ? AND warehouse_id = ?`,
            item.Quantity, item.Quantity, item.SkuID, item.WarehouseID,
        )
        if err != nil {
            return fmt.Errorf("预占库存失败: sku=%s, err=%w", item.SkuID, err)
        }
    }

    // 记录预占信息到 Redis(用于超时自动释放)
    if err := is.setReserveExpiry(ctx, orderID, 15*time.Minute); err != nil {
        return fmt.Errorf("设置预占过期失败: %w", err)
    }

    return tx.Commit()
}

// ConfirmStock 实扣库存——支付成功回调后调用
func (is *InventoryService) ConfirmStock(ctx context.Context, orderID string) error {
    tx, err := is.db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer tx.Rollback()

    // reserved - N, total - N(真实库存减少)
    // 此处的具体实现需要通过订单明细获取 SKU 列表
    _, err = tx.ExecContext(ctx,
        `UPDATE inventory 
         SET reserved = reserved - ?, total = total - ?
         WHERE order_id = ?`,
        // 参数通过查询预占记录获取
    )
    return err
}

// ReleaseStock 释放预占——订单超时或取消时调用
func (is *InventoryService) ReleaseStock(ctx context.Context, orderID string) error {
    tx, err := is.db.BeginTx(ctx, nil)
    if err != nil {
        return err
    }
    defer tx.Rollback()

    // reserved - N, available + N(归还可用库存)
    _, err = tx.ExecContext(ctx,
        `UPDATE inventory 
         SET reserved = reserved - ?, available = available + ?
         WHERE order_id = ?`,
    )
    return err
}

订单服务——编排角色

package order

import (
    "context"
    "fmt"
    "time"
)

// OrderStatus 订单状态机
type OrderStatus string

const (
    StatusPending   OrderStatus = "pending"     // 待支付
    StatusPaid      OrderStatus = "paid"        // 已支付
    StatusShipped   OrderStatus = "shipped"     // 已发货
    StatusCompleted OrderStatus = "completed"   // 已完成
    StatusCancelled OrderStatus = "cancelled"   // 已取消
    StatusRefunding OrderStatus = "refunding"   // 退款中
)

// Order 订单聚合根
type Order struct {
    OrderID     string
    UserID      string
    Items       []OrderItem
    TotalAmount float64
    Status      OrderStatus
    CreatedAt   time.Time
    PaidAt      *time.Time
}

// OrderService 订单服务——编排其他服务
type OrderService struct {
    db        *sql.DB
    inventory InventoryClient  // 调用库存服务
    payment   PaymentClient    // 调用支付服务
    coupon    CouponClient     // 调用优惠服务
    notify    NotifyClient     // 调用通知服务
    
    eventBus  EventBus         // 事件总线(异步通知)
}

// CreateOrder 创建订单——跨服务编排
func (os *OrderService) CreateOrder(ctx context.Context, req CreateOrderReq) (*Order, error) {
    // 步骤一:计算总金额 + 优惠
    totalAmount, err := os.calculateAmount(ctx, req.Items, req.CouponCode)
    if err != nil {
        return nil, fmt.Errorf("金额计算失败: %w", err)
    }

    // 步骤二:预占库存(调用库存服务)
    reserveReq := os.buildReserveReq(req.Items)
    if err := os.inventory.ReserveStock(ctx, reserveReq); err != nil {
        return nil, fmt.Errorf("库存预占失败: %w", err)
    }

    // 步骤三:创建订单(写订单表)
    order := &Order{
        OrderID:     generateOrderID(),
        UserID:      req.UserID,
        Items:       req.Items,
        TotalAmount: totalAmount,
        Status:      StatusPending,
        CreatedAt:   time.Now(),
    }

    if err := os.saveOrder(ctx, order); err != nil {
        // 保存失败 → 回滚库存预占
        os.inventory.ReleaseStock(ctx, order.OrderID)
        return nil, fmt.Errorf("保存订单失败: %w", err)
    }

    // 步骤四:异步通知——不阻塞下单流程
    os.eventBus.Publish(ctx, Event{
        Type:    "order.created",
        Payload: order,
    })

    // 设置订单超时(15 分钟后未支付自动取消)
    os.scheduleOrderExpiry(order.OrderID, 15*time.Minute)

    return order, nil
}

// HandlePaymentCallback 处理支付回调——状态机驱动
func (os *OrderService) HandlePaymentCallback(ctx context.Context, payResult PaymentResult) error {
    // 查询订单
    order, err := os.getOrder(ctx, payResult.OrderID)
    if err != nil {
        return err
    }

    // 状态校验:只有待支付的订单才能流转到已支付
    if order.Status != StatusPending {
        return fmt.Errorf("订单状态异常: 当前=%s, 期望=%s", order.Status, StatusPending)
    }

    // 更新订单状态
    now := time.Now()
    order.Status = StatusPaid
    order.PaidAt = &now
    
    if err := os.updateOrder(ctx, order); err != nil {
        return err
    }

    // 通知库存服务实扣
    if err := os.inventory.ConfirmStock(ctx, order.OrderID); err != nil {
        // 库存实扣失败 → 进入人工处理流程
        os.eventBus.Publish(ctx, Event{
            Type:    "order.stock_confirm_failed",
            Payload: order,
        })
        return fmt.Errorf("库存实扣失败,已转人工处理: %w", err)
    }

    // 发布支付完成事件
    os.eventBus.Publish(ctx, Event{Type: "order.paid", Payload: order})

    return nil
}

四、边界分析与 Trade-offs

分布式事务的挑战

  • 下单涉及多个服务,不能使用数据库事务保证一致性
  • 解决方案:Saga 模式(正向补偿 + 逆向补偿)
  • 库存预占失败 → 订单不创建(正向阻断)
  • 支付失败 → 释放库存 + 取消订单(逆向补偿)

每个服务独享数据库

  • 这是微服务架构的红线——不能通过共享数据库来"简化"开发
  • 跨服务数据查询通过 API 调用,不能直接 JOIN 其他服务的表
  • 如果需要报表查询,建立独立的只读库(由 CDC 同步)

服务间通信的选择

  • 同步调用(gRPC/HTTP):适合需要立即返回结果的场景(如库存查询)
  • 异步消息(Kafka/RabbitMQ):适合通知类场景(如发短信、更新统计)
  • 不推荐:直接读其他服务的数据库

库存服务的性能:高并发秒杀场景下,行锁会成为瓶颈。需要引入 Redis + Lua 脚本做前置限流,这是下篇文章的主题。

五、总结

电商中台服务边界的划分遵循 DDD 的聚合根原则:

  1. 库存服务——拥有库存数据,提供预占/实扣/释放能力(独立数据库)
  2. 订单服务——拥有订单数据,编排其他服务完成下单流程
  3. 支付服务——抽象支付渠道,处理回调通知
  4. 优惠服务——独立管理优惠规则和核销记录

边界一旦确定,就不要因为"方便"而共享数据库。短期方便带来的技术债,会在业务的快速增长中被成倍放大。

Logo

电商企业物流数字化转型必备!快递鸟 API 接口,72 小时快速完成物流系统集成。全流程实战1V1指导,营造开放的API技术生态圈。

更多推荐