Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

附录B 面试题精选

使用说明

本附录为《电商系统架构设计与实现》的配套面试准备资料,包含 123 道主线精选题 / 案例78 道章节补充题,合计 201 道明确题目,重点关注电商核心系统的架构设计、工程实现和面试答辩。

题型标注

  • 📊 系统设计题: 需要设计架构、数据模型、分析流程
  • 🔧 技术方案题: 需要给出具体技术选型和实现思路
  • 💡 场景分析题: 给定业务场景,分析问题并提出解决方案
  • 🚀 综合案例题: 跨系统的复杂场景,考察全局架构能力

答案结构

每道题的答案包含:

  1. 问题分析:核心挑战是什么
  2. 方案设计:2-3种可选方案
  3. 方案对比:trade-offs分析
  4. 推荐方案:结合实际场景的建议
  5. 延伸思考:相关的深入问题

难度说明

本附录的题目均面向中高级工程师和架构师(3-5年以上经验),重点考察:

  • 对电商业务的深度理解
  • 分布式系统设计能力
  • 技术方案权衡能力
  • 实战经验和坑点认知

如何使用

面试准备:

  • 按章节顺序学习,每题都尝试独立思考后再看答案
  • 重点关注“方案对比“和“推荐方案“部分
  • 结合书中章节深入理解背景知识

自我评估:

  • 完成一章后,挑选相关题目进行测试
  • 对比答案找出思考盲区
  • 关注“延伸思考“拓展知识面

团队讨论:

  • 选择综合案例题进行技术分享
  • 讨论不同方案的适用场景
  • 结合实际项目经验交流

面试官版题库导航

201 道题不应该按顺序逐题使用。真实面试要先判断候选人的层级,再选择不同深度的问题。建议把题库分成四层:

层级用途适合候选人面试目标
必问题判断是否具备电商后端基本盘中高级后端、资深后端看边界、状态、一致性、幂等、缓存和交易安全是否扎实
深挖题判断是否做过真实复杂业务资深后端、技术骨干看是否能讲清失败、补偿、异步任务、运营后台和可观测性
专家题区分架构师和技术负责人架构师、TL、平台负责人看是否能处理多系统边界、长期演进、组织协作和成本权衡
备选题库按项目背景补充追问所有候选人根据简历方向选择搜索、支付、营销、供应商同步、库存等专项

题量总览

部分内容题量
第一部分电商架构基础20
第二部分商品与库存管理43
第三部分交易核心链路50
第四部分综合实战案例10
第五部分章节补充面试题与答辩话术78
合计主线题库 + 章节补充题201

第五部分里的答辩话术、表述模板和技术速查不计入 201 道明确题目。

核心必问题清单

如果只做一轮 60-90 分钟面试,优先从下面这些题里选。它们能快速判断候选人是否真的做过电商核心系统,而不是只背通用分布式八股。

能力域推荐题目重点看什么
系统边界1.1-1 服务拆分、1.1-6 微服务粒度、1.1-9 DDD 应用能否按业务语义拆边界,而不是按表或接口拆服务
一致性1.2-1 订单库存一致性、1.2-3 同步 vs 异步、1.2-6 可靠投递、1.2-8 对账补偿是否理解本地事务、Outbox、幂等、补偿和对账
商品中心2.1-1 SPU/SKU、2.1-3 搜索不一致、2.1-7 上架流程、2.1-9 商品快照是否能区分正式主数据、流程对象、快照和派生读模型
库存系统2.2-0 库存创建、2.2-0B 生命周期联动、2.2-1 超卖、2.2-4 预占释放、2.2-8 并发更新是否能讲清库存事实、预占、账本、券码池和可售投影
计价营销2.3-1 价格引擎、2.3-2 优惠券、2.3-5 秒杀活动、2.3-8 组合促销是否能区分基础价、优惠计算、营销预算和结算价
搜索导购3.1-1 搜索架构、3.1-4 多维过滤、3.1-8 性能优化、5.10 搜索边界追问是否理解 ES 只是派生读模型,交易不能信索引
购物车结算3.2-1 购物车存储、3.2-2 价格计算、3.2-4 库存校验、3.2-9 结算流程是否能把购物车、结算页、计价、库存和订单边界拆清
订单支付3.3-1 状态机、3.3-5 并发创建、3.3-6 Saga、3.4-1 支付架构、3.4-2 回调幂等是否理解订单状态机、支付单、回调幂等、资金安全和补偿
商品供给5.2-1 供给平台边界、5.2-13 Publish 流程、5.2-20 下游一致、5.2-21 生命周期库存联动、5.2-22 库存运营任务是否理解供给控制面、发布版本、库存任务、营销协同和可售闭环
供应商同步5.3-1 长任务设计、5.3-4 Worker 抢占、5.3-9 任务互斥、5.3-15 MySQL DLQ是否理解外部供给治理、Checkpoint、租约、Raw Snapshot 和 DLQ

面试路径建议

60 分钟资深后端面试:

10 分钟:项目背景和候选人真实职责
15 分钟:订单 / 库存 / 支付一致性
15 分钟:商品中心 / 供给平台 / 库存创建边界
10 分钟:缓存、搜索、Outbox 或异步任务深挖
10 分钟:线上故障、监控、补偿和复盘

90 分钟架构师面试:

15 分钟:业务架构与限界上下文
20 分钟:商品、库存、计价、营销、订单的事实归属
20 分钟:发布、上线、可售、Outbox、可售投影
15 分钟:大促、供应商同步、批量任务和 DLQ
10 分钟:成本、演进、组织协作和技术债
10 分钟:候选人反问与方案复盘

商品 / 库存专项面试:

商品中心:2.1-1、2.1-3、2.1-7、2.1-9、5.1-1、5.1-5
库存系统:2.2-0、2.2-0B、2.2-1、2.2-4、2.2-8、2.2-14
供给运营:5.2-1、5.2-13、5.2-20、5.2-21、5.2-22、5.5-20

评分标准

面试官不要只看候选人是否能背出“缓存、MQ、分库分表”。更重要的是看他能否在约束下做边界和取舍。

评分维度合格表现优秀表现
业务边界能说清系统职责能说明事实归属、控制面 / 数据面、读模型 / 写模型
状态建模能画基本状态机能区分流程状态、生命周期状态、交易状态和补偿状态
一致性能说出 MQ、事务、重试能落到 Outbox、幂等键、版本、对账、DLQ 和人工修复
数据模型能给出核心表能解释唯一键、索引、分片、快照、历史版本和审计字段
高并发能讲缓存和限流能讲热点、降级、预热、削峰、隔离队列和读写路径分离
可运营性能说有后台能设计任务进度、错误文件、可售诊断、补偿入口和审计链路
风险意识能识别超卖、重复支付能识别资损、历史订单解释、营销成本、供应商脏数据和人工误操作

常见减分点:

  1. 把商品供给平台讲成商品表 CRUD。
  2. 把库存当成一个 stock 字段,不讲预占、释放、账本和幂等。
  3. 把 Redis、ES、MQ 当权威事实源。
  4. 订单回读最新商品、价格、履约规则解释历史交易。
  5. 发布事务同步调用所有下游,忽略最终一致和补偿。
  6. 只讲技术组件,不讲运营修复、错误文件、DLQ 和审计。

第一部分:电商架构基础(20题)

1.1 系统全景与架构设计(10题)

📊 题目1:如何设计一个中大型电商平台的服务拆分边界?

问题描述: 假设你接手一个日订单50万的电商平台,目前是单体应用,团队规模100人。现在需要进行微服务化改造。请设计服务拆分方案。

答案

问题分析: 服务拆分的核心挑战在于:

  1. 既要保证领域边界清晰(DDD视角)
  2. 又要考虑团队规模和协作效率
  3. 还要兼顾性能和一致性要求
  4. 需要平衡拆分粒度和运维复杂度

方案一:按业务能力垂直拆分

核心服务划分:

  • 核心交易域:订单、支付、结算、购物车(4个服务)
  • 商品供给域:商品中心、库存、计价、营销(4个服务)
  • 用户导购域:搜索、推荐、用户中心(3个服务)
  • 运营支撑域:商品上架、B端运营、数据分析(3个服务)

拆分原则:

  1. 每个服务对应一个限界上下文(Bounded Context)
  2. 服务间通过稳定的API契约通信
  3. 核心链路服务优先拆分,支撑域可暂时保留

优点:

  • 团队自治性强,可并行开发
  • 业务边界清晰,易于理解和维护
  • 符合DDD最佳实践
  • 故障隔离效果好

缺点:

  • 需要处理分布式事务
  • 服务间调用增加网络开销
  • 初期实施复杂度较高
  • 需要完善的基础设施支持

方案二:按技术特征水平拆分

划分:

  • 高并发读服务:商品详情、搜索、列表(使用缓存和ES)
  • 强一致写服务:订单、支付、库存扣减(使用分布式事务)
  • 异步处理服务:消息通知、数据同步、报表生成

优点:

  • 技术栈统一,便于基础设施复用
  • 性能优化方向明确
  • 团队技能要求更聚焦

缺点:

  • 业务边界模糊,团队协作困难
  • 不符合微服务自治原则
  • 业务变更可能需要跨多个服务修改

方案三:绞杀者模式渐进拆分

实施步骤:

  1. 第一阶段:拆出搜索(读多写少,影响面小)
  2. 第二阶段:拆商品中心(依赖少,数据模型清晰)
  3. 第三阶段:拆订单链路(核心但复杂,需要Saga编排)
  4. 第四阶段:处理遗留单体(逐步清理剩余功能)

优点:

  • 风险可控,可持续交付
  • 团队学习曲线平缓
  • 每个阶段都有明确产出

缺点:

  • 迁移周期较长(可能需要6-12个月)
  • 中间状态维护成本高
  • 需要维护双写逻辑

方案对比

维度方案一(垂直)方案二(水平)方案三(渐进)
团队自治★★★★★★★☆☆☆★★★☆☆
技术复杂度★★★☆☆★★★★☆★★☆☆☆
交付速度★★★☆☆★★★★☆★★★★☆
长期维护★★★★★★★☆☆☆★★★★☆

推荐方案: 采用方案一+方案三的结合:按领域垂直划分目标架构,采用绞杀者模式渐进实施。

实施要点:

  1. 前期准备:梳理系统依赖图,识别核心路径和边界
  2. 基础设施先行:建立服务网格、监控、链路追踪、配置中心
  3. 服务契约规范:定义API标准、版本管理、错误码体系
  4. 数据迁移策略:双写+对账+延迟删除
  5. 灰度发布机制:按用户维度或地域逐步切流量

延伸思考

  1. 如何处理拆分过程中的数据迁移?
  2. 分布式事务如何保证?
  3. 服务间调用的超时和重试策略如何设计?

🔧 题目2:如何设计电商系统的数据一致性方案?

问题描述: 电商系统中,订单创建涉及库存扣减、优惠券核销、积分扣除等多个操作,这些操作分散在不同的微服务中。如何保证数据一致性?

答案

问题分析: 数据一致性的核心挑战:

  1. 多个微服务涉及写操作,无法使用传统数据库事务
  2. 部分操作可能失败,需要补偿机制
  3. 性能要求高,不能因为一致性牺牲太多性能
  4. 需要考虑系统可用性,不能因为一个服务故障导致整体不可用

方案一:Saga模式(编排式)

设计思路: 由订单服务作为编排器(Orchestrator),协调各个服务的操作。

流程:

  1. 订单服务:创建订单(状态:PENDING)
  2. 调用库存服务:预占库存(成功继续,失败取消订单)
  3. 调用营销服务:锁定优惠券(成功继续,失败释放库存)
  4. 调用积分服务:扣除积分(成功继续,失败释放库存+券)
  5. 更新订单状态:CONFIRMED

优点:

  • 流程清晰,易于理解和调试
  • 中心化控制,便于监控和排查问题
  • 补偿逻辑集中管理

缺点:

  • 订单服务成为单点,压力较大
  • 流程变更需要修改编排器
  • 服务间耦合度较高

方案二:Saga模式(事件编排式)

设计思路: 通过事件总线(Kafka)进行编排,各服务监听事件并发布新事件。

流程:

  1. 订单服务:创建订单 → 发布 OrderCreated 事件
  2. 库存服务:监听 OrderCreated → 预占库存 → 发布 InventoryReserved 事件
  3. 营销服务:监听 InventoryReserved → 锁定优惠券 → 发布 CouponLocked 事件
  4. 积分服务:监听 CouponLocked → 扣除积分 → 发布 PointsDeducted 事件
  5. 订单服务:监听 PointsDeducted → 更新订单状态为 CONFIRMED

优点:

  • 服务解耦,各服务独立演进
  • 无中心化瓶颈,扩展性好
  • 天然支持异步,性能更好

缺点:

  • 流程分散,难以全局把控
  • 调试困难,需要完善的链路追踪
  • 事件顺序和幂等性要求高

方案三:TCC(Try-Confirm-Cancel)

设计思路: 分为三个阶段:Try(预留)、Confirm(确认)、Cancel(取消)。

流程:

  • Try阶段:预留资源(库存预占、券锁定、积分冻结)
  • Confirm阶段:确认扣减(库存确认、券核销、积分扣除)
  • Cancel阶段:取消操作(释放库存、释放券、解冻积分)

优点:

  • 强一致性,业务语义清晰
  • 资源锁定明确,不会出现超卖
  • 适合对一致性要求极高的场景

缺点:

  • 实现复杂,需要每个服务提供三个接口
  • 性能开销大(锁定资源时间长)
  • Try阶段占用资源,影响并发度

方案对比

维度Saga-编排Saga-事件TCC
实现复杂度★★★☆☆★★★★☆★★★★★
性能★★★☆☆★★★★☆★★☆☆☆
一致性强度最终一致最终一致强一致
可观测性★★★★☆★★★☆☆★★★★☆

推荐方案: 对于电商系统,推荐Saga-编排模式作为主方案,辅以事件通知。

实施要点:

  1. 订单服务作为编排器,维护状态机
  2. 每个操作携带唯一业务ID保证幂等性
  3. 每个步骤设置合理超时(如库存3s,优惠券2s)
  4. 维护补偿表记录需要补偿的操作
  5. 每个步骤记录日志,包含traceId

延伸思考

  1. 如何处理补偿失败的情况?
  2. 最终一致性的“最终“是多久?
  3. 如何进行一致性验证和对账?

💡 题目3:大促期间如何保证系统稳定性?

问题描述: 公司准备参加双11大促,预估流量是平时的50倍,订单量达到平时的100倍。你需要确保系统在大促期间稳定运行,请设计保障方案。

答案

问题分析: 大促稳定性的核心挑战:

  1. 流量突增:如何应对峰值流量(平时5000 QPS → 25万 QPS)
  2. 资源瓶颈:数据库、缓存、网络等基础设施能否支撑
  3. 热点问题:少量商品承载大部分流量
  4. 故障隔离:如何避免局部故障扩散为全局故障

方案一:垂直扩容+全链路压测

核心思路: 通过提升单机性能和压测验证来保障稳定性。

技术方案:

  1. 数据库层:升级配置(32核128G → 64核256G),增加只读从库
  2. 应用层:服务器扩容(50台 → 200台),JVM调优
  3. 缓存层:Redis扩容(3节点 → 12节点),缓存预热
  4. 压测验证:提前1个月进行全链路压测,发现瓶颈

优点:

  • 改动小,风险可控
  • 实施周期短
  • 回退方便

缺点:

  • 成本高(需要高配机器)
  • 扩展性有限
  • 单点风险依然存在

方案二:限流降级+分级保障

核心思路: 通过限流降级保护核心链路,非核心功能可降级。

技术方案:

  1. 多级限流

    • 网关层:总QPS限流(25万)
    • 服务层:单服务限流(如订单创建10万)
    • 接口层:单接口限流(如查询详情5万)
  2. 分级降级

    • P0核心:下单、支付、查询订单(必保)
    • P1重要:搜索、详情、加购(降级返回缓存)
    • P2一般:推荐、评论、收藏(直接关闭)
  3. 熔断机制:连续失败达阈值后自动熔断

优点:

  • 保护核心链路
  • 成本可控
  • 局部故障不扩散

缺点:

  • 用户体验下降(部分功能不可用)
  • 降级逻辑需要提前准备
  • 限流阈值难以精确设定

方案三:异步化+削峰填谷

核心思路: 将同步操作改为异步,通过消息队列削峰填谷。

技术方案:

  1. 订单异步化:下单成功后立即返回,后台异步处理
  2. 库存预占:Redis预扣,后台异步同步到DB
  3. 消息队列:Kafka承载峰值流量,消费者慢慢处理
  4. 任务调度:非实时任务延迟处理(如数据统计)

优点:

  • 峰值流量平滑处理
  • 用户体验好(快速响应)
  • 系统压力平缓

缺点:

  • 架构改造较大
  • 数据最终一致性
  • 异常处理复杂

方案对比

维度垂直扩容限流降级异步化
成本★★☆☆☆★★★★☆★★★☆☆
用户体验★★★★★★★★☆☆★★★★☆
实施难度★★★★☆★★★☆☆★★☆☆☆
扩展性★★☆☆☆★★★☆☆★★★★★

推荐方案: 采用三种方案的组合:垂直扩容作为基础,限流降级作为保护,异步化作为优化。

实施要点:

  1. 提前3个月准备:容量规划、全链路压测、应急演练
  2. 分级保障策略:明确P0/P1/P2功能,准备降级开关
  3. 实时监控:QPS、成功率、响应时间、错误率
  4. 应急预案:数据库主从切换、缓存雪崩处理、快速扩容
  5. 值班机制:7×24值守,关键节点实时响应

延伸思考

  1. 如何评估系统需要的容量?
  2. 大促期间出现故障如何应急?
  3. 如何验证限流降级策略是否有效?

📊 题目4:设计电商系统的多机房多活架构

问题描述: 电商平台需要支持异地多活,要求在任一机房故障时,业务可以快速切换到其他机房继续提供服务。请设计多机房多活方案。

答案

问题分析: 多机房多活的核心挑战:

  1. 数据一致性:跨机房数据同步延迟和一致性保证
  2. 流量路由:如何将用户请求路由到合适的机房
  3. 故障切换:机房故障时如何快速切换
  4. 成本控制:多机房部署成本是单机房的2-3倍

方案一:单元化架构

核心思路: 按用户维度进行分片,每个单元独立提供服务。

设计:

  • 单元划分:按用户ID hash分为N个单元(如8个)
  • 单元部署:每个单元在2-3个机房部署
  • 路由规则:网关根据用户ID路由到对应单元
  • 数据存储:每个单元独立数据库,跨单元数据通过消息同步

优点:

  • 单元内强一致性
  • 扩展性好,可按单元扩容
  • 故障隔离(单元故障不影响其他单元)

缺点:

  • 跨单元数据访问困难
  • 全局数据(如商品、库存)需要特殊处理
  • 单元分配不均可能导致热点

方案二:两地三中心

核心思路: 在同城部署两个机房(主备),异地部署一个灾备机房。

设计:

  • 同城双活:机房A和机房B互为主备,承载线上流量
  • 异地灾备:机房C作为灾备,平时不承载流量
  • 数据同步:机房A、B实时同步,机房C异步同步
  • 流量分配:机房A和B各承载50%流量

优点:

  • 同城延迟低(<1ms)
  • 异地容灾(城市级灾难)
  • 实施相对简单

缺点:

  • 机房C资源闲置
  • 跨城切换仍有数据丢失风险
  • 仅能容忍单机房故障

方案三:三地五中心

核心思路: 在三个城市各部署机房,每个城市至少2个机房,实现真正的多活。

设计:

  • 北京:机房A、机房B(承载华北流量)
  • 上海:机房C、机房D(承载华东流量)
  • 深圳:机房E(承载华南流量)
  • 数据同步:同城实时同步,跨城异步同步
  • 流量路由:根据用户地域就近路由

优点:

  • 真正的异地多活
  • 就近访问,延迟低
  • 可容忍城市级故障

缺点:

  • 成本极高(5个机房)
  • 数据一致性复杂(跨城同步)
  • 运维复杂度高

方案对比

维度单元化两地三中心三地五中心
数据一致性★★★★☆★★★★☆★★★☆☆
成本★★★☆☆★★★★☆★☆☆☆☆
容灾能力★★★☆☆★★★★☆★★★★★
实施难度★★★★☆★★★☆☆★★☆☆☆

推荐方案: 对于中大型电商,推荐两地三中心+单元化的组合方案。

实施要点:

  1. 单元划分:按用户ID分8个单元,每个单元在2个机房部署
  2. 同城双活:北京A、B机房互为主备
  3. 异地灾备:上海C机房作为灾备
  4. 路由策略:用户→单元→机房的三级路由
  5. 数据同步:同城强同步(Raft/Paxos),异地异步同步
  6. 故障切换:自动检测+自动切换,RTO<5分钟

延伸思考

  1. 如何处理跨单元的全局数据(如商品库存)?
  2. 机房故障时如何保证不丢数据?
  3. 如何验证多活架构的有效性?

🔧 题目5:如何设计服务间的调用链路追踪?

问题描述: 微服务架构下,一个用户请求可能经过十几个服务。当出现问题时,如何快速定位是哪个服务出了问题?请设计分布式追踪方案。

答案

问题分析: 链路追踪的核心挑战:

  1. 如何关联一次请求涉及的所有服务调用
  2. 如何记录调用链路的详细信息(耗时、参数、结果)
  3. 如何在性能开销和可观测性之间平衡
  4. 如何快速检索和分析海量追踪数据

方案一:自研追踪系统

核心思路: 基于唯一TraceID串联整个调用链,每个服务记录SpanID。

设计:

  • TraceID:全局唯一ID,标识一次完整请求
  • SpanID:服务内部的调用单元ID
  • 传递机制:通过HTTP Header或RPC Context传递
  • 数据收集:每个服务将Span数据异步上报
  • 存储分析:存储到Elasticsearch,Kibana可视化

实现步骤:

  1. 网关生成TraceID
  2. 服务间传递TraceID和ParentSpanID
  3. 每个服务记录:服务名、方法名、开始时间、结束时间、状态
  4. 异步上报到追踪系统

优点:

  • 完全可控,可定制
  • 无外部依赖
  • 数据私密性好

缺点:

  • 开发成本高
  • 需要所有服务埋点
  • 维护成本高

方案二:使用开源APM(如Skywalking)

核心思路: 使用Java Agent无侵入式采集调用链数据。

设计:

  • Agent方式:通过JavaAgent字节码增强自动埋点
  • OAP Server:接收、分析、存储追踪数据
  • UI:可视化展示调用链拓扑、性能指标
  • 告警:支持性能阈值告警

优点:

  • 无侵入,不需要改代码
  • 功能完善(拓扑、指标、告警)
  • 社区活跃,文档丰富
  • 支持多种框架(Spring、Dubbo、gRPC)

缺点:

  • Agent有一定性能开销
  • 不支持非Java语言
  • 定制化能力有限

方案三:使用云厂商APM(如阿里云ARMS)

核心思路: 使用云厂商提供的APM服务,开箱即用。

设计:

  • SDK集成:引入SDK,自动上报追踪数据
  • 云端分析:云厂商负责数据存储和分析
  • 控制台:提供丰富的可视化和分析能力
  • AI诊断:智能分析性能瓶颈

优点:

  • 开箱即用,实施快
  • 功能强大(AI诊断、实时监控)
  • 无需自建基础设施
  • 技术支持好

缺点:

  • 成本高(按量付费)
  • 数据外传有安全风险
  • 被云厂商锁定

方案对比

维度自研Skywalking云APM
实施成本★★☆☆☆★★★★☆★★★★★
功能丰富度★★★☆☆★★★★☆★★★★★
定制能力★★★★★★★★☆☆★★☆☆☆
运维成本★★☆☆☆★★★☆☆★★★★★

推荐方案: 根据团队规模选择:

  • 大团队(100+人):自研追踪系统,可定制化
  • 中等团队(20-100人):使用Skywalking,开源免费
  • 小团队(<20人):使用云APM,开箱即用

实施要点:

  1. 采样策略:不是所有请求都追踪,按比例采样(如1%)
  2. 性能优化:异步上报,避免阻塞主流程
  3. 标准化:定义统一的TraceID、SpanID规范
  4. 关键节点:重点追踪慢查询、外部调用、错误日志
  5. 告警配置:P99延迟、错误率等关键指标告警

延伸思考

  1. 如何在不影响性能的前提下采集足够的追踪数据?
  2. 如何处理跨语言服务的追踪?
  3. 追踪数据如何与日志、指标关联?

💡 题目6:如何平衡微服务拆分的粒度?

问题描述: 在进行微服务拆分时,服务拆得太粗会失去微服务的优势,拆得太细会导致运维复杂度爆炸。如何平衡服务拆分的粒度?

答案

问题分析: 服务粒度的核心挑战:

  1. 服务太粗:失去独立部署、技术异构的优势
  2. 服务太细:服务数量多,运维成本高,调用链路长
  3. 边界不清:职责重叠,数据冗余
  4. 团队匹配:服务划分要与团队结构匹配

方案一:按领域模型拆分(DDD)

核心思路: 基于领域驱动设计(DDD)的限界上下文划分服务。

判断标准:

  1. 业务完整性:一个聚合根对应一个服务
  2. 独立演进:服务内部变化不影响其他服务
  3. 团队自治:一个团队(5-9人)负责一个服务
  4. 数据自治:服务拥有独立的数据库

示例:

  • 订单服务:负责订单生命周期管理
  • 库存服务:负责库存预占、扣减、释放
  • 支付服务:负责支付路由、状态管理、对账

优点:

  • 业务边界清晰
  • 符合康威定律
  • 易于理解和维护

缺点:

  • 需要团队理解DDD
  • 前期建模成本高
  • 对业务专家依赖大

方案二:按变化频率拆分

核心思路: 将变化频繁的功能和稳定的功能拆开。

判断标准:

  1. 变化频率:营销活动(周级)vs 用户中心(月级)
  2. 技术栈:搜索(ES)vs 订单(MySQL)
  3. 性能要求:详情页(缓存)vs 下单(强一致)

示例:

  • 营销服务:促销规则频繁变化,独立拆分
  • 计价服务:计算逻辑复杂但相对稳定
  • 用户服务:用户信息变化少,可合并

优点:

  • 快速响应业务变化
  • 技术选型灵活
  • 易于优化性能

缺点:

  • 可能打破业务边界
  • 数据一致性复杂
  • 难以预测变化频率

方案三:按团队规模拆分(两个披萨原则)

核心思路: 一个团队负责的服务数量,要让团队可以“用两个披萨喂饱“(5-9人)。

判断标准:

  1. 团队规模:一个5-9人团队负责2-3个服务
  2. 认知负载:团队能理解和维护的复杂度
  3. 沟通成本:减少跨团队协作

示例:

  • 订单团队:订单服务 + 售后服务 + 物流编排服务
  • 商品团队:商品服务 + 类目服务 + 品牌服务
  • 库存团队:库存服务 + 仓储服务

优点:

  • 团队职责清晰
  • 减少沟通成本
  • 符合组织架构

缺点:

  • 服务粒度可能不均衡
  • 团队变化时需要调整
  • 可能打破业务边界

方案对比

维度DDD拆分变化频率团队规模
业务清晰度★★★★★★★★☆☆★★★☆☆
响应速度★★★☆☆★★★★★★★★★☆
实施难度★★☆☆☆★★★★☆★★★★★
长期维护★★★★★★★★☆☆★★★★☆

推荐方案: 采用DDD为主,兼顾变化频率和团队规模的混合策略。

实施要点:

  1. 核心域优先DDD:订单、支付等核心域严格按DDD拆分
  2. 支撑域灵活拆分:搜索、推荐等可按技术特征拆分
  3. 团队匹配:确保每个团队能hold住负责的服务
  4. 逐步演进:初期粗粒度,随业务发展逐步拆细
  5. 防止过度拆分:服务数量控制在团队规模的2-3倍

判断粒度是否合适的指标:

  • 服务代码量:1-3万行最佳
  • 团队负担:一个团队2-3个服务
  • 调用链路:核心链路不超过5跳
  • 部署频率:每周至少部署1次
  • 故障恢复:单服务故障不影响全局

延伸思考

  1. 如何判断服务拆得太细了?
  2. 已有服务如何合并?
  3. 服务拆分后如何保证数据一致性?

📊 题目7:电商系统的技术债治理策略

问题描述: 随着业务快速发展,系统积累了大量技术债(如代码重复、过度耦合、缺少测试)。如何在保证业务持续交付的前提下,有效治理技术债?

答案

问题分析: 技术债治理的核心挑战:

  1. 业务压力大,没有时间重构
  2. 技术债范围广,不知从何下手
  3. ROI不明确,难以说服业务
  4. 改造风险高,担心引入新问题

方案一:停止新功能,集中还债

核心思路: 暂停新功能开发1-2个月,团队集中精力重构优化。

实施步骤:

  1. 评估技术债清单(代码质量、架构问题、性能问题)
  2. 按影响面和风险排优先级
  3. 集中2个月时间重构
  4. 重构完成后恢复业务开发

优点:

  • 集中资源,效率高
  • 可以做系统性重构
  • 团队专注度高

缺点:

  • 业务方难以接受(2个月不交付)
  • 改造风险集中爆发
  • 团队压力大

方案二:绞杀式重构,逐步替换

核心思路: 不停止业务开发,在交付新功能时逐步重构。

实施策略:

  1. 新功能新写法:新功能用新架构实现
  2. 改功能顺便重构:修改老功能时顺便重构
  3. 热点优先:优先重构变化频繁的模块
  4. 设置重构配额:每个迭代20%时间用于重构

示例:

  • 迭代1:新功能A(新架构) + 重构模块X
  • 迭代2:新功能B(新架构) + 重构模块Y
  • 迭代3:新功能C(新架构) + 重构模块Z

优点:

  • 业务持续交付
  • 风险分散,可控
  • 团队适应性好

缺点:

  • 周期长(可能需要6-12个月)
  • 需要团队自律
  • 新老代码共存,维护成本高

方案三:分层治理,重点突破

核心思路: 识别高价值技术债,集中资源重点治理。

分层策略:

  1. P0紧急债:影响线上稳定性(立即处理)
    • 例:核心接口性能问题、安全漏洞
  2. P1重要债:影响开发效率(1个月内处理)
    • 例:核心模块耦合严重、缺少测试
  3. P2普通债:代码质量问题(逐步优化)
    • 例:代码重复、命名不规范
  4. P3可忽略:历史遗留问题(不处理)
    • 例:废弃功能的代码

实施步骤:

  1. 扫描技术债(代码扫描工具 + 人工评估)
  2. 分级打标(P0/P1/P2/P3)
  3. P0立即处理,P1排入迭代,P2见缝插针
  4. 每月review技术债清单

优点:

  • 重点突出,ROI高
  • 灵活可控
  • 容易说服业务

缺点:

  • 需要持续跟进
  • 分级标准难以统一
  • P2/P3的债永远还不完

方案对比

维度集中还债绞杀重构分层治理
业务影响★★☆☆☆★★★★★★★★★☆
治理效率★★★★★★★★☆☆★★★★☆
风险控制★★☆☆☆★★★★☆★★★★★
实施难度★★★☆☆★★★★☆★★★★☆

推荐方案: 采用分层治理为主,绞杀重构为辅的组合策略。

实施要点:

  1. 建立技术债看板:可视化展示技术债清单和进度
  2. 设置重构配额:每个迭代15-20%时间用于技术债
  3. 重构优先级:P0立即处理,P1必须进迭代,P2机动
  4. 度量指标:代码覆盖率、圈复杂度、重复率、Bug密度
  5. 自动化检测:SonarQube扫描,PR卡点
  6. 技术债评审会:每月评审技术债治理进展

关键原则:

  • 不积累新债:新代码必须符合质量标准
  • 热点优先:优先重构变化频繁的模块
  • 小步快跑:每次重构范围可控,及时验证
  • 有始有终:重构必须完整,不能半途而废
  • 文档同步:重构后及时更新架构文档

延伸思考

  1. 如何评估技术债的严重程度和优先级?
  2. 重构过程中如何保证不引入新问题?
  3. 如何说服业务投入资源治理技术债?

🔧 题目8:如何设计配置中心?

问题描述: 微服务架构下,配置分散在各个服务中,修改配置需要重启服务。请设计一个配置中心,支持配置的集中管理和动态更新。

答案

问题分析: 配置中心的核心挑战:

  1. 配置变更如何实时推送到服务
  2. 如何保证配置的一致性和可靠性
  3. 如何支持灰度发布和快速回滚
  4. 如何保证配置的安全性(敏感信息加密)

方案一:基于文件+定时拉取

核心思路: 配置存储在Git,服务定时拉取配置文件。

设计:

  • 存储:配置文件存储在Git仓库
  • 拉取:服务每隔30秒拉取一次配置
  • 生效:检测到配置变更,重新加载
  • 版本:Git commit作为配置版本

优点:

  • 实现简单
  • 天然的版本管理
  • 可以code review配置

缺点:

  • 实时性差(最多30秒延迟)
  • Git作为配置中心不太合适
  • 无法精准推送

方案二:基于数据库+长轮询

核心思路: 配置存储在数据库,服务通过长轮询获取配置更新。

设计:

  • 存储:配置存储在MySQL,包含版本号
  • 推送:服务发起长轮询请求(超时60秒)
  • 变更检测:配置中心检测到配置变更,立即返回
  • 生效:服务收到变更通知,重新加载配置

优点:

  • 实时性好(秒级)
  • 实现相对简单
  • 支持灰度发布

缺点:

  • 长连接占用资源
  • 数据库压力大(大量服务轮询)
  • 扩展性有限

方案三:基于注册中心+Watch机制

核心思路: 配置存储在注册中心(如Nacos、Apollo),通过Watch机制推送。

设计:

  • 存储:配置存储在Nacos
  • Watch:服务订阅配置,Nacos主动推送变更
  • 命名空间:按环境(dev/test/prod)隔离
  • 灰度发布:支持按IP、比例灰度
  • 权限控制:RBAC权限管理

实现:

1. 服务启动时注册到Nacos
2. 订阅所需的配置(dataId + group)
3. Nacos检测到配置变更,通过长连接推送
4. 服务收到通知,触发配置刷新回调

优点:

  • 实时性强(毫秒级)
  • 功能完善(灰度、回滚、审计)
  • 高可用(Nacos集群)
  • 开箱即用

缺点:

  • 依赖第三方组件
  • 学习成本
  • 运维复杂度

方案对比

维度文件+拉取数据库+长轮询注册中心+Watch
实时性★★☆☆☆★★★★☆★★★★★
可靠性★★★☆☆★★★☆☆★★★★★
扩展性★★★☆☆★★☆☆☆★★★★★
实施成本★★★★★★★★★☆★★★☆☆

推荐方案: 对于中大型系统,推荐使用Nacos作为配置中心

实施要点:

  1. 配置分层

    • 环境配置(dev/test/prod)
    • 公共配置(数据库连接池、日志级别)
    • 应用配置(业务参数)
    • 敏感配置(密码、密钥)加密存储
  2. 命名规范

    • dataId:${应用名}-${环境}.${格式}
    • group:${业务域}
    • 例:order-service-prod.yaml,group:transaction
  3. 配置刷新

    • 使用 @RefreshScope 注解
    • 或实现 ConfigChangeListener 接口
    • 敏感配置(数据库连接)不支持热更新
  4. 灰度发布

    • 先在灰度环境验证
    • 按IP或比例逐步推送
    • 监控关键指标,出问题立即回滚
  5. 安全控制

    • 敏感配置加密存储(AES/RSA)
    • RBAC权限管理(谁能改什么配置)
    • 审计日志(谁在什么时候改了什么)
    • 配置变更必须经过审批流程
  6. 高可用

    • Nacos集群部署(至少3节点)
    • 客户端本地缓存(Nacos不可用时降级)
    • 配置备份(定期导出到Git)

延伸思考

  1. 配置中心本身如何保证高可用?
  2. 敏感配置如何加密存储?
  3. 如何保证配置变更的安全性(防止误操作)?

💡 题目9:领域驱动设计在电商系统中的应用

问题描述: 你需要用DDD的思想设计电商订单系统。请描述如何识别聚合根、实体、值对象,以及如何划分限界上下文。

答案

问题分析: DDD在电商中的核心挑战:

  1. 如何识别领域边界(限界上下文)
  2. 如何设计聚合根和实体关系
  3. 如何处理跨聚合的数据一致性
  4. 如何平衡领域纯粹性和工程实践

方案一:贫血模型+服务层

核心思路: 实体只包含数据,业务逻辑在服务层。

设计:

实体:
- Order(订单):id、userId、items、status、amount
- OrderItem(订单项):productId、quantity、price

服务层:
- OrderService.createOrder()
- OrderService.pay()
- OrderService.cancel()

优点:

  • 简单直观,易于理解
  • 数据库映射方便
  • 团队容易上手

缺点:

  • 不是真正的DDD(贫血模型)
  • 业务逻辑分散在服务层
  • 领域知识不内聚

方案二:充血模型+聚合根

核心思路: 实体包含数据和行为,聚合根负责维护聚合内的一致性。

设计:

聚合根:Order
- 领域对象:
  - Order(聚合根):id、userId、items、status、amount
  - OrderItem(实体):productId、quantity、price
  - Address(值对象):province、city、street
  
- 领域行为:
  - Order.create():创建订单
  - Order.pay():支付订单
  - Order.cancel():取消订单
  - Order.addItem():添加商品
  
- 不变量:
  - 订单金额 = sum(items.price * items.quantity)
  - 订单只能从PENDING状态支付

优点:

  • 业务逻辑内聚在领域对象中
  • 符合DDD思想
  • 易于测试(单元测试聚合根)

缺点:

  • 学习曲线陡峭
  • ORM映射复杂
  • 团队需要理解DDD

方案三:CQRS+事件溯源

核心思路: 读写分离,写入用事件溯源,读取用专门的查询模型。

设计:

写模型(Command):
- 命令:CreateOrderCommand、PayOrderCommand
- 聚合根:Order(通过重放事件恢复状态)
- 事件:OrderCreated、OrderPaid、OrderCancelled

读模型(Query):
- 查询模型:OrderView(专门优化查询)
- 投影:监听事件,更新查询模型

优点:

  • 读写分离,性能优化
  • 完整的审计日志(事件)
  • 易于扩展(新增读模型)

缺点:

  • 架构复杂度高
  • 事件版本管理困难
  • 最终一致性

方案对比

维度贫血模型充血模型CQRS+ES
实施难度★★★★★★★★☆☆★☆☆☆☆
DDD纯粹性★★☆☆☆★★★★☆★★★★★
性能★★★☆☆★★★☆☆★★★★★
适用场景简单CRUD复杂业务高性能+审计

推荐方案: 对于电商订单系统,推荐充血模型+聚合根

实施要点:

  1. 识别限界上下文

    • 订单上下文:订单生命周期、订单状态管理
    • 库存上下文:库存预占、扣减、释放
    • 支付上下文:支付路由、支付状态、对账
    • 物流上下文:物流单、轨迹跟踪
  2. 设计聚合根

    • Order(订单聚合根)
      • 实体:OrderItem
      • 值对象:Address、Money
      • 行为:create、pay、ship、cancel
      • 不变量:订单金额一致性、状态流转规则
  3. 跨聚合协作

    • 强一致性:同一聚合内(订单和订单项)
    • 最终一致性:跨聚合(订单和库存)
    • 集成方式:领域事件 + Saga编排
  4. 代码组织

order/
├── domain/           # 领域层
│   ├── Order.java    # 聚合根
│   ├── OrderItem.java # 实体
│   ├── Address.java   # 值对象
│   └── OrderStatus.java # 枚举
├── application/      # 应用层
│   └── OrderService.java # 应用服务(编排)
├── infrastructure/   # 基础设施层
│   ├── OrderRepository.java # 仓储接口
│   └── OrderRepositoryImpl.java # 仓储实现
└── api/             # 接口层
    └── OrderController.java # REST API
  1. 关键设计决策
    • 聚合边界:Order包含OrderItem,不包含Product(属于商品上下文)
    • ID生成:聚合根负责生成ID(UUID或雪花ID)
    • 领域事件:订单状态变更发布事件(OrderPaid、OrderShipped)
    • 仓储模式:通过Repository加载/保存聚合根
    • 工厂模式:复杂的聚合创建用工厂

延伸思考

  1. 如何处理跨聚合根的事务(如订单和库存)?
  2. 充血模型如何映射到数据库(JPA/MyBatis)?
  3. DDD在微服务中如何落地(一个聚合根一个服务?)?

📊 题目10:设计电商系统的监控告警体系

问题描述: 电商系统涉及十几个微服务,需要建立完善的监控告警体系。请设计监控方案,确保能及时发现和处理线上问题。

答案

问题分析: 监控告警的核心挑战:

  1. 监控什么:需要监控哪些指标
  2. 如何监控:使用什么工具和技术
  3. 告警策略:如何避免告警疲劳
  4. 快速定位:如何从告警快速定位问题

方案一:基础监控(单维度)

核心思路: 监控基础指标(CPU、内存、磁盘),发现异常告警。

监控内容:

  • 主机监控:CPU、内存、磁盘、网络
  • 应用监控:JVM(堆内存、GC)
  • 接口监控:QPS、响应时间、错误率
  • 日志监控:ERROR日志、异常堆栈

工具选择:

  • 指标采集:Prometheus
  • 可视化:Grafana
  • 告警:Prometheus Alertmanager

优点:

  • 覆盖基础指标
  • 开源免费
  • 社区活跃

缺点:

  • 单维度监控,难以关联分析
  • 告警规则简单(阈值告警)
  • 缺少业务视角

方案二:全链路监控(多维度)

核心思路: 结合指标、日志、链路追踪三个维度,全方位监控。

监控体系:

  1. 指标监控(Metrics):

    • RED指标:Rate(QPS)、Error(错误率)、Duration(延迟)
    • USE指标:Utilization(使用率)、Saturation(饱和度)、Error(错误)
    • 业务指标:订单量、支付成功率、库存水位
  2. 日志监控(Logging):

    • 结构化日志:JSON格式,包含traceId
    • 日志聚合:ELK(Elasticsearch + Logstash + Kibana)
    • 日志告警:关键错误日志触发告警
  3. 链路追踪(Tracing):

    • APM工具:Skywalking / Jaeger
    • 调用链可视化:拓扑图、火焰图
    • 性能分析:慢查询、慢接口
  4. 关联分析

    • traceId串联三个维度
    • 从告警跳转到日志和链路
    • 快速定位问题根因

优点:

  • 立体化监控,全面覆盖
  • 可快速定位问题
  • 支持根因分析

缺点:

  • 成本高(存储、计算)
  • 运维复杂度高
  • 需要统一traceId

方案三:智能监控(AIOps)

核心思路: 基于机器学习,智能检测异常和预测故障。

核心能力:

  1. 异常检测

    • 基线学习:学习历史数据建立基线
    • 异常识别:偏离基线自动告警
    • 减少误报:智能过滤噪音
  2. 根因分析

    • 故障关联:自动分析告警之间的关联
    • 根因定位:从众多告警中识别根因
    • 建议修复:推荐修复方案
  3. 容量预测

    • 趋势分析:分析资源使用趋势
    • 容量预警:提前预警资源不足
    • 扩容建议:推荐扩容方案

优点:

  • 智能化,减少人工成本
  • 预测性,提前发现问题
  • 自动化根因分析

缺点:

  • 成本高(算法研发、算力)
  • 需要大量历史数据
  • 准确率受限

方案对比

维度基础监控全链路监控智能监控
实施成本★★★★★★★★☆☆★☆☆☆☆
覆盖全面性★★☆☆☆★★★★★★★★★★
定位效率★★☆☆☆★★★★☆★★★★★
适用规模小型中大型大型

推荐方案: 对于中大型电商系统,推荐全链路监控

实施要点:

  1. 指标体系设计

    • 黄金指标(核心业务):

      • 订单创建成功率 > 99.9%
      • 支付成功率 > 99.95%
      • 接口P99延迟 < 500ms
    • 基础指标(资源):

      • CPU使用率 < 70%
      • 内存使用率 < 80%
      • 磁盘使用率 < 85%
    • 业务指标(自定义):

      • 实时订单量
      • 库存水位
      • 支付渠道成功率
  2. 告警分级

    • P0(紧急):影响核心功能(下单、支付失败),5分钟响应
    • P1(重要):影响部分功能(搜索慢、详情页错误),30分钟响应
    • P2(一般):资源告警(CPU高、磁盘满),2小时响应
    • P3(提示):趋势告警(流量增长、容量预警),工作时间处理
  3. 告警策略

    • 避免告警疲劳:合并同类告警、设置静默期
    • 智能降噪:工作时间和非工作时间不同阈值
    • 分级通知:P0电话+短信,P1钉钉,P2邮件
    • 告警收敛:同一问题5分钟内只告警一次
  4. 监控大盘

    • 业务大盘:实时订单量、支付成功率、GMV
    • 应用大盘:服务健康度、QPS、错误率、P99延迟
    • 基础设施大盘:主机、数据库、缓存、消息队列
    • 告警大盘:实时告警、告警趋势、MTTR
  5. 应急响应

    • SOP(标准操作流程):不同告警对应的处理步骤
    • On-call轮值:7×24值班,保证及时响应
    • 故障复盘:每次P0/P1故障必须复盘,沉淀经验
    • 演练机制:定期故障演练,验证应急预案

延伸思考

  1. 如何设计监控指标体系(业务指标 vs 技术指标)?
  2. 如何避免告警疲劳(告警太多导致麻木)?
  3. 监控数据如何存储和查询(时序数据库)?

1.2 系统集成与一致性(10题)

📊 题目1:设计订单与库存的一致性保证方案

问题描述: 在订单创建流程中,需要同时扣减库存。订单服务和库存服务是两个独立的微服务,如何保证订单创建和库存扣减的一致性?

答案

问题分析: 订单库存一致性的核心挑战:

  1. 不能使用分布式事务(性能差、可用性低)
  2. 需要防止库存超卖
  3. 订单创建失败时库存要回滚
  4. 用户取消订单时库存要释放

方案一:订单服务调用库存服务(同步)

核心思路: 订单创建时同步调用库存服务扣减库存,失败则取消订单。

流程:

  1. 订单服务:创建订单(状态:PENDING)
  2. 同步调用库存服务:扣减库存
    • 成功:订单状态更新为CONFIRMED
    • 失败:订单状态更新为CANCELLED
  3. 返回结果给用户

优点:

  • 实现简单直观
  • 实时一致性
  • 用户立即知道结果

缺点:

  • 同步调用性能差
  • 库存服务故障影响订单创建
  • 网络超时难处理(已扣减但订单不知道)

方案二:本地消息表+最终一致性

核心思路: 订单创建后发送消息给库存服务,库存服务异步扣减。

流程:

  1. 订单服务:
    • 开启事务
    • 插入订单(状态:PENDING)
    • 插入本地消息表(OrderCreated事件)
    • 提交事务
  2. 消息发送器:定时扫描本地消息表,发送到MQ
  3. 库存服务:监听MQ,扣减库存
  4. 库存服务:发送库存扣减成功事件
  5. 订单服务:监听事件,更新订单状态为CONFIRMED

优点:

  • 异步解耦,性能好
  • 库存服务故障不影响订单创建
  • 消息可靠性高(本地消息表)

缺点:

  • 最终一致性(用户不能立即知道结果)
  • 实现复杂
  • 需要处理消息重复

方案三:库存预占+两阶段提交

核心思路: 订单创建时先预占库存,支付成功后确认扣减,取消时释放。

流程:

  1. 订单服务:创建订单(状态:PENDING)
  2. 同步调用库存服务:预占库存(库存减少,预占量增加)
  3. 用户支付成功后:
    • 订单状态更新为PAID
    • 异步通知库存服务确认扣减(预占量减少)
  4. 用户取消订单:
    • 订单状态更新为CANCELLED
    • 异步通知库存服务释放预占(库存增加,预占量减少)

优点:

  • 防止超卖(预占时检查库存)
  • 用户体验好(立即知道有没有库存)
  • 支持取消释放

缺点:

  • 库存设计复杂(实际库存、预占库存、可售库存)
  • 预占超时释放机制
  • 长时间预占影响其他用户

方案对比

维度同步调用本地消息表库存预占
一致性强一致最终一致强一致
性能★★☆☆☆★★★★★★★★★☆
用户体验★★★★★★★☆☆☆★★★★★
实施难度★★★★☆★★☆☆☆★★★☆☆

推荐方案: 对于电商系统,推荐库存预占方案

实施要点:

  1. 库存表设计:
    • total_stock:总库存
    • reserved_stock:预占库存
    • available_stock = total_stock - reserved_stock
  2. 预占接口:先检查available_stock,再扣减
  3. 预占超时:定时任务扫描超时订单,释放预占
  4. 幂等保证:使用orderId+action作为唯一键
  5. 监控告警:监控预占释放率、超时订单量

延伸思考

  1. 如果库存预占接口超时,订单服务如何处理?
  2. 预占超时时间如何设定(太短影响支付,太长占用库存)?
  3. 秒杀场景下如何优化库存扣减性能?

🔧 题目2:Saga模式在电商系统中的应用

问题描述: 电商订单创建涉及多个服务(库存、优惠券、积分、支付),如何使用Saga模式保证分布式事务的一致性?请详细设计Saga编排方案。

答案

问题分析: Saga在电商中的核心挑战:

  1. 如何设计补偿逻辑
  2. 如何处理部分失败
  3. 如何保证幂等性
  4. 如何监控和排查问题

方案一:编排式Saga(Orchestration)

核心思路: 由订单服务作为Saga协调器,集中编排整个流程。

设计:

SagaOrchestrator(订单服务)
├── Step1: 创建订单
├── Step2: 预占库存
│   └── 补偿:释放库存
├── Step3: 锁定优惠券
│   └── 补偿:释放优惠券
├── Step4: 扣除积分
│   └── 补偿:返还积分
└── Step5: 创建支付单
    └── 补偿:取消支付单

实现代码结构:

public class OrderSagaOrchestrator {
    private final SagaStateMachine stateMachine;
    
    public void createOrder(OrderRequest req) {
        SagaContext ctx = new SagaContext(req);
        
        // 执行Saga步骤
        stateMachine
            .step("CreateOrder", this::createOrder, this::cancelOrder)
            .step("ReserveInventory", this::reserveInventory, this::releaseInventory)
            .step("LockCoupon", this::lockCoupon, this::releaseCoupon)
            .step("DeductPoints", this::deductPoints, this::refundPoints)
            .step("CreatePayment", this::createPayment, this::cancelPayment)
            .execute(ctx);
    }
}

优点:

  • 流程集中,易于理解
  • 便于监控和调试
  • 补偿逻辑清晰

缺点:

  • 订单服务耦合度高
  • 协调器是单点

方案二:事件编排式Saga(Choreography)

核心思路: 通过事件驱动,各服务监听事件自主决策。

设计:

OrderService → OrderCreated事件
↓
InventoryService → 监听OrderCreated → 预占库存 → InventoryReserved事件
↓
CouponService → 监听InventoryReserved → 锁定券 → CouponLocked事件
↓
PointsService → 监听CouponLocked → 扣积分 → PointsDeducted事件
↓
PaymentService → 监听PointsDeducted → 创建支付单 → PaymentCreated事件
↓
OrderService → 监听PaymentCreated → 更新订单状态CONFIRMED

失败处理:

如果某一步失败,发布失败事件:
InventoryService → InventoryReserveFailed事件
↓
OrderService → 监听失败事件 → 发布OrderCancelled事件
↓
所有服务 → 监听OrderCancelled → 执行补偿操作

优点:

  • 服务解耦,独立演进
  • 无中心化瓶颈
  • 扩展性好

缺点:

  • 流程分散,难以全局把控
  • 调试困难
  • 需要完善的事件溯源

方案三:混合模式

核心思路: 核心流程用编排式,非核心流程用事件式。

设计:

编排式(核心流程):
订单服务编排:库存预占 → 优惠券锁定 → 积分扣除

事件式(通知流程):
OrderPaid事件 → 
  - 物流服务:创建运单
  - 消息服务:发送通知
  - 数据服务:更新报表

优点:

  • 核心流程可控
  • 非核心流程解耦
  • 平衡复杂度和灵活性

缺点:

  • 两种模式混用,理解成本高

方案对比

维度编排式事件式混合式
流程可见性★★★★★★★☆☆☆★★★★☆
服务解耦★★☆☆☆★★★★★★★★★☆
调试难度★★★★☆★★☆☆☆★★★☆☆
扩展性★★★☆☆★★★★★★★★★☆

推荐方案: 对于电商订单,推荐编排式Saga

实施要点:

  1. 状态机设计

    状态表:saga_execution
    - saga_id: UUID
    - current_step: 当前步骤
    - status: RUNNING/SUCCESS/COMPENSATING/FAILED
    - context: JSON(上下文数据)
    - created_at, updated_at
    
    步骤表:saga_step
    - saga_id
    - step_name: ReserveInventory
    - status: PENDING/SUCCESS/FAILED/COMPENSATED
    - retry_count: 重试次数
    - error_message: 错误信息
    
  2. 幂等性保证

    • 每个步骤携带saga_id作为业务唯一键
    • 服务侧记录已处理的saga_id
    • 重复请求直接返回成功
  3. 超时处理

    • 每个步骤设置超时时间(如3秒)
    • 超时后执行补偿
    • 定时任务兜底扫描长时间未完成的Saga
  4. 补偿策略

    • 向前补偿:重试直到成功
    • 向后补偿:回滚已完成的步骤
    • 混合补偿:核心步骤向前,非核心向后
  5. 监控告警

    • Saga成功率
    • 平均执行时间
    • 补偿执行次数
    • 长时间未完成的Saga

延伸思考

  1. 如果补偿操作也失败了怎么办?
  2. Saga执行过程中服务重启如何恢复?
  3. 如何设计Saga的测试策略?

💡 题目3:如何选择同步调用vs异步消息?

问题描述: 在微服务架构中,服务间通信可以使用同步RPC或异步消息队列。在电商系统中,如何选择合适的通信方式?

答案

问题分析: 同步vs异步的核心考量:

  1. 业务语义:是否需要立即返回结果
  2. 性能要求:延迟vs吞吐量
  3. 可靠性:是否允许消息丢失
  4. 复杂度:实现和运维成本

方案一:全部使用同步RPC

适用场景:

  • 查询操作(查询订单详情、商品信息)
  • 强实时性要求(下单时检查库存)
  • 需要立即返回结果(用户等待响应)

优点:

  • 实现简单
  • 调用链路清晰
  • 易于调试

缺点:

  • 性能瓶颈(串行调用)
  • 可用性差(下游故障影响上游)
  • 难以削峰

方案二:全部使用异步消息

适用场景:

  • 通知类操作(发送短信、邮件)
  • 可延迟处理(数据同步、报表生成)
  • 需要削峰填谷(秒杀、大促)

优点:

  • 解耦(服务独立)
  • 削峰(消息堆积)
  • 高吞吐

缺点:

  • 最终一致性
  • 消息丢失风险
  • 调试困难

方案三:混合使用(推荐)

决策矩阵:

场景通信方式理由
查询商品详情同步RPC需要立即返回
下单扣减库存同步RPC需要立即知道结果
订单支付成功→通知物流异步消息不需要立即处理
订单支付成功→发送短信异步消息允许延迟
商品信息变更→更新搜索异步消息最终一致即可
计算订单金额同步RPC需要立即返回金额
订单创建→更新统计报表异步消息非实时

决策原则:

  1. 用户在等待:使用同步(如下单、支付、查询)
  2. 用户不在等待:使用异步(如通知、数据同步)
  3. 强一致性:使用同步(如扣款、扣库存)
  4. 最终一致性:使用异步(如积分、优惠券)
  5. 高并发:优先异步(如秒杀、大促)

优点:

  • 平衡性能和一致性
  • 灵活应对不同场景
  • 整体架构合理

缺点:

  • 需要维护两套通信机制
  • 团队需要理解选择原则

方案对比

维度全同步全异步混合
实时性★★★★★★★☆☆☆★★★★☆
吞吐量★★☆☆☆★★★★★★★★★☆
可用性★★☆☆☆★★★★★★★★★☆
复杂度★★★★☆★★☆☆☆★★★☆☆

推荐方案: 采用混合模式,根据场景选择合适的通信方式。

实施要点:

  1. 同步调用优化

    • 设置合理超时(如3秒)
    • 使用断路器防止雪崩
    • 重要接口设置重试机制
    • 监控调用成功率和延迟
  2. 异步消息优化

    • 使用本地消息表保证可靠性
    • 消费端幂等处理
    • 死信队列处理失败消息
    • 监控消息积压和消费延迟
  3. 场景识别技巧

    • 问:用户是否在等待结果?
    • 问:失败了是否需要立即知道?
    • 问:是否需要强一致性?
    • 问:并发量有多大?
  4. 混合调用模式

    示例:订单支付成功后
    
    同步:
    - 更新订单状态(用户需要立即看到)
    - 扣减库存(强一致性)
    
    异步:
    - 通知物流(可延迟)
    - 发送短信(可延迟)
    - 更新报表(可延迟)
    - 赠送积分(最终一致)
    
  5. 降级策略

    • 异步消息:队列满时拒绝接入
    • 同步调用:超时降级(返回默认值或缓存)

延伸思考

  1. 如何处理异步消息丢失的情况?
  2. 同步调用超时后如何判断是否成功?
  3. 如何在同步和异步之间切换(如异步改同步)?

🔧 题目4:分布式事务的几种实现方案对比

问题描述: 请对比2PC、TCC、Saga、本地消息表这几种分布式事务方案,并说明在电商系统中各自的适用场景。

答案

问题分析: 分布式事务方案的核心差异:

  1. 一致性强度(强一致 vs 最终一致)
  2. 性能开销(锁定时间、网络开销)
  3. 实现复杂度(接口数量、补偿逻辑)
  4. 适用场景(金融 vs 电商)

方案一:2PC(Two-Phase Commit)

核心思想: 分为准备阶段和提交阶段,由协调者统一协调。

流程:

准备阶段(Prepare):
协调者 → 所有参与者:准备事务
参与者 → 锁定资源,记录日志
参与者 → 协调者:返回YES/NO

提交阶段(Commit):
如果所有参与者返回YES:
  协调者 → 所有参与者:提交事务
  参与者 → 释放资源,返回ACK
如果任一参与者返回NO:
  协调者 → 所有参与者:回滚事务

优点:

  • 强一致性
  • 原理简单

缺点:

  • 性能差(两次网络往返)
  • 阻塞(参与者锁定资源)
  • 单点故障(协调者宕机)
  • 不适合微服务

适用场景:

  • 数据库分布式事务
  • 小规模系统
  • 对一致性要求极高且并发不高的场景

方案二:TCC(Try-Confirm-Cancel)

核心思想: 每个服务提供三个接口:Try、Confirm、Cancel。

流程:

Try阶段:
- 订单服务:tryCreateOrder(创建订单,状态TRYING)
- 库存服务:tryReserveInventory(预占库存)
- 支付服务:tryPreparePayment(冻结金额)

全部成功 → Confirm阶段:
- 订单服务:confirmCreateOrder(状态CONFIRMED)
- 库存服务:confirmReserveInventory(确认扣减)
- 支付服务:confirmPreparePayment(确认扣款)

任一失败 → Cancel阶段:
- 订单服务:cancelCreateOrder(取消订单)
- 库存服务:cancelReserveInventory(释放库存)
- 支付服务:cancelPreparePayment(解冻金额)

优点:

  • 强一致性
  • 业务语义清晰
  • 资源锁定明确

缺点:

  • 实现复杂(每个服务3个接口)
  • Try阶段占用资源时间长
  • 需要考虑幂等性

适用场景:

  • 金融交易(转账、支付)
  • 核心交易链路
  • 对一致性要求极高的场景

方案三:Saga

核心思想: 将长事务拆分为多个本地事务,失败时执行补偿。

流程:

正向流程:
T1: 创建订单 → T2: 扣减库存 → T3: 扣除积分 → T4: 创建支付

失败补偿:
T4失败 → C3: 返还积分 → C2: 释放库存 → C1: 取消订单

优点:

  • 最终一致性
  • 性能好(异步)
  • 实现相对简单

缺点:

  • 补偿逻辑复杂
  • 隔离性弱(中间状态可见)
  • 需要考虑补偿失败

适用场景:

  • 电商订单流程
  • 长流程业务
  • 可接受最终一致性的场景

方案四:本地消息表

核心思想: 利用本地事务保证消息发送,通过消息驱动下游操作。

流程:

1. 订单服务:
   BEGIN TRANSACTION
     INSERT INTO orders ...
     INSERT INTO outbox_messages (event: OrderCreated)
   COMMIT
   
2. 消息发送器:
   定时扫描outbox_messages
   发送到MQ
   标记为已发送
   
3. 库存服务:
   监听MQ OrderCreated事件
   扣减库存
   发送InventoryReduced事件
   
4. 订单服务:
   监听InventoryReduced事件
   更新订单状态

优点:

  • 实现简单
  • 消息可靠性高
  • 无锁,性能好

缺点:

  • 最终一致性
  • 需要扫描任务
  • 消息顺序性

适用场景:

  • 异步通知场景
  • 跨系统数据同步
  • 对实时性要求不高的场景

方案对比

方案一致性性能复杂度隔离性适用场景
2PC强一致★☆☆☆☆★★☆☆☆★★★★★数据库事务
TCC强一致★★☆☆☆★★★★★★★★★☆金融交易
Saga最终一致★★★★☆★★★☆☆★★☆☆☆电商订单
本地消息表最终一致★★★★★★★★★☆★★☆☆☆异步通知

推荐方案: 电商系统不同场景使用不同方案:

  • 订单创建流程:Saga(性能和一致性平衡)
  • 支付流程:TCC(强一致性)
  • 数据同步:本地消息表(异步解耦)
  • 库存扣减:预占机制(类似TCC)

延伸思考

  1. 为什么电商系统很少用2PC?
  2. TCC的Try阶段如何设计才能保证性能?
  3. Saga的补偿操作如何保证一定成功?

📊 题目5:设计事件驱动架构

问题描述: 电商系统需要实现事件驱动架构,当订单状态变更时,自动触发物流、消息通知、数据统计等下游操作。请设计事件驱动方案。

答案

问题分析: 事件驱动架构的核心挑战:

  1. 如何设计领域事件
  2. 如何保证事件不丢失
  3. 如何处理事件顺序性
  4. 如何避免事件风暴

方案一:基于数据库Change Data Capture(CDC)

核心思想: 监听数据库变更日志(binlog),自动生成事件。

设计:

MySQL binlog → Debezium → Kafka → 下游服务

示例:
orders表INSERT → Debezium捕获 → 
  发送到Kafka主题: order.events → 
  物流服务消费 → 创建运单

优点:

  • 零侵入(不需要改业务代码)
  • 事件不丢失(基于binlog)
  • 实时性高

缺点:

  • 事件语义不清晰(只有数据变更)
  • 难以表达业务意图
  • 依赖数据库

适用场景:

  • 数据同步
  • 数据库归档
  • 实时数仓

方案二:基于领域事件+Outbox模式

核心思想: 业务代码显式发布领域事件,通过Outbox模式保证可靠性。

设计:

1. 订单服务发布事件:
   BEGIN TRANSACTION
     UPDATE orders SET status='PAID'
     INSERT INTO outbox_events (
       event_id, event_type, payload, status
     ) VALUES (
       UUID(), 'OrderPaid', '{"orderId":"123"}', 'PENDING'
     )
   COMMIT

2. 事件发布器(独立进程):
   while (true) {
     events = SELECT * FROM outbox_events WHERE status='PENDING' LIMIT 100
     for (event in events) {
       kafka.send(event.event_type, event.payload)
       UPDATE outbox_events SET status='SENT' WHERE event_id=event.id
     }
     sleep(1s)
   }

3. 下游服务消费:
   物流服务监听order.paid主题
   消息服务监听order.paid主题
   报表服务监听order.paid主题

领域事件设计:

OrderCreated {
  orderId, userId, items[], totalAmount, createdAt
}

OrderPaid {
  orderId, paymentId, paidAmount, paidAt
}

OrderShipped {
  orderId, trackingNumber, shippedAt
}

OrderCompleted {
  orderId, completedAt
}

优点:

  • 业务语义清晰
  • 事件可靠性高(Outbox模式)
  • 下游解耦

缺点:

  • 需要Outbox表和发布器
  • 实现复杂度中等

适用场景:

  • 微服务间通信
  • 业务事件通知
  • 事件溯源

方案三:基于消息队列事务消息

核心思想: 使用RocketMQ的事务消息,保证事件发送和本地事务一致性。

设计:

1. 发送半消息(Half Message):
   rocketMQ.sendHalfMessage(topic, "OrderPaid", payload)
   
2. 执行本地事务:
   BEGIN TRANSACTION
     UPDATE orders SET status='PAID'
   COMMIT
   
3. 提交/回滚消息:
   if (本地事务成功) {
     rocketMQ.commit(messageId)
   } else {
     rocketMQ.rollback(messageId)
   }
   
4. 消息回查(如果未收到commit/rollback):
   rocketMQ定期回查 → 订单服务检查订单状态 → 返回commit/rollback

优点:

  • 事件可靠性高
  • 不需要Outbox表
  • RocketMQ原生支持

缺点:

  • 依赖特定MQ(RocketMQ)
  • 需要实现回查接口

适用场景:

  • 使用RocketMQ的系统
  • 需要强可靠性的场景

方案对比

维度CDCOutbox事务消息
事件语义★★☆☆☆★★★★★★★★★☆
可靠性★★★★★★★★★★★★★★★
侵入性★★★★★★★★☆☆★★★☆☆
实施难度★★★★☆★★★☆☆★★★★☆

推荐方案: 对于电商系统,推荐Outbox模式

实施要点:

  1. 事件设计原则

    • 事件名称:过去时(OrderPaid、OrderShipped)
    • 包含完整上下文(避免下游再查询)
    • 不可变(发出后不能修改)
    • 幂等性(包含event_id)
  2. Outbox表设计

    CREATE TABLE outbox_events (
      event_id VARCHAR(64) PRIMARY KEY,
      aggregate_type VARCHAR(50), -- Order/Product
      aggregate_id VARCHAR(64),   -- orderId
      event_type VARCHAR(50),      -- OrderPaid
      payload JSON,                -- 事件数据
      status VARCHAR(20),          -- PENDING/SENT/FAILED
      retry_count INT DEFAULT 0,
      created_at TIMESTAMP,
      sent_at TIMESTAMP
    );
    
  3. 事件发布器优化

    • 批量读取(每次100条)
    • 批量发送(提高吞吐)
    • 失败重试(指数退避)
    • 定期清理已发送事件(保留7天)
  4. 消费端幂等

    消费端维护已处理事件表:
    CREATE TABLE processed_events (
      event_id VARCHAR(64) PRIMARY KEY,
      processed_at TIMESTAMP
    );
    
    处理逻辑:
    1. 检查event_id是否已处理
    2. 如果已处理,直接返回
    3. 执行业务逻辑
    4. 记录event_id到processed_events
    
  5. 监控告警

    • Outbox待发送事件数
    • 事件发送失败率
    • 消费延迟

延伸思考

  1. 如何保证事件的顺序性(同一订单的多个事件)?
  2. 如何处理事件风暴(大量事件同时发布)?
  3. 事件版本如何管理(EventV1、EventV2)?

🔧 题目6:如何保证消息的可靠投递?

问题描述: 在消息队列(Kafka/RocketMQ)中,如何保证消息从生产者到消费者的可靠投递,不丢失、不重复、不乱序?

答案

问题分析: 消息可靠性的三个维度:

  1. 生产端:如何保证消息发送成功
  2. 存储端:如何保证消息不丢失
  3. 消费端:如何保证消息处理成功

方案一:At Least Once(至少一次)

核心思想: 保证消息至少被消费一次,可能重复但不会丢失。

实现:

生产端:
1. 发送消息到MQ
2. 等待MQ确认(ACK)
3. 如果超时或失败,重试发送

MQ端:
1. 消息写入磁盘后返回ACK
2. 多副本复制(至少2个副本确认)

消费端:
1. 消费消息
2. 处理业务逻辑
3. 手动提交offset
4. 如果处理失败,不提交offset,下次重新消费

优点:

  • 消息不丢失
  • 实现相对简单

缺点:

  • 可能重复消费
  • 需要业务幂等

适用场景:

  • 大部分业务场景
  • 可以接受重复的场景

方案二:At Most Once(至多一次)

核心思想: 消息可能丢失,但不会重复。

实现:

生产端:
1. 发送消息
2. 不等待确认,直接返回

消费端:
1. 先提交offset
2. 再处理业务逻辑
3. 如果处理失败,消息丢失

优点:

  • 不会重复
  • 性能高

缺点:

  • 可能丢消息

适用场景:

  • 日志采集
  • 监控数据上报
  • 允许丢失的场景

方案三:Exactly Once(精确一次)

核心思想: 消息既不丢失也不重复,精确消费一次。

实现(基于Kafka事务):

生产端:
producer.initTransactions()
try {
  producer.beginTransaction()
  producer.send(record1)
  producer.send(record2)
  // 更新本地数据库
  db.update(...)
  producer.commitTransaction()
} catch (Exception e) {
  producer.abortTransaction()
}

消费端:
consumer.subscribe(topic)
consumer.setIsolationLevel(READ_COMMITTED) // 只读已提交
while (true) {
  records = consumer.poll()
  for (record in records) {
    // 幂等处理
    if (isProcessed(record.key)) {
      continue
    }
    process(record)
    markProcessed(record.key)
  }
  consumer.commitSync()
}

实现(基于业务幂等):

消息设计:
{
  "messageId": "uuid",   // 全局唯一ID
  "orderId": "123",
  "payload": {...}
}

消费端:
1. 检查messageId是否已处理(查Redis/DB)
2. 如果已处理,直接返回成功
3. 执行业务逻辑 + 记录messageId(同一事务)
4. 提交offset

优点:

  • 不丢不重
  • 语义最强

缺点:

  • 实现复杂
  • 性能开销大
  • 需要事务支持

适用场景:

  • 金融交易
  • 支付扣款
  • 对准确性要求极高的场景

方案对比

语义是否丢失是否重复性能复杂度适用场景
At Least Once不丢失可能重复★★★★☆★★★☆☆大部分场景
At Most Once可能丢失不重复★★★★★★★★★☆日志、监控
Exactly Once不丢失不重复★★☆☆☆★★☆☆☆金融、支付

推荐方案: 对于电商系统,推荐At Least Once + 业务幂等

实施要点:

  1. 生产端可靠性

    // 同步发送(等待确认)
    producer.send(record).get(3, TimeUnit.SECONDS)
    
    // 配置
    acks=all              // 所有副本确认
    retries=3            // 重试3次
    max.in.flight.requests.per.connection=1  // 保证顺序
    
  2. MQ端可靠性

    Kafka配置:
    - replication.factor=3     // 3副本
    - min.insync.replicas=2    // 至少2个副本确认
    - unclean.leader.election.enable=false  // 禁止非ISR副本成为leader
    
    RocketMQ配置:
    - flushDiskType=SYNC_FLUSH  // 同步刷盘
    
  3. 消费端可靠性

    // 手动提交offset
    while (true) {
      records = consumer.poll()
      try {
        for (record in records) {
          // 幂等处理
          if (!isProcessed(record.messageId)) {
            process(record)
            markProcessed(record.messageId)
          }
        }
        // 处理成功后提交offset
        consumer.commitSync()
      } catch (Exception e) {
        // 处理失败,不提交offset,下次重新消费
        log.error("Process failed", e)
      }
    }
    
  4. 幂等性设计

    方案1:基于唯一键
    - 消息包含messageId
    - 消费端用messageId去重(Redis/DB)
    
    方案2:基于业务唯一键
    - 订单号、支付流水号等
    - 数据库唯一索引约束
    
    方案3:基于版本号
    - 数据包含版本号
    - 更新时检查版本号(乐观锁)
    
  5. 顺序性保证

    发送端:
    - 同一订单的消息发到同一分区(按orderId hash)
    - max.in.flight.requests=1(保证分区内有序)
    
    消费端:
    - 单线程消费同一分区
    - 或使用版本号检查顺序
    

延伸思考

  1. 如果消费失败,消息应该重试几次?
  2. 如何处理消息积压问题?
  3. 如何实现消息的延迟投递?

💡 题目7:幂等性设计的最佳实践

问题描述: 在分布式系统中,由于网络重试、消息重复等原因,同一个请求可能被处理多次。如何设计幂等机制,保证重复请求不会产生副作用?

答案

问题分析: 幂等性的核心挑战:

  1. 如何识别重复请求
  2. 如何防止并发重复处理
  3. 如何设计幂等键
  4. 幂等状态如何存储和清理

方案一:基于唯一请求ID

核心思想: 客户端为每个请求生成唯一ID,服务端记录已处理的ID。

实现:

客户端:
POST /api/orders
Headers: {
  X-Request-Id: uuid-xxx-xxx
}
Body: {...}

服务端:
public void createOrder(OrderRequest req, String requestId) {
  // 1. 检查requestId是否已处理
  if (redis.exists("idempotent:" + requestId)) {
    return getCachedResult(requestId);
  }
  
  // 2. 加分布式锁(防止并发)
  if (!redis.setNX("lock:" + requestId, 1, 10)) {
    throw new ConcurrentException();
  }
  
  try {
    // 3. 执行业务逻辑
    Order order = orderService.create(req);
    
    // 4. 记录已处理+结果
    redis.setEx("idempotent:" + requestId, 
                JSON.stringify(order), 
                86400); // 保留24小时
    
    return order;
  } finally {
    redis.del("lock:" + requestId);
  }
}

优点:

  • 实现简单
  • 通用性强

缺点:

  • 需要客户端配合生成requestId
  • Redis存储成本

适用场景:

  • API接口调用
  • 用户操作(防止重复点击)

方案二:基于业务唯一键

核心思想: 利用业务本身的唯一性(订单号、流水号)作为幂等键。

实现:

数据库设计:
CREATE TABLE orders (
  order_id VARCHAR(64) PRIMARY KEY,  -- 业务唯一键
  user_id VARCHAR(64),
  amount DECIMAL(10,2),
  status VARCHAR(20),
  UNIQUE KEY uk_user_order(user_id, external_order_id)  -- 组合唯一键
);

代码:
public Order createOrder(OrderRequest req) {
  String orderId = generateOrderId();  // 客户端提供或服务端生成
  
  try {
    // INSERT会因为主键冲突失败(幂等)
    db.insert("INSERT INTO orders VALUES (?, ?, ?, ?)",
              orderId, req.getUserId(), req.getAmount(), "PENDING");
    
    return getOrder(orderId);
  } catch (DuplicateKeyException e) {
    // 重复请求,返回已存在的订单
    return getOrder(orderId);
  }
}

优点:

  • 不需要额外存储
  • 性能好(数据库索引)
  • 天然幂等

缺点:

  • 需要设计业务唯一键
  • 不适合更新操作

适用场景:

  • 创建类操作(订单、支付)
  • 有明确业务唯一键的场景

方案三:基于状态机+版本号

核心思想: 利用状态机和版本号,保证状态流转的幂等性。

实现:

数据库设计:
CREATE TABLE orders (
  order_id VARCHAR(64) PRIMARY KEY,
  status VARCHAR(20),
  version INT DEFAULT 0,  -- 版本号
  updated_at TIMESTAMP
);

代码:
public void payOrder(String orderId) {
  // 乐观锁:只有状态为PENDING且版本匹配才更新
  int affected = db.update(
    "UPDATE orders SET status='PAID', version=version+1, updated_at=NOW() " +
    "WHERE order_id=? AND status='PENDING' AND version=?",
    orderId, currentVersion
  );
  
  if (affected == 0) {
    // 已经支付过了(幂等)或并发冲突
    Order order = getOrder(orderId);
    if (order.getStatus() == "PAID") {
      return; // 幂等,直接返回
    } else {
      throw new ConcurrentModificationException(); // 并发冲突,重试
    }
  }
}

优点:

  • 不需要额外存储
  • 天然解决并发问题
  • 适合更新操作

缺点:

  • 需要理解状态机
  • 并发冲突需要重试

适用场景:

  • 状态流转操作(订单状态、支付状态)
  • 更新操作

方案对比

方案适用操作存储成本并发安全实施难度
请求ID所有★★☆☆☆★★★★★★★★★☆
业务唯一键创建★★★★★★★★★★★★★★★
状态机+版本号更新★★★★★★★★★☆★★★☆☆

推荐方案: 根据场景选择合适方案:

  • 创建操作:业务唯一键(如orderId)
  • 更新操作:状态机+版本号
  • 通用接口:请求ID

实施要点:

  1. 幂等键设计原则

    • 唯一性:能唯一标识一次操作
    • 稳定性:多次请求幂等键相同
    • 业务相关:优先用业务键(orderId)而非技术键(requestId)
  2. 幂等粒度

    粗粒度(操作级):
    - 幂等键:orderId
    - 含义:同一订单不能重复创建
    
    细粒度(步骤级):
    - 幂等键:orderId + operationType(如 "order123:pay")
    - 含义:同一订单的支付操作不能重复
    
  3. 幂等状态清理

    Redis存储:
    - 设置过期时间(24小时)
    - 定期清理过期数据
    
    数据库存储:
    - 不需要清理(利用业务唯一键)
    
  4. 并发安全

    分布式锁:
    String lockKey = "lock:order:" + orderId;
    if (redis.setNX(lockKey, 1, 10)) {
      try {
        // 业务逻辑
      } finally {
        redis.del(lockKey);
      }
    }
    
    数据库乐观锁:
    UPDATE orders 
    SET status=?, version=version+1 
    WHERE order_id=? AND version=?
    
  5. 幂等测试

    测试用例:
    1. 相同参数调用2次,验证第2次返回相同结果
    2. 并发调用2次,验证只有1次生效
    3. 延迟重试,验证中间状态不影响幂等性
    

延伸思考

  1. 如何设计支付接口的幂等性?
  2. 消息队列消费端如何保证幂等性?
  3. 幂等状态如何跨服务共享?

📊 题目8:设计数据最终一致性的对账补偿机制

问题描述: 在分布式系统中,虽然采用了Saga等最终一致性方案,但仍可能因为网络、重试等原因导致数据不一致。如何设计对账补偿机制?

答案

问题分析: 对账补偿的核心挑战:

  1. 如何发现数据不一致
  2. 如何定位不一致的根本原因
  3. 如何自动补偿修复
  4. 如何避免补偿引入新问题

方案一:实时对账

核心思想: 在关键操作后立即对账,发现问题立即修复。

设计:

订单支付成功后:
1. 订单服务:更新订单状态为PAID
2. 支付服务:更新支付单状态为SUCCESS
3. 对账服务:
   - 查询订单状态
   - 查询支付单状态
   - 对比是否一致
   - 如果不一致,触发告警和补偿

补偿逻辑:
if (订单状态=PAID && 支付单状态!=SUCCESS) {
  // 订单已支付但支付单未成功,可能是支付服务更新失败
  重试更新支付单状态
} else if (订单状态!=PAID && 支付单状态=SUCCESS) {
  // 支付成功但订单未更新,可能是订单服务更新失败
  重试更新订单状态
}

优点:

  • 及时发现问题
  • 用户感知小

缺点:

  • 增加延迟
  • 对账服务成为瓶颈

适用场景:

  • 核心流程(支付、扣款)
  • 对一致性要求极高的场景

方案二:定时对账

核心思想: 定时(如每小时、每天)对账,批量发现和修复问题。

设计:

对账任务(每小时执行):
1. 查询最近1小时的订单:
   SELECT * FROM orders 
   WHERE created_at >= NOW() - INTERVAL 1 HOUR
   
2. 对每个订单进行对账:
   - 查询订单信息(order)
   - 查询库存记录(inventory_log)
   - 查询支付记录(payment)
   - 查询物流记录(logistics)
   
3. 检查一致性:
   订单状态 vs 支付状态
   订单金额 vs 支付金额
   订单库存扣减 vs 库存日志
   
4. 发现不一致:
   - 记录到对账差异表
   - 触发告警
   - 尝试自动补偿

对账差异表:

CREATE TABLE reconciliation_diff (
  id BIGINT PRIMARY KEY,
  order_id VARCHAR(64),
  diff_type VARCHAR(50),  -- PAYMENT_MISMATCH/INVENTORY_MISMATCH
  expected_value VARCHAR(200),
  actual_value VARCHAR(200),
  status VARCHAR(20),     -- PENDING/COMPENSATED/MANUAL
  created_at TIMESTAMP,
  compensated_at TIMESTAMP
);

优点:

  • 批量处理,效率高
  • 不影响业务性能
  • 可以发现各种不一致

缺点:

  • 延迟高(小时级)
  • 用户可能已感知问题

适用场景:

  • 非核心流程
  • 对实时性要求不高的场景
  • 大批量数据对账

方案三:事件溯源+状态重建

核心思想: 记录所有事件,通过重放事件重建状态,与当前状态对比。

设计:

事件表:
CREATE TABLE domain_events (
  event_id VARCHAR(64) PRIMARY KEY,
  aggregate_id VARCHAR(64),  -- orderId
  event_type VARCHAR(50),    -- OrderCreated/OrderPaid
  payload JSON,
  version INT,
  created_at TIMESTAMP
);

对账逻辑:
1. 查询订单的所有事件:
   SELECT * FROM domain_events 
   WHERE aggregate_id='order123' 
   ORDER BY version

2. 重放事件,重建订单状态:
   Order order = new Order();
   for (event in events) {
     order.apply(event);  // OrderCreated → OrderPaid → OrderShipped
   }
   
3. 对比重建状态 vs 数据库状态:
   rebuiltOrder.status vs dbOrder.status
   rebuiltOrder.amount vs dbOrder.amount
   
4. 如果不一致,说明有事件丢失或数据被错误修改

优点:

  • 可追溯完整历史
  • 可精确定位问题
  • 天然支持审计

缺点:

  • 事件存储成本高
  • 重建状态复杂
  • 实施难度大

适用场景:

  • 金融系统
  • 审计要求高的场景

方案对比

方案实时性准确性成本复杂度适用场景
实时对账★★★★★★★★☆☆★★★☆☆★★★★☆核心流程
定时对账★★☆☆☆★★★★★★★★★☆★★★☆☆一般流程
事件溯源★★★☆☆★★★★★★★☆☆☆★★☆☆☆金融系统

推荐方案: 对于电商系统,推荐定时对账为主,实时对账为辅

实施要点:

  1. 对账维度设计

    维度1:订单-支付对账
    - 订单状态=PAID ⇔ 存在成功的支付记录
    - 订单金额 = 支付金额
    
    维度2:订单-库存对账
    - 订单商品 ⇔ 库存扣减记录
    - 订单数量 = 库存扣减数量
    
    维度3:订单-物流对账
    - 订单状态=SHIPPED ⇔ 存在物流单
    
    维度4:财务对账
    - 订单收入 = 支付收入 - 退款
    
  2. 补偿策略

    自动补偿(低风险):
    - 订单已支付,库存未扣减 → 自动扣减库存
    - 订单已取消,库存未释放 → 自动释放库存
    
    人工介入(高风险):
    - 订单金额与支付金额不一致 → 人工审核
    - 库存为负数 → 人工调整
    - 重复支付 → 人工退款
    
  3. 对账任务调度

    实时对账:
    - 支付成功后:立即对账订单-支付
    
    分钟级对账:
    - 每5分钟:对账最近10分钟的订单
    
    小时级对账:
    - 每小时:对账最近2小时的订单
    
    日级对账:
    - 每天凌晨:对账前一天所有订单
    - 生成对账报表
    
  4. 补偿幂等性

    补偿操作必须幂等:
    - 记录补偿历史(避免重复补偿)
    - 使用业务唯一键
    - 状态机保证(只能从错误状态补偿到正确状态)
    
  5. 监控告警

    指标:
    - 对账差异数量
    - 自动补偿成功率
    - 人工处理待办数量
    
    告警:
    - 差异数量超过阈值
    - 自动补偿失败
    - 关键差异(金额不一致)
    

延伸思考

  1. 如果对账也失败了(如查询超时),如何处理?
  2. 补偿操作失败后如何处理?
  3. 如何设计对账结果的可视化展示?

🔧 题目9:如何处理分布式系统的时钟问题?

问题描述: 在分布式系统中,不同服务器的时钟可能不同步,导致时间戳不一致、时序错误等问题。如何处理分布式系统的时钟问题?

答案

问题分析: 分布式时钟的核心挑战:

  1. 时钟漂移:不同机器时钟不一致
  2. 时序依赖:如何判断事件先后顺序
  3. 超时判断:如何准确计算超时
  4. 数据时效:如何判断数据是否过期

方案一:使用NTP时钟同步

核心思想: 通过NTP协议同步所有服务器的时钟。

实现:

1. 配置NTP服务器:
   所有应用服务器配置相同的NTP源
   
2. 定期同步:
   ntpd守护进程自动同步时钟
   
3. 监控时钟偏移:
   监控各服务器与NTP服务器的时钟差
   差异超过100ms告警

优点:

  • 实施简单
  • 对应用透明
  • 时钟基本一致

缺点:

  • 无法完全同步(仍有毫秒级误差)
  • 时钟回拨问题(同步时时钟可能后退)
  • 依赖网络

适用场景:

  • 对时间精度要求不高的场景
  • 作为基础设施

方案二:逻辑时钟(Lamport时钟)

核心思想: 不依赖物理时钟,用逻辑计数器表示事件顺序。

实现:

每个节点维护一个计数器:
1. 节点发生事件时,计数器+1
2. 节点发送消息时,附带当前计数器值
3. 节点收到消息时,更新计数器:
   counter = max(local_counter, message_counter) + 1

示例:
节点A: counter=5, 发送消息(counter=5)
节点B: 收到消息, local_counter=3
        更新counter = max(3, 5) + 1 = 6

优点:

  • 不依赖物理时钟
  • 可以判断因果关系
  • 实现简单

缺点:

  • 只能判断偏序(不能判断所有事件的先后)
  • 不是真实时间
  • 计数器可能很大

适用场景:

  • 分布式日志
  • 事件顺序
  • 因果一致性

方案三:混合逻辑时钟(HLC)

核心思想: 结合物理时钟和逻辑时钟,既有真实时间又有顺序保证。

实现:

HLC = (physicalTime, logicalCounter)

更新规则:
1. 本地事件:
   pt = 物理时钟
   if (pt > hlc.pt) {
     hlc = (pt, 0)
   } else {
     hlc = (hlc.pt, hlc.lc + 1)
   }
   
2. 收到消息(msg_hlc):
   pt = max(物理时钟, msg_hlc.pt, hlc.pt)
   if (pt > hlc.pt) {
     hlc = (pt, 0)
   } else if (pt == msg_hlc.pt) {
     hlc = (pt, max(hlc.lc, msg_hlc.lc) + 1)
   } else {
     hlc = (hlc.pt, hlc.lc + 1)
   }

示例:
节点A: HLC=(100, 0), 发生事件
       物理时钟=99 < 100
       HLC=(100, 1)
       
节点B: 收到消息HLC=(100, 1)
       物理时钟=105
       HLC=(105, 0)

优点:

  • 有真实时间(可展示给用户)
  • 有顺序保证(逻辑计数器)
  • 时钟单调递增(不会回退)

缺点:

  • 实现复杂
  • 需要所有节点支持

适用场景:

  • 分布式数据库(CockroachDB使用)
  • 需要时间戳又要顺序的场景

方案对比

方案时间准确性顺序保证实施难度适用场景
NTP同步★★★★☆★★☆☆☆★★★★★通用
Lamport时钟★☆☆☆☆★★★★★★★★★☆事件顺序
混合逻辑时钟★★★★☆★★★★★★★★☆☆分布式DB

推荐方案: 对于电商系统,推荐NTP同步 + 避免依赖绝对时间

实施要点:

  1. NTP基础设施

    - 部署内网NTP服务器
    - 所有应用服务器同步内网NTP
    - 监控时钟偏移(超过100ms告警)
    - 禁止手动修改系统时间
    
  2. 避免依赖绝对时间

    错误示例:
    // 判断订单是否超时(依赖绝对时间)
    if (now() - order.createdAt > 30分钟) {
      cancelOrder()
    }
    问题:如果时钟回拨,可能误判
    
    正确示例:
    // 使用相对时间或逻辑状态
    if (order.status == PENDING && order.createdAt < now() - 30分钟) {
      // 且使用定时任务扫描(而非实时判断)
      cancelOrder()
    }
    
  3. 处理时钟回拨

    生成ID时(如雪花ID):
    if (currentTime < lastTime) {
      // 时钟回拨,等待追上
      wait(lastTime - currentTime)
    }
    
    或使用单调递增ID:
    // 不依赖时间,只保证递增
    nextId = atomicIncrement()
    
  4. 使用逻辑版本号

    数据表:
    CREATE TABLE orders (
      order_id VARCHAR(64),
      version INT,  -- 逻辑版本号(不依赖时间)
      updated_at TIMESTAMP
    );
    
    更新时:
    UPDATE orders 
    SET version=version+1, updated_at=NOW()
    WHERE order_id=? AND version=?
    
  5. 时间窗口设计

    对账任务:
    // 不对账"最近5分钟"的数据(避免时钟误差)
    SELECT * FROM orders 
    WHERE created_at < NOW() - INTERVAL 5 MINUTE
      AND created_at >= NOW() - INTERVAL 1 HOUR
    

延伸思考

  1. 如何设计分布式系统的全局唯一ID(考虑时钟回拨)?
  2. 如何在不同时区的数据中心部署系统?
  3. 时钟跳跃(突然快进)如何处理?

💡 题目10:微服务间的版本兼容性设计

问题描述: 微服务架构下,服务A调用服务B的接口。当服务B升级时,如何保证向后兼容,不影响服务A?请设计API版本管理方案。

答案

问题分析: API版本兼容的核心挑战:

  1. 如何在不影响老版本的情况下升级
  2. 如何管理多个版本的共存
  3. 如何平滑下线老版本
  4. 如何处理数据结构变更

方案一:URL版本控制

核心思想: 在URL中包含版本号,不同版本独立部署。

实现:

版本1:GET /api/v1/orders/{id}
返回:{
  "orderId": "123",
  "amount": 100.00,
  "status": "PAID"
}

版本2:GET /api/v2/orders/{id}
返回:{
  "orderId": "123",
  "totalAmount": 100.00,    // 字段重命名
  "discountAmount": 10.00,  // 新增字段
  "paymentStatus": "PAID"   // 字段重命名
}

服务端:
@RestController
@RequestMapping("/api/v1")
public class OrderControllerV1 {
  @GetMapping("/orders/{id}")
  public OrderV1 getOrder(@PathVariable String id) {
    return orderService.getOrderV1(id);
  }
}

@RestController
@RequestMapping("/api/v2")
public class OrderControllerV2 {
  @GetMapping("/orders/{id}")
  public OrderV2 getOrder(@PathVariable String id) {
    return orderService.getOrderV2(id);
  }
}

优点:

  • 版本隔离清晰
  • 易于理解
  • 支持大版本升级

缺点:

  • 需要维护多份代码
  • URL变化对客户端不友好
  • 版本爆炸

适用场景:

  • 大版本升级(API重构)
  • 需要长期支持多版本

方案二:Header版本控制

核心思想: URL不变,通过HTTP Header指定版本。

实现:

请求:
GET /api/orders/123
Headers: {
  Accept: application/vnd.company.order.v2+json
}

或:
GET /api/orders/123
Headers: {
  API-Version: 2
}

服务端:
@GetMapping(value = "/orders/{id}", 
            produces = "application/vnd.company.order.v1+json")
public OrderV1 getOrderV1(@PathVariable String id) {
  return orderService.getOrderV1(id);
}

@GetMapping(value = "/orders/{id}", 
            produces = "application/vnd.company.order.v2+json")
public OrderV2 getOrderV2(@PathVariable String id) {
  return orderService.getOrderV2(id);
}

优点:

  • URL不变,对客户端友好
  • 符合RESTful规范
  • 版本信息不污染URL

缺点:

  • 调试不方便(Header不可见)
  • 浏览器访问不友好
  • 实现复杂

适用场景:

  • RESTful API
  • 对URL稳定性要求高的场景

方案三:向后兼容设计(推荐)

核心思想: 不使用显式版本号,通过向后兼容的设计避免版本问题。

兼容原则:

1. 只增不删:
   ✅ 新增字段(老客户端忽略)
   ❌ 删除字段(老客户端会报错)
   
2. 字段可选:
   ✅ 新字段设为可选
   ❌ 新字段设为必填
   
3. 默认值:
   ✅ 新字段提供默认值
   ❌ 新字段没有默认值
   
4. 字段弃用:
   ✅ 保留旧字段,标记为@Deprecated
   ❌ 直接删除旧字段

实现示例:

V1版本:
{
  "orderId": "123",
  "amount": 100.00
}

V2版本(向后兼容):
{
  "orderId": "123",
  "amount": 100.00,          // 保留(兼容V1)
  "totalAmount": 100.00,      // 新增
  "discountAmount": 10.00    // 新增
}

代码:
public class Order {
  private String orderId;
  
  @Deprecated  // 标记弃用但保留
  private BigDecimal amount;
  
  private BigDecimal totalAmount;
  private BigDecimal discountAmount;
  
  // Getter/Setter
  public BigDecimal getAmount() {
    return totalAmount; // 返回新字段值(保证语义一致)
  }
}

优点:

  • 无需版本管理
  • 客户端无需修改
  • 平滑升级

缺点:

  • 字段累积(历史包袱)
  • 无法做破坏性变更
  • 需要严格遵守兼容原则

适用场景:

  • 微服务间调用
  • 小版本迭代
  • 高频发布的系统

方案对比

方案URL稳定性维护成本兼容性适用场景
URL版本★★☆☆☆★★☆☆☆★★★★★大版本升级
Header版本★★★★★★★☆☆☆★★★★★RESTful API
向后兼容★★★★★★★★★☆★★★★☆微服务

推荐方案: 对于微服务间调用,推荐向后兼容设计。对外API可使用URL版本控制

实施要点:

  1. API设计规范

    必须遵守:
    - 新增字段必须可选
    - 新增字段必须有默认值
    - 不删除现有字段
    - 不修改字段类型
    - 不修改字段语义
    
    字段弃用流程:
    1. 标记@Deprecated,文档说明
    2. 观察调用量,确认无人使用
    3. 至少保留3个月
    4. 下个大版本时删除
    
  2. Protobuf向后兼容

    message Order {
      string order_id = 1;
      double amount = 2;
      
      // V2新增字段
      double discount_amount = 3;
      string payment_method = 4;
      
      // 不要重用字段编号!
      // reserved 5; // 如果删除字段5,标记为reserved
    }
    
  3. 版本协商机制

    客户端声明支持的版本:
    Headers: {
      X-Client-Version: 2.0
      X-Compatible-Versions: 1.0,1.5,2.0
    }
    
    服务端根据客户端版本返回合适格式:
    if (clientVersion >= 2.0) {
      return OrderV2(order);
    } else {
      return OrderV1(order); // 降级返回
    }
    
  4. 版本监控

    监控指标:
    - 各版本API调用量
    - 废弃API的调用量
    - 客户端版本分布
    
    告警:
    - 废弃API仍有调用
    - 客户端版本过低
    
  5. 契约测试

    测试用例:
    1. V1客户端调用V2服务(向后兼容)
    2. V2客户端调用V1服务(新字段有默认值)
    3. 并发调用不同版本
    4. 字段缺失时的降级处理
    

延伸思考

  1. 如何处理数据库表结构的版本兼容?
  2. 如何平滑下线一个旧版本API?
  3. gRPC/Protobuf的版本兼容如何保证?


第二部分:商品与库存管理(43题)

2.1 商品中心系统(16题)

📊 题目1:设计支持多品类的SPU/SKU数据模型

问题描述: 电商平台需要支持实物商品(服装、3C)、虚拟商品(充值卡、会员)、服务类商品(保险、课程)。如何设计一个统一且可扩展的商品数据模型?

答案

问题分析: 多品类商品模型的核心挑战:

  1. 不同品类属性差异巨大(服装有尺码颜色,充值卡有卡密)
  2. 需要支持灵活的属性扩展,避免频繁加字段
  3. 查询性能要求高(详情页、列表页高并发)
  4. 需要支持类目体系和属性继承

方案一:EAV(实体-属性-值)模式

核心思想: 将商品属性拆分为独立的键值对存储。

表结构:

product(商品主表)
├── product_id
├── spu_code
├── category_id
├── name
└── status

product_attribute(属性表)
├── product_id
├── attribute_key
├── attribute_value
└── attribute_type

category_template(类目模板)
├── category_id
├── attribute_definitions(JSON)
└── validation_rules

优点:

  • 扩展性极强,加属性不需要改表结构
  • 适合属性差异大的场景
  • 灵活度高

缺点:

  • 查询性能差(需要多次JOIN)
  • 难以建立索引
  • 类型校验在应用层
  • SQL复杂

方案二:宽表+JSON扩展字段

核心思想: 核心字段固定,扩展字段用JSON存储。

表结构:

product
├── id, spu, name, category
├── common_attrs(固定字段:brand、主图等)
└── ext_attrs(JSONB:类目特有属性)

sku
├── sku_code, spu_id
├── spec_attrs(JSONB:颜色、尺码等规格)
└── ext_attrs(JSONB:其他扩展)

优点:

  • 查询性能好(单表查询)
  • PostgreSQL的JSONB支持索引
  • 平衡灵活性和性能

缺点:

  • JSON字段查询能力有限
  • 需要应用层解析和校验
  • 不同数据库支持程度不同

方案三:混合模式(推荐)

核心设计:

  1. 主表存储通用字段:product_core(id, spu, name, category, status)
  2. 类目模板定义属性规范:attribute_meta(属性元数据、类型、校验规则)
  3. 分层存储
    • product_common_attr:高频查询字段(品牌、价格区间)
    • product_ext_attr:JSONB,低频字段
    • product_spec:SKU规格,单独表
  4. 搜索侧异步构建宽表:ES文档包含所有筛选字段

数据流:

  • 写入:商品创建 → 按模板校验 → 分表存储 → 事件发布 → ES同步
  • 读取详情页:主表+扩展表(缓存)
  • 读取列表页:直接查ES
  • 后台管理:全量字段(可接受慢查询)

优点:

  • 扩展性强
  • 查询性能好
  • 支持复杂筛选(通过ES)
  • 核心字段有索引

缺点:

  • 架构复杂度中等
  • 需要维护ES同步
  • 最终一致性

方案对比

维度EAV宽表+JSON混合模式
扩展性★★★★★★★★★☆★★★★★
查询性能★★☆☆☆★★★★☆★★★★★
开发复杂度★★★☆☆★★★★☆★★★☆☆
类型安全★★☆☆☆★★★☆☆★★★★☆

推荐方案: 采用混合模式

实施要点:

  1. 核心字段晋升机制:高频查询字段从JSON移到固定列
  2. JSONB索引:PostgreSQL建立GIN索引
  3. ES映射模板:自动从类目模板生成
  4. 缓存策略:L1进程内 + L2 Redis,TTL分层设置
  5. 属性校验:类目模板定义规则,运行时校验

虚拟商品特殊处理:

  • 充值卡:卡密存储加密、核销记录独立表
  • 会员服务:有效期、权益包用JSON存储
  • 服务类:预约时间、服务人员信息扩展字段

延伸思考

  1. 如何处理类目属性变更(模板升级)?
  2. 历史订单中的商品快照如何存储?
  3. 跨类目搜索时如何统一属性映射?

🔧 题目2:商品详情页的缓存架构设计

问题描述: 商品详情页是电商系统访问量最大的页面,QPS可达百万级。请设计商品详情页的缓存架构,保证高性能和数据一致性。

答案

问题分析: 详情页缓存的核心挑战:

  1. 流量巨大,需要多级缓存
  2. 数据来源多(商品、价格、库存、营销),聚合复杂
  3. 数据更新频繁,缓存一致性难保证
  4. 热点商品流量集中

方案一:纯CDN缓存

核心思想: 详情页直接缓存在CDN,用户请求直接命中CDN。

设计:

用户 → CDN → 源站

CDN配置:
- 缓存时间:5分钟
- 缓存键:/product/{productId}
- 回源:CDN未命中时请求源站

更新策略:
- 商品信息变更 → 主动刷新CDN
- 或等待TTL过期自然更新

优点:

  • 性能极高(边缘节点响应)
  • 减轻源站压力
  • 成本低

缺点:

  • 实时性差(分钟级延迟)
  • 个性化内容难处理(如用户登录状态)
  • 价格库存等动态信息不适合

适用场景:

  • 纯静态内容(商品图文)
  • 对实时性要求不高

方案二:多级缓存(推荐)

核心思想: L1本地缓存 + L2 Redis + L3数据库。

架构:

用户 → 应用服务器
       ├→ L1: 本地缓存(Caffeine/Guava)
       ├→ L2: Redis(集中式)
       └→ L3: MySQL(源数据)

缓存策略:
L1: 热点数据,容量1000条,TTL 30秒
L2: 全量数据,TTL 5分钟
L3: 源数据

查询流程:
1. 查L1,命中返回
2. L1未命中,查L2,写入L1,返回
3. L2未命中,查L3,写入L2和L1,返回

详情页数据聚合:

详情页数据:
- 商品基本信息(商品中心)→ 缓存5分钟
- 价格信息(计价系统)→ 缓存1分钟
- 库存信息(库存系统)→ 不缓存或缓存10秒
- 营销信息(营销系统)→ 缓存1分钟
- 推荐商品(推荐系统)→ 缓存30分钟

聚合策略:
// 并行调用
Future<Product> product = getProductAsync(productId);
Future<Price> price = getPriceAsync(productId);
Future<Stock> stock = getStockAsync(productId);
Future<Promotion> promo = getPromotionAsync(productId);

// 等待所有结果
ProductDetail detail = new ProductDetail(
  product.get(500, MILLISECONDS),
  price.get(300, MILLISECONDS),
  stock.get(200, MILLISECONDS),
  promo.get(300, MILLISECONDS)
);

优点:

  • 性能好(多级缓存)
  • 灵活度高(可针对不同数据设置不同TTL)
  • 支持个性化

缺点:

  • 架构复杂度中等
  • 缓存一致性需要处理
  • 多级缓存增加运维成本

方案三:缓存+预热+旁路

核心思想: 提前预热热点数据,冷数据旁路查询。

设计:

1. 预热:
   - 大促前:提前加载热销商品
   - 运营后台:手动预热重点商品
   - 定时任务:每小时预热TOP 1000热门商品

2. 热点识别:
   - 实时统计访问频率
   - 超过阈值的商品加入热点列表
   - 热点商品缓存时间更长

3. 旁路加载:
   - 热点商品:L1+L2缓存
   - 普通商品:L2缓存
   - 长尾商品:直接查数据库

4. 缓存更新:
   - 商品信息变更 → 发布事件 → 主动失效缓存
   - 或使用版本号:缓存键包含版本号

优点:

  • 热点商品性能极高
  • 资源利用率高
  • 大促效果好

缺点:

  • 预热逻辑复杂
  • 热点识别有延迟
  • 需要实时监控

方案对比

维度纯CDN多级缓存缓存+预热
性能★★★★★★★★★☆★★★★★
实时性★★☆☆☆★★★★☆★★★★☆
个性化★☆☆☆☆★★★★★★★★★★
复杂度★★★★★★★★☆☆★★☆☆☆

推荐方案: 采用多级缓存+热点预热的组合。

实施要点:

  1. 缓存分层

    L1(本地缓存):
    - 容量:1000条
    - TTL:30秒
    - 淘汰策略:LRU
    - 适用:超热门商品(TOP 100)
    
    L2(Redis):
    - 容量:100万条
    - TTL:5分钟
    - 集群部署:主从+哨兵
    - 适用:热门+普通商品
    
    L3(数据库):
    - 全量数据
    - 读写分离
    
  2. 缓存键设计

    方案1:不带版本号
    Key: product:detail:{productId}
    Value: JSON
    更新:商品变更时主动删除key
    
    方案2:带版本号(推荐)
    Key: product:detail:{productId}:{version}
    Value: JSON
    更新:版本号+1,旧key自然过期
    
  3. 缓存更新策略

    Cache Aside模式:
    1. 读取:先查缓存,未命中再查DB,写入缓存
    2. 更新:先更新DB,再删除缓存
    
    Write Through模式:
    1. 更新:同时更新DB和缓存
    2. 读取:直接读缓存
    
  4. 热点治理

    识别热点:
    - 实时统计访问频率(滑动窗口)
    - 超过阈值(如10000 QPS)标记为热点
    
    热点处理:
    - 本地缓存延长TTL(30秒 → 5分钟)
    - Redis分片存储(product:123:1, product:123:2...)
    - 限流保护(单商品限流)
    
  5. 缓存穿透/击穿/雪崩

    穿透(查询不存在的数据):
    - 布隆过滤器预判
    - 空值缓存(TTL短,如1分钟)
    
    击穿(热点key过期):
    - 互斥锁(只有一个请求回源)
    - 热点key永不过期(后台异步更新)
    
    雪崩(大量key同时过期):
    - TTL加随机值(5分钟±30秒)
    - 缓存预热
    - 降级方案(返回旧数据)
    

延伸思考

  1. 缓存和数据库数据不一致如何处理?
  2. 如何设计缓存的监控指标?
  3. 大促时如何做缓存容量规划?

💡 题目3:如何解决商品信息变更后搜索不一致问题?

问题描述: 运营修改了商品标题和价格,但搜索结果中仍然显示旧信息。这是典型的最终一致性问题。如何设计商品到搜索的数据同步方案?

答案

问题分析: 商品搜索一致性的核心挑战:

  1. 数据变更频繁(价格调整、库存变化)
  2. 搜索索引构建有延迟
  3. 用户期望实时看到最新信息
  4. 大量商品同步对ES集群压力大

方案一:实时同步(强一致性)

核心思想: 商品信息变更时,同步更新ES索引。

设计:

1. 运营后台:修改商品信息
2. 商品服务:
   BEGIN TRANSACTION
     UPDATE products SET title=?, price=?
     // 同步更新ES
     esClient.update(productId, {title, price})
   COMMIT
3. 用户搜索:立即看到最新数据

优点:

  • 实时一致性
  • 用户体验好

缺点:

  • ES更新慢(可能超时)
  • 影响商品更新性能
  • ES故障影响商品服务

适用场景:

  • 对一致性要求极高
  • 变更频率低

方案二:异步同步(最终一致性)

核心思想: 通过消息队列异步同步,保证最终一致性。

设计:

1. 商品服务:
   BEGIN TRANSACTION
     UPDATE products SET title=?, price=?, version=version+1
     INSERT INTO outbox_events (
       event_type='ProductUpdated',
       payload={productId, title, price, version}
     )
   COMMIT

2. 事件发布器:
   扫描outbox_events → 发送到Kafka

3. 搜索同步Worker:
   监听Kafka ProductUpdated事件
   更新ES索引
   
4. 幂等处理:
   根据version判断是否需要更新
   if (event.version > es_doc.version) {
     update ES
   }

优点:

  • 解耦,不影响商品服务性能
  • ES故障不影响商品更新
  • 支持重试和补偿

缺点:

  • 最终一致性(秒级延迟)
  • 实现复杂度中等

适用场景:

  • 大部分场景
  • 可接受秒级延迟

方案三:双写+对账

核心思想: 同时写MySQL和ES,对账纠正不一致。

设计:

1. 商品服务写入:
   // 双写(并行)
   Future<Void> f1 = mysqlClient.update(...)
   Future<Void> f2 = esClient.update(...)
   
   // 等待两个都成功
   f1.get()
   f2.get()

2. 对账任务(每小时):
   - 查询MySQL最近变更的商品
   - 与ES中的数据对比
   - 发现不一致,重新同步

3. 增量同步(每分钟):
   - 基于updated_at增量同步
   - 作为对账的补充

优点:

  • 接近实时
  • 有补偿机制

缺点:

  • 双写失败处理复杂
  • 两个数据源可能不一致
  • 实现复杂

方案对比

维度实时同步异步同步双写+对账
实时性★★★★★★★★★☆★★★★☆
系统解耦★★☆☆☆★★★★★★★★☆☆
一致性保证★★★★☆★★★★★★★★★★
实施难度★★★★☆★★★☆☆★★☆☆☆

推荐方案: 采用异步同步+对账

实施要点:

  1. 事件设计

    ProductCreated:商品创建
    ProductUpdated:商品信息变更(title、desc、images)
    ProductPriceChanged:价格变更
    ProductStatusChanged:上下架
    ProductDeleted:删除
    
  2. 同步Worker设计

    消费逻辑:
    1. 从Kafka消费ProductUpdated事件
    2. 根据productId查询完整商品信息
    3. 构建ES文档
    4. 批量更新ES(bulk API,提高吞吐)
    5. 提交offset
    
    批量优化:
    - 攒批:100条或1秒批量提交
    - 去重:同一商品多次变更只保留最新
    - 合并:多个字段变更合并为一次更新
    
  3. 幂等处理

    ES文档设计:
    {
      "productId": "123",
      "title": "iPhone 15",
      "price": 5999,
      "version": 10,  // 版本号
      "updatedAt": 1679800000
    }
    
    更新逻辑:
    if (event.version > doc.version) {
      update ES
    } else {
      skip (乱序消息)
    }
    
  4. 对账机制

    对账任务(每小时):
    SELECT product_id, version, updated_at 
    FROM products 
    WHERE updated_at >= NOW() - INTERVAL 2 HOUR
    
    对每个商品:
    - 查询ES中的version
    - 如果MySQL.version > ES.version
    - 发送补偿事件到Kafka
    
  5. 监控告警

    指标:
    - 同步延迟(消息产生到ES更新完成的时间)
    - 失败率(同步失败的比例)
    - 对账差异数(MySQL和ES不一致的商品数)
    
    告警:
    - 同步延迟 > 10秒
    - 失败率 > 1%
    - 对账差异 > 100条
    

延伸思考

  1. 如果ES集群故障,搜索如何降级?
  2. 商品删除后ES索引如何处理?
  3. 大批量商品导入如何优化ES同步性能?

🔧 题目3 扩展:直接订阅 Binlog 同步 ES 的弊端是什么?如果不同变更之间存在依赖关系,应该怎么处理?

问题描述: 一些电商系统会通过 Binlog / CDC 捕获商品表变更,然后由 ES Synchronizer 消费消息并更新搜索索引。例如商品主表、SKU 表、Offer 表、类目映射表、供应商映射表发生变更后,同步服务根据表名和字段变化去更新 ES 文档。这种方式有什么弊端?如果一个 ES 文档依赖多张表,不同变更之间存在先后关系和依赖关系,应该如何设计?

答案

问题分析

直接订阅 Binlog 同步 ES 的本质是:

数据库表级变化
  → 触发 ES 文档更新

而商品搜索索引的本质通常是:

多张业务表
  → 聚合成一个商品搜索宽文档

两者粒度不一致。Binlog 看到的是“某张表某一行变了”,ES 需要的是“某个商品聚合视图应该变成什么样”。这会带来几个典型问题:

  1. 业务语义弱:Binlog 只表达 insert/update/delete,不表达 ProductPublishedProductOfflineOfferChangedRefundRuleChanged
  2. 强依赖表结构:字段新增、删除、顺序变化、JSON 结构变化,都可能影响同步逻辑。
  3. 跨表依赖复杂:一个 ES 商品文档可能依赖 item、spu、sku、offer、resource、category、stock config、refund rule 等多张表。
  4. 顺序不稳定:同一业务发布可能写多张表,Binlog 事件到达不同 consumer 时不一定按业务语义有序。
  5. 并发覆盖风险:两个表变更同时 patch 同一个 ES doc,可能出现后写基于旧 doc 覆盖前写结果。
  6. 版本语义不足:Binlog timestamp 或 position 不等价于商品业务版本,难以判断旧事件是否应该覆盖新事件。
  7. 失败补偿困难:失败消息只知道表和字段,不一定知道影响哪个商品、哪个发布版本、是否可以安全重建。

典型错误做法:按每条 Binlog 直接 patch ES

carrier_tab update
  → 查询旧 ES doc
  → 修改 carrier 基础字段
  → update ES

mapping_tab update
  → 查询旧 ES doc
  → 修改 support category / entrance
  → update ES

这种做法的问题是:两个 handler 都可能先读取旧 ES doc,再各自修改一部分字段,最后谁后写谁赢。如果后写的 doc 是基于旧版本读出来的,就可能把前一个变更覆盖掉。

方案一:继续直接 Binlog Patch

核心思想: 每张表的 Binlog handler 只更新 ES 文档中自己负责的字段。

优点:

  • 实现直观。
  • 延迟低。
  • 不需要改上游业务系统。

缺点:

  • 依赖关系散落在多个 handler 中。
  • 跨表顺序难保证。
  • 多个 handler patch 同一个 doc 时容易覆盖字段。
  • 表结构变化会影响同步逻辑。
  • 出问题后难以判断 ES doc 应该重建成什么样。

适用场景:

  • ES 文档和 DB 表几乎一对一。
  • 变更字段简单,没有跨表依赖。
  • 对一致性要求不高。

方案二:Binlog 只标记 Dirty Doc,再重建完整 ES 文档

核心思想: Binlog 不直接写 ES,而是只负责发现“哪个聚合根脏了”。

Binlog Event
  → 解析影响对象
  → mark dirty(doc_type, doc_id)
  → Index Worker 从 DB 读取最新数据
  → rebuild full ES doc
  → versioned upsert ES

例如:

product_offer_tab changed
  → affected item_id = item_80001
  → mark dirty: product_doc / item_80001

refund_rule_tab changed
  → affected item_id = item_80001
  → mark dirty: product_doc / item_80001

category_mapping_tab changed
  → affected item_id list
  → mark dirty for each item

Dirty Doc 表可以这样设计:

CREATE TABLE es_sync_dirty_doc (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    doc_type VARCHAR(64) NOT NULL,
    doc_id VARCHAR(128) NOT NULL,
    source_table VARCHAR(128) NOT NULL,
    source_event_id VARCHAR(128) DEFAULT NULL,
    source_version BIGINT DEFAULT NULL,
    status VARCHAR(32) NOT NULL DEFAULT 'PENDING'
        COMMENT 'PENDING/RUNNING/SUCCESS/FAILED/DLQ',
    retry_count INT NOT NULL DEFAULT 0,
    next_retry_at DATETIME DEFAULT NULL,
    last_error_message VARCHAR(1024) DEFAULT NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL,
    UNIQUE KEY uk_doc (doc_type, doc_id),
    KEY idx_status_retry (status, next_retry_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ES 同步脏文档队列';

同一个 doc 在短时间内多次变化,只保留一条 dirty 记录:

item update
offer update
refund rule update
  → 合并成 item_80001 的一次 rebuild

重建逻辑:

读取 item_id
  → 查询 item 最新状态
  → 查询 spu / sku / offer
  → 查询类目、资源、库存配置、履约规则、退款规则
  → 判断是否应该被索引
      是:upsert ES doc
      否:delete ES doc

优点:

  • 不依赖 Binlog 到达顺序。
  • 不会因为局部 patch 覆盖字段。
  • ES 文档构建逻辑集中。
  • 可以合并多次变更,降低 ES 写入压力。
  • 失败后可以按 doc_type + doc_id 重试和补偿。

缺点:

  • 延迟比直接 patch 略高。
  • 每次重建需要回查 DB,DB 压力更大。
  • 需要维护 dependency mapping。

适用场景:

  • ES 文档是多表聚合宽文档。
  • 商品、Offer、类目、规则之间存在依赖。
  • 搜索一致性和可恢复性比毫秒级延迟更重要。

方案三:业务事件 + Outbox + 快照重建

核心思想: 不要让 ES Synchronizer 从表级 Binlog 里猜业务含义,而是让商品发布链路明确发出业务事件。

Publish Transaction
  → 写商品正式表
  → 写 publish_version
  → 写 product_snapshot
  → 写 product_outbox_event(ProductPublished)
  → ES Synchronizer 消费 ProductPublished
  → 按 item_id + publish_version 读取快照
  → rebuild ES doc

事件示例:

{
  "event_id": "evt_20260428_000001",
  "event_type": "ProductPublished",
  "item_id": "item_80001",
  "publish_version": 4,
  "publish_id": "pub_20001",
  "snapshot_id": "snap_90001",
  "changed_fields": ["title", "offer", "refund_rule"]
}

ES 写入时带版本:

if event.publish_version < es_doc.publish_version:
    ignore
else:
    upsert

优点:

  • 业务语义清晰。
  • 下游不依赖内部表结构。
  • publish_version 可以防乱序。
  • 可以基于发布快照构建 ES,结果更稳定。
  • 排查问题时能回到一次发布动作,而不是一堆表变更。

缺点:

  • 需要上游商品中心或供给平台改造。
  • 需要设计事件契约和 Outbox。
  • 对存量 Binlog 同步系统需要渐进迁移。

适用场景:

  • 商品发布、上下架、封禁、回滚等核心业务链路。
  • 多系统依赖商品变更通知。
  • 搜索、缓存、计价上下文和营销资格消费者都需要一致理解商品版本。

方案对比

维度直接 Binlog PatchDirty Doc 重建业务事件 + Outbox
实现成本中高
业务语义
跨表依赖处理很好
防并发覆盖很好
防乱序能力
对表结构耦合
故障补偿很好
适合场景简单索引多表聚合索引核心商品发布链路

推荐方案

短期采用 Binlog → Dirty Doc Queue → Full Rebuild ES Doc,中长期演进到 业务事件 + Outbox + 商品快照重建 ES

推荐落地路径:

  1. 定义 ES doc 聚合根

    product index:
      doc_id = item_id
    
    carrier index:
      doc_id = carrier_id
    
    event index:
      doc_id = event_id
    
  2. 维护依赖映射

    product_item_tab              → item_id
    product_offer_tab             → item_id
    product_refund_rule_tab       → item_id
    resource_tab                  → affected item_id list
    supplier_product_mapping_tab  → item_id
    category_mapping_tab          → affected item_id list
    
  3. Binlog handler 只做 mark dirty

    onBinlog(table, row):
        doc_ids = resolveAffectedDocIds(table, row)
        for doc_id in doc_ids:
            upsert es_sync_dirty_doc(doc_type, doc_id)
    
  4. Index Worker 串行处理同一个 doc

    SELECT *
    FROM es_sync_dirty_doc
    WHERE status = 'PENDING'
    ORDER BY updated_at ASC
    LIMIT 100;
    

    同一个 doc_type + doc_id 通过唯一键合并,Worker 抢占后重建完整文档。

  5. 重建时读取 DB 最新状态

    buildProductDoc(item_id):
        item = query item
        offers = query offers
        rules = query fulfillment / refund rules
        if item is not indexable:
            delete ES doc
        else:
            upsert full doc
    
  6. 写 ES 带版本

    商品类索引用 publish_version;没有业务版本的对象至少使用 updated_atrebuild_seqsource_version

  7. 失败进入 DLQ 和补偿

    失败时记录:

    doc_type
    doc_id
    source_table
    error_code
    retry_count
    next_retry_at
    
  8. 定期 full sync 和对账

    DB latest hash != ES doc hash
      → mark dirty
    

    对于全量重建,建议使用新索引 + alias switch,避免重建期间影响线上查询。

面试总结

直接订阅 Binlog 同步 ES 不是不能用,而是要清楚它的边界:

Binlog 是表级数据变化,ES index 是业务聚合视图。两者粒度不一致,直接 patch ES 会在跨表依赖、事件顺序、并发覆盖、版本防乱序和失败补偿上变复杂。

更稳的设计是:

短期:
  Binlog 只负责发现哪个 doc 脏了
  Dirty Queue 合并变更
  Worker 从 DB 重建完整 ES doc

长期:
  商品发布事务写 Outbox 业务事件
  ES Synchronizer 消费 ProductPublished / ProductOffline
  按 item_id + publish_version 读取商品快照
  versioned upsert ES

这样 ES 同步消费的是“商品版本已发布”这个业务事实,而不是从一堆表级 Binlog 里猜商品到底发生了什么。

延伸思考

  1. 如何设计 resolveAffectedDocIds,避免一张配置表变更导致全量商品都被标脏?
  2. ES 写入使用 external version 有什么限制?
  3. Dirty Queue 堆积时,如何区分高优先级商品和普通商品?
  4. 全量重建和增量同步同时发生时,如何避免旧增量写到新索引?

📊 题目4:设计商品类目体系和属性管理

问题描述: 电商平台有上千个类目(如手机、服装、食品),每个类目有不同的属性(手机有内存、颜色,服装有尺码、材质)。如何设计类目体系和属性管理系统?

答案

问题分析: 类目属性管理的核心挑战:

  1. 类目层级深(最多5-6级)
  2. 属性类型多样(文本、数值、枚举、多选)
  3. 属性继承和覆盖
  4. 属性校验规则复杂

方案一:树形类目+固定属性

核心思想: 类目按树形组织,每个类目预定义固定属性。

设计:

category(类目表)
├── category_id
├── parent_id
├── name
├── level
├── path(/1/10/100/,便于查询祖先)
└── leaf(是否叶子节点)

category_attribute(类目属性定义)
├── category_id
├── attribute_id
├── required(是否必填)
└── display_order

attribute_definition(属性定义)
├── attribute_id
├── name
├── input_type(text/number/enum/multi_enum)
├── validation_rule(JSON)
└── options(枚举值)

优点:

  • 结构清晰
  • 属性定义规范
  • 易于校验

缺点:

  • 属性变更需要改表结构
  • 不够灵活
  • 类目迁移困难

方案二:动态属性模板

核心思想: 类目关联属性模板,属性模板可复用和继承。

设计:

category
├── category_id
├── parent_id
├── attribute_template_id(属性模板)
└── inherit_parent(是否继承父类目属性)

attribute_template(属性模板)
├── template_id
├── name
└── description

template_attribute(模板属性关联)
├── template_id
├── attribute_id
├── required
├── display_order
└── default_value

attribute_meta(属性元数据)
├── attribute_id
├── name
├── code(唯一标识,如"screen_size")
├── data_type(string/int/decimal/enum/boolean)
├── input_type(input/select/checkbox/radio)
├── validation_rule(JSON:min/max/regex/enum_values)
└── searchable(是否可搜索)

继承规则:

示例:手机 → 智能手机 → iPhone

手机类目(一级):
- 品牌、型号、屏幕尺寸、操作系统

智能手机(二级):
- 继承手机的所有属性
- 新增:前置摄像头、后置摄像头、电池容量

iPhone(三级):
- 继承智能手机的所有属性
- 新增:Face ID、MagSafe
- 覆盖:操作系统固定为"iOS"

优点:

  • 高度灵活
  • 支持继承和复用
  • 属性可动态添加

缺点:

  • 实现复杂
  • 继承逻辑复杂
  • 性能有一定影响

方案三:属性分组+扩展字段

核心思想: 将属性分为核心属性(固定字段)和扩展属性(JSON)。

设计:

product
├── 核心属性(固定字段):
│   brand_id, price, weight, status
└── 扩展属性(JSONB):
    ext_attrs: {
      "screen_size": "6.1英寸",
      "memory": "256GB",
      "color": "深空黑"
    }

category_attr_group(属性分组)
├── category_id
├── group_name(基本信息/规格参数/包装清单)
└── attributes(JSON数组)

优点:

  • 平衡性能和灵活性
  • 核心属性有索引
  • 扩展属性灵活

缺点:

  • JSON查询能力有限
  • 属性分组需要人工维护

方案对比

维度固定属性动态模板分组+扩展
灵活性★★☆☆☆★★★★★★★★★☆
性能★★★★★★★★☆☆★★★★☆
实施难度★★★★★★★☆☆☆★★★☆☆
可维护性★★★☆☆★★★★☆★★★★☆

推荐方案: 采用动态属性模板+继承

实施要点:

  1. 类目层级设计

    建议:不超过4级
    L1:大类(手机、服装、食品)
    L2:中类(智能手机、T恤、零食)
    L3:小类(iPhone、圆领T恤、膨化食品)
    L4:细分类(iPhone 15系列)
    
  2. 属性校验

    public void validateProduct(Product product, Category category) {
      // 1. 获取类目属性模板
      List<AttributeMeta> attrs = getAttributesByCategory(category);
      
      // 2. 检查必填属性
      for (AttributeMeta attr : attrs) {
        if (attr.isRequired() && !product.hasAttribute(attr.getCode())) {
          throw new ValidationException("缺少必填属性: " + attr.getName());
        }
      }
      
      // 3. 校验属性值
      for (ProductAttribute attr : product.getAttributes()) {
        AttributeMeta meta = getAttributeMeta(attr.getCode());
        meta.validate(attr.getValue()); // 类型、范围、枚举值校验
      }
    }
    
  3. 属性搜索支持

    ES映射自动生成:
    {
      "mappings": {
        "properties": {
          "productId": {"type": "keyword"},
          "title": {"type": "text", "analyzer": "ik_max_word"},
          "category_id": {"type": "long"},
          "brand_id": {"type": "long"},
          // 动态属性
          "attrs": {
            "type": "nested",
            "properties": {
              "code": {"type": "keyword"},
              "value": {"type": "keyword"}
            }
          }
        }
      }
    }
    
  4. 属性演进

    新增属性:
    1. 在attribute_meta表添加属性定义
    2. 关联到类目模板
    3. 存量商品渐进补齐(批量任务或人工)
    
    弃用属性:
    1. 标记为deprecated
    2. 新商品不展示该属性
    3. 老商品保留(不删除)
    
  5. 多语言支持

    attribute_i18n(属性国际化)
    ├── attribute_id
    ├── locale(zh_CN/en_US)
    ├── name
    └── description
    

延伸思考

  1. 如何处理类目合并和拆分?
  2. 属性过多时如何优化详情页加载性能?
  3. 跨类目搜索时属性如何映射?

🔧 题目5:商品图片的存储和CDN方案

问题描述: 电商平台商品图片数量巨大(百万级),每天上传图片数万张。如何设计图片存储和CDN方案,保证加载速度和成本可控?

答案

问题分析: 图片存储的核心挑战:

  1. 存储成本高(TB级数据)
  2. 访问量大(详情页、列表页都需要图片)
  3. 需要支持多种尺寸(缩略图、中图、大图)
  4. 图片上传和审核流程

方案一:自建存储+Nginx

核心思想: 图片存储在自有服务器,通过Nginx提供静态服务。

设计:

上传流程:
1. 应用服务器接收图片
2. 保存到本地磁盘:/data/images/{年}/{月}/{日}/{uuid}.jpg
3. 返回URL:http://img.example.com/2026/04/18/xxx.jpg

访问流程:
用户 → Nginx → 本地磁盘

多尺寸处理:
- 上传时生成多个尺寸
- 或使用Nginx image_filter模块动态缩放

优点:

  • 完全可控
  • 无外部依赖
  • 成本可控

缺点:

  • 带宽成本高
  • 跨地域访问慢
  • 需要自己做高可用
  • 缺少图片处理能力

方案二:对象存储OSS + CDN(推荐)

核心思想: 图片存储在云厂商对象存储,通过CDN加速访问。

设计:

上传流程:
1. 客户端 → 应用服务器申请上传凭证
2. 应用服务器 → OSS生成临时上传URL(STS)
3. 客户端 → 直传OSS
4. OSS → 回调应用服务器(上传成功)
5. 应用服务器 → 保存图片URL到数据库

访问流程:
用户 → CDN → OSS

图片处理:
URL参数控制:
- 缩放:?x-oss-process=image/resize,w_800
- 裁剪:?x-oss-process=image/crop,w_200,h_200
- 水印:?x-oss-process=image/watermark,text_xxx
- 格式转换:?x-oss-process=image/format,webp

优点:

  • 性能好(CDN加速)
  • 可靠性高(99.999999999%)
  • 图片处理能力强
  • 无需运维

缺点:

  • 成本较高(按量付费)
  • 被云厂商锁定
  • 数据外传

方案三:分层存储

核心思想: 热图片存储在SSD+CDN,冷图片存储在归档存储。

设计:

热存储(最近30天):
- OSS标准存储 + CDN
- 访问速度快
- 成本高

冷存储(30天以上):
- OSS归档存储
- 访问需要解冻(分钟级)
- 成本低(1/10)

智能分层:
- 根据访问频率自动迁移
- 热点商品图片永久在热存储

优点:

  • 成本优化
  • 性能保证

缺点:

  • 归档解冻有延迟
  • 分层逻辑复杂

方案对比

维度自建OSS+CDN分层存储
性能★★★☆☆★★★★★★★★★☆
成本★★★☆☆★★★☆☆★★★★☆
运维成本★★☆☆☆★★★★★★★★☆☆
功能丰富度★★☆☆☆★★★★★★★★★☆

推荐方案: 采用OSS+CDN

实施要点:

  1. 图片命名规范

    {bucket}/{年}/{月}/{日}/{category}/{uuid}.{ext}
    
    示例:
    product-images/2026/04/18/phone/550e8400-e29b-41d4-a716-446655440000.jpg
    
  2. 多尺寸策略

    方案A:上传时生成(推荐)
    - 上传1张原图
    - 后台异步生成:缩略图(100x100)、小图(400x400)、中图(800x800)
    - 分别存储:{uuid}_thumb.jpg, {uuid}_small.jpg, {uuid}_medium.jpg
    
    方案B:访问时生成
    - 只存储原图
    - 通过OSS图片处理参数动态生成
    - URL:{url}?x-oss-process=image/resize,w_400
    
  3. CDN配置

    缓存策略:
    - 原图:缓存7天
    - 缩略图:缓存30天
    - 回源策略:304协商缓存
    
    防盗链:
    - Referer白名单
    - 签名URL(临时访问)
    - IP黑名单
    
  4. 图片审核

    流程:
    1. 上传到临时bucket
    2. 触发审核(内容安全API)
    3. 审核通过 → 移动到正式bucket
    4. 审核不通过 → 标记为违规,删除
    
    审核内容:
    - 色情识别
    - 暴恐识别
    - 二维码识别
    - 文字OCR+敏感词
    
  5. 性能优化

    图片格式:
    - 优先WebP(体积小30%)
    - 降级JPEG/PNG(老浏览器)
    
    懒加载:
    - 首屏图片优先加载
    - 下方图片懒加载
    - 占位图优化体验
    
    压缩:
    - JPEG质量80%(肉眼无感知)
    - PNG使用TinyPNG压缩
    

延伸思考

  1. 如何防止图片盗链?
  2. 商家上传违规图片如何处理?
  3. 图片存储成本如何优化?

💡 题目6:虚拟商品vs实物商品的设计差异

问题描述: 实物商品需要物流配送,虚拟商品(如充值卡、会员)是即时发货。两者在系统设计上有哪些差异?

答案

问题分析: 虚拟商品的核心差异:

  1. 无需物流,履约方式不同
  2. 库存是卡密池,不是物理库存
  3. 发货是推送卡密,不是创建运单
  4. 支持自动发货

方案一:统一建模,类型区分

核心思想: 实物和虚拟商品共用一套模型,通过类型字段区分。

设计:

product
├── product_id
├── product_type(PHYSICAL/VIRTUAL/SERVICE)
├── fulfillment_type(LOGISTICS/INSTANT/APPOINTMENT)
└── 其他通用字段

订单履约流程:
if (product_type == PHYSICAL) {
  创建运单 → 发货 → 签收
} else if (product_type == VIRTUAL) {
  分配卡密 → 推送用户 → 确认收货
} else if (product_type == SERVICE) {
  预约 → 服务 → 评价
}

优点:

  • 模型统一,代码复用
  • 易于扩展新类型
  • 适合混合场景(一单既有实物又有虚拟)

缺点:

  • 需要大量if/else判断
  • 虚拟商品的特殊字段无法体现

方案二:拆分建模,独立系统

核心思想: 实物商品和虚拟商品拆分为两个系统。

设计:

实物商品系统:
- product, sku(标准商品模型)
- order, order_item
- logistics(物流)

虚拟商品系统:
- virtual_product(虚拟商品)
  ├── card_type(充值卡类型)
  ├── face_value(面值)
  └── validity_period(有效期)
- card_pool / inventory_code_pool_XX(卡密 / 券码池)
  ├── card_no
  ├── card_pwd
  ├── status(AVAILABLE/BOOKING/SOLD/LOCKED/EXPIRED/INVALID)
  └── order_id
- virtual_order(虚拟订单)

优点:

  • 模型清晰,职责分明
  • 可针对性优化
  • 团队独立

缺点:

  • 系统重复(订单、支付)
  • 混合订单难处理
  • 用户体验割裂

方案三:统一订单,差异化履约

核心思想: 订单系统统一,履约环节根据商品类型路由到不同履约系统。

设计:

订单系统(统一):
- 统一的订单模型
- 统一的下单流程
- 统一的支付流程

履约路由:
if (orderItem.productType == PHYSICAL) {
  route to LogisticsService
} else if (orderItem.productType == VIRTUAL) {
  route to CardDistributionService
} else if (orderItem.productType == SERVICE) {
  route to AppointmentService
}

卡密分配服务:
1. 从卡密池分配未使用的卡密
2. 绑定到订单
3. 推送给用户(短信/App)
4. 标记卡密为已分配

优点:

  • 订单模型统一
  • 支持混合订单
  • 履约解耦

缺点:

  • 履约系统复杂度增加

方案对比

维度统一建模拆分系统统一订单+差异履约
模型清晰度★★★☆☆★★★★★★★★★☆
混合订单★★★★★★★☆☆☆★★★★★
实施难度★★★★☆★★☆☆☆★★★☆☆
用户体验★★★★★★★★☆☆★★★★★

推荐方案: 采用统一订单+差异化履约

实施要点:

  1. 虚拟商品特殊字段

    virtual_product_ext
    ├── product_id
    ├── card_type(MOBILE_CHARGE/VIP_CARD/GAME_COIN)
    ├── face_value(面值)
    ├── validity_days(有效天数)
    └── auto_deliver(是否自动发货)
    
  2. 卡密池设计

    card_pool
    ├── card_id
    ├── product_id
    ├── card_no
    ├── card_pwd(加密存储)
    ├── status(AVAILABLE/LOCKED/USED/INVALID)
    ├── locked_at(预占时间)
    ├── order_id
    ├── used_at
    └── expire_at
    
    预占机制:
    1. 下单时:status=LOCKED, locked_at=NOW()
    2. 支付成功:status=USED, order_id=xxx
    3. 超时未支付:定时任务释放(status=AVAILABLE)
    
  3. 自动发货

    触发条件:
    - 支付成功事件
    - 商品类型=虚拟
    - auto_deliver=true
    
    发货流程:
    1. 从卡密池分配卡密
    2. 更新订单状态=COMPLETED
    3. 推送卡密给用户(短信/App推送)
    4. 记录发货日志
    
  4. 卡密补货

    监控:
    - 可用卡密数量 < 1000 → 告警
    
    补货:
    - 供应商批量导入
    - 或系统自动生成(如游戏币)
    
  5. 安全控制

    - 卡密加密存储(AES)
    - 卡密脱敏展示(只显示后4位)
    - 限制查询频率(防止爬虫)
    - 异常查询告警
    

延伸思考

  1. 如何防止卡密被盗刷?
  2. 卡密分配失败如何处理?
  3. 虚拟商品是否需要支持退款?

📊 题目7:商品上架流程的工作流设计

问题描述: 商品从创建到上架需要经过多个环节(信息录入、图片上传、价格设置、审核)。请设计商品上架的工作流系统。

答案

问题分析: 商品上架工作流的核心挑战:

  1. 流程长,涉及多个环节和角色
  2. 需要支持驳回和重新提交
  3. 审核规则复杂(机审+人审)
  4. 大批量商品上架性能

方案一:状态机模式

核心思想: 商品的状态流转按状态机管理。

状态定义:

DRAFT(草稿)
→ PENDING_REVIEW(待审核)
  → APPROVED(审核通过)
    → ONLINE(已上架)
    → OFFLINE(已下架)
  → REJECTED(审核拒绝)→ DRAFT(重新编辑)

状态表:

product
├── product_id
├── status(当前状态)
├── review_status(审核状态:PENDING/PASS/REJECT)
└── reject_reason

product_status_history(状态流水)
├── product_id
├── from_status
├── to_status
├── operator
├── reason
└── created_at

优点:

  • 简单直观
  • 状态清晰

缺点:

  • 复杂流程表达力不足
  • 难以支持并行审核

方案二:工作流引擎

核心思想: 使用工作流引擎(如Activiti、Camunda)编排流程。

流程定义(BPMN):

开始 → 填写基本信息 → 上传图片 → 设置价格 
    → 提交审核 → 
      [机器审核] → 通过?
        → YES → [人工审核] → 通过?
          → YES → 上架成功
          → NO → 驳回
        → NO → 驳回

工作流表:

workflow_instance(流程实例)
├── instance_id
├── business_id(product_id)
├── workflow_def_id(流程定义ID)
├── current_node(当前节点)
├── status(RUNNING/COMPLETED/TERMINATED)
└── variables(流程变量,JSON)

workflow_task(任务)
├── task_id
├── instance_id
├── assignee(处理人)
├── status(PENDING/COMPLETED)
└── completed_at

优点:

  • 流程可视化(BPMN图)
  • 支持复杂流程(并行、分支、子流程)
  • 易于调整流程

缺点:

  • 引入工作流引擎,学习成本
  • 重量级方案
  • 调试困难

方案三:轻量级流程引擎

核心思想: 自己实现简化版工作流引擎,满足基本需求。

设计:

// 流程定义(代码配置)
WorkflowDefinition productOnboard = new WorkflowDefinition()
  .addNode("FILL_INFO", new FillInfoNode())
  .addNode("UPLOAD_IMAGE", new UploadImageNode())
  .addNode("SET_PRICE", new SetPriceNode())
  .addNode("MACHINE_REVIEW", new MachineReviewNode())
  .addNode("MANUAL_REVIEW", new ManualReviewNode())
  .addTransition("FILL_INFO", "UPLOAD_IMAGE")
  .addTransition("UPLOAD_IMAGE", "SET_PRICE")
  .addTransition("SET_PRICE", "MACHINE_REVIEW")
  .addTransition("MACHINE_REVIEW", "MANUAL_REVIEW", condition="pass")
  .addTransition("MACHINE_REVIEW", "FILL_INFO", condition="reject")
  .addTransition("MANUAL_REVIEW", "ONLINE", condition="pass")
  .addTransition("MANUAL_REVIEW", "FILL_INFO", condition="reject");

// 流程执行引擎
public class WorkflowEngine {
  public void execute(String instanceId) {
    WorkflowInstance instance = getInstances(instanceId);
    Node currentNode = instance.getCurrentNode();
    
    // 执行当前节点
    NodeResult result = currentNode.execute(instance.getContext());
    
    // 根据结果流转到下一节点
    Node nextNode = getNextNode(currentNode, result);
    instance.setCurrentNode(nextNode);
    
    // 保存状态
    saveInstance(instance);
  }
}

优点:

  • 轻量级,无外部依赖
  • 代码即文档
  • 易于调试和定制

缺点:

  • 功能相对简单
  • 不支持BPMN可视化
  • 需要自己维护

方案对比

维度状态机工作流引擎轻量引擎
实施难度★★★★★★★☆☆☆★★★★☆
流程表达力★★☆☆☆★★★★★★★★★☆
维护成本★★★★☆★★★☆☆★★★★☆
适用场景简单流程复杂流程中等流程

推荐方案: 对于商品上架,推荐轻量级流程引擎

实施要点:

  1. 审核规则设计

    机器审核:
    - 图片审核(色情、暴恐)
    - 标题敏感词检测
    - 价格合理性检测(异常低价)
    - 类目属性完整性检测
    
    人工审核:
    - 机器审核不通过 → 必须人审
    - 高风险类目(药品、食品) → 必须人审
    - 新商家首批商品 → 必须人审
    - 其他商品 → 机审通过直接上架
    
  2. 批量上架优化

    单个上架:
    - 提交 → 立即审核 → 立即上架
    
    批量上架:
    - 提交100个商品
    - 异步审核(队列)
    - 审核完成后批量回调
    - 生成审核报告
    
  3. 驳回重审

    驳回原因分类:
    - 图片问题(重新上传图片即可)
    - 价格问题(重新设置价格)
    - 类目错误(重新选择类目,属性重填)
    
    重审流程:
    - 修改后自动重新提审
    - 或需要人工重新提交
    
  4. 工作流监控

    指标:
    - 待审核商品数量
    - 平均审核时长
    - 审核通过率
    - 驳回原因分布
    
    告警:
    - 待审核积压 > 1000
    - 审核通过率 < 80%
    

延伸思考

  1. 如何设计商品的定时上架功能?
  2. 批量上架如何保证事务性?
  3. 审核规则如何动态配置?

🔧 题目8:如何支持商品的多规格选择(颜色、尺码等)?

问题描述: 服装类商品有多个规格(颜色、尺码),用户需要先选择规格再下单。如何设计商品规格和SKU的选择逻辑?

答案

问题分析: 多规格选择的核心挑战:

  1. 规格组合爆炸(3个颜色×5个尺码=15个SKU)
  2. 无效组合处理(某颜色没有某尺码)
  3. 库存关联(每个SKU独立库存)
  4. 价格差异(不同规格价格不同)

方案一:预生成所有SKU

核心思想: 商品创建时生成所有可能的规格组合。

设计:

spu(商品)
├── spu_id
├── title
└── spec_definitions(规格定义)
    {
      "color": ["黑色", "白色", "蓝色"],
      "size": ["S", "M", "L", "XL"]
    }

sku(商品SKU)
├── sku_id
├── spu_id
├── spec_values(规格取值)
    {"color": "黑色", "size": "M"}
├── price
├── stock
└── status(可售/售罄/下架)

生成逻辑:
笛卡尔积:3颜色 × 4尺码 = 12个SKU

前端逻辑:

1. 用户选择颜色"黑色"
   → 查询:黑色有哪些尺码可选
   → 禁用无货尺码

2. 用户选择尺码"M"
   → 确定SKU:{color:黑色, size:M}
   → 显示价格、库存
   → 加入购物车(记录sku_id)

优点:

  • 逻辑简单
  • 查询性能好(直接查SKU表)
  • 库存价格独立管理

缺点:

  • SKU数量多(组合爆炸)
  • 无效组合浪费存储
  • 规格变更需要重新生成

方案二:动态组合

核心思想: 不预生成SKU,用户选择时动态计算。

设计:

spu表:
只存储SPU和规格定义,不生成SKU

规格库存表:
spec_stock
├── spu_id
├── spec_hash(规格组合hash)
    MD5("color:黑色,size:M")
├── stock
└── price

查询逻辑:
1. 用户选择规格 → 计算spec_hash
2. 查询spec_stock表获取库存价格
3. 下单时记录spec_hash

优点:

  • 灵活,规格可动态调整
  • 不会产生无效SKU
  • 节省存储

缺点:

  • 查询复杂(需要计算hash)
  • 订单记录不直观(spec_hash)
  • 难以支持SKU级别的运营(如促销、限购)

方案三:混合模式(主流+无效过滤)

核心思想: 预生成SKU,但只生成有效组合。

设计:

sku_constraint(无效组合)
├── spu_id
├── constraint_type(DENY/ALLOW)
├── constraint_rule(JSON)
    {"color": "黑色", "size": "XL"}  // 黑色没有XL

SKU生成逻辑:
1. 计算笛卡尔积
2. 过滤无效组合(根据constraint规则)
3. 生成有效SKU

前端逻辑:
1. 查询所有有效的规格组合
2. 根据用户已选规格,计算可选项
3. 禁用无货或无效的选项

优点:

  • 灵活性和性能兼顾
  • 支持无效组合
  • SKU数量合理

缺点:

  • 需要维护约束规则
  • 生成逻辑复杂

方案对比

维度预生成所有动态组合混合模式
SKU数量适中
查询性能★★★★★★★★☆☆★★★★☆
灵活性★★☆☆☆★★★★★★★★★☆
运营友好★★★★★★★☆☆☆★★★★☆

推荐方案: 采用混合模式(预生成+无效过滤)

实施要点:

  1. 前端规格选择组件

    逻辑:
    1. 加载所有有效SKU
    2. 构建规格树
    3. 根据已选规格,计算可选项
    4. 禁用无货或无效选项
    
    示例(用户已选"黑色"):
    可选尺码 = 筛选(所有SKU, color="黑色" && stock>0)
    禁用尺码 = 筛选(所有SKU, color="黑色" && stock=0)
    
  2. 规格约束表达

    方案A:黑名单
    "不存在黑色XL"
    
    方案B:白名单
    "只有这些组合:黑色+M, 黑色+L, 白色+S, ..."
    
    推荐:黑名单(灵活)
    
  3. SKU图片

    商品主图:展示默认规格
    规格图:每个颜色独立图片
    
    用户选择颜色 → 切换主图
    
  4. 性能优化

    缓存:
    - 缓存商品的所有SKU(减少查询)
    - 缓存规格树(减少计算)
    
    压缩:
    - 规格数据压缩传输
    

延伸思考

  1. 如何支持规格变更(新增颜色、下架尺码)?
  2. 用户加购时记录SKU还是规格组合?
  3. 如何优化规格选择的用户体验?

💡 题目9:商品快照在订单中的应用

问题描述: 用户下单后,商家可能修改商品标题、价格、图片。为了避免纠纷,需要在订单中保存商品快照。请设计商品快照方案。

答案

问题分析: 商品快照的核心挑战:

  1. 快照内容:保存哪些字段
  2. 存储成本:每个订单都存快照,数据量大
  3. 快照时机:下单时还是支付时
  4. 快照更新:商品变更后订单快照是否更新

方案一:订单表冗余字段

核心思想: 在订单明细表中冗余商品关键字段。

设计:

order_item
├── order_id
├── product_id
├── sku_id
├── product_title(快照)
├── product_image(快照)
├── price(快照)
├── quantity
└── total_amount

优点:

  • 查询方便
  • 无需JOIN

缺点:

  • 字段冗余
  • 快照内容有限
  • 表结构膨胀

方案二:独立快照表

核心思想: 商品快照存储在独立表,订单引用快照ID。

设计:

product_snapshot
├── snapshot_id
├── product_id
├── sku_id
├── snapshot_data(JSON)
    {
      "title": "iPhone 15 Pro",
      "price": 7999,
      "images": ["url1", "url2"],
      "specs": {"color": "黑色", "storage": "256GB"},
      "brand": "Apple",
      "attributes": {...}
    }
├── content_hash(MD5,去重)
├── version
└── created_at

order_item
├── order_id
├── snapshot_id(引用快照)
├── quantity
└── total_amount

快照生成时机:

时机1:用户下单时
- 优点:反映下单时的商品信息
- 缺点:未支付订单占用存储

时机2:用户支付时
- 优点:反映支付时的商品信息,更准确
- 缺点:支付时商品可能已下架

推荐:下单时生成,支付时校验

优点:

  • 快照完整(可存储任意字段)
  • 去重优化(相同快照共享)
  • 订单表轻量

缺点:

  • 需要JOIN查询
  • 存储成本高

方案三:按需快照+延迟生成

核心思想: 下单时不生成快照,只有在需要时(如退货纠纷)才生成。

设计:

order_item
├── product_id
├── sku_id
├── snapshot_id(初始为NULL)
└── snapshot_at(快照生成时间)

生成时机:
1. 用户申请退货
2. 商家纠纷
3. 定时任务(订单完成后30天生成快照)

生成逻辑:
1. 根据product_id查询当前商品信息
2. 生成快照(尽力而为)
3. 如果商品已删除,快照为空

优点:

  • 存储成本低
  • 按需生成

缺点:

  • 延迟生成可能获取不到准确信息
  • 商品删除后无法生成

方案对比

维度冗余字段独立快照表按需快照
快照完整性★★☆☆☆★★★★★★★★☆☆
存储成本★★★☆☆★★☆☆☆★★★★★
查询性能★★★★★★★★★☆★★★☆☆
准确性★★★★★★★★★★★★★☆☆

推荐方案: 采用独立快照表+去重优化

实施要点:

  1. 快照内容设计

    必须包含:
    - 商品标题、主图
    - SKU规格、价格
    - 品牌、类目
    
    可选包含:
    - 商品详情图(占用空间大)
    - 营销信息(优惠券、满减)
    - 服务承诺(七天无理由退货)
    
  2. 快照去重

    生成流程:
    1. 计算快照内容的MD5: content_hash
    2. 查询是否已存在相同hash的快照
    3. 如果存在,复用snapshot_id
    4. 如果不存在,创建新快照
    
    收益:
    - 相同商品的订单共享快照
    - 存储成本降低50%+
    
  3. 快照压缩

    JSON压缩:
    - 使用gzip压缩snapshot_data
    - 读取时解压
    
    字段裁剪:
    - 只保留关键字段
    - 详情图等大字段不保存
    
  4. 快照过期清理

    策略:
    - 订单完成后保留2年(法律要求)
    - 2年后匿名化处理(删除用户信息,保留快照)
    - 5年后归档到对象存储
    
  5. 快照版本化

    快照schema版本:
    V1: {title, price, image}
    V2: {title, price, images[], brand, specs}
    
    读取时兼容:
    if (snapshot.version == 1) {
      return convertV1ToV2(snapshot)
    }
    

延伸思考

  1. 商品快照如何支持营销信息(如“限时折扣“)?
  2. 快照生成失败如何处理?
  3. 如何设计快照的版本兼容?

📊 题目10:设计商品推荐系统的架构

问题描述: 电商平台需要在详情页、列表页、首页展示个性化推荐商品。请设计商品推荐系统的架构。

答案

问题分析: 推荐系统的核心挑战:

  1. 推荐算法复杂(协同过滤、深度学习)
  2. 实时性要求(用户行为实时影响推荐)
  3. 冷启动问题(新用户、新商品)
  4. 性能要求高(毫秒级响应)

方案一:基于规则的推荐

核心思想: 使用人工配置的规则进行推荐。

规则示例:

规则1:看了还看
- 用户浏览商品A
- 推荐:浏览过A的用户还浏览了哪些商品

规则2:相似商品
- 用户浏览iPhone 15
- 推荐:同类目、相似价格的商品

规则3:热门商品
- 推荐:该类目下销量TOP 10

规则4:运营配置
- 推荐:运营手动配置的商品(大促主推)

优点:

  • 实现简单
  • 可控性强
  • 无需算法团队

缺点:

  • 推荐效果一般
  • 不支持个性化
  • 规则难以维护

方案二:离线推荐+在线召回

核心思想: 离线计算推荐结果,在线实时召回。

架构:

离线计算(T+1):
1. 收集用户行为数据(浏览、加购、购买)
2. 训练推荐模型(协同过滤、矩阵分解)
3. 计算用户-商品推荐矩阵
4. 存储到Redis:user:123:rec → [prod1, prod2, ...]

在线召回:
1. 用户请求推荐
2. 从Redis查询预计算结果
3. 过滤下架/无货商品
4. 返回推荐列表

实时反馈:
用户点击推荐 → 记录日志 → 下次离线计算时使用

优点:

  • 支持复杂算法
  • 性能好(在线只查询)
  • 推荐效果好

缺点:

  • 实时性差(T+1)
  • 冷启动问题
  • 存储成本高

方案三:实时推荐(流式计算)

核心思想: 使用流式计算(Flink)实时更新推荐结果。

架构:

用户行为 → Kafka → Flink流式计算 → 更新Redis推荐结果

Flink计算逻辑:
1. 实时聚合用户行为(滑动窗口)
2. 更新用户画像(兴趣标签)
3. 实时计算推荐(基于规则或轻量模型)
4. 更新Redis

在线服务:
查询Redis获取实时推荐结果

优点:

  • 实时性好(秒级)
  • 支持个性化
  • 反馈快

缺点:

  • 架构复杂
  • 成本高
  • 算法受限(不能用复杂模型)

方案对比

维度规则推荐离线+在线实时推荐
推荐效果★★☆☆☆★★★★☆★★★★★
实时性★★★★★★★☆☆☆★★★★★
实施难度★★★★★★★★☆☆★★☆☆☆
成本★★★★★★★★☆☆★★☆☆☆

推荐方案: 采用离线推荐+实时规则补充的混合方案。

实施要点:

  1. 推荐场景分类

    首页推荐:
    - 个性化推荐(基于用户画像)
    - 热门推荐(兜底)
    
    详情页推荐:
    - 看了还看(基于商品相似度)
    - 买了还买(基于订单关联)
    
    购物车推荐:
    - 凑单推荐(基于购物车商品关联)
    - 优惠推荐(基于满减规则)
    
  2. 推荐召回链路

    第一层:个性化召回(离线计算)
    - 协同过滤召回
    - 内容召回(基于用户兴趣标签)
    
    第二层:规则召回(在线计算)
    - 热门商品
    - 运营配置
    
    第三层:排序
    - 点击率预估
    - 转化率预估
    - 业务规则调权(如新品扶持)
    
    第四层:过滤
    - 去重
    - 过滤下架/无货商品
    - 多样性(不全是同一类目)
    
  3. 冷启动处理

    新用户:
    - 展示热门商品
    - 根据注册信息推断兴趣(地域、年龄)
    - 引导用户选择兴趣标签
    
    新商品:
    - 基于类目和属性推荐给相关用户
    - 运营人工推送给种子用户
    - 根据早期反馈调整推荐策略
    
  4. A/B测试

    实验:
    - 对照组:规则推荐
    - 实验组:算法推荐
    
    指标:
    - 点击率(CTR)
    - 转化率(CVR)
    - 人均订单金额
    
  5. 监控指标

    业务指标:
    - 推荐位点击率
    - 推荐商品转化率
    - 推荐覆盖度(多少用户有推荐)
    
    技术指标:
    - 推荐响应时间
    - 推荐服务可用性
    - 离线计算任务成功率
    

延伸思考

  1. 如何评估推荐系统的效果?
  2. 推荐系统如何防止马太效应(热门更热,冷门更冷)?
  3. 如何保护用户隐私(不过度使用用户数据)?

🔧 题目11:商品搜索的倒排索引设计

问题描述: 搜索引擎的核心是倒排索引。请说明电商商品搜索的倒排索引如何设计,包括分词、索引结构、查询优化等。

答案

问题分析: 倒排索引的核心要点:

  1. 分词策略(中文分词难点)
  2. 索引字段选择(哪些字段需要索引)
  3. 相关性打分(如何排序)
  4. 性能优化(索引大小、查询速度)

方案一:基于Elasticsearch标准分词

核心思想: 使用ES内置的standard分词器。

配置:

{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "standard"
      }
    }
  }
}

倒排索引示例:
商品标题:"Apple iPhone 15 Pro 256GB 黑色"
分词结果:[Apple, iPhone, 15, Pro, 256GB, 黑色]

倒排索引:
Apple → [doc1, doc3, doc8]
iPhone → [doc1, doc2, doc3]
15 → [doc1, doc5]
Pro → [doc1, doc4]

优点:

  • 实现简单
  • 无需额外配置

缺点:

  • 中文分词效果差
  • 不支持同义词
  • 相关性一般

方案二:基于IK分词器(推荐)

核心思想: 使用中文分词器(IK Analyzer),支持智能分词。

配置:

{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_max_word",      // 索引时:最细粒度分词
        "search_analyzer": "ik_smart"   // 搜索时:智能分词
      },
      "brand": {
        "type": "keyword"  // 不分词
      },
      "category": {
        "type": "keyword"
      },
      "price": {
        "type": "double"
      },
      "sales": {
        "type": "long"
      }
    }
  }
}

分词示例:
标题:"小米手机13 Ultra 5G智能手机"
ik_max_word:[小米, 米手, 手机, 小米手机, 13, Ultra, 5G, 智能, 智能手机]
ik_smart:[小米, 手机, 13, Ultra, 5G, 智能手机]

优点:

  • 中文分词准确
  • 支持自定义词典
  • 搜索效果好

缺点:

  • 需要安装插件
  • 词典需要维护

方案三:多字段+权重

核心思想: 对不同字段建立索引,搜索时设置不同权重。

配置:

{
  "mappings": {
    "properties": {
      "title": {
        "type": "text",
        "analyzer": "ik_max_word",
        "boost": 3.0  // 标题权重最高
      },
      "brand": {
        "type": "keyword",
        "boost": 2.0  // 品牌权重次之
      },
      "category": {
        "type": "keyword",
        "boost": 1.5
      },
      "description": {
        "type": "text",
        "analyzer": "ik_max_word",
        "boost": 1.0  // 描述权重最低
      }
    }
  }
}

查询:
{
  "query": {
    "multi_match": {
      "query": "小米手机",
      "fields": ["title^3", "brand^2", "description"]
    }
  }
}

优点:

  • 相关性更准确
  • 可调整权重
  • 支持多字段搜索

缺点:

  • 查询复杂度增加
  • 权重调优需要经验

方案对比

维度标准分词IK分词多字段+权重
中文效果★★☆☆☆★★★★☆★★★★★
实施难度★★★★★★★★★☆★★★☆☆
相关性★★★☆☆★★★★☆★★★★★
性能★★★★☆★★★★☆★★★☆☆

推荐方案: 采用IK分词+多字段权重

实施要点:

  1. 自定义词典

    品牌词:小米、iPhone、华为
    型号词:13Ultra、15Pro、Mate60
    行业词:闪充、快充、护眼屏
    
    维护:
    - 定期更新词典
    - 新品牌/新词及时添加
    
  2. 同义词处理

    {
      "filter": {
        "synonym_filter": {
          "type": "synonym",
          "synonyms": [
            "手机,移动电话",
            "充电器,充电头",
            "iPhone,苹果手机"
          ]
        }
      }
    }
    
  3. 拼音搜索

    支持拼音搜索:
    "xiaomi" → 小米
    "pingguo" → 苹果
    
    实现:
    - 使用pinyin分词插件
    - 或维护拼音映射表
    
  4. 搜索建议(suggest)

    输入"xiao"  → 建议:[小米, 小天才, 小度]
    输入"iphone" → 建议:[iPhone 15, iPhone 14, iPhone 13]
    
    实现:
    - 使用ES的completion suggester
    - 基于前缀匹配
    
  5. 性能优化

    索引优化:
    - 只索引需要搜索的字段
    - 使用doc_values减少内存占用
    - 定期合并段(segment merge)
    
    查询优化:
    - 结果分页(from+size < 10000)
    - 深度分页用scroll或search_after
    - 热门查询结果缓存
    

延伸思考

  1. 如何实现搜索纠错(“小米手及” → “小米手机”)?
  2. 如何优化长尾查询的性能?
  3. 搜索结果如何排序(相关性、销量、价格)?

💡 题目12:如何处理商品数据的历史版本?

问题描述: 商品信息会不断变更(价格调整、标题修改、图片更换)。为了审计和纠纷处理,需要保留商品的历史版本。如何设计商品版本管理?

答案

问题分析: 商品版本管理的核心挑战:

  1. 版本数据量大(每次变更都存储)
  2. 查询历史版本(某个时间点的商品信息)
  3. 版本对比(对比两个版本的差异)
  4. 存储成本

方案一:全量版本存储

核心思想: 每次变更都保存完整的商品数据。

设计:

product(当前版本)
├── product_id
├── title
├── price
├── version(当前版本号)
└── updated_at

product_history(历史版本)
├── history_id
├── product_id
├── version
├── title
├── price
├── changed_fields(变更字段)
├── operator(操作人)
└── created_at

查询历史:

-- 查询商品在2024-01-15的版本
SELECT * FROM product_history
WHERE product_id='123' 
  AND created_at <= '2024-01-15'
ORDER BY created_at DESC
LIMIT 1

优点:

  • 查询简单
  • 可完整恢复任意版本

缺点:

  • 存储成本高(每次变更都全量存储)
  • 字段冗余

方案二:增量版本存储

核心思想: 只保存变更的字段(diff)。

设计:

product_version
├── version_id
├── product_id
├── version_no
├── changed_fields(JSON)
    {
      "title": {"old": "iPhone 14", "new": "iPhone 15"},
      "price": {"old": 5999, "new": 7999}
    }
├── operator
└── created_at

恢复历史版本:

1. 查询当前版本
2. 查询所有版本变更记录(按时间倒序)
3. 依次应用反向变更
4. 得到目标时间点的版本

优点:

  • 存储成本低
  • 可追踪变更内容

缺点:

  • 查询复杂(需要计算)
  • 版本恢复慢

方案三:混合模式(快照+增量)

核心思想: 定期保存全量快照,中间保存增量。

设计:

product_snapshot(快照,每周保存)
├── snapshot_id
├── product_id
├── snapshot_data(JSON,完整数据)
├── snapshot_version
└── created_at

product_changelog(变更日志)
├── change_id
├── product_id
├── version
├── changed_fields(JSON)
└── created_at

查询策略:
1. 找到目标时间点之前最近的快照
2. 应用快照之后的变更日志
3. 得到目标版本

优点:

  • 平衡存储和查询性能
  • 快照恢复快
  • 增量节省空间

缺点:

  • 实现复杂度中等

方案对比

维度全量版本增量版本混合模式
存储成本★★☆☆☆★★★★★★★★★☆
查询性能★★★★★★★☆☆☆★★★★☆
实施难度★★★★★★★★☆☆★★★☆☆
审计能力★★★★★★★★★★★★★★★

推荐方案: 对于电商系统,推荐混合模式

实施要点:

  1. 快照策略

    触发快照的时机:
    - 商品上架时(V1)
    - 每周日凌晨(定期快照)
    - 重大变更时(价格变动>20%)
    
  2. 变更日志记录

    public void updateProduct(Product product, ProductUpdate update) {
      Product old = getProduct(product.getId());
      
      // 1. 更新商品
      product.apply(update);
      product.setVersion(old.getVersion() + 1);
      productRepository.save(product);
      
      // 2. 记录变更日志
      ChangeLog log = new ChangeLog();
      log.setProductId(product.getId());
      log.setVersion(product.getVersion());
      log.setChangedFields(diff(old, product));  // 计算diff
      log.setOperator(getCurrentUser());
      changeLogRepository.save(log);
    }
    
  3. 版本查询API

    GET /api/products/{productId}/versions
    → 返回所有版本列表
    
    GET /api/products/{productId}/versions/{version}
    → 返回指定版本数据
    
    GET /api/products/{productId}/diff?from=10&to=12
    → 返回版本差异
    
  4. 存储优化

    - 快照使用压缩存储(gzip)
    - 超过1年的版本归档到对象存储
    - 变更日志保留2年(法律要求)
    

延伸思考

  1. 如何支持版本回滚(恢复到历史版本)?
  2. 版本数据如何支持跨表查询(如关联订单)?
  3. 大批量商品版本查询如何优化?

📊 题目13:多租户场景下的商品数据隔离

问题描述: 在B2B2C平台中,多个商家共用一套系统。如何设计商品数据的租户隔离,保证数据安全和性能?

答案

问题分析: 多租户隔离的核心挑战:

  1. 数据隔离:商家A看不到商家B的商品
  2. 性能隔离:商家A的流量不影响商家B
  3. 成本优化:共享基础设施降低成本
  4. 个性化:支持商家自定义配置

方案一:独立数据库(物理隔离)

核心思想: 每个租户独立数据库。

设计:

租户A → 数据库A → product_a, order_a
租户B → 数据库B → product_b, order_b
租户C → 数据库C → product_c, order_c

路由逻辑:
public DataSource getDataSource(String tenantId) {
  return dataSourceMap.get(tenantId);
}

优点:

  • 隔离性强(物理隔离)
  • 性能互不影响
  • 支持定制化schema
  • 数据迁移方便

缺点:

  • 成本高(每个租户一个数据库)
  • 运维复杂(管理多个数据库)
  • 跨租户查询困难

适用场景:

  • 大租户(数据量大、QPS高)
  • 对隔离要求极高

方案二:共享数据库+tenant_id字段(逻辑隔离)

核心思想: 所有租户共享一个数据库,通过tenant_id字段隔离。

设计:

product
├── product_id
├── tenant_id(租户ID)
├── title
├── price
└── ...
INDEX idx_tenant_product (tenant_id, product_id)

查询:
SELECT * FROM product 
WHERE tenant_id='tenant_001' AND product_id='123'

Row-Level Security(PostgreSQL):

CREATE POLICY tenant_isolation ON product
  USING (tenant_id = current_setting('app.current_tenant')::text);

-- 应用层设置
SET app.current_tenant = 'tenant_001';

优点:

  • 成本低(共享资源)
  • 运维简单(一个数据库)
  • 跨租户查询方便

缺点:

  • 隔离性弱(逻辑隔离)
  • 性能互相影响
  • 数据量大时性能下降
  • 误删风险(忘记加tenant_id条件)

适用场景:

  • 小租户(数据量小、QPS低)
  • 成本敏感

方案三:分库分表(混合隔离)

核心思想: 大租户独立数据库,小租户共享分片。

设计:

大租户(VIP):
tenant_001 → database_001
tenant_002 → database_002

小租户(普通):
tenant_101, tenant_102, ... → database_shared_01
tenant_201, tenant_202, ... → database_shared_02

路由策略:
if (isVIPTenant(tenantId)) {
  return getDedicatedDataSource(tenantId);
} else {
  int shardId = hash(tenantId) % 8;
  return getSharedDataSource(shardId);
}

优点:

  • 成本优化(大租户独享,小租户共享)
  • 性能隔离(大租户独立)
  • 灵活(可动态迁移)

缺点:

  • 架构复杂
  • 租户迁移成本

方案对比

维度独立数据库共享+tenant_id混合隔离
隔离性★★★★★★★☆☆☆★★★★☆
成本★★☆☆☆★★★★★★★★★☆
运维复杂度★★☆☆☆★★★★★★★★☆☆
扩展性★★★★★★★★☆☆★★★★☆

推荐方案: 采用混合隔离(分库分表)

实施要点:

  1. 租户分级

    VIP租户(月GMV>1000万):
    - 独立数据库
    - 独立Redis
    - 独立ES索引
    
    普通租户:
    - 共享分片数据库
    - 共享Redis(按tenant_id前缀隔离)
    - 共享ES索引(按tenant_id过滤)
    
  2. 数据源路由

    @Aspect
    public class TenantDataSourceAspect {
      @Around("execution(* com.example..*Repository.*(..))")
      public Object route(ProceedingJoinPoint pjp) {
        String tenantId = TenantContext.get();
        DataSource ds = getDataSource(tenantId);
        // 切换数据源
        DynamicDataSourceHolder.set(ds);
        return pjp.proceed();
      }
    }
    
  3. 租户升降级

    普通→VIP(升级):
    1. 创建独立数据库
    2. 数据迁移(双写验证)
    3. 切换路由
    4. 清理旧数据
    
    VIP→普通(降级):
    1. 迁移到共享分片
    2. 切换路由
    3. 删除独立数据库
    
  4. 安全控制

    - 强制tenant_id过滤(ORM拦截器)
    - 禁止跨租户查询
    - API鉴权(JWT包含tenant_id)
    - 审计日志(记录租户操作)
    

延伸思考

  1. 如何防止误查询跨租户数据(ORM层面)?
  2. 租户数据如何备份和恢复?
  3. 如何支持租户级别的功能开关?

🔧 题目14:商品导入的批量处理优化

问题描述: 商家需要批量导入商品(一次导入1000-10000个)。如何设计批量导入功能,保证性能和数据正确性?

答案

问题分析: 批量导入的核心挑战:

  1. 数据量大,处理时间长
  2. 需要校验每个商品(格式、必填项、业务规则)
  3. 部分成功部分失败如何处理
  4. 导入进度如何实时反馈

方案一:同步导入

核心思想: 用户上传文件,服务端同步处理,处理完返回结果。

流程:

1. 用户上传Excel/CSV文件
2. 服务端解析文件
3. 逐行校验和插入数据库
4. 返回导入结果(成功X条,失败Y条)

优点:

  • 实现简单
  • 用户立即知道结果

缺点:

  • 同步处理,用户等待时间长
  • 大文件可能超时
  • 占用服务器资源

适用场景:

  • 小批量(<1000条)
  • 对实时性要求高

方案二:异步导入+进度查询

核心思想: 用户上传文件后立即返回,后台异步处理。

流程:

1. 用户上传文件
2. 服务端:
   - 保存文件到OSS
   - 创建导入任务(状态:PENDING)
   - 返回任务ID
3. 后台Worker:
   - 异步处理导入任务
   - 更新任务进度
   - 完成后通知用户
4. 用户查询进度:
   GET /api/import-tasks/{taskId}

导入任务表:

import_task
├── task_id
├── tenant_id
├── file_url(OSS地址)
├── total_count(总数)
├── success_count(成功数)
├── fail_count(失败数)
├── status(PENDING/PROCESSING/SUCCESS/FAILED)
├── error_file_url(失败记录文件)
├── progress(进度百分比)
└── created_at

import_detail(导入明细,可选)
├── task_id
├── row_no(行号)
├── product_data(JSON)
├── status(SUCCESS/FAILED)
└── error_message

优点:

  • 用户体验好(不用等待)
  • 支持大批量
  • 不占用Web线程

缺点:

  • 实现复杂
  • 需要进度查询接口

方案三:流式导入+实时反馈

核心思想: 使用WebSocket实时推送导入进度。

流程:

1. 用户上传文件
2. 建立WebSocket连接
3. 服务端:
   - 边解析边处理
   - 每处理100条推送进度
   - 实时返回失败记录
4. 用户实时看到进度和错误

优点:

  • 实时反馈
  • 用户体验最好
  • 可随时中断

缺点:

  • 需要维护WebSocket连接
  • 实现最复杂

方案对比

维度同步导入异步导入流式导入
用户体验★★☆☆☆★★★★☆★★★★★
支持规模★★☆☆☆★★★★★★★★★☆
实施难度★★★★★★★★☆☆★★☆☆☆
实时反馈★★★★★★★☆☆☆★★★★★

推荐方案: 采用异步导入+进度查询

实施要点:

  1. 文件解析

    支持格式:
    - Excel(.xlsx)
    - CSV
    - JSON
    
    解析优化:
    - 流式解析(不一次加载全文件)
    - 分批处理(每100条一批)
    
  2. 数据校验

    校验层级:
    L1:格式校验(必填字段、字段类型)
    L2:业务校验(价格合理性、类目有效性)
    L3:关联校验(品牌是否存在、图片URL是否有效)
    
    快速失败:
    - 格式错误直接返回,不处理后续数据
    
  3. 事务处理

    方案A:全量事务
    - 全部成功才提交,任一失败全部回滚
    - 适合小批量、关联性强的数据
    
    方案B:分批事务(推荐)
    - 每100条一个事务
    - 部分失败不影响其他批次
    - 生成失败报告
    
  4. 性能优化

    - 批量INSERT(100条一次)
    - 异步同步ES(不阻塞导入)
    - 限流(防止导入占用所有资源)
    - 分时段(凌晨处理大批量)
    
  5. 失败处理

    失败记录:
    - 生成Excel文件,标注失败原因
    - 用户下载修改后重新导入
    
    部分成功:
    - 成功的商品已入库
    - 失败的记录在error_file中
    

延伸思考

  1. 如何支持导入任务的取消?
  2. 导入过程中商品数据变更如何处理?
  3. 如何设计商品导入的幂等性?

💡 题目15:商品审核流程的设计

问题描述: 商家上传的商品需要经过审核才能上架(防止违规商品)。请设计商品审核系统,包括机审和人审。

答案

问题分析: 商品审核的核心挑战:

  1. 审核效率:大量商品等待审核
  2. 审核准确性:机审误报,人审成本高
  3. 审核优先级:重点类目优先审核
  4. 申诉流程:商家对审核结果不满

方案一:纯人工审核

核心思想: 所有商品都由审核人员人工审核。

流程:

1. 商家提交商品
2. 进入审核队列
3. 审核员登录审核后台
4. 逐个审核(通过/拒绝)
5. 通过的商品上架

优点:

  • 准确性高
  • 实现简单

缺点:

  • 效率低
  • 人力成本高
  • 审核周期长

适用场景:

  • 商品量少(每天<100个)
  • 高风险类目(药品)

方案二:机审+人审(推荐)

核心思想: 机器审核过滤大部分,人工审核复杂case。

流程:

商品提交 
→ 机器审核
  → 通过(80%)→ 直接上架
  → 不确定(15%)→ 人工审核
  → 拒绝(5%)→ 直接拒绝

机器审核规则:
1. 图片审核:
   - 调用内容安全API
   - 检测色情、暴恐、二维码
   - 置信度 > 0.9 → 拒绝
   - 置信度 0.7-0.9 → 转人审
   - 置信度 < 0.7 → 通过

2. 文本审核:
   - 标题敏感词检测
   - 虚假宣传检测("最好"、"第一")
   - 医疗广告检测

3. 价格审核:
   - 异常低价(低于市场价50%)
   - 异常高价(高于市场价200%)

4. 类目审核:
   - 类目与商品不匹配
   - 必填属性缺失

人工审核:

审核任务分配:
- 按类目分配(服装审核员、3C审核员)
- 按优先级(大商家优先、付费商家优先)
- 负载均衡(平均分配)

审核操作:
- 通过:商品上架
- 拒绝:填写拒绝原因(类目错误、图片违规、价格虚高)
- 待定:标记问题,转高级审核员

优点:

  • 效率高(机审处理80%)
  • 成本可控
  • 准确性较好

缺点:

  • 需要维护审核规则
  • 机审误报需要人工校正

方案三:智能审核(AI审核)

核心思想: 使用机器学习模型进行审核。

模型训练:

训练数据:
- 正样本:审核通过的商品
- 负样本:审核拒绝的商品

特征工程:
- 文本特征:标题、描述的词频、TF-IDF
- 图片特征:图片分类、OCR文字
- 商家特征:店铺等级、历史通过率
- 类目特征:类目风险等级

模型:
- LR、GBDT、Deep Learning

输出:
- 通过概率:0.9 → 直接通过
- 拒绝概率:0.8 → 直接拒绝
- 中间态:0.5-0.8 → 人工审核

优点:

  • 准确率高(持续学习)
  • 自动化程度高
  • 可处理复杂case

缺点:

  • 需要算法团队
  • 需要大量训练数据
  • 模型维护成本高

方案对比

维度纯人审机审+人审AI审核
审核效率★★☆☆☆★★★★☆★★★★★
准确率★★★★★★★★★☆★★★★★
成本★★☆☆☆★★★★☆★★★☆☆
实施难度★★★★★★★★★☆★★☆☆☆

推荐方案: 采用机审+人审,逐步引入AI审核。

实施要点:

  1. 审核规则配置化

    审核规则表:
    review_rule
    ├── rule_id
    ├── rule_name
    ├── rule_type(IMAGE/TEXT/PRICE/CATEGORY)
    ├── rule_config(JSON)
    ├── severity(HIGH/MEDIUM/LOW)
    ├── action(REJECT/MANUAL_REVIEW/PASS)
    └── enabled
    
    示例规则:
    {
      "rule_name": "敏感词检测",
      "keywords": ["假货", "高仿", ...],
      "action": "REJECT"
    }
    
  2. 审核任务队列

    优先级队列:
    P0:付费商家、大商家
    P1:普通商家
    P2:新商家
    
    分配策略:
    - P0优先分配
    - 同优先级按提交时间
    - 负载均衡(每个审核员任务量相当)
    
  3. 审核SLA

    目标:
    - 机审:5秒内完成
    - 人审:2小时内完成(工作时间)
    
    超时告警:
    - 待审核任务积压 > 500
    - 人审超时 > 50个
    
  4. 申诉流程

    商家不满审核结果:
    1. 点击"申诉"
    2. 填写申诉理由
    3. 转高级审核员复审
    4. 复审结果通知商家
    

延伸思考

  1. 如何设计审核人员的绩效考核?
  2. 机审规则如何动态调整(根据审核质量)?
  3. 如何防止商家恶意提交违规商品?

2.2 库存系统(17题)

🔧 题目0扩展:库存是怎么创建出来的?

问题描述: 很多库存系统只讲扣减、预占和释放,但真实业务里库存首先要被创建出来。有的 SKU 只是简单数量,有的需要券码池,有的是系统自己生成券码,有的还和门店、日期、时段有关。如何设计库存创建链路?

答案

库存创建不是简单 insert stock=100,而是把商品中心的销售契约物化成库存域可扣减、可对账、可恢复的实例。推荐把库存创建做成独立命令和任务:

ProductPublished / OpsImportSubmitted / SupplierSnapshotReady
  → InventoryCreateCommand
  → inventory_create_task
  → InventoryInitWorker
  → inventory_config / inventory_balance / inventory_code_pool_XX
  → Redis 热视图预热
  → InventoryReady / InventoryCreateFailed

创建命令要表达清楚:

sku_id / offer_id
management_type:平台自管 / 供应商管理 / 无限库存
unit_type:数量 / 券码 / 时间 / 座位 / 组合
scope_type / scope_id:GLOBAL / STORE / CITY / WAREHOUSE / DATE / CHANNEL
batch_id:券码批次或货品批次
calendar_date / time_slot:日期或时段
initial_quantity:初始数量
code_source:IMPORTED / SYSTEM_GENERATED / SUPPLIER_GENERATED
idempotency_key:防重复创建

不同库存类型的创建方式不同:

类型创建方式关键点
简单数量库存创建 inventory_config 和一行 inventory_balanceINIT/INBOUND 流水,不能绕过账本直接改 stock
门店数量库存sku_id + store_id 创建库存行门店上下线要支持锁定、迁移和审计
日期 / 时段库存sku_id + store_id + date + slot 创建切片高流量品类提前物化,长尾门店懒创建
导入券码库存创建 inventory_code_batch,逐行写 inventory_code_pool_XX加密存储、哈希去重、Redis LIST 只预热 code_id
系统生成券码预生成批次,或按订单幂等生成后落库返回给用户前必须先有 MySQL 权威行
供应商库存创建供应商映射和本地快照本地快照不是最终承诺,下单前需要强刷或预订

面试时可以强调三个原则:

  1. 库存创建要任务化:商品发布事务不应该同步创建海量券码或未来 365 天日历库存,否则发布链路会被库存写放大拖垮。
  2. 库存创建要幂等:同一个发布版本、导入批次或供应商快照重复投递时,不能重复入库或重复生成券码。
  3. 库存创建要能解释来源:每一次初始化、导入、补货、系统生码都要有任务、批次和账本流水,否则后续对账只能看到“库存变了”,无法解释为什么变。

对于券码制,最容易踩坑的是把 Redis 当成码池权威。正确做法是:

导入或生成券码
  → 加密写入 inventory_code_pool_XX
  → status=AVAILABLE
  → Redis LIST 只灌入 code_id
  → 下单时弹出 code_id
  → MySQL CAS: AVAILABLE -> BOOKING

只有 MySQL 状态机更新成功,才算真正锁码成功。Redis 可以丢、可以重建,但不能成为唯一账本。

延伸思考

  1. 库存创建任务部分成功时,哪些数据可以继续保留,哪些必须回滚?
  2. 系统生成券码如何防止被猜测和批量撞库?
  3. 酒店或门店预约类库存,未来多久的日历切片应该提前物化?

🔧 题目0扩展B:库存如何和商品供给运营平台、商品生命周期联动?

问题描述: 作为一个长期做电商平台的工程师,不能只讲库存扣减。商品从供给入口进入平台、经过审核发布、上线、下架、结束销售、售后核销,库存系统应该如何和商品供给运营平台以及商品生命周期联动?

答案

核心判断是:商品发布不等于商品可售,审核通过也不等于库存 ready

三层职责要分开:

负责什么不能做什么
商品供给运营平台Draft、Staging、QC、Diff、风险审核、发布任务直接写库存余额和券码池
商品生命周期ONLINE/OFFLINE/ENDED/BANNED/ARCHIVED、销售时间、发布版本直接判断库存扣减是否成功
库存系统库存配置、数量、码池、门店 / 日期切片、预占、账本决定商品标题、类目、审核结果
营销系统活动、券、补贴、预算、营销库存、优惠规则直接改商品生命周期和库存账本
可售投影合成商品、库存、价格、营销、履约、渠道、风控状态不能替代库存权威账本

推荐链路:

供给入口 / 运营编辑 / 供应商同步
  → Draft / Staging / QC / Diff
  → Publish Transaction
      写正式商品、publish_version、交易契约、Outbox
  → InventoryCreateCommand / InventoryAdjustCommand
  → 库存任务创建或调整库存实例
  → InventoryReady / InventoryChanged / InventoryFailed
  → Marketing Command / Eligibility Event
  → Availability Projector 合成可售状态
  → 搜索、缓存、详情页、运营看板刷新

生命周期和库存动作可以这样对应:

商品生命周期动作库存系统动作可售影响
Draft / Staging只做配置校验,不创建 C 端可用库存不可见、不可售
QC 通过可以预创建库存任务,但不开放 Reserve仍不可售
Publish 成功消费 Outbox,创建 inventory_config、数量行、码池或时间切片等待 InventoryReady
ONLINE 生效若库存 ready 且未锁定,允许 Reserve可售
运营补货AdjustInventory/ImportCodeBatch/GenerateCodeBatch可售水位变化
OFFLINE 下架停止新 Reserve,保留历史预占和已售记录不可下单
ENDED 销售结束锁定剩余库存,过期未售券码,停止供应商 booking不可售,只保留售后
BANNED 风控封禁立即冻结新预占,必要时锁定码池不可售,人工处理

成熟平台通常会单独做可售投影:

Sellable =
  product_status == ONLINE
  AND now in sale_time_window
  AND inventory_status in READY/AVAILABLE
  AND price_status == READY
  AND fulfillment_status == READY
  AND channel_policy allows current channel
  AND risk_status not in BLOCKED

这样运营后台可以解释商品为什么不能卖:

商品已发布,但不可售:
- 库存创建任务失败:券码文件有重复码
- 门店 1001 未配置营业时段
- 供应商 external_sku_id 映射缺失
- 搜索索引刷新失败,等待 Outbox 重试

要避免的反模式:

  1. 供给后台直接改 stock 字段,绕过库存账本;
  2. 商品 ONLINE 后默认可卖,忽略库存、价格、履约和搜索刷新状态;
  3. 下架时删除库存行,导致历史订单、售后和券码核销不可追溯;
  4. 供应商同步直接覆盖运营手工修复的库存策略;
  5. 库存系统直接改商品生命周期,绕过审核和发布版本。

一句话总结:

供给平台治理变更,生命周期控制线上状态,库存系统提供可承诺资源,可售投影把这些状态合成用户能否下单。它们通过命令、事件、版本和幂等键协作,而不是互相直接改库。

延伸思考

  1. 商品已发布但库存初始化失败,是否允许展示“售罄”?
  2. 运营手工补货和供应商同步库存冲突时,字段主导权怎么判定?
  3. 下架后已有预占订单是否继续履约,谁来仲裁?

📊 题目1:设计防止库存超卖的方案

问题描述: 电商大促时,热门商品库存100件,但短时间涌入1000个订单。如何设计库存扣减方案,防止超卖?

答案

问题分析: 库存超卖的核心原因:

  1. 并发扣减:多个请求同时扣减库存
  2. 分布式环境:库存分散在多个节点
  3. 缓存不一致:Redis和DB库存不同步
  4. 库存回滚:订单取消后库存未释放

方案一:数据库悲观锁

核心思想: 使用数据库行锁保证原子性。

实现:

-- 查询并锁定
SELECT stock FROM inventory 
WHERE sku_id='123' 
FOR UPDATE;

-- 检查库存
if (stock >= quantity) {
  -- 扣减库存
  UPDATE inventory 
  SET stock = stock - quantity
  WHERE sku_id='123';
  
  COMMIT;
} else {
  ROLLBACK;
  throw new OutOfStockException();
}

优点:

  • 强一致性
  • 不会超卖
  • 实现简单

缺点:

  • 性能差(锁冲突)
  • 并发度低
  • 可能死锁

适用场景:

  • 并发不高(QPS<1000)
  • 小规模系统

方案二:数据库乐观锁

核心思想: 使用版本号,更新失败时重试。

实现:

-- 查询库存和版本号
SELECT stock, version FROM inventory WHERE sku_id='123';

-- 扣减库存(带版本号)
affected = UPDATE inventory 
SET stock = stock - quantity, version = version + 1
WHERE sku_id='123' AND version = {oldVersion} AND stock >= quantity;

if (affected == 0) {
  // 更新失败,重试
  retry();
}

优点:

  • 无锁,性能好
  • 不会超卖

缺点:

  • 高并发时重试多
  • 用户体验差(重试慢)

适用场景:

  • 中等并发(QPS 1000-5000)
  • 普通商品

方案三:Redis原子操作(推荐)

核心思想: 使用Redis的DECR原子操作扣减库存。

实现:

-- Lua脚本(原子执行)
local stock = redis.call('GET', KEYS[1])
if tonumber(stock) >= tonumber(ARGV[1]) then
  redis.call('DECRBY', KEYS[1], ARGV[1])
  return 1
else
  return 0
end

调用:
String key = "stock:sku:123";
Long result = redis.eval(luaScript, 
                         Collections.singletonList(key), 
                         Collections.singletonList(quantity));

if (result == 1) {
  // 扣减成功,异步同步到DB
  createOrder();
} else {
  // 库存不足
  throw new OutOfStockException();
}

异步同步DB:
定时任务(每10秒):
1. 收集Redis库存变更
2. 批量更新MySQL
3. 对账纠偏

优点:

  • 性能极高(Redis内存操作)
  • 支持高并发(10万+ QPS)
  • 不会超卖

缺点:

  • Redis和DB最终一致性
  • Redis故障风险
  • 需要对账

方案对比

方案性能一致性并发度适用场景
悲观锁★★☆☆☆强一致★★☆☆☆低并发
乐观锁★★★☆☆强一致★★★☆☆中并发
Redis原子★★★★★最终一致★★★★★高并发

推荐方案: 采用Redis原子操作+异步同步DB

实施要点:

  1. 双层库存设计

    Redis(实时库存):
    - 用于扣减判断
    - 高性能
    - 可能丢失
    
    MySQL(权威库存):
    - 定期同步Redis
    - 数据持久化
    - 对账基准
    
  2. 库存同步

    Redis → MySQL:
    - 定时任务(每10秒)
    - 批量更新(减少DB压力)
    - 增量同步(只同步变更的SKU)
    
    MySQL → Redis:
    - 商品上架时初始化Redis
    - 运营调整库存时更新Redis
    - Redis故障恢复时从MySQL加载
    
  3. 库存预热

    大促前:
    1. 识别热门商品(预测销量)
    2. 提前加载到Redis
    3. 设置永不过期
    4. 多副本(主从)
    
  4. 降级方案

    Redis故障:
    - 降级到MySQL悲观锁
    - 限流(降低并发度)
    - 提示用户(商品火爆)
    
  5. 监控告警

    指标:
    - Redis和MySQL库存差异
    - 库存扣减QPS
    - 库存不足次数
    - 超卖告警(库存为负)
    
    告警:
    - 库存差异 > 100
    - 超卖发生
    - Redis同步延迟 > 1分钟
    

延伸思考

  1. 秒杀场景如何进一步优化(如库存分段、令牌桶)?
  2. Redis故障导致库存丢失如何恢复?
  3. 如何处理订单取消后的库存回补?

🔧 题目2:如何设计分布式库存系统?

问题描述: 电商平台有多个仓库(北京、上海、深圳),商品在不同仓库有不同库存。如何设计分布式库存系统?

答案

问题分析: 分布式库存的核心挑战:

  1. 库存分布:如何在多仓库间分配库存
  2. 库存查询:如何快速查询总库存
  3. 库存分配:用户下单时选择哪个仓库发货
  4. 库存调拨:仓库间库存转移

方案一:集中式库存

核心思想: 所有仓库库存汇总到一个中心库存池。

设计:

inventory
├── sku_id
├── total_stock(总库存 = sum(所有仓库))
├── reserved_stock(预占库存)
└── available_stock(可售库存)

warehouse_inventory(仓库库存明细)
├── sku_id
├── warehouse_id
├── stock
└── reserved_stock

库存扣减:
1. 扣减total_stock(集中判断)
2. 分配仓库(路由算法)
3. 扣减warehouse_inventory

优点:

  • 逻辑简单
  • 总库存查询快
  • 不会出现“有总库存但无仓库可发“

缺点:

  • 集中式瓶颈
  • 仓库分配逻辑复杂

方案二:分布式库存(独立核算)

核心思想: 每个仓库独立管理库存,用户下单时路由到最优仓库。

设计:

warehouse_inventory
├── sku_id
├── warehouse_id
├── stock
├── reserved_stock
└── available_stock

用户下单流程:
1. 根据用户地址选择就近仓库
2. 查询该仓库库存
3. 如果有货,扣减该仓库库存
4. 如果无货,选择次近仓库

仓库路由策略:

策略1:就近原则
- 北京用户 → 北京仓
- 上海用户 → 上海仓

策略2:库存优先
- 查询所有仓库库存
- 优先选择库存最多的仓库

策略3:成本优先
- 考虑运费、配送时效
- 选择性价比最高的仓库

优点:

  • 分布式,无单点
  • 性能好
  • 仓库自治

缺点:

  • 总库存需要聚合
  • 仓库间库存不均
  • 路由策略复杂

方案三:虚拟库存池(推荐)

核心思想: 前台展示虚拟总库存,后台按规则分配实际仓库。

设计:

前台层(用户可见):
inventory_view
├── sku_id
├── total_available(虚拟总库存)
    = sum(warehouse_inventory.available_stock)

后台层(实际库存):
warehouse_inventory
├── sku_id
├── warehouse_id
├── physical_stock(实际库存)
├── reserved_stock(预占)
├── safety_stock(安全库存)
└── available_stock = physical_stock - reserved_stock - safety_stock

用户下单:
1. 检查虚拟总库存(快速判断)
2. 预占总库存(防止超卖)
3. 路由算法选择仓库
4. 扣减仓库库存
5. 如果仓库分配失败,尝试其他仓库

路由算法:

优先级:
1. 就近仓库(配送快)
2. 库存充足仓库(避免缺货)
3. 成本低仓库(运费低)

加权打分:
score = w1 * distance_score + w2 * stock_score + w3 * cost_score
选择score最高的仓库

优点:

  • 用户体验好(总库存可见)
  • 灵活分配(后台优化)
  • 支持复杂路由

缺点:

  • 实现复杂
  • 需要智能分配算法

方案对比

维度集中式分布式虚拟池
用户体验★★★★★★★★☆☆★★★★★
性能★★★☆☆★★★★★★★★★☆
库存利用率★★★★★★★★☆☆★★★★★
实施难度★★★★☆★★★★☆★★★☆☆

推荐方案: 采用虚拟库存池

实施要点:

  1. 库存聚合

    实时聚合(Redis):
    total_stock:sku:123 = 
      stock:warehouse:1:sku:123 + 
      stock:warehouse:2:sku:123 + 
      stock:warehouse:3:sku:123
    
    更新触发:
    - 仓库库存变更 → 更新总库存
    - 使用Redis Pipeline批量更新
    
  2. 仓库选择算法

    public Warehouse selectWarehouse(
      String userId, Address address, String skuId, int quantity
    ) {
      // 1. 筛选有货仓库
      List<Warehouse> candidates = warehouses.stream()
        .filter(w -> w.getStock(skuId) >= quantity)
        .collect(Collectors.toList());
      
      // 2. 计算每个仓库的得分
      return candidates.stream()
        .map(w -> new ScoredWarehouse(w, calculateScore(w, address)))
        .max(Comparator.comparing(ScoredWarehouse::getScore))
        .map(ScoredWarehouse::getWarehouse)
        .orElseThrow(OutOfStockException::new);
    }
    
    private double calculateScore(Warehouse w, Address addr) {
      double distanceScore = 1.0 / distance(w, addr);  // 距离越近越高
      double stockScore = w.getStock() / 100.0;         // 库存越多越高
      double costScore = 1.0 / w.getShippingCost();    // 成本越低越高
      
      return 0.5 * distanceScore + 0.3 * stockScore + 0.2 * costScore;
    }
    
  3. 库存预占

    预占流程:
    1. 用户下单 → 预占库存(reserved_stock +quantity)
    2. 用户支付 → 确认扣减(stock -quantity, reserved_stock -quantity)
    3. 用户取消 → 释放库存(reserved_stock -quantity)
    
    超时释放:
    - 未支付订单30分钟后自动取消
    - 定时任务扫描超时预占,自动释放
    
  4. 库存调拨

    场景:
    - 北京仓库存100,上海仓库存0
    - 上海用户下单,需要从北京调拨
    
    调拨流程:
    1. 创建调拨单
    2. 北京仓库:stock -10
    3. 运输中...
    4. 上海仓库:stock +10
    
  5. 安全库存

    设计:
    available_stock = physical_stock - reserved_stock - safety_stock
    
    作用:
    - 预留库存应对盘点误差
    - 预留库存应对损坏、丢失
    - 建议:safety_stock = physical_stock * 5%
    

延伸思考

  1. 如何设计库存预警机制(库存不足提醒)?
  2. 多仓库场景下如何最优化运费成本?
  3. 如何处理商品跨仓拆单(一单多仓发货)?

💡 题目3:大促场景下的库存预热和削峰方案

问题描述: 双11大促,预计订单量是平时的100倍。如何对库存系统进行预热和削峰,保证不超卖且性能可控?

答案

问题分析: 大促库存的核心挑战:

  1. 瞬时流量暴增(平时1000 QPS → 10万 QPS)
  2. 热点商品集中(TOP 100商品占80%流量)
  3. Redis/DB压力大
  4. 需要防止库存击穿

方案一:库存分段+令牌桶

核心思想: 将库存分为多段,每段独立扣减,最后汇总。

设计:

库存分段:
总库存10000件,分为10段:
segment_1: 1000件
segment_2: 1000件
...
segment_10: 1000件

Redis存储:
stock:sku:123:segment:1 = 1000
stock:sku:123:segment:2 = 1000
...

扣减逻辑:
1. 随机选择一个segment
2. 尝试扣减该segment库存
3. 如果成功,返回
4. 如果失败(库存不足),重试其他segment
5. 所有segment都不足,返回无货

优点:

  • 降低Redis单key热点
  • 提高并发度
  • 不会超卖

缺点:

  • 可能出现库存碎片(某段有货但其他段无货)
  • 需要定期平衡segment

方案二:本地库存+定期同步

核心思想: 将库存预分配到应用服务器本地内存,减少Redis压力。

设计:

初始化(大促前):
1. 总库存10000件
2. 分配到100台服务器
3. 每台服务器本地内存:100件

扣减流程:
1. 用户请求到服务器A
2. 扣减服务器A本地库存(内存操作,极快)
3. 本地库存不足时,向Redis申请补货
4. Redis库存不足,返回无货

补货机制:
if (local_stock < 10) {
  申请补货100件
  Redis扣减100件
  local_stock += 100
}

优点:

  • 性能极高(内存操作)
  • 减轻Redis压力
  • 支持极高并发

缺点:

  • 服务器重启库存丢失(需要归还Redis)
  • 库存分散,利用率低
  • 需要补货机制

方案三:队列削峰+异步扣减(推荐)

核心思想: 请求进队列,消费端限速扣减,流量削峰。

设计:

用户下单 
→ 请求入队(Kafka)
→ 库存扣减Worker(限速消费)
→ 扣减成功/失败
→ 通知用户(WebSocket/轮询)

限速策略:
1. 设置消费速率:5000 TPS
2. 队列堆积:允许100万消息堆积
3. 超时处理:队列中超过5分钟的请求自动取消

用户体验:
1. 提交订单立即返回"排队中"
2. 显示排队位置(前面还有XXX人)
3. 扣减成功后通知用户
4. 扣减失败(无货)通知用户

优点:

  • 削峰效果好
  • 库存系统压力可控
  • 用户体验可接受(秒杀场景)

缺点:

  • 用户等待时间长
  • 需要排队机制
  • 实现复杂

方案对比

方案性能削峰效果用户体验实施难度
库存分段★★★★☆★★★☆☆★★★★★★★★☆☆
本地库存★★★★★★★★★★★★★★★★★★☆☆
队列削峰★★★☆☆★★★★★★★★☆☆★★☆☆☆

推荐方案: 采用库存分段+本地库存的组合。

实施要点:

  1. 库存预热

    大促前3天:
    1. 识别热销商品(TOP 1000)
    2. Redis预加载:
       - 库存数据
       - 商品信息
       - 价格信息
    3. 本地缓存预加载
    4. 压测验证
    
  2. 分段策略

    分段数量 = max(库存数量 / 100, 服务器数量)
    
    示例:库存10000,服务器100台
    → 分段数 = max(10000/100, 100) = 100段
    → 每段100件
    
    优点:
    - 降低单key热度
    - 并发度=分段数
    
  3. 本地库存管理

    public class LocalInventory {
      private final ConcurrentHashMap<String, AtomicInteger> localStock;
      
      public boolean tryDeduct(String skuId, int quantity) {
        AtomicInteger stock = localStock.computeIfAbsent(
          skuId, k -> new AtomicInteger(0)
        );
        
        // 乐观尝试扣减
        int current = stock.get();
        if (current >= quantity) {
          if (stock.compareAndSet(current, current - quantity)) {
            return true;
          }
        }
        
        // 本地库存不足,申请补货
        if (requestRecharge(skuId, 100)) {
          return tryDeduct(skuId, quantity); // 重试
        }
        
        return false;
      }
    }
    
  4. 监控大盘

    实时监控:
    - 总库存水位
    - 扣减QPS
    - 成功率
    - Redis热key
    - 本地库存分布
    
    告警:
    - 库存水位 < 20%
    - 扣减失败率 > 5%
    - Redis单key QPS > 10万
    

延伸思考

  1. 秒杀开始前如何预热(避免冷启动)?
  2. 大促结束后如何回收本地库存?
  3. 如何应对恶意刷单占用库存?

📊 题目4:库存预占与释放的设计

问题描述: 用户加入购物车或进入结算页时,需要预占库存,防止其他用户抢走。但如果用户不支付,需要释放库存。如何设计库存预占机制?

答案

问题分析: 库存预占的核心挑战:

  1. 预占时机:什么时候预占(加购、结算、下单)
  2. 预占时长:预占多久(太短影响支付,太长占用库存)
  3. 超时释放:如何自动释放超时预占
  4. 并发安全:多个请求同时预占

方案一:下单时预占

核心思想: 用户下单时才预占库存,加购和结算不预占。

设计:

加购物车:不预占库存
进入结算页:不预占库存
提交订单:预占库存
  → 成功:进入支付流程
  → 失败:提示库存不足

预占超时:30分钟
支付成功:确认扣减
订单取消:释放库存

优点:

  • 库存利用率高
  • 实现简单

缺点:

  • 用户结算时可能无货(体验差)
  • 无法保证结算页的库存

适用场景:

  • 普通商品
  • 库存充足

方案二:结算时预占(推荐)

核心思想: 用户进入结算页时预占库存,支付成功确认,超时释放。

设计:

inventory
├── sku_id
├── total_stock(总库存)
├── reserved_stock(预占库存)
├── sold_stock(已售库存)
└── available_stock = total_stock - reserved_stock - sold_stock

预占记录表:
reservation
├── reservation_id
├── sku_id
├── order_id
├── quantity
├── status(RESERVED/CONFIRMED/RELEASED)
├── expire_at(过期时间)
└── created_at

流程:

1. 进入结算页:
   BEGIN TRANSACTION
     UPDATE inventory 
     SET reserved_stock = reserved_stock + quantity
     WHERE sku_id=? AND available_stock >= quantity;
     
     INSERT INTO reservation (sku_id, order_id, quantity, expire_at)
     VALUES (?, ?, ?, NOW() + INTERVAL 15 MINUTE);
   COMMIT

2. 支付成功:
   UPDATE inventory 
   SET reserved_stock = reserved_stock - quantity,
       sold_stock = sold_stock + quantity
   WHERE sku_id=?;
   
   UPDATE reservation SET status='CONFIRMED' WHERE reservation_id=?;

3. 超时释放(定时任务):
   SELECT * FROM reservation 
   WHERE status='RESERVED' AND expire_at < NOW();
   
   For each expired:
     UPDATE inventory 
     SET reserved_stock = reserved_stock - quantity;
     
     UPDATE reservation SET status='RELEASED';

优点:

  • 保证结算页库存
  • 用户体验好
  • 防止超卖

缺点:

  • 预占时间内库存被占用
  • 需要定时任务释放

方案三:分级预占

核心思想: 根据用户等级和商品类型,设置不同的预占时长。

设计:

预占时长策略:
VIP用户:30分钟
普通用户:15分钟
新用户:10分钟

热门商品:10分钟(快速流转)
普通商品:15分钟
冷门商品:30分钟(不占用热门商品库存)

动态调整:
if (available_stock < 10% * total_stock) {
  // 库存紧张,缩短预占时间
  expire_time = 5分钟
} else {
  expire_time = 15分钟
}

优点:

  • 差异化服务
  • 库存利用率高
  • 灵活调整

缺点:

  • 规则复杂
  • 实现成本高

方案对比

方案用户体验库存利用率超卖风险实施难度
下单预占★★★☆☆★★★★★★★★☆☆★★★★★
结算预占★★★★★★★★★☆★★★★★★★★☆☆
分级预占★★★★★★★★★★★★★★★★★☆☆☆

推荐方案: 采用结算时预占

实施要点:

  1. 预占时长设置

    考虑因素:
    - 支付流程耗时(通常2-3分钟)
    - 用户犹豫时间(5-10分钟)
    - 库存周转率(紧俏商品缩短)
    
    建议:
    - 默认15分钟
    - 库存<10%时缩短到5分钟
    - VIP用户延长到30分钟
    
  2. 预占幂等性

    使用order_id作为幂等键:
    INSERT INTO reservation (reservation_id, order_id, ...)
    ON DUPLICATE KEY UPDATE updated_at=NOW();
    
    防止重复预占:
    - 同一订单多次预占,使用相同reservation记录
    - 延长expire_at即可
    
  3. 超时释放优化

    方案A:定时任务扫描
    - 每分钟扫描一次
    - 查询expire_at < NOW()
    - 批量释放
    
    方案B:延迟队列(推荐)
    - 预占时发送延迟消息(延迟15分钟)
    - 消息到期时检查状态
    - 如果未支付,释放库存
    
    优点:精确释放,无需轮询
    
  4. 库存保护

    最大预占比例:
    - 允许预占库存 <= total_stock * 90%
    - 保留10%库存应对预占释放后的瞬时需求
    
    预占限流:
    - 单用户最多预占5个订单
    - 单商品最多被预占total_stock * 80%
    

延伸思考

  1. 用户在结算页停留很久不支付,如何处理?
  2. 预占释放后其他用户如何得知库存恢复?
  3. 如何设计库存预占的监控指标?

🔧 题目5:如何设计库存的分级管理(前台可售vs仓库实际)?

问题描述: 仓库实际库存100件,但前台可售库存只有80件(预留20件应对售后、损耗)。如何设计库存的分级管理?

答案

问题分析: 库存分级的核心挑战:

  1. 不同层级库存含义不同
  2. 层级间库存同步
  3. 安全库存设置
  4. 库存占用追踪

方案一:单一库存(简化版)

核心思想: 只维护一个库存字段,不区分层级。

设计:

inventory
├── sku_id
├── stock(唯一库存字段)
└── reserved_stock(预占)

优点:

  • 实现简单
  • 无需同步

缺点:

  • 无法预留安全库存
  • 无法应对损耗

方案二:多级库存(推荐)

核心思想: 区分物理库存、可售库存、预占库存、已售库存。

设计:

inventory
├── sku_id
├── physical_stock(物理库存,仓库实际数量)
├── reserved_stock(预占库存,待支付订单)
├── sold_stock(已售库存,已支付待发货)
├── safety_stock(安全库存,预留)
├── available_stock(可售库存,计算得出)
    = physical_stock - reserved_stock - sold_stock - safety_stock
└── version

库存关系:
physical_stock(100)
  - safety_stock(10,安全库存)
  - sold_stock(20,已售待发货)
  - reserved_stock(15,预占待支付)
  = available_stock(55,可售)

库存流转:

用户下单:
available_stock -10, reserved_stock +10

用户支付:
reserved_stock -10, sold_stock +10

商品发货:
sold_stock -10, physical_stock -10

订单取消:
reserved_stock -10, available_stock +10

售后退货:
physical_stock +10, available_stock +10

优点:

  • 库存含义清晰
  • 支持安全库存
  • 易于追踪

缺点:

  • 字段多,维护成本高
  • 同步逻辑复杂

方案三:占用日志模式

核心思想: 只维护物理库存,所有占用记录在日志表。

设计:

inventory
├── sku_id
└── physical_stock

inventory_occupation(库存占用日志)
├── occupation_id
├── sku_id
├── occupation_type(RESERVED/SOLD/SAFETY)
├── quantity
├── reference_id(order_id/warehouse_id)
├── status(ACTIVE/RELEASED)
└── expire_at

可售库存计算:
available_stock = physical_stock - sum(active_occupations)

优点:

  • 灵活,支持多种占用类型
  • 可追溯所有占用历史
  • 易于扩展

缺点:

  • 查询需要聚合计算
  • 性能较差

方案对比

维度单一库存多级库存占用日志
清晰度★★☆☆☆★★★★★★★★★☆
性能★★★★★★★★★☆★★★☆☆
灵活性★★☆☆☆★★★☆☆★★★★★
实施难度★★★★★★★★☆☆★★☆☆☆

推荐方案: 采用多级库存

实施要点:

  1. 安全库存设置

    策略:
    - 标准:safety_stock = 5% * physical_stock
    - 易损商品:safety_stock = 10% * physical_stock
    - 高价商品:safety_stock = 2% * physical_stock
    
    动态调整:
    - 根据历史损耗率调整
    - 旺季增加,淡季减少
    
  2. 库存同步检查

    不变量检查:
    physical_stock = 
      available_stock + 
      reserved_stock + 
      sold_stock + 
      safety_stock
    
    定期对账:
    如果不等式不成立,说明库存有问题
    
  3. 库存调整接口

    运营调整物理库存:
    adjustPhysicalStock(skuId, delta, reason)
    
    自动调整安全库存:
    adjustSafetyStock(skuId, percentage)
    
  4. 库存报表

    库存健康度:
    - 库存周转率 = 销量 / 平均库存
    - 滞销率 = 30天未售商品数 / 总商品数
    - 缺货率 = 用户下单失败次数 / 总下单次数
    

延伸思考

  1. 如何设计库存盘点功能(盘点期间库存锁定)?
  2. 安全库存不足时如何处理?
  3. 已售库存发货后如何核减?

💡 题目6:库存扣减失败的补偿机制

问题描述: 在订单创建流程中,扣减库存可能失败(并发冲突、网络超时、服务故障)。如何设计补偿机制,保证数据一致性?

答案

问题分析: 库存扣减失败的核心场景:

  1. 网络超时:不知道是否扣减成功
  2. 服务故障:库存服务不可用
  3. 并发冲突:乐观锁更新失败
  4. 数据不一致:订单已创建但库存未扣减

方案一:同步重试

核心思想: 扣减失败时立即重试,最多重试3次。

实现:

public void deductInventory(String skuId, int quantity) {
  int maxRetries = 3;
  for (int i = 0; i < maxRetries; i++) {
    try {
      inventoryService.deduct(skuId, quantity);
      return; // 成功
    } catch (ConcurrentModificationException e) {
      if (i == maxRetries - 1) {
        throw e; // 最后一次重试失败,抛出异常
      }
      Thread.sleep(100 * (i + 1)); // 指数退避
    }
  }
}

优点:

  • 实现简单
  • 实时性好

缺点:

  • 重试占用用户等待时间
  • 多次重试可能仍失败
  • 影响用户体验

方案二:异步补偿

核心思想: 扣减失败时订单标记为待处理,后台异步补偿。

流程:

1. 订单创建:
   if (扣减库存失败) {
     订单状态 = PENDING_INVENTORY
     记录补偿任务
   }

2. 补偿Worker:
   定时扫描PENDING_INVENTORY订单
   重试扣减库存
   成功 → 更新订单状态CONFIRMED
   失败 → 继续重试或人工介入

3. 补偿任务表:
   compensation_task
   ├── task_id
   ├── order_id
   ├── task_type(DEDUCT_INVENTORY)
   ├── payload(JSON)
   ├── status(PENDING/SUCCESS/FAILED)
   ├── retry_count
   └── next_retry_at

优点:

  • 不阻塞用户
  • 支持多次重试
  • 可人工介入

缺点:

  • 最终一致性
  • 用户可能看到“处理中“状态
  • 实现复杂

方案三:补偿+对账(推荐)

核心思想: 结合同步重试和异步补偿,再加对账兜底。

流程:

1. 扣减库存:
   try {
     inventoryService.deduct(skuId, quantity);
   } catch (Exception e) {
     // 同步重试1次
     retry once
     if (still failed) {
       // 记录补偿任务
       compensationService.record(orderId, "DEDUCT_INVENTORY");
     }
   }

2. 补偿Worker(每分钟):
   查询补偿任务
   重试执行
   成功 → 标记完成
   失败 → retry_count +1

3. 对账任务(每小时):
   查询已支付订单
   检查库存是否已扣减
   未扣减 → 创建补偿任务

4. 人工兜底:
   - retry_count > 5次仍失败
   - 转人工处理
   - 排查根本原因

优点:

  • 多层保障
  • 可靠性高
  • 覆盖各种异常

缺点:

  • 实现最复杂

方案对比

方案实时性可靠性用户体验实施难度
同步重试★★★★★★★★☆☆★★★☆☆★★★★★
异步补偿★★★☆☆★★★★☆★★★★☆★★★☆☆
补偿+对账★★★☆☆★★★★★★★★★☆★★☆☆☆

推荐方案: 采用补偿+对账

实施要点:

  1. 幂等性保证

    public void deductInventory(DeductRequest req) {
      // 使用orderId作为幂等键
      if (isAlreadyDeducted(req.getOrderId())) {
        return; // 已扣减,直接返回
      }
      
      // 执行扣减
      doDeduct(req);
      
      // 记录已扣减
      markDeducted(req.getOrderId());
    }
    
  2. 补偿任务重试策略

    指数退避:
    第1次:立即重试
    第2次:1分钟后
    第3次:5分钟后
    第4次:15分钟后
    第5次:1小时后
    
    超过5次 → 转人工
    
  3. 补偿任务优先级

    P0:已支付订单(优先处理)
    P1:待支付订单
    P2:其他
    
  4. 对账规则

    检查项:
    1. 订单状态=PAID → 库存必须已扣减
    2. 订单金额 = 商品价格 × 数量
    3. 库存不能为负数
    
    差异处理:
    - 自动补偿(低风险)
    - 人工介入(高风险)
    

延伸思考

  1. 如果补偿重试多次仍失败,如何处理?
  2. 补偿过程中订单状态如何展示给用户?
  3. 如何监控补偿任务的执行情况?

📊 题目7:设计库存盘点系统

问题描述: 仓库需要定期盘点库存,核对系统库存和实际库存是否一致。如何设计库存盘点系统?

答案

问题分析: 库存盘点的核心挑战:

  1. 盘点期间如何处理库存变更
  2. 盘点差异如何调整
  3. 大规模商品盘点效率
  4. 盘点结果审核

方案一:冻结盘点

核心思想: 盘点期间冻结库存,禁止出入库。

流程:

1. 创建盘点任务:
   - 选择仓库
   - 选择商品范围(全部/部分)
   - 冻结库存(禁止扣减和补货)

2. 仓库人员盘点:
   - 扫描商品条码
   - 录入实际数量

3. 生成盘点报告:
   - 系统库存 vs 实际库存
   - 差异清单

4. 审核调整:
   - 审核员确认差异
   - 调整系统库存
   - 解冻库存

优点:

  • 准确性高
  • 实现简单

缺点:

  • 盘点期间影响业务
  • 效率低
  • 用户体验差

方案二:动态盘点(推荐)

核心思想: 盘点期间不冻结,记录盘点时间段的出入库,最后计算差异。

流程:

1. 开始盘点:
   记录盘点开始时间T1
   快照当前系统库存S1

2. 盘点期间:
   正常出入库
   记录所有库存变更日志

3. 结束盘点:
   记录盘点结束时间T2
   记录实际库存数量P

4. 计算差异:
   期间出库:delta_out = sum(T1到T2的出库)
   期间入库:delta_in = sum(T1到T2的入库)
   
   理论库存:S2 = S1 - delta_out + delta_in
   实际库存:P
   差异:diff = P - S2

5. 调整库存:
   if (diff != 0) {
     inventory.physical_stock += diff
     记录盘点调整日志
   }

优点:

  • 不影响业务
  • 准确性高
  • 可并行盘点

缺点:

  • 计算复杂
  • 需要完整的出入库日志

方案三:循环盘点

核心思想: 不是一次性盘点所有商品,而是每天盘点一部分。

流程:

将商品分为ABC类:
A类(高价值,20%):每月盘点
B类(中价值,30%):每季度盘点
C类(低价值,50%):每年盘点

每日盘点:
1. 系统自动生成今日盘点任务
2. 仓库人员按任务盘点
3. 异常差异及时调整
4. 正常差异汇总报告

优点:

  • 分散盘点,效率高
  • 重点商品关注度高
  • 不影响业务

缺点:

  • 需要分类管理
  • 全盘点周期长

方案对比

方案对业务影响准确性效率适用场景
冻结盘点★★☆☆☆★★★★★★★☆☆☆小仓库
动态盘点★★★★★★★★★★★★★★☆大仓库
循环盘点★★★★★★★★★☆★★★★★商品多

推荐方案: 采用动态盘点+循环盘点的组合。

实施要点:

  1. 盘点任务生成

    创建盘点单:
    inventory_check
    ├── check_id
    ├── warehouse_id
    ├── check_type(FULL/PARTIAL/CYCLE)
    ├── status(PENDING/CHECKING/COMPLETED)
    ├── start_snapshot_id(开始时库存快照)
    ├── start_at
    ├── end_at
    └── operator
    
    盘点明细:
    check_detail
    ├── check_id
    ├── sku_id
    ├── system_stock(系统库存)
    ├── actual_stock(实际库存)
    ├── diff(差异)
    ├── reason(差异原因)
    └── adjusted(是否已调整)
    
  2. 盘点APP设计

    功能:
    - 扫码盘点(扫条码自动录入)
    - 语音录入(解放双手)
    - 拍照记录(有问题的商品拍照)
    - 离线模式(网络不好时)
    
    优化:
    - 按货架号排序(减少走动)
    - 实时同步(避免数据丢失)
    
  3. 差异分析

    差异原因分类:
    - 损耗(DAMAGE):商品破损
    - 丢失(LOSS):商品丢失
    - 错发(WRONG_SHIP):发错货
    - 漏记(MISSING_RECORD):出入库漏记
    - 系统bug(SYSTEM_ERROR)
    
    自动调整规则:
    - diff < 5% → 自动调整
    - diff >= 5% → 需要审核
    - diff > 20% → 必须复盘(可能系统bug)
    
  4. 盘点报告

    报告内容:
    - 盘点汇总:总商品数、差异数、差异金额
    - 差异TOP 10:差异最大的商品
    - 差异原因分布:损耗X件、丢失Y件
    - 仓库对比:各仓库差异率
    

延伸思考

  1. 如何设计盘点的权限控制(防止作弊)?
  2. 盘点差异过大时如何追责?
  3. 如何设计移动盘点的离线模式?

🔧 题目8:如何处理库存的并发更新?

问题描述: 多个订单同时扣减同一商品库存,如何处理并发冲突,保证库存不超卖?

答案

问题分析: 并发更新的核心场景:

  1. 秒杀场景:1万人抢100件商品
  2. 正常场景:多个用户同时下单
  3. 分布式场景:多个服务器同时扣减

方案一:数据库行锁(FOR UPDATE)

实现:

BEGIN TRANSACTION;

-- 锁定行
SELECT stock FROM inventory 
WHERE sku_id='123' FOR UPDATE;

-- 检查库存
if (stock >= quantity) {
  UPDATE inventory SET stock = stock - quantity;
  COMMIT;
} else {
  ROLLBACK;
}

优点:

  • 强一致性
  • 不会超卖

缺点:

  • 锁冲突,性能差
  • 并发度低
  • 长事务风险

吞吐量:约1000 TPS

方案二:乐观锁(CAS)

实现:

-- 查询当前库存
SELECT stock, version FROM inventory WHERE sku_id='123';

-- 尝试更新(CAS)
affected = UPDATE inventory 
SET stock = stock - quantity, version = version + 1
WHERE sku_id='123' 
  AND version = oldVersion 
  AND stock >= quantity;

if (affected == 0) {
  // 更新失败,重试
  retry with exponential backoff
}

优点:

  • 无锁,性能好
  • 并发度高

缺点:

  • 高并发时重试多,成功率低
  • 可能饿死(一直重试失败)

吞吐量:约5000-10000 TPS

方案三:Redis+Lua脚本(推荐)

实现:

-- Lua脚本(Redis原子执行)
local stock_key = KEYS[1]
local quantity = tonumber(ARGV[1])

local stock = tonumber(redis.call('GET', stock_key) or "0")

if stock >= quantity then
  redis.call('DECRBY', stock_key, quantity)
  return 1
else
  return 0
end

调用:

String key = "inventory:sku:123";
Long result = redis.eval(luaScript, 
                         Arrays.asList(key), 
                         Arrays.asList(String.valueOf(quantity)));

if (result == 1) {
  // 扣减成功
  createOrder();
  // 异步同步到MySQL
  asyncSyncToMySQL(skuId, -quantity);
} else {
  throw new OutOfStockException();
}

优点:

  • 性能极高(内存操作)
  • 原子性(Lua脚本)
  • 支持极高并发

缺点:

  • Redis和MySQL最终一致
  • Redis故障风险
  • 需要对账机制

吞吐量:约10万+ TPS

方案对比

方案TPS超卖风险一致性复杂度
行锁1K强一致★★★★☆
乐观锁5-10K强一致★★★☆☆
Redis+Lua100K+最终一致★★★☆☆

推荐方案: 根据场景选择:

  • 普通商品:乐观锁(MySQL)
  • 秒杀商品:Redis+Lua
  • 低并发:悲观锁

实施要点:

  1. Redis高可用

    - Redis主从+哨兵
    - 双机房部署
    - 持久化:AOF every second
    
  2. 库存同步

    Redis → MySQL:
    - 定时任务(每10秒)
    - 批量更新(减少DB压力)
    - 对账纠偏(每小时)
    
  3. 降级方案

    Redis故障 → 降级到MySQL乐观锁
    MySQL故障 → 停止扣减,返回系统繁忙
    
  4. 监控

    - Redis和MySQL库存差异
    - 扣减成功率
    - 扣减耗时P99
    - 并发冲突次数
    

延伸思考

  1. 如何设计秒杀的库存扣减(更极端的高并发)?
  2. 分库分表场景下如何扣减库存?
  3. Redis和MySQL数据不一致如何恢复?

💡 题目9:虚拟库存vs实物库存的差异

问题描述: 实物商品有物理库存限制,虚拟商品(如充值卡、游戏币)可以无限生成。两者在库存设计上有什么差异?

答案

问题分析: 虚拟库存的核心特点:

  1. 可按需生成(理论无限)
  2. 实际受限于供应商配额
  3. 卡密池管理(有卡密才能售卖)
  4. 即时发货(无需物流)

方案一:无限库存模式

核心思想: 虚拟商品库存设为无限大,不限制购买。

设计:

product
├── product_id
├── product_type(PHYSICAL/VIRTUAL)
└── unlimited_stock(布尔,是否无限库存)

扣减逻辑:
if (product.unlimitedStock) {
  // 虚拟商品,不扣减库存
  return true;
} else {
  // 实物商品,正常扣减
  return deductStock(skuId, quantity);
}

优点:

  • 实现最简单
  • 用户体验好(永不缺货)

缺点:

  • 不适合卡密类商品(卡密有限)
  • 无法控制销售节奏
  • 可能超过供应商配额

适用场景:

  • 可按需生成的虚拟商品(游戏币、积分)

方案二:卡密 / 券码池模式(推荐)

核心思想: 维护卡密 / 券码池,库存=可用卡密或券码数量。

设计:

virtual_product
├── product_id
├── supplier_id(供应商)
├── card_type(充值卡类型)
└── face_value(面值)

card_pool(卡密 / 券码池)
├── card_id
├── product_id
├── card_no(卡号)
├── card_pwd(密码,加密存储)
├── status(AVAILABLE/BOOKING/SOLD/LOCKED/EXPIRED/INVALID)
├── booked_at
├── reservation_id / order_id
├── sold_at
└── sold_order_id

库存计算:
available_stock = COUNT(*) WHERE status='AVAILABLE'
reserved_stock = COUNT(*) WHERE status='BOOKING'

生产级设计里,不建议只用一张简单 card_pool 表,更推荐把库存域的券码池收敛成 inventory_code_pool_XX 分表:

inventory_code_pool_XX
├── code_id(全局唯一,Redis LIST 只缓存这个 ID)
├── batch_id / inventory_key / sku_id(批次、库存项、SKU)
├── code_cipher(加密后的券码或卡密)
├── code_hash(去重和排查,不保存明文)
├── status(AVAILABLE/BOOKING/SOLD/LOCKED/EXPIRED/INVALID)
├── reservation_id / order_id / user_id
├── booked_at / sold_at / expire_at
└── version(CAS 与幂等控制)

面试时要特别强调:Redis LIST 不是权威库存,只是 code_id 热队列。下单时可以先从 Redis 弹出 code_id,但必须再执行 MySQL CAS:

UPDATE inventory_code_pool_XX
SET status='BOOKING',
    reservation_id=?,
    order_id=?,
    booked_at=NOW(),
    version=version+1
WHERE code_id=? AND status='AVAILABLE';

只有这条更新成功,才算真正锁码成功。支付成功后 BOOKING -> SOLD;订单取消或超时后 BOOKING -> AVAILABLE,再通过 Outbox 或补偿任务把 code_id 回填到 Redis。已经交付或核销链路可见的 SOLD 码,不应直接回到可售池,退款要走售后和履约规则。

这个设计的价值是:

  • 防止 Redis 丢数据导致无法追溯;
  • 避免 LIST 存明文券码造成泄漏;
  • 用状态机防止重复发码和并发超卖;
  • Redis 故障后可以从 MySQL AVAILABLE 状态重建热队列;
  • 对账时能按订单、批次、供应商和码状态逐行追踪。

库存流转:

用户下单:
1. SELECT * FROM card_pool 
   WHERE product_id=? AND status='AVAILABLE' 
   LIMIT 1 FOR UPDATE;
   
2. UPDATE card_pool 
   SET status='BOOKING', booked_at=NOW(), order_id=?
   WHERE card_id=?;

用户支付:
UPDATE card_pool 
SET status='SOLD', sold_at=NOW(), sold_order_id=?
WHERE card_id=? AND status='BOOKING';

订单取消:
UPDATE card_pool 
SET status='AVAILABLE', order_id=NULL
WHERE card_id=? AND status='BOOKING';

优点:

  • 库存真实(有卡密才能售)
  • 支持卡密管理
  • 防止超卖

缺点:

  • 需要维护卡密池
  • 卡密补货

方案三:配额模式

核心思想: 供应商给定配额,按配额售卖。

设计:

supplier_quota(供应商配额)
├── supplier_id
├── product_id
├── total_quota(总配额)
├── used_quota(已使用)
├── remaining_quota(剩余)
└── validity_period(有效期)

扣减逻辑:
1. 检查剩余配额
2. 扣减配额
3. 订单成功后,向供应商申请实际卡密
4. 发货给用户

优点:

  • 无需提前准备卡密
  • 按需申请
  • 库存灵活

缺点:

  • 实时性依赖供应商
  • 供应商故障风险

方案对比

方案准确性供应商依赖实施难度适用场景
无限库存★★☆☆☆★★★★★★★★★★可生成虚拟品
卡密池★★★★★★★★☆☆★★★☆☆充值卡、券码
配额模式★★★★☆★★☆☆☆★★★☆☆供应商直连

推荐方案: 根据虚拟商品类型选择:

  • 可生成(游戏币、积分):无限库存
  • 卡密类(充值卡、激活码):卡密池
  • 供应商直连(机票、酒店):配额模式

实施要点:

  1. 卡密安全

    - 卡密加密存储(AES-256)
    - 卡密传输加密(HTTPS)
    - 卡密脱敏展示(**** **** **** 1234)
    - 限制查询频率(防止批量获取)
    
  2. 卡密补货

    补货触发:
    - 可用卡密 < 安全阈值(如1000张)
    - 自动告警
    
    补货方式:
    - 供应商API自动拉取
    - 或人工Excel导入
    
  3. 卡密有效期

    过期处理:
    - 定时任务扫描过期卡密
    - 状态更新为INVALID
    - 库存减少(不可售)
    - 向供应商申请补卡
    
  4. 虚拟发货

    自动发货:
    - 支付成功 → 立即分配卡密
    - 推送给用户(短信/App)
    - 订单状态 → COMPLETED
    
    发货耗时:< 30秒
    

延伸思考

  1. 卡密被盗用如何防范?
  2. 虚拟商品是否需要支持退款?
  3. 供应商配额不足时如何处理?

📊 题目10:多仓库场景下的库存分配策略

问题描述: 电商平台有5个仓库(华北、华东、华南、西南、西北),用户下单时如何选择仓库发货?请设计库存分配策略。

答案

问题分析: 仓库选择的核心考量:

  1. 配送时效:就近仓库配送快
  2. 运费成本:距离影响运费
  3. 库存充足度:优先选择库存多的仓库
  4. 仓库负载:避免单仓库压力过大

方案一:就近原则

核心思想: 根据用户地址,选择最近的仓库。

设计:

仓库覆盖范围:
- 北京仓:北京、天津、河北
- 上海仓:上海、江苏、浙江
- 深圳仓:广东、广西、福建
- 成都仓:四川、重庆、云南
- 西安仓:陕西、甘肃、新疆

路由逻辑:
1. 解析用户收货地址的省份
2. 查找覆盖该省份的仓库
3. 检查库存
4. 有货 → 该仓库发货
5. 无货 → 选择次近仓库

优点:

  • 配送快
  • 用户体验好
  • 运费低

缺点:

  • 库存可能不均衡
  • 跨区发货增加成本

方案二:智能调度(推荐)

核心思想: 综合考虑配送时效、库存、成本,动态选择最优仓库。

设计:

评分模型:
score = w1 * distance_score + 
        w2 * stock_score + 
        w3 * cost_score +
        w4 * load_score

各项得分计算:
1. distance_score(距离):
   = 1.0 / (distance_km + 100)
   距离越近分越高

2. stock_score(库存):
   = warehouse_stock / max_stock
   库存越多分越高

3. cost_score(成本):
   = 1.0 / shipping_cost
   运费越低分越高

4. load_score(负载):
   = 1.0 - (current_orders / capacity)
   当前订单越少分越高

权重设置:
- 普通商品:w1=0.5, w2=0.3, w3=0.1, w4=0.1
- 秒杀商品:w1=0.3, w2=0.5, w3=0.1, w4=0.1(库存优先)
- 大件商品:w1=0.4, w2=0.2, w3=0.3, w4=0.1(成本优先)

优点:

  • 全局最优
  • 灵活可配置
  • 支持多种策略

缺点:

  • 计算复杂
  • 需要实时数据(各仓库负载)

方案三:库存均衡策略

核心思想: 主动调配库存,保持各仓库库存均衡。

设计:

库存均衡算法:
1. 计算各仓库库存偏离度
   deviation = (warehouse_stock - avg_stock) / avg_stock

2. 如果偏离度 > 30%,触发调拨
   从库存多的仓库调拨到库存少的仓库

3. 调拨优先级:
   - 距离近优先
   - 库存差距大优先

调拨执行:
1. 创建调拨单
2. 源仓库出库
3. 物流运输
4. 目标仓库入库

优点:

  • 库存均衡,利用率高
  • 减少缺货
  • 优化全局

缺点:

  • 调拨成本高
  • 调拨周期长(天级)
  • 需要预测算法

方案对比

方案配送时效成本库存利用率复杂度
就近原则★★★★★★★★★☆★★★☆☆★★★★★
智能调度★★★★☆★★★★★★★★★☆★★★☆☆
均衡策略★★★☆☆★★★☆☆★★★★★★★☆☆☆

推荐方案: 采用智能调度

实施要点:

  1. 仓库路由服务

    public interface WarehouseRouter {
      // 选择单个仓库
      Warehouse route(Order order);
      
      // 多商品拆单(可能分多仓库发货)
      Map<Warehouse, List<OrderItem>> routeMulti(Order order);
    }
    
    实现:
    public Warehouse route(Order order) {
      List<Warehouse> candidates = getCandidateWarehouses(order);
      
      return candidates.stream()
        .filter(w -> hasStock(w, order))
        .map(w -> new ScoredWarehouse(w, calculateScore(w, order)))
        .max(Comparator.comparing(ScoredWarehouse::getScore))
        .map(ScoredWarehouse::getWarehouse)
        .orElseThrow(OutOfStockException::new);
    }
    
  2. 拆单策略

    场景:用户购买商品A、B、C
    - 商品A:北京仓有货
    - 商品B:上海仓有货
    - 商品C:两个仓库都有货
    
    策略1:优先合单
    - 查找能满足所有商品的仓库
    - 减少拆单,降低运费
    
    策略2:就近发货
    - 每个商品从最近仓库发货
    - 可能拆多单,但配送快
    
    策略3:混合
    - 大件商品就近发货
    - 小件商品合单发货
    
  3. 库存预测

    预测模型:
    - 输入:历史销量、季节、促销活动
    - 输出:未来7天各仓库销量预测
    
    预分配:
    - 根据预测提前调拨库存
    - 避免大促时调拨来不及
    
  4. 负载均衡

    仓库容量管理:
    - 每个仓库设置日处理能力(如1万单/天)
    - 接近容量时降低选择权重
    - 超过容量时停止分配
    
    动态调整:
    - 实时监控各仓库订单量
    - 动态调整路由权重
    

延伸思考

  1. 如何处理跨仓拆单的运费计算?
  2. 用户能否指定发货仓库?
  3. 仓库之间如何协同(库存调拨、应急支援)?

🔧 题目11:如何设计库存安全水位和补货机制?

问题描述: 电商系统需要设置库存安全水位,当库存低于安全水位时自动触发补货。如何设计这套机制?

答案

问题分析: 库存安全水位的核心要素:

  1. 安全水位如何设置(太高占用资金,太低容易缺货)
  2. 补货时机和数量
  3. 补货周期(供应商交付时间)
  4. 多SKU的补货优先级

方案一:固定安全水位

核心思想: 为每个SKU设置固定的安全库存数量。

设计:

inventory
├── sku_id
├── stock
├── safety_stock(安全库存,人工设置)
└── reorder_point(补货点 = safety_stock + lead_time_demand)

补货触发:
if (stock <= reorder_point) {
  创建补货单
  补货数量 = (max_stock - current_stock)
}

优点:

  • 实现简单
  • 易于理解

缺点:

  • 不够灵活
  • 无法应对销量波动
  • 需要人工调整

方案二:动态安全水位(推荐)

核心思想: 根据销量预测动态调整安全水位。

设计:

销量预测:
avg_daily_sales = sum(last_30_days_sales) / 30

前置时间:
lead_time = 供应商交付周期(如7天)

安全库存:
safety_stock = avg_daily_sales * lead_time * safety_factor

其中:
- safety_factor = 1.5(安全系数,应对波动)
- 旺季调高到2.0
- 淡季调低到1.2

补货点:
reorder_point = safety_stock + lead_time * avg_daily_sales

补货数量(EOQ经济订货批量):
order_quantity = sqrt((2 * annual_demand * order_cost) / holding_cost)

优点:

  • 动态调整
  • 科学合理
  • 节省成本

缺点:

  • 依赖销量预测准确性
  • 计算复杂

方案三:ABC分类管理

核心思想: 将商品分为ABC类,采用不同的补货策略。

分类标准:

A类商品(20%商品,80%销售额):
- 高价值,严格管理
- 低安全库存(减少资金占用)
- 频繁补货(每周)
- 精准预测

B类商品(30%商品,15%销售额):
- 中等价值,常规管理
- 中等安全库存
- 定期补货(每月)
- 简单预测

C类商品(50%商品,5%销售额):
- 低价值,粗放管理
- 高安全库存(减少缺货)
- 批量补货(每季度)
- 不预测

优点:

  • 差异化管理
  • 资源聚焦
  • 效率高

缺点:

  • 需要定期重分类
  • ABC边界商品难处理

方案对比

方案准确性资金占用维护成本适用规模
固定水位★★★☆☆★★☆☆☆★★★★★小规模
动态水位★★★★★★★★★☆★★★☆☆大规模
ABC管理★★★★☆★★★★★★★☆☆☆超大规模

推荐方案: 采用动态水位+ABC分类

实施要点:

  1. 销量预测模型

    简单移动平均:
    avg_sales = sum(last_N_days) / N
    
    加权移动平均:
    avg_sales = sum(sales[i] * weight[i])
    权重:最近的销量权重更高
    
    指数平滑:
    forecast[t] = α * actual[t-1] + (1-α) * forecast[t-1]
    α = 0.3(平滑系数)
    
    时间序列模型(高级):
    - ARIMA
    - Prophet(Facebook开源)
    - 考虑季节性、趋势、促销影响
    
  2. 补货决策表

    replenishment_rule
    ├── sku_id
    ├── category(ABC分类)
    ├── safety_stock
    ├── reorder_point
    ├── lead_time(补货周期)
    ├── order_quantity(建议补货量)
    ├── max_stock(最大库存)
    └── updated_at
    
  3. 自动补货流程

    定时任务(每天凌晨):
    1. 扫描所有SKU库存
    2. 识别低于补货点的SKU
    3. 生成补货建议单
    4. 采购员审核
    5. 自动下单给供应商(或人工)
    
    补货单:
    purchase_order
    ├── po_id
    ├── supplier_id
    ├── sku_id
    ├── quantity
    ├── expected_delivery_date
    ├── status(PENDING/CONFIRMED/SHIPPED/RECEIVED)
    └── created_at
    
  4. 补货优先级

    优先级计算:
    priority = w1 * shortage_ratio + 
               w2 * sales_velocity + 
               w3 * profit_margin
    
    shortage_ratio = (reorder_point - current_stock) / reorder_point
    sales_velocity = daily_sales
    profit_margin = (price - cost) / price
    
    优先补货:
    - 严重缺货(shortage_ratio > 0.5)
    - 高销量
    - 高利润
    
  5. 监控告警

    告警条件:
    - 库存 < 安全库存 → 缺货预警
    - 库存 > 最大库存 * 1.5 → 积压告警
    - 补货单超期未到货 → 交付延迟告警
    
    报表:
    - 缺货率(SKU缺货天数 / 总天数)
    - 库存周转率(销量 / 平均库存)
    - 补货及时率(按时到货 / 总补货单)
    

延伸思考

  1. 促销活动前如何调整补货策略?
  2. 供应商交付不稳定如何应对?
  3. 新品如何设置安全库存(无历史数据)?

💡 题目12:库存快照在订单中的应用

问题描述: 订单下单时需要记录当时的库存状态,用于售后和数据分析。如何设计库存快照机制?

答案

问题分析: 库存快照的核心目的:

  1. 售后分析(为何超卖、缺货)
  2. 数据审计(库存变更追溯)
  3. 报表统计(某时刻库存状态)
  4. 性能要求(不能影响下单)

方案一:订单表冗余库存字段

核心思想: 在订单表记录下单时的库存数量。

设计:

order_item
├── order_id
├── sku_id
├── quantity(购买数量)
├── stock_at_order(下单时库存,快照)
└── ...

优点:

  • 实现最简单
  • 查询方便

缺点:

  • 快照信息有限
  • 无法追溯详细变更

适用场景:

  • 简单记录,不需要详细分析

方案二:库存变更日志

核心思想: 记录所有库存变更,按需查询历史状态。

设计:

inventory_change_log
├── log_id
├── sku_id
├── change_type(ORDER/CANCEL/REPLENISH/ADJUST)
├── quantity_delta(变更量,±)
├── stock_before(变更前库存)
├── stock_after(变更后库存)
├── reference_id(关联ID:order_id/po_id)
├── operator
└── created_at

查询某时刻库存:
1. 获取当前库存
2. 反向应用change_log(created_at > target_time)
3. 得到目标时刻库存

优点:

  • 完整追溯
  • 支持任意时刻查询
  • 审计能力强

缺点:

  • 查询需要计算
  • 存储成本高

方案三:定期快照+增量日志(推荐)

核心思想: 定期保存全量快照,中间记录增量日志。

设计:

inventory_snapshot(快照,每小时)
├── snapshot_id
├── sku_id
├── stock
├── reserved_stock
├── snapshot_time
└── created_at

inventory_change_log(增量日志)
├── log_id
├── sku_id
├── change_type
├── quantity_delta
├── stock_after
├── reference_id
└── created_at

查询某时刻库存:
1. 找到目标时刻之前最近的快照
2. 应用快照之后的增量日志
3. 得到目标时刻库存

示例:
查询2024-04-18 15:30的库存
→ 找到15:00的快照(stock=100)
→ 应用15:00-15:30的日志(-5, -3, -2)
→ 结果:100 - 5 - 3 - 2 = 90

优点:

  • 平衡性能和存储
  • 快照恢复快
  • 审计能力强

缺点:

  • 实现复杂度中等

方案对比

方案查询性能存储成本审计能力实施难度
冗余字段★★★★★★★★★★★★☆☆☆★★★★★
变更日志★★★☆☆★★☆☆☆★★★★★★★★☆☆
快照+日志★★★★☆★★★★☆★★★★★★★★☆☆

推荐方案: 采用定期快照+增量日志

实施要点:

  1. 快照生成策略

    定时快照:
    - 每小时生成一次快照
    - 或库存变更超过1000次时生成
    
    快照内容:
    - SKU ID
    - 物理库存
    - 预占库存
    - 已售库存
    - 可售库存
    - 快照时间
    
  2. 变更日志记录

    @Aspect
    public class InventoryChangeLogger {
      @Around("execution(* InventoryService.deduct*(..))")
      public Object logChange(ProceedingJoinPoint pjp) {
        // 记录变更前库存
        int stockBefore = getStock(skuId);
        
        // 执行扣减
        Object result = pjp.proceed();
        
        // 记录变更后库存
        int stockAfter = getStock(skuId);
        
        // 保存日志
        InventoryChangeLog log = new InventoryChangeLog();
        log.setSkuId(skuId);
        log.setChangeType("ORDER");
        log.setQuantityDelta(stockBefore - stockAfter);
        log.setStockBefore(stockBefore);
        log.setStockAfter(stockAfter);
        log.setReferenceId(orderId);
        logRepository.save(log);
        
        return result;
      }
    }
    
  3. 历史库存查询API

    GET /api/inventory/{skuId}/history?time=2024-04-18T15:30:00
    
    响应:
    {
      "skuId": "123",
      "stock": 90,
      "reserved": 10,
      "available": 80,
      "snapshotTime": "2024-04-18T15:30:00"
    }
    
  4. 数据归档

    归档策略:
    - 变更日志保留90天
    - 90天后归档到对象存储(OSS)
    - 快照保留1年
    - 1年后删除(保留年度快照)
    
  5. 应用场景

    场景1:售后分析
    用户投诉超卖 → 查询下单时库存 → 分析扣减日志 → 定位问题
    
    场景2:数据对账
    每日对账:今日库存 = 昨日库存 + 今日入库 - 今日出库
    不一致 → 查询变更日志 → 找出差异
    
    场景3:报表统计
    生成"每日库存报表" → 查询每日0点快照 → 生成报表
    

延伸思考

  1. 如何设计库存变更的审计流程?
  2. 变更日志如何支持回滚操作?
  3. 大批量商品的快照如何优化存储?

📊 题目13:库存的实时性vs一致性权衡

问题描述: 库存系统中,Redis提供高性能但可能丢失数据,MySQL提供强一致但性能较低。如何在实时性和一致性之间权衡?

答案

问题分析: 实时性vs一致性的核心矛盾:

  1. 用户期望实时看到库存
  2. 系统要保证不超卖
  3. 高并发下性能压力大
  4. 数据一致性难保证

方案一:强一致性优先(MySQL为准)

核心思想: 所有库存操作直接读写MySQL,放弃Redis。

设计:

-- 使用悲观锁
BEGIN;
SELECT stock FROM inventory WHERE sku_id=? FOR UPDATE;
UPDATE inventory SET stock = stock - ? WHERE sku_id=?;
COMMIT;

CAP理论选择:

  • C(一致性):强一致性
  • A(可用性):可用性一般(锁冲突)
  • P(分区容错):单机MySQL,不支持分区

优点:

  • 绝对一致性
  • 不会超卖
  • 不会丢数据

缺点:

  • 性能差(1000-5000 TPS)
  • 无法支持秒杀
  • 并发度低

适用场景:

  • 库存量少的高价商品(奢侈品)
  • 对一致性要求极高的场景

方案二:最终一致性(Redis为主)

核心思想: 库存扣减在Redis,异步同步到MySQL。

设计:

扣减流程:
1. Redis DECR扣减
2. 扣减成功,创建订单
3. 异步同步到MySQL

同步策略:
- 定时任务(每10秒)批量同步
- 或消息队列异步同步

数据恢复:
- Redis故障 → 从MySQL加载
- 对账任务(每小时)纠正差异

CAP理论选择:

  • C(一致性):最终一致性
  • A(可用性):高可用
  • P(分区容错):支持分区

优点:

  • 性能极高(10万+ TPS)
  • 支持高并发
  • 用户体验好

缺点:

  • Redis和MySQL可能不一致
  • Redis故障可能丢数据
  • 需要对账机制

适用场景:

  • 秒杀场景
  • 高并发场景
  • 普通商品

方案三:分层一致性(推荐)

核心思想: 根据商品类型和场景,采用不同一致性策略。

设计:

商品分类:
1. 高价商品(>10000元):
   - 使用MySQL悲观锁
   - 强一致性
   - 不追求性能
   
2. 秒杀商品:
   - 使用Redis+Lua
   - 最终一致性
   - 极致性能
   
3. 普通商品:
   - 使用MySQL乐观锁
   - 强一致性
   - 中等性能

扣减逻辑:
if (product.type == HIGH_VALUE) {
  return deductWithPessimisticLock();
} else if (product.type == SECKILL) {
  return deductWithRedis();
} else {
  return deductWithOptimisticLock();
}

优点:

  • 灵活权衡
  • 性能和一致性兼顾
  • 差异化服务

缺点:

  • 实现复杂
  • 需要商品分类

方案对比

方案一致性性能实现难度适用场景
强一致★★★★★★★☆☆☆★★★★☆高价商品
最终一致★★★☆☆★★★★★★★★☆☆秒杀
分层一致★★★★☆★★★★☆★★☆☆☆综合场景

推荐方案: 采用分层一致性

实施要点:

  1. 一致性级别定义

    强一致(Strong Consistency):
    - MySQL事务
    - 悲观锁或串行化
    - 实时一致
    
    最终一致(Eventual Consistency):
    - Redis扣减 + 异步同步
    - 秒级延迟
    - 需要对账
    
    因果一致(Causal Consistency):
    - 同一用户操作有序
    - 不同用户可能看到不同状态
    
  2. 降级策略

    正常模式:
    - 秒杀商品:Redis(最终一致)
    - 普通商品:MySQL乐观锁(强一致)
    
    降级模式(Redis故障):
    - 秒杀商品:暂停售卖或限流到MySQL
    - 普通商品:MySQL悲观锁
    
    极端模式(MySQL故障):
    - 只读Redis,禁止扣减
    - 提示用户稍后再试
    
  3. 一致性检查

    实时检查:
    - 扣减后检查Redis和MySQL差异
    - 差异 > 阈值(如100)→ 告警
    
    定期对账:
    - 每小时全量对账
    - 自动纠正小差异(< 5)
    - 大差异(> 10)→ 人工介入
    
  4. 监控指标

    一致性指标:
    - Redis-MySQL差异数量
    - 差异持续时间
    - 对账修复次数
    
    性能指标:
    - 扣减TPS
    - 扣减耗时P99
    - Redis命中率
    

延伸思考

  1. 如何设计Redis的持久化策略(AOF/RDB)?
  2. 分布式场景下如何保证Redis和MySQL一致性?
  3. CAP理论在库存系统中如何权衡?

🔧 题目14:库存回滚机制的设计

问题描述: 用户下单后未支付,或者订单取消,需要回滚库存。如何设计库存回滚机制,保证幂等性和正确性?

答案

问题分析: 库存回滚的核心场景:

  1. 订单取消(用户主动取消)
  2. 超时未支付(30分钟自动取消)
  3. 支付失败(扣款失败)
  4. 售后退货(订单完成后退货)

核心挑战:

  1. 幂等性:重复回滚不能多加库存
  2. 并发安全:多个回滚请求同时执行
  3. 部分回滚:一单多商品部分退货
  4. 补偿机制:回滚失败如何处理

方案一:直接加库存

核心思想: 取消订单时直接增加库存。

实现:

-- 订单取消
UPDATE inventory 
SET stock = stock + quantity
WHERE sku_id = ?;

-- 更新订单状态
UPDATE orders 
SET status = 'CANCELLED'
WHERE order_id = ?;

优点:

  • 实现简单

缺点:

  • 无法保证幂等性(重复调用会多加库存)
  • 并发不安全

方案二:基于订单状态回滚

核心思想: 检查订单状态,只有首次取消才回滚库存。

实现:

-- 原子更新订单状态
UPDATE orders 
SET status = 'CANCELLED'
WHERE order_id = ? AND status = 'PENDING';

if (affected_rows == 1) {
  // 状态更新成功,说明是首次取消
  UPDATE inventory 
  SET reserved_stock = reserved_stock - quantity,
      available_stock = available_stock + quantity
  WHERE sku_id = ?;
}

优点:

  • 保证幂等性
  • 并发安全

缺点:

  • 需要精确的状态流转
  • 状态机复杂

方案三:回滚记录表(推荐)

核心思想: 维护库存回滚记录,保证幂等性和可追溯。

设计:

inventory_rollback
├── rollback_id
├── order_id
├── sku_id
├── quantity
├── rollback_type(CANCEL/REFUND/TIMEOUT)
├── status(PENDING/SUCCESS/FAILED)
├── retry_count
├── created_at
└── updated_at

回滚流程:
1. 创建回滚记录(唯一约束:order_id + sku_id)
2. 执行回滚:
   UPDATE inventory 
   SET reserved_stock = reserved_stock - quantity
   WHERE sku_id = ?;
   
3. 更新回滚记录状态为SUCCESS
4. 如果失败,标记为FAILED,后台重试

幂等性保证:
INSERT INTO inventory_rollback (order_id, sku_id, quantity)
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE updated_at = NOW();

if (affected_rows == 1) {
  // 首次插入,执行回滚
  doRollback();
}

优点:

  • 幂等性强
  • 可追溯
  • 支持重试
  • 审计友好

缺点:

  • 实现复杂度高
  • 需要额外表

方案对比

方案幂等性并发安全可追溯实施难度
直接加库存★☆☆☆☆★★☆☆☆★☆☆☆☆★★★★★
基于状态★★★★☆★★★★☆★★★☆☆★★★☆☆
回滚记录★★★★★★★★★★★★★★★★★☆☆☆

推荐方案: 采用回滚记录表

实施要点:

  1. 回滚类型设计

    CANCEL:订单取消
    - 释放预占库存
    - 回补可售库存
    
    REFUND:售后退货
    - 增加物理库存
    - 增加可售库存
    
    TIMEOUT:超时未支付
    - 释放预占库存
    
    ADJUST:库存调整(人工)
    
  2. 回滚执行逻辑

    @Transactional
    public void rollbackInventory(String orderId) {
      // 1. 创建回滚记录(幂等键)
      RollbackRecord record = new RollbackRecord();
      record.setOrderId(orderId);
      record.setSkuId(skuId);
      record.setQuantity(quantity);
      record.setStatus("PENDING");
      
      try {
        rollbackRepository.insert(record);
      } catch (DuplicateKeyException e) {
        // 已存在回滚记录,直接返回
        return;
      }
      
      // 2. 执行库存回滚
      try {
        inventoryService.release(skuId, quantity);
        record.setStatus("SUCCESS");
      } catch (Exception e) {
        record.setStatus("FAILED");
        record.setRetryCount(record.getRetryCount() + 1);
        throw e;
      } finally {
        rollbackRepository.update(record);
      }
    }
    
  3. 部分退货处理

    场景:用户购买3件商品,退货1件
    
    处理:
    1. 创建部分回滚记录
    2. 回滚数量 = 退货数量(1件)
    3. 更新订单项状态(2件已发货,1件已退货)
    
  4. 失败重试

    补偿Worker:
    1. 定时扫描FAILED状态的回滚记录
    2. 重试执行回滚
    3. 最多重试5次
    4. 仍失败 → 转人工处理
    
  5. 监控告警

    指标:
    - 回滚成功率
    - 回滚延迟(下单到回滚的时间)
    - 失败回滚数量
    
    告警:
    - 回滚成功率 < 99%
    - 失败回滚 > 100条
    

延伸思考

  1. 如何防止恶意下单占用库存?
  2. 库存回滚失败如何人工介入?
  3. 大批量订单取消如何优化回滚性能?

💡 题目15:跨境电商的库存管理(多国库存)

问题描述: 跨境电商在中国、美国、欧洲都有仓库,同一商品在不同地区有库存。如何设计全球库存管理系统?

答案

问题分析: 跨境库存的核心挑战:

  1. 时区差异(中国和美国相差12小时)
  2. 币种不同(人民币、美元、欧元)
  3. 清关周期长(跨境物流10-30天)
  4. 库存调拨困难

方案一:独立库存池

核心思想: 每个国家/地区独立管理库存,互不共享。

设计:

inventory
├── sku_id
├── country_code(US/CN/EU)
├── warehouse_id
├── stock
└── currency

用户购买:
1. 根据用户IP或选择的站点确定国家
2. 查询该国家的库存
3. 扣减该国家库存
4. 不跨国发货

优点:

  • 实现简单
  • 各国独立运营
  • 无跨境调拨

缺点:

  • 库存利用率低(美国有货但中国无货)
  • 用户体验差(本地无货无法购买)

方案二:全球库存池(虚拟统一)

核心思想: 虚拟层展示全球总库存,实际按地区分配。

设计:

虚拟层:
global_inventory
├── sku_id
├── total_stock = sum(所有国家库存)

实际层:
regional_inventory
├── sku_id
├── region_code
├── stock

用户下单:
1. 展示全球总库存(用户可见)
2. 选择发货国家(就近优先)
3. 扣减该国库存
4. 跨境发货(如果本地无货)

优点:

  • 用户体验好(看到全球库存)
  • 库存利用率高
  • 支持跨境发货

缺点:

  • 跨境物流慢、贵
  • 复杂的库存分配

方案三:混合模式(推荐)

核心思想: 优先本地发货,支持跨境应急。

设计:

库存层级:
1. 本地库存(Local Stock):
   - 用户所在国家的库存
   - 优先扣减
   - 配送快(2-3天)

2. 区域库存(Regional Stock):
   - 相邻国家的库存
   - 次优选择
   - 配送中等(5-7天)

3. 全球库存(Global Stock):
   - 其他国家的库存
   - 最后选择
   - 配送慢(10-30天)

路由策略:
1. 查询本地库存
   - 有货 → 本地发货
2. 查询区域库存
   - 有货 → 跨境发货(用户确认)
3. 查询全球库存
   - 有货 → 全球发货(用户确认)
4. 都无货 → 缺货

优点:

  • 平衡速度和成本
  • 灵活
  • 用户可选

缺点:

  • 需要智能路由
  • 用户决策成本

方案对比

方案库存利用率配送速度用户体验实施难度
独立池★★☆☆☆★★★★★★★★☆☆★★★★★
全球池★★★★★★★☆☆☆★★★★★★★★☆☆
混合模式★★★★☆★★★★☆★★★★☆★★☆☆☆

推荐方案: 采用混合模式

实施要点:

  1. 库存数据结构

    global_inventory
    ├── sku_id
    ├── region_code(US/CN/EU/JP)
    ├── warehouse_id
    ├── stock
    ├── currency
    ├── local_price(本地售价)
    └── shipping_cost_to_other(跨境运费)
    
  2. 库存分配策略

    初始分配(新品上架):
    - 根据各地区历史销量预测
    - US: 40%, EU: 30%, CN: 20%, JP: 10%
    
    动态调整(运营中):
    - 每周根据销量调整
    - 滞销地区调拨到热销地区
    
  3. 跨境发货流程

    用户下单:
    1. 显示配送选项:
       - 本地发货(2-3天,免运费)
       - 跨境发货(10-15天,运费$20)
    
    2. 用户选择跨境发货
    
    3. 扣减源国库存
    
    4. 清关、物流
    
    5. 配送到用户
    
  4. 币种和价格

    价格策略:
    - 每个地区独立定价(考虑关税、运费)
    - 实时汇率转换
    
    示例:
    商品成本:$100
    - 美国售价:$150(含税15%,利润$35)
    - 中国售价:¥1200(含税13%,利润约$40)
    - 欧洲售价:€140(含税20%,利润约$30)
    
  5. 库存同步

    同步机制:
    - 各地区库存独立数据库
    - 聚合到全球视图(Redis缓存)
    - 更新延迟 < 1秒
    
    时区处理:
    - 所有时间戳使用UTC
    - 本地展示转换为用户时区
    

延伸思考

  1. 如何设计跨境库存调拨的审批流程?
  2. 清关失败如何处理库存回滚?
  3. 不同国家的退货政策如何影响库存管理?

2.3 营销与计价系统(10题)

📊 题目1:设计支持多种促销规则的价格计算引擎

问题描述: 电商平台有多种促销(满减、折扣、优惠券、满赠、阶梯价),用户下单时需要计算最终价格。如何设计灵活的价格计算引擎?

答案

问题分析: 价格计算的核心挑战:

  1. 规则类型多(满减、折扣、优惠券、积分抵扣)
  2. 规则可组合(同时使用多种优惠)
  3. 优先级和互斥(有些优惠不能同时用)
  4. 实时计算性能

方案一:硬编码规则

核心思想: 在代码中直接编写每种促销规则的计算逻辑。

实现:

public BigDecimal calculatePrice(Order order) {
  BigDecimal price = order.getOriginalPrice();
  
  // 应用满减
  if (order.getTotal() >= 200) {
    price = price.subtract(new BigDecimal("30"));
  }
  
  // 应用折扣
  if (order.hasDiscount()) {
    price = price.multiply(new BigDecimal("0.9"));
  }
  
  // 应用优惠券
  if (order.hasCoupon()) {
    price = price.subtract(order.getCouponAmount());
  }
  
  return price;
}

优点:

  • 实现简单
  • 性能好

缺点:

  • 不灵活(新增规则需要改代码)
  • 难维护
  • 运营无法自主配置

适用场景:

  • 规则简单且固定
  • 小型电商

方案二:规则引擎(推荐)

核心思想: 将促销规则配置化,使用规则引擎动态执行。

设计:

promotion_rule(促销规则表)
├── rule_id
├── rule_name
├── rule_type(DISCOUNT/FULL_REDUCE/COUPON/GIFT/TIER_PRICE)
├── rule_config(JSON)
    {
      "type": "FULL_REDUCE",
      "threshold": 200,
      "reduce": 30,
      "priority": 10,
      "exclusive": false
    }
├── begin_time
├── end_time
├── priority(优先级,数字越小越优先)
├── exclusive(是否与其他规则互斥)
└── status

规则执行引擎:
public class PriceCalculator {
  public BigDecimal calculate(Order order) {
    // 1. 加载适用的规则
    List<Rule> rules = ruleEngine.getApplicableRules(order);
    
    // 2. 按优先级排序
    rules.sort(Comparator.comparing(Rule::getPriority));
    
    // 3. 依次应用规则
    BigDecimal finalPrice = order.getOriginalPrice();
    for (Rule rule : rules) {
      if (rule.isApplicable(order)) {
        finalPrice = rule.apply(finalPrice, order);
      }
    }
    
    return finalPrice;
  }
}

规则类型示例:

满减规则:
{
  "type": "FULL_REDUCE",
  "threshold": 200,  // 满200
  "reduce": 30       // 减30
}

折扣规则:
{
  "type": "DISCOUNT",
  "rate": 0.85       // 8.5折
}

阶梯价:
{
  "type": "TIER_PRICE",
  "tiers": [
    {"quantity": 1, "price": 100},
    {"quantity": 10, "price": 90},
    {"quantity": 100, "price": 80}
  ]
}

满赠规则:
{
  "type": "GIFT",
  "threshold": 300,
  "giftSkuId": "gift_001"
}

优点:

  • 灵活可配置
  • 运营自主管理
  • 易于扩展新规则
  • 支持复杂组合

缺点:

  • 实现复杂
  • 性能略低于硬编码

方案三:脚本引擎(Groovy/JavaScript)

核心思想: 将规则写成脚本,动态加载执行。

设计:

promotion_script
├── script_id
├── script_name
├── script_content(Groovy脚本)
    """
    if (order.total >= 200) {
      return order.total - 30
    }
    return order.total
    """
├── priority
└── ...

执行:
public BigDecimal calculate(Order order) {
  for (Script script : scripts) {
    BigDecimal price = groovyEngine.eval(script, order);
    order.setPrice(price);
  }
  return order.getPrice();
}

优点:

  • 极致灵活(可写任意逻辑)
  • 无需发布代码

缺点:

  • 安全风险(脚本注入)
  • 调试困难
  • 性能开销大

方案对比

方案灵活性性能运营友好安全性实施难度
硬编码★★☆☆☆★★★★★★☆☆☆☆★★★★★★★★★★
规则引擎★★★★☆★★★★☆★★★★★★★★★☆★★★☆☆
脚本引擎★★★★★★★★☆☆★★★☆☆★★☆☆☆★★☆☆☆

推荐方案: 采用规则引擎

实施要点:

  1. 规则抽象

    public interface PromotionRule {
      // 规则是否适用
      boolean isApplicable(Order order);
      
      // 应用规则,返回新价格
      BigDecimal apply(BigDecimal currentPrice, Order order);
      
      // 规则优先级
      int getPriority();
      
      // 是否与其他规则互斥
      boolean isExclusive();
    }
    
    // 满减规则实现
    public class FullReduceRule implements PromotionRule {
      private BigDecimal threshold;
      private BigDecimal reduceAmount;
      
      public boolean isApplicable(Order order) {
        return order.getTotal().compareTo(threshold) >= 0;
      }
      
      public BigDecimal apply(BigDecimal currentPrice, Order order) {
        return currentPrice.subtract(reduceAmount);
      }
    }
    
  2. 规则组合策略

    互斥规则:
    - 满减和折扣互斥(选优惠力度大的)
    - 用户只能使用一张优惠券
    
    可叠加规则:
    - 满减 + 积分抵扣
    - 会员折扣 + 优惠券
    
    执行顺序:
    1. 商品级促销(商品折扣)
    2. 订单级促销(满减)
    3. 用户级促销(会员折扣)
    4. 优惠券
    5. 积分抵扣
    
  3. 价格明细

    原价:¥500
    - 商品折扣:-¥50(9折)
    - 满减优惠:-¥30(满200减30)
    - 会员折扣:-¥42(额外9折)
    - 优惠券:-¥20
    = 实付:¥358
    
    用户可见每项优惠的金额
    
  4. 性能优化

    规则缓存:
    - 缓存活跃的促销规则(Redis)
    - TTL 5分钟
    - 规则变更时主动刷新
    
    批量计算:
    - 购物车多商品批量计算
    - 减少数据库查询
    
  5. 试算API

    POST /api/price/calculate
    {
      "items": [
        {"skuId": "123", "quantity": 2},
        {"skuId": "456", "quantity": 1}
      ],
      "couponCode": "SUMMER20",
      "usePoints": 100
    }
    
    响应:
    {
      "originalPrice": 500,
      "discounts": [
        {"type": "FULL_REDUCE", "amount": 30},
        {"type": "COUPON", "amount": 20}
      ],
      "finalPrice": 450
    }
    

延伸思考

  1. 如何设计促销规则的AB测试?
  2. 多种促销组合时如何选择最优组合?
  3. 促销规则变更如何保证已下单的订单价格不变?

🔧 题目2:优惠券系统的设计

问题描述: 电商平台需要支持优惠券(满减券、折扣券、品类券)。如何设计优惠券系统,包括发放、使用、核销?

答案

问题分析: 优惠券的核心要素:

  1. 发放方式(批量发放、用户领取、定向发放)
  2. 使用规则(满减、折扣、品类限制、商品限制)
  3. 并发领取(秒杀券,1万人抢100张)
  4. 防刷机制(防止用户重复领取)

方案一:简单优惠券

核心思想: 优惠券模板+用户优惠券实例。

设计:

coupon_template(优惠券模板)
├── template_id
├── name
├── coupon_type(FULL_REDUCE/DISCOUNT/CASH)
├── discount_amount(满减金额)
├── discount_rate(折扣率,如0.9)
├── threshold(使用门槛,如满200可用)
├── total_count(总发行量)
├── used_count(已使用数量)
├── begin_time
├── end_time
└── status

user_coupon(用户优惠券)
├── coupon_id
├── template_id
├── user_id
├── coupon_code(券码)
├── status(UNUSED/USED/EXPIRED)
├── used_order_id
├── received_at
├── used_at
└── expire_at

优点:

  • 实现简单
  • 易于理解

缺点:

  • 功能单一
  • 不支持复杂规则

方案二:规则化优惠券(推荐)

核心思想: 优惠券支持丰富的使用规则和发放规则。

设计:

coupon_template
├── template_id
├── name
├── coupon_type
├── discount_config(JSON)
    {
      "type": "FULL_REDUCE",
      "threshold": 200,
      "amount": 30
    }
├── usage_rule(JSON)
    {
      "validCategories": [1, 2, 3],  // 限定品类
      "validSkus": ["sku1", "sku2"],  // 限定商品
      "maxDiscountPerOrder": 50,      // 单笔最高优惠
      "excludeBrands": [10, 20]       // 排除品牌
    }
├── receive_rule(JSON)
    {
      "maxReceivePerUser": 1,         // 每人限领1张
      "newUserOnly": false,            // 是否新用户专享
      "memberLevelRequired": "VIP"    // 会员等级要求
    }
├── total_count
├── received_count
├── used_count
└── ...

user_coupon
├── coupon_id
├── template_id
├── user_id
├── status
├── lock_order_id(预占:锁定到某订单)
├── locked_at
└── ...

优点:

  • 规则灵活
  • 支持复杂场景
  • 运营可配置

缺点:

  • 实现复杂度高

方案三:优惠券码模式

核心思想: 预生成优惠券码,用户输入券码兑换。

设计:

coupon_code
├── code(券码,如SUMMER2024)
├── template_id
├── status(AVAILABLE/USED/EXPIRED)
├── user_id(已兑换用户)
├── used_at
└── expire_at

使用流程:
1. 运营批量生成券码
2. 用户输入券码兑换
3. 绑定到user_coupon
4. 下单时使用

优点:

  • 支持券码分享
  • 灵活发放(短信、广告)

缺点:

  • 券码可能被盗用
  • 需要生成大量券码

方案对比

方案灵活性并发性能防刷能力实施难度
简单券★★☆☆☆★★★★☆★★★☆☆★★★★★
规则券★★★★★★★★★☆★★★★☆★★★☆☆
券码模式★★★☆☆★★★★★★★☆☆☆★★★☆☆

推荐方案: 采用规则化优惠券

实施要点:

  1. 领券流程

    用户点击"领取":
    1. 检查用户是否已领取(防重复)
    2. 检查是否满足领取条件(新用户、会员等级)
    3. 检查库存(received_count < total_count)
    4. 扣减库存(乐观锁)
    5. 创建user_coupon记录
    
    并发控制:
    UPDATE coupon_template 
    SET received_count = received_count + 1
    WHERE template_id = ? 
      AND received_count < total_count
      AND version = ?;
    
    if (affected_rows == 0) {
      throw new CouponSoldOutException();
    }
    
  2. 用券流程

    下单时使用优惠券:
    1. 检查优惠券是否属于当前用户
    2. 检查优惠券状态(UNUSED)
    3. 检查是否过期
    4. 检查订单是否满足使用条件(品类、金额)
    5. 锁定优惠券(防止重复使用)
    6. 计算优惠金额
    
    支付成功:
    - 核销优惠券(status=USED)
    
    订单取消:
    - 释放优惠券(status=UNUSED, lock_order_id=NULL)
    
  3. 防刷策略

    策略1:用户限制
    - 每人限领1张
    - 同一手机号/设备ID限领
    
    策略2:行为检测
    - 短时间多次领取 → 拉黑
    - 领取后不使用 → 降低权重
    
    策略3:风控
    - 新注册用户限制
    - 异常IP拦截
    
  4. 券叠加规则

    规则:
    - 单笔订单最多使用1张优惠券
    - 优惠券和满减活动可叠加
    - 优惠券和积分抵扣可叠加
    
    选券策略:
    - 自动选择优惠最大的券
    - 或用户手动选择
    
  5. 券过期处理

    定时任务(每天凌晨):
    1. 扫描即将过期的券(expire_at < NOW() + 3天)
    2. 发送提醒通知(App推送、短信)
    3. 扫描已过期的券
    4. 状态更新为EXPIRED
    

延伸思考

  1. 如何设计优惠券的转赠功能?
  2. 优惠券如何支持多次使用(如月卡券)?
  3. 如何设计优惠券的效果分析(发放ROI)?

💡 题目3:阶梯价和批发价的设计

问题描述: 电商平台支持批发场景,购买数量越多价格越低(如买1件100元,买10件90元,买100件80元)。如何设计阶梯价系统?

答案

问题分析: 阶梯价的核心要素:

  1. 阶梯定义(数量区间和对应价格)
  2. 混合SKU计算(多个商品如何累计数量)
  3. 拆单问题(阶梯内和阶梯外商品分开发货)
  4. 实时计算性能

方案一:SKU级阶梯价

核心思想: 每个SKU独立设置阶梯价,不同SKU不累计。

设计:

sku_tier_price
├── sku_id
├── tier_level(阶梯级别:1,2,3...)
├── min_quantity(最小数量)
├── max_quantity(最大数量,NULL表示无上限)
├── price
└── ...

示例数据:
SKU: iPhone15
tier_1: 1-9件, ¥7999
tier_2: 10-99件, ¥7500
tier_3: 100+件, ¥7000

价格计算:
if (quantity >= 100) {
  return 7000 * quantity;
} else if (quantity >= 10) {
  return 7500 * quantity;
} else {
  return 7999 * quantity;
}

优点:

  • 简单直观
  • 计算快速

缺点:

  • 不支持跨SKU累计
  • 批发商体验差(买不同商品无法享受折扣)

方案二:品类级阶梯价

核心思想: 同一品类的商品数量累计,达到阶梯享受折扣。

设计:

category_tier_price
├── category_id
├── tier_level
├── min_quantity
├── discount_rate(折扣率)
└── ...

示例:
手机品类阶梯折扣:
tier_1: 1-9件, 无折扣
tier_2: 10-99件, 95折
tier_3: 100+件, 90折

计算:
用户购买:
- iPhone15: 5件 × ¥7999
- 小米14: 6件 × ¥3999
- 总数量:11件(属于tier_2)
- 享受95折

最终价格:
(5 × 7999 + 6 × 3999) × 0.95

优点:

  • 支持跨SKU累计
  • 批发商友好

缺点:

  • 品类定义需要清晰
  • 计算复杂

方案三:订单级阶梯价(推荐)

核心思想: 按订单总金额或总件数,应用阶梯折扣。

设计:

order_tier_price
├── tier_id
├── tier_type(BY_QUANTITY/BY_AMOUNT)
├── min_value(最小值)
├── max_value
├── discount_type(RATE/AMOUNT)
├── discount_value
└── ...

示例1:按数量
tier_1: 1-9件, 无折扣
tier_2: 10-49件, 95折
tier_3: 50+件, 90折

示例2:按金额
tier_1: <¥1000, 无折扣
tier_2: ¥1000-¥5000, 减¥100
tier_3: >¥5000, 减¥500

优点:

  • 灵活
  • 适用多种场景
  • 计算简单

缺点:

  • 需要明确阶梯规则

方案对比

方案灵活性批发友好计算复杂度适用场景
SKU级★★☆☆☆★★☆☆☆★★★★★零售
品类级★★★★☆★★★★☆★★★☆☆批发
订单级★★★★★★★★★★★★★★☆混合

推荐方案: 采用订单级阶梯价

实施要点:

  1. 阶梯计算引擎

    public BigDecimal calculateTierPrice(Order order) {
      // 1. 计算订单总量/总额
      int totalQuantity = order.getTotalQuantity();
      BigDecimal totalAmount = order.getTotalAmount();
      
      // 2. 查找匹配的阶梯
      TierPrice tier = tierPriceService.findMatchingTier(
        totalQuantity, totalAmount
      );
      
      // 3. 应用折扣
      if (tier.getDiscountType() == RATE) {
        return totalAmount.multiply(tier.getDiscountRate());
      } else {
        return totalAmount.subtract(tier.getDiscountAmount());
      }
    }
    
  2. 实时试算

    购物车实时显示:
    - 当前数量:8件
    - 当前价格:¥1000
    - 提示:"再买2件,享受95折,可省¥50"
    
    动态提示:
    引导用户凑单,提高客单价
    
  3. 拆单策略

    场景:用户购买120件商品
    - 100件享受阶梯价(¥80/件)
    - 20件普通价(¥100/件)
    
    方案A:不拆单
    - 所有商品按最高阶梯价
    - 用户体验好
    
    方案B:拆单
    - 100件一单,20件一单
    - 复杂,不推荐
    
  4. 会员叠加

    规则:
    - 阶梯价和会员折扣可叠加
    - 先应用阶梯价,再应用会员折扣
    
    示例:
    原价:¥10000
    阶梯价(95折):¥9500
    会员折扣(98折):¥9310
    
  5. 报表分析

    阶梯价效果分析:
    - 各阶梯成交订单数
    - 平均客单价提升
    - 转化率(凑单率)
    

延伸思考

  1. 阶梯价如何与优惠券组合?
  2. 用户退货部分商品如何重新计算价格?
  3. 大促期间阶梯价如何调整?

📊 题目4:会员等级和积分体系的设计

问题描述: 电商平台有会员体系(普通、银卡、金卡、钻石),不同等级享受不同权益(折扣、包邮、专属客服)。如何设计会员和积分系统?

答案

问题分析: 会员体系的核心要素:

  1. 等级划分(如何升降级)
  2. 权益设计(不同等级的差异化权益)
  3. 积分规则(获取、消耗、过期)
  4. 成长值体系(区分消费积分和成长值)

方案一:简单会员(单一积分)

核心思想: 只有积分,达到一定积分自动升级。

设计:

member
├── user_id
├── member_level(NORMAL/SILVER/GOLD/DIAMOND)
├── points(积分)
├── total_points(累计积分,用于升级)
└── ...

升级规则:
- 累计积分 >= 10000 → 钻石
- 累计积分 >= 5000 → 金卡
- 累计积分 >= 1000 → 银卡

优点:

  • 实现简单
  • 易于理解

缺点:

  • 权益单一
  • 无法区分消费和成长

方案二:双轨制(积分+成长值,推荐)

核心思想: 积分用于消费抵扣,成长值用于等级提升。

设计:

member
├── user_id
├── member_level
├── points(可消费积分)
├── growth_value(成长值,只增不减)
├── upgrade_time(升级时间)
├── downgrade_time(预计降级时间)
└── ...

member_level_config
├── level
├── min_growth_value(最低成长值)
├── benefits(JSON,权益配置)
    {
      "discount": 0.95,           // 95折
      "freeShipping": true,       // 包邮
      "pointsRate": 1.2,          // 积分倍率
      "birthdayCoupon": 50,       // 生日券
      "exclusiveService": true    // 专属客服
    }
└── ...

积分规则:
point_rule
├── rule_id
├── action(ORDER/CHECKIN/SHARE/REVIEW)
├── points_reward
├── growth_reward
└── ...

示例:
- 购物:每消费1元获得1积分 + 1成长值
- 签到:每天签到获得5积分 + 0成长值
- 分享:每次分享获得10积分 + 0成长值
- 评价:每次评价获得20积分 + 5成长值

优点:

  • 积分和等级分离,科学
  • 防止用户消费积分后降级
  • 权益丰富

缺点:

  • 复杂度高

方案三:付费会员(Prime模式)

核心思想: 用户付费购买会员资格,享受权益。

设计:

member_subscription
├── user_id
├── plan_type(MONTH/YEAR)
├── status(ACTIVE/EXPIRED/CANCELLED)
├── begin_time
├── end_time
├── auto_renew(是否自动续费)
└── ...

会员权益:
- 全场95折
- 全年包邮
- 专属客服
- 优先发货
- 会员专享价

优点:

  • 现金流稳定
  • 用户粘性高
  • 权益明确

缺点:

  • 需要足够吸引力的权益
  • 续费率是关键

方案对比

方案用户粘性权益丰富度实施难度盈利能力
单一积分★★★☆☆★★☆☆☆★★★★★★★☆☆☆
双轨制★★★★☆★★★★★★★★☆☆★★★☆☆
付费会员★★★★★★★★★☆★★★★☆★★★★★

推荐方案: 采用双轨制(积分+成长值)

实施要点:

  1. 积分获取规则

    消费积分:
    - 订单完成后发放
    - 1元 = 1积分
    - 会员等级倍率(金卡1.5倍)
    
    行为积分:
    - 签到:5积分/天
    - 分享:10积分/次
    - 评价:20积分/次(带图50积分)
    - 首次购买:100积分
    
  2. 积分消费

    抵扣规则:
    - 100积分 = 1元
    - 单笔订单最多抵扣订单金额的50%
    - 部分品类不支持积分抵扣(如iPhone)
    
    兑换商品:
    - 积分商城
    - 固定积分兑换商品
    
  3. 等级维护

    升级:
    - 成长值达到阈值立即升级
    - 发送升级通知
    
    降级:
    - 每年12月31日统计年度成长值
    - 未达标的会员降级
    - 降级前1个月提醒
    - 保级活动(充值、消费保级)
    
  4. 积分过期

    策略:
    - 积分有效期1年
    - 每年12月31日清零即将过期积分
    - 提前3个月、1个月、1周提醒
    
  5. 防刷策略

    - 签到积分:每天限1次
    - 分享积分:每天限3次
    - 评价积分:每订单限1次
    - 异常行为检测(短时间大量操作)
    

延伸思考

  1. 如何设计会员等级的有效期(年度会员)?
  2. 积分如何支持转赠功能?
  3. 会员权益如何动态调整(AB测试)?

🔧 题目5:秒杀活动的价格和库存设计

问题描述: 秒杀活动商品价格远低于平时,流量集中,如何设计秒杀的价格和库存系统,保证不超卖且性能可控?

答案

问题分析: 秒杀的核心挑战:

  1. 瞬时高并发(10万+ QPS)
  2. 库存精准控制(100件商品,10万人抢)
  3. 价格隔离(秒杀价和正常价不能混淆)
  4. 防黄牛(防止脚本抢购)

方案一:独立秒杀表

核心思想: 秒杀商品和库存独立存储,与正常商品隔离。

设计:

seckill_activity(秒杀活动)
├── activity_id
├── name
├── start_time
├── end_time
└── status

seckill_product(秒杀商品)
├── seckill_id
├── activity_id
├── sku_id
├── seckill_price(秒杀价)
├── normal_price(原价)
├── total_stock(秒杀库存)
├── remaining_stock(剩余库存)
├── limit_per_user(每人限购)
└── ...

用户下单:
1. 检查活动时间
2. 检查用户是否已购买(限购)
3. 扣减秒杀库存(Redis)
4. 创建订单(秒杀价)

优点:

  • 隔离性好
  • 不影响正常业务
  • 数据清晰

缺点:

  • 数据冗余

方案二:共享商品表+秒杀标记

核心思想: 秒杀商品复用商品表,通过标记区分。

设计:

product
├── sku_id
├── normal_price
├── is_seckill(是否秒杀商品)
├── seckill_price
├── seckill_stock
└── ...

价格查询:
if (product.is_seckill && isInSeckillTime()) {
  return product.seckill_price;
} else {
  return product.normal_price;
}

优点:

  • 无冗余
  • 实现简单

缺点:

  • 秒杀和正常业务混在一起
  • 容易出错(价格混淆)

方案三:分层架构(推荐)

核心思想: 前台秒杀系统 + 后台正常系统,数据隔离。

架构:

秒杀系统:
- 秒杀商品(独立表)
- 秒杀库存(Redis)
- 秒杀订单(独立表)
- 秒杀队列(削峰)

正常系统:
- 商品表
- 库存表
- 订单表

数据同步:
- 秒杀结束后同步到正常订单表
- 库存变更同步

优点:

  • 完全隔离
  • 互不影响
  • 可针对性优化

缺点:

  • 架构复杂
  • 数据同步成本

方案对比

方案隔离性性能实施难度适用场景
独立秒杀表★★★★☆★★★★☆★★★☆☆中小型
共享表★★☆☆☆★★★☆☆★★★★★小型
分层架构★★★★★★★★★★★★☆☆☆大型

推荐方案: 采用独立秒杀表

实施要点:

  1. 秒杀库存

    Redis存储:
    key: seckill:stock:{seckill_id}
    value: 剩余库存数量
    
    扣减(Lua脚本):
    local stock = redis.call('GET', KEYS[1])
    if tonumber(stock) > 0 then
      redis.call('DECR', KEYS[1])
      return 1
    else
      return 0
    end
    
  2. 限购控制

    Redis Set记录已购买用户:
    key: seckill:bought:{seckill_id}
    value: Set<user_id>
    
    检查:
    if (redis.sismember(key, user_id)) {
      return "已购买,不能重复购买";
    }
    
    记录:
    redis.sadd(key, user_id);
    
  3. 排队机制

    流程:
    1. 用户点击"立即抢购"
    2. 请求进入队列(Kafka)
    3. 显示排队位置
    4. Worker消费队列,限速扣减库存
    5. 扣减成功,通知用户
    6. 扣减失败,提示已售罄
    
    优点:
    - 削峰
    - 用户体验可控
    - 系统稳定
    
  4. 防黄牛

    策略1:验证码
    - 点击抢购后弹出验证码
    - 通过验证才能提交订单
    
    策略2:实人认证
    - 首次参与秒杀需要实人认证
    - 人脸识别
    
    策略3:行为分析
    - 检测异常高频请求
    - IP黑名单
    - 设备指纹
    
  5. 价格展示

    商品详情页:
    - 正常价:¥999(划线价)
    - 秒杀价:¥199(红色突出显示)
    - 倒计时:距开始还剩 01:23:45
    - 提醒:每人限购1件
    

延伸思考

  1. 秒杀订单未支付如何处理(是否释放库存)?
  2. 秒杀活动如何预热(提前加载数据)?
  3. 秒杀流量如何监控和应急处理?

💡 题目6:动态定价系统的设计

问题描述: 电商平台希望实现动态定价(如机票、酒店根据供需实时调价)。如何设计动态定价系统?

答案

问题分析: 动态定价的核心要素:

  1. 定价因子(库存、时间、竞争对手、需求)
  2. 定价策略(规则还是算法)
  3. 价格变动频率
  4. 用户体验(频繁变价影响用户信任)

方案一:规则引擎定价

核心思想: 根据预设规则调整价格。

规则示例:

规则1:库存定价
- 库存 > 80% → 原价
- 库存 50%-80% → 原价 × 1.1
- 库存 20%-50% → 原价 × 1.2
- 库存 < 20% → 原价 × 1.3

规则2:时间定价
- 旺季(11-12月)→ 原价 × 1.2
- 淡季(3-4月)→ 原价 × 0.8

规则3:竞争对手定价
- 获取竞争对手价格
- 自己价格 = 竞对价格 × 0.95(低5%)

规则4:用户画像定价
- 高价值用户 → 原价
- 价格敏感用户 → 原价 × 0.9

优点:

  • 可控
  • 易于理解
  • 运营可配置

缺点:

  • 规则固定,不够灵活
  • 无法自适应市场变化

方案二:算法定价(推荐)

核心思想: 使用机器学习预测最优价格。

设计:

输入特征:
- 商品属性(品牌、类目、成本)
- 库存水位
- 历史销量
- 竞争对手价格
- 用户画像(购买力、价格敏感度)
- 时间特征(星期几、节假日)
- 外部因素(天气、事件)

模型:
- 回归模型:预测最优价格
- 强化学习:实时调整价格,最大化收益

输出:
- 推荐价格
- 置信度

优点:

  • 智能化
  • 自适应
  • 收益最大化

缺点:

  • 需要算法团队
  • 冷启动问题
  • 黑盒,不透明

方案三:AB测试定价

核心思想: 多个价格同时测试,选择效果最好的。

流程:

1. 设定价格组:
   A: ¥99
   B: ¥109
   C: ¥119

2. 随机分流用户

3. 统计各价格组的转化率和收益

4. 选择最优价格作为主价格

5. 持续迭代测试

优点:

  • 基于实际数据
  • 科学决策

缺点:

  • 测试周期长
  • 需要流量支持

方案对比

方案灵活性效果实施难度适用场景
规则引擎★★★☆☆★★★☆☆★★★★☆标品
算法定价★★★★★★★★★★★★☆☆☆大平台
AB测试★★★★☆★★★★☆★★★★☆新品

推荐方案: 采用规则引擎+算法定价的混合方案。

实施要点:

  1. 定价数据收集

    price_history(价格历史)
    ├── sku_id
    ├── price
    ├── stock
    ├── sales_quantity(该价格下的销量)
    ├── conversion_rate(转化率)
    ├── start_time
    └── end_time
    
    competitor_price(竞对价格)
    ├── sku_id
    ├── competitor_name
    ├── price
    ├── crawled_at
    └── ...
    
  2. 定价决策流程

    定时任务(每小时):
    1. 收集数据(库存、销量、竞对价格)
    2. 输入定价模型
    3. 模型输出推荐价格
    4. 人工审核(可选)
    5. 更新商品价格
    6. 记录价格变更日志
    
  3. 价格锁定

    用户加购物车:
    - 锁定当前价格15分钟
    - 15分钟内下单按锁定价
    - 超时按最新价
    
    或:
    - 不锁定价格
    - 下单时实时计算(用户体验差)
    
  4. 价格展示

    对用户:
    - 显示当前价
    - 历史最低价(增加紧迫感)
    - 降价通知(用户订阅)
    
    对运营:
    - 价格趋势图
    - 竞对价格对比
    - 销量-价格关系
    
  5. 价格保护

    规则:
    - 单次调价幅度 <= 20%
    - 每天最多调价3次
    - 价格不低于成本价 × 1.1(保证毛利)
    - 价格不高于市场价 × 1.5(防止离谱)
    

延伸思考

  1. 如何处理用户对频繁变价的不满?
  2. 价格歧视(同一商品不同用户不同价)的法律风险?
  3. 如何设计价格保护机制(买贵退差价)?

📊 题目7:跨境电商的汇率和税费计算

问题描述: 跨境电商需要处理多币种和不同国家的税费。如何设计汇率转换和税费计算系统?

答案

问题分析: 跨境价格的核心要素:

  1. 汇率实时变动
  2. 不同国家税率不同(关税、增值税)
  3. 币种展示(用户看到本地币种)
  4. 结算币种(实际收款币种)

方案一:实时汇率

核心思想: 每次计算价格时查询实时汇率。

设计:

价格计算:
1. 商品基础价格(USD $100)
2. 查询实时汇率(USD/CNY = 7.2)
3. 转换为人民币(¥720)
4. 加税费(关税10%,增值税13%)
5. 最终价格(¥720 × 1.1 × 1.13 = ¥894)

汇率来源:
- 调用汇率API(如XE, OANDA)
- 每分钟更新一次

优点:

  • 汇率准确
  • 实时性好

缺点:

  • 价格频繁变化
  • 用户体验差
  • API成本高

方案二:固定汇率(推荐)

核心思想: 每天固定汇率,当天内价格不变。

设计:

exchange_rate(汇率表)
├── from_currency
├── to_currency
├── rate
├── effective_date(生效日期)
└── created_at

价格计算:
1. 查询今日汇率(缓存)
2. 转换币种
3. 加税费
4. 展示价格

汇率更新:
- 每天凌晨0点更新汇率
- 或管理员手动更新

优点:

  • 价格稳定
  • 用户体验好
  • 缓存友好

缺点:

  • 汇率不是实时
  • 可能有汇兑损失

方案三:汇率浮动区间

核心思想: 设置汇率波动阈值,超过阈值才更新。

设计:

固定汇率:7.2(基准)
浮动区间:±2%(7.056 - 7.344)

实时汇率:7.25
→ 在区间内,使用固定汇率7.2

实时汇率:7.40
→ 超出区间,更新固定汇率为7.4

优点:

  • 平衡稳定性和准确性
  • 减少价格变化频率

缺点:

  • 实现复杂度高

方案对比

方案准确性稳定性用户体验实施难度
实时汇率★★★★★★★☆☆☆★★☆☆☆★★★☆☆
固定汇率★★★☆☆★★★★★★★★★★★★★★☆
浮动区间★★★★☆★★★★☆★★★★☆★★☆☆☆

推荐方案: 采用固定汇率(每日更新)

实施要点:

  1. 汇率管理

    public class ExchangeRateService {
      @Scheduled(cron = "0 0 0 * * ?")  // 每天0点
      public void updateExchangeRate() {
        // 1. 调用汇率API获取最新汇率
        Map<String, BigDecimal> rates = fetchRatesFromAPI();
        
        // 2. 保存到数据库
        for (String pair : rates.keySet()) {
          ExchangeRate rate = new ExchangeRate();
          rate.setPair(pair);
          rate.setRate(rates.get(pair));
          rate.setEffectiveDate(LocalDate.now());
          repository.save(rate);
        }
        
        // 3. 刷新缓存
        cacheService.refreshRates(rates);
      }
    }
    
  2. 税费计算

    tax_rule(税费规则)
    ├── country_code
    ├── category_id
    ├── import_duty_rate(关税率)
    ├── vat_rate(增值税率)
    ├── min_tax_free_amount(免税额)
    └── ...
    
    示例:
    中国:
    - 关税:10%
    - 增值税:13%
    - 免税额:¥5000以下免税
    
    美国:
    - 关税:0%
    - 州税:0-10%(各州不同)
    
  3. 价格展示

    商品页展示:
    - 商品价格:$100
    - 运费:$20
    - 关税:$10(预估)
    - 总计:$130(约¥936)
    
    结算页:
    - 确认最终价格(包含税费)
    - 币种选择(CNY/USD)
    
  4. 结算币种

    策略1:统一结算币种
    - 平台统一收USD
    - 用户支付CNY → 银行自动换汇
    
    策略2:多币种账户
    - 平台有USD、CNY、EUR账户
    - 用户付CNY → 直接入CNY账户
    - 减少汇兑成本
    
  5. 汇率风险对冲

    风险:
    - 用户下单时汇率7.2
    - 商家收款时汇率7.0
    - 平台损失2%
    
    对冲策略:
    - 购买外汇期货
    - 设置汇率浮动保护(±1%)
    - 及时结汇
    

延伸思考

  1. 如何设计多币种支付(用户用USD支付CNY订单)?
  2. 汇率变化导致退款金额不一致如何处理?
  3. 跨境税费如何合规申报?

🔧 题目8:组合促销的价格计算(满减+折扣+券)

问题描述: 用户下单时同时享受满减(满200减30)、商品折扣(9折)、优惠券(20元)。如何设计组合促销的价格计算逻辑?

答案

问题分析: 组合促销的核心挑战:

  1. 计算顺序(先满减还是先折扣影响最终价)
  2. 规则冲突(有些促销不能同时用)
  3. 最优组合(如何选择让用户优惠最大)
  4. 性能(实时计算)

方案一:固定计算顺序

核心思想: 规定促销的固定计算顺序。

计算顺序:

原价:¥500

顺序1:折扣 → 满减 → 优惠券
1. 商品折扣(9折):¥500 × 0.9 = ¥450
2. 满减(满200减30):¥450 - ¥30 = ¥420
3. 优惠券(20元):¥420 - ¥20 = ¥400

顺序2:满减 → 折扣 → 优惠券
1. 满减:¥500 - ¥30 = ¥470
2. 折扣:¥470 × 0.9 = ¥423
3. 优惠券:¥423 - ¥20 = ¥403

结果不同!

推荐顺序:

1. 商品级促销(商品折扣、第二件半价)
2. 订单级促销(满减、满赠)
3. 平台级促销(优惠券、积分抵扣)
4. 会员折扣

原则:
- 商品自身属性优先
- 门槛促销次之
- 通用促销最后

优点:

  • 简单清晰
  • 易于实现

缺点:

  • 不够灵活
  • 可能不是最优惠

方案二:最优组合(推荐)

核心思想: 尝试所有可能的组合,选择最优惠的。

算法:

public BigDecimal calculateBestPrice(Order order) {
  // 1. 获取所有适用的促销
  List<Promotion> promotions = getApplicablePromotions(order);
  
  // 2. 生成所有可能的组合(考虑互斥规则)
  List<List<Promotion>> combinations = generateCombinations(promotions);
  
  // 3. 计算每种组合的最终价
  BigDecimal minPrice = order.getOriginalPrice();
  List<Promotion> bestCombination = null;
  
  for (List<Promotion> combo : combinations) {
    BigDecimal price = calculatePrice(order, combo);
    if (price.compareTo(minPrice) < 0) {
      minPrice = price;
      bestCombination = combo;
    }
  }
  
  // 4. 应用最优组合
  return applyPromotions(order, bestCombination);
}

生成组合时考虑互斥:
- 满减A和满减B互斥(只能选一个)
- 优惠券互斥(只能用一张)
- 其他可叠加

优点:

  • 保证最优惠
  • 用户体验最好

缺点:

  • 计算量大(组合爆炸)
  • 性能压力

优化:

- 限制促销数量(最多5个)
- 剪枝(提前排除明显不优的组合)
- 缓存(相同商品+促销组合缓存结果)

方案三:优先级+互斥

核心思想: 促销有优先级,互斥的选优先级高的。

设计:

促销列表(按优先级排序):
1. 优惠券20元(优先级10,互斥组A)
2. 满减30元(优先级20,互斥组A)
3. 商品折扣9折(优先级30,可叠加)
4. 会员折扣95折(优先级40,可叠加)

计算逻辑:
1. 在互斥组A中选择优惠力度最大的(满减30元)
2. 应用可叠加的促销(商品折扣、会员折扣)

最终:
¥500 - ¥30(满减)× 0.9(商品折扣)× 0.95(会员折扣)= ¥401.5

优点:

  • 平衡灵活性和性能
  • 运营可配置

缺点:

  • 可能不是全局最优

方案对比

方案最优性性能灵活性实施难度
固定顺序★★☆☆☆★★★★★★★☆☆☆★★★★★
最优组合★★★★★★★★☆☆★★★★★★★☆☆☆
优先级+互斥★★★★☆★★★★☆★★★★☆★★★☆☆

推荐方案: 采用优先级+互斥,必要时计算最优组合。

实施要点:

  1. 促销配置

    promotion
    ├── promotion_id
    ├── name
    ├── type(DISCOUNT/FULL_REDUCE/COUPON)
    ├── priority(优先级)
    ├── exclusive_group(互斥组,NULL表示可叠加)
    ├── stackable(是否可叠加)
    └── ...
    
  2. 价格明细

    订单价格明细:
    {
      "originalPrice": 500,
      "appliedPromotions": [
        {
          "name": "商品9折",
          "discountAmount": 50,
          "afterPrice": 450
        },
        {
          "name": "满200减30",
          "discountAmount": 30,
          "afterPrice": 420
        },
        {
          "name": "优惠券",
          "discountAmount": 20,
          "afterPrice": 400
        }
      ],
      "finalPrice": 400
    }
    
    用户可见每一步的优惠
    
  3. 试算接口

    POST /api/price/preview
    {
      "items": [...],
      "promotions": [...],
      "coupon": "SUMMER20"
    }
    
    响应:
    {
      "scenarios": [
        {
          "name": "推荐方案",
          "finalPrice": 400,
          "savings": 100,
          "appliedPromotions": [...]
        },
        {
          "name": "仅用优惠券",
          "finalPrice": 480,
          "savings": 20,
          "appliedPromotions": [...]
        }
      ]
    }
    
    让用户选择方案
    
  4. 性能优化

    缓存:
    key: price:calculate:{商品ID}:{促销IDs哈希}
    value: 计算结果
    TTL: 5分钟
    
    避免重复计算
    

延伸思考

  1. 如何向用户推荐最优促销组合?
  2. 促销规则变更如何保证已下单的订单价格不变?
  3. 如何设计促销的AB测试?

💡 题目9:预售和定金膨胀的设计

问题描述: 预售活动中,用户支付定金(如50元),尾款时定金可抵100元。如何设计预售和定金膨胀系统?

答案

问题分析: 预售定金的核心要素:

  1. 定金不可退(锁定用户)
  2. 定金膨胀(50元抵100元)
  3. 尾款支付期限(超时定金不退)
  4. 库存预占

方案一:双订单模式

核心思想: 定金订单和尾款订单分开。

设计:

presale_activity(预售活动)
├── activity_id
├── sku_id
├── deposit_amount(定金)
├── deposit_expand_amount(定金膨胀金额)
├── final_price(商品总价)
├── deposit_start_time
├── deposit_end_time
├── balance_start_time(尾款开始时间)
├── balance_end_time
└── ...

deposit_order(定金订单)
├── order_id
├── activity_id
├── user_id
├── deposit_amount
├── status(PAID/UNPAID)
└── ...

balance_order(尾款订单)
├── order_id
├── deposit_order_id(关联定金订单)
├── balance_amount(尾款金额 = 总价 - 定金膨胀)
├── status
└── ...

流程:
1. 预售期:用户支付定金 → 创建deposit_order
2. 尾款期:系统自动创建balance_order
3. 用户支付尾款
4. 发货

优点:

  • 清晰分离
  • 易于管理

缺点:

  • 两个订单,用户理解成本高

方案二:单订单分阶段(推荐)

核心思想: 一个订单,分阶段支付。

设计:

order
├── order_id
├── order_type(PRESALE)
├── presale_activity_id
├── total_amount(商品总价)
├── deposit_amount(已付定金)
├── balance_amount(待付尾款)
├── current_stage(DEPOSIT/BALANCE/COMPLETED)
├── deposit_paid_at
├── balance_deadline
└── ...

order_payment(支付记录)
├── payment_id
├── order_id
├── payment_type(DEPOSIT/BALANCE)
├── amount
├── paid_at
└── ...

流程:
1. 预售期:用户下单,支付定金
   order.current_stage = DEPOSIT
   order.deposit_amount = 50
   order.balance_amount = 总价 - 定金膨胀金额
   
2. 尾款期:订单进入尾款阶段
   order.current_stage = BALANCE
   发送尾款提醒
   
3. 用户支付尾款
   order.current_stage = COMPLETED
   
4. 发货

优点:

  • 订单统一
  • 用户理解成本低
  • 易于追踪

缺点:

  • 订单状态复杂

方案三:虚拟商品模式

核心思想: 定金作为虚拟商品,尾款时抵扣。

设计:

1. 用户购买"定金商品"(¥50)
2. 定金支付成功后,发放"抵扣券"(可抵¥100)
3. 尾款期,用户购买商品,使用抵扣券
4. 实付 = 商品价格 - 抵扣券金额

优点:

  • 复用现有优惠券系统
  • 灵活

缺点:

  • 定金和尾款割裂
  • 用户可能不理解

方案对比

方案清晰度实施难度用户体验适用场景
双订单★★★☆☆★★★☆☆★★★☆☆复杂预售
单订单分阶段★★★★★★★★★☆★★★★★通用
虚拟商品★★☆☆☆★★★★☆★★☆☆☆简单预售

推荐方案: 采用单订单分阶段

实施要点:

  1. 定金膨胀计算

    商品总价:¥999
    定金:¥50
    定金膨胀:¥100(2倍膨胀)
    尾款:¥999 - ¥100 = ¥899
    
    用户总共支付:¥50 + ¥899 = ¥949(省¥50)
    
  2. 库存管理

    定金支付成功:
    - 预占库存(reserved_stock +1)
    - 锁定到该订单
    
    尾款支付成功:
    - 确认库存(sold_stock +1, reserved_stock -1)
    
    超时未付尾款:
    - 释放库存(reserved_stock -1)
    - 定金不退
    
  3. 尾款提醒

    提醒策略:
    - 尾款开始:立即推送
    - 尾款截止前3天:提醒
    - 尾款截止前1天:紧急提醒
    - 尾款截止前1小时:最后提醒
    
    提醒渠道:
    - App推送
    - 短信
    - 站内信
    
  4. 超时处理

    定时任务(每小时):
    1. 扫描超时未付尾款的订单
    2. 订单状态 → CLOSED
    3. 释放库存
    4. 定金记为平台收入(不退)
    5. 通知用户
    
  5. 退款规则

    规则:
    - 支付定金后,不可取消订单
    - 定金不退
    - 尾款支付后,可申请退款
    - 退款金额 = 定金 + 尾款
    

延伸思考

  1. 如何防止用户恶意付定金占用库存?
  2. 预售商品如何设置发货时间?
  3. 定金膨胀活动如何设计ROI分析?

📊 题目10:价格歧视与个性化定价的设计

问题描述: 电商平台希望根据用户画像(新老用户、购买力、价格敏感度)实现个性化定价。如何设计价格歧视系统?同时如何规避法律风险?

答案

问题分析: 个性化定价的核心要素:

  1. 用户分层(高价值、普通、价格敏感)
  2. 定价策略(不同用户看到不同价格)
  3. 法律风险(价格歧视在某些国家违法)
  4. 用户信任(发现差价后的负面影响)

方案一:明面价格歧视(不推荐)

核心思想: 不同用户直接看到不同价格。

示例:

用户A(新用户):¥99
用户B(老用户):¥129
用户C(高价值用户):¥149

价格查询:
price = getPriceByUser(skuId, userId);

优点:

  • 简单直接
  • 收益最大化

缺点:

  • 法律风险大(违反价格法)
  • 用户信任崩塌(发现后口碑崩盘)
  • 媒体曝光风险

方案二:差异化优惠(推荐)

核心思想: 价格统一,但不同用户获得不同优惠。

设计:

基础价格:统一¥129

新用户:
- 新人专享券:¥30
- 实付:¥99

普通用户:
- 无优惠
- 实付:¥129

高价值用户:
- 会员折扣:9折
- 实付:¥116

关键:
- 价格统一展示
- 优惠透明(标注"新人专享"、"会员专享")

优点:

  • 合法合规
  • 用户可接受
  • 价格透明

缺点:

  • 收益优化程度不如价格歧视

方案三:隐性定价(灰色地带)

核心思想: 通过算法展示不同的商品推荐和排序。

策略:

高价值用户:
- 推荐高价商品
- 搜索结果优先展示高价商品

价格敏感用户:
- 推荐促销商品
- 搜索结果优先展示低价商品

不直接改价格,但影响用户选择

优点:

  • 间接影响购买
  • 法律风险小

缺点:

  • 效果不如直接定价
  • 算法复杂

方案对比

方案收益合规性用户信任风险
明面歧视★★★★★★☆☆☆☆★☆☆☆☆★★★★★
差异化优惠★★★★☆★★★★★★★★★☆★★☆☆☆
隐性定价★★★☆☆★★★★☆★★★★☆★★★☆☆

推荐方案: 采用差异化优惠

实施要点:

  1. 用户分层

    基于RFM模型:
    - R(最近一次购买)
    - F(购买频次)
    - M(购买金额)
    
    用户分层:
    - 高价值用户(VIP):R<30天, F>10次, M>1万
    - 活跃用户:R<90天, F>3次
    - 沉睡用户:R>90天
    - 新用户:注册<30天,F=0
    - 价格敏感用户:经常搜索低价、使用优惠券
    
  2. 差异化优惠策略

    新用户:
    - 新人专享券(大额)
    - 首单免运费
    - 新人专区(低价引流商品)
    
    沉睡用户:
    - 唤醒券(定向发放)
    - "好久不见,给你优惠"
    
    高价值用户:
    - 会员折扣
    - 生日礼包
    - 专属客服
    
    价格敏感用户:
    - 推荐促销商品
    - 凑单优惠
    
  3. 透明化展示

    商品页:
    - 价格:¥129(统一价格)
    - 您的优惠:
      ✓ 新人券:-¥30
      ✓ 首单免运费
    - 实付:¥99
    
    标注优惠来源,避免误解
    
  4. 法律合规

    避免:
    - 同一商品同一时间不同价格(价格歧视)
    - 隐藏真实价格
    - 大数据杀熟
    
    合法:
    - 不同用户不同优惠(促销活动)
    - 会员专享价(明确标注)
    - 新人优惠(限定条件)
    
  5. 监控与风控

    监控指标:
    - 用户投诉率(价格差异投诉)
    - 媒体舆情
    - 价格离散度(同商品价格差异)
    
    风控:
    - 价格差异 < 30%
    - 优惠透明化
    - 避免同一用户看到不同价格
    

延伸思考

  1. 如何平衡个性化定价和用户信任?
  2. 用户发现价格差异后如何应对?
  3. 如何设计价格歧视的AB测试(避免法律风险)?


第三部分:交易核心链路(50题)

3.1 搜索与导购(10题)

📊 题目1:电商搜索引擎的架构设计

问题描述: 电商平台每天有百万级搜索请求,需要支持全文搜索、属性筛选、排序。如何设计电商搜索引擎的整体架构?

答案

问题分析: 电商搜索的核心要素:

  1. 海量数据(千万级商品)
  2. 复杂查询(关键词+品类+价格区间+品牌)
  3. 实时性(商品上下架实时更新)
  4. 相关性排序(搜索“手机“优先展示热门手机)
  5. 性能要求(毫秒级响应)

方案一:基于MySQL的搜索

核心思想: 使用MySQL的LIKE查询和索引。

实现:

SELECT * FROM products 
WHERE title LIKE '%手机%' 
  AND category_id = 10
  AND price BETWEEN 1000 AND 5000
ORDER BY sales DESC
LIMIT 20;

优点:

  • 实现简单
  • 无需额外组件

缺点:

  • LIKE ‘%keyword%’ 无法使用索引,性能差
  • 不支持中文分词
  • 不支持相关性排序
  • 并发能力弱

适用场景:

  • 小型电商(商品<10万)
  • 简单搜索

方案二:Elasticsearch搜索(推荐)

核心思想: 使用专业搜索引擎ES,支持全文搜索和复杂查询。

架构:

用户搜索 
→ 搜索服务(API层)
→ Elasticsearch集群
→ 返回结果

数据同步:
商品变更 → Kafka → 同步Worker → ES索引

ES索引设计:

{
  "mappings": {
    "properties": {
      "productId": {"type": "keyword"},
      "title": {
        "type": "text",
        "analyzer": "ik_max_word",
        "fields": {
          "keyword": {"type": "keyword"}
        }
      },
      "brand": {"type": "keyword"},
      "categoryId": {"type": "long"},
      "price": {"type": "double"},
      "sales": {"type": "long"},
      "stock": {"type": "long"},
      "onSale": {"type": "boolean"},
      "attrs": {
        "type": "nested",
        "properties": {
          "name": {"type": "keyword"},
          "value": {"type": "keyword"}
        }
      },
      "createdAt": {"type": "date"}
    }
  }
}

搜索查询:

{
  "query": {
    "bool": {
      "must": [
        {"match": {"title": "手机"}}
      ],
      "filter": [
        {"term": {"onSale": true}},
        {"term": {"categoryId": 10}},
        {"range": {"price": {"gte": 1000, "lte": 5000}}},
        {"term": {"brand": "Apple"}}
      ]
    }
  },
  "sort": [
    {"sales": {"order": "desc"}},
    {"_score": {"order": "desc"}}
  ],
  "from": 0,
  "size": 20
}

优点:

  • 性能高(分布式搜索)
  • 支持复杂查询
  • 中文分词
  • 相关性排序
  • 实时性好

缺点:

  • 运维成本高
  • 数据同步复杂

方案三:混合架构

核心思想: ES负责搜索,MySQL负责详情查询。

流程:

1. 用户搜索"iPhone" 
2. ES返回productId列表:[123, 456, 789]
3. 根据productId批量查询MySQL获取完整商品信息
4. 组装返回

优点:

  • ES只存储搜索字段,节省空间
  • MySQL保证数据完整性
  • 职责分离

缺点:

  • 多次查询,延迟增加
  • 实现复杂

方案对比

方案性能功能运维成本适用规模
MySQL★★☆☆☆★★☆☆☆★★★★★小型
Elasticsearch★★★★★★★★★★★★★☆☆大型
混合架构★★★★☆★★★★★★★☆☆☆超大型

推荐方案: 采用Elasticsearch

实施要点:

  1. 索引设计

    索引名称:products_v1
    分片数:5(根据数据量调整)
    副本数:2(高可用)
    
    字段类型选择:
    - keyword:不分词(品牌、类目ID)
    - text:分词(标题、描述)
    - nested:嵌套对象(属性列表)
    
  2. 数据同步

    实时同步:
    - 商品创建/更新 → 发送Kafka消息
    - 同步Worker消费消息 → 更新ES
    - 延迟 < 5秒
    
    全量同步(兜底):
    - 每天凌晨全量同步
    - 对比MySQL和ES差异
    - 修复不一致数据
    
  3. 搜索优化

    查询缓存:
    - 热门搜索词缓存(Redis)
    - TTL 5分钟
    
    搜索建议:
    - 输入"iph" → 建议"iPhone 15"
    - 使用completion suggester
    
    拼写纠错:
    - 输入"ipone" → 自动纠正为"iPhone"
    
  4. 性能优化

    分页优化:
    - 浅分页:from+size(前10页)
    - 深分页:search_after(10页以后)
    
    字段裁剪:
    - 只返回必要字段
    - _source: ["productId", "title", "price"]
    
    路由优化:
    - 按类目路由到不同分片
    
  5. 监控告警

    监控指标:
    - 搜索QPS
    - 搜索延迟P99
    - ES集群健康度
    - 索引大小
    
    告警:
    - 搜索延迟 > 500ms
    - ES集群RED状态
    - 数据同步延迟 > 1分钟
    

延伸思考

  1. 如何设计搜索的AB测试(不同排序策略)?
  2. 搜索无结果时如何处理(推荐、纠错)?
  3. 如何防止恶意搜索(刷流量、爬虫)?

🔧 题目2:搜索相关性排序算法设计

问题描述: 用户搜索“手机“,返回1000个结果,如何排序保证用户最想要的商品排在前面?请设计相关性排序算法。

答案

问题分析: 相关性排序的核心要素:

  1. 文本相关性(标题匹配度)
  2. 商品热度(销量、点击量)
  3. 商品质量(评分、评价数)
  4. 商品新鲜度(新品)
  5. 个性化(用户偏好)

方案一:单一得分排序

核心思想: 只按一个维度排序(如销量)。

实现:

SELECT * FROM products 
WHERE title LIKE '%手机%'
ORDER BY sales DESC
LIMIT 20;

优点:

  • 简单
  • 性能好

缺点:

  • 忽略相关性(标题匹配度差的商品可能排前面)
  • 马太效应(热门商品更热门)

方案二:多因子加权(推荐)

核心思想: 综合多个因子,加权计算总分。

算法:

总分 = w1 × 文本相关性得分 +
       w2 × 销量得分 +
       w3 × 评分得分 +
       w4 × 新鲜度得分

各项得分计算:

1. 文本相关性(ES _score):
   - 标题完全匹配:1.0
   - 标题部分匹配:0.5-0.9
   - 只在描述中匹配:0.1-0.4

2. 销量得分:
   - 归一化:sales_score = log(sales + 1) / log(max_sales)
   - 取对数避免马太效应

3. 评分得分:
   - rating_score = (rating / 5.0) × log(review_count + 1)
   - 考虑评分和评价数

4. 新鲜度得分:
   - freshness_score = 1.0 / (days_since_published + 1)
   - 新品加权

权重设置:
w1 = 0.4(文本相关性最重要)
w2 = 0.3(销量)
w3 = 0.2(评分)
w4 = 0.1(新鲜度)

ES实现:

{
  "query": {
    "function_score": {
      "query": {"match": {"title": "手机"}},
      "functions": [
        {
          "field_value_factor": {
            "field": "sales",
            "modifier": "log1p",
            "factor": 0.3
          }
        },
        {
          "field_value_factor": {
            "field": "rating",
            "factor": 0.2
          }
        },
        {
          "gauss": {
            "createdAt": {
              "origin": "now",
              "scale": "30d",
              "decay": 0.5
            }
          },
          "weight": 0.1
        }
      ],
      "score_mode": "sum",
      "boost_mode": "sum"
    }
  }
}

优点:

  • 综合考虑多因素
  • 可调整权重
  • 效果好

缺点:

  • 权重调优需要经验
  • 计算复杂

方案三:机器学习排序(LTR)

核心思想: 使用机器学习模型预测点击率/转化率,按预测得分排序。

流程:

1. 特征工程:
   - 文本特征:TF-IDF、BM25
   - 商品特征:价格、销量、评分、库存
   - 用户特征:历史行为、偏好品类
   - 上下文特征:时间、地域

2. 训练数据:
   - 正样本:用户点击/购买的商品
   - 负样本:展示但未点击的商品

3. 模型训练:
   - GBDT、XGBoost
   - 或深度学习模型(Wide & Deep)

4. 在线预测:
   - 搜索返回候选商品
   - 模型预测点击率
   - 按预测得分排序

优点:

  • 效果最优
  • 自动学习最优权重
  • 支持个性化

缺点:

  • 需要算法团队
  • 需要大量训练数据
  • 冷启动问题

方案对比

方案效果实施难度计算成本个性化
单一得分★★☆☆☆★★★★★★★★★★★☆☆☆☆
多因子加权★★★★☆★★★☆☆★★★★☆★★☆☆☆
机器学习★★★★★★★☆☆☆★★★☆☆★★★★★

推荐方案: 采用多因子加权,逐步引入机器学习。

实施要点:

  1. 初期(多因子加权)

    public double calculateScore(Product product, String keyword) {
      // 1. 文本相关性(ES返回)
      double textScore = product.getElasticSearchScore();
      
      // 2. 销量得分
      double salesScore = Math.log(product.getSales() + 1) / 
                          Math.log(maxSales);
      
      // 3. 评分得分
      double ratingScore = (product.getRating() / 5.0) * 
                           Math.log(product.getReviewCount() + 1);
      
      // 4. 新鲜度得分
      long daysSince = ChronoUnit.DAYS.between(
        product.getCreatedAt(), LocalDate.now()
      );
      double freshnessScore = 1.0 / (daysSince + 1);
      
      // 5. 加权求和
      return 0.4 * textScore + 
             0.3 * salesScore + 
             0.2 * ratingScore + 
             0.1 * freshnessScore;
    }
    
  2. 权重调优

    AB测试:
    - A组:权重方案1(w1=0.4, w2=0.3, w3=0.2, w4=0.1)
    - B组:权重方案2(w1=0.5, w2=0.2, w3=0.2, w4=0.1)
    
    评估指标:
    - 点击率(CTR)
    - 转化率(CVR)
    - 用户停留时长
    
    选择效果最好的权重
    
  3. 个性化因子

    用户偏好品牌:
    if (user.favoriteBrands.contains(product.brand)) {
      score *= 1.2;  // 加权20%
    }
    
    用户价格偏好:
    if (product.price in user.priceRange) {
      score *= 1.1;
    }
    
    用户浏览历史:
    if (user.recentlyViewedCategories.contains(product.category)) {
      score *= 1.15;
    }
    
  4. 排序规则

    规则1:置顶广告位
    - 前3个位置:竞价广告
    - 标注"广告"
    
    规则2:新品扶持
    - 7天内新品得分 × 1.5
    
    规则3:库存保护
    - 库存 < 10件,降权(× 0.8)
    - 避免缺货商品排前面
    
  5. 监控与迭代

    监控指标:
    - 搜索结果点击率
    - 搜索转化率
    - 平均点击位置
    
    定期优化:
    - 每月分析数据
    - 调整权重
    - 新增因子
    

延伸思考

  1. 如何处理搜索作弊(刷销量、刷好评)?
  2. 长尾商品如何获得曝光机会?
  3. 如何设计搜索排序的解释性(为何这个商品排第一)?

💡 题目3:搜索建议(Suggest)的实现

问题描述: 用户输入“iph“,搜索框下方实时展示“iPhone 15“、“iPhone 14“等建议。如何实现搜索建议功能?

答案

问题分析: 搜索建议的核心要素:

  1. 实时性(输入即显示)
  2. 准确性(建议与输入相关)
  3. 热度排序(热门建议优先)
  4. 性能(毫秒级响应)

方案一:数据库LIKE查询

核心思想: 从数据库查询以输入开头的关键词。

实现:

-- 假设有关键词表
SELECT keyword, search_count 
FROM search_keywords 
WHERE keyword LIKE 'iph%'
ORDER BY search_count DESC
LIMIT 10;

优点:

  • 实现简单

缺点:

  • 性能差(每次输入都查库)
  • 前缀索引占用空间
  • 不支持中文拼音

方案二:Trie树(字典树)

核心思想: 将热门搜索词构建为Trie树,内存查询。

数据结构:

Trie树示例(存储iPhone, iPad, iMac):
       root
        |
        i
       / \
      P   M
     /|    \
    h a     a
    | |     |
    o d     c
    |
    n
    |
    e

每个节点存储:
- 字符
- 是否是词的结尾
- 热度(search_count)

查询:

public List<String> suggest(String prefix) {
  TrieNode node = root;
  
  // 1. 定位到前缀节点
  for (char c : prefix.toCharArray()) {
    if (!node.children.containsKey(c)) {
      return Collections.emptyList();
    }
    node = node.children.get(c);
  }
  
  // 2. DFS收集所有以该前缀开头的词
  List<String> results = new ArrayList<>();
  dfs(node, prefix, results);
  
  // 3. 按热度排序
  results.sort(Comparator.comparing(this::getHotness).reversed());
  
  return results.subList(0, Math.min(10, results.size()));
}

优点:

  • 速度快(内存查询)
  • 空间效率高(共享前缀)

缺点:

  • 不支持中文拼音
  • 内存占用大(全量词库)

方案三:Elasticsearch Completion Suggester(推荐)

核心思想: 使用ES的completion类型,支持高效前缀匹配。

索引设计:

{
  "mappings": {
    "properties": {
      "keyword": {
        "type": "completion",
        "analyzer": "simple",
        "search_analyzer": "simple"
      },
      "weight": {"type": "integer"}
    }
  }
}

数据导入:

{
  "keyword": {
    "input": ["iPhone 15", "iPhone15", "苹果15"],
    "weight": 10000
  }
}

查询:

{
  "suggest": {
    "keyword-suggest": {
      "prefix": "iph",
      "completion": {
        "field": "keyword",
        "size": 10,
        "skip_duplicates": true
      }
    }
  }
}

优点:

  • 性能极高(FST结构)
  • 支持拼音、同义词
  • 支持热度排序(weight)
  • 分布式

缺点:

  • 需要ES

方案对比

方案性能功能实施难度适用规模
数据库LIKE★★☆☆☆★★☆☆☆★★★★★小型
Trie树★★★★☆★★★☆☆★★★☆☆中型
ES Completion★★★★★★★★★★★★★★☆大型

推荐方案: 采用ES Completion Suggester

实施要点:

  1. 数据准备

    建议词来源:
    - 热门搜索词(用户历史搜索)
    - 商品标题(高销量商品)
    - 品牌名称
    - 类目名称
    - 运营配置词(促销活动)
    
    权重设置:
    - 用户搜索频次作为权重
    - 权重 = log(search_count + 1)
    
  2. 拼音支持

    安装pinyin分词器:
    - elasticsearch-analysis-pinyin
    
    索引配置:
    {
      "keyword": {
        "type": "completion",
        "analyzer": "pinyin_analyzer"
      }
    }
    
    输入"pingguo" → 建议"苹果"、"iPhone"
    
  3. 个性化建议

    用户维度:
    - 记录用户搜索历史(Redis)
    - 优先展示用户历史搜索
    
    示例:
    用户输入"ip"
    → ES返回:["iPhone 15", "iPad Pro", "iPod"]
    → 叠加用户历史:["iPhone 14"(历史搜索), "iPhone 15", "iPad Pro"]
    → 最终展示前10个
    
  4. 缓存策略

    热门建议缓存:
    - 缓存TOP 1000热门前缀的建议结果
    - key: suggest:iph
    - value: ["iPhone 15", "iPhone 14", ...]
    - TTL: 10分钟
    
    减少ES压力
    
  5. 建议词更新

    实时更新:
    - 用户搜索 → Kafka → 统计Worker → 更新ES
    
    定时更新(每小时):
    - 统计最近1小时热搜词
    - 更新权重
    - 新增热搜词
    

延伸思考

  1. 如何防止建议词中的敏感词?
  2. 搜索建议如何支持纠错(ipone → iPhone)?
  3. 如何设计多语言的搜索建议?

📊 题目4:商品筛选和多维度过滤的设计

问题描述: 用户搜索“手机“后,可以按品牌、价格区间、屏幕尺寸、内存等多个维度筛选。如何设计筛选系统?

答案

问题分析: 筛选系统的核心要素:

  1. 动态筛选项(不同类目的筛选项不同)
  2. 多条件组合(品牌AND价格区间AND内存)
  3. 筛选项计数(显示每个选项的商品数量)
  4. 性能(实时计算筛选结果)

方案一:前端筛选

核心思想: 一次性返回所有结果,前端JavaScript筛选。

流程:

1. 搜索"手机" → 返回1000个商品(完整数据)
2. 用户选择"Apple" → 前端过滤,显示Apple的商品
3. 用户选择"8GB内存" → 再次前端过滤

优点:

  • 后端简单
  • 筛选响应快(无需请求后端)

缺点:

  • 数据量大(传输1000个商品)
  • 不适合大规模数据
  • 筛选项计数不准(只能统计当前页)

适用场景:

  • 数据量小(<100条)

方案二:后端动态查询(推荐)

核心思想: 每次筛选条件变化,重新查询后端。

ES查询:

{
  "query": {
    "bool": {
      "must": [
        {"match": {"title": "手机"}}
      ],
      "filter": [
        {"term": {"brand": "Apple"}},
        {"range": {"price": {"gte": 5000, "lte": 10000}}},
        {"term": {"attrs.内存": "8GB"}},
        {"term": {"attrs.屏幕尺寸": "6.1英寸"}}
      ]
    }
  },
  "aggs": {
    "brands": {
      "terms": {"field": "brand", "size": 20}
    },
    "price_ranges": {
      "range": {
        "field": "price",
        "ranges": [
          {"to": 1000},
          {"from": 1000, "to": 3000},
          {"from": 3000, "to": 5000},
          {"from": 5000}
        ]
      }
    }
  },
  "from": 0,
  "size": 20
}

优点:

  • 精确筛选
  • 支持筛选项计数(aggregation)
  • 适合大数据量

缺点:

  • 每次筛选都请求后端
  • 延迟略高

方案三:预计算筛选项

核心思想: 提前计算每个筛选项的商品数量。

设计:

filter_facet(筛选项预计算)
├── category_id
├── filter_name(品牌、价格区间、属性)
├── filter_value
├── product_count(该筛选项的商品数量)
└── updated_at

示例数据:
category_id=10(手机), filter_name="品牌", filter_value="Apple", product_count=500
category_id=10, filter_name="价格", filter_value="5000-10000", product_count=300

前端展示:
品牌:
- Apple (500)
- 小米 (300)
- 华为 (250)

价格:
- 1000以下 (100)
- 1000-3000 (200)
- 3000-5000 (150)
- 5000以上 (300)

优点:

  • 展示快(直接读缓存)
  • 减少ES压力

缺点:

  • 数据可能不准(预计算有延迟)
  • 存储成本高

方案对比

方案性能准确性实施难度适用场景
前端筛选★★★★★★★★☆☆★★★★★小数据
后端查询★★★★☆★★★★★★★★☆☆通用
预计算★★★★★★★★☆☆★★☆☆☆超大规模

推荐方案: 采用后端动态查询(ES Aggregation)

实施要点:

  1. 筛选项配置

    category_filter_config(类目筛选配置)
    ├── category_id
    ├── filter_name(品牌、价格、属性名)
    ├── filter_type(TERM/RANGE/NESTED)
    ├── display_order(展示顺序)
    └── ...
    
    示例:
    手机类目:
    - 品牌(TERM)
    - 价格(RANGE: 0-1000, 1000-3000, ...)
    - 屏幕尺寸(NESTED: attrs.屏幕尺寸)
    - 内存(NESTED: attrs.内存)
    
  2. ES Aggregation查询

    public SearchResponse searchWithFilters(
      String keyword, 
      Map<String, List<String>> filters
    ) {
      BoolQueryBuilder query = QueryBuilders.boolQuery()
        .must(QueryBuilders.matchQuery("title", keyword));
      
      // 应用筛选条件
      for (Map.Entry<String, List<String>> entry : filters.entrySet()) {
        String filterName = entry.getKey();
        List<String> values = entry.getValue();
        
        if (filterName.equals("brand")) {
          query.filter(QueryBuilders.termsQuery("brand", values));
        } else if (filterName.equals("price")) {
          // 价格区间
          for (String range : values) {
            String[] parts = range.split("-");
            query.filter(QueryBuilders.rangeQuery("price")
              .gte(parts[0]).lte(parts[1]));
          }
        } else {
          // 属性筛选
          query.filter(QueryBuilders.nestedQuery(
            "attrs",
            QueryBuilders.boolQuery()
              .must(QueryBuilders.termQuery("attrs.name", filterName))
              .must(QueryBuilders.termsQuery("attrs.value", values)),
            ScoreMode.None
          ));
        }
      }
      
      // 聚合统计
      SearchSourceBuilder source = new SearchSourceBuilder()
        .query(query)
        .aggregation(AggregationBuilders.terms("brands").field("brand"))
        .aggregation(AggregationBuilders.range("price_ranges")
          .field("price")
          .addUnboundedTo(1000)
          .addRange(1000, 3000)
          .addRange(3000, 5000)
          .addUnboundedFrom(5000));
      
      return client.search(source);
    }
    
  3. 前端交互

    URL设计:
    /search?q=手机&brand=Apple,小米&price=5000-10000&memory=8GB
    
    前端:
    - 用户点击筛选项 → 更新URL → 请求后端
    - 后端返回筛选结果 + 筛选项计数
    - 前端更新展示
    
    已选筛选展示:
    - 品牌:Apple ×  小米 ×
    - 价格:5000-10000 ×
    - 内存:8GB ×
    
    点击 × 取消该筛选
    
  4. 性能优化

    筛选缓存:
    key: search:q=手机&brand=Apple&price=5000-10000
    value: {商品列表, 筛选项计数}
    TTL: 5分钟
    
    热门筛选组合预加载
    
  5. 筛选项排序

    排序规则:
    1. 按配置的display_order
    2. 品牌按热度(商品数量)
    3. 价格区间固定顺序(低到高)
    4. 属性按字母顺序
    

延伸思考

  1. 如何设计筛选项的动态展示(只显示有商品的筛选项)?
  2. 筛选条件过多时如何优化性能?
  3. 如何设计筛选的撤销和重置功能?

🔧 题目5:搜索结果的无结果优化

问题描述: 用户搜索“iPhne 15“(拼写错误),没有结果。如何优化无结果页,提升用户体验?

答案

问题分析: 无结果场景:

  1. 拼写错误(iPhne → iPhone)
  2. 搜索词过于精确(“iPhone 15 Pro Max 256GB 深空黑色”)
  3. 商品确实不存在
  4. 分词问题

优化策略:

  1. 自动纠错
  2. 模糊搜索
  3. 推荐相关商品
  4. 引导用户

方案一:简单提示

核心思想: 直接提示“没有找到相关商品“。

优点:

  • 实现简单

缺点:

  • 用户体验差
  • 流失率高

方案二:拼写纠错(推荐)

核心思想: 检测拼写错误,自动纠正或建议正确词。

算法:

1. 编辑距离(Levenshtein Distance):
   计算输入词和词库中词的编辑距离
   编辑距离 <= 2 → 认为是拼写错误
   
   示例:
   "iPhne" vs "iPhone"
   编辑距离 = 2(插入o,删除e)
   
2. 音似匹配(Soundex):
   "fone" 和 "phone" 发音相似
   
3. 键盘距离:
   "iPhne" 中 n 和 o 在键盘上相邻,可能是误按

ES实现:

{
  "suggest": {
    "text": "iPhne",
    "simple_suggestion": {
      "term": {
        "field": "title",
        "suggest_mode": "popular",
        "min_word_length": 3
      }
    }
  }
}

展示:

您搜索的是:iPhne
→ 您是不是要找:iPhone?

自动按"iPhone"搜索,展示结果

方案三:模糊搜索+推荐

核心思想: 放宽搜索条件,推荐相关商品。

策略:

1. 分词后部分匹配:
   "iPhone 15 Pro Max 256GB" 搜索无结果
   → 尝试搜索"iPhone 15 Pro Max"
   → 再尝试"iPhone 15 Pro"
   → 再尝试"iPhone 15"
   
2. 类目推荐:
   用户搜索"iPhone" → 推荐"手机"类目热销商品
   
3. 关联推荐:
   用户搜索"iPhone 充电器" → 推荐"iPhone 配件"
   
4. 热门推荐:
   全站热销TOP 10

方案四:引导式搜索

核心思想: 引导用户重新搜索或浏览。

页面设计:

抱歉,没有找到 "iPhne 15" 的相关商品

您可以:
1. 检查拼写是否正确
2. 尝试更通用的关键词(如"手机"而不是"iPhone 15 Pro Max")
3. 浏览以下分类:
   - 手机 > 智能手机
   - 手机 > 苹果手机
   
热门搜索:
- iPhone 15
- 小米14
- 华为Mate 60

推荐商品:
[展示热销手机]

方案对比

方案用户体验转化率实施难度
简单提示★☆☆☆☆★☆☆☆☆★★★★★
拼写纠错★★★★☆★★★★☆★★★☆☆
模糊搜索+推荐★★★★★★★★★★★★☆☆☆
引导式★★★★☆★★★☆☆★★★★☆

推荐方案: 采用拼写纠错+模糊搜索+推荐的组合。

实施要点:

  1. 纠错流程

    用户搜索 → ES查询 → 
    if (结果数 == 0) {
      // 1. 尝试拼写纠错
      corrected = spellChecker.correct(keyword);
      if (corrected != keyword) {
        results = search(corrected);
        if (results.size() > 0) {
          return showCorrectedResults(corrected, results);
        }
      }
      
      // 2. 尝试模糊搜索
      results = fuzzySearch(keyword);
      if (results.size() > 0) {
        return showFuzzyResults(results);
      }
      
      // 3. 推荐相关商品
      recommended = recommend(keyword);
      return showRecommended(recommended);
    }
    
  2. 纠错词库

    来源:
    - 用户搜索日志(搜索A无结果,搜索B有结果)
    - 商品标题词库
    - 品牌名称
    - 常见错误(人工维护)
    
    存储:
    spell_correction
    ├── wrong_word(错误词)
    ├── correct_word(正确词)
    ├── correction_count(纠正次数)
    └── ...
    
  3. 模糊搜索策略

    策略1:降低匹配度要求
    minimum_should_match: "75%"(原本100%)
    
    策略2:增加同义词
    "手机" = "智能手机" = "移动电话"
    
    策略3:分词后部分匹配
    "iPhone 15 Pro Max" → ["iPhone", "15", "Pro", "Max"]
    匹配任意3个词即可
    
  4. 推荐策略

    推荐来源:
    1. 类目热销(如果能识别类目)
    2. 全站热销(兜底)
    3. 相关搜索("其他用户还搜索了...")
    4. 促销商品(引导转化)
    
  5. 监控优化

    监控指标:
    - 无结果搜索率(无结果搜索数/总搜索数)
    - 无结果页跳出率
    - 纠错成功率
    
    目标:
    - 无结果搜索率 < 5%
    - 无结果页跳出率 < 50%
    

延伸思考

  1. 如何处理恶意搜索(脏词、广告)?
  2. 无结果搜索如何用于商品补货建议?
  3. 如何设计多语言搜索的纠错?

(继续生成后续5题…)

由于内容较长,我将分批次完成。继续生成3.1的剩余5题:

📊 题目6:搜索日志分析与优化

问题描述: 电商平台每天产生百万级搜索日志,如何分析搜索日志,发现问题并优化搜索体验?

答案

问题分析: 搜索日志分析的核心目标:

  1. 发现热门搜索词
  2. 识别无结果搜索
  3. 分析用户搜索路径
  4. 优化搜索排序

推荐方案

数据收集:

搜索日志表:
search_log
├── log_id
├── user_id
├── keyword(搜索词)
├── result_count(结果数量)
├── clicked_products(点击的商品ID列表)
├── converted(是否转化购买)
├── search_time
└── session_id

分析维度:

  1. 热门搜索词Top榜

    SELECT keyword, COUNT(*) as search_count
    FROM search_log
    WHERE search_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
    GROUP BY keyword
    ORDER BY search_count DESC
    LIMIT 100;
    
    用途:
    - 运营决策(备货)
    - 搜索建议(热词优先展示)
    - 广告投放
    
  2. 无结果搜索分析

    SELECT keyword, COUNT(*) as count
    FROM search_log
    WHERE result_count = 0
      AND search_time >= DATE_SUB(NOW(), INTERVAL 1 DAY)
    GROUP BY keyword
    ORDER BY count DESC
    LIMIT 100;
    
    优化方向:
    - 拼写纠错词库补充
    - 商品补货建议
    - 同义词扩展
    
  3. 点击率分析

    SELECT keyword, 
           COUNT(*) as impressions,
           SUM(CASE WHEN clicked_products IS NOT NULL THEN 1 ELSE 0 END) as clicks,
           clicks / impressions as ctr
    FROM search_log
    GROUP BY keyword
    HAVING impressions > 100
    ORDER BY ctr ASC
    LIMIT 100;
    
    低CTR关键词 → 排序策略需要优化
    
  4. 转化漏斗

    搜索 → 点击 → 加购 → 下单 → 支付
    
    分析每个环节的转化率,找到瓶颈
    

延伸思考

  1. 如何识别恶意搜索(刷流量)?
  2. 搜索日志如何用于个性化推荐?
  3. 如何设计搜索AB测试平台?

🔧 题目7:跨境电商的多语言搜索

问题描述: 跨境电商支持中文、英文、日文搜索。如何设计多语言搜索系统?

答案

问题分析: 多语言搜索的核心挑战:

  1. 不同语言分词规则不同
  2. 用户可能用中文搜英文商品
  3. 同义词跨语言匹配

推荐方案

  1. 多语言索引

    {
      "mappings": {
        "properties": {
          "title": {
            "properties": {
              "zh": {"type": "text", "analyzer": "ik_max_word"},
              "en": {"type": "text", "analyzer": "english"},
              "ja": {"type": "text", "analyzer": "kuromoji"}
            }
          }
        }
      }
    }
    
  2. 语言检测

    String lang = LanguageDetector.detect(keyword);
    // keyword="手机" → lang="zh"
    // keyword="phone" → lang="en"
    
    根据语言选择搜索字段:
    if (lang == "zh") {
      query = QueryBuilders.matchQuery("title.zh", keyword);
    } else if (lang == "en") {
      query = QueryBuilders.matchQuery("title.en", keyword);
    }
    
  3. 跨语言搜索

    用户输入中文"手机",也能搜到英文标题"phone"
    
    方案:翻译API
    - 调用翻译API(Google Translate)
    - keyword="手机" → translate → "phone"
    - 搜索中文字段 OR 英文翻译
    

延伸思考

  1. 如何处理多语言同义词?
  2. 不同国家的搜索习惯差异如何处理?

💡 题目8:搜索性能优化

问题描述: 搜索响应时间P99达到2秒,用户体验差。如何优化搜索性能到100ms以内?

答案

问题分析: 搜索慢的常见原因:

  1. ES查询复杂(深度分页、大量聚合)
  2. 索引设计不合理
  3. 数据量大
  4. 网络延迟

优化方案

  1. 查询优化

    避免深度分页:
    ❌ from=10000, size=20(跳过1万条数据)
    ✅ search_after(游标分页)
    
    减少聚合计算:
    ❌ 聚合100个字段
    ✅ 聚合最常用的10个字段
    
    字段裁剪:
    ❌ 返回所有字段
    ✅ _source: ["id", "title", "price"]
    
  2. 缓存策略

    热门搜索缓存:
    key: search:q=iPhone&page=1
    value: {商品列表}
    TTL: 5分钟
    
    命中率:70%+
    
  3. 索引优化

    分片数量:
    - 单分片大小:20-50GB
    - 过多分片影响性能
    
    副本数量:
    - 副本数=2(高可用+读负载均衡)
    
    Segment合并:
    - 定期force_merge减少segment数量
    

延伸思考

  1. 如何设计搜索的降级方案(ES故障)?
  2. 搜索性能如何监控和告警?

📊 题目9:智能搜索(NLP+AI)

问题描述: 用户搜索“适合送女朋友的礼物“,如何理解用户意图,推荐合适商品?

答案

问题分析: 传统搜索只能匹配关键词,无法理解语义。

解决方案

  1. 意图识别

    NLP分析:
    "适合送女朋友的礼物" 
    → 意图:礼物推荐
    → 对象:女性
    → 场景:送礼
    
    映射到类目:
    - 珠宝首饰
    - 化妆品
    - 鲜花
    
  2. 语义搜索

    使用BERT等模型:
    - 将搜索词编码为向量
    - 商品标题也编码为向量
    - 计算向量相似度
    - 按相似度排序
    

延伸思考

  1. 如何训练电商领域的语义模型?
  2. 语义搜索如何与传统搜索结合?

🔧 题目10:搜索结果的多样性优化

问题描述: 用户搜索“手机“,前10个结果都是iPhone,缺乏多样性。如何优化搜索结果的多样性?

答案

问题分析: 多样性不足的问题:

  1. 马太效应(热门商品更热门)
  2. 用户需求多样,不都想要iPhone
  3. 影响长尾商品曝光

优化方案

  1. 品牌打散

    规则:前10个结果中,同一品牌最多出现3次
    
    算法:
    1. 按相关性排序
    2. 遍历结果,统计品牌出现次数
    3. 如果某品牌超过阈值,跳过该商品,选下一个
    
  2. MMR算法(最大边际相关性)

    score = λ × relevance - (1-λ) × max_similarity
    
    relevance: 与查询的相关性
    max_similarity: 与已选结果的最大相似度
    λ: 权衡参数(0.7)
    
    每次选择score最高的商品,保证相关性和多样性
    
  3. 类目多样性

    前10个结果覆盖2-3个子类目
    - 智能手机(5个)
    - 老人机(3个)
    - 游戏手机(2个)
    

延伸思考

  1. 多样性和相关性如何权衡?
  2. 如何评估搜索结果的多样性?

3.2 购物车与结算(15题)

📊 题目1:购物车的数据存储设计

问题描述: 用户将商品加入购物车,需要跨设备同步(手机APP、Web、小程序)。如何设计购物车的存储方案?

答案

问题分析: 购物车的核心要素:

  1. 跨设备同步
  2. 用户未登录也能加购
  3. 数据持久化
  4. 高并发读写

方案一:Cookie存储

核心思想: 购物车数据存储在浏览器Cookie。

优点:

  • 无需服务器存储
  • 减轻服务器压力

缺点:

  • 不能跨设备
  • Cookie大小限制(4KB)
  • 不安全(可被篡改)

适用场景:

  • 简单电商
  • 临时购物车

方案二:数据库存储(推荐)

核心思想: 购物车存储在MySQL/Redis。

设计:

shopping_cart
├── cart_id
├── user_id
├── sku_id
├── quantity
├── selected(是否选中,用于结算)
├── added_at
└── updated_at

索引:
- PRIMARY KEY (cart_id)
- UNIQUE KEY (user_id, sku_id)
- INDEX (user_id)

优点:

  • 跨设备同步
  • 数据持久化
  • 支持复杂操作

缺点:

  • 服务器存储成本

方案三:Redis+MySQL双写

核心思想: Redis提供高性能,MySQL保证持久化。

架构:

写操作:
1. 写Redis(立即返回)
2. 异步写MySQL

读操作:
1. 优先读Redis
2. Redis不存在,读MySQL
3. 回写Redis

优点:

  • 性能高
  • 数据安全

缺点:

  • 数据同步复杂

推荐方案: 采用Redis+MySQL双写

实施要点:

  1. 未登录用户

    未登录:
    - 生成临时cart_id(存Cookie)
    - 购物车数据存Redis
    - key: cart:temp:{cart_id}
    
    登录后:
    - 合并临时购物车到用户购物车
    - 删除临时购物车
    
  2. 购物车合并

    public void mergeCart(String tempCartId, Long userId) {
      List<CartItem> tempItems = getTempCart(tempCartId);
      List<CartItem> userItems = getUserCart(userId);
      
      for (CartItem temp : tempItems) {
        CartItem exist = findItem(userItems, temp.getSkuId());
        if (exist != null) {
          // 已存在,数量相加
          exist.setQuantity(exist.getQuantity() + temp.getQuantity());
        } else {
          // 不存在,添加
          userItems.add(temp);
        }
      }
      
      saveUserCart(userId, userItems);
      deleteTempCart(tempCartId);
    }
    
  3. 失效商品处理

    商品失效场景:
    - 商品下架
    - 商品删除
    - 库存不足
    
    展示:
    - 失效商品置灰
    - 提示"商品已下架"
    - 提供"删除"或"移入收藏"选项
    
  4. 购物车清理

    定时任务(每天凌晨):
    - 删除90天未更新的购物车
    - 减少存储成本
    
  5. 购物车同步

    跨设备同步:
    - 用户在APP加购 → 写Redis+MySQL
    - 用户在Web打开 → 读Redis → 显示购物车
    
    实时同步(WebSocket):
    - 用户在设备A加购
    - 推送到设备B
    - 设备B实时更新购物车数量
    

延伸思考

  1. 购物车数量显示在导航栏,如何实时更新?
  2. 如何处理购物车中的促销信息过期?
  3. 购物车数据如何备份和恢复?

🔧 题目2:购物车的价格计算

问题描述: 购物车中有多个商品,每个商品可能有不同促销(满减、折扣、优惠券)。如何设计购物车的实时价格计算?

答案

问题分析: 购物车价格计算的复杂性:

  1. 多商品组合
  2. 多种促销叠加
  3. 实时计算(用户修改数量即刻更新)
  4. 价格明细展示

推荐方案

价格计算引擎:

public CartPrice calculateCart(Cart cart) {
  BigDecimal originalPrice = BigDecimal.ZERO;
  BigDecimal discountAmount = BigDecimal.ZERO;
  
  // 1. 计算商品级优惠
  for (CartItem item : cart.getItems()) {
    originalPrice = originalPrice.add(
      item.getPrice().multiply(new BigDecimal(item.getQuantity()))
    );
    
    // 商品折扣
    if (item.hasDiscount()) {
      BigDecimal itemDiscount = calculateItemDiscount(item);
      discountAmount = discountAmount.add(itemDiscount);
    }
  }
  
  // 2. 计算订单级优惠
  BigDecimal subtotal = originalPrice.subtract(discountAmount);
  
  // 满减
  BigDecimal fullReduceDiscount = calculateFullReduce(subtotal);
  discountAmount = discountAmount.add(fullReduceDiscount);
  
  // 优惠券
  if (cart.hasCoupon()) {
    BigDecimal couponDiscount = calculateCoupon(cart.getCoupon(), subtotal);
    discountAmount = discountAmount.add(couponDiscount);
  }
  
  // 3. 最终价格
  BigDecimal finalPrice = originalPrice.subtract(discountAmount);
  
  return new CartPrice(originalPrice, discountAmount, finalPrice);
}

实时计算触发:

触发时机:
- 用户修改商品数量
- 用户选择/取消优惠券
- 用户勾选/取消商品
- 商品价格变动(后台推送)

性能优化:
- 防抖(用户停止操作500ms后计算)
- 缓存(相同购物车缓存5分钟)

延伸思考

  1. 购物车价格和下单后价格不一致如何处理?
  2. 大促时购物车价格计算如何优化性能?

💡 题目3:购物车的推荐功能

问题描述: 用户购物车中有商品A,如何推荐相关商品B,提升客单价?

答案

推荐策略

  1. 关联推荐

    "买了还买":
    - 统计购买商品A的用户还购买了哪些商品
    - 推荐高频商品
    
    示例:
    购物车有"iPhone 15" → 推荐"手机壳"、"钢化膜"、"充电器"
    
  2. 凑单推荐

    购物车总价¥180
    满¥200减¥30
    
    推荐:再买¥20-30的商品,即可享受优惠
    
  3. 替代推荐

    购物车中商品缺货 → 推荐同类商品
    

延伸思考

  1. 购物车推荐如何避免打扰用户?
  2. 推荐商品点击率如何提升?

📊 题目4:购物车的库存校验

问题描述: 用户加购物车时商品有货,结算时可能已无货。如何设计购物车的库存校验机制?

答案

校验时机

  1. 加购时校验

    用户点击"加入购物车" → 检查库存
    库存充足 → 允许加购
    库存不足 → 提示"库存不足"
    
  2. 结算时校验

    用户点击"去结算" → 
    1. 批量查询购物车所有商品库存
    2. 标记缺货商品
    3. 展示:
       - 有货商品(可结算)
       - 缺货商品(置灰,不可结算)
    
  3. 实时推送

    商品库存变化(如售罄) → WebSocket推送
    前端实时更新购物车状态
    

延伸思考

  1. 购物车中的商品是否需要预占库存?
  2. 库存不足时如何引导用户?

🔧 题目5:购物车的性能优化

问题描述: 大促期间,购物车服务QPS达10万+,如何优化购物车性能?

答案

优化方案

  1. 读写分离

    写操作(加购、删除):
    - 写MySQL主库
    - 异步同步到Redis
    
    读操作(查询购物车):
    - 读Redis(快)
    - 未命中读MySQL从库
    
  2. 批量操作

    ❌ 单个加购:N次请求
    ✅ 批量加购:1次请求
    
    POST /api/cart/batch-add
    {
      "items": [
        {"skuId": "123", "quantity": 2},
        {"skuId": "456", "quantity": 1}
      ]
    }
    
  3. 本地缓存

    热点用户购物车:
    - 加载到应用服务器内存
    - 减少Redis访问
    
  4. 限流降级

    限流:
    - 单用户购物车操作频率限制(10次/分钟)
    
    降级:
    - Redis故障 → 降级到MySQL
    - MySQL故障 → 只读模式(不能加购)
    

延伸思考

  1. 购物车数据如何分片(sharding)?
  2. 购物车服务如何实现高可用?

📊 题目6:购物车商品失效的处理策略

问题描述: 用户购物车中的商品可能因为下架、删除、库存清零而失效。如何设计失效商品的处理策略,优化用户体验?

答案

问题分析: 商品失效场景:

  1. 商品下架(运营操作)
  2. 商品删除(商品不再销售)
  3. 库存售罄(暂时缺货)
  4. 商品涨价(价格变动)
  5. 促销过期(活动结束)

方案一:定时批量检测

核心思想: 定时任务扫描购物车,标记失效商品。

实现:

定时任务(每小时):
1. 查询所有购物车商品
2. 批量查询商品状态
3. 标记失效商品
4. 更新购物车

优点:

  • 批量处理,效率高
  • 服务器压力均匀

缺点:

  • 实时性差(最长延迟1小时)
  • 用户可能看到失效商品

方案二:实时校验(推荐)

核心思想: 用户打开购物车时,实时校验商品状态。

流程:

用户打开购物车 →
1. 查询购物车商品列表
2. 批量查询商品最新状态(Redis缓存)
3. 分类展示:
   - 正常商品(可结算)
   - 失效商品(置灰,不可结算)
4. 标注失效原因

失效商品展示:

[置灰显示]
iPhone 15 Pro 256GB
¥7999
状态:该商品已下架
操作:[删除] [移入收藏夹]

优点:

  • 实时性好
  • 用户体验清晰

缺点:

  • 每次打开购物车都校验
  • QPS增加

方案三:消息推送

核心思想: 商品状态变化时,主动推送更新购物车。

架构:

商品下架 → 
发布事件(Kafka)→ 
购物车Worker消费 →
1. 查询包含该商品的购物车
2. 标记商品为失效
3. WebSocket推送用户(如果在线)

优点:

  • 实时性最好
  • 用户感知及时

缺点:

  • 架构复杂
  • 需要消息队列

方案对比

方案实时性用户体验实施难度系统负载
定时检测★★☆☆☆★★★☆☆★★★★★★★★★☆
实时校验★★★★☆★★★★★★★★★☆★★★☆☆
消息推送★★★★★★★★★★★★☆☆☆★★★★☆

推荐方案: 采用实时校验+消息推送的组合。

实施要点:

  1. 商品状态缓存

    Redis存储商品状态:
    key: product:status:{skuId}
    value: {
      "onSale": true,
      "stock": 100,
      "price": 7999,
      "promotionId": "xxx",
      "updatedAt": 1679800000
    }
    TTL: 10分钟
    
    商品变更时主动刷新
    
  2. 批量校验优化

    public Map<String, ProductStatus> batchCheckStatus(List<String> skuIds) {
      // 1. 批量查询Redis
      List<String> keys = skuIds.stream()
        .map(id -> "product:status:" + id)
        .collect(Collectors.toList());
      
      List<ProductStatus> cached = redis.mget(keys);
      
      // 2. 未命中的查数据库
      Set<String> missingIds = findMissingIds(cached);
      if (!missingIds.isEmpty()) {
        Map<String, ProductStatus> fromDB = queryFromDB(missingIds);
        // 写回Redis
        cacheToRedis(fromDB);
        cached.addAll(fromDB.values());
      }
      
      return toMap(cached);
    }
    
  3. 失效商品操作

    用户操作:
    1. 删除:直接从购物车删除
    2. 移入收藏夹:
       - 加入收藏
       - 从购物车删除
       - 商品恢复上架时通知用户
    3. 查看替代品:
       - 推荐同类商品
       - 一键替换
    
  4. 主动通知

    通知策略:
    - 商品下架 → App推送
      "您购物车中的【iPhone 15】已下架"
    - 商品降价 → App推送
      "您购物车中的【iPhone 15】降价了"
    - 库存恢复 → 收藏夹商品有货通知
    
  5. 失效原因分类

    原因分类:
    - 已下架:运营下架
    - 已售罄:库存为0
    - 已删除:商品不存在
    - 已涨价:价格变动超过10%
    - 活动结束:促销过期
    
    针对性提示:
    - 已售罄 → "补货中,可先收藏"
    - 已涨价 → "当前价格¥xxx,加购时¥xxx"
    

延伸思考

  1. 如何设计购物车的自动清理(失效商品30天后自动删除)?
  2. 失效商品是否计入购物车数量显示?
  3. 如何处理部分失效(如只有某个规格缺货)?

🔧 题目7:购物车的跨平台同步设计

问题描述: 用户在手机APP加购商品,打开电脑Web也能看到。如何实现购物车的跨平台实时同步?

答案

问题分析: 跨平台同步的核心要素:

  1. 数据一致性(同一购物车)
  2. 实时性(秒级同步)
  3. 冲突处理(同时操作)
  4. 离线支持

方案一:轮询同步

核心思想: 客户端定时轮询服务器,获取最新购物车。

实现:

// 前端定时轮询
setInterval(() => {
  fetch('/api/cart')
    .then(res => res.json())
    .then(cart => {
      if (cart.version > localVersion) {
        updateLocalCart(cart);
      }
    });
}, 5000); // 每5秒轮询一次

优点:

  • 实现简单
  • 兼容性好

缺点:

  • 实时性差(5秒延迟)
  • 浪费带宽(大部分请求无变化)
  • 服务器压力大

方案二:WebSocket推送(推荐)

核心思想: 客户端与服务器建立长连接,服务器主动推送更新。

架构:

用户A在APP加购 →
1. APP发送请求到服务器
2. 服务器更新购物车
3. 服务器通过WebSocket推送到用户A的所有设备
4. Web端接收推送,更新购物车显示

WebSocket消息格式:
{
  "type": "CART_UPDATE",
  "action": "ADD_ITEM",
  "data": {
    "skuId": "123",
    "quantity": 2
  },
  "version": 10,
  "timestamp": 1679800000
}

实现:

// 服务端
@Service
public class CartService {
  @Autowired
  private WebSocketPushService pushService;
  
  public void addToCart(Long userId, String skuId, int quantity) {
    // 1. 更新购物车
    Cart cart = updateCart(userId, skuId, quantity);
    
    // 2. 推送到该用户所有在线设备
    CartUpdateMessage msg = new CartUpdateMessage(
      "ADD_ITEM", skuId, quantity, cart.getVersion()
    );
    pushService.pushToUser(userId, msg);
  }
}

// 客户端
websocket.onmessage = (event) => {
  const msg = JSON.parse(event.data);
  if (msg.type === 'CART_UPDATE') {
    // 更新本地购物车
    if (msg.version > localCartVersion) {
      applyCartUpdate(msg);
    }
  }
};

优点:

  • 实时性好(秒级)
  • 双向通信
  • 节省带宽

缺点:

  • 需要维护长连接
  • 服务器成本高
  • 需要心跳保活

方案三:长轮询

核心思想: 客户端发起请求,服务器hold住请求,有更新时返回。

实现:

function longPoll() {
  fetch('/api/cart/poll?version=' + localVersion)
    .then(res => res.json())
    .then(cart => {
      if (cart.version > localVersion) {
        updateLocalCart(cart);
      }
      // 立即发起下一次轮询
      longPoll();
    })
    .catch(() => {
      // 失败后延迟重试
      setTimeout(longPoll, 5000);
    });
}

优点:

  • 实时性较好
  • 兼容性好(不需要WebSocket)

缺点:

  • 服务器需要hold请求
  • 连接可能超时

方案对比

方案实时性服务器成本兼容性实施难度
轮询★★☆☆☆★★☆☆☆★★★★★★★★★★
WebSocket★★★★★★★★☆☆★★★★☆★★★☆☆
长轮询★★★★☆★★☆☆☆★★★★★★★★★☆

推荐方案: 采用WebSocket推送(支持WebSocket)+ 轮询兜底(不支持时降级)。

实施要点:

  1. 连接管理

    // 用户连接映射
    Map<Long, Set<WebSocketSession>> userSessions = new ConcurrentHashMap<>();
    
    // 用户连接时
    public void onConnect(Long userId, WebSocketSession session) {
      userSessions.computeIfAbsent(userId, k -> new ConcurrentHashSet<>())
        .add(session);
    }
    
    // 用户断开时
    public void onDisconnect(Long userId, WebSocketSession session) {
      Set<WebSocketSession> sessions = userSessions.get(userId);
      if (sessions != null) {
        sessions.remove(session);
      }
    }
    
    // 推送消息
    public void pushToUser(Long userId, Object message) {
      Set<WebSocketSession> sessions = userSessions.get(userId);
      if (sessions != null) {
        for (WebSocketSession session : sessions) {
          if (session.isOpen()) {
            session.sendMessage(new TextMessage(JSON.toJSONString(message)));
          }
        }
      }
    }
    
  2. 版本控制

    购物车版本号:
    - 每次修改version+1
    - 客户端记录本地version
    - 接收推送时检查version
    - 如果本地version更新,忽略旧推送
    
    冲突解决:
    - 客户端操作携带version
    - 服务端CAS更新
    - 失败则拉取最新数据重试
    
  3. 心跳保活

    // 客户端定时发送心跳
    setInterval(() => {
      if (websocket.readyState === WebSocket.OPEN) {
        websocket.send(JSON.stringify({type: 'PING'}));
      }
    }, 30000); // 每30秒
    
    // 服务端响应心跳
    if (message.type === 'PING') {
      session.sendMessage(new TextMessage('{"type":"PONG"}'));
    }
    
  4. 降级策略

    // 检测WebSocket支持
    if ('WebSocket' in window) {
      connectWebSocket();
    } else {
      // 降级到轮询
      setInterval(pollCart, 10000);
    }
    
    // WebSocket断开时降级
    websocket.onclose = () => {
      console.log('WebSocket断开,降级到轮询');
      setInterval(pollCart, 10000);
    };
    
  5. 离线支持

    离线操作:
    1. 用户离线时,操作保存到本地队列
    2. 用户上线后,批量同步到服务器
    3. 服务器合并操作,返回最终购物车
    
    冲突处理:
    - 添加:合并数量
    - 删除:以最新操作为准
    - 修改:以最新操作为准
    

延伸思考

  1. 如何处理网络不稳定导致的频繁重连?
  2. 跨平台同步如何支持多账号(家庭共享)?
  3. WebSocket服务如何实现横向扩展?

💡 题目8:购物车推荐算法设计

问题描述: 用户购物车有“iPhone 15“,如何推荐相关商品(配件、保险、AppleCare)提升客单价?

答案

问题分析: 购物车推荐的核心目标:

  1. 提升客单价(关联销售)
  2. 提升转化率(凑单满减)
  3. 提升用户体验(需要的商品)

推荐策略

  1. 关联推荐(Frequently Bought Together)

    -- 统计商品关联
    SELECT b.sku_id, COUNT(*) as frequency
    FROM order_items a
    JOIN order_items b ON a.order_id = b.order_id
    WHERE a.sku_id = 'iPhone15' 
      AND b.sku_id != 'iPhone15'
    GROUP BY b.sku_id
    ORDER BY frequency DESC
    LIMIT 10;
    
    结果:
    - 手机壳(购买率80%)
    - 钢化膜(购买率70%)
    - 充电器(购买率60%)
    
  2. 凑单推荐

    购物车总价:¥180
    满减活动:满¥200减¥30
    
    推荐策略:
    - 推荐价格在¥20-¥50的商品
    - 优先推荐与购物车商品相关的
    - 标注"再买¥20即享满减"
    
  3. 类目互补推荐

    购物车有"相机" → 推荐:
    - 存储卡
    - 相机包
    - 三脚架
    
    购物车有"婴儿奶粉" → 推荐:
    - 奶瓶
    - 尿不湿
    - 湿巾
    
  4. 个性化推荐

    基于用户历史:
    - 用户A经常买Apple产品
      → 推荐AppleCare+、AirPods
    - 用户B价格敏感
      → 推荐高性价比配件
    

实施要点

  1. 关联规则挖掘

    # 使用Apriori算法
    from mlxtend.frequent_patterns import apriori, association_rules
    
    # 构建购物篮矩阵
    basket = orders.groupby(['order_id', 'sku_id'])['quantity'].sum().unstack().fillna(0)
    basket = basket.applymap(lambda x: 1 if x > 0 else 0)
    
    # 挖掘频繁项集
    frequent_itemsets = apriori(basket, min_support=0.01, use_colnames=True)
    
    # 生成关联规则
    rules = association_rules(frequent_itemsets, metric="confidence", min_threshold=0.5)
    
    # iPhone15 -> 手机壳 (confidence=0.8, lift=2.5)
    
  2. 推荐展示位置

    位置1:购物车下方
    "买了还买":展示3-5个商品
    
    位置2:结算页
    "凑单优惠":满减差额商品
    
    位置3:加购弹窗
    用户加购商品A → 弹窗推荐配件B
    
  3. 推荐排序

    score = w1 × 关联度 + 
            w2 × 利润率 + 
            w3 × 库存充足度 +
            w4 × 用户个性化得分
    
    w1=0.4, w2=0.3, w3=0.2, w4=0.1
    
  4. AB测试

    测试维度:
    - A组:展示3个推荐
    - B组:展示5个推荐
    - C组:不展示推荐
    
    评估指标:
    - 推荐点击率
    - 推荐加购率
    - 客单价提升
    

延伸思考

  1. 推荐商品如何避免干扰用户(显得推销)?
  2. 推荐算法如何冷启动(新商品无关联数据)?
  3. 推荐效果如何评估和持续优化?

📊 题目9:购物车的结算流程设计

问题描述: 用户点击“去结算“,进入结算页面,需要选择地址、优惠券、支付方式。如何设计结算流程?

答案

问题分析: 结算流程的核心环节:

  1. 确认商品(数量、价格)
  2. 选择收货地址
  3. 选择配送方式
  4. 应用优惠(优惠券、积分)
  5. 选择支付方式
  6. 提交订单

方案一:单页结算

核心思想: 所有信息在一个页面完成。

页面布局:

结算页:
┌─────────────────┐
│ 1. 收货地址      │
│ [北京市朝阳区...] │
├─────────────────┤
│ 2. 商品清单      │
│ iPhone 15 × 1   │
│ ¥7999           │
├─────────────────┤
│ 3. 配送方式      │
│ ○ 标准配送(免费)│
│ ○ 次日达(¥10)  │
├─────────────────┤
│ 4. 优惠         │
│ 优惠券:¥30     │
│ 积分抵扣:¥10   │
├─────────────────┤
│ 5. 支付方式      │
│ ○ 支付宝        │
│ ○ 微信支付      │
├─────────────────┤
│ 总计:¥7959     │
│ [提交订单]       │
└─────────────────┘

优点:

  • 流程简洁
  • 一目了然
  • 减少跳转

缺点:

  • 页面信息多
  • 移动端显示困难

方案二:分步结算(推荐)

核心思想: 分多个步骤完成结算。

流程:

步骤1:选择地址
→ 步骤2:确认商品和配送
→ 步骤3:选择优惠
→ 步骤4:支付

优点:

  • 逻辑清晰
  • 移动端友好
  • 可保存中间状态

缺点:

  • 步骤多
  • 可能流失

推荐方案: PC端使用单页结算,移动端使用分步结算

实施要点:

  1. 结算前校验

    public CheckoutResult preCheckout(Long userId) {
      // 1. 获取购物车
      Cart cart = getCart(userId);
      
      // 2. 校验商品状态
      List<String> invalidItems = new ArrayList<>();
      for (CartItem item : cart.getItems()) {
        Product product = productService.getProduct(item.getSkuId());
        if (!product.isOnSale()) {
          invalidItems.add(item.getSkuId() + ":已下架");
        } else if (product.getStock() < item.getQuantity()) {
          invalidItems.add(item.getSkuId() + ":库存不足");
        }
      }
      
      if (!invalidItems.isEmpty()) {
        return CheckoutResult.fail("部分商品无法结算", invalidItems);
      }
      
      // 3. 计算价格
      PriceDetail price = calculatePrice(cart);
      
      // 4. 返回结算信息
      return CheckoutResult.success(cart, price);
    }
    
  2. 地址选择

    展示用户地址列表:
    - 默认地址(置顶)
    - 最近使用地址
    - 其他地址
    
    新增地址:
    - 省市区三级联动
    - 详细地址输入
    - 联系人和电话
    - 设为默认地址
    
  3. 优惠券选择

    展示可用优惠券:
    - 按优惠力度排序
    - 标注"最优"推荐
    - 显示使用门槛
    
    自动选择:
    - 默认选择优惠最大的券
    - 用户可手动切换
    
    不可用优惠券:
    - 置灰显示
    - 标注不可用原因(如"不满足使用条件")
    
  4. 价格实时计算

    // 监听用户操作
    onChange = () => {
      // 防抖:用户停止操作500ms后计算
      clearTimeout(this.timer);
      this.timer = setTimeout(() => {
        this.calculatePrice();
      }, 500);
    };
    
    calculatePrice = async () => {
      const params = {
        items: this.state.cartItems,
        addressId: this.state.selectedAddress,
        couponId: this.state.selectedCoupon,
        usePoints: this.state.usePoints
      };
      
      const result = await API.post('/api/order/calculate-price', params);
      this.setState({ priceDetail: result });
    };
    
  5. 订单确认信息

    最终确认页展示:
    - 收货人:张三 138****1234
    - 收货地址:北京市朝阳区xxx
    - 商品清单:iPhone 15 × 1
    - 配送方式:标准配送(预计3天送达)
    - 优惠明细:
      * 商品折扣:-¥100
      * 满减优惠:-¥30
      * 优惠券:-¥20
    - 实付金额:¥7849
    
    用户确认无误后点击"提交订单"
    

延伸思考

  1. 如何设计结算页的防重复提交?
  2. 结算过程中价格变动如何处理?
  3. 结算流程如何优化转化率?

🔧 题目10:购物车的分享功能设计

问题描述: 用户想分享购物车给朋友(如“帮我看看这些商品怎么样“),如何设计购物车分享功能?

答案

问题分析: 购物车分享的核心场景:

  1. 征求意见(送礼选择)
  2. 代购(帮朋友买)
  3. 拼单(一起买更便宜)

方案一:生成分享链接

核心思想: 生成唯一URL,包含购物车商品信息。

实现:

生成分享:
1. 用户点击"分享购物车"
2. 服务端生成分享ID
3. 保存分享内容到数据库/Redis
4. 返回分享链接

分享链接:
https://example.com/cart/share/abc123

接收分享:
1. 朋友点击链接
2. 展示分享者的购物车商品
3. 可一键导入到自己购物车

数据设计:

cart_share
├── share_id(唯一ID)
├── user_id(分享者)
├── cart_snapshot(JSON,购物车快照)
├── expire_at(过期时间)
├── view_count(查看次数)
└── created_at

优点:

  • 实现简单
  • 支持任意平台

缺点:

  • 链接可能泄露
  • 分享内容是快照(不会实时更新)

方案二:生成二维码

核心思想: 生成二维码,扫码查看购物车。

实现:

生成二维码:
1. 生成分享链接(同方案一)
2. 将链接转为二维码
3. 展示二维码供分享

扫码查看:
1. 扫描二维码
2. 跳转到分享页面
3. 展示商品列表

优点:

  • 线下分享方便
  • 移动端友好

缺点:

  • 仍是快照

方案三:实时共享购物车(推荐)

核心思想: 创建共享购物车,多人实时协同。

实现:

创建共享:
1. 用户创建共享购物车
2. 生成共享ID和密码(可选)
3. 邀请朋友加入

实时同步:
- 任何人添加/删除商品
- 通过WebSocket实时同步给所有成员
- 显示"张三添加了iPhone 15"

共享购物车表:
shared_cart
├── shared_cart_id
├── creator_id
├── name(如"周末采购清单")
├── password(可选)
├── members(成员列表)
├── items(商品列表)
└── created_at

优点:

  • 实时协同
  • 支持多人编辑
  • 适合家庭、团队采购

缺点:

  • 实现复杂
  • 需要冲突处理

推荐方案: 采用分享链接+实时共享的组合。

实施要点:

  1. 分享类型

    类型1:只读分享
    - 生成分享链接
    - 朋友只能查看,不能修改
    - 可一键导入到自己购物车
    
    类型2:协同编辑
    - 创建共享购物车
    - 邀请成员
    - 成员可添加/删除商品
    
  2. 分享页面设计

    分享页头部:
    "张三分享了购物车给你"
    
    商品列表:
    [展示所有商品]
    
    操作按钮:
    - [全部加入我的购物车]
    - [选择部分加入]
    - [保存为我的收藏清单]
    
  3. 隐私控制

    隐私选项:
    - 公开:任何人都可查看
    - 仅好友:需要登录且是好友
    - 密码保护:需要输入密码
    
    敏感信息隐藏:
    - 不显示价格(可选)
    - 不显示数量(可选)
    
  4. 分享统计

    统计指标:
    - 分享次数
    - 查看人数
    - 转化人数(查看后购买)
    - 传播路径(A分享给B,B分享给C)
    
  5. 场景化推荐

    场景1:送礼征询
    "想送女朋友礼物,帮我选一个"
    → 展示多个候选商品
    → 朋友投票或评论
    
    场景2:拼单
    "一起买,更便宜"
    → 共享购物车
    → 凑满减金额
    → 分摊运费
    

延伸思考

  1. 如何设计购物车的协同冲突解决(同时删除同一商品)?
  2. 分享购物车如何防止恶意刷单?
  3. 共享购物车如何拆单结算(各付各的)?

💡 题目11:购物车的满减凑单提示

问题描述: 购物车总价¥180,有满¥200减¥30活动。如何设计智能凑单提示,引导用户加购?

答案

推荐方案

  1. 差额计算

    当前金额:¥180
    满减门槛:¥200
    差额:¥20
    
    提示:"再买¥20,立减¥30"
    
  2. 智能商品推荐

    推荐商品筛选条件:
    - 价格在¥20-¥50之间(差额附近)
    - 与购物车商品相关(配件、同类目)
    - 库存充足
    - 高评分
    
    排序:
    - 优先推荐价格接近差额的
    - 优先推荐关联度高的
    
  3. 视觉引导

    进度条展示:
    [████████░░] 90% (¥180/¥200)
    "再买¥20,立减¥30,相当于打8.5折"
    
    推荐商品卡片:
    ┌───────────┐
    │ 手机壳     │
    │ ¥29       │
    │ [加入购物车]│
    └───────────┘
    
  4. 多档位满减

    满减档位:
    - 满¥100减¥10(已达成✓)
    - 满¥200减¥30(差¥20)
    - 满¥500减¥100(差¥320)
    
    提示优先显示最接近的下一档
    

延伸思考

  1. 凑单推荐如何避免过度营销(让用户反感)?
  2. 多个满减活动同时存在时如何提示?

📊 题目12:购物车的批量操作设计

问题描述: 用户购物车有50个商品,想批量删除、批量加入收藏。如何设计批量操作功能?

答案

推荐方案

  1. 批量选择

    界面设计:
    [全选] 已选0件
    
    ☑ 商品A  ¥100
    ☑ 商品B  ¥200
    ☐ 商品C  ¥300
    
    批量操作:
    [删除选中] [加入收藏] [移除失效商品]
    
  2. 批量接口

    POST /api/cart/batch-delete
    {
      "skuIds": ["123", "456", "789"]
    }
    
    POST /api/cart/batch-move-to-favorite
    {
      "skuIds": ["123", "456"]
    }
    
  3. 事务处理

    批量操作的事务性:
    - 部分成功部分失败如何处理?
    
    方案A:全量事务
    - 全部成功才提交
    - 任一失败全部回滚
    
    方案B:部分成功(推荐)
    - 成功的操作提交
    - 失败的返回错误信息
    - 前端展示"成功X件,失败Y件"
    
  4. 性能优化

    批量删除50个商品:
    ❌ for循环50次DELETE
    ✅ 一次DELETE WHERE sku_id IN (...)
    
    批量更新库存:
    ❌ 50次UPDATE
    ✅ 批量UPDATE CASE WHEN
    

延伸思考

  1. 批量操作如何支持撤销(Undo)?
  2. 批量操作的进度如何展示?

🔧 题目13:购物车的收藏夹联动

问题描述: 购物车和收藏夹如何联动?商品从购物车移入收藏,或从收藏加入购物车。

答案

推荐方案

  1. 数据模型

    favorite
    ├── favorite_id
    ├── user_id
    ├── sku_id
    ├── source(CART/BROWSE)
    ├── added_at
    └── ...
    
  2. 互相转换

    购物车 → 收藏夹:
    1. 用户点击"移入收藏"
    2. 加入收藏夹
    3. 从购物车删除
    4. 提示"已移入收藏夹"
    
    收藏夹 → 购物车:
    1. 用户点击"加入购物车"
    2. 加入购物车
    3. 保留在收藏夹(不删除)
    
  3. 降价提醒

    收藏商品降价:
    - 监控收藏商品价格
    - 降价时推送通知
    - 引导用户加购
    

延伸思考

  1. 收藏夹和购物车的区别是什么?
  2. 如何设计收藏夹的分组功能?

💡 题目14:购物车的历史记录

问题描述: 用户删除了购物车商品,想恢复。如何设计购物车的历史记录功能?

答案

推荐方案

  1. 软删除

    shopping_cart
    ├── ...
    ├── deleted_at(软删除标记)
    └── deleted(是否删除)
    
    查询购物车:
    SELECT * FROM shopping_cart 
    WHERE user_id=? AND deleted=0
    
    查询历史:
    SELECT * FROM shopping_cart 
    WHERE user_id=? AND deleted=1
    ORDER BY deleted_at DESC
    
  2. 恢复功能

    历史记录页面:
    最近删除:
    - 商品A(3天前删除)[恢复]
    - 商品B(7天前删除)[恢复]
    
    恢复操作:
    UPDATE shopping_cart 
    SET deleted=0, deleted_at=NULL
    WHERE cart_id=?
    
  3. 自动清理

    定时任务:
    - 删除30天后的历史记录
    - 减少存储成本
    

延伸思考

  1. 购物车历史记录是否需要版本控制(记录每次修改)?
  2. 如何设计购物车的快照功能(保存多个购物清单)?

📊 题目15:购物车的AB测试设计

问题描述: 想测试新的购物车布局对转化率的影响。如何设计购物车的AB测试?

答案

推荐方案

  1. 分流策略

    public String getCartVersion(Long userId) {
      // 基于用户ID哈希分流
      int hash = userId.hashCode();
      if (hash % 2 == 0) {
        return "A"; // 对照组
      } else {
        return "B"; // 实验组
      }
    }
    
  2. 实验设计

    对照组A(50%用户):
    - 旧购物车布局
    
    实验组B(50%用户):
    - 新购物车布局(优化后)
    
    评估指标:
    - 加购率
    - 结算率
    - 转化率
    - 客单价
    
  3. 数据埋点

    // 购物车页面浏览
    track('cart_view', {
      version: 'A', // 或 'B'
      cartItemCount: 5
    });
    
    // 点击结算
    track('cart_checkout_click', {
      version: 'A',
      cartTotal: 1000
    });
    
    // 完成下单
    track('order_created', {
      version: 'A',
      orderAmount: 1000
    });
    
  4. 结果分析

    结果对比:
    | 指标 | A组 | B组 | 提升 |
    |------|-----|-----|------|
    | 结算率 | 60% | 65% | +8.3% |
    | 转化率 | 40% | 45% | +12.5% |
    | 客单价 | ¥800 | ¥850 | +6.25% |
    
    结论:B组效果更好,全量发布
    

延伸思考

  1. AB测试如何保证结果的统计显著性?
  2. 多个AB测试同时进行时如何隔离影响?
  3. 如何设计购物车的渐进式发布(灰度发布)?

3.3 订单系统(15题)

📊 题目1:订单状态机的设计

问题描述: 订单从创建到完成,经历多个状态(待支付、待发货、待收货、已完成)。如何设计订单状态机,保证状态流转的正确性?

答案

问题分析: 订单状态流转的核心要素:

  1. 状态定义清晰
  2. 流转规则明确
  3. 防止非法跳转
  4. 支持异常流程(取消、退款)

状态定义

正向流程:
PENDING_PAYMENT(待支付)
→ PAID(已支付/待发货)
→ SHIPPED(已发货/待收货)
→ RECEIVED(已收货/待评价)
→ COMPLETED(已完成)

逆向流程:
CANCELLED(已取消)
REFUNDING(退款中)
REFUNDED(已退款)

特殊状态:
TIMEOUT(超时关闭)

状态机实现

方案一:If-Else判断

public void updateOrderStatus(Order order, OrderStatus newStatus) {
  OrderStatus currentStatus = order.getStatus();
  
  if (currentStatus == PENDING_PAYMENT) {
    if (newStatus == PAID || newStatus == CANCELLED || newStatus == TIMEOUT) {
      order.setStatus(newStatus);
    } else {
      throw new IllegalStateException("非法状态转换");
    }
  } else if (currentStatus == PAID) {
    if (newStatus == SHIPPED || newStatus == REFUNDING) {
      order.setStatus(newStatus);
    } else {
      throw new IllegalStateException("非法状态转换");
    }
  }
  // ... 更多判断
}

缺点:

  • 代码冗长
  • 难以维护
  • 状态多时复杂度爆炸

方案二:状态转换表(推荐)

// 定义状态转换规则
private static final Map<OrderStatus, Set<OrderStatus>> TRANSITIONS = Map.of(
  PENDING_PAYMENT, Set.of(PAID, CANCELLED, TIMEOUT),
  PAID, Set.of(SHIPPED, REFUNDING),
  SHIPPED, Set.of(RECEIVED, REFUNDING),
  RECEIVED, Set.of(COMPLETED, REFUNDING),
  REFUNDING, Set.of(REFUNDED)
);

public void updateOrderStatus(Order order, OrderStatus newStatus) {
  OrderStatus currentStatus = order.getStatus();
  
  Set<OrderStatus> allowedTransitions = TRANSITIONS.get(currentStatus);
  if (allowedTransitions == null || !allowedTransitions.contains(newStatus)) {
    throw new IllegalStateException(
      String.format("不允许从%s转换到%s", currentStatus, newStatus)
    );
  }
  
  // 记录状态变更历史
  OrderStatusHistory history = new OrderStatusHistory();
  history.setOrderId(order.getId());
  history.setFromStatus(currentStatus);
  history.setToStatus(newStatus);
  history.setOperator(getCurrentUser());
  history.setReason(reason);
  historyRepository.save(history);
  
  // 更新订单状态
  order.setStatus(newStatus);
  orderRepository.save(order);
  
  // 发布状态变更事件
  eventPublisher.publish(new OrderStatusChangedEvent(order, currentStatus, newStatus));
}

优点:

  • 规则清晰
  • 易于维护
  • 可扩展

状态流转图

                    ┌─> CANCELLED
                    │
PENDING_PAYMENT ──┬─┴─> PAID ───> SHIPPED ───> RECEIVED ───> COMPLETED
                  │                  │            │
                  └─> TIMEOUT        │            │
                                     │            │
                                     └─> REFUNDING <─┘
                                            │
                                            └─> REFUNDED

延伸思考

  1. 如何设计订单的子状态(如待发货细分为待拣货、待打包、待出库)?
  2. 订单状态变更如何触发后续操作(如发货后通知物流)?
  3. 如何处理状态流转的并发冲突?

🔧 题目2:订单号生成规则

问题描述: 订单号需要唯一、有序、不易被猜测。如何设计订单号生成规则?

答案

订单号设计要求

  1. 全局唯一
  2. 趋势递增(便于分库分表)
  3. 信息可读(包含时间、业务类型)
  4. 安全性(不易被遍历)
  5. 长度适中(15-20位)

方案一:数据库自增ID

优点:

  • 简单
  • 唯一

缺点:

  • 连续,易被猜测
  • 分布式环境难实现
  • 信息量少

方案二:UUID

优点:

  • 全局唯一
  • 无需中心化

缺点:

  • 无序(影响索引性能)
  • 长度太长(36位)
  • 无业务含义

方案三:Snowflake算法(推荐)

结构:

64位Long型:
1位符号位 + 41位时间戳 + 10位机器ID + 12位序列号

示例:
0 - 00000000000000000000000000000000000000000 - 0000000000 - 000000000000
│   └─────────────41位时间戳─────────────────┘   └10位机器┘   └12位序列┘
符号位

生成的订单号:1234567890123456789(19位)

实现:

public class SnowflakeIdGenerator {
  // 起始时间戳(2020-01-01)
  private final long epoch = 1577836800000L;
  
  // 机器ID(数据中心ID + 机器ID)
  private final long workerId;
  
  // 序列号
  private long sequence = 0L;
  
  // 上次生成ID的时间戳
  private long lastTimestamp = -1L;
  
  public synchronized long nextId() {
    long timestamp = System.currentTimeMillis();
    
    // 时钟回拨检测
    if (timestamp < lastTimestamp) {
      throw new RuntimeException("时钟回拨");
    }
    
    // 同一毫秒内
    if (timestamp == lastTimestamp) {
      sequence = (sequence + 1) & 4095; // 4095=2^12-1
      if (sequence == 0) {
        // 序列号用完,等待下一毫秒
        timestamp = waitNextMillis(lastTimestamp);
      }
    } else {
      sequence = 0;
    }
    
    lastTimestamp = timestamp;
    
    // 组装ID
    return ((timestamp - epoch) << 22) 
         | (workerId << 12) 
         | sequence;
  }
}

优点:

  • 趋势递增
  • 高性能
  • 分布式友好

缺点:

  • 依赖机器时钟
  • 机器ID需要管理

方案四:业务规则拼接

结构:

订单号格式:业务前缀 + 日期 + 随机数

示例:
OR20260418123456789
│  └────┘└───────┘
│   日期   随机数
业务前缀(OR=Order)

生成:
String orderId = "OR" 
               + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE)
               + RandomStringUtils.randomNumeric(9);

优点:

  • 可读性强
  • 包含业务信息
  • 可自定义

缺点:

  • 需要保证随机数不重复
  • 长度较长

推荐方案: 使用Snowflake算法生成基础ID,再转为业务订单号。

实现:

public String generateOrderNo() {
  long snowflakeId = idGenerator.nextId();
  
  // 转为订单号(添加业务前缀)
  return "OR" + snowflakeId;
}

延伸思考

  1. 如何设计订单号的校验规则(防止伪造)?
  2. 订单号如何支持多业务类型(普通订单、预售订单、拼团订单)?
  3. 分库分表场景下订单号如何设计路由键?

💡 题目3:订单超时自动取消

问题描述: 用户下单30分钟未支付,订单自动关闭并释放库存。如何实现订单超时自动取消?

答案

方案一:定时任务扫描

核心思想: 定时任务定期扫描超时订单。

实现:

@Scheduled(fixedDelay = 60000) // 每分钟执行
public void cancelTimeoutOrders() {
  // 查询超时未支付订单
  List<Order> timeoutOrders = orderRepository.findByStatusAndCreateTimeBefore(
    OrderStatus.PENDING_PAYMENT,
    LocalDateTime.now().minus(30, ChronoUnit.MINUTES)
  );
  
  for (Order order : timeoutOrders) {
    try {
      // 取消订单
      orderService.cancel(order.getId(), "超时未支付自动取消");
      
      // 释放库存
      inventoryService.release(order.getItems());
      
      // 通知用户
      notificationService.send(order.getUserId(), "订单已超时关闭");
    } catch (Exception e) {
      log.error("取消订单失败", e);
    }
  }
}

优点:

  • 实现简单
  • 可靠性高

缺点:

  • 实时性差(最长延迟1分钟)
  • 数据库扫描压力大
  • 定时任务单点故障

方案二:延迟队列(推荐)

核心思想: 订单创建时发送延迟消息,30分钟后消费取消订单。

使用RabbitMQ延迟队列:

// 创建订单时
public void createOrder(Order order) {
  // 1. 保存订单
  orderRepository.save(order);
  
  // 2. 发送延迟消息(30分钟后)
  rabbitTemplate.convertAndSend(
    "order.cancel.exchange",
    "order.cancel.routing.key",
    order.getId(),
    message -> {
      message.getMessageProperties().setDelay(30 * 60 * 1000); // 30分钟
      return message;
    }
  );
}

// 消费延迟消息
@RabbitListener(queues = "order.cancel.queue")
public void handleOrderCancel(Long orderId) {
  Order order = orderRepository.findById(orderId);
  
  // 检查订单状态
  if (order.getStatus() == OrderStatus.PENDING_PAYMENT) {
    // 仍未支付,取消订单
    orderService.cancel(orderId, "超时未支付自动取消");
    inventoryService.release(order.getItems());
  }
  // 如果已支付,忽略
}

使用Redis实现延迟队列:

// 创建订单时
public void createOrder(Order order) {
  orderRepository.save(order);
  
  // 添加到Redis有序集合(Sorted Set)
  long expireTime = System.currentTimeMillis() + 30 * 60 * 1000;
  redis.zadd("order:timeout", expireTime, order.getId());
}

// 定时消费
@Scheduled(fixedDelay = 1000) // 每秒执行
public void processTimeoutOrders() {
  long now = System.currentTimeMillis();
  
  // 获取已到期的订单ID
  Set<String> orderIds = redis.zrangeByScore("order:timeout", 0, now);
  
  for (String orderId : orderIds) {
    try {
      // 处理超时订单
      processTimeoutOrder(Long.parseLong(orderId));
      
      // 从集合中移除
      redis.zrem("order:timeout", orderId);
    } catch (Exception e) {
      log.error("处理超时订单失败", e);
    }
  }
}

优点:

  • 准确到秒
  • 分布式友好
  • 性能好

缺点:

  • 依赖消息队列
  • 需要处理消息丢失

方案三:时间轮算法

核心思想: 使用时间轮数据结构管理超时任务。

实现(Netty HashedWheelTimer):

private final HashedWheelTimer timer = new HashedWheelTimer(
  1, TimeUnit.SECONDS,  // 每秒tick一次
  60                     // 60个槽位
);

public void createOrder(Order order) {
  orderRepository.save(order);
  
  // 添加超时任务
  timer.newTimeout(timeout -> {
    Order latestOrder = orderRepository.findById(order.getId());
    if (latestOrder.getStatus() == OrderStatus.PENDING_PAYMENT) {
      orderService.cancel(order.getId(), "超时未支付自动取消");
    }
  }, 30, TimeUnit.MINUTES);
}

优点:

  • 高性能
  • 精确度高

缺点:

  • 内存占用(任务在内存)
  • 单机方案(不支持分布式)
  • 服务重启任务丢失

方案对比

方案实时性可靠性分布式实施难度
定时扫描★★☆☆☆★★★★★★★★★☆★★★★★
延迟队列★★★★★★★★★☆★★★★★★★★☆☆
时间轮★★★★★★★★☆☆★★☆☆☆★★★☆☆

推荐方案: 采用延迟队列(RabbitMQ或Redis)

实施要点:

  1. 幂等性保证

    @Transactional
    public void cancel(Long orderId, String reason) {
      Order order = orderRepository.findById(orderId);
      
      // 检查当前状态
      if (order.getStatus() != OrderStatus.PENDING_PAYMENT) {
        log.warn("订单{}状态不是待支付,跳过取消", orderId);
        return; // 已被其他线程处理
      }
      
      // CAS更新状态
      int updated = orderRepository.updateStatus(
        orderId, 
        OrderStatus.CANCELLED,
        OrderStatus.PENDING_PAYMENT // 期望的旧状态
      );
      
      if (updated == 0) {
        log.warn("订单{}取消失败,可能已被处理", orderId);
        return;
      }
      
      // 释放库存
      inventoryService.release(order.getItems());
    }
    
  2. 异常重试

    取消失败的处理:
    - 消息重新入队,稍后重试
    - 最多重试3次
    - 仍失败则记录告警,人工处理
    
  3. 监控告警

    监控指标:
    - 超时订单数量
    - 取消成功率
    - 延迟队列堆积量
    
    告警:
    - 取消失败率 > 1%
    - 延迟队列堆积 > 10000
    

延伸思考

  1. 如何设计不同订单类型的不同超时时间(普通30分钟,秒杀10分钟)?
  2. 订单超时取消如何通知用户?
  3. 大促期间超时订单激增如何处理?

📊 题目4:订单拆单与合单策略

问题描述: 用户购买多个商品,可能来自不同仓库或不同商家。如何设计订单拆单与合单策略?

答案

问题分析: 拆单场景:

  1. 多仓库发货(就近发货)
  2. 多商家发货(平台+第三方卖家)
  3. 预售+现货(发货时间不同)
  4. 自营+跨境(清关时间不同)

合单场景:

  1. 同一地址多笔订单(节省运费)
  2. 同一商家商品(方便发货)

方案一:用户下单时拆单

核心思想: 用户提交订单时,系统自动拆分为多个子订单。

流程:

用户购物车:
- 商品A(北京仓)
- 商品B(上海仓)
- 商品C(北京仓)

拆单规则:
按仓库拆分:
→ 子订单1:商品A + C(北京仓)
→ 子订单2:商品B(上海仓)

数据结构:
parent_order(父订单)
├── parent_order_id
├── user_id
├── total_amount
└── status

sub_order(子订单)
├── sub_order_id
├── parent_order_id
├── warehouse_id
├── items
└── status

用户支付:

用户支付父订单 → 分配金额到各子订单
子订单独立发货、收货

优点:

  • 逻辑清晰
  • 用户感知明确

缺点:

  • 用户体验复杂(多个运单号)
  • 退款复杂(部分退款)

方案二:后台自动拆单(推荐)

核心思想: 用户下单时是一个订单,后台根据规则自动拆分为多个发货单。

流程:

用户下单:创建订单(单个)
↓
订单支付成功
↓
订单中心分析:需要拆单
↓
创建多个发货单(shipment)
- 发货单1:商品A+C → 北京仓
- 发货单2:商品B → 上海仓
↓
各仓库独立发货

数据结构:

order(订单)
├── order_id
├── user_id
├── total_amount
└── status

shipment(发货单)
├── shipment_id
├── order_id
├── warehouse_id
├── items(发货商品)
├── tracking_number(运单号)
└── status

优点:

  • 用户无感知(看到的是一个订单)
  • 退款简单(按订单退)
  • 灵活(可随时调整拆单规则)

缺点:

  • 实现复杂
  • 需要维护订单和发货单的关系

拆单规则

  1. 按仓库拆分

    public List<Shipment> splitByWarehouse(Order order) {
      // 1. 为每个商品选择最优仓库
      Map<String, Warehouse> itemWarehouse = new HashMap<>();
      for (OrderItem item : order.getItems()) {
        Warehouse warehouse = selectWarehouse(item.getSkuId(), order.getAddress());
        itemWarehouse.put(item.getSkuId(), warehouse);
      }
      
      // 2. 按仓库分组
      Map<Warehouse, List<OrderItem>> grouped = order.getItems().stream()
        .collect(Collectors.groupBy(item -> itemWarehouse.get(item.getSkuId())));
      
      // 3. 生成发货单
      List<Shipment> shipments = new ArrayList<>();
      for (Map.Entry<Warehouse, List<OrderItem>> entry : grouped.entrySet()) {
        Shipment shipment = new Shipment();
        shipment.setOrderId(order.getId());
        shipment.setWarehouseId(entry.getKey().getId());
        shipment.setItems(entry.getValue());
        shipments.add(shipment);
      }
      
      return shipments;
    }
    
  2. 按商家拆分

    平台订单包含:
    - 自营商品(平台发货)
    - 第三方商品(商家发货)
    
    拆分:
    - 子订单1:自营商品
    - 子订单2:商家A的商品
    - 子订单3:商家B的商品
    
  3. 按发货时间拆分

    订单包含:
    - 现货商品(立即发货)
    - 预售商品(15天后发货)
    
    拆分:
    - 发货单1:现货(立即发)
    - 发货单2:预售(延迟发)
    

合单策略

  1. 同地址合并

    用户A在1小时内下了3笔订单:
    - 订单1:商品A(北京仓)
    - 订单2:商品B(北京仓)
    - 订单3:商品C(上海仓)
    
    合单:
    - 发货单1:订单1+订单2的商品(北京仓合并发货)
    - 发货单2:订单3的商品(上海仓单独发货)
    
    好处:
    - 节省运费
    - 减少包裹数量
    
  2. 运费优化

    规则:
    - 同一仓库、同一地址、24小时内的订单
    - 自动合并发货
    - 运费退还到用户余额
    

推荐方案: 采用后台自动拆单

实施要点:

  1. 拆单时机

    时机选择:
    - 订单支付后立即拆单(推荐)
    - 发货前拆单(更灵活)
    
  2. 用户展示

    订单详情页:
    订单号:OR123456
    总金额:¥1000
    
    发货信息:
    - 包裹1:商品A+B(运单号:SF123)
      状态:已发货
    - 包裹2:商品C(运单号:SF456)
      状态:待发货
    
  3. 退款处理

    部分商品退款:
    - 用户申请退商品A
    - 计算退款金额(商品价 + 分摊运费)
    - 只退部分金额
    - 其他商品正常履约
    

延伸思考

  1. 如何设计拆单的运费分摊规则?
  2. 拆单后如何保证库存一致性?
  3. 跨境订单的拆单有何特殊性?

🔧 题目5:订单的并发创建与幂等性

问题描述: 用户可能重复点击“提交订单“按钮,导致创建多个订单。如何保证订单创建的幂等性?

答案

问题分析: 重复下单的原因:

  1. 用户重复点击
  2. 网络超时重试
  3. 前端未防抖
  4. 恶意刷单

方案一:前端防抖

核心思想: 前端限制用户短时间内多次点击。

实现:

let submitting = false;

function submitOrder() {
  if (submitting) {
    return; // 正在提交中,忽略
  }
  
  submitting = true;
  
  fetch('/api/order/create', {
    method: 'POST',
    body: JSON.stringify(orderData)
  })
  .then(res => {
    // 处理结果
  })
  .finally(() => {
    submitting = false; // 完成后恢复
  });
}

优点:

  • 简单有效

缺点:

  • 仅防止前端重复
  • 无法防止恶意绕过前端

方案二:唯一索引(推荐)

核心思想: 数据库层面保证唯一性。

实现:

CREATE TABLE orders (
  order_id BIGINT PRIMARY KEY,
  user_id BIGINT NOT NULL,
  idempotent_key VARCHAR(64) UNIQUE, -- 幂等键
  ...
);

CREATE UNIQUE INDEX uk_user_idempotent ON orders(user_id, idempotent_key);

创建订单:

@Transactional
public Order createOrder(OrderRequest request, String idempotentKey) {
  try {
    // 1. 构建订单
    Order order = new Order();
    order.setUserId(request.getUserId());
    order.setIdempotentKey(idempotentKey);
    order.setItems(request.getItems());
    // ...
    
    // 2. 保存订单(唯一索引保证幂等)
    orderRepository.save(order);
    
    // 3. 扣减库存
    inventoryService.deduct(order.getItems());
    
    return order;
  } catch (DuplicateKeyException e) {
    // 幂等键重复,说明订单已创建
    return orderRepository.findByIdempotentKey(idempotentKey);
  }
}

幂等键生成:

// 方案1:前端生成UUID
String idempotentKey = UUID.randomUUID().toString();

// 方案2:后端生成(基于购物车内容)
String idempotentKey = DigestUtils.md5Hex(
  userId + ":" + cartItems.toString() + ":" + timestamp
);

优点:

  • 数据库层面保证
  • 可靠性高

缺点:

  • 依赖唯一索引
  • 需要生成幂等键

方案三:分布式锁

核心思想: 使用Redis分布式锁,同一用户同时只能创建一个订单。

实现:

public Order createOrder(OrderRequest request) {
  String lockKey = "order:create:" + request.getUserId();
  
  // 尝试获取锁
  boolean locked = redisLock.tryLock(lockKey, 10, TimeUnit.SECONDS);
  if (!locked) {
    throw new BizException("正在创建订单,请勿重复提交");
  }
  
  try {
    // 创建订单
    Order order = doCreateOrder(request);
    return order;
  } finally {
    // 释放锁
    redisLock.unlock(lockKey);
  }
}

优点:

  • 防止并发创建
  • 灵活控制

缺点:

  • 依赖Redis
  • 锁超时需要处理

方案四:Token机制

核心思想: 用户进入结算页时,服务端生成唯一Token,提交订单时校验Token。

流程:

1. 用户进入结算页
   → 请求服务端生成Token
   → 服务端生成Token并存Redis
   → 返回Token给前端

2. 用户提交订单
   → 携带Token
   → 服务端校验Token是否存在
   → 存在则删除Token,创建订单
   → 不存在则拒绝(重复提交)

实现:

// 生成Token
public String generateOrderToken(Long userId) {
  String token = UUID.randomUUID().toString();
  String key = "order:token:" + token;
  redis.setex(key, 300, userId.toString()); // 5分钟有效
  return token;
}

// 创建订单(校验Token)
@Transactional
public Order createOrder(OrderRequest request, String token) {
  String key = "order:token:" + token;
  
  // 检查Token是否存在
  String userId = redis.get(key);
  if (userId == null) {
    throw new BizException("订单Token无效或已使用");
  }
  
  // 验证Token归属
  if (!userId.equals(request.getUserId().toString())) {
    throw new BizException("订单Token不匹配");
  }
  
  // 删除Token(保证一次性)
  redis.del(key);
  
  // 创建订单
  return doCreateOrder(request);
}

优点:

  • 防止重复提交
  • 安全性高(Token一次性)

缺点:

  • 需要多次交互
  • Token过期需要重新获取

方案对比

方案可靠性易用性性能适用场景
前端防抖★★☆☆☆★★★★★★★★★★辅助手段
唯一索引★★★★★★★★★☆★★★★☆通用
分布式锁★★★★☆★★★☆☆★★★☆☆高并发
Token机制★★★★★★★★☆☆★★★★☆安全性要求高

推荐方案: 采用唯一索引+Token机制的组合。

实施要点:

  1. 多层防护

    L1:前端防抖(用户体验)
    L2:Token机制(防恶意)
    L3:唯一索引(最后防线)
    
  2. 幂等键设计

    幂等键组成:
    userId + cartVersion + timestamp
    
    例如:
    123_v10_1679800000
    
    说明:
    - userId:用户ID
    - cartVersion:购物车版本(购物车内容变化版本号+1)
    - timestamp:提交时间戳(精确到秒)
    
  3. 异常处理

    try {
      return createOrder(request, token);
    } catch (DuplicateKeyException e) {
      // 唯一索引冲突,查询已存在的订单
      Order existingOrder = findByIdempotentKey(idempotentKey);
      return existingOrder;
    } catch (BizException e) {
      // Token无效等业务异常
      throw e;
    }
    

延伸思考

  1. 如何设计订单创建的限流(防止刷单)?
  2. 订单创建失败如何回滚库存?
  3. 分布式事务下如何保证订单创建的一致性?

📊 题目6:订单的分布式事务设计(Saga模式)

问题描述: 订单创建涉及多个服务(订单服务、库存服务、优惠券服务、积分服务)。如何使用Saga模式保证分布式事务一致性?

答案

问题分析: 订单创建的分布式事务流程:

  1. 扣减库存(库存服务)
  2. 核销优惠券(营销服务)
  3. 扣减积分(会员服务)
  4. 创建订单(订单服务)

任一环节失败,已执行的操作需要回滚。

Saga模式实现(使用Go):

package saga

import (
	"context"
	"fmt"
)

// SagaStep 定义Saga步骤
type SagaStep struct {
	Name         string
	Execute      func(ctx context.Context, data interface{}) error
	Compensate   func(ctx context.Context, data interface{}) error
}

// SagaOrchestrator Saga编排器
type SagaOrchestrator struct {
	steps []SagaStep
}

// Execute 执行Saga
func (s *SagaOrchestrator) Execute(ctx context.Context, data interface{}) error {
	executedSteps := make([]int, 0)
	
	// 正向执行
	for i, step := range s.steps {
		if err := step.Execute(ctx, data); err != nil {
			// 执行失败,触发补偿
			s.compensate(ctx, data, executedSteps)
			return fmt.Errorf("步骤 %s 执行失败: %w", step.Name, err)
		}
		executedSteps = append(executedSteps, i)
	}
	
	return nil
}

// compensate 执行补偿
func (s *SagaOrchestrator) compensate(ctx context.Context, data interface{}, executedSteps []int) {
	// 反向补偿
	for i := len(executedSteps) - 1; i >= 0; i-- {
		stepIndex := executedSteps[i]
		step := s.steps[stepIndex]
		
		if err := step.Compensate(ctx, data); err != nil {
			// 补偿失败,记录日志,转人工处理
			log.Errorf("步骤 %s 补偿失败: %v", step.Name, err)
		}
	}
}

// 订单创建Saga示例
func CreateOrderSaga(orderReq *CreateOrderRequest) error {
	saga := &SagaOrchestrator{
		steps: []SagaStep{
			// 步骤1:扣减库存
			{
				Name: "DeductInventory",
				Execute: func(ctx context.Context, data interface{}) error {
					req := data.(*CreateOrderRequest)
					return inventoryService.Deduct(ctx, req.Items)
				},
				Compensate: func(ctx context.Context, data interface{}) error {
					req := data.(*CreateOrderRequest)
					return inventoryService.Release(ctx, req.Items)
				},
			},
			// 步骤2:核销优惠券
			{
				Name: "UseCoupon",
				Execute: func(ctx context.Context, data interface{}) error {
					req := data.(*CreateOrderRequest)
					if req.CouponID == "" {
						return nil // 无优惠券,跳过
					}
					return couponService.Use(ctx, req.UserID, req.CouponID)
				},
				Compensate: func(ctx context.Context, data interface{}) error {
					req := data.(*CreateOrderRequest)
					if req.CouponID == "" {
						return nil
					}
					return couponService.Release(ctx, req.UserID, req.CouponID)
				},
			},
			// 步骤3:扣减积分
			{
				Name: "DeductPoints",
				Execute: func(ctx context.Context, data interface{}) error {
					req := data.(*CreateOrderRequest)
					if req.PointsToUse == 0 {
						return nil
					}
					return pointsService.Deduct(ctx, req.UserID, req.PointsToUse)
				},
				Compensate: func(ctx context.Context, data interface{}) error {
					req := data.(*CreateOrderRequest)
					if req.PointsToUse == 0 {
						return nil
					}
					return pointsService.Refund(ctx, req.UserID, req.PointsToUse)
				},
			},
			// 步骤4:创建订单
			{
				Name: "CreateOrder",
				Execute: func(ctx context.Context, data interface{}) error {
					req := data.(*CreateOrderRequest)
					order := &Order{
						OrderID:   generateOrderID(),
						UserID:    req.UserID,
						Items:     req.Items,
						Status:    OrderStatusPending,
					}
					return orderRepo.Create(ctx, order)
				},
				Compensate: func(ctx context.Context, data interface{}) error {
					req := data.(*CreateOrderRequest)
					// 订单创建失败不需要补偿(未持久化)
					return nil
				},
			},
		},
	}
	
	return saga.Execute(context.Background(), orderReq)
}

优点

  • 逻辑清晰(正向+补偿)
  • 解耦各服务
  • 支持长事务

缺点

  • 实现复杂
  • 补偿可能失败(需要人工介入)
  • 中间状态可见(不是强一致性)

延伸思考

  1. Saga补偿失败如何处理?
  2. 如何设计Saga的可视化监控?
  3. Saga vs 2PC(两阶段提交)如何选择?

🔧 题目7:订单数据的分库分表设计

问题描述: 订单表数据量达到亿级,单表查询性能下降。如何设计订单的分库分表方案?

答案

问题分析: 订单分库分表的核心要素:

  1. 分片键选择(user_id还是order_id)
  2. 分片数量(16、32、64、128)
  3. 跨片查询(如运营查询某时间段订单)
  4. 数据扩容

方案一:按user_id分片(推荐)

核心思想: 同一用户的订单存储在同一分片。

分片规则:

// 分片数量
const ShardCount = 64

// 计算分片
func GetShardIndex(userID int64) int {
	return int(userID % ShardCount)
}

// 路由到数据源
func GetDataSource(userID int64) *sql.DB {
	shardIndex := GetShardIndex(userID)
	return dataSources[shardIndex]
}

表结构:

-- 64个库,每个库有orders表
database_00.orders
database_01.orders
...
database_63.orders

订单ID生成:
order_id = snowflake_id
不包含分片信息(通过user_id路由)

优点:

  • 用户维度查询高效(“我的订单”)
  • 单用户订单聚合容易
  • 避免跨库JOIN

缺点:

  • 按订单ID查询需要广播(查所有分片)
  • 数据可能不均匀(大客户订单多)

方案二:按order_id分片

核心思想: 按订单ID散列分片。

分片规则:

func GetShardIndex(orderID int64) int {
	return int(orderID % ShardCount)
}

订单ID生成(包含分片信息):

// 订单ID结构:分片位 + Snowflake ID
// 前6位:分片号(0-63)
// 后13位:Snowflake ID

func GenerateOrderID(userID int64) int64 {
	shardIndex := GetShardIndex(userID)
	snowflakeID := snowflake.Generate()
	
	// 组装:分片号(6位) + snowflake(13位)
	return int64(shardIndex)*1e13 + snowflakeID
}

// 解析分片
func ParseShard(orderID int64) int {
	return int(orderID / 1e13)
}

优点:

  • 按订单ID查询高效(直接定位分片)
  • 数据均匀

缺点:

  • 用户维度查询需要广播
  • “我的订单“查询慢

方案三:复合分片

核心思想: 主表按user_id分片,建立order_id到分片的映射表。

设计:

主表(按user_id分片):
shard_00.orders
shard_01.orders

映射表(不分片,单独集群):
order_routing
├── order_id(主键)
├── shard_index(分片号)
└── user_id

查询流程:
1. 按订单ID查询:
   - 查询order_routing获取分片号
   - 路由到对应分片查询

2. 按用户ID查询:
   - 直接路由到用户分片

优点:

  • 支持多种查询方式
  • 灵活

缺点:

  • 映射表是单点
  • 实现复杂

方案对比

方案用户查询订单查询数据均匀度实施难度
按user_id★★★★★★★☆☆☆★★★☆☆★★★★☆
按order_id★★☆☆☆★★★★★★★★★★★★★★☆
复合分片★★★★★★★★★★★★★★★★★☆☆☆

推荐方案: 采用按user_id分片

实施要点(Go实现):

  1. 分片路由中间件

    package sharding
    
    import (
    	"context"
    	"database/sql"
    )
    
    // ShardingManager 分片管理器
    type ShardingManager struct {
    	dataSources []*sql.DB
    	shardCount  int
    }
    
    // NewShardingManager 创建分片管理器
    func NewShardingManager(dsns []string) (*ShardingManager, error) {
    	dbs := make([]*sql.DB, len(dsns))
    	for i, dsn := range dsns {
    		db, err := sql.Open("mysql", dsn)
    		if err != nil {
    			return nil, err
    		}
    		dbs[i] = db
    	}
    	
    	return &ShardingManager{
    		dataSources: dbs,
    		shardCount:  len(dsns),
    	}, nil
    }
    
    // GetDB 根据用户ID获取数据库连接
    func (sm *ShardingManager) GetDB(userID int64) *sql.DB {
    	shardIndex := userID % int64(sm.shardCount)
    	return sm.dataSources[shardIndex]
    }
    
    // ExecuteOnShard 在指定分片执行查询
    func (sm *ShardingManager) ExecuteOnShard(ctx context.Context, userID int64, 
    	fn func(*sql.DB) error) error {
    	db := sm.GetDB(userID)
    	return fn(db)
    }
    
    // Broadcast 广播到所有分片执行
    func (sm *ShardingManager) Broadcast(ctx context.Context, 
    	fn func(*sql.DB) error) []error {
    	errors := make([]error, 0)
    	for _, db := range sm.dataSources {
    		if err := fn(db); err != nil {
    			errors = append(errors, err)
    		}
    	}
    	return errors
    }
    
  2. 订单Repository实现

    type OrderRepository struct {
    	shardingMgr *ShardingManager
    }
    
    // Create 创建订单
    func (r *OrderRepository) Create(ctx context.Context, order *Order) error {
    	return r.shardingMgr.ExecuteOnShard(ctx, order.UserID, func(db *sql.DB) error {
    		query := `INSERT INTO orders (order_id, user_id, total_amount, status, created_at)
    		          VALUES (?, ?, ?, ?, ?)`
    		_, err := db.ExecContext(ctx, query, 
    			order.OrderID, order.UserID, order.TotalAmount, 
    			order.Status, time.Now())
    		return err
    	})
    }
    
    // FindByUserID 查询用户订单(单分片)
    func (r *OrderRepository) FindByUserID(ctx context.Context, userID int64, 
    	page, size int) ([]*Order, error) {
    	var orders []*Order
    	
    	err := r.shardingMgr.ExecuteOnShard(ctx, userID, func(db *sql.DB) error {
    		query := `SELECT * FROM orders 
    		          WHERE user_id=? 
    		          ORDER BY created_at DESC 
    		          LIMIT ? OFFSET ?`
    		rows, err := db.QueryContext(ctx, query, userID, size, (page-1)*size)
    		if err != nil {
    			return err
    		}
    		defer rows.Close()
    		
    		for rows.Next() {
    			order := &Order{}
    			// 扫描数据...
    			orders = append(orders, order)
    		}
    		return nil
    	})
    	
    	return orders, err
    }
    
    // FindByOrderID 按订单ID查询(需要广播)
    func (r *OrderRepository) FindByOrderID(ctx context.Context, orderID int64) (*Order, error) {
    	// 方案1:广播到所有分片查询(慢)
    	for _, db := range r.shardingMgr.dataSources {
    		order, err := queryFromDB(db, orderID)
    		if err == nil && order != nil {
    			return order, nil
    		}
    	}
    	return nil, ErrOrderNotFound
    	
    	// 方案2:维护order_id -> user_id映射(推荐)
    	// userID := r.getOrderUserMapping(orderID)
    	// return r.FindByUserAndOrderID(ctx, userID, orderID)
    }
    
  3. 订单ID包含分片信息

    // 订单ID结构:6位分片号 + 13位Snowflake
    
    func GenerateOrderIDWithShard(userID int64) int64 {
    	shardIndex := userID % ShardCount
    	snowflakeID := snowflake.NextID()
    	
    	// 组装:前6位是分片号
    	return shardIndex*1e13 + snowflakeID
    }
    
    // 解析分片号
    func ParseShardFromOrderID(orderID int64) int {
    	return int(orderID / 1e13)
    }
    
    // 直接定位查询
    func (r *OrderRepository) FindByOrderIDFast(ctx context.Context, orderID int64) (*Order, error) {
    	shardIndex := ParseShardFromOrderID(orderID)
    	db := r.shardingMgr.dataSources[shardIndex]
    	
    	query := `SELECT * FROM orders WHERE order_id=?`
    	row := db.QueryRowContext(ctx, query, orderID)
    	
    	order := &Order{}
    	err := row.Scan(&order.OrderID, &order.UserID, ...) 
    	return order, err
    }
    
  4. 扩容方案

    扩容策略(64 → 128分片):
    
    方案A:双写期
    1. 新建64个分片(总共128个)
    2. 新订单写入新分片规则
    3. 老订单保留在老分片
    4. 查询时先查新分片,未命中再查老分片
    
    方案B:一致性哈希
    1. 使用一致性哈希算法
    2. 扩容时只需迁移部分数据
    3. 数据迁移期间双写
    

延伸思考

  1. 如何设计分库分表的全局查询(如运营后台)?
  2. 订单归档如何设计(冷热数据分离)?
  3. 分库分表如何支持跨库JOIN?

💡 题目8:订单履约流程的编排

问题描述: 订单支付成功后,需要依次执行:分配仓库、创建拣货单、打包、出库、创建运单、发货。如何设计订单履约流程的编排?

答案

推荐方案:事件驱动+状态机

架构(Go实现):

package fulfillment

import (
	"context"
)

// FulfillmentEvent 履约事件
type FulfillmentEvent struct {
	OrderID   int64
	EventType string
	Data      map[string]interface{}
}

// FulfillmentOrchestrator 履约编排器
type FulfillmentOrchestrator struct {
	eventBus EventBus
}

// OnOrderPaid 订单支付事件处理
func (o *FulfillmentOrchestrator) OnOrderPaid(ctx context.Context, orderID int64) error {
	// 1. 分配仓库
	warehouse, err := o.allocateWarehouse(ctx, orderID)
	if err != nil {
		return err
	}
	
	// 2. 创建拣货单
	pickingOrder, err := o.createPickingOrder(ctx, orderID, warehouse.ID)
	if err != nil {
		return err
	}
	
	// 3. 发布拣货事件
	o.eventBus.Publish(&FulfillmentEvent{
		OrderID:   orderID,
		EventType: "PickingOrderCreated",
		Data: map[string]interface{}{
			"pickingOrderID": pickingOrder.ID,
			"warehouseID":    warehouse.ID,
		},
	})
	
	return nil
}

// OnPickingCompleted 拣货完成事件处理
func (o *FulfillmentOrchestrator) OnPickingCompleted(ctx context.Context, event *FulfillmentEvent) error {
	orderID := event.OrderID
	
	// 1. 打包
	if err := o.pack(ctx, orderID); err != nil {
		return err
	}
	
	// 2. 出库
	if err := o.outbound(ctx, orderID); err != nil {
		return err
	}
	
	// 3. 创建物流运单
	trackingNumber, err := o.createShipment(ctx, orderID)
	if err != nil {
		return err
	}
	
	// 4. 发布发货事件
	o.eventBus.Publish(&FulfillmentEvent{
		OrderID:   orderID,
		EventType: "OrderShipped",
		Data: map[string]interface{}{
			"trackingNumber": trackingNumber,
		},
	})
	
	return nil
}

// 事件监听器
func (o *FulfillmentOrchestrator) Start() {
	o.eventBus.Subscribe("OrderPaid", o.OnOrderPaid)
	o.eventBus.Subscribe("PickingCompleted", o.OnPickingCompleted)
	o.eventBus.Subscribe("PackingCompleted", o.OnPackingCompleted)
	// ...
}

履约状态机

type FulfillmentStatus int

const (
	FulfillmentPending      FulfillmentStatus = 0  // 待履约
	FulfillmentWarehouseAllocated FulfillmentStatus = 1  // 已分配仓库
	FulfillmentPicking      FulfillmentStatus = 2  // 拣货中
	FulfillmentPacked       FulfillmentStatus = 3  // 已打包
	FulfillmentOutbound     FulfillmentStatus = 4  // 已出库
	FulfillmentShipped      FulfillmentStatus = 5  // 已发货
	FulfillmentReceived     FulfillmentStatus = 6  // 已签收
)

// 状态流转规则
var fulfillmentTransitions = map[FulfillmentStatus][]FulfillmentStatus{
	FulfillmentPending:            {FulfillmentWarehouseAllocated},
	FulfillmentWarehouseAllocated: {FulfillmentPicking},
	FulfillmentPicking:            {FulfillmentPacked},
	FulfillmentPacked:             {FulfillmentOutbound},
	FulfillmentOutbound:           {FulfillmentShipped},
	FulfillmentShipped:            {FulfillmentReceived},
}

// UpdateStatus 更新履约状态
func (o *FulfillmentOrchestrator) UpdateStatus(ctx context.Context, 
	orderID int64, newStatus FulfillmentStatus) error {
	// 1. 查询当前状态
	currentStatus, err := o.getStatus(ctx, orderID)
	if err != nil {
		return err
	}
	
	// 2. 检查状态流转是否合法
	allowedTransitions := fulfillmentTransitions[currentStatus]
	if !contains(allowedTransitions, newStatus) {
		return fmt.Errorf("不允许从%v转换到%v", currentStatus, newStatus)
	}
	
	// 3. 更新状态
	return o.updateStatusInDB(ctx, orderID, newStatus)
}

延伸思考

  1. 履约流程如何支持异常处理(缺货、商品损坏)?
  2. 多个发货单如何协调履约进度?
  3. 履约时效如何监控和告警?

📊 题目9:订单的退款和售后流程设计

问题描述: 用户申请退款(仅退款、退货退款),如何设计售后流程,保证资金安全和用户体验?

答案

退款场景

  1. 仅退款(未发货)
  2. 退货退款(已发货)
  3. 部分退款(退部分商品)
  4. 售后退款(商品质量问题)

推荐方案(Go实现):

退款状态机:

type RefundStatus int

const (
	RefundPending   RefundStatus = 0  // 待审核
	RefundApproved  RefundStatus = 1  // 已同意
	RefundRejected  RefundStatus = 2  // 已拒绝
	RefundReturning RefundStatus = 3  // 退货中
	RefundReturned  RefundStatus = 4  // 已退货
	RefundCompleted RefundStatus = 5  // 已退款
)

// Refund 退款单
type Refund struct {
	RefundID     int64
	OrderID      int64
	UserID       int64
	RefundType   string  // REFUND_ONLY, RETURN_REFUND
	RefundAmount decimal.Decimal
	Reason       string
	Status       RefundStatus
	CreatedAt    time.Time
}

// RefundService 退款服务
type RefundService struct {
	orderRepo   OrderRepository
	paymentSvc  PaymentService
	inventorySvc InventoryService
}

// CreateRefund 创建退款申请
func (s *RefundService) CreateRefund(ctx context.Context, req *RefundRequest) (*Refund, error) {
	// 1. 校验订单状态
	order, err := s.orderRepo.FindByID(ctx, req.OrderID)
	if err != nil {
		return nil, err
	}
	
	if order.Status != OrderStatusPaid && order.Status != OrderStatusShipped {
		return nil, errors.New("订单状态不允许退款")
	}
	
	// 2. 校验退款金额
	if req.RefundAmount.GreaterThan(order.PaidAmount) {
		return nil, errors.New("退款金额超过实付金额")
	}
	
	// 3. 创建退款单
	refund := &Refund{
		RefundID:     generateRefundID(),
		OrderID:      req.OrderID,
		UserID:       req.UserID,
		RefundType:   req.RefundType,
		RefundAmount: req.RefundAmount,
		Reason:       req.Reason,
		Status:       RefundPending,
		CreatedAt:    time.Now(),
	}
	
	if err := s.refundRepo.Create(ctx, refund); err != nil {
		return nil, err
	}
	
	// 4. 自动审核(部分场景)
	if s.shouldAutoApprove(refund) {
		return s.Approve(ctx, refund.RefundID)
	}
	
	return refund, nil
}

// Approve 审核通过退款
func (s *RefundService) Approve(ctx context.Context, refundID int64) (*Refund, error) {
	refund, err := s.refundRepo.FindByID(ctx, refundID)
	if err != nil {
		return nil, err
	}
	
	// 1. 更新退款状态
	refund.Status = RefundApproved
	if err := s.refundRepo.Update(ctx, refund); err != nil {
		return nil, err
	}
	
	// 2. 根据退款类型处理
	if refund.RefundType == "REFUND_ONLY" {
		// 仅退款:直接退款
		return s.processRefund(ctx, refund)
	} else {
		// 退货退款:等待用户退货
		refund.Status = RefundReturning
		s.refundRepo.Update(ctx, refund)
		// 生成退货地址和快递单号
		s.generateReturnLabel(ctx, refund)
		return refund, nil
	}
}

// processRefund 执行退款
func (s *RefundService) processRefund(ctx context.Context, refund *Refund) (*Refund, error) {
	// 1. 调用支付服务退款
	if err := s.paymentSvc.Refund(ctx, refund.OrderID, refund.RefundAmount); err != nil {
		return nil, fmt.Errorf("退款失败: %w", err)
	}
	
	// 2. 回补库存
	order, _ := s.orderRepo.FindByID(ctx, refund.OrderID)
	if err := s.inventorySvc.Return(ctx, order.Items); err != nil {
		log.Errorf("回补库存失败: %v", err)
		// 不阻塞退款流程,记录异常任务
		s.createCompensationTask(ctx, "ReturnInventory", refund.RefundID)
	}
	
	// 3. 更新退款状态
	refund.Status = RefundCompleted
	if err := s.refundRepo.Update(ctx, refund); err != nil {
		return nil, err
	}
	
	// 4. 更新订单状态
	s.orderRepo.UpdateStatus(ctx, refund.OrderID, OrderStatusRefunded)
	
	// 5. 发送通知
	s.notifySvc.Send(ctx, refund.UserID, "退款已到账")
	
	return refund, nil
}

自动审核规则

func (s *RefundService) shouldAutoApprove(refund *Refund) bool {
	// 自动同意条件:
	// 1. 订单未发货
	// 2. 退款金额 < 500元
	// 3. 用户信用良好
	
	order, _ := s.orderRepo.FindByID(context.Background(), refund.OrderID)
	
	if order.Status == OrderStatusPaid &&
		refund.RefundAmount.LessThan(decimal.NewFromInt(500)) &&
		s.userSvc.IsTrusted(refund.UserID) {
		return true
	}
	
	return false
}

延伸思考

  1. 退款失败如何重试和补偿?
  2. 恶意退款如何识别和防范?
  3. 部分退款如何计算退款金额(商品价+运费分摊)?

🔧 题目10:订单的异常处理(缺货、地址错误)

问题描述: 订单履约过程中可能出现异常(缺货、地址无法送达、商品损坏)。如何设计异常处理流程?

答案

异常场景及处理方案

  1. 库存不足(超卖)

    // 发现超卖
    func (s *FulfillmentService) HandleOutOfStock(ctx context.Context, orderID int64) error {
    	// 1. 联系用户
    	s.notifySvc.Send(ctx, order.UserID, "商品暂时缺货,为您申请退款")
    	
    	// 2. 创建退款
    	refund := &Refund{
    		OrderID:      orderID,
    		RefundType:   "OUT_OF_STOCK",
    		RefundAmount: order.PaidAmount,
    		AutoApprove:  true,
    	}
    	return s.refundSvc.CreateRefund(ctx, refund)
    }
    
  2. 地址无法送达

    func (s *FulfillmentService) HandleUndeliverableAddress(ctx context.Context, 
    	orderID int64) error {
    	// 1. 通知用户修改地址
    	s.notifySvc.Send(ctx, order.UserID, "收货地址无法送达,请修改地址")
    	
    	// 2. 订单挂起
    	s.orderRepo.UpdateStatus(ctx, orderID, OrderStatusAddressError)
    	
    	// 3. 用户修改地址后重新履约
    	// 或超时自动退款
    	s.scheduleAutoRefund(ctx, orderID, 48*time.Hour)
    	
    	return nil
    }
    
  3. 商品损坏

    func (s *FulfillmentService) HandleDamaged(ctx context.Context, 
    	orderID int64, itemID string) error {
    	// 1. 记录损坏
    	s.logDamage(ctx, orderID, itemID)
    	
    	// 2. 检查是否有替代品
    	if hasReplace, err := s.inventorySvc.CheckStock(ctx, itemID); err == nil && hasReplace {
    		// 有替代品,重新拣货
    		return s.repick(ctx, orderID, itemID)
    	}
    	
    	// 3. 无替代品,部分退款
    	item := s.getOrderItem(ctx, orderID, itemID)
    	return s.refundSvc.CreatePartialRefund(ctx, orderID, item.Amount)
    }
    

延伸思考

  1. 异常订单如何统计和分析?
  2. 如何设计异常的自动化处理规则?

💡 题目11:订单的搜索和查询优化

问题描述: 用户需要查询历史订单(按时间、状态、商品筛选),运营需要查询全部订单。如何设计订单查询系统?

答案

方案一:主从分离

用户查询(读从库):

// 查询我的订单
func (r *OrderRepository) FindUserOrders(ctx context.Context, 
	userID int64, filter *OrderFilter) ([]*Order, error) {
	// 路由到从库
	db := r.shardingMgr.GetReadDB(userID)
	
	query := `SELECT * FROM orders WHERE user_id=?`
	args := []interface{}{userID}
	
	// 添加筛选条件
	if filter.Status != "" {
		query += ` AND status=?`
		args = append(args, filter.Status)
	}
	
	if !filter.StartTime.IsZero() {
		query += ` AND created_at >= ?`
		args = append(args, filter.StartTime)
	}
	
	query += ` ORDER BY created_at DESC LIMIT ? OFFSET ?`
	args = append(args, filter.PageSize, filter.Offset)
	
	rows, err := db.QueryContext(ctx, query, args...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	
	return scanOrders(rows)
}

方案二:ES同步(推荐)

架构:

订单创建/更新 → Kafka → 同步Worker → Elasticsearch

ES索引设计:
{
  "order_id": "123",
  "user_id": 456,
  "status": "PAID",
  "total_amount": 1000,
  "created_at": "2024-04-18T10:00:00Z",
  "items": [
    {"sku_id": "789", "title": "iPhone 15"}
  ]
}

查询实现:

// 复杂查询用ES
func (r *OrderRepository) SearchOrders(ctx context.Context, 
	query *OrderSearchQuery) (*SearchResult, error) {
	esQuery := elastic.NewBoolQuery()
	
	// 用户维度
	if query.UserID > 0 {
		esQuery.Must(elastic.NewTermQuery("user_id", query.UserID))
	}
	
	// 订单号
	if query.OrderID != "" {
		esQuery.Must(elastic.NewTermQuery("order_id", query.OrderID))
	}
	
	// 状态
	if len(query.Statuses) > 0 {
		esQuery.Must(elastic.NewTermsQuery("status", query.Statuses...))
	}
	
	// 时间范围
	if !query.StartTime.IsZero() || !query.EndTime.IsZero() {
		rangeQuery := elastic.NewRangeQuery("created_at")
		if !query.StartTime.IsZero() {
			rangeQuery.Gte(query.StartTime)
		}
		if !query.EndTime.IsZero() {
			rangeQuery.Lte(query.EndTime)
		}
		esQuery.Must(rangeQuery)
	}
	
	// 商品筛选(嵌套查询)
	if query.SkuID != "" {
		esQuery.Must(elastic.NewNestedQuery("items",
			elastic.NewTermQuery("items.sku_id", query.SkuID)))
	}
	
	// 执行查询
	searchResult, err := r.esClient.Search().
		Index("orders").
		Query(esQuery).
		From(query.From).
		Size(query.Size).
		Sort("created_at", false).
		Do(ctx)
	
	if err != nil {
		return nil, err
	}
	
	return parseESResult(searchResult), nil
}

延伸思考

  1. 订单数据如何归档(如1年前的订单)?
  2. 分库分表+ES同步如何保证一致性?

📊 题目12:订单的消息通知设计

问题描述: 订单状态变化时需要通知用户(下单成功、发货、签收)。如何设计消息通知系统?

答案

通知渠道

  1. App推送
  2. 短信
  3. 微信公众号/服务号
  4. 站内信
  5. 邮件

推荐方案(Go实现):

package notification

import (
	"context"
)

// NotificationService 通知服务
type NotificationService struct {
	pushSvc     PushService     // App推送
	smsSvc      SMSService      // 短信
	wechatSvc   WechatService   // 微信
	emailSvc    EmailService    // 邮件
	inboxSvc    InboxService    // 站内信
}

// NotifyOrderStatusChanged 订单状态变更通知
func (s *NotificationService) NotifyOrderStatusChanged(ctx context.Context, 
	order *Order, oldStatus, newStatus OrderStatus) error {
	
	// 根据状态确定通知内容
	template := s.getTemplate(newStatus)
	
	// 并行发送多渠道通知
	errChan := make(chan error, 5)
	
	// 1. App推送(必发)
	go func() {
		errChan <- s.pushSvc.Push(ctx, order.UserID, PushMessage{
			Title:   template.Title,
			Content: template.Content,
			Data:    map[string]interface{}{"order_id": order.OrderID},
		})
	}()
	
	// 2. 短信(重要状态才发)
	if s.shouldSendSMS(newStatus) {
		go func() {
			phone := s.getUserPhone(ctx, order.UserID)
			errChan <- s.smsSvc.Send(ctx, phone, template.SMSContent)
		}()
	} else {
		errChan <- nil
	}
	
	// 3. 微信(用户已绑定才发)
	go func() {
		if openID := s.getUserWechatOpenID(ctx, order.UserID); openID != "" {
			errChan <- s.wechatSvc.SendTemplateMessage(ctx, openID, template.WechatTemplate)
		} else {
			errChan <- nil
		}
	}()
	
	// 4. 站内信(必发)
	go func() {
		errChan <- s.inboxSvc.Create(ctx, &InboxMessage{
			UserID:  order.UserID,
			Title:   template.Title,
			Content: template.Content,
			Type:    "ORDER_UPDATE",
		})
	}()
	
	// 5. 邮件(用户订阅才发)
	go func() {
		if s.userHasEmailSubscription(ctx, order.UserID) {
			email := s.getUserEmail(ctx, order.UserID)
			errChan <- s.emailSvc.Send(ctx, email, template.EmailContent)
		} else {
			errChan <- nil
		}
	}()
	
	// 收集结果(至少一个渠道成功即可)
	successCount := 0
	for i := 0; i < 5; i++ {
		if err := <-errChan; err == nil {
			successCount++
		}
	}
	
	if successCount == 0 {
		return errors.New("所有通知渠道都失败")
	}
	
	return nil
}

// 通知模板
func (s *NotificationService) getTemplate(status OrderStatus) *NotificationTemplate {
	templates := map[OrderStatus]*NotificationTemplate{
		OrderStatusPaid: {
			Title:       "订单支付成功",
			Content:     "您的订单已支付成功,我们将尽快为您发货",
			SMSContent:  "【京东】您的订单已支付成功,预计3天内送达",
		},
		OrderStatusShipped: {
			Title:       "订单已发货",
			Content:     "您的订单已发货,快递单号:SF1234567890",
			SMSContent:  "【京东】您的订单已发货,单号SF1234567890",
		},
		OrderStatusReceived: {
			Title:       "订单已签收",
			Content:     "您的订单已签收,期待您的评价",
		},
	}
	
	return templates[status]
}

// 是否发送短信
func (s *NotificationService) shouldSendSMS(status OrderStatus) bool {
	// 只有关键状态发短信(控制成本)
	importantStatuses := []OrderStatus{
		OrderStatusPaid,
		OrderStatusShipped,
		OrderStatusRefunded,
	}
	
	for _, s := range importantStatuses {
		if s == status {
			return true
		}
	}
	return false
}

延伸思考

  1. 通知失败如何重试?
  2. 如何设计通知的用户偏好设置(关闭某些通知)?
  3. 大批量通知如何限流(避免骚扰)?

🔧 题目13:订单数据的冷热分离

问题描述: 订单数据90天后很少查询,但占用大量存储。如何设计订单数据的冷热分离?

答案

推荐方案

// 冷热分离策略
type OrderArchiveService struct {
	hotDB  *sql.DB  // 热数据库(MySQL)
	coldDB *sql.DB  // 冷数据库(可以是低成本存储)
	ossClient OSSClient // 对象存储
}

// 归档策略
func (s *OrderArchiveService) ArchiveOrders(ctx context.Context) error {
	// 1. 查询90天前已完成的订单
	cutoffTime := time.Now().AddDate(0, 0, -90)
	
	query := `SELECT * FROM orders 
	          WHERE status IN ('COMPLETED', 'CANCELLED', 'REFUNDED')
	          AND updated_at < ?
	          LIMIT 1000`
	
	rows, err := s.hotDB.QueryContext(ctx, query, cutoffTime)
	if err != nil {
		return err
	}
	defer rows.Close()
	
	orders := make([]*Order, 0)
	for rows.Next() {
		order := &Order{}
		// 扫描数据...
		orders = append(orders, order)
	}
	
	// 2. 写入冷库
	for _, order := range orders {
		if err := s.writeToArchive(ctx, order); err != nil {
			log.Errorf("归档订单%d失败: %v", order.OrderID, err)
			continue
		}
		
		// 3. 删除热库数据
		if err := s.deleteFromHot(ctx, order.OrderID); err != nil {
			log.Errorf("删除热库订单%d失败: %v", order.OrderID, err)
		}
	}
	
	return nil
}

// 查询时智能路由
func (s *OrderArchiveService) FindByID(ctx context.Context, orderID int64) (*Order, error) {
	// 1. 先查热库
	order, err := s.queryFromHot(ctx, orderID)
	if err == nil && order != nil {
		return order, nil
	}
	
	// 2. 查冷库
	order, err = s.queryFromArchive(ctx, orderID)
	if err == nil && order != nil {
		return order, nil
	}
	
	return nil, ErrOrderNotFound
}

延伸思考

  1. 归档订单如何支持查询?
  2. 冷数据恢复到热库的策略?

💡 题目14:订单的限流和防刷

问题描述: 恶意用户频繁下单不支付,占用库存和系统资源。如何设计订单的限流和防刷机制?

答案

推荐方案(Go实现):

package ratelimit

import (
	"context"
	"fmt"
	"time"
	
	"github.com/go-redis/redis/v8"
)

// OrderRateLimiter 订单限流器
type OrderRateLimiter struct {
	rdb *redis.Client
}

// CheckLimit 检查用户是否超过限流
func (l *OrderRateLimiter) CheckLimit(ctx context.Context, userID int64) error {
	// 限流规则:
	// 1. 每分钟最多下单5次
	// 2. 每小时最多下单20次
	// 3. 每天最多50个待支付订单
	
	// 规则1:每分钟限流
	key1 := fmt.Sprintf("order:limit:min:%d:%s", userID, time.Now().Format("200601021504"))
	count1, err := l.rdb.Incr(ctx, key1).Result()
	if err != nil {
		return err
	}
	if count1 == 1 {
		l.rdb.Expire(ctx, key1, time.Minute)
	}
	if count1 > 5 {
		return errors.New("下单太频繁,请稍后再试")
	}
	
	// 规则2:每小时限流
	key2 := fmt.Sprintf("order:limit:hour:%d:%s", userID, time.Now().Format("2006010215"))
	count2, err := l.rdb.Incr(ctx, key2).Result()
	if err != nil {
		return err
	}
	if count2 == 1 {
		l.rdb.Expire(ctx, key2, time.Hour)
	}
	if count2 > 20 {
		return errors.New("您今天下单次数过多,请明天再试")
	}
	
	// 规则3:待支付订单数量限制
	pendingCount, err := l.getPendingOrderCount(ctx, userID)
	if err != nil {
		return err
	}
	if pendingCount >= 50 {
		return errors.New("您有过多待支付订单,请先完成支付")
	}
	
	return nil
}

// 用户信用评分
type UserCreditService struct {
	repo UserCreditRepository
}

func (s *UserCreditService) CheckCredit(ctx context.Context, userID int64) error {
	credit := s.repo.GetCredit(ctx, userID)
	
	// 信用分低于60分,禁止下单
	if credit.Score < 60 {
		return errors.New("您的信用分过低,暂时无法下单")
	}
	
	return nil
}

// 信用分扣减规则
func (s *UserCreditService) UpdateCredit(ctx context.Context, userID int64, behavior string) {
	switch behavior {
	case "ORDER_TIMEOUT":
		// 订单超时未支付:-5分
		s.repo.DeductCredit(ctx, userID, 5, "订单超时未支付")
	case "MALICIOUS_REFUND":
		// 恶意退款:-10分
		s.repo.DeductCredit(ctx, userID, 10, "恶意退款")
	case "ORDER_COMPLETED":
		// 订单完成:+1分
		s.repo.AddCredit(ctx, userID, 1, "订单完成")
	}
}

延伸思考

  1. 如何识别黄牛和恶意用户?
  2. 限流策略如何针对不同用户等级差异化?

📊 题目15:订单的实时数据统计

问题描述: 运营大盘需要实时显示订单量、GMV、转化率。如何设计订单的实时统计系统?

答案

推荐方案:Flink流式计算

// 实时统计指标
type OrderMetrics struct {
	Timestamp      time.Time
	OrderCount     int64           // 订单数
	GMV            decimal.Decimal // 交易额
	PaidOrderCount int64           // 已支付订单数
	AvgOrderAmount decimal.Decimal // 客单价
}

// 指标计算Worker(消费Kafka)
func ConsumeOrderEvents(ctx context.Context) {
	consumer := kafka.NewConsumer(...)
	
	for {
		msg, err := consumer.ReadMessage(ctx)
		if err != nil {
			continue
		}
		
		event := parseOrderEvent(msg.Value)
		
		switch event.Type {
		case "OrderCreated":
			// 订单数+1
			metrics.IncrOrderCount()
			
		case "OrderPaid":
			// 已支付订单数+1
			metrics.IncrPaidOrderCount()
			// GMV累加
			metrics.AddGMV(event.Order.PaidAmount)
			
		case "OrderCancelled":
			// 订单数-1(或单独统计取消数)
			metrics.IncrCancelledOrderCount()
		}
		
		// 定期刷新到Redis
		if time.Now().Unix()%10 == 0 {
			metrics.FlushToRedis()
		}
	}
}

// 实时大盘查询
func GetRealTimeMetrics(ctx context.Context) (*OrderMetrics, error) {
	// 从Redis读取实时指标
	rdb := redis.NewClient(...)
	
	orderCount, _ := rdb.Get(ctx, "metrics:order:count").Int64()
	gmv, _ := rdb.Get(ctx, "metrics:order:gmv").Float64()
	paidCount, _ := rdb.Get(ctx, "metrics:order:paid_count").Int64()
	
	return &OrderMetrics{
		Timestamp:      time.Now(),
		OrderCount:     orderCount,
		GMV:            decimal.NewFromFloat(gmv),
		PaidOrderCount: paidCount,
		AvgOrderAmount: decimal.NewFromFloat(gmv).Div(decimal.NewFromInt(paidCount)),
	}, nil
}

延伸思考

  1. 实时统计如何保证准确性(与离线对账)?
  2. 多维度统计(按类目、品牌)如何设计?

3.4 支付系统(10题)

📊 题目1:支付系统的整体架构设计

问题描述: 电商平台需要支持多种支付方式(支付宝、微信、银行卡)。如何设计支付系统的整体架构?

答案

问题分析: 支付系统的核心要素:

  1. 多渠道接入(支付宝、微信、银联)
  2. 支付安全性
  3. 异步回调处理
  4. 对账和资金安全

架构设计(Go实现):

package payment

import (
	"context"
	"time"
)

// PaymentChannel 支付渠道
type PaymentChannel string

const (
	ChannelAlipay PaymentChannel = "ALIPAY"
	ChannelWechat PaymentChannel = "WECHAT"
	ChannelUnion  PaymentChannel = "UNION"
)

// PaymentService 支付服务
type PaymentService struct {
	alipayAdapter  PaymentAdapter
	wechatAdapter  PaymentAdapter
	unionAdapter   PaymentAdapter
	paymentRepo    PaymentRepository
	orderSvc       OrderService
}

// PaymentAdapter 支付适配器接口(适配器模式)
type PaymentAdapter interface {
	// 创建支付
	CreatePayment(ctx context.Context, req *PaymentRequest) (*PaymentResponse, error)
	// 查询支付状态
	QueryPayment(ctx context.Context, paymentID string) (*PaymentStatus, error)
	// 申请退款
	Refund(ctx context.Context, req *RefundRequest) error
	// 验证回调签名
	VerifyCallback(callback *CallbackData) error
}

// Payment 支付单
type Payment struct {
	PaymentID       string
	OrderID         int64
	UserID          int64
	Channel         PaymentChannel
	Amount          decimal.Decimal
	Status          PaymentStatus
	ThirdPartyID    string  // 第三方支付单号
	CallbackData    string  // 回调原始数据
	CreatedAt       time.Time
	PaidAt          *time.Time
}

// CreatePayment 创建支付
func (s *PaymentService) CreatePayment(ctx context.Context, 
	orderID int64, channel PaymentChannel) (*PaymentResponse, error) {
	
	// 1. 查询订单
	order, err := s.orderSvc.GetOrder(ctx, orderID)
	if err != nil {
		return nil, err
	}
	
	// 2. 校验订单状态
	if order.Status != OrderStatusPending {
		return nil, errors.New("订单状态不正确")
	}
	
	// 3. 创建支付单
	payment := &Payment{
		PaymentID: generatePaymentID(),
		OrderID:   orderID,
		UserID:    order.UserID,
		Channel:   channel,
		Amount:    order.TotalAmount,
		Status:    PaymentStatusPending,
		CreatedAt: time.Now(),
	}
	
	if err := s.paymentRepo.Create(ctx, payment); err != nil {
		return nil, err
	}
	
	// 4. 调用支付渠道
	adapter := s.getAdapter(channel)
	resp, err := adapter.CreatePayment(ctx, &PaymentRequest{
		OutTradeNo:  payment.PaymentID,
		Amount:      payment.Amount,
		Subject:     fmt.Sprintf("订单%d支付", orderID),
		NotifyURL:   "https://api.example.com/payment/callback",
		ReturnURL:   "https://www.example.com/order/success",
	})
	
	if err != nil {
		return nil, err
	}
	
	// 5. 保存第三方支付单号
	payment.ThirdPartyID = resp.TradeNo
	s.paymentRepo.Update(ctx, payment)
	
	return resp, nil
}

// 获取支付适配器
func (s *PaymentService) getAdapter(channel PaymentChannel) PaymentAdapter {
	switch channel {
	case ChannelAlipay:
		return s.alipayAdapter
	case ChannelWechat:
		return s.wechatAdapter
	case ChannelUnion:
		return s.unionAdapter
	default:
		return nil
	}
}

// HandleCallback 处理支付回调
func (s *PaymentService) HandleCallback(ctx context.Context, 
	channel PaymentChannel, callback *CallbackData) error {
	
	// 1. 验证签名
	adapter := s.getAdapter(channel)
	if err := adapter.VerifyCallback(callback); err != nil {
		return fmt.Errorf("签名验证失败: %w", err)
	}
	
	// 2. 查询支付单
	payment, err := s.paymentRepo.FindByID(ctx, callback.OutTradeNo)
	if err != nil {
		return err
	}
	
	// 3. 幂等性检查
	if payment.Status == PaymentStatusSuccess {
		return nil // 已处理,直接返回
	}
	
	// 4. 更新支付单状态
	payment.Status = PaymentStatusSuccess
	payment.PaidAt = &callback.PayTime
	payment.CallbackData = callback.RawData
	
	if err := s.paymentRepo.Update(ctx, payment); err != nil {
		return err
	}
	
	// 5. 更新订单状态
	if err := s.orderSvc.MarkAsPaid(ctx, payment.OrderID); err != nil {
		// 支付成功但订单更新失败,记录补偿任务
		s.createCompensationTask(ctx, payment.PaymentID)
		return err
	}
	
	// 6. 发布支付成功事件
	s.eventBus.Publish(&PaymentSuccessEvent{
		OrderID:   payment.OrderID,
		PaymentID: payment.PaymentID,
		Amount:    payment.Amount,
	})
	
	return nil
}

支付宝适配器示例

type AlipayAdapter struct {
	client *alipay.Client
}

func (a *AlipayAdapter) CreatePayment(ctx context.Context, 
	req *PaymentRequest) (*PaymentResponse, error) {
	
	// 调用支付宝SDK
	payReq := alipay.TradeAppPay{
		OutTradeNo:  req.OutTradeNo,
		TotalAmount: req.Amount.String(),
		Subject:     req.Subject,
		NotifyURL:   req.NotifyURL,
	}
	
	orderStr, err := a.client.TradeAppPay(payReq)
	if err != nil {
		return nil, err
	}
	
	return &PaymentResponse{
		PayData: orderStr, // APP端拉起支付宝所需的参数
	}, nil
}

func (a *AlipayAdapter) VerifyCallback(callback *CallbackData) error {
	// 验证支付宝回调签名
	return a.client.VerifySign(callback.RawData)
}

延伸思考

  1. 支付系统如何实现高可用?
  2. 支付渠道故障如何降级?
  3. 支付回调丢失如何处理?

🔧 题目2:支付回调的幂等性处理

问题描述: 支付回调可能重复发送(网络重试、第三方重推)。如何保证支付回调处理的幂等性?

答案

推荐方案(Go实现):

// 幂等性处理
func (s *PaymentService) HandleCallbackIdempotent(ctx context.Context, 
	callback *CallbackData) error {
	
	paymentID := callback.OutTradeNo
	lockKey := fmt.Sprintf("payment:callback:lock:%s", paymentID)
	
	// 1. 获取分布式锁
	lock := redis.NewDistributedLock(s.rdb, lockKey)
	acquired, err := lock.TryLock(ctx, 30*time.Second)
	if err != nil {
		return err
	}
	if !acquired {
		// 其他请求正在处理,直接返回成功
		return nil
	}
	defer lock.Unlock(ctx)
	
	// 2. 查询支付单
	payment, err := s.paymentRepo.FindByID(ctx, paymentID)
	if err != nil {
		return err
	}
	
	// 3. 状态检查(幂等性)
	if payment.Status == PaymentStatusSuccess {
		log.Infof("支付单%s已处理,跳过", paymentID)
		return nil // 已成功,幂等返回
	}
	
	// 4. 使用数据库行锁+版本号
	affected, err := s.paymentRepo.UpdateStatusWithVersion(ctx, 
		paymentID, 
		PaymentStatusSuccess,
		payment.Version,
	)
	
	if err != nil {
		return err
	}
	
	if affected == 0 {
		// 版本号不匹配,说明已被其他请求处理
		log.Warnf("支付单%s已被处理,版本冲突", paymentID)
		return nil
	}
	
	// 5. 执行后续操作
	return s.postPaymentProcess(ctx, payment)
}

// 数据库更新(带版本号)
func (r *PaymentRepository) UpdateStatusWithVersion(ctx context.Context, 
	paymentID string, newStatus PaymentStatus, expectedVersion int) (int64, error) {
	
	query := `UPDATE payments 
	          SET status=?, version=version+1, updated_at=?
	          WHERE payment_id=? AND version=? AND status!=?`
	
	result, err := r.db.ExecContext(ctx, query, 
		newStatus, time.Now(), paymentID, expectedVersion, PaymentStatusSuccess)
	if err != nil {
		return 0, err
	}
	
	return result.RowsAffected()
}

延伸思考

  1. 如何设计支付回调的重试机制?
  2. 回调处理失败如何人工介入?

💡 题目3:支付的对账系统设计

问题描述: 每天需要与支付宝、微信对账,确保平台账和渠道账一致。如何设计支付对账系统?

答案

对账流程(Go实现):

package reconciliation

import (
	"context"
	"time"
)

// ReconciliationService 对账服务
type ReconciliationService struct {
	paymentRepo PaymentRepository
	alipayClient *alipay.Client
	wechatClient *wechat.Client
}

// DailyReconciliation 每日对账
func (s *ReconciliationService) DailyReconciliation(ctx context.Context, date time.Time) error {
	// 1. 下载渠道对账单
	alipayBill, err := s.downloadAlipayBill(ctx, date)
	if err != nil {
		return err
	}
	
	wechatBill, err := s.downloadWechatBill(ctx, date)
	if err != nil {
		return err
	}
	
	// 2. 查询平台当日支付记录
	platformRecords, err := s.paymentRepo.FindByDate(ctx, date)
	if err != nil {
		return err
	}
	
	// 3. 三方对账
	diff := s.compare(platformRecords, alipayBill, wechatBill)
	
	// 4. 处理差异
	if err := s.handleDifferences(ctx, diff); err != nil {
		return err
	}
	
	// 5. 生成对账报告
	report := s.generateReport(diff)
	s.saveReport(ctx, report)
	
	return nil
}

// ReconciliationDiff 对账差异
type ReconciliationDiff struct {
	OnlyInPlatform   []*Payment  // 只在平台有
	OnlyInChannel    []*ChannelRecord  // 只在渠道有
	AmountMismatch   []*Mismatch  // 金额不一致
	StatusMismatch   []*Mismatch  // 状态不一致
}

// compare 比对数据
func (s *ReconciliationService) compare(platform []*Payment, 
	alipay, wechat []*ChannelRecord) *ReconciliationDiff {
	
	diff := &ReconciliationDiff{}
	
	// 构建平台数据map
	platformMap := make(map[string]*Payment)
	for _, p := range platform {
		platformMap[p.ThirdPartyID] = p
	}
	
	// 构建渠道数据map
	channelMap := make(map[string]*ChannelRecord)
	for _, c := range alipay {
		channelMap[c.TradeNo] = c
	}
	for _, c := range wechat {
		channelMap[c.TransactionID] = c
	}
	
	// 比对
	for tradeNo, channelRecord := range channelMap {
		platformRecord, exists := platformMap[tradeNo]
		
		if !exists {
			// 只在渠道有,平台无
			diff.OnlyInChannel = append(diff.OnlyInChannel, channelRecord)
		} else {
			// 金额比对
			if !platformRecord.Amount.Equal(channelRecord.Amount) {
				diff.AmountMismatch = append(diff.AmountMismatch, &Mismatch{
					TradeNo:        tradeNo,
					PlatformAmount: platformRecord.Amount,
					ChannelAmount:  channelRecord.Amount,
				})
			}
			
			// 状态比对
			if platformRecord.Status != channelRecord.Status {
				diff.StatusMismatch = append(diff.StatusMismatch, &Mismatch{
					TradeNo:       tradeNo,
					PlatformStatus: platformRecord.Status,
					ChannelStatus:  channelRecord.Status,
				})
			}
			
			delete(platformMap, tradeNo)
		}
	}
	
	// 只在平台有的
	for _, p := range platformMap {
		diff.OnlyInPlatform = append(diff.OnlyInPlatform, p)
	}
	
	return diff
}

// handleDifferences 处理差异
func (s *ReconciliationService) handleDifferences(ctx context.Context, 
	diff *ReconciliationDiff) error {
	
	// 1. 只在渠道有的(平台漏单)
	for _, record := range diff.OnlyInChannel {
		log.Warnf("平台漏单: %s", record.TradeNo)
		// 补单:创建支付记录
		s.createMissingPayment(ctx, record)
	}
	
	// 2. 只在平台有的(渠道无记录,可能未支付成功)
	for _, payment := range diff.OnlyInPlatform {
		log.Warnf("渠道无记录: %s", payment.PaymentID)
		// 主动查询第三方状态
		s.queryThirdPartyStatus(ctx, payment)
	}
	
	// 3. 金额不一致
	for _, mismatch := range diff.AmountMismatch {
		log.Errorf("金额不一致: %s, 平台=%v, 渠道=%v", 
			mismatch.TradeNo, mismatch.PlatformAmount, mismatch.ChannelAmount)
		// 转人工处理
		s.createManualTask(ctx, "AMOUNT_MISMATCH", mismatch)
	}
	
	// 4. 状态不一致
	for _, mismatch := range diff.StatusMismatch {
		log.Warnf("状态不一致: %s", mismatch.TradeNo)
		// 以渠道状态为准,更新平台状态
		s.syncStatus(ctx, mismatch)
	}
	
	return nil
}

对账报告

type ReconciliationReport struct {
	Date              time.Time
	TotalCount        int
	MatchCount        int
	MismatchCount     int
	OnlyInPlatform    int
	OnlyInChannel     int
	AmountMismatch    int
	TotalAmount       decimal.Decimal
	ChannelTotalAmount decimal.Decimal
}

延伸思考

  1. 对账差异如何自动修复?
  2. 对账失败如何告警和处理?
  3. 实时对账和T+1对账如何结合?

📊 题目4:支付的异步回调处理

问题描述: 支付成功后,第三方通过回调通知平台。回调可能延迟、丢失、重复。如何设计健壮的回调处理机制?

答案

推荐方案(Go实现):

// 回调处理器
type CallbackHandler struct {
	paymentSvc  *PaymentService
	orderSvc    *OrderService
	lockSvc     *DistributedLockService
}

// HandleCallback 处理回调
func (h *CallbackHandler) HandleCallback(ctx context.Context, 
	channel PaymentChannel, rawData []byte) error {
	
	// 1. 解析回调数据
	callback, err := parseCallback(channel, rawData)
	if err != nil {
		return fmt.Errorf("解析回调失败: %w", err)
	}
	
	// 2. 记录回调日志(用于排查问题)
	h.logCallback(ctx, callback)
	
	// 3. 验证签名
	adapter := h.paymentSvc.getAdapter(channel)
	if err := adapter.VerifyCallback(callback); err != nil {
		log.Errorf("回调签名验证失败: %v", err)
		return err
	}
	
	// 4. 幂等性处理(分布式锁)
	lockKey := fmt.Sprintf("payment:callback:%s", callback.OutTradeNo)
	acquired, err := h.lockSvc.TryLock(ctx, lockKey, 30*time.Second)
	if err != nil {
		return err
	}
	if !acquired {
		log.Infof("回调%s正在处理中,跳过", callback.OutTradeNo)
		return nil
	}
	defer h.lockSvc.Unlock(ctx, lockKey)
	
	// 5. 处理支付结果
	return h.paymentSvc.HandleCallback(ctx, channel, callback)
}

// 主动查询(回调超时补偿)
func (h *CallbackHandler) QueryPaymentStatus(ctx context.Context) {
	// 定时任务:查询10分钟前创建但未回调的支付单
	ticker := time.NewTicker(1 * time.Minute)
	defer ticker.Stop()
	
	for range ticker.C {
		cutoffTime := time.Now().Add(-10 * time.Minute)
		
		// 查询超时支付单
		payments, err := h.paymentSvc.FindPendingPayments(ctx, cutoffTime)
		if err != nil {
			log.Errorf("查询超时支付单失败: %v", err)
			continue
		}
		
		for _, payment := range payments {
			// 主动查询第三方状态
			go func(p *Payment) {
				adapter := h.paymentSvc.getAdapter(p.Channel)
				status, err := adapter.QueryPayment(ctx, p.ThirdPartyID)
				if err != nil {
					log.Errorf("查询支付状态失败: %v", err)
					return
				}
				
				// 如果已支付,补偿处理
				if status.Status == "SUCCESS" {
					log.Warnf("支付单%s回调丢失,主动补偿", p.PaymentID)
					h.paymentSvc.MarkAsPaid(ctx, p.PaymentID)
				}
			}(payment)
		}
	}
}

回调重试策略

// 回调处理失败时的重试
func (h *CallbackHandler) retryCallback(ctx context.Context, 
	callback *CallbackData) error {
	
	maxRetries := 5
	backoff := []time.Duration{
		1 * time.Second,
		5 * time.Second,
		30 * time.Second,
		2 * time.Minute,
		10 * time.Minute,
	}
	
	for i := 0; i < maxRetries; i++ {
		err := h.HandleCallback(ctx, callback.Channel, callback.RawData)
		if err == nil {
			return nil // 成功
		}
		
		log.Warnf("回调处理失败,第%d次重试: %v", i+1, err)
		
		if i < maxRetries-1 {
			time.Sleep(backoff[i])
		}
	}
	
	// 所有重试失败,记录人工任务
	return h.createManualTask(ctx, "CALLBACK_FAILED", callback)
}

延伸思考

  1. 回调接口如何防止伪造(恶意请求)?
  2. 回调处理超时如何设置?

🔧 题目5:支付的分账系统设计(平台+商家)

问题描述: B2B2C平台,用户支付100元,平台抽佣10%,商家获得90元。如何设计支付分账系统?

答案

推荐方案(Go实现):

// Settlement 结算单
type Settlement struct {
	SettlementID   string
	OrderID        int64
	MerchantID     int64
	TotalAmount    decimal.Decimal  // 订单总额
	PlatformAmount decimal.Decimal  // 平台佣金
	MerchantAmount decimal.Decimal  // 商家收入
	Status         SettlementStatus
	SettledAt      *time.Time
}

// SettlementService 结算服务
type SettlementService struct {
	settlementRepo SettlementRepository
	paymentSvc     PaymentService
}

// CreateSettlement 创建结算单
func (s *SettlementService) CreateSettlement(ctx context.Context, 
	orderID int64) error {
	
	// 1. 查询订单
	order := s.orderSvc.GetOrder(ctx, orderID)
	
	// 2. 计算佣金
	commissionRate := s.getCommissionRate(ctx, order.MerchantID)
	platformAmount := order.TotalAmount.Mul(commissionRate)
	merchantAmount := order.TotalAmount.Sub(platformAmount)
	
	// 3. 创建结算单
	settlement := &Settlement{
		SettlementID:   generateSettlementID(),
		OrderID:        orderID,
		MerchantID:     order.MerchantID,
		TotalAmount:    order.TotalAmount,
		PlatformAmount: platformAmount,
		MerchantAmount: merchantAmount,
		Status:         SettlementPending,
	}
	
	return s.settlementRepo.Create(ctx, settlement)
}

// Settle 执行结算(T+N结算)
func (s *SettlementService) Settle(ctx context.Context, merchantID int64, date time.Time) error {
	// 1. 查询该商家待结算的订单
	settlements, err := s.settlementRepo.FindPendingByMerchant(ctx, merchantID, date)
	if err != nil {
		return err
	}
	
	// 2. 汇总金额
	totalAmount := decimal.Zero
	for _, s := range settlements {
		totalAmount = totalAmount.Add(s.MerchantAmount)
	}
	
	// 3. 调用支付渠道分账/转账
	if err := s.paymentSvc.Transfer(ctx, &TransferRequest{
		ToAccount: s.getMerchantAccount(ctx, merchantID),
		Amount:    totalAmount,
		Remark:    fmt.Sprintf("商家%d的%s结算", merchantID, date.Format("2006-01-02")),
	}); err != nil {
		return err
	}
	
	// 4. 更新结算单状态
	for _, settlement := range settlements {
		settlement.Status = SettlementCompleted
		settlement.SettledAt = timePtr(time.Now())
		s.settlementRepo.Update(ctx, settlement)
	}
	
	return nil
}

// 佣金率配置
func (s *SettlementService) getCommissionRate(ctx context.Context, merchantID int64) decimal.Decimal {
	// 根据商家等级、类目等确定佣金率
	merchant := s.merchantSvc.GetMerchant(ctx, merchantID)
	
	switch merchant.Level {
	case "VIP":
		return decimal.NewFromFloat(0.05) // 5%
	case "GOLD":
		return decimal.NewFromFloat(0.08) // 8%
	default:
		return decimal.NewFromFloat(0.10) // 10%
	}
}

结算周期

T+0:实时结算(高成本,高信用商家)
T+1:次日结算(平衡)
T+7:周结算(标准)
T+30:月结算(新商家)

延伸思考

  1. 如何设计结算的对账机制?
  2. 商家提现如何设计?
  3. 结算失败如何处理?

📊 题目6:支付密码和安全设计

问题描述: 支付环节涉及资金安全,如何设计支付密码、短信验证码等安全机制?

答案

推荐方案(Go实现):

// PaymentSecurityService 支付安全服务
type PaymentSecurityService struct {
	rdb        *redis.Client
	smsSvc     SMSService
	encryptSvc EncryptService
}

// VerifyPaymentPassword 验证支付密码
func (s *PaymentSecurityService) VerifyPaymentPassword(ctx context.Context, 
	userID int64, password string) error {
	
	// 1. 获取用户存储的支付密码(加密)
	user, err := s.userRepo.FindByID(ctx, userID)
	if err != nil {
		return err
	}
	
	if user.PaymentPassword == "" {
		return errors.New("请先设置支付密码")
	}
	
	// 2. 验证密码
	if !s.encryptSvc.VerifyPassword(password, user.PaymentPassword) {
		// 记录失败次数
		failCount := s.incrFailCount(ctx, userID)
		
		// 超过5次锁定账户
		if failCount >= 5 {
			s.lockAccount(ctx, userID, 30*time.Minute)
			return errors.New("密码错误次数过多,账户已锁定30分钟")
		}
		
		return fmt.Errorf("密码错误,还可尝试%d次", 5-failCount)
	}
	
	// 3. 清除失败计数
	s.clearFailCount(ctx, userID)
	
	return nil
}

// SendPaymentSMS 发送支付验证码
func (s *PaymentSecurityService) SendPaymentSMS(ctx context.Context, 
	userID int64, phone string) error {
	
	// 1. 限流检查(防止短信轰炸)
	key := fmt.Sprintf("sms:limit:%s", phone)
	count, err := s.rdb.Incr(ctx, key).Result()
	if err != nil {
		return err
	}
	if count == 1 {
		s.rdb.Expire(ctx, key, time.Hour)
	}
	if count > 5 {
		return errors.New("发送次数过多,请1小时后再试")
	}
	
	// 2. 生成6位验证码
	code := fmt.Sprintf("%06d", rand.Intn(1000000))
	
	// 3. 存储验证码(5分钟有效)
	codeKey := fmt.Sprintf("sms:code:%s", phone)
	s.rdb.SetEX(ctx, codeKey, code, 5*time.Minute)
	
	// 4. 发送短信
	return s.smsSvc.Send(ctx, phone, fmt.Sprintf("您的支付验证码是%s,5分钟内有效", code))
}

// VerifySMSCode 验证短信验证码
func (s *PaymentSecurityService) VerifySMSCode(ctx context.Context, 
	phone, code string) error {
	
	codeKey := fmt.Sprintf("sms:code:%s", phone)
	
	// 查询验证码
	storedCode, err := s.rdb.Get(ctx, codeKey).Result()
	if err == redis.Nil {
		return errors.New("验证码已过期")
	}
	if err != nil {
		return err
	}
	
	// 验证
	if storedCode != code {
		return errors.New("验证码错误")
	}
	
	// 验证成功,删除验证码(防止重复使用)
	s.rdb.Del(ctx, codeKey)
	
	return nil
}

// 风控检查
func (s *PaymentSecurityService) RiskCheck(ctx context.Context, 
	userID int64, amount decimal.Decimal) error {
	
	// 规则1:大额支付需要额外验证
	if amount.GreaterThan(decimal.NewFromInt(5000)) {
		// 需要短信验证码或支付密码
		return errors.New("REQUIRE_SMS_OR_PASSWORD")
	}
	
	// 规则2:新用户限额
	user := s.userSvc.GetUser(ctx, userID)
	if user.RegisterDays() < 7 && amount.GreaterThan(decimal.NewFromInt(1000)) {
		return errors.New("新用户单笔限额1000元")
	}
	
	// 规则3:异常IP检测
	ip := s.getRequestIP(ctx)
	if s.isBlacklistIP(ctx, ip) {
		return errors.New("异常IP,禁止支付")
	}
	
	// 规则4:高频支付检测
	recentPayments := s.getRecentPaymentCount(ctx, userID, 10*time.Minute)
	if recentPayments > 10 {
		return errors.New("支付频率异常")
	}
	
	return nil
}

延伸思考

  1. 支付密码如何加密存储?
  2. 如何设计支付的二次确认(大额支付)?
  3. 支付安全如何平衡用户体验?

🔧 题目7:支付渠道的路由和降级

问题描述: 支付宝渠道故障时,如何自动切换到微信支付?如何设计支付渠道的路由和降级策略?

答案

推荐方案(Go实现):

// ChannelRouter 支付渠道路由器
type ChannelRouter struct {
	healthChecker *ChannelHealthChecker
	config        *RoutingConfig
}

// SelectChannel 选择支付渠道
func (r *ChannelRouter) SelectChannel(ctx context.Context, 
	preferredChannel PaymentChannel) (PaymentChannel, error) {
	
	// 1. 检查首选渠道健康状态
	if r.healthChecker.IsHealthy(preferredChannel) {
		return preferredChannel, nil
	}
	
	log.Warnf("渠道%s不可用,尝试降级", preferredChannel)
	
	// 2. 降级到备用渠道
	fallbackChannels := r.config.GetFallback(preferredChannel)
	for _, channel := range fallbackChannels {
		if r.healthChecker.IsHealthy(channel) {
			log.Infof("降级到渠道%s", channel)
			return channel, nil
		}
	}
	
	// 3. 所有渠道都不可用
	return "", errors.New("支付渠道暂时不可用,请稍后再试")
}

// ChannelHealthChecker 渠道健康检查
type ChannelHealthChecker struct {
	rdb *redis.Client
}

func (c *ChannelHealthChecker) IsHealthy(channel PaymentChannel) bool {
	key := fmt.Sprintf("payment:channel:health:%s", channel)
	
	// 从Redis读取健康状态
	status, err := c.rdb.Get(context.Background(), key).Result()
	if err != nil || status != "UP" {
		return false
	}
	
	return true
}

// 健康检查任务(心跳)
func (c *ChannelHealthChecker) StartHealthCheck(ctx context.Context) {
	ticker := time.NewTicker(30 * time.Second)
	defer ticker.Stop()
	
	for range ticker.C {
		// 对每个渠道执行健康检查
		for _, channel := range AllChannels {
			go c.checkChannel(ctx, channel)
		}
	}
}

func (c *ChannelHealthChecker) checkChannel(ctx context.Context, 
	channel PaymentChannel) {
	
	adapter := getAdapter(channel)
	
	// 调用渠道健康检查接口(或创建1分钱订单测试)
	err := adapter.HealthCheck(ctx)
	
	key := fmt.Sprintf("payment:channel:health:%s", channel)
	if err != nil {
		// 不健康
		c.rdb.SetEX(ctx, key, "DOWN", 5*time.Minute)
		log.Errorf("渠道%s健康检查失败: %v", channel, err)
		
		// 告警
		c.alertSvc.Send(fmt.Sprintf("支付渠道%s故障", channel))
	} else {
		// 健康
		c.rdb.SetEX(ctx, key, "UP", 5*time.Minute)
	}
}

// 路由配置
type RoutingConfig struct {
	fallbacks map[PaymentChannel][]PaymentChannel
}

func NewRoutingConfig() *RoutingConfig {
	return &RoutingConfig{
		fallbacks: map[PaymentChannel][]PaymentChannel{
			ChannelAlipay: {ChannelWechat, ChannelUnion},  // 支付宝 → 微信 → 银联
			ChannelWechat: {ChannelAlipay, ChannelUnion},
			ChannelUnion:  {ChannelAlipay, ChannelWechat},
		},
	}
}

延伸思考

  1. 如何设计支付渠道的成本优化(选择手续费低的)?
  2. 支付渠道限额如何处理?

💡 题目8:支付的退款处理

问题描述: 用户申请退款,需要原路退回。如何设计退款流程,处理退款失败、部分退款等场景?

答案

推荐方案(Go实现):

// RefundService 退款服务
type RefundService struct {
	paymentRepo PaymentRepository
}

// Refund 申请退款
func (s *RefundService) Refund(ctx context.Context, req *RefundRequest) error {
	// 1. 查询原支付记录
	payment, err := s.paymentRepo.FindByOrderID(ctx, req.OrderID)
	if err != nil {
		return err
	}
	
	// 2. 校验退款金额
	if req.RefundAmount.GreaterThan(payment.Amount) {
		return errors.New("退款金额超过支付金额")
	}
	
	// 3. 检查是否已退款
	totalRefunded, err := s.paymentRepo.GetTotalRefundedAmount(ctx, payment.PaymentID)
	if err != nil {
		return err
	}
	
	if totalRefunded.Add(req.RefundAmount).GreaterThan(payment.Amount) {
		return errors.New("累计退款金额超过支付金额")
	}
	
	// 4. 创建退款记录
	refund := &PaymentRefund{
		RefundID:    generateRefundID(),
		PaymentID:   payment.PaymentID,
		OrderID:     req.OrderID,
		Amount:      req.RefundAmount,
		Reason:      req.Reason,
		Status:      RefundStatusPending,
		CreatedAt:   time.Now(),
	}
	
	if err := s.refundRepo.Create(ctx, refund); err != nil {
		return err
	}
	
	// 5. 调用第三方退款接口
	adapter := s.getAdapter(payment.Channel)
	err = adapter.Refund(ctx, &ThirdPartyRefundRequest{
		OutRefundNo:   refund.RefundID,
		OutTradeNo:    payment.ThirdPartyID,
		RefundAmount:  req.RefundAmount,
		TotalAmount:   payment.Amount,
		RefundReason:  req.Reason,
	})
	
	if err != nil {
		refund.Status = RefundStatusFailed
		refund.FailReason = err.Error()
		s.refundRepo.Update(ctx, refund)
		return err
	}
	
	// 6. 更新退款状态
	refund.Status = RefundStatusSuccess
	refund.RefundedAt = timePtr(time.Now())
	s.refundRepo.Update(ctx, refund)
	
	return nil
}

// 退款重试(定时任务)
func (s *RefundService) RetryFailedRefunds(ctx context.Context) {
	ticker := time.NewTicker(5 * time.Minute)
	defer ticker.Stop()
	
	for range ticker.C {
		// 查询失败的退款(创建时间<30分钟前)
		refunds, err := s.refundRepo.FindFailed(ctx, time.Now().Add(-30*time.Minute))
		if err != nil {
			log.Errorf("查询失败退款失败: %v", err)
			continue
		}
		
		for _, refund := range refunds {
			// 重试退款
			go func(r *PaymentRefund) {
				if r.RetryCount >= 5 {
					log.Errorf("退款%s重试次数过多,转人工处理", r.RefundID)
					s.createManualTask(ctx, r.RefundID)
					return
				}
				
				payment, _ := s.paymentRepo.FindByID(ctx, r.PaymentID)
				adapter := s.getAdapter(payment.Channel)
				
				err := adapter.Refund(ctx, &ThirdPartyRefundRequest{
					OutRefundNo:  r.RefundID,
					OutTradeNo:   payment.ThirdPartyID,
					RefundAmount: r.Amount,
					TotalAmount:  payment.Amount,
				})
				
				if err == nil {
					r.Status = RefundStatusSuccess
					r.RefundedAt = timePtr(time.Now())
				} else {
					r.RetryCount++
					r.FailReason = err.Error()
				}
				
				s.refundRepo.Update(ctx, r)
			}(refund)
		}
	}
}

部分退款处理

// 部分退款(一单多件商品,退部分)
func (s *RefundService) PartialRefund(ctx context.Context, 
	orderID int64, items []RefundItem) error {
	
	// 1. 计算退款金额
	var refundAmount decimal.Decimal
	for _, item := range items {
		itemAmount := item.Price.Mul(decimal.NewFromInt(int64(item.Quantity)))
		refundAmount = refundAmount.Add(itemAmount)
	}
	
	// 2. 分摊运费
	order := s.orderSvc.GetOrder(ctx, orderID)
	refundItemCount := len(items)
	totalItemCount := len(order.Items)
	
	shippingRefund := order.ShippingFee.
		Mul(decimal.NewFromInt(int64(refundItemCount))).
		Div(decimal.NewFromInt(int64(totalItemCount)))
	
	refundAmount = refundAmount.Add(shippingRefund)
	
	// 3. 执行退款
	return s.Refund(ctx, &RefundRequest{
		OrderID:      orderID,
		RefundAmount: refundAmount,
		RefundItems:  items,
		Reason:       "部分退货",
	})
}

延伸思考

  1. 退款失败如何通知用户?
  2. 如何设计退款的限额控制(防止洗钱)?

🔧 题目9:支付的容灾和降级

问题描述: 支付是核心链路,不能中断。如何设计支付系统的容灾和降级方案?

答案

推荐方案(Go实现):

// PaymentFallbackService 支付降级服务
type PaymentFallbackService struct {
	primarySvc   *PaymentService
	fallbackMode bool
}

// Pay 支付(带降级)
func (s *PaymentFallbackService) Pay(ctx context.Context, 
	req *PaymentRequest) (*PaymentResponse, error) {
	
	// 1. 尝试正常支付
	resp, err := s.primarySvc.CreatePayment(ctx, req)
	if err == nil {
		return resp, nil
	}
	
	log.Warnf("支付失败: %v,尝试降级", err)
	
	// 2. 降级方案
	if s.shouldFallback(err) {
		return s.fallbackPay(ctx, req)
	}
	
	return nil, err
}

// 降级支付
func (s *PaymentFallbackService) fallbackPay(ctx context.Context, 
	req *PaymentRequest) (*PaymentResponse, error) {
	
	// 降级策略1:切换支付渠道
	if req.Channel == ChannelAlipay {
		req.Channel = ChannelWechat
		return s.primarySvc.CreatePayment(ctx, req)
	}
	
	// 降级策略2:使用货到付款
	if s.isCODAvailable(req) {
		return s.createCODOrder(ctx, req)
	}
	
	// 降级策略3:延迟支付(订单保留,稍后支付)
	return s.createDelayedPayment(ctx, req)
}

// 熔断器
type CircuitBreaker struct {
	failureThreshold int
	timeout          time.Duration
	state            CircuitState
	failureCount     int
	lastFailTime     time.Time
}

type CircuitState int

const (
	StateClosed CircuitState = 0  // 闭合(正常)
	StateOpen   CircuitState = 1  // 开启(熔断)
	StateHalfOpen CircuitState = 2  // 半开(尝试恢复)
)

func (cb *CircuitBreaker) Execute(ctx context.Context, 
	fn func() error) error {
	
	// 检查熔断器状态
	if cb.state == StateOpen {
		// 检查是否可以尝试恢复
		if time.Since(cb.lastFailTime) > cb.timeout {
			cb.state = StateHalfOpen
		} else {
			return errors.New("熔断器开启,拒绝请求")
		}
	}
	
	// 执行函数
	err := fn()
	
	if err != nil {
		cb.onFailure()
	} else {
		cb.onSuccess()
	}
	
	return err
}

func (cb *CircuitBreaker) onFailure() {
	cb.failureCount++
	cb.lastFailTime = time.Now()
	
	if cb.failureCount >= cb.failureThreshold {
		cb.state = StateOpen
		log.Warn("熔断器开启")
	}
}

func (cb *CircuitBreaker) onSuccess() {
	if cb.state == StateHalfOpen {
		// 半开状态成功,恢复到闭合
		cb.state = StateClosed
		cb.failureCount = 0
		log.Info("熔断器关闭,恢复正常")
	}
}

延伸思考

  1. 如何设计支付系统的多机房容灾?
  2. 支付降级后如何通知用户?

📊 题目10:预授权支付的设计(酒店、租车场景)

问题描述: 酒店预订需要预授权(冻结资金但不扣款),退房时根据实际消费扣款。如何设计预授权支付?

答案

推荐方案(Go实现):

// PreAuthService 预授权服务
type PreAuthService struct {
	paymentAdapter PaymentAdapter
	preAuthRepo    PreAuthRepository
}

// PreAuthorize 预授权
func (s *PreAuthService) PreAuthorize(ctx context.Context, 
	req *PreAuthRequest) (*PreAuthResponse, error) {
	
	// 1. 创建预授权记录
	preAuth := &PreAuthorization{
		PreAuthID:  generatePreAuthID(),
		OrderID:    req.OrderID,
		UserID:     req.UserID,
		Amount:     req.Amount,  // 冻结金额
		Status:     PreAuthStatusFrozen,
		CreatedAt:  time.Now(),
		ExpireAt:   time.Now().Add(30 * 24 * time.Hour), // 30天有效期
	}
	
	if err := s.preAuthRepo.Create(ctx, preAuth); err != nil {
		return nil, err
	}
	
	// 2. 调用支付渠道预授权接口
	resp, err := s.paymentAdapter.PreAuthorize(ctx, &ThirdPartyPreAuthRequest{
		OutRequestNo: preAuth.PreAuthID,
		Amount:       req.Amount,
		ExpireTime:   preAuth.ExpireAt,
	})
	
	if err != nil {
		preAuth.Status = PreAuthStatusFailed
		s.preAuthRepo.Update(ctx, preAuth)
		return nil, err
	}
	
	// 3. 保存第三方预授权号
	preAuth.ThirdPartyID = resp.AuthNo
	s.preAuthRepo.Update(ctx, preAuth)
	
	return &PreAuthResponse{
		PreAuthID: preAuth.PreAuthID,
		AuthNo:    resp.AuthNo,
	}, nil
}

// Complete 完成预授权(实际扣款)
func (s *PreAuthService) Complete(ctx context.Context, 
	preAuthID string, actualAmount decimal.Decimal) error {
	
	// 1. 查询预授权
	preAuth, err := s.preAuthRepo.FindByID(ctx, preAuthID)
	if err != nil {
		return err
	}
	
	// 2. 校验金额
	if actualAmount.GreaterThan(preAuth.Amount) {
		return errors.New("实际金额超过预授权金额")
	}
	
	// 3. 调用支付渠道完成预授权
	err = s.paymentAdapter.CompletePreAuth(ctx, &CompletePreAuthRequest{
		AuthNo: preAuth.ThirdPartyID,
		Amount: actualAmount,
	})
	
	if err != nil {
		return err
	}
	
	// 4. 更新状态
	preAuth.Status = PreAuthStatusCompleted
	preAuth.ActualAmount = actualAmount
	preAuth.CompletedAt = timePtr(time.Now())
	s.preAuthRepo.Update(ctx, preAuth)
	
	// 5. 多余金额解冻
	if actualAmount.LessThan(preAuth.Amount) {
		unfreezeAmount := preAuth.Amount.Sub(actualAmount)
		log.Infof("解冻多余金额: %v", unfreezeAmount)
	}
	
	return nil
}

// Cancel 取消预授权
func (s *PreAuthService) Cancel(ctx context.Context, preAuthID string) error {
	preAuth, err := s.preAuthRepo.FindByID(ctx, preAuthID)
	if err != nil {
		return err
	}
	
	// 调用支付渠道取消预授权
	err = s.paymentAdapter.CancelPreAuth(ctx, preAuth.ThirdPartyID)
	if err != nil {
		return err
	}
	
	preAuth.Status = PreAuthStatusCancelled
	s.preAuthRepo.Update(ctx, preAuth)
	
	return nil
}

延伸思考

  1. 预授权过期如何自动解冻?
  2. 预授权场景下的对账如何设计?

第四部分:综合实战案例(10题)

本部分将前面所学知识点整合,设计完整的端到端场景,涵盖系统设计、技术选型、性能优化、故障处理等多个维度。


🚀 案例1:设计一个百万级QPS的商品详情页系统

问题描述: 电商平台的商品详情页是流量最大的页面,大促期间QPS可达100万。请设计一套完整的商品详情页系统,保证高性能和高可用。

答案

系统架构设计(Go实现):

package product

import (
	"context"
	"encoding/json"
	"fmt"
	"time"
	
	"github.com/go-redis/redis/v8"
)

// ProductDetailService 商品详情服务
type ProductDetailService struct {
	productRepo   ProductRepository
	l1Cache       LocalCache       // L1缓存:本地内存
	l2Cache       *redis.Client    // L2缓存:Redis
	cdn           CDNService       // CDN
	mq            MessageQueue     // 消息队列
}

// GetProductDetail 获取商品详情(多级缓存)
func (s *ProductDetailService) GetProductDetail(ctx context.Context, 
	productID int64) (*ProductDetail, error) {
	
	cacheKey := fmt.Sprintf("product:detail:%d", productID)
	
	// L1缓存:本地内存(命中率60%)
	if detail := s.l1Cache.Get(cacheKey); detail != nil {
		return detail.(*ProductDetail), nil
	}
	
	// L2缓存:Redis(命中率95%)
	detailJSON, err := s.l2Cache.Get(ctx, cacheKey).Result()
	if err == nil {
		detail := &ProductDetail{}
		json.Unmarshal([]byte(detailJSON), detail)
		
		// 回填L1
		s.l1Cache.Set(cacheKey, detail, 5*time.Minute)
		return detail, nil
	}
	
	// L3:数据库(命中率5%)
	detail, err := s.productRepo.FindByID(ctx, productID)
	if err != nil {
		return nil, err
	}
	
	// 异步回填缓存
	go func() {
		detailJSON, _ := json.Marshal(detail)
		s.l2Cache.SetEX(context.Background(), cacheKey, detailJSON, time.Hour)
		s.l1Cache.Set(cacheKey, detail, 5*time.Minute)
	}()
	
	return detail, nil
}

// 缓存预热
func (s *ProductDetailService) WarmUpCache(ctx context.Context, productIDs []int64) error {
	for _, pid := range productIDs {
		detail, err := s.productRepo.FindByID(ctx, pid)
		if err != nil {
			continue
		}
		
		// 预热到Redis
		cacheKey := fmt.Sprintf("product:detail:%d", pid)
		detailJSON, _ := json.Marshal(detail)
		s.l2Cache.SetEX(ctx, cacheKey, detailJSON, time.Hour)
	}
	
	return nil
}

// 缓存失效策略(商品更新时)
func (s *ProductDetailService) UpdateProduct(ctx context.Context, 
	product *Product) error {
	
	// 1. 更新数据库
	if err := s.productRepo.Update(ctx, product); err != nil {
		return err
	}
	
	// 2. 发布缓存失效消息
	msg := CacheInvalidateMessage{
		ProductID: product.ProductID,
		Timestamp: time.Now(),
	}
	
	s.mq.Publish("product.cache.invalidate", msg)
	
	return nil
}

// 监听缓存失效消息
func (s *ProductDetailService) StartCacheInvalidateListener() {
	s.mq.Subscribe("product.cache.invalidate", func(msg *CacheInvalidateMessage) {
		cacheKey := fmt.Sprintf("product:detail:%d", msg.ProductID)
		
		// 删除L1和L2缓存
		s.l1Cache.Delete(cacheKey)
		s.l2Cache.Del(context.Background(), cacheKey)
		
		log.Infof("商品%d缓存已失效", msg.ProductID)
	})
}

CDN静态化

// 静态化商品详情页(HTML)
func (s *ProductDetailService) GenerateStaticHTML(ctx context.Context, 
	productID int64) (string, error) {
	
	detail, err := s.GetProductDetail(ctx, productID)
	if err != nil {
		return "", err
	}
	
	// 渲染HTML模板
	html := s.renderTemplate(detail)
	
	// 上传到CDN
	cdnURL := fmt.Sprintf("https://cdn.example.com/product/%d.html", productID)
	if err := s.cdn.Upload(cdnURL, html); err != nil {
		return "", err
	}
	
	return cdnURL, nil
}

性能优化点

  1. 多级缓存:本地内存(1ms)→ Redis(5ms)→ MySQL(50ms)
  2. CDN静态化:核心商品预生成HTML,加载时间<100ms
  3. 缓存预热:大促前1小时预热热门商品
  4. 异步刷新:Cache Aside模式,回源不阻塞请求
  5. 降级策略:Redis故障时降级到MySQL+限流

容量规划

QPS:100万
平均响应时间:50ms
并发连接数:100万 * 0.05 = 5万

服务器配置:
- 应用服务器:100台(每台1万QPS)
- Redis集群:50个主节点(每节点2万QPS)
- MySQL:主从+分库分表(32个分片)

延伸思考

  1. 商品详情页的AB测试如何设计?
  2. 图片加载优化(WebP、懒加载)如何实现?
  3. 缓存雪崩如何防范?

💡 案例2:秒杀系统的完整设计

问题描述: 设计一个支持10万QPS的秒杀系统,商品库存100件,要求:防止超卖、保证公平性、抵抗恶意刷单。

答案

架构设计(Go实现):

package seckill

import (
	"context"
	"errors"
	"fmt"
	"time"
	
	"github.com/go-redis/redis/v8"
)

// SeckillService 秒杀服务
type SeckillService struct {
	rdb         *redis.Client
	orderSvc    OrderService
	inventorySvc InventoryService
	mq          MessageQueue
}

// CreateSeckill 创建秒杀活动
func (s *SeckillService) CreateSeckill(ctx context.Context, 
	seckill *Seckill) error {
	
	// 1. 创建秒杀活动
	if err := s.seckillRepo.Create(ctx, seckill); err != nil {
		return err
	}
	
	// 2. 库存预热到Redis
	stockKey := fmt.Sprintf("seckill:stock:%d", seckill.SeckillID)
	s.rdb.Set(ctx, stockKey, seckill.Stock, 0)
	
	// 3. 创建商品详情页缓存
	s.preWarmCache(ctx, seckill.ProductID)
	
	return nil
}

// Seckill 秒杀下单
func (s *SeckillService) Seckill(ctx context.Context, 
	userID int64, seckillID int64) error {
	
	// 1. 限流(单用户限流+全局限流)
	if err := s.checkRateLimit(ctx, userID, seckillID); err != nil {
		return err
	}
	
	// 2. 风控检查
	if err := s.riskCheck(ctx, userID); err != nil {
		return err
	}
	
	// 3. 扣减Redis库存(Lua脚本保证原子性)
	stock, err := s.deductStock(ctx, seckillID)
	if err != nil {
		return err
	}
	
	if stock < 0 {
		return errors.New("商品已抢光")
	}
	
	// 4. 发送消息到MQ异步创建订单
	msg := SeckillOrderMessage{
		UserID:    userID,
		SeckillID: seckillID,
		Timestamp: time.Now(),
	}
	
	if err := s.mq.Publish("seckill.order.create", msg); err != nil {
		// MQ发送失败,回补库存
		s.increaseStock(ctx, seckillID)
		return err
	}
	
	return nil
}

// 扣减库存(Lua脚本)
func (s *SeckillService) deductStock(ctx context.Context, 
	seckillID int64) (int64, error) {
	
	stockKey := fmt.Sprintf("seckill:stock:%d", seckillID)
	
	// Lua脚本保证原子性
	script := `
		local stock = redis.call('GET', KEYS[1])
		if tonumber(stock) <= 0 then
			return -1
		end
		redis.call('DECR', KEYS[1])
		return stock - 1
	`
	
	result, err := s.rdb.Eval(ctx, script, []string{stockKey}).Result()
	if err != nil {
		return 0, err
	}
	
	return result.(int64), nil
}

// 限流(令牌桶)
func (s *SeckillService) checkRateLimit(ctx context.Context, 
	userID int64, seckillID int64) error {
	
	// 单用户限流:1秒内最多1次
	userKey := fmt.Sprintf("seckill:ratelimit:user:%d:%d", userID, seckillID)
	exists, _ := s.rdb.Exists(ctx, userKey).Result()
	if exists > 0 {
		return errors.New("操作太频繁")
	}
	
	s.rdb.SetEX(ctx, userKey, 1, time.Second)
	
	// 全局限流:令牌桶(10万QPS)
	globalKey := fmt.Sprintf("seckill:ratelimit:global:%d", seckillID)
	token, _ := s.rdb.Incr(ctx, globalKey).Result()
	if token == 1 {
		s.rdb.Expire(ctx, globalKey, time.Second)
	}
	
	if token > 100000 {
		return errors.New("系统繁忙,请稍后再试")
	}
	
	return nil
}

// 异步创建订单(消费MQ)
func (s *SeckillService) CreateOrderAsync() {
	s.mq.Subscribe("seckill.order.create", func(msg *SeckillOrderMessage) {
		ctx := context.Background()
		
		// 1. 防重(幂等性)
		orderKey := fmt.Sprintf("seckill:order:%d:%d", msg.UserID, msg.SeckillID)
		exists, _ := s.rdb.Exists(ctx, orderKey).Result()
		if exists > 0 {
			log.Warnf("用户%d已抢购过", msg.UserID)
			return
		}
		
		// 2. 创建订单
		order, err := s.orderSvc.CreateSeckillOrder(ctx, msg.UserID, msg.SeckillID)
		if err != nil {
			log.Errorf("创建订单失败: %v", err)
			// 回补库存
			s.increaseStock(ctx, msg.SeckillID)
			return
		}
		
		// 3. 标记已抢购
		s.rdb.SetEX(ctx, orderKey, order.OrderID, 24*time.Hour)
		
		// 4. 通知用户
		s.notifySvc.Send(ctx, msg.UserID, "秒杀成功,请尽快支付")
	})
}

// 定时任务:取消未支付订单
func (s *SeckillService) CancelUnpaidOrders() {
	ticker := time.NewTicker(1 * time.Minute)
	defer ticker.Stop()
	
	for range ticker.C {
		// 查询15分钟前创建的未支付秒杀订单
		ctx := context.Background()
		cutoffTime := time.Now().Add(-15 * time.Minute)
		
		orders, _ := s.orderRepo.FindUnpaidSeckillOrders(ctx, cutoffTime)
		
		for _, order := range orders {
			// 取消订单
			s.orderSvc.Cancel(ctx, order.OrderID)
			
			// 回补库存
			s.increaseStock(ctx, order.SeckillID)
		}
	}
}

架构要点

  1. 前端限流:按钮置灰、验证码、排队页
  2. 网关限流:Nginx限流(10万QPS)
  3. Redis预扣库存:Lua脚本原子操作
  4. 异步下单:MQ削峰,提升吞吐
  5. 超时取消:15分钟未支付自动取消+回补库存

延伸思考

  1. 秒杀如何防止黄牛?
  2. 分布式锁如何选型(Redis vs Etcd)?

🔧 案例3:订单履约的全链路监控

问题描述: 订单从创建到签收,涉及多个服务(订单、库存、物流、支付)。如何设计全链路监控,快速定位问题?

答案

推荐方案:分布式追踪(OpenTelemetry + Jaeger)

package tracing

import (
	"context"
	
	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/trace"
)

// OrderFulfillmentService 订单履约服务
type OrderFulfillmentService struct {
	tracer trace.Tracer
}

// FulfillOrder 履约订单(带追踪)
func (s *OrderFulfillmentService) FulfillOrder(ctx context.Context, 
	orderID int64) error {
	
	// 创建根Span
	ctx, span := s.tracer.Start(ctx, "FulfillOrder",
		trace.WithAttributes(
			attribute.Int64("order.id", orderID),
		),
	)
	defer span.End()
	
	// 步骤1:扣减库存
	if err := s.deductInventory(ctx, orderID); err != nil {
		span.RecordError(err)
		return err
	}
	
	// 步骤2:创建拣货单
	if err := s.createPickingOrder(ctx, orderID); err != nil {
		span.RecordError(err)
		return err
	}
	
	// 步骤3:创建物流运单
	if err := s.createShipment(ctx, orderID); err != nil {
		span.RecordError(err)
		return err
	}
	
	span.SetAttributes(attribute.String("status", "success"))
	return nil
}

// deductInventory 扣减库存(子Span)
func (s *OrderFulfillmentService) deductInventory(ctx context.Context, 
	orderID int64) error {
	
	ctx, span := s.tracer.Start(ctx, "DeductInventory")
	defer span.End()
	
	// 调用库存服务
	start := time.Now()
	err := s.inventorySvc.Deduct(ctx, orderID)
	duration := time.Since(start)
	
	span.SetAttributes(
		attribute.String("service", "inventory"),
		attribute.Int64("duration_ms", duration.Milliseconds()),
	)
	
	if err != nil {
		span.RecordError(err)
		return err
	}
	
	return nil
}

监控指标

// RED指标(请求速率、错误率、耗时)
type Metrics struct {
	// Rate: 请求速率
	OrderCreateRate   *prometheus.CounterVec
	
	// Error: 错误率
	OrderCreateErrors *prometheus.CounterVec
	
	// Duration: 耗时分布
	OrderCreateDuration *prometheus.HistogramVec
}

// 记录指标
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*Order, error) {
	start := time.Now()
	
	// 请求计数
	s.metrics.OrderCreateRate.WithLabelValues("order_service").Inc()
	
	// 执行业务逻辑
	order, err := s.doCreateOrder(ctx, req)
	
	// 记录耗时
	duration := time.Since(start).Seconds()
	s.metrics.OrderCreateDuration.WithLabelValues("order_service").Observe(duration)
	
	// 记录错误
	if err != nil {
		s.metrics.OrderCreateErrors.WithLabelValues("order_service", "error").Inc()
	} else {
		s.metrics.OrderCreateErrors.WithLabelValues("order_service", "success").Inc()
	}
	
	return order, err
}

延伸思考

  1. 如何设计告警规则(P95延迟>500ms告警)?
  2. 如何追踪跨语言调用链(Go → Java → Python)?

📊 案例4:大促准备的全链路压测

问题描述: 618大促前,需要对整个系统进行压测,验证系统能否支撑预期流量。如何设计全链路压测方案?

答案

压测方案(Go实现):

package loadtest

import (
	"context"
	"sync"
	"time"
)

// LoadTester 压测工具
type LoadTester struct {
	targetQPS int
	duration  time.Duration
	workers   int
}

// Run 执行压测
func (lt *LoadTester) Run(ctx context.Context, 
	testFunc func(context.Context) error) *LoadTestResult {
	
	result := &LoadTestResult{
		StartTime: time.Now(),
	}
	
	// 计算每个worker的QPS
	qpsPerWorker := lt.targetQPS / lt.workers
	interval := time.Second / time.Duration(qpsPerWorker)
	
	var wg sync.WaitGroup
	resultChan := make(chan *RequestResult, lt.targetQPS*int(lt.duration.Seconds()))
	
	// 启动workers
	for i := 0; i < lt.workers; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			
			ticker := time.NewTicker(interval)
			defer ticker.Stop()
			
			timeout := time.After(lt.duration)
			
			for {
				select {
				case <-ticker.C:
					// 执行请求
					reqResult := lt.executeRequest(ctx, testFunc)
					resultChan <- reqResult
					
				case <-timeout:
					return
				}
			}
		}()
	}
	
	// 等待所有workers完成
	go func() {
		wg.Wait()
		close(resultChan)
	}()
	
	// 收集结果
	for reqResult := range resultChan {
		result.TotalRequests++
		
		if reqResult.Success {
			result.SuccessCount++
			result.TotalLatency += reqResult.Latency
		} else {
			result.FailureCount++
		}
		
		// 记录延迟分布
		result.LatencyDistribution = append(result.LatencyDistribution, reqResult.Latency)
	}
	
	result.EndTime = time.Now()
	result.Calculate()
	
	return result
}

// executeRequest 执行单次请求
func (lt *LoadTester) executeRequest(ctx context.Context, 
	testFunc func(context.Context) error) *RequestResult {
	
	result := &RequestResult{
		StartTime: time.Now(),
	}
	
	err := testFunc(ctx)
	
	result.EndTime = time.Now()
	result.Latency = result.EndTime.Sub(result.StartTime)
	result.Success = (err == nil)
	
	return result
}

// LoadTestResult 压测结果
type LoadTestResult struct {
	StartTime           time.Time
	EndTime             time.Time
	TotalRequests       int
	SuccessCount        int
	FailureCount        int
	TotalLatency        time.Duration
	LatencyDistribution []time.Duration
	
	// 计算指标
	QPS         float64
	AvgLatency  time.Duration
	P50Latency  time.Duration
	P95Latency  time.Duration
	P99Latency  time.Duration
	SuccessRate float64
}

// Calculate 计算统计指标
func (r *LoadTestResult) Calculate() {
	duration := r.EndTime.Sub(r.StartTime).Seconds()
	r.QPS = float64(r.TotalRequests) / duration
	
	if r.SuccessCount > 0 {
		r.AvgLatency = r.TotalLatency / time.Duration(r.SuccessCount)
	}
	
	r.SuccessRate = float64(r.SuccessCount) / float64(r.TotalRequests) * 100
	
	// 计算P50/P95/P99
	sort.Slice(r.LatencyDistribution, func(i, j int) bool {
		return r.LatencyDistribution[i] < r.LatencyDistribution[j]
	})
	
	if len(r.LatencyDistribution) > 0 {
		r.P50Latency = r.LatencyDistribution[len(r.LatencyDistribution)*50/100]
		r.P95Latency = r.LatencyDistribution[len(r.LatencyDistribution)*95/100]
		r.P99Latency = r.LatencyDistribution[len(r.LatencyDistribution)*99/100]
	}
}

// 使用示例
func TestOrderCreate() {
	tester := &LoadTester{
		targetQPS: 10000,  // 目标1万QPS
		duration:  5 * time.Minute,
		workers:   100,
	}
	
	result := tester.Run(context.Background(), func(ctx context.Context) error {
		// 模拟下单
		return orderSvc.CreateOrder(ctx, &CreateOrderRequest{
			UserID: randomUserID(),
			Items:  randomItems(),
		})
	})
	
	// 输出结果
	fmt.Printf("QPS: %.2f\n", result.QPS)
	fmt.Printf("成功率: %.2f%%\n", result.SuccessRate)
	fmt.Printf("平均延迟: %v\n", result.AvgLatency)
	fmt.Printf("P95延迟: %v\n", result.P95Latency)
	fmt.Printf("P99延迟: %v\n", result.P99Latency)
}

压测环境隔离

生产环境:不能压测
预发环境:配置与生产一致,数据隔离
压测流量标记:HTTP Header: X-Load-Test: true

延伸思考

  1. 如何设计压测数据构造?
  2. 压测导致的脏数据如何清理?

🔧 案例5:跨境电商的多币种结算

问题描述: 跨境电商平台支持美元、欧元、人民币等多币种。如何设计多币种的定价、支付、结算系统?

答案

推荐方案(Go实现):

package currency

import (
	"context"
	"time"
	
	"github.com/shopspring/decimal"
)

// Currency 币种
type Currency string

const (
	CNY Currency = "CNY"  // 人民币
	USD Currency = "USD"  // 美元
	EUR Currency = "EUR"  // 欧元
)

// ExchangeRateService 汇率服务
type ExchangeRateService struct {
	rdb   *redis.Client
	repo  ExchangeRateRepository
}

// GetExchangeRate 获取汇率
func (s *ExchangeRateService) GetExchangeRate(ctx context.Context, 
	from, to Currency) (decimal.Decimal, error) {
	
	if from == to {
		return decimal.NewFromInt(1), nil
	}
	
	// 从缓存读取
	cacheKey := fmt.Sprintf("exchange_rate:%s:%s", from, to)
	rateStr, err := s.rdb.Get(ctx, cacheKey).Result()
	if err == nil {
		rate, _ := decimal.NewFromString(rateStr)
		return rate, nil
	}
	
	// 从数据库读取
	rate, err := s.repo.FindLatest(ctx, from, to)
	if err != nil {
		return decimal.Zero, err
	}
	
	// 缓存1小时
	s.rdb.SetEX(ctx, cacheKey, rate.Rate.String(), time.Hour)
	
	return rate.Rate, nil
}

// Convert 货币转换
func (s *ExchangeRateService) Convert(ctx context.Context, 
	amount decimal.Decimal, from, to Currency) (decimal.Decimal, error) {
	
	rate, err := s.GetExchangeRate(ctx, from, to)
	if err != nil {
		return decimal.Zero, err
	}
	
	return amount.Mul(rate), nil
}

// ProductPricing 商品多币种定价
type ProductPricing struct {
	ProductID int64
	Prices    map[Currency]decimal.Decimal
}

// GetPrice 获取指定币种的价格
func (p *ProductPricing) GetPrice(currency Currency) (decimal.Decimal, error) {
	if price, exists := p.Prices[currency]; exists {
		return price, nil
	}
	
	return decimal.Zero, errors.New("该币种暂不支持")
}

// OrderService 订单服务(多币种)
type OrderService struct {
	exchangeRateSvc *ExchangeRateService
}

// CreateOrder 创建订单(多币种)
func (s *OrderService) CreateOrder(ctx context.Context, 
	req *CreateOrderRequest) (*Order, error) {
	
	// 1. 计算订单金额(用户选择的币种)
	var totalAmount decimal.Decimal
	for _, item := range req.Items {
		price := item.Product.GetPrice(req.Currency)
		totalAmount = totalAmount.Add(price.Mul(decimal.NewFromInt(int64(item.Quantity))))
	}
	
	// 2. 转换为平台基准币种(CNY)
	baseCurrencyAmount, err := s.exchangeRateSvc.Convert(ctx, 
		totalAmount, req.Currency, CNY)
	if err != nil {
		return nil, err
	}
	
	// 3. 创建订单
	order := &Order{
		OrderID:            generateOrderID(),
		UserID:             req.UserID,
		Currency:           req.Currency,        // 显示币种
		TotalAmount:        totalAmount,         // 显示金额
		BaseCurrency:       CNY,                 // 基准币种
		BaseCurrencyAmount: baseCurrencyAmount,  // 基准金额
		ExchangeRate:       s.getExchangeRate(ctx, req.Currency, CNY),
		CreatedAt:          time.Now(),
	}
	
	return order, s.orderRepo.Create(ctx, order)
}

// 汇率快照(订单创建时记录汇率)
func (s *OrderService) getExchangeRate(ctx context.Context, 
	from, to Currency) decimal.Decimal {
	
	rate, _ := s.exchangeRateSvc.GetExchangeRate(ctx, from, to)
	return rate
}

结算处理

// SettlementService 结算服务(多币种)
type SettlementService struct {
	exchangeRateSvc *ExchangeRateService
}

// Settle 结算
func (s *SettlementService) Settle(ctx context.Context, 
	merchantID int64, date time.Time) error {
	
	// 1. 查询待结算订单
	orders, _ := s.orderRepo.FindPendingSettlement(ctx, merchantID, date)
	
	// 2. 按币种分组汇总
	settlementByCurrency := make(map[Currency]decimal.Decimal)
	for _, order := range orders {
		current := settlementByCurrency[order.Currency]
		settlementByCurrency[order.Currency] = current.Add(order.MerchantAmount)
	}
	
	// 3. 转换为商家收款币种并结算
	merchantCurrency := s.getMerchantCurrency(ctx, merchantID)
	
	for currency, amount := range settlementByCurrency {
		// 转换币种
		settleAmount, _ := s.exchangeRateSvc.Convert(ctx, 
			amount, currency, merchantCurrency)
		
		// 调用支付渠道转账
		s.paymentSvc.Transfer(ctx, merchantID, settleAmount, merchantCurrency)
	}
	
	return nil
}

延伸思考

  1. 汇率波动如何处理(订单创建时汇率 vs 支付时汇率)?
  2. 跨境支付的关税如何计算?

💡 案例6:电商搜索的智能排序

问题描述: 用户搜索“手机“,返回1000个结果。如何设计排序算法,让用户最可能购买的商品排在前面?

答案

推荐方案:多因子排序模型

package search

import (
	"context"
	"math"
	
	"github.com/shopspring/decimal"
)

// SearchRankingService 搜索排序服务
type SearchRankingService struct {
	userProfileSvc UserProfileService
}

// RankProducts 对商品排序
func (s *SearchRankingService) RankProducts(ctx context.Context, 
	userID int64, products []*Product) []*ScoredProduct {
	
	scoredProducts := make([]*ScoredProduct, 0, len(products))
	
	for _, product := range products {
		score := s.calculateScore(ctx, userID, product)
		scoredProducts = append(scoredProducts, &ScoredProduct{
			Product: product,
			Score:   score,
		})
	}
	
	// 按分数降序排序
	sort.Slice(scoredProducts, func(i, j int) bool {
		return scoredProducts[i].Score > scoredProducts[j].Score
	})
	
	return scoredProducts
}

// calculateScore 计算商品综合分数
func (s *SearchRankingService) calculateScore(ctx context.Context, 
	userID int64, product *Product) float64 {
	
	// 多因子加权求和
	score := 0.0
	
	// 1. 文本相关性(权重20%)
	textRelevance := s.calculateTextRelevance(product)
	score += textRelevance * 0.2
	
	// 2. 销量(权重15%)
	salesScore := s.normalizeSales(product.SalesCount)
	score += salesScore * 0.15
	
	// 3. 好评率(权重10%)
	ratingScore := product.Rating / 5.0
	score += ratingScore * 0.1
	
	// 4. 价格(权重10%)
	priceScore := s.calculatePriceScore(product.Price)
	score += priceScore * 0.1
	
	// 5. 个性化(权重30%)
	personalScore := s.calculatePersonalScore(ctx, userID, product)
	score += personalScore * 0.3
	
	// 6. 时效性(权重5%)
	timeScore := s.calculateTimeScore(product.CreatedAt)
	score += timeScore * 0.05
	
	// 7. 商家质量(权重10%)
	merchantScore := s.calculateMerchantScore(product.MerchantID)
	score += merchantScore * 0.1
	
	return score
}

// 个性化分数(基于用户画像)
func (s *SearchRankingService) calculatePersonalScore(ctx context.Context, 
	userID int64, product *Product) float64 {
	
	profile := s.userProfileSvc.GetProfile(ctx, userID)
	
	score := 0.0
	
	// 1. 品牌偏好
	if contains(profile.FavoriteBrands, product.Brand) {
		score += 0.3
	}
	
	// 2. 类目偏好
	if contains(profile.FavoriteCategories, product.CategoryID) {
		score += 0.3
	}
	
	// 3. 价格区间偏好
	if product.Price.GreaterThanOrEqual(profile.MinPrice) &&
		product.Price.LessThanOrEqual(profile.MaxPrice) {
		score += 0.2
	}
	
	// 4. 历史浏览相似度
	similarity := s.calculateSimilarity(product, profile.ViewedProducts)
	score += similarity * 0.2
	
	return score
}

// 销量归一化(对数变换)
func (s *SearchRankingService) normalizeSales(salesCount int64) float64 {
	if salesCount == 0 {
		return 0
	}
	
	// 对数变换平滑销量差异
	return math.Log10(float64(salesCount)+1) / math.Log10(1000000)
}

机器学习排序

// LearningToRank 学习排序模型
type LearningToRank struct {
	model MLModel
}

// Rank 使用模型排序
func (ltr *LearningToRank) Rank(ctx context.Context, 
	userID int64, products []*Product) []*ScoredProduct {
	
	scoredProducts := make([]*ScoredProduct, 0)
	
	for _, product := range products {
		// 提取特征
		features := ltr.extractFeatures(ctx, userID, product)
		
		// 模型预测分数
		score := ltr.model.Predict(features)
		
		scoredProducts = append(scoredProducts, &ScoredProduct{
			Product: product,
			Score:   score,
		})
	}
	
	// 排序
	sort.Slice(scoredProducts, func(i, j int) bool {
		return scoredProducts[i].Score > scoredProducts[j].Score
	})
	
	return scoredProducts
}

// 特征提取
func (ltr *LearningToRank) extractFeatures(ctx context.Context, 
	userID int64, product *Product) []float64 {
	
	return []float64{
		float64(product.SalesCount),
		product.Rating,
		product.Price.InexactFloat64(),
		float64(product.ReviewCount),
		// ... 更多特征
	}
}

延伸思考

  1. 如何设计AB测试验证排序效果?
  2. 如何平衡新品曝光和热销商品?

🚀 案例7:异常流量的应急处理

问题描述: 凌晨2点,监控告警:订单服务QPS突增10倍,响应时间飙升至5秒,疑似遭受攻击。如何快速定位和处理?

答案

应急响应流程(Go实现):

package emergency

import (
	"context"
	"time"
)

// EmergencyHandler 应急处理器
type EmergencyHandler struct {
	rateLimiter *RateLimiter
	ipBlacklist *IPBlacklist
	alertSvc    AlertService
}

// HandleAbnormalTraffic 处理异常流量
func (h *EmergencyHandler) HandleAbnormalTraffic(ctx context.Context) error {
	// 第1步:分析流量特征
	analysis := h.analyzeTraffic(ctx)
	
	// 第2步:判断攻击类型
	attackType := h.identifyAttackType(analysis)
	
	// 第3步:执行防御措施
	switch attackType {
	case AttackTypeDDoS:
		return h.handleDDoS(ctx, analysis)
	case AttackTypeCrawler:
		return h.handleCrawler(ctx, analysis)
	case AttackTypeBrushOrder:
		return h.handleBrushOrder(ctx, analysis)
	default:
		return h.handleUnknown(ctx, analysis)
	}
}

// analyzeTraffic 分析流量
func (h *EmergencyHandler) analyzeTraffic(ctx context.Context) *TrafficAnalysis {
	now := time.Now()
	last5Min := now.Add(-5 * time.Minute)
	
	// 查询最近5分钟的请求日志
	logs := h.logSvc.Query(ctx, last5Min, now)
	
	analysis := &TrafficAnalysis{
		TotalRequests: len(logs),
		IPDistribution: make(map[string]int),
		UADistribution: make(map[string]int),
		URLDistribution: make(map[string]int),
	}
	
	for _, log := range logs {
		// IP分布
		analysis.IPDistribution[log.IP]++
		
		// User-Agent分布
		analysis.UADistribution[log.UserAgent]++
		
		// URL分布
		analysis.URLDistribution[log.URL]++
	}
	
	// 识别异常IP(单IP请求占比>10%)
	for ip, count := range analysis.IPDistribution {
		ratio := float64(count) / float64(analysis.TotalRequests)
		if ratio > 0.1 {
			analysis.AbnormalIPs = append(analysis.AbnormalIPs, ip)
		}
	}
	
	return analysis
}

// handleDDoS 处理DDoS攻击
func (h *EmergencyHandler) handleDDoS(ctx context.Context, 
	analysis *TrafficAnalysis) error {
	
	// 1. 立即限流(全局QPS降低到正常值的50%)
	h.rateLimiter.SetGlobalLimit(10000)
	
	// 2. 封禁异常IP
	for _, ip := range analysis.AbnormalIPs {
		h.ipBlacklist.Add(ip, 1*time.Hour)
		log.Warnf("封禁IP: %s", ip)
	}
	
	// 3. 启用验证码
	h.enableCaptcha()
	
	// 4. 通知运维
	h.alertSvc.Send("紧急:疑似DDoS攻击,已自动防御")
	
	return nil
}

// handleBrushOrder 处理刷单攻击
func (h *EmergencyHandler) handleBrushOrder(ctx context.Context, 
	analysis *TrafficAnalysis) error {
	
	// 1. 识别刷单用户
	suspiciousUsers := h.identifySuspiciousUsers(ctx)
	
	// 2. 限制下单频率
	for _, userID := range suspiciousUsers {
		h.rateLimiter.SetUserLimit(userID, 1) // 1分钟1单
		log.Warnf("限制用户%d下单频率", userID)
	}
	
	// 3. 启用风控策略(大额订单人工审核)
	h.enableManualReview()
	
	return nil
}

// 降级策略
func (h *EmergencyHandler) Degrade(ctx context.Context) error {
	// Level 1:关闭非核心功能
	h.disableRecommendation()  // 关闭推荐
	h.disableSearch()          // 关闭搜索
	
	// Level 2:只读模式(禁止下单)
	h.enableReadOnlyMode()
	
	// Level 3:返回静态页面
	h.enableStaticMode()
	
	return nil
}

监控告警

// AlertRule 告警规则
type AlertRule struct {
	Name      string
	Metric    string
	Threshold float64
	Duration  time.Duration
	Severity  string  // P0/P1/P2/P3
}

// 告警规则示例
var alertRules = []AlertRule{
	{
		Name:      "订单QPS异常",
		Metric:    "order_create_qps",
		Threshold: 10000,  // QPS超过1万
		Duration:  1 * time.Minute,
		Severity:  "P0",
	},
	{
		Name:      "订单延迟异常",
		Metric:    "order_create_p99_latency",
		Threshold: 1000,  // P99超过1秒
		Duration:  5 * time.Minute,
		Severity:  "P1",
	},
}

延伸思考

  1. 如何设计自动化的应急响应系统?
  2. 如何平衡防御和用户体验(误封正常用户)?

🔧 案例8:订单的柔性事务设计

问题描述: 订单创建涉及多个服务(扣库存、扣优惠券、扣积分)。如何设计柔性事务,保证最终一致性?

答案

推荐方案:本地消息表 + 定时补偿

package transaction

import (
	"context"
	"time"
)

// LocalMessageTable 本地消息表
type LocalMessage struct {
	MessageID   string
	BizType     string          // 业务类型
	BizID       string          // 业务ID
	Content     string          // 消息内容
	Status      MessageStatus   // 待发送/已发送/发送失败
	RetryCount  int
	NextRetryAt time.Time
	CreatedAt   time.Time
}

// OrderService 订单服务(柔性事务)
type OrderService struct {
	orderRepo   OrderRepository
	messageSvc  LocalMessageService
	eventBus    EventBus
}

// CreateOrder 创建订单(本地消息表)
func (s *OrderService) CreateOrder(ctx context.Context, 
	req *CreateOrderRequest) (*Order, error) {
	
	// 开启数据库事务
	tx, err := s.db.BeginTx(ctx, nil)
	if err != nil {
		return nil, err
	}
	defer tx.Rollback()
	
	// 1. 创建订单
	order := &Order{
		OrderID:   generateOrderID(),
		UserID:    req.UserID,
		Items:     req.Items,
		Status:    OrderStatusPending,
		CreatedAt: time.Now(),
	}
	
	if err := s.orderRepo.CreateWithTx(ctx, tx, order); err != nil {
		return nil, err
	}
	
	// 2. 写入本地消息表(同一个事务)
	messages := []LocalMessage{
		{
			MessageID: generateMessageID(),
			BizType:   "ORDER_CREATED",
			BizID:     fmt.Sprintf("%d", order.OrderID),
			Content:   s.serializeOrderEvent(order),
			Status:    MessageStatusPending,
			CreatedAt: time.Now(),
		},
	}
	
	for _, msg := range messages {
		if err := s.messageSvc.CreateWithTx(ctx, tx, &msg); err != nil {
			return nil, err
		}
	}
	
	// 3. 提交事务
	if err := tx.Commit(); err != nil {
		return nil, err
	}
	
	// 4. 异步发送消息(事务外)
	go s.publishPendingMessages(context.Background())
	
	return order, nil
}

// publishPendingMessages 发布待发送消息
func (s *OrderService) publishPendingMessages(ctx context.Context) {
	// 查询待发送消息
	messages, err := s.messageSvc.FindPending(ctx, 100)
	if err != nil {
		return
	}
	
	for _, msg := range messages {
		// 发送到消息队列
		err := s.eventBus.Publish(msg.BizType, msg.Content)
		
		if err == nil {
			// 发送成功,更新状态
			msg.Status = MessageStatusSent
			s.messageSvc.Update(ctx, &msg)
		} else {
			// 发送失败,记录重试
			msg.RetryCount++
			msg.NextRetryAt = time.Now().Add(time.Duration(msg.RetryCount) * time.Minute)
			s.messageSvc.Update(ctx, &msg)
		}
	}
}

// RetryFailedMessages 定时重试失败消息
func (s *OrderService) RetryFailedMessages() {
	ticker := time.NewTicker(1 * time.Minute)
	defer ticker.Stop()
	
	for range ticker.C {
		ctx := context.Background()
		
		// 查询需要重试的消息
		messages, _ := s.messageSvc.FindRetryable(ctx, time.Now())
		
		for _, msg := range messages {
			if msg.RetryCount >= 5 {
				// 超过最大重试次数,转人工处理
				msg.Status = MessageStatusFailed
				s.messageSvc.Update(ctx, &msg)
				s.createManualTask(ctx, &msg)
				continue
			}
			
			// 重试发送
			s.publishMessage(ctx, &msg)
		}
	}
}

// 下游服务消费消息
type InventoryConsumer struct {
	inventorySvc InventoryService
}

func (c *InventoryConsumer) Consume(ctx context.Context, msg *OrderCreatedEvent) error {
	// 幂等性检查
	if c.isProcessed(ctx, msg.OrderID) {
		log.Infof("订单%d已处理,跳过", msg.OrderID)
		return nil
	}
	
	// 扣减库存
	err := c.inventorySvc.Deduct(ctx, msg.Items)
	if err != nil {
		// 扣减失败,发送补偿消息
		c.publishCompensation(ctx, msg.OrderID)
		return err
	}
	
	// 标记已处理
	c.markAsProcessed(ctx, msg.OrderID)
	
	return nil
}

延伸思考

  1. 本地消息表 vs Saga vs TCC如何选择?
  2. 消息发送失败如何保证最终一致性?

📊 案例9:用户画像系统的设计

问题描述: 为了实现个性化推荐,需要构建用户画像(年龄、性别、消费能力、兴趣偏好)。如何设计用户画像系统?

答案

推荐方案:实时+离线双层架构

package userprofile

import (
	"context"
	"time"
)

// UserProfile 用户画像
type UserProfile struct {
	UserID int64
	
	// 基础信息
	Age    int
	Gender string
	City   string
	
	// 消费画像
	AvgOrderAmount    decimal.Decimal  // 客单价
	TotalOrderCount   int              // 订单数
	ConsumptionLevel  string           // 消费能力:高/中/低
	
	// 兴趣画像
	FavoriteCategories []int64         // 偏好类目
	FavoriteBrands     []string        // 偏好品牌
	PriceRange         PriceRange      // 价格区间
	
	// 行为特征
	ActiveTime         []int           // 活跃时段
	ShoppingFrequency  string          // 购物频次
	LastPurchaseTime   time.Time
	
	UpdatedAt time.Time
}

// UserProfileService 用户画像服务
type UserProfileService struct {
	repo        UserProfileRepository
	rdb         *redis.Client
	kafkaWriter *kafka.Writer
}

// GetProfile 获取用户画像
func (s *UserProfileService) GetProfile(ctx context.Context, 
	userID int64) (*UserProfile, error) {
	
	// 从缓存读取
	cacheKey := fmt.Sprintf("user:profile:%d", userID)
	profileJSON, err := s.rdb.Get(ctx, cacheKey).Result()
	if err == nil {
		profile := &UserProfile{}
		json.Unmarshal([]byte(profileJSON), profile)
		return profile, nil
	}
	
	// 从数据库读取
	profile, err := s.repo.FindByUserID(ctx, userID)
	if err != nil {
		return nil, err
	}
	
	// 缓存
	profileJSON, _ = json.Marshal(profile)
	s.rdb.SetEX(ctx, cacheKey, profileJSON, 6*time.Hour)
	
	return profile, nil
}

// UpdateProfileRealtime 实时更新画像
func (s *UserProfileService) UpdateProfileRealtime(ctx context.Context, 
	event *UserBehaviorEvent) error {
	
	// 将行为事件写入Kafka
	return s.kafkaWriter.WriteMessages(ctx, kafka.Message{
		Key:   []byte(fmt.Sprintf("%d", event.UserID)),
		Value: s.serializeEvent(event),
	})
}

// Flink实时计算(伪代码)
/*
用户行为流 → Flink → 实时画像

Flink Job:
1. 消费Kafka用户行为流
2. 计算实时指标(浏览、加购、下单)
3. 更新Redis画像缓存
4. 每小时写入HBase
*/

// 离线计算(每日凌晨执行)
func (s *UserProfileService) BatchUpdateProfiles() error {
	ctx := context.Background()
	yesterday := time.Now().AddDate(0, 0, -1)
	
	// 1. 查询昨天的用户行为数据
	behaviors, _ := s.behaviorRepo.FindByDate(ctx, yesterday)
	
	// 2. 聚合计算
	profileUpdates := s.aggregateBehaviors(behaviors)
	
	// 3. 批量更新画像
	for _, update := range profileUpdates {
		s.repo.Update(ctx, update)
		
		// 清除缓存
		cacheKey := fmt.Sprintf("user:profile:%d", update.UserID)
		s.rdb.Del(ctx, cacheKey)
	}
	
	return nil
}

// 消费能力分层
func (s *UserProfileService) calculateConsumptionLevel(
	avgOrderAmount decimal.Decimal, totalOrderCount int) string {
	
	if avgOrderAmount.GreaterThanOrEqual(decimal.NewFromInt(500)) && 
		totalOrderCount >= 10 {
		return "高"
	} else if avgOrderAmount.GreaterThanOrEqual(decimal.NewFromInt(200)) && 
		totalOrderCount >= 3 {
		return "中"
	} else {
		return "低"
	}
}

延伸思考

  1. 如何保护用户隐私(GDPR合规)?
  2. 画像准确性如何评估?

💡 案例10:大促后的系统复盘

问题描述: 618大促结束后,需要对系统表现进行复盘。如何设计复盘报告,总结经验和改进点?

答案

复盘维度

  1. 业务指标

    • GMV:50亿
    • 订单量:1000万
    • 转化率:3.5%
    • 客单价:500元
  2. 技术指标

    • 峰值QPS:50万
    • 平均响应时间:200ms
    • P99响应时间:800ms
    • 可用性:99.95%
  3. 故障复盘

    • 23:00-23:15 订单服务QPS突增导致响应变慢
    • 根因:数据库连接池不足
    • 影响:15分钟内订单延迟,影响1000笔订单
    • 改进:增加连接池大小,增加熔断降级
  4. 优化建议

    • 缓存命中率从90%提升到95%
    • 数据库慢查询优化(TOP 10)
    • 增加自动扩容策略
// PromotionReview 大促复盘
type PromotionReview struct {
	PromotionName string
	StartTime     time.Time
	EndTime       time.Time
	
	// 业务指标
	GMV           decimal.Decimal
	OrderCount    int64
	ConversionRate float64
	
	// 技术指标
	PeakQPS       int64
	AvgLatency    time.Duration
	P99Latency    time.Duration
	Availability  float64
	
	// 故障列表
	Incidents     []*Incident
	
	// 改进建议
	Improvements  []string
}

// GenerateReviewReport 生成复盘报告
func GenerateReviewReport(ctx context.Context, 
	promotionID int64) (*PromotionReview, error) {
	
	// 1. 查询业务数据
	orders := queryOrders(ctx, promotionID)
	
	// 2. 查询监控数据
	metrics := queryMetrics(ctx, promotionID)
	
	// 3. 查询故障记录
	incidents := queryIncidents(ctx, promotionID)
	
	// 4. 生成报告
	review := &PromotionReview{
		PromotionName: "618大促",
		GMV:           calculateGMV(orders),
		OrderCount:    int64(len(orders)),
		PeakQPS:       metrics.PeakQPS,
		Incidents:     incidents,
	}
	
	return review, nil
}

延伸思考

  1. 如何设计大促演练(压测、故障演练)?
  2. 如何量化技术优化的ROI?

第五部分:章节补充面试题与答辩话术(78题)

本部分集中收录原本散落在正文和专题附录中的面试总结、常见追问、参考回答和答辩话术。正文各章只保留交叉引用,避免同一类面试材料分散在多个位置。

5.1 第8章 商品中心:常见问题与总结

来源:第8章 商品中心系统

8.15 常见面试问题

问题 1:商品中心和商品供给平台有什么区别?

参考回答:商品供给平台负责 Draft、Staging、QC、批量导入、库存创建 / 修改运营入口、供应商同步、DLQ 和发布编排;商品中心负责正式商品主数据、交易前契约、发布版本、商品快照和读模型。供给平台是“商品和供给能力如何进入平台”,商品中心是“正式商品如何被交易系统稳定使用”。

问题 2:为什么商品正式表不应该有 Draft、QC Pending、Rejected 状态?

参考回答:这些是供给流程状态,不是正式商品生命周期状态。新建商品在发布前还没有正式 item_id;编辑商品待审时,线上旧版本仍然有效。如果把流程状态塞进正式商品表,会导致搜索、缓存、订单误读未审核数据。

问题 3:为什么需要 Resource,而不是只用 SPU/SKU?

参考回答:很多数字商品依附于现实或外部资源,例如酒店、门店、活动、运营商、账单机构。Resource 表达资源事实,SPU/SKU 表达商品定义,Offer 表达销售承诺。这样供应商同步可以先沉淀资源,运营再决定如何售卖。

问题 4:Product Item、SPU、SKU、Offer 的关系是什么?

参考回答:Product Item 是前台商品入口或聚合根;SPU 表达共性商品定义;SKU 表达可下单规格单元;Offer 表达销售条件、渠道、销售期、库存来源、输入、履约和退款规则。

问题 5:为什么需要 publish_version?

参考回答:publish_version 用于防止旧编辑覆盖新版本,也用于搜索、缓存、订单按版本处理。编辑发布时要校验 base_publish_version == current_publish_version,否则进入版本冲突。

问题 6:订单为什么不能回读最新商品表?

参考回答:商品会不断修改标题、价格计划、履约参数和退款规则。历史订单必须按照下单时的商品、报价、履约和退款契约解释,所以订单要保存或引用创单时的商品快照。

问题 7:商品中心是否保存实时库存和最终成交价?

参考回答:不应该把实时库存和最终成交价作为商品中心唯一事实。商品中心保存库存配置和基础价、Offer 规则;实时库存由库存系统判断,最终成交价由计价系统试算。

问题 8:发布成功后搜索刷新失败怎么办?

参考回答:不回滚商品发布。商品正式表、版本、快照和 Outbox 同事务写入;搜索刷新通过 Outbox 异步重试,失败进入补偿任务。搜索索引按 publish_version 幂等更新,并通过巡检发现版本落后。

问题 9:供应商同步是否直接写商品中心?

参考回答:不建议。供应商同步先保存 Raw Snapshot,然后标准化、映射、Diff,进入供给平台 Staging 和发布治理。商品中心只接收发布命令和正式结果。

问题 10:商品中心如何支持异构品类?

参考回答:用类目模板定义 Resource、SPU、SKU、Offer、交易契约和展示字段;核心交易字段结构化,品类核心字段可以用扩展表,长尾展示字段用 JSON,搜索筛选字段进入搜索投影。

问题 11:为什么发布事务里不直接刷新缓存和 ES?

参考回答:刷新缓存和 ES 是下游投影动作,可能慢、可能失败,不应该放在商品发布事务里。发布事务只写正式表、版本、快照和 Outbox;下游通过事件异步刷新并可补偿。

问题 12:商品中心如何排查线上展示旧数据?

参考回答:先查 product_item_tab.current_publish_version,再查 product_snapshot_tabproduct_outbox_event,然后对比 Redis、搜索索引里的 publish_version。如果索引或缓存落后,通过 Outbox 重放或补偿任务刷新。


8.16 面试总结

商品中心不是供给后台,也不是简单 SPU/SKU CRUD。一个成熟的商品中心应该是正式商品主数据和交易前契约中心:用 Resource、Product Item、SPU/SKU、Offer / Rate Plan 表达“卖什么”和“怎么卖”,用库存配置、Input Schema、履约规则、退款规则表达“如何下单、如何履约、如何售后”,用 publish_version 和商品快照保证历史订单可解释,用 Outbox 保证搜索、缓存、计价上下文、数据平台和营销资格消费者最终一致。

面试时可以这样概括:

第 11 章的供给平台解决“商品如何进入平台”,第 8 章的商品中心解决“正式商品如何稳定支撑交易”。Draft、Staging、QC、批量任务、供应商同步都不应该污染商品正式表;商品中心只接受发布命令,写入正式主数据、交易前契约、发布版本、商品快照和 Outbox。订单只信快照,下游按版本消费事件。这样商品中心才能同时支撑多品类、多供应商、高并发导购和长期运营治理。


5.2 第11章 商品供给管理:总结与常见题

来源:第11章 商品供给管理:运营、库存与生命周期

11.16 答辩材料:面试总结

面试中可以这样总结商品供给管理:

我不会把商品供给设计成后台 CRUD,也不会把供应商同步、人工运营和库存运营割裂成几套系统。我的设计是统一供给治理平台:人工创建、批量导入、运营编辑、库存创建 / 修改和供应商同步都先进入 Draft、Task、Item 和 Staging,通过标准化、质量校验、Diff、风险审核、版本化发布、Outbox、DLQ、补偿和质量巡检后,再写入正式商品主数据和交易契约。库存配置、补货、券码导入、系统生码、门店 / 日期库存调整属于供给平台的运营工作流,但库存余额、码池状态机、预占记录和账本流水属于库存系统。单商品创建和编辑要保证同步体验,批量导入、批量编辑、库存批量导入和供应商同步必须异步任务化。Draft、Staging、QC、正式 Item、Task 状态要分开建模,避免状态语义混乱。发布时生成 publish_version 和商品快照,订单只相信创单时快照,下游刷新通过 Outbox 最终一致。这样才能支撑多品类、多供应商和强运营平台的长期演进。

如果只能做三件事:

  1. 收敛写入口:所有商品变更必须经过命令 API、Task、Staging 和发布事务。
  2. 建立版本和审核:所有高风险变更必须有 Diff、风险理由、审核记录和 publish_version
  3. 补齐补偿闭环:错误文件、DLQ、Outbox、质量巡检和运营看板必须能让失败被发现、被修复、被重新投递。

11.16 答辩材料:常见面试题

11.16.1 基础边界

问题 1:为什么商品供给与运营平台不能设计成商品中心的后台 CRUD?

参考要点:商品中心负责正式主数据和查询契约,供给平台负责 Draft、Task、Staging、审核、发布、补偿和审计。后台直接 CRUD 会让未审核数据污染线上,也会造成搜索、缓存、订单快照和发布版本不可追溯。

问题 2:商品中心和供给运营平台的边界是什么?

参考要点:商品中心保存正式 Resource / SPU / SKU / Offer / Rulepublish_version;供给运营平台保存供给流程对象,例如 Draft、Staging、QC、Task、DLQ。供给平台通过发布事务写商品中心,不直接让运营后台改正式表。

问题 3:供应商同步和人工上传要分成两套系统吗?

参考要点:不要简单回答“分开”或“不分开”。更准确的设计是:入口层、执行层要分开,标准化之后的治理和发布层要合流

供应商同步和人工上传的执行问题完全不同:

维度人工上传 / 批量导入供应商同步
触发方式用户点击、上传 Excel、保存 Draft定时任务、全量同步、增量同步、Push
数据来源本地运营、商家外部供应商 API / 消息
失败原因字段填错、模板错误、图片不合规超时、限流、5xx、游标失效、字段漂移
进度模型文件行号、导入 item、错误文件city / page / cursor / checkpoint
恢复机制重传文件、失败行重试checkpoint 续跑、lease 抢占、DLQ
证据保存上传文件、表单 payloadRaw Snapshot、payload hash
新鲜度通常不要求秒级酒店、票务、库存价格可能要求高新鲜度

所以供应商同步需要独立执行层:

supplier_sync_task
supplier_sync_batch
supplier_sync_snapshot
supplier_sync_diff_log
supplier_sync_dead_letter

但它们不能完全拆成两套商品发布系统。否则人工上传一套 QC 和发布逻辑,供应商同步另一套 QC 和发布逻辑,很容易出现审核规则重复、发布版本乱序、字段主导权混乱、搜索缓存刷新不一致、订单快照口径不统一等问题。

推荐架构是:

人工上传 / 批量导入
  → product_supply_task
  → product_supply_task_item
  → product_supply_staging
  → Validation
  → Change Request / Risk
  → QC / Auto Approve
  → Publish
  → Outbox

供应商同步
  → supplier_sync_task
  → supplier_sync_batch
  → Raw Snapshot
  → Normalize
  → Supplier Mapping
  → Diff
  → product_supply_task(task_type=SUPPLIER_SYNC_IMPORT)
  → product_supply_staging
  → Validation
  → Change Request / Risk
  → QC / Auto Approve
  → Publish
  → Outbox

能力拆分可以这样判断:

能力是否分开原因
供应商 API Adapter分开每个供应商协议不同
全量 / 增量同步任务分开需要 checkpoint、lease、分页游标
Raw Snapshot分开供应商原始响应要可追溯和回放
供应商限流 / 熔断分开外部依赖治理
文件解析人工链路独有Excel / CSV / 模板
Draft 编辑体验人工链路独有用户可反复保存
标准化模型合并最终都要转成平台 Resource / SKU / Offer
质量校验合并商品发布门禁应该统一
Diff / Risk合并风险判断口径要统一
QC 审核合并审核工作台和策略要统一
Publish合并只能有一个正式发布入口
Outbox合并下游刷新口径要统一
DLQ / 补偿部分合并供应商执行 DLQ 分开,发布治理 DLQ 合并

面试时可以收束成一句话:

供应商同步有自己的“采集和恢复系统”,但不应该有自己的“商品发布系统”。采集链路可以分,发布治理必须合。

问题 4:本地运营上传和商家上传为什么审核策略不同?

参考要点:本地运营是内部可信来源,默认 AUTO_APPROVE,但仍要走 Validation、Staging、Publish 和 Outbox;商家是外部来源,默认 QC_REQUIRED,QC 通过后才能发布。

11.16.2 生命周期与 ID 设计

问题 5:为什么 Draft、Staging、QC、正式 Item 要有不同状态机?

参考要点:它们描述的是不同对象。Draft 是可编辑工作区,Staging 是提交冻结快照,QC 是审核工单,正式 Item 是线上商品资产。把这些状态混在一个字段里,会出现 DRAFT/QC_PENDING/ONLINE/REJECTED 语义混乱。

问题 6:新建商品在 Draft 和 QC 阶段还没有正式 item_id,怎么追踪全链路?

参考要点:首次创建时生成 supply_trace_idtemporary_object_key。发布成功后生成正式 item_id,再写 product_supply_object_mapping,后续可以用 item_id 反查 supply_trace_id

问题 7:商品已经在线后再次编辑,哪些 ID 会新建,哪些 ID 会复用?

参考要点:item_idsupply_trace_id 复用;operation_iddraft_idstaging_id、可能的 review_id 都新建;发布成功后 publish_version 递增。

问题 8:为什么商家提交 Draft 后要生成不可随意修改的 Staging Ticket?

参考要点:Draft 是工作区,可以反复保存;Staging 是提交证据和审核发布对象。冻结 Staging 可以保证 QC 审核的内容和最终发布内容一致,避免“审核 A,发布 B”。

问题 9:Pending 阶段发现内容填错,还能直接编辑吗?

参考要点:不建议直接编辑待审 Staging。推荐语义是撤回当前 QC 和 Staging,再基于 Staging payload 生成新 Draft,修改后重新提交,生成新的 Staging 和 QC。

11.16.3 审核、Diff 与发布

问题 10:为什么需要 product_qc_reviewproduct_qc_review_item 两张表?

参考要点:product_qc_review 是审核工单主表,记录整体状态、审核人、结论;product_qc_review_item 是审核项明细,记录字段 Diff、风险原因、分项结论和驳回原因。批量任务和字段级驳回都需要明细表。

问题 11:product_change_requestproduct_supply_operation_logproduct_field_ownership 分别解决什么问题?

参考要点:product_change_request 记录改了什么、风险多高、是否需要 QC;product_supply_operation_log 记录从 Draft 到下线全过程发生了什么;product_field_ownership 记录字段由谁主导,防止供应商同步覆盖人工治理结果。

问题 12:product_publish_recordproduct_publish_snapshotproduct_change_log 的区别是什么?

参考要点:publish_record 记录一次发布动作和结果;publish_snapshot 保存发布后的正式商品上下文;change_log 解释正式商品从旧版本到新版本变了哪些字段。

问题 13:Publish 背后的实际流程是什么?

参考要点:发布前校验 Staging 状态、QC 状态、幂等、版本冲突和交易契约完整性;事务内写正式商品表、库存配置、履约退款规则、publish_version、发布快照、变更日志和 Outbox;事务后由搜索、缓存、计价上下文和数据平台消费者异步重建投影。涉及活动配置时,由供给平台发起营销系统命令或营销资格事件,但营销规则、预算和优惠计算仍归营销系统。

问题 14:为什么 QC 通过不等于商品已经上线?

参考要点:QC 通过只表示允许发布。真正上线还需要 Publish Worker 完成正式表写入、生成发布版本、写 Outbox、刷新搜索缓存,并且商品满足销售时间、库存、渠道和风控可售条件。

问题 15:为什么发布前要校验 base_publish_version

参考要点:编辑是基于某个线上版本产生的。如果当前线上版本已经变化,旧 Staging 继续发布会覆盖别人的新修改。发布前做 CAS 版本校验,冲突时进入 VERSION_CONFLICT 并要求重新编辑。

11.16.4 批量任务与供应商同步

问题 16:为什么批量导入不能同步循环写正式表?

参考要点:批量导入可能有大量数据、行级错误、长耗时和下游压力。应该先创建 product_supply_task,再流式解析文件、生成 task_item、行级校验、部分成功、错误文件和补偿任务。

问题 17:Parser Worker 和 Item Worker 为什么要拆开?

参考要点:Parser Worker 只负责文件解析和生成行级 item;Item Worker 负责标准化、校验、Staging、Diff、QC 和发布准备。拆开后解析失败不会污染业务处理,行级处理可以限流、重试和并行。

问题 18:product_supply_taskproduct_supply_task_item 的关系是什么?

参考要点:Task 是一次供给动作的批次状态,Task Item 是行级或对象级处理单元。Task 状态由 Item 聚合,支持部分成功、部分失败、部分等待 QC,避免一个失败项拖垮整批任务。

问题 19:供应商同步为什么需要 Raw Snapshot、Checkpoint 和 DLQ?

参考要点:供应商同步是长任务且外部不稳定。Raw Snapshot 用于追溯和回放;Checkpoint 用于断点续跑;DLQ 用于保存字段缺失、映射失败、发布失败等可运营问题单。

11.16.5 一致性、补偿与交易安全

问题 20:如何保证商品发布后搜索、缓存、计价上下文最终一致?营销活动如何协同?

参考要点:正式表、发布记录、发布快照和 Outbox 在同一事务内写入;搜索、缓存、计价上下文和数据平台通过 Outbox 异步消费,按 event_idpublish_version 幂等处理,失败进入重试、DLQ 或 product_compensation_task。供给平台不直接写搜索索引、不直接写最终价格,也不直接改订单。营销活动是控制面协同:供给平台可以发起圈品、活动资格和活动绑定命令,营销系统负责活动规则、预算、券、补贴、营销库存和最终优惠计算。订单创建只相信当时的商品、报价、履约和退款快照,不回读最新商品解释历史订单。

问题 21:商品供给运营平台、商品生命周期和库存系统如何联动?

参考要点:创建和修改库存属于供给运营平台的业务工作,但不属于供给运营平台的数据事实。供给平台是库存变更的控制面,负责表单、导入、审批、任务、错误文件、可售诊断和审计;库存系统是库存事实的数据面,负责 inventory_configinventory_balance、券码池、预占记录、状态机和账本流水。供给平台治理变更是否可以发布,商品生命周期控制正式商品何时在线、下线、结束和封禁,库存系统控制某个范围内是否有可承诺资源,营销系统控制活动、券、补贴、预算和优惠规则。发布成功不等于可售成功,Publish 只写正式商品、发布版本、交易契约和 Outbox;库存通过 CreateInventoryAdjustInventoryImportCodeBatchGenerateCodeBatch 等命令异步创建或调整库存实例,再发布 InventoryReady/InventoryChanged/InventoryFailed。最终由可售投影合成 product_status + inventory_status + price_status + marketing_status + fulfillment_status + channel_policy + risk_status,告诉搜索、缓存和详情页是否可卖。供给后台不能直接改库存余额,也不能直接写营销优惠计算结果;库存系统也不能绕过审核和发布版本直接改商品生命周期。

问题 22:库存运营任务为什么要单独建模?

参考要点:库存运营任务不是库存扣减本身,而是库存变更进入平台的控制面。简单数量制库存可以随商品发布自动创建,但也应该由供给平台发起 CreateInventory 命令,库存系统幂等创建库存实例和账本;后台补货、调库存、锁库存要有权限、审批、原因和审计;手动上传券码要有文件任务、行级错误、重复码校验和错误文件;系统生码要有批次、规则、数量、有效期和幂等恢复;门店 / 日期 / 时段库存要支持批量物化和局部调整。供给平台负责 INVENTORY_CREATE / INVENTORY_ADJUST / CODE_IMPORT / CODE_GENERATE / TIME_STORE_STOCK_MATERIALIZE 等任务的入口、进度、补偿和审计,库存系统负责余额、码池状态机、预占、扣减、释放和账本。关键是每层都要有幂等键:发布创建库存用 publish_id + sku_id + scope,行级任务用 task_id + item_no,券码用 batch_id + code_hash,库存命令用 operation_id


5.3 附录F 供应商数据同步链路:总结与题目

来源:附录F 供应商数据同步链路

17. 面试总结

100 万酒店、10 小时的供应商全量同步任务,我不会设计成依赖进程内状态的单进程长循环,而会先设计成 Batch + Page/Cursor Checkpoint 的可恢复流水线。供应商原始响应先保存 Raw Snapshot,然后做标准化、质量校验、平台模型映射、Diff 和发布。同步版本、快照版本和发布版本要分离,保证可追溯、可回放和可审计。失败数据进入 MySQL DLQ,支持自动重试、人工修复和重新投递。监控上不仅看任务成功率,还要看字段缺失率、映射失败率、新鲜度延迟、DLQ 修复率和下游刷新失败率。任务分片和分布式 Worker 抢占可以作为后续优化,在第一阶段不强行复杂化主链路。

18. 面试题目

18.1 基础理解

问题 1:为什么不能把 100 万酒店同步设计成一个单进程长循环?

参考回答:因为任务运行时间长,中途机器重启、供应商超时、进程发布、网络抖动的概率都很高。单进程长循环的进度通常在内存里,失败后只能从头跑,排查也困难。更合理的设计是 Task + Batch + Checkpoint + DLQ:任务先落库,执行过程持续推进 checkpoint,失败数据进入 DLQ,机器重启后从 checkpoint 恢复。

问题 2:Task 和 Batch 有什么区别?

参考回答:Task 是任务定义,描述“同步什么、怎么同步、什么时候同步”,比如某供应商酒店全量同步;Batch 是一次具体执行,描述“这一次跑到了哪里、成功多少、失败多少、当前 worker 是谁、租约什么时候过期”。一个 Task 会产生多次 Batch。

问题 3:Checkpoint 是什么,什么时候更新?

参考回答:Checkpoint 是任务进度水位,例如当前城市、页码、供应商 cursor、最后处理成功的供应商酒店 ID。它用于断点续跑。推荐先处理本页数据,再推进 checkpoint。这样即使机器在中间宕机,最多重复处理上一页,不会跳过未处理数据。

18.2 分布式执行与恢复

问题 4:worker 如何抢占任务?

参考回答:通过数据库 CAS 抢占。worker 执行一条带条件的 UPDATE,只有 status=PENDINGstatus=RUNNING AND lease_until < NOW() 的 batch 才能被抢占。rows_affected=1 才说明抢占成功,其他 worker 必须退出。

问题 5:worker_idlease_token 的区别是什么?

参考回答:worker_id 标识执行器实例,通常由服务名、机器名或容器名、进程号、启动时间组成,方便排查和监控。lease_token 标识一次抢占行为,每次抢占都重新生成。关键写操作必须同时校验 batch_id + worker_id + lease_token,防止旧 worker 恢复后覆盖新 worker 的进度。

问题 6:心跳、租约、checkpoint 分别解决什么问题?

参考回答:心跳说明 worker 是否还活着;租约说明当前任务执行权属于谁;checkpoint 说明任务恢复时从哪里继续。心跳正常不代表任务在前进,所以还要看 last_checkpoint_at。租约过期才允许新 worker 抢占,checkpoint 用于恢复位置。

问题 7:机器重启后怎么恢复?

参考回答:机器重启后原 worker 不再续租,lease_until 到期。新 worker 通过 CAS 抢占过期 batch,读取 end_checkpoint,从对应城市、页码或 cursor 继续。由于可能重复处理上一页,所以落库必须基于 supplier_id + supplier_resource_code + supplier_product_code 做幂等。

问题 8:旧 worker 在长 GC 或网络抖动后恢复了怎么办?

参考回答:旧 worker 恢复后可能以为自己还拥有任务。所有续租、checkpoint、发布和结束任务的 SQL 都必须带 worker_id + lease_token 条件。如果更新影响行数为 0,说明租约已经丢失,旧 worker 必须停止执行,不能继续写平台表。

18.3 任务互斥与边界场景

问题 9:上一次任务还没跑完,又下发了一次任务怎么办?

参考回答:要用显式互斥策略。默认 SKIP_IF_RUNNING,如果已有同 task_codePENDING/RUNNING batch,新任务直接跳过;人工强制重跑可使用 CANCEL_PREVIOUS;只有数据范围不重叠时才允许 ALLOW_PARALLEL

问题 10:心跳正常但 checkpoint 长时间不动,说明什么?

参考回答:说明 worker 还活着,但任务可能卡在某个阶段,例如供应商慢请求、对象存储写入慢、数据库锁等待、发布阻塞。此时不应立即抢占,而应告警并结合 last_heartbeat_stage 定位卡点。只有租约过期才允许新 worker 抢占。

问题 11:checkpoint 更新失败怎么办?

参考回答:如果本页处理成功但 checkpoint 更新失败,下次恢复可能重复处理本页。因此页内落库和发布必须幂等。相反,不能先更新 checkpoint 再处理数据,否则宕机会跳过未处理页面。

问题 12:任务被人工取消时 worker 还在跑,怎么停?

参考回答:worker 不应该只在启动时读取状态,而要在每页处理前后检查 batch status。如果发现 CANCELLED,停止继续拉供应商,不再发布新数据,只做必要的清理和日志记录。

18.4 数据治理与补偿

问题 13:Raw Snapshot 的价值是什么?

参考回答:Raw Snapshot 是供应商原始响应的证据,不是平台商品模型。它用于排查线上问题、回放同步、验证标准化规则、做 diff,也能区分是供应商数据错误还是平台映射错误。

问题 14:为什么需要 Diff?

参考回答:同步成功不等于应该发布。Diff 用来比较标准化后的数据和当前线上发布版本,识别字段变化、图片变化、坐标变化、房型变化和可售变化。低风险变化可以自动发布,高风险变化进入审核或 DLQ。

问题 15:DLQ 为什么建议用 MySQL,而不是只用消息队列?

参考回答:供应商同步失败往往不是简单消息消费失败,而是字段缺失、城市映射失败、价格异常、发布失败等需要人工修复、状态流转和审计的问题。MySQL DLQ 可以作为权威问题单,支持查询、分派、重试、忽略、修复和报表。消息队列 DLQ 可以做短期缓冲,但不适合作为运营修复台账。

问题 16:100 万酒店 10 小时如何估算吞吐?

参考回答:100 万 / 10 小时约等于 27.8 个酒店/秒。如果每页 100 个酒店,大约需要 10000 页,10 小时内只需要 0.28 页/秒。但如果要逐个拉详情,就是 27.8 QPS,还要受供应商限流、超时、重试和数据处理速度约束。

18.5 Redis 抢占问题

问题 17:worker 可以从 Redis 中抢占任务吗?

参考回答:可以,但我会把 Redis 作为加速锁或短租约,不把它作为唯一权威状态。任务状态、checkpoint、统计、DLQ 和审计仍然落 MySQL。Redis 可以用 SET lock_key value NX EX 300 抢锁,用 Lua 保证续租和释放的原子性。但真正开始执行前仍要更新 MySQL batch 的 worker_id + lease_token + lease_until,避免 Redis 主从切换、锁丢失或网络分区导致状态不可追溯。

5.4 附录G 商品供给与运营治理平台:面试总结

来源:附录G 商品供给与运营治理平台

18. 面试总结

我不会把商品供给和运营链路设计成后台 CRUD。我的设计是统一供给治理平台:人工创建、批量导入、运营编辑、库存创建 / 修改和供应商同步都先进入任务和暂存区,经过标准化、质量校验、Diff、风险审核、版本化发布、Outbox、补偿和质量巡检后,才写入正式商品主数据和交易契约。系统难点不在于建几张商品表,而在于入口语义统一、线上隔离、类目差异、批量部分成功、库存控制面、运营误操作防护、字段主导权、发布一致性、订单快照和失败运营化。供应商同步属于供给链路,但因为它有 Checkpoint、Raw Snapshot、Worker 租约和数据新鲜度问题,所以作为专项链路单独设计。

5.5 附录G 商品供给与运营治理平台:常见题

来源:附录G 商品供给与运营治理平台

19. 面试题目

问题 1:为什么商品供给不能直接写商品正式表?

参考回答:因为供给入口产生的是未校验、未审核、未发布的数据。直接写正式表会让半成品商品被搜索、下单或履约系统读取。正确做法是先写 Draft / Staging,通过校验、审核和发布事务后再写正式表。

问题 2:供应商同步是不是商品供给链路的一部分?

参考回答:是。供应商同步是商品供给的一种自动化入口,但不是全部。统一供给平台要承接人工创建、批量导入、运营编辑、库存创建 / 修改和供应商同步。供应商同步因为有长任务、Checkpoint、Raw Snapshot、Worker 租约和新鲜度问题,所以需要专项链路。

问题 3:批量导入为什么要支持部分成功?

参考回答:大批量导入中少量错误很常见。如果 10 万行因为 100 行失败全部回滚,运营效率会非常低。更合理的是行级状态,成功项继续发布,失败项生成错误文件,运营修复后重新提交。

问题 4:运营编辑如何防止覆盖供应商同步的数据?

参考回答:要定义字段主导权。平台运营主导的标题、卖点、活动标签不应被供应商自动覆盖;供应商主导的库存和价格可以按策略更新;高风险字段进入审核。运营人工覆盖供应商字段时要记录原因、责任人和保护期。

问题 5:审核通过为什么不等于商品可售?

参考回答:审核只说明变更可以发布,真正可售还依赖商品主数据、Offer、库存、Input Schema、履约规则、退款规则、搜索索引和缓存刷新都就绪。因此发布后还要做可售校验和下游刷新补偿。

问题 6:如何保证商品发布和搜索缓存一致?

参考回答:商品正式表更新和 Outbox 事件写入同一个事务。事务提交后,由异步消费者刷新 ES、缓存、计价上下文和数据平台。涉及营销活动时,由供给平台发起活动绑定、圈品或营销资格命令,营销系统负责规则、预算、券和优惠计算。刷新或协同失败进入补偿任务,保证最终一致。

问题 7:为什么订单要保存商品快照?

参考回答:商品会持续变化,包括价格、标题、履约规则和退款规则。如果历史订单回读最新商品配置,会导致售后争议和财务对不上。创单时必须保存商品快照、报价快照、履约契约和退款规则快照。

问题 8:Draft 和 Staging 有什么区别?

参考回答:Draft 面向编辑过程,允许运营反复保存、撤销和补充信息,不代表一次可发布变更。Staging 面向发布过程,保存已经标准化、校验过、可审核的候选数据。Draft 更像工作区,Staging 更像发布前的候选版本,两者都不能直接被 C 端读取。

问题 9:为什么批量导入需要 Task 和 Task Item 两层模型?

参考回答:Task 描述一次批量动作的整体状态,例如总行数、成功数、失败数和当前阶段;Task Item 描述每一行、每个商品、每个 Offer 的处理结果。没有 Item 层,就无法定位“第几行为什么失败”,也无法支持部分成功、错误文件和行级重试。

问题 10:供给任务如何做幂等?

参考回答:入口层要有业务幂等键,例如 task_type + trigger_id、文件 Hash、供应商批次号或变更单号。发布层要用 publish_idbase_publish_version 和唯一约束防止重复写正式表。Outbox 消费侧也要支持事件幂等,避免重复刷新缓存、索引或下游配置。

问题 11:大文件批量导入如何避免内存爆和长事务?

参考回答:上传后先落对象存储,异步流式解析文件,按批次写入 Task Item,不把全文件一次性加载到内存。校验和标准化由 Worker 分片处理,发布时按商品或变更单分批提交,避免一个 10 万行文件变成一个超大事务。

问题 12:商品供给链路中哪些校验必须前置?

参考回答:字段格式、必填项、类目属性、SPU / SKU / Offer 关系、价格、库存来源、履约规则、退款规则、渠道和站点可售性都要前置校验。尤其是交易契约类字段不能只在下单时兜底,否则线上商品看似发布成功,实际无法购买或无法履约。

问题 13:类目模板变更后,历史商品怎么处理?

参考回答:类目模板要版本化,历史商品保留创建或上次发布时的模板版本。模板升级后可以触发质量巡检或迁移任务,对缺失新必填属性的商品标记风险、限制重新发布或要求运营补齐,不能静默把历史商品全部判为非法。

问题 14:哪些运营变更需要强审核?

参考回答:高风险字段需要强审核,例如价格大幅调整、批量下架、退款规则收紧、履约方式变更、类目迁移、供应商字段覆盖、C 端展示敏感文案和活动标签。系统可以用 Diff + 风险评分决定是否自动通过、二次确认或进入人工审核。

问题 15:为什么发布时要校验 base_publish_version

参考回答:供给变更通常基于某个旧版本编辑。如果发布时正式商品已经被其他任务修改,直接覆盖会丢失别人刚发布的变更。base_publish_version 用来做乐观并发控制,发现版本不一致时进入冲突处理,让运营选择合并、放弃或重新编辑。

问题 16:商品发布失败如何回滚?

参考回答:发布要尽量把正式表变更和 Outbox 写入放在同一事务中。事务内失败直接回滚;事务后下游刷新失败不回滚主数据,而是进入补偿队列和 DLQ。对于已发布但业务上要撤回的变更,应通过新的反向发布版本恢复,而不是手工改库。

问题 17:下游搜索、缓存或计价上下文刷新失败怎么办?

参考回答:发布事务提交后通过 Outbox 事件驱动读侧投影刷新。消费者失败时记录重试次数、错误原因和 TraceID,超过阈值进入 DLQ,由补偿任务或人工修复重新投递。运营后台要展示“商品已发布但部分投影未同步”的状态;如果是营销活动协同失败,也要展示活动绑定失败原因和重试入口,避免误判为完全成功。

问题 18:错误文件应该包含哪些信息?

参考回答:错误文件要能让运营直接修复问题,至少包含原始行号、商品标识、字段名、错误类型、错误原因、修复建议和是否可重试。对于映射类错误,还应给出候选类目、候选属性或合法枚举值,而不是只写“导入失败”。

问题 19:如何设计商品质量分?

参考回答:质量分可以从内容完整度、类目属性完整度、图片质量、价格有效性、库存可售性、履约规则、退款规则、供应商新鲜度和历史投诉率等维度计算。质量分既用于运营看板,也可以驱动自动下架、限制投放、召回补录和供应商质量考核。

问题 20:运营后台最需要展示哪些链路状态?

参考回答:运营后台要展示任务进度、成功失败数量、当前处理阶段、失败明细、审核状态、发布版本、下游同步状态、错误文件下载、可重试入口和质量巡检结果。后台不是简单 CRUD,而是要把长链路的不确定性运营化。

问题 21:两个运营同时编辑同一个商品怎么办?

参考回答:可以用草稿锁、版本号和 Diff 合并共同解决。编辑态可以提示“商品已被某人编辑”,提交时用 base_publish_version 做并发校验;若版本冲突,则展示双方 Diff,让运营选择覆盖、合并或重新基于最新版本编辑。

问题 22:供应商价格和库存很新,但运营有人工覆盖,系统听谁的?

参考回答:要按字段主导权处理。价格、库存通常由供应商或库存系统主导,但平台可以允许有期限、有原因、有审批记录的人工覆盖。覆盖期内供应商同步只记录 Diff 或进入待审核,覆盖到期后再恢复自动更新,避免人工修复被下一次同步冲掉。

问题 23:商品上下架和发布版本是什么关系?

参考回答:上下架是商品生命周期状态,发布版本是商品数据变更记录。一次发布可以改变上下架状态,但两者不能混为一谈。商品可以有多个历史发布版本,当前只有一个生效版本;上下架还要受质量、库存、价格、渠道、风控和审核状态共同约束。

问题 24:如何支持定时发布和灰度发布?

参考回答:变更单可以携带生效时间、渠道范围、城市范围、人群范围或流量比例。到达生效时间后由发布调度器执行,先写正式版本和 Outbox,再按范围刷新搜索、缓存和计价上下文;如涉及营销活动,同步发起营销活动配置或资格变更命令。灰度期间要能观察转化、投诉和履约异常,必要时快速撤回。

问题 25:自动下架会带来什么风险?

参考回答:自动下架能阻止缺图、缺价、无库存或无履约规则的商品继续售卖,但也可能误伤 GMV 和活动资源。设计时要区分阻断级、告警级和观察级问题;高风险商品自动下架,低风险商品先告警并给运营修复窗口,同时保留审计和恢复入口。

问题 26:供给链路如何做审计追踪?

参考回答:每次变更都要记录操作者、来源入口、TraceID、原始输入、标准化结果、Diff、审核记录、发布版本、下游事件和补偿记录。审计不是只看最终商品表,而是要能还原“谁在什么时候因为什么把商品改成了什么”。

问题 27:如果面试官让你一句话概括架构,你怎么说?

参考回答:我会说商品供给与运营链路的核心是“统一入口、暂存隔离、质量校验、风险审核、版本发布、事件同步、失败补偿和运营可观测”。它的目标不是让运营能改商品,而是让商品从各种入口进入平台后,稳定、可控、可追溯地变成可售供给。

5.6 业务架构图表述模板

来源:第5章 业务架构与上下文映射

面试与评审中的表述模板:「业务架构图不是组织架构图;它描述的是能力边界与语义所有权。若两个团队共改一张宽表,通常意味着限界上下文识别失败或缺少明确的集成契约。」

5.7 搜索导购容量估算口径

来源:第12章 搜索与导购

容量估算(面试与立项常问)可以按乘法拆解:峰值 QPS × 每页条数 ×(Hydrate 下游 RPC 数)≈ 下游扇出;再叠加 重试风暴 系数(建议保守取 1.3~1.8)。ES 侧则关注 分片热点(超大店、超级品牌)与 聚合查询 对 CPU 的抢占;必要时对 shop 场景做 单独索引或单独副本组,把热点从全站检索隔离出去,成本换稳定性。

5.8 bool 查询语义高频点

来源:第12章 搜索与导购

bool 查询语义 是面试高频点:must 参与评分,适合承载关键词相关性;filter 不计分且可缓存,适合承载「硬门槛」。实践中常见错误是把「品牌=耐克」放在 must 里参与打分,导致品牌词意外影响相关性曲线;更推荐 品牌进 filter,把「品牌相关 boost」交给 function_score 或在精排阶段处理。另一个错误是把大量 低选择性 条件全部堆在 must,使 _score 退化为常数,精排阶段只能「白手起家」——这会放大后续服务压力。

5.9 搜索深分页标准答法

来源:第12章 搜索与导购

面试标准答法:C 端深分页用 search_after + 稳定 tie-breaker(如 spu_id);随机跳页用产品约束(最多第 N 页)或改写交互。

5.10 搜索系统边界追问

来源:第12章 搜索与导购

面试追问小结(边界类):「搜索是不是商品中心的一部分?」标准回答是 :商品中心是主数据真相源;搜索维护 派生读模型 以优化检索与排序特征。「为什么不在 ES 里算最终价?」因为价格解释依赖会员、渠道、动态规则与版本,索引天然滞后;列表允许弱一致,交易必须强一致。「推荐能否直接调用搜索接口拿候选?」不建议默认耦合:推荐候选集生成逻辑与搜索意图不同,但可以在 特征与埋点层复用

5.11 搜索导购一句话总结

来源:第12章 搜索与导购

若你用一句话向面试官总结本章,可以这样说:搜索索引解决「找得到与排得动」,Hydrate 解决「展示得像且不太假」;交易链路再用强一致服务解决「买得对」。 三条链路各司其职,边界清晰,才能把高并发读路径做成 可演进、可回滚、可观测 的工程系统,而不是一堆 DSL 拼接技巧。

5.12 购物车与结算域追问答法

来源:第13章 购物车与结算

面试映射:面试官若问「购物车要不要用分布式锁」,优先回答「行级乐观锁 + Redis 原子写足够,锁购物车会放大死锁与热点」;若问「结算页是不是微服务必须的拆分点」,可以回答「逻辑边界必须清晰,物理部署可渐进」——先把包边界与数据边界立住,比一上来拆两个集群更有性价比。

5.13 订单系统白板答辩串联

来源:第14章 订单系统

面试与答辩串联:若需要在白板前讲解订单系统,推荐顺序为:先画主状态机,再画创单 Saga 时序,再补幂等键与补偿表,最后点出「订单不直连渠道」。评委追问超卖时,回到库存 Reserve 与 CAS;追问重复支付时,回到支付幂等与订单状态机;追问消息丢失时,回到 Outbox。把四条线闭合,通常比堆技术名词更有说服力。

5.14 支付系统子系统职责对照

来源:第15章 支付系统

子系统职责对照(面试与评审常用):

子系统核心职责关键数据典型反模式
账户系统余额、冻结、流水、充值提现账户余额、冻结单、账务流水把渠道手续费写进用户余额宽表
支付网关路由、报文、签名、重试渠道请求 / 响应摘要、路由决策在网关里改支付单状态机
支付核心支付单、退款单、幂等、OutboxpaymentrefundoutboxHandler 直连第三方 SDK
清结算引擎分账、批次、结算周期分账明细、结算单、提现单清结算回调里直接操作支付单
风控系统限额、名单、挑战风控决策流水风控结果不落库导致无法复盘

5.15 支付系统边界反模式

来源:第15章 支付系统

反模式清单(面试与评审可直用):在支付服务里写供应商下单、在支付服务里计算运费、在支付回调里直接改 SKU 库存、在支付库里维护商品税率版本。它们共同症状是:支付发布频率被迫与业务域绑定,任何小改动都触碰资金链路。

5.16 供应商同步重复下发答法

来源:附录F 供应商数据同步链路

面试时可以强调:重复下发不是靠“大家约定别点两次”解决,而要靠任务互斥策略和数据库状态控制解决

5.17 商品供给系统核心判断

来源:附录G 商品供给与运营治理平台

面试时可以先强调:商品供给系统最难的不是“建商品表”,而是入口治理、暂存隔离、质量校验、风险审核、发布一致性和失败运营化

5.18 库存语义混淆答辩提示

来源:第9章 库存系统

面试和架构评审里最容易犯的错误,是把这些语义混成一个 stock 字段。这样短期能跑,长期一定会在支付重试、订单关单、退款回补、供应商晚到回调和运营手工调整时失控。

5.19 商品供给与供应商同步关系判断

来源:第16章 电商系统综合设计

面试时可以先给出这个判断:

供应商同步是商品供给链路的一种入口,但不能把商品供给系统等同于供应商同步系统。商品供给平台要统一承接人工创建、批量导入、运营编辑、库存创建 / 修改和供应商同步;其中供应商同步是最复杂的自动化供给分支,需要独立的同步任务、Checkpoint 和数据治理链路。

5.20 人工创建链路答辩提示

来源:第16章 电商系统综合设计

面试时可以强调:人工创建链路最容易被低估。真正难点不是页面表单,而是类目差异、交易契约完整性、审核证据和发布一致性。

5.21 商品供给统一治理总结

来源:第16章 电商系统综合设计

面试总结

我不会把商品供给设计成后台 CRUD,也不会把供应商同步、人工运营和库存运营割裂成几套互不相干的系统。我的设计是统一供给治理平台:人工创建、批量导入、运营编辑、库存创建 / 修改和供应商同步都先进入任务和暂存区,通过标准化、质量校验、Diff、差异化审核、版本化发布、Outbox、补偿和巡检后,再写入正式商品主数据和交易契约。供应商同步属于供给链路,但因为它涉及 Raw Snapshot、Checkpoint、Worker 租约、DLQ 和数据新鲜度,所以作为专项链路单独展开。这样既能保证入口统一,又能处理不同来源的复杂度差异。


5.22 供应商同步失败治理答法

来源:第16章 电商系统综合设计

面试时可以强调:供应商同步系统不是追求 100% 同步成功,而是要做到失败可定位、可隔离、可修复、可补偿

5.23 供应商同步 DLQ 总结

来源:第16章 电商系统综合设计

面试时可以这样总结:

我会把供应商同步 DLQ 设计成 MySQL 主存储,而不是只依赖 Kafka 死信 Topic。因为同步失败往往不是单纯消息消费失败,而是字段缺失、映射失败、价格异常、发布失败这些需要人工修复、状态流转和审计的问题。Kafka 可以作为失败消息的缓冲层,但真正的死信治理要落 MySQL,记录供应商、外部编码、平台映射、错误阶段、payload 引用、重试次数、下次重试时间和处理状态。这样问题才能被查询、补偿、统计和运营化处理。

5.24 供应商同步整体总结

来源:第16章 电商系统综合设计

面试总结

我不会把供应商同步理解成“写几个定时任务拉数据”。在 OTA、O2O 和虚拟商品平台里,供应商同步本质上是一套外部供给数据治理体系。它要解决幂等映射、版本回溯、质量校验、新鲜度控制、失败补偿和监控治理。列表页可以使用缓存提升性能,详情页要更接近实时,创单前必须实时确认。这样才能在供应商接口不稳定、商品模型不一致、价格库存频繁变化的情况下,仍然保证平台商品数据可信、交易链路安全、问题可追踪。


5.25 搜索导购链路总结

来源:第16章 电商系统综合设计

面试时可以这样总结:搜索导购不是单纯 ES 查询,而是“召回 + 排序 + 商品补齐 + 库存价格营销融合 + 降级”的完整读链路


附录:常见技术方案速查

缓存策略

策略适用场景优点缺点
Cache Aside通用实现简单缓存穿透风险
Read Through读密集透明缓存实现复杂
Write Through读写均衡一致性强写性能差
Write Behind写密集写性能好数据可能丢失

分布式事务

方案一致性性能复杂度适用场景
2PC强一致金融、支付
TCC最终一致订单、库存
Saga最终一致长流程
本地消息表最终一致通用

限流算法

算法平滑性突发处理实现难度
计数器简单
滑动窗口
令牌桶
漏桶最好

分库分表

维度用户ID分片订单ID分片时间分片
用户查询★★★★★★☆☆☆☆★★☆☆☆
订单查询★★☆☆☆★★★★★★★★☆☆
数据均匀★★★☆☆★★★★★★★☆☆☆
扩容难度

常用Go库推荐

// Web框架
github.com/gin-gonic/gin

// ORM
gorm.io/gorm

// Redis
github.com/go-redis/redis/v8

// 消息队列
github.com/Shopify/sarama  // Kafka
github.com/streadway/amqp  // RabbitMQ

// 分布式追踪
go.opentelemetry.io/otel

// 配置管理
github.com/spf13/viper

// 限流
golang.org/x/time/rate

// 日志
go.uber.org/zap

// 数值计算
github.com/shopspring/decimal

结语

本面试题精选覆盖了电商系统的核心技术栈,从系统架构到具体实现,从理论到实践。建议读者:

  1. 系统学习:按章节顺序学习,建立完整知识体系
  2. 动手实践:核心算法和代码自己实现一遍
  3. 举一反三:理解原理,能够应用到实际项目
  4. 持续更新:技术在演进,保持学习新技术

面试技巧

  • 先理清思路,再动手画图
  • 多提供几种方案,对比优缺点
  • 关注非功能性需求(性能、可用性、扩展性)
  • 结合实际项目经验

祝各位求职顺利!🎉