Go 电商中台架构:订单、库存和支付的服务边界划分
·
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 的聚合根原则:
- 库存服务——拥有库存数据,提供预占/实扣/释放能力(独立数据库)
- 订单服务——拥有订单数据,编排其他服务完成下单流程
- 支付服务——抽象支付渠道,处理回调通知
- 优惠服务——独立管理优惠规则和核销记录
边界一旦确定,就不要因为"方便"而共享数据库。短期方便带来的技术债,会在业务的快速增长中被成倍放大。
更多推荐




所有评论(0)