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

发布时间:2026/7/21 0:46:45

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

相关新闻