电商平台
商品、订单、库存、支付、营销全链路的电商平台开发实践。电商是最经典的后端练兵场——业务谁都懂,但把「下单」这一个动作在高并发下做对,里面全是学问。
一、系统架构
┌─────────────┐
用户 ──────→ │ 网关/Nginx │
└──────┬──────┘
┌───────┬───────┬───┴────┬────────┬──────────┐
▼ ▼ ▼ ▼ ▼ ▼
商品服务 订单服务 库存服务 支付服务 营销服务 用户服务
│ │ │ │ │
└───────┴───┬───┴────────┴────────┘
▼
MySQL(分库分表) + Redis(缓存) + MQ(异步解耦)技术栈:Spring Boot + MySQL + Redis + RocketMQ【待补充:实际选型】。服务规模按业务阶段演进:初期单体跑得欢,订单量上来后按「最容易成为瓶颈」的顺序拆——先拆库存和订单,商品服务最后拆(读多写少,加缓存比拆服务有效)。
二、核心设计
1. 商品与缓存
商品页是典型的读多写少,三板斧:
- Cache-Aside:读请求先查 Redis,未命中回源 MySQL 并回填;商品变更时删缓存(不是更新缓存——删除比更新安全,避免并发写的脏数据)
- 缓存三大问题的标准应对:热 key 本地缓存二级兜底;缓存空值防穿透;互斥重建防击穿
- 多级价格:营销价、会员价、限时价叠在基础价上,价格计算独立成定价服务,规则引擎可配置
2. 订单:状态机是灵魂
订单的一生是一条状态机链路,用显式状态机而不是散落的 if-else:
待支付 ──支付成功──→ 待发货 ──发货──→ 已发货 ──确认收货──→ 已完成
│ │
└──超时未支付──→ 已取消 售后申请──→ 退款/退货流程非法状态转移(比如「已取消」的订单发起发货)在状态机层直接拒绝——大量线上事故的本质就是非法状态转移没拦住。订单号设计成「时间戳 + 机器位 + 序列号」,趋势递增,对 InnoDB 的页写入友好。
3. 库存:抢购的核心矛盾
超卖和少卖都要命。方案按并发量级递进:
- 数据库乐观锁(起步够用):
UPDATE sku SET stock = stock - #{n} WHERE id = #{id} AND stock >= #{n},影响行数为 0 即抢购失败 - Redis 预扣(量大后):库存预热进 Redis,
DECR原子预扣,异步落库。预扣成功的资格进 MQ,下游慢慢消化创建订单——把同步的「抢」和异步的「办」拆开,是秒杀架构的核心 - 防刷:抢购接口限流 + 验证码拉平瞬时峰值 + 同一用户购买资格去重
4. 分布式事务:认清现实再做选择
下单要扣库存、创建订单、扣优惠券——跨服务的原子性。强一致(XA/2PC)在互联网交易场景基本不用(性能和可用性代价太大),实践中的选择:
- 本地消息表 + MQ:订单库顺手写一条待发送消息,事务保证「订单和消息同生共死」,MQ 投递后下游(库存/优惠券)保证最终一致
- 对账兜底:定时任务比对订单-库存-资金流水,发现不一致自动修复或告警人工介入——最终一致不是「不管了」,是把不一致的发现和修复自动化
5. 支付与回调
- 支付渠道回调必须幂等(回调会重复通知),以渠道流水号去重
- 回调先落「支付流水」再驱动订单状态机,两步之间任何崩溃都能靠对账追平
- 金额一律用分(整数)存储,浮点数碰钱就是事故预备
三、踩过的坑
- 缓存与数据库不一致窗口:先删缓存后更新库,并发读把旧值又填回去了。最终上「延迟双删 + 订阅 binlog(Canal)异步失效」把窗口压到毫秒级【待补充:实际方案】
- MQ 消息积压:大促时订单消息把下游库存服务打挂。教训是消费端必须有自己的限流和降级(积压时先保核心链路),且 MQ 的堆积监控要接到告警上
- 优惠券超发:发券的「先查后插」在并发下重复发放,改成唯一索引兜底(用户+活动维度唯一约束)——数据库约束是并发正确性的最后一道防线
四、成果
| 指标 | 数值 |
|---|---|
| 日订单量 / 峰值 QPS | 【待补充】 |
| 核心链路可用性 | 【待补充】 |
| 秒杀场景承载 | 【待补充】 |
五、收获
电商系统做完,对「一致性」的理解立体了:强一致、最终一致、幂等、对账,不是教科书概念,是每一条具体的 UPDATE 语句和每一个报警规则。
💬 对这个项目的架构细节、技术选型有想法?欢迎加我泡泡(微信)一起探讨技术与科技前沿 → 🍵 加我泡泡