附录B 面试题精选
使用说明
本附录为《电商系统架构设计与实现》的配套面试准备资料,包含 123 道主线精选题 / 案例 和 78 道章节补充题,合计 201 道明确题目,重点关注电商核心系统的架构设计、工程实现和面试答辩。
题型标注
- 📊 系统设计题: 需要设计架构、数据模型、分析流程
- 🔧 技术方案题: 需要给出具体技术选型和实现思路
- 💡 场景分析题: 给定业务场景,分析问题并提出解决方案
- 🚀 综合案例题: 跨系统的复杂场景,考察全局架构能力
答案结构
每道题的答案包含:
- 问题分析:核心挑战是什么
- 方案设计:2-3种可选方案
- 方案对比:trade-offs分析
- 推荐方案:结合实际场景的建议
- 延伸思考:相关的深入问题
难度说明
本附录的题目均面向中高级工程师和架构师(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 和人工修复 |
| 数据模型 | 能给出核心表 | 能解释唯一键、索引、分片、快照、历史版本和审计字段 |
| 高并发 | 能讲缓存和限流 | 能讲热点、降级、预热、削峰、隔离队列和读写路径分离 |
| 可运营性 | 能说有后台 | 能设计任务进度、错误文件、可售诊断、补偿入口和审计链路 |
| 风险意识 | 能识别超卖、重复支付 | 能识别资损、历史订单解释、营销成本、供应商脏数据和人工误操作 |
常见减分点:
- 把商品供给平台讲成商品表 CRUD。
- 把库存当成一个
stock字段,不讲预占、释放、账本和幂等。 - 把 Redis、ES、MQ 当权威事实源。
- 订单回读最新商品、价格、履约规则解释历史交易。
- 发布事务同步调用所有下游,忽略最终一致和补偿。
- 只讲技术组件,不讲运营修复、错误文件、DLQ 和审计。
第一部分:电商架构基础(20题)
1.1 系统全景与架构设计(10题)
📊 题目1:如何设计一个中大型电商平台的服务拆分边界?
问题描述: 假设你接手一个日订单50万的电商平台,目前是单体应用,团队规模100人。现在需要进行微服务化改造。请设计服务拆分方案。
答案:
问题分析: 服务拆分的核心挑战在于:
- 既要保证领域边界清晰(DDD视角)
- 又要考虑团队规模和协作效率
- 还要兼顾性能和一致性要求
- 需要平衡拆分粒度和运维复杂度
方案一:按业务能力垂直拆分
核心服务划分:
- 核心交易域:订单、支付、结算、购物车(4个服务)
- 商品供给域:商品中心、库存、计价、营销(4个服务)
- 用户导购域:搜索、推荐、用户中心(3个服务)
- 运营支撑域:商品上架、B端运营、数据分析(3个服务)
拆分原则:
- 每个服务对应一个限界上下文(Bounded Context)
- 服务间通过稳定的API契约通信
- 核心链路服务优先拆分,支撑域可暂时保留
优点:
- 团队自治性强,可并行开发
- 业务边界清晰,易于理解和维护
- 符合DDD最佳实践
- 故障隔离效果好
缺点:
- 需要处理分布式事务
- 服务间调用增加网络开销
- 初期实施复杂度较高
- 需要完善的基础设施支持
方案二:按技术特征水平拆分
划分:
- 高并发读服务:商品详情、搜索、列表(使用缓存和ES)
- 强一致写服务:订单、支付、库存扣减(使用分布式事务)
- 异步处理服务:消息通知、数据同步、报表生成
优点:
- 技术栈统一,便于基础设施复用
- 性能优化方向明确
- 团队技能要求更聚焦
缺点:
- 业务边界模糊,团队协作困难
- 不符合微服务自治原则
- 业务变更可能需要跨多个服务修改
方案三:绞杀者模式渐进拆分
实施步骤:
- 第一阶段:拆出搜索(读多写少,影响面小)
- 第二阶段:拆商品中心(依赖少,数据模型清晰)
- 第三阶段:拆订单链路(核心但复杂,需要Saga编排)
- 第四阶段:处理遗留单体(逐步清理剩余功能)
优点:
- 风险可控,可持续交付
- 团队学习曲线平缓
- 每个阶段都有明确产出
缺点:
- 迁移周期较长(可能需要6-12个月)
- 中间状态维护成本高
- 需要维护双写逻辑
方案对比:
| 维度 | 方案一(垂直) | 方案二(水平) | 方案三(渐进) |
|---|---|---|---|
| 团队自治 | ★★★★★ | ★★☆☆☆ | ★★★☆☆ |
| 技术复杂度 | ★★★☆☆ | ★★★★☆ | ★★☆☆☆ |
| 交付速度 | ★★★☆☆ | ★★★★☆ | ★★★★☆ |
| 长期维护 | ★★★★★ | ★★☆☆☆ | ★★★★☆ |
推荐方案: 采用方案一+方案三的结合:按领域垂直划分目标架构,采用绞杀者模式渐进实施。
实施要点:
- 前期准备:梳理系统依赖图,识别核心路径和边界
- 基础设施先行:建立服务网格、监控、链路追踪、配置中心
- 服务契约规范:定义API标准、版本管理、错误码体系
- 数据迁移策略:双写+对账+延迟删除
- 灰度发布机制:按用户维度或地域逐步切流量
延伸思考:
- 如何处理拆分过程中的数据迁移?
- 分布式事务如何保证?
- 服务间调用的超时和重试策略如何设计?
🔧 题目2:如何设计电商系统的数据一致性方案?
问题描述: 电商系统中,订单创建涉及库存扣减、优惠券核销、积分扣除等多个操作,这些操作分散在不同的微服务中。如何保证数据一致性?
答案:
问题分析: 数据一致性的核心挑战:
- 多个微服务涉及写操作,无法使用传统数据库事务
- 部分操作可能失败,需要补偿机制
- 性能要求高,不能因为一致性牺牲太多性能
- 需要考虑系统可用性,不能因为一个服务故障导致整体不可用
方案一:Saga模式(编排式)
设计思路: 由订单服务作为编排器(Orchestrator),协调各个服务的操作。
流程:
- 订单服务:创建订单(状态:PENDING)
- 调用库存服务:预占库存(成功继续,失败取消订单)
- 调用营销服务:锁定优惠券(成功继续,失败释放库存)
- 调用积分服务:扣除积分(成功继续,失败释放库存+券)
- 更新订单状态:CONFIRMED
优点:
- 流程清晰,易于理解和调试
- 中心化控制,便于监控和排查问题
- 补偿逻辑集中管理
缺点:
- 订单服务成为单点,压力较大
- 流程变更需要修改编排器
- 服务间耦合度较高
方案二:Saga模式(事件编排式)
设计思路: 通过事件总线(Kafka)进行编排,各服务监听事件并发布新事件。
流程:
- 订单服务:创建订单 → 发布 OrderCreated 事件
- 库存服务:监听 OrderCreated → 预占库存 → 发布 InventoryReserved 事件
- 营销服务:监听 InventoryReserved → 锁定优惠券 → 发布 CouponLocked 事件
- 积分服务:监听 CouponLocked → 扣除积分 → 发布 PointsDeducted 事件
- 订单服务:监听 PointsDeducted → 更新订单状态为 CONFIRMED
优点:
- 服务解耦,各服务独立演进
- 无中心化瓶颈,扩展性好
- 天然支持异步,性能更好
缺点:
- 流程分散,难以全局把控
- 调试困难,需要完善的链路追踪
- 事件顺序和幂等性要求高
方案三:TCC(Try-Confirm-Cancel)
设计思路: 分为三个阶段:Try(预留)、Confirm(确认)、Cancel(取消)。
流程:
- Try阶段:预留资源(库存预占、券锁定、积分冻结)
- Confirm阶段:确认扣减(库存确认、券核销、积分扣除)
- Cancel阶段:取消操作(释放库存、释放券、解冻积分)
优点:
- 强一致性,业务语义清晰
- 资源锁定明确,不会出现超卖
- 适合对一致性要求极高的场景
缺点:
- 实现复杂,需要每个服务提供三个接口
- 性能开销大(锁定资源时间长)
- Try阶段占用资源,影响并发度
方案对比:
| 维度 | Saga-编排 | Saga-事件 | TCC |
|---|---|---|---|
| 实现复杂度 | ★★★☆☆ | ★★★★☆ | ★★★★★ |
| 性能 | ★★★☆☆ | ★★★★☆ | ★★☆☆☆ |
| 一致性强度 | 最终一致 | 最终一致 | 强一致 |
| 可观测性 | ★★★★☆ | ★★★☆☆ | ★★★★☆ |
推荐方案: 对于电商系统,推荐Saga-编排模式作为主方案,辅以事件通知。
实施要点:
- 订单服务作为编排器,维护状态机
- 每个操作携带唯一业务ID保证幂等性
- 每个步骤设置合理超时(如库存3s,优惠券2s)
- 维护补偿表记录需要补偿的操作
- 每个步骤记录日志,包含traceId
延伸思考:
- 如何处理补偿失败的情况?
- 最终一致性的“最终“是多久?
- 如何进行一致性验证和对账?
💡 题目3:大促期间如何保证系统稳定性?
问题描述: 公司准备参加双11大促,预估流量是平时的50倍,订单量达到平时的100倍。你需要确保系统在大促期间稳定运行,请设计保障方案。
答案:
问题分析: 大促稳定性的核心挑战:
- 流量突增:如何应对峰值流量(平时5000 QPS → 25万 QPS)
- 资源瓶颈:数据库、缓存、网络等基础设施能否支撑
- 热点问题:少量商品承载大部分流量
- 故障隔离:如何避免局部故障扩散为全局故障
方案一:垂直扩容+全链路压测
核心思路: 通过提升单机性能和压测验证来保障稳定性。
技术方案:
- 数据库层:升级配置(32核128G → 64核256G),增加只读从库
- 应用层:服务器扩容(50台 → 200台),JVM调优
- 缓存层:Redis扩容(3节点 → 12节点),缓存预热
- 压测验证:提前1个月进行全链路压测,发现瓶颈
优点:
- 改动小,风险可控
- 实施周期短
- 回退方便
缺点:
- 成本高(需要高配机器)
- 扩展性有限
- 单点风险依然存在
方案二:限流降级+分级保障
核心思路: 通过限流降级保护核心链路,非核心功能可降级。
技术方案:
-
多级限流:
- 网关层:总QPS限流(25万)
- 服务层:单服务限流(如订单创建10万)
- 接口层:单接口限流(如查询详情5万)
-
分级降级:
- P0核心:下单、支付、查询订单(必保)
- P1重要:搜索、详情、加购(降级返回缓存)
- P2一般:推荐、评论、收藏(直接关闭)
-
熔断机制:连续失败达阈值后自动熔断
优点:
- 保护核心链路
- 成本可控
- 局部故障不扩散
缺点:
- 用户体验下降(部分功能不可用)
- 降级逻辑需要提前准备
- 限流阈值难以精确设定
方案三:异步化+削峰填谷
核心思路: 将同步操作改为异步,通过消息队列削峰填谷。
技术方案:
- 订单异步化:下单成功后立即返回,后台异步处理
- 库存预占:Redis预扣,后台异步同步到DB
- 消息队列:Kafka承载峰值流量,消费者慢慢处理
- 任务调度:非实时任务延迟处理(如数据统计)
优点:
- 峰值流量平滑处理
- 用户体验好(快速响应)
- 系统压力平缓
缺点:
- 架构改造较大
- 数据最终一致性
- 异常处理复杂
方案对比:
| 维度 | 垂直扩容 | 限流降级 | 异步化 |
|---|---|---|---|
| 成本 | ★★☆☆☆ | ★★★★☆ | ★★★☆☆ |
| 用户体验 | ★★★★★ | ★★★☆☆ | ★★★★☆ |
| 实施难度 | ★★★★☆ | ★★★☆☆ | ★★☆☆☆ |
| 扩展性 | ★★☆☆☆ | ★★★☆☆ | ★★★★★ |
推荐方案: 采用三种方案的组合:垂直扩容作为基础,限流降级作为保护,异步化作为优化。
实施要点:
- 提前3个月准备:容量规划、全链路压测、应急演练
- 分级保障策略:明确P0/P1/P2功能,准备降级开关
- 实时监控:QPS、成功率、响应时间、错误率
- 应急预案:数据库主从切换、缓存雪崩处理、快速扩容
- 值班机制:7×24值守,关键节点实时响应
延伸思考:
- 如何评估系统需要的容量?
- 大促期间出现故障如何应急?
- 如何验证限流降级策略是否有效?
📊 题目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个机房)
- 数据一致性复杂(跨城同步)
- 运维复杂度高
方案对比:
| 维度 | 单元化 | 两地三中心 | 三地五中心 |
|---|---|---|---|
| 数据一致性 | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
| 成本 | ★★★☆☆ | ★★★★☆ | ★☆☆☆☆ |
| 容灾能力 | ★★★☆☆ | ★★★★☆ | ★★★★★ |
| 实施难度 | ★★★★☆ | ★★★☆☆ | ★★☆☆☆ |
推荐方案: 对于中大型电商,推荐两地三中心+单元化的组合方案。
实施要点:
- 单元划分:按用户ID分8个单元,每个单元在2个机房部署
- 同城双活:北京A、B机房互为主备
- 异地灾备:上海C机房作为灾备
- 路由策略:用户→单元→机房的三级路由
- 数据同步:同城强同步(Raft/Paxos),异地异步同步
- 故障切换:自动检测+自动切换,RTO<5分钟
延伸思考:
- 如何处理跨单元的全局数据(如商品库存)?
- 机房故障时如何保证不丢数据?
- 如何验证多活架构的有效性?
🔧 题目5:如何设计服务间的调用链路追踪?
问题描述: 微服务架构下,一个用户请求可能经过十几个服务。当出现问题时,如何快速定位是哪个服务出了问题?请设计分布式追踪方案。
答案:
问题分析: 链路追踪的核心挑战:
- 如何关联一次请求涉及的所有服务调用
- 如何记录调用链路的详细信息(耗时、参数、结果)
- 如何在性能开销和可观测性之间平衡
- 如何快速检索和分析海量追踪数据
方案一:自研追踪系统
核心思路: 基于唯一TraceID串联整个调用链,每个服务记录SpanID。
设计:
- TraceID:全局唯一ID,标识一次完整请求
- SpanID:服务内部的调用单元ID
- 传递机制:通过HTTP Header或RPC Context传递
- 数据收集:每个服务将Span数据异步上报
- 存储分析:存储到Elasticsearch,Kibana可视化
实现步骤:
- 网关生成TraceID
- 服务间传递TraceID和ParentSpanID
- 每个服务记录:服务名、方法名、开始时间、结束时间、状态
- 异步上报到追踪系统
优点:
- 完全可控,可定制
- 无外部依赖
- 数据私密性好
缺点:
- 开发成本高
- 需要所有服务埋点
- 维护成本高
方案二:使用开源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%)
- 性能优化:异步上报,避免阻塞主流程
- 标准化:定义统一的TraceID、SpanID规范
- 关键节点:重点追踪慢查询、外部调用、错误日志
- 告警配置:P99延迟、错误率等关键指标告警
延伸思考:
- 如何在不影响性能的前提下采集足够的追踪数据?
- 如何处理跨语言服务的追踪?
- 追踪数据如何与日志、指标关联?
💡 题目6:如何平衡微服务拆分的粒度?
问题描述: 在进行微服务拆分时,服务拆得太粗会失去微服务的优势,拆得太细会导致运维复杂度爆炸。如何平衡服务拆分的粒度?
答案:
问题分析: 服务粒度的核心挑战:
- 服务太粗:失去独立部署、技术异构的优势
- 服务太细:服务数量多,运维成本高,调用链路长
- 边界不清:职责重叠,数据冗余
- 团队匹配:服务划分要与团队结构匹配
方案一:按领域模型拆分(DDD)
核心思路: 基于领域驱动设计(DDD)的限界上下文划分服务。
判断标准:
- 业务完整性:一个聚合根对应一个服务
- 独立演进:服务内部变化不影响其他服务
- 团队自治:一个团队(5-9人)负责一个服务
- 数据自治:服务拥有独立的数据库
示例:
- 订单服务:负责订单生命周期管理
- 库存服务:负责库存预占、扣减、释放
- 支付服务:负责支付路由、状态管理、对账
优点:
- 业务边界清晰
- 符合康威定律
- 易于理解和维护
缺点:
- 需要团队理解DDD
- 前期建模成本高
- 对业务专家依赖大
方案二:按变化频率拆分
核心思路: 将变化频繁的功能和稳定的功能拆开。
判断标准:
- 变化频率:营销活动(周级)vs 用户中心(月级)
- 技术栈:搜索(ES)vs 订单(MySQL)
- 性能要求:详情页(缓存)vs 下单(强一致)
示例:
- 营销服务:促销规则频繁变化,独立拆分
- 计价服务:计算逻辑复杂但相对稳定
- 用户服务:用户信息变化少,可合并
优点:
- 快速响应业务变化
- 技术选型灵活
- 易于优化性能
缺点:
- 可能打破业务边界
- 数据一致性复杂
- 难以预测变化频率
方案三:按团队规模拆分(两个披萨原则)
核心思路: 一个团队负责的服务数量,要让团队可以“用两个披萨喂饱“(5-9人)。
判断标准:
- 团队规模:一个5-9人团队负责2-3个服务
- 认知负载:团队能理解和维护的复杂度
- 沟通成本:减少跨团队协作
示例:
- 订单团队:订单服务 + 售后服务 + 物流编排服务
- 商品团队:商品服务 + 类目服务 + 品牌服务
- 库存团队:库存服务 + 仓储服务
优点:
- 团队职责清晰
- 减少沟通成本
- 符合组织架构
缺点:
- 服务粒度可能不均衡
- 团队变化时需要调整
- 可能打破业务边界
方案对比:
| 维度 | DDD拆分 | 变化频率 | 团队规模 |
|---|---|---|---|
| 业务清晰度 | ★★★★★ | ★★★☆☆ | ★★★☆☆ |
| 响应速度 | ★★★☆☆ | ★★★★★ | ★★★★☆ |
| 实施难度 | ★★☆☆☆ | ★★★★☆ | ★★★★★ |
| 长期维护 | ★★★★★ | ★★★☆☆ | ★★★★☆ |
推荐方案: 采用DDD为主,兼顾变化频率和团队规模的混合策略。
实施要点:
- 核心域优先DDD:订单、支付等核心域严格按DDD拆分
- 支撑域灵活拆分:搜索、推荐等可按技术特征拆分
- 团队匹配:确保每个团队能hold住负责的服务
- 逐步演进:初期粗粒度,随业务发展逐步拆细
- 防止过度拆分:服务数量控制在团队规模的2-3倍
判断粒度是否合适的指标:
- 服务代码量:1-3万行最佳
- 团队负担:一个团队2-3个服务
- 调用链路:核心链路不超过5跳
- 部署频率:每周至少部署1次
- 故障恢复:单服务故障不影响全局
延伸思考:
- 如何判断服务拆得太细了?
- 已有服务如何合并?
- 服务拆分后如何保证数据一致性?
📊 题目7:电商系统的技术债治理策略
问题描述: 随着业务快速发展,系统积累了大量技术债(如代码重复、过度耦合、缺少测试)。如何在保证业务持续交付的前提下,有效治理技术债?
答案:
问题分析: 技术债治理的核心挑战:
- 业务压力大,没有时间重构
- 技术债范围广,不知从何下手
- ROI不明确,难以说服业务
- 改造风险高,担心引入新问题
方案一:停止新功能,集中还债
核心思路: 暂停新功能开发1-2个月,团队集中精力重构优化。
实施步骤:
- 评估技术债清单(代码质量、架构问题、性能问题)
- 按影响面和风险排优先级
- 集中2个月时间重构
- 重构完成后恢复业务开发
优点:
- 集中资源,效率高
- 可以做系统性重构
- 团队专注度高
缺点:
- 业务方难以接受(2个月不交付)
- 改造风险集中爆发
- 团队压力大
方案二:绞杀式重构,逐步替换
核心思路: 不停止业务开发,在交付新功能时逐步重构。
实施策略:
- 新功能新写法:新功能用新架构实现
- 改功能顺便重构:修改老功能时顺便重构
- 热点优先:优先重构变化频繁的模块
- 设置重构配额:每个迭代20%时间用于重构
示例:
- 迭代1:新功能A(新架构) + 重构模块X
- 迭代2:新功能B(新架构) + 重构模块Y
- 迭代3:新功能C(新架构) + 重构模块Z
优点:
- 业务持续交付
- 风险分散,可控
- 团队适应性好
缺点:
- 周期长(可能需要6-12个月)
- 需要团队自律
- 新老代码共存,维护成本高
方案三:分层治理,重点突破
核心思路: 识别高价值技术债,集中资源重点治理。
分层策略:
- P0紧急债:影响线上稳定性(立即处理)
- 例:核心接口性能问题、安全漏洞
- P1重要债:影响开发效率(1个月内处理)
- 例:核心模块耦合严重、缺少测试
- P2普通债:代码质量问题(逐步优化)
- 例:代码重复、命名不规范
- P3可忽略:历史遗留问题(不处理)
- 例:废弃功能的代码
实施步骤:
- 扫描技术债(代码扫描工具 + 人工评估)
- 分级打标(P0/P1/P2/P3)
- P0立即处理,P1排入迭代,P2见缝插针
- 每月review技术债清单
优点:
- 重点突出,ROI高
- 灵活可控
- 容易说服业务
缺点:
- 需要持续跟进
- 分级标准难以统一
- P2/P3的债永远还不完
方案对比:
| 维度 | 集中还债 | 绞杀重构 | 分层治理 |
|---|---|---|---|
| 业务影响 | ★★☆☆☆ | ★★★★★ | ★★★★☆ |
| 治理效率 | ★★★★★ | ★★★☆☆ | ★★★★☆ |
| 风险控制 | ★★☆☆☆ | ★★★★☆ | ★★★★★ |
| 实施难度 | ★★★☆☆ | ★★★★☆ | ★★★★☆ |
推荐方案: 采用分层治理为主,绞杀重构为辅的组合策略。
实施要点:
- 建立技术债看板:可视化展示技术债清单和进度
- 设置重构配额:每个迭代15-20%时间用于技术债
- 重构优先级:P0立即处理,P1必须进迭代,P2机动
- 度量指标:代码覆盖率、圈复杂度、重复率、Bug密度
- 自动化检测:SonarQube扫描,PR卡点
- 技术债评审会:每月评审技术债治理进展
关键原则:
- 不积累新债:新代码必须符合质量标准
- 热点优先:优先重构变化频繁的模块
- 小步快跑:每次重构范围可控,及时验证
- 有始有终:重构必须完整,不能半途而废
- 文档同步:重构后及时更新架构文档
延伸思考:
- 如何评估技术债的严重程度和优先级?
- 重构过程中如何保证不引入新问题?
- 如何说服业务投入资源治理技术债?
🔧 题目8:如何设计配置中心?
问题描述: 微服务架构下,配置分散在各个服务中,修改配置需要重启服务。请设计一个配置中心,支持配置的集中管理和动态更新。
答案:
问题分析: 配置中心的核心挑战:
- 配置变更如何实时推送到服务
- 如何保证配置的一致性和可靠性
- 如何支持灰度发布和快速回滚
- 如何保证配置的安全性(敏感信息加密)
方案一:基于文件+定时拉取
核心思路: 配置存储在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作为配置中心。
实施要点:
-
配置分层:
- 环境配置(dev/test/prod)
- 公共配置(数据库连接池、日志级别)
- 应用配置(业务参数)
- 敏感配置(密码、密钥)加密存储
-
命名规范:
- dataId:
${应用名}-${环境}.${格式} - group:
${业务域} - 例:
order-service-prod.yaml,group:transaction
- dataId:
-
配置刷新:
- 使用
@RefreshScope注解 - 或实现
ConfigChangeListener接口 - 敏感配置(数据库连接)不支持热更新
- 使用
-
灰度发布:
- 先在灰度环境验证
- 按IP或比例逐步推送
- 监控关键指标,出问题立即回滚
-
安全控制:
- 敏感配置加密存储(AES/RSA)
- RBAC权限管理(谁能改什么配置)
- 审计日志(谁在什么时候改了什么)
- 配置变更必须经过审批流程
-
高可用:
- Nacos集群部署(至少3节点)
- 客户端本地缓存(Nacos不可用时降级)
- 配置备份(定期导出到Git)
延伸思考:
- 配置中心本身如何保证高可用?
- 敏感配置如何加密存储?
- 如何保证配置变更的安全性(防止误操作)?
💡 题目9:领域驱动设计在电商系统中的应用
问题描述: 你需要用DDD的思想设计电商订单系统。请描述如何识别聚合根、实体、值对象,以及如何划分限界上下文。
答案:
问题分析: DDD在电商中的核心挑战:
- 如何识别领域边界(限界上下文)
- 如何设计聚合根和实体关系
- 如何处理跨聚合的数据一致性
- 如何平衡领域纯粹性和工程实践
方案一:贫血模型+服务层
核心思路: 实体只包含数据,业务逻辑在服务层。
设计:
实体:
- 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 | 复杂业务 | 高性能+审计 |
推荐方案: 对于电商订单系统,推荐充血模型+聚合根。
实施要点:
-
识别限界上下文:
- 订单上下文:订单生命周期、订单状态管理
- 库存上下文:库存预占、扣减、释放
- 支付上下文:支付路由、支付状态、对账
- 物流上下文:物流单、轨迹跟踪
-
设计聚合根:
- Order(订单聚合根)
- 实体:OrderItem
- 值对象:Address、Money
- 行为:create、pay、ship、cancel
- 不变量:订单金额一致性、状态流转规则
- Order(订单聚合根)
-
跨聚合协作:
- 强一致性:同一聚合内(订单和订单项)
- 最终一致性:跨聚合(订单和库存)
- 集成方式:领域事件 + Saga编排
-
代码组织:
order/
├── domain/ # 领域层
│ ├── Order.java # 聚合根
│ ├── OrderItem.java # 实体
│ ├── Address.java # 值对象
│ └── OrderStatus.java # 枚举
├── application/ # 应用层
│ └── OrderService.java # 应用服务(编排)
├── infrastructure/ # 基础设施层
│ ├── OrderRepository.java # 仓储接口
│ └── OrderRepositoryImpl.java # 仓储实现
└── api/ # 接口层
└── OrderController.java # REST API
- 关键设计决策:
- 聚合边界:Order包含OrderItem,不包含Product(属于商品上下文)
- ID生成:聚合根负责生成ID(UUID或雪花ID)
- 领域事件:订单状态变更发布事件(OrderPaid、OrderShipped)
- 仓储模式:通过Repository加载/保存聚合根
- 工厂模式:复杂的聚合创建用工厂
延伸思考:
- 如何处理跨聚合根的事务(如订单和库存)?
- 充血模型如何映射到数据库(JPA/MyBatis)?
- DDD在微服务中如何落地(一个聚合根一个服务?)?
📊 题目10:设计电商系统的监控告警体系
问题描述: 电商系统涉及十几个微服务,需要建立完善的监控告警体系。请设计监控方案,确保能及时发现和处理线上问题。
答案:
问题分析: 监控告警的核心挑战:
- 监控什么:需要监控哪些指标
- 如何监控:使用什么工具和技术
- 告警策略:如何避免告警疲劳
- 快速定位:如何从告警快速定位问题
方案一:基础监控(单维度)
核心思路: 监控基础指标(CPU、内存、磁盘),发现异常告警。
监控内容:
- 主机监控:CPU、内存、磁盘、网络
- 应用监控:JVM(堆内存、GC)
- 接口监控:QPS、响应时间、错误率
- 日志监控:ERROR日志、异常堆栈
工具选择:
- 指标采集:Prometheus
- 可视化:Grafana
- 告警:Prometheus Alertmanager
优点:
- 覆盖基础指标
- 开源免费
- 社区活跃
缺点:
- 单维度监控,难以关联分析
- 告警规则简单(阈值告警)
- 缺少业务视角
方案二:全链路监控(多维度)
核心思路: 结合指标、日志、链路追踪三个维度,全方位监控。
监控体系:
-
指标监控(Metrics):
- RED指标:Rate(QPS)、Error(错误率)、Duration(延迟)
- USE指标:Utilization(使用率)、Saturation(饱和度)、Error(错误)
- 业务指标:订单量、支付成功率、库存水位
-
日志监控(Logging):
- 结构化日志:JSON格式,包含traceId
- 日志聚合:ELK(Elasticsearch + Logstash + Kibana)
- 日志告警:关键错误日志触发告警
-
链路追踪(Tracing):
- APM工具:Skywalking / Jaeger
- 调用链可视化:拓扑图、火焰图
- 性能分析:慢查询、慢接口
-
关联分析:
- traceId串联三个维度
- 从告警跳转到日志和链路
- 快速定位问题根因
优点:
- 立体化监控,全面覆盖
- 可快速定位问题
- 支持根因分析
缺点:
- 成本高(存储、计算)
- 运维复杂度高
- 需要统一traceId
方案三:智能监控(AIOps)
核心思路: 基于机器学习,智能检测异常和预测故障。
核心能力:
-
异常检测:
- 基线学习:学习历史数据建立基线
- 异常识别:偏离基线自动告警
- 减少误报:智能过滤噪音
-
根因分析:
- 故障关联:自动分析告警之间的关联
- 根因定位:从众多告警中识别根因
- 建议修复:推荐修复方案
-
容量预测:
- 趋势分析:分析资源使用趋势
- 容量预警:提前预警资源不足
- 扩容建议:推荐扩容方案
优点:
- 智能化,减少人工成本
- 预测性,提前发现问题
- 自动化根因分析
缺点:
- 成本高(算法研发、算力)
- 需要大量历史数据
- 准确率受限
方案对比:
| 维度 | 基础监控 | 全链路监控 | 智能监控 |
|---|---|---|---|
| 实施成本 | ★★★★★ | ★★★☆☆ | ★☆☆☆☆ |
| 覆盖全面性 | ★★☆☆☆ | ★★★★★ | ★★★★★ |
| 定位效率 | ★★☆☆☆ | ★★★★☆ | ★★★★★ |
| 适用规模 | 小型 | 中大型 | 大型 |
推荐方案: 对于中大型电商系统,推荐全链路监控。
实施要点:
-
指标体系设计:
-
黄金指标(核心业务):
- 订单创建成功率 > 99.9%
- 支付成功率 > 99.95%
- 接口P99延迟 < 500ms
-
基础指标(资源):
- CPU使用率 < 70%
- 内存使用率 < 80%
- 磁盘使用率 < 85%
-
业务指标(自定义):
- 实时订单量
- 库存水位
- 支付渠道成功率
-
-
告警分级:
- P0(紧急):影响核心功能(下单、支付失败),5分钟响应
- P1(重要):影响部分功能(搜索慢、详情页错误),30分钟响应
- P2(一般):资源告警(CPU高、磁盘满),2小时响应
- P3(提示):趋势告警(流量增长、容量预警),工作时间处理
-
告警策略:
- 避免告警疲劳:合并同类告警、设置静默期
- 智能降噪:工作时间和非工作时间不同阈值
- 分级通知:P0电话+短信,P1钉钉,P2邮件
- 告警收敛:同一问题5分钟内只告警一次
-
监控大盘:
- 业务大盘:实时订单量、支付成功率、GMV
- 应用大盘:服务健康度、QPS、错误率、P99延迟
- 基础设施大盘:主机、数据库、缓存、消息队列
- 告警大盘:实时告警、告警趋势、MTTR
-
应急响应:
- SOP(标准操作流程):不同告警对应的处理步骤
- On-call轮值:7×24值班,保证及时响应
- 故障复盘:每次P0/P1故障必须复盘,沉淀经验
- 演练机制:定期故障演练,验证应急预案
延伸思考:
- 如何设计监控指标体系(业务指标 vs 技术指标)?
- 如何避免告警疲劳(告警太多导致麻木)?
- 监控数据如何存储和查询(时序数据库)?
1.2 系统集成与一致性(10题)
📊 题目1:设计订单与库存的一致性保证方案
问题描述: 在订单创建流程中,需要同时扣减库存。订单服务和库存服务是两个独立的微服务,如何保证订单创建和库存扣减的一致性?
答案:
问题分析: 订单库存一致性的核心挑战:
- 不能使用分布式事务(性能差、可用性低)
- 需要防止库存超卖
- 订单创建失败时库存要回滚
- 用户取消订单时库存要释放
方案一:订单服务调用库存服务(同步)
核心思路: 订单创建时同步调用库存服务扣减库存,失败则取消订单。
流程:
- 订单服务:创建订单(状态:PENDING)
- 同步调用库存服务:扣减库存
- 成功:订单状态更新为CONFIRMED
- 失败:订单状态更新为CANCELLED
- 返回结果给用户
优点:
- 实现简单直观
- 实时一致性
- 用户立即知道结果
缺点:
- 同步调用性能差
- 库存服务故障影响订单创建
- 网络超时难处理(已扣减但订单不知道)
方案二:本地消息表+最终一致性
核心思路: 订单创建后发送消息给库存服务,库存服务异步扣减。
流程:
- 订单服务:
- 开启事务
- 插入订单(状态:PENDING)
- 插入本地消息表(OrderCreated事件)
- 提交事务
- 消息发送器:定时扫描本地消息表,发送到MQ
- 库存服务:监听MQ,扣减库存
- 库存服务:发送库存扣减成功事件
- 订单服务:监听事件,更新订单状态为CONFIRMED
优点:
- 异步解耦,性能好
- 库存服务故障不影响订单创建
- 消息可靠性高(本地消息表)
缺点:
- 最终一致性(用户不能立即知道结果)
- 实现复杂
- 需要处理消息重复
方案三:库存预占+两阶段提交
核心思路: 订单创建时先预占库存,支付成功后确认扣减,取消时释放。
流程:
- 订单服务:创建订单(状态:PENDING)
- 同步调用库存服务:预占库存(库存减少,预占量增加)
- 用户支付成功后:
- 订单状态更新为PAID
- 异步通知库存服务确认扣减(预占量减少)
- 用户取消订单:
- 订单状态更新为CANCELLED
- 异步通知库存服务释放预占(库存增加,预占量减少)
优点:
- 防止超卖(预占时检查库存)
- 用户体验好(立即知道有没有库存)
- 支持取消释放
缺点:
- 库存设计复杂(实际库存、预占库存、可售库存)
- 预占超时释放机制
- 长时间预占影响其他用户
方案对比:
| 维度 | 同步调用 | 本地消息表 | 库存预占 |
|---|---|---|---|
| 一致性 | 强一致 | 最终一致 | 强一致 |
| 性能 | ★★☆☆☆ | ★★★★★ | ★★★★☆ |
| 用户体验 | ★★★★★ | ★★☆☆☆ | ★★★★★ |
| 实施难度 | ★★★★☆ | ★★☆☆☆ | ★★★☆☆ |
推荐方案: 对于电商系统,推荐库存预占方案。
实施要点:
- 库存表设计:
total_stock:总库存reserved_stock:预占库存available_stock= total_stock - reserved_stock
- 预占接口:先检查available_stock,再扣减
- 预占超时:定时任务扫描超时订单,释放预占
- 幂等保证:使用orderId+action作为唯一键
- 监控告警:监控预占释放率、超时订单量
延伸思考:
- 如果库存预占接口超时,订单服务如何处理?
- 预占超时时间如何设定(太短影响支付,太长占用库存)?
- 秒杀场景下如何优化库存扣减性能?
🔧 题目2:Saga模式在电商系统中的应用
问题描述: 电商订单创建涉及多个服务(库存、优惠券、积分、支付),如何使用Saga模式保证分布式事务的一致性?请详细设计Saga编排方案。
答案:
问题分析: Saga在电商中的核心挑战:
- 如何设计补偿逻辑
- 如何处理部分失败
- 如何保证幂等性
- 如何监控和排查问题
方案一:编排式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。
实施要点:
-
状态机设计:
状态表: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: 错误信息 -
幂等性保证:
- 每个步骤携带saga_id作为业务唯一键
- 服务侧记录已处理的saga_id
- 重复请求直接返回成功
-
超时处理:
- 每个步骤设置超时时间(如3秒)
- 超时后执行补偿
- 定时任务兜底扫描长时间未完成的Saga
-
补偿策略:
- 向前补偿:重试直到成功
- 向后补偿:回滚已完成的步骤
- 混合补偿:核心步骤向前,非核心向后
-
监控告警:
- Saga成功率
- 平均执行时间
- 补偿执行次数
- 长时间未完成的Saga
延伸思考:
- 如果补偿操作也失败了怎么办?
- Saga执行过程中服务重启如何恢复?
- 如何设计Saga的测试策略?
💡 题目3:如何选择同步调用vs异步消息?
问题描述: 在微服务架构中,服务间通信可以使用同步RPC或异步消息队列。在电商系统中,如何选择合适的通信方式?
答案:
问题分析: 同步vs异步的核心考量:
- 业务语义:是否需要立即返回结果
- 性能要求:延迟vs吞吐量
- 可靠性:是否允许消息丢失
- 复杂度:实现和运维成本
方案一:全部使用同步RPC
适用场景:
- 查询操作(查询订单详情、商品信息)
- 强实时性要求(下单时检查库存)
- 需要立即返回结果(用户等待响应)
优点:
- 实现简单
- 调用链路清晰
- 易于调试
缺点:
- 性能瓶颈(串行调用)
- 可用性差(下游故障影响上游)
- 难以削峰
方案二:全部使用异步消息
适用场景:
- 通知类操作(发送短信、邮件)
- 可延迟处理(数据同步、报表生成)
- 需要削峰填谷(秒杀、大促)
优点:
- 解耦(服务独立)
- 削峰(消息堆积)
- 高吞吐
缺点:
- 最终一致性
- 消息丢失风险
- 调试困难
方案三:混合使用(推荐)
决策矩阵:
| 场景 | 通信方式 | 理由 |
|---|---|---|
| 查询商品详情 | 同步RPC | 需要立即返回 |
| 下单扣减库存 | 同步RPC | 需要立即知道结果 |
| 订单支付成功→通知物流 | 异步消息 | 不需要立即处理 |
| 订单支付成功→发送短信 | 异步消息 | 允许延迟 |
| 商品信息变更→更新搜索 | 异步消息 | 最终一致即可 |
| 计算订单金额 | 同步RPC | 需要立即返回金额 |
| 订单创建→更新统计报表 | 异步消息 | 非实时 |
决策原则:
- 用户在等待:使用同步(如下单、支付、查询)
- 用户不在等待:使用异步(如通知、数据同步)
- 强一致性:使用同步(如扣款、扣库存)
- 最终一致性:使用异步(如积分、优惠券)
- 高并发:优先异步(如秒杀、大促)
优点:
- 平衡性能和一致性
- 灵活应对不同场景
- 整体架构合理
缺点:
- 需要维护两套通信机制
- 团队需要理解选择原则
方案对比:
| 维度 | 全同步 | 全异步 | 混合 |
|---|---|---|---|
| 实时性 | ★★★★★ | ★★☆☆☆ | ★★★★☆ |
| 吞吐量 | ★★☆☆☆ | ★★★★★ | ★★★★☆ |
| 可用性 | ★★☆☆☆ | ★★★★★ | ★★★★☆ |
| 复杂度 | ★★★★☆ | ★★☆☆☆ | ★★★☆☆ |
推荐方案: 采用混合模式,根据场景选择合适的通信方式。
实施要点:
-
同步调用优化:
- 设置合理超时(如3秒)
- 使用断路器防止雪崩
- 重要接口设置重试机制
- 监控调用成功率和延迟
-
异步消息优化:
- 使用本地消息表保证可靠性
- 消费端幂等处理
- 死信队列处理失败消息
- 监控消息积压和消费延迟
-
场景识别技巧:
- 问:用户是否在等待结果?
- 问:失败了是否需要立即知道?
- 问:是否需要强一致性?
- 问:并发量有多大?
-
混合调用模式:
示例:订单支付成功后 同步: - 更新订单状态(用户需要立即看到) - 扣减库存(强一致性) 异步: - 通知物流(可延迟) - 发送短信(可延迟) - 更新报表(可延迟) - 赠送积分(最终一致) -
降级策略:
- 异步消息:队列满时拒绝接入
- 同步调用:超时降级(返回默认值或缓存)
延伸思考:
- 如何处理异步消息丢失的情况?
- 同步调用超时后如何判断是否成功?
- 如何在同步和异步之间切换(如异步改同步)?
🔧 题目4:分布式事务的几种实现方案对比
问题描述: 请对比2PC、TCC、Saga、本地消息表这几种分布式事务方案,并说明在电商系统中各自的适用场景。
答案:
问题分析: 分布式事务方案的核心差异:
- 一致性强度(强一致 vs 最终一致)
- 性能开销(锁定时间、网络开销)
- 实现复杂度(接口数量、补偿逻辑)
- 适用场景(金融 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)
延伸思考:
- 为什么电商系统很少用2PC?
- TCC的Try阶段如何设计才能保证性能?
- Saga的补偿操作如何保证一定成功?
📊 题目5:设计事件驱动架构
问题描述: 电商系统需要实现事件驱动架构,当订单状态变更时,自动触发物流、消息通知、数据统计等下游操作。请设计事件驱动方案。
答案:
问题分析: 事件驱动架构的核心挑战:
- 如何设计领域事件
- 如何保证事件不丢失
- 如何处理事件顺序性
- 如何避免事件风暴
方案一:基于数据库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的系统
- 需要强可靠性的场景
方案对比:
| 维度 | CDC | Outbox | 事务消息 |
|---|---|---|---|
| 事件语义 | ★★☆☆☆ | ★★★★★ | ★★★★☆ |
| 可靠性 | ★★★★★ | ★★★★★ | ★★★★★ |
| 侵入性 | ★★★★★ | ★★★☆☆ | ★★★☆☆ |
| 实施难度 | ★★★★☆ | ★★★☆☆ | ★★★★☆ |
推荐方案: 对于电商系统,推荐Outbox模式。
实施要点:
-
事件设计原则:
- 事件名称:过去时(OrderPaid、OrderShipped)
- 包含完整上下文(避免下游再查询)
- 不可变(发出后不能修改)
- 幂等性(包含event_id)
-
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 ); -
事件发布器优化:
- 批量读取(每次100条)
- 批量发送(提高吞吐)
- 失败重试(指数退避)
- 定期清理已发送事件(保留7天)
-
消费端幂等:
消费端维护已处理事件表: CREATE TABLE processed_events ( event_id VARCHAR(64) PRIMARY KEY, processed_at TIMESTAMP ); 处理逻辑: 1. 检查event_id是否已处理 2. 如果已处理,直接返回 3. 执行业务逻辑 4. 记录event_id到processed_events -
监控告警:
- Outbox待发送事件数
- 事件发送失败率
- 消费延迟
延伸思考:
- 如何保证事件的顺序性(同一订单的多个事件)?
- 如何处理事件风暴(大量事件同时发布)?
- 事件版本如何管理(EventV1、EventV2)?
🔧 题目6:如何保证消息的可靠投递?
问题描述: 在消息队列(Kafka/RocketMQ)中,如何保证消息从生产者到消费者的可靠投递,不丢失、不重复、不乱序?
答案:
问题分析: 消息可靠性的三个维度:
- 生产端:如何保证消息发送成功
- 存储端:如何保证消息不丢失
- 消费端:如何保证消息处理成功
方案一: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 + 业务幂等。
实施要点:
-
生产端可靠性:
// 同步发送(等待确认) producer.send(record).get(3, TimeUnit.SECONDS) // 配置 acks=all // 所有副本确认 retries=3 // 重试3次 max.in.flight.requests.per.connection=1 // 保证顺序 -
MQ端可靠性:
Kafka配置: - replication.factor=3 // 3副本 - min.insync.replicas=2 // 至少2个副本确认 - unclean.leader.election.enable=false // 禁止非ISR副本成为leader RocketMQ配置: - flushDiskType=SYNC_FLUSH // 同步刷盘 -
消费端可靠性:
// 手动提交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) } } -
幂等性设计:
方案1:基于唯一键 - 消息包含messageId - 消费端用messageId去重(Redis/DB) 方案2:基于业务唯一键 - 订单号、支付流水号等 - 数据库唯一索引约束 方案3:基于版本号 - 数据包含版本号 - 更新时检查版本号(乐观锁) -
顺序性保证:
发送端: - 同一订单的消息发到同一分区(按orderId hash) - max.in.flight.requests=1(保证分区内有序) 消费端: - 单线程消费同一分区 - 或使用版本号检查顺序
延伸思考:
- 如果消费失败,消息应该重试几次?
- 如何处理消息积压问题?
- 如何实现消息的延迟投递?
💡 题目7:幂等性设计的最佳实践
问题描述: 在分布式系统中,由于网络重试、消息重复等原因,同一个请求可能被处理多次。如何设计幂等机制,保证重复请求不会产生副作用?
答案:
问题分析: 幂等性的核心挑战:
- 如何识别重复请求
- 如何防止并发重复处理
- 如何设计幂等键
- 幂等状态如何存储和清理
方案一:基于唯一请求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
实施要点:
-
幂等键设计原则:
- 唯一性:能唯一标识一次操作
- 稳定性:多次请求幂等键相同
- 业务相关:优先用业务键(orderId)而非技术键(requestId)
-
幂等粒度:
粗粒度(操作级): - 幂等键:orderId - 含义:同一订单不能重复创建 细粒度(步骤级): - 幂等键:orderId + operationType(如 "order123:pay") - 含义:同一订单的支付操作不能重复 -
幂等状态清理:
Redis存储: - 设置过期时间(24小时) - 定期清理过期数据 数据库存储: - 不需要清理(利用业务唯一键) -
并发安全:
分布式锁: 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=? -
幂等测试:
测试用例: 1. 相同参数调用2次,验证第2次返回相同结果 2. 并发调用2次,验证只有1次生效 3. 延迟重试,验证中间状态不影响幂等性
延伸思考:
- 如何设计支付接口的幂等性?
- 消息队列消费端如何保证幂等性?
- 幂等状态如何跨服务共享?
📊 题目8:设计数据最终一致性的对账补偿机制
问题描述: 在分布式系统中,虽然采用了Saga等最终一致性方案,但仍可能因为网络、重试等原因导致数据不一致。如何设计对账补偿机制?
答案:
问题分析: 对账补偿的核心挑战:
- 如何发现数据不一致
- 如何定位不一致的根本原因
- 如何自动补偿修复
- 如何避免补偿引入新问题
方案一:实时对账
核心思想: 在关键操作后立即对账,发现问题立即修复。
设计:
订单支付成功后:
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:订单-支付对账 - 订单状态=PAID ⇔ 存在成功的支付记录 - 订单金额 = 支付金额 维度2:订单-库存对账 - 订单商品 ⇔ 库存扣减记录 - 订单数量 = 库存扣减数量 维度3:订单-物流对账 - 订单状态=SHIPPED ⇔ 存在物流单 维度4:财务对账 - 订单收入 = 支付收入 - 退款 -
补偿策略:
自动补偿(低风险): - 订单已支付,库存未扣减 → 自动扣减库存 - 订单已取消,库存未释放 → 自动释放库存 人工介入(高风险): - 订单金额与支付金额不一致 → 人工审核 - 库存为负数 → 人工调整 - 重复支付 → 人工退款 -
对账任务调度:
实时对账: - 支付成功后:立即对账订单-支付 分钟级对账: - 每5分钟:对账最近10分钟的订单 小时级对账: - 每小时:对账最近2小时的订单 日级对账: - 每天凌晨:对账前一天所有订单 - 生成对账报表 -
补偿幂等性:
补偿操作必须幂等: - 记录补偿历史(避免重复补偿) - 使用业务唯一键 - 状态机保证(只能从错误状态补偿到正确状态) -
监控告警:
指标: - 对账差异数量 - 自动补偿成功率 - 人工处理待办数量 告警: - 差异数量超过阈值 - 自动补偿失败 - 关键差异(金额不一致)
延伸思考:
- 如果对账也失败了(如查询超时),如何处理?
- 补偿操作失败后如何处理?
- 如何设计对账结果的可视化展示?
🔧 题目9:如何处理分布式系统的时钟问题?
问题描述: 在分布式系统中,不同服务器的时钟可能不同步,导致时间戳不一致、时序错误等问题。如何处理分布式系统的时钟问题?
答案:
问题分析: 分布式时钟的核心挑战:
- 时钟漂移:不同机器时钟不一致
- 时序依赖:如何判断事件先后顺序
- 超时判断:如何准确计算超时
- 数据时效:如何判断数据是否过期
方案一:使用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同步 + 避免依赖绝对时间。
实施要点:
-
NTP基础设施:
- 部署内网NTP服务器 - 所有应用服务器同步内网NTP - 监控时钟偏移(超过100ms告警) - 禁止手动修改系统时间 -
避免依赖绝对时间:
错误示例: // 判断订单是否超时(依赖绝对时间) if (now() - order.createdAt > 30分钟) { cancelOrder() } 问题:如果时钟回拨,可能误判 正确示例: // 使用相对时间或逻辑状态 if (order.status == PENDING && order.createdAt < now() - 30分钟) { // 且使用定时任务扫描(而非实时判断) cancelOrder() } -
处理时钟回拨:
生成ID时(如雪花ID): if (currentTime < lastTime) { // 时钟回拨,等待追上 wait(lastTime - currentTime) } 或使用单调递增ID: // 不依赖时间,只保证递增 nextId = atomicIncrement() -
使用逻辑版本号:
数据表: 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分钟"的数据(避免时钟误差) SELECT * FROM orders WHERE created_at < NOW() - INTERVAL 5 MINUTE AND created_at >= NOW() - INTERVAL 1 HOUR
延伸思考:
- 如何设计分布式系统的全局唯一ID(考虑时钟回拨)?
- 如何在不同时区的数据中心部署系统?
- 时钟跳跃(突然快进)如何处理?
💡 题目10:微服务间的版本兼容性设计
问题描述: 微服务架构下,服务A调用服务B的接口。当服务B升级时,如何保证向后兼容,不影响服务A?请设计API版本管理方案。
答案:
问题分析: API版本兼容的核心挑战:
- 如何在不影响老版本的情况下升级
- 如何管理多个版本的共存
- 如何平滑下线老版本
- 如何处理数据结构变更
方案一: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版本控制。
实施要点:
-
API设计规范:
必须遵守: - 新增字段必须可选 - 新增字段必须有默认值 - 不删除现有字段 - 不修改字段类型 - 不修改字段语义 字段弃用流程: 1. 标记@Deprecated,文档说明 2. 观察调用量,确认无人使用 3. 至少保留3个月 4. 下个大版本时删除 -
Protobuf向后兼容:
message Order { string order_id = 1; double amount = 2; // V2新增字段 double discount_amount = 3; string payment_method = 4; // 不要重用字段编号! // reserved 5; // 如果删除字段5,标记为reserved } -
版本协商机制:
客户端声明支持的版本: 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); // 降级返回 } -
版本监控:
监控指标: - 各版本API调用量 - 废弃API的调用量 - 客户端版本分布 告警: - 废弃API仍有调用 - 客户端版本过低 -
契约测试:
测试用例: 1. V1客户端调用V2服务(向后兼容) 2. V2客户端调用V1服务(新字段有默认值) 3. 并发调用不同版本 4. 字段缺失时的降级处理
延伸思考:
- 如何处理数据库表结构的版本兼容?
- 如何平滑下线一个旧版本API?
- gRPC/Protobuf的版本兼容如何保证?
第二部分:商品与库存管理(43题)
2.1 商品中心系统(16题)
📊 题目1:设计支持多品类的SPU/SKU数据模型
问题描述: 电商平台需要支持实物商品(服装、3C)、虚拟商品(充值卡、会员)、服务类商品(保险、课程)。如何设计一个统一且可扩展的商品数据模型?
答案:
问题分析: 多品类商品模型的核心挑战:
- 不同品类属性差异巨大(服装有尺码颜色,充值卡有卡密)
- 需要支持灵活的属性扩展,避免频繁加字段
- 查询性能要求高(详情页、列表页高并发)
- 需要支持类目体系和属性继承
方案一: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字段查询能力有限
- 需要应用层解析和校验
- 不同数据库支持程度不同
方案三:混合模式(推荐)
核心设计:
- 主表存储通用字段:product_core(id, spu, name, category, status)
- 类目模板定义属性规范:attribute_meta(属性元数据、类型、校验规则)
- 分层存储:
- product_common_attr:高频查询字段(品牌、价格区间)
- product_ext_attr:JSONB,低频字段
- product_spec:SKU规格,单独表
- 搜索侧异步构建宽表:ES文档包含所有筛选字段
数据流:
- 写入:商品创建 → 按模板校验 → 分表存储 → 事件发布 → ES同步
- 读取详情页:主表+扩展表(缓存)
- 读取列表页:直接查ES
- 后台管理:全量字段(可接受慢查询)
优点:
- 扩展性强
- 查询性能好
- 支持复杂筛选(通过ES)
- 核心字段有索引
缺点:
- 架构复杂度中等
- 需要维护ES同步
- 最终一致性
方案对比:
| 维度 | EAV | 宽表+JSON | 混合模式 |
|---|---|---|---|
| 扩展性 | ★★★★★ | ★★★★☆ | ★★★★★ |
| 查询性能 | ★★☆☆☆ | ★★★★☆ | ★★★★★ |
| 开发复杂度 | ★★★☆☆ | ★★★★☆ | ★★★☆☆ |
| 类型安全 | ★★☆☆☆ | ★★★☆☆ | ★★★★☆ |
推荐方案: 采用混合模式。
实施要点:
- 核心字段晋升机制:高频查询字段从JSON移到固定列
- JSONB索引:PostgreSQL建立GIN索引
- ES映射模板:自动从类目模板生成
- 缓存策略:L1进程内 + L2 Redis,TTL分层设置
- 属性校验:类目模板定义规则,运行时校验
虚拟商品特殊处理:
- 充值卡:卡密存储加密、核销记录独立表
- 会员服务:有效期、权益包用JSON存储
- 服务类:预约时间、服务人员信息扩展字段
延伸思考:
- 如何处理类目属性变更(模板升级)?
- 历史订单中的商品快照如何存储?
- 跨类目搜索时如何统一属性映射?
🔧 题目2:商品详情页的缓存架构设计
问题描述: 商品详情页是电商系统访问量最大的页面,QPS可达百万级。请设计商品详情页的缓存架构,保证高性能和数据一致性。
答案:
问题分析: 详情页缓存的核心挑战:
- 流量巨大,需要多级缓存
- 数据来源多(商品、价格、库存、营销),聚合复杂
- 数据更新频繁,缓存一致性难保证
- 热点商品流量集中
方案一:纯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 | 多级缓存 | 缓存+预热 |
|---|---|---|---|
| 性能 | ★★★★★ | ★★★★☆ | ★★★★★ |
| 实时性 | ★★☆☆☆ | ★★★★☆ | ★★★★☆ |
| 个性化 | ★☆☆☆☆ | ★★★★★ | ★★★★★ |
| 复杂度 | ★★★★★ | ★★★☆☆ | ★★☆☆☆ |
推荐方案: 采用多级缓存+热点预热的组合。
实施要点:
-
缓存分层:
L1(本地缓存): - 容量:1000条 - TTL:30秒 - 淘汰策略:LRU - 适用:超热门商品(TOP 100) L2(Redis): - 容量:100万条 - TTL:5分钟 - 集群部署:主从+哨兵 - 适用:热门+普通商品 L3(数据库): - 全量数据 - 读写分离 -
缓存键设计:
方案1:不带版本号 Key: product:detail:{productId} Value: JSON 更新:商品变更时主动删除key 方案2:带版本号(推荐) Key: product:detail:{productId}:{version} Value: JSON 更新:版本号+1,旧key自然过期 -
缓存更新策略:
Cache Aside模式: 1. 读取:先查缓存,未命中再查DB,写入缓存 2. 更新:先更新DB,再删除缓存 Write Through模式: 1. 更新:同时更新DB和缓存 2. 读取:直接读缓存 -
热点治理:
识别热点: - 实时统计访问频率(滑动窗口) - 超过阈值(如10000 QPS)标记为热点 热点处理: - 本地缓存延长TTL(30秒 → 5分钟) - Redis分片存储(product:123:1, product:123:2...) - 限流保护(单商品限流) -
缓存穿透/击穿/雪崩:
穿透(查询不存在的数据): - 布隆过滤器预判 - 空值缓存(TTL短,如1分钟) 击穿(热点key过期): - 互斥锁(只有一个请求回源) - 热点key永不过期(后台异步更新) 雪崩(大量key同时过期): - TTL加随机值(5分钟±30秒) - 缓存预热 - 降级方案(返回旧数据)
延伸思考:
- 缓存和数据库数据不一致如何处理?
- 如何设计缓存的监控指标?
- 大促时如何做缓存容量规划?
💡 题目3:如何解决商品信息变更后搜索不一致问题?
问题描述: 运营修改了商品标题和价格,但搜索结果中仍然显示旧信息。这是典型的最终一致性问题。如何设计商品到搜索的数据同步方案?
答案:
问题分析: 商品搜索一致性的核心挑战:
- 数据变更频繁(价格调整、库存变化)
- 搜索索引构建有延迟
- 用户期望实时看到最新信息
- 大量商品同步对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增量同步
- 作为对账的补充
优点:
- 接近实时
- 有补偿机制
缺点:
- 双写失败处理复杂
- 两个数据源可能不一致
- 实现复杂
方案对比:
| 维度 | 实时同步 | 异步同步 | 双写+对账 |
|---|---|---|---|
| 实时性 | ★★★★★ | ★★★★☆ | ★★★★☆ |
| 系统解耦 | ★★☆☆☆ | ★★★★★ | ★★★☆☆ |
| 一致性保证 | ★★★★☆ | ★★★★★ | ★★★★★ |
| 实施难度 | ★★★★☆ | ★★★☆☆ | ★★☆☆☆ |
推荐方案: 采用异步同步+对账。
实施要点:
-
事件设计:
ProductCreated:商品创建 ProductUpdated:商品信息变更(title、desc、images) ProductPriceChanged:价格变更 ProductStatusChanged:上下架 ProductDeleted:删除 -
同步Worker设计:
消费逻辑: 1. 从Kafka消费ProductUpdated事件 2. 根据productId查询完整商品信息 3. 构建ES文档 4. 批量更新ES(bulk API,提高吞吐) 5. 提交offset 批量优化: - 攒批:100条或1秒批量提交 - 去重:同一商品多次变更只保留最新 - 合并:多个字段变更合并为一次更新 -
幂等处理:
ES文档设计: { "productId": "123", "title": "iPhone 15", "price": 5999, "version": 10, // 版本号 "updatedAt": 1679800000 } 更新逻辑: if (event.version > doc.version) { update ES } else { skip (乱序消息) } -
对账机制:
对账任务(每小时): SELECT product_id, version, updated_at FROM products WHERE updated_at >= NOW() - INTERVAL 2 HOUR 对每个商品: - 查询ES中的version - 如果MySQL.version > ES.version - 发送补偿事件到Kafka -
监控告警:
指标: - 同步延迟(消息产生到ES更新完成的时间) - 失败率(同步失败的比例) - 对账差异数(MySQL和ES不一致的商品数) 告警: - 同步延迟 > 10秒 - 失败率 > 1% - 对账差异 > 100条
延伸思考:
- 如果ES集群故障,搜索如何降级?
- 商品删除后ES索引如何处理?
- 大批量商品导入如何优化ES同步性能?
🔧 题目3 扩展:直接订阅 Binlog 同步 ES 的弊端是什么?如果不同变更之间存在依赖关系,应该怎么处理?
问题描述: 一些电商系统会通过 Binlog / CDC 捕获商品表变更,然后由 ES Synchronizer 消费消息并更新搜索索引。例如商品主表、SKU 表、Offer 表、类目映射表、供应商映射表发生变更后,同步服务根据表名和字段变化去更新 ES 文档。这种方式有什么弊端?如果一个 ES 文档依赖多张表,不同变更之间存在先后关系和依赖关系,应该如何设计?
答案:
问题分析:
直接订阅 Binlog 同步 ES 的本质是:
数据库表级变化
→ 触发 ES 文档更新
而商品搜索索引的本质通常是:
多张业务表
→ 聚合成一个商品搜索宽文档
两者粒度不一致。Binlog 看到的是“某张表某一行变了”,ES 需要的是“某个商品聚合视图应该变成什么样”。这会带来几个典型问题:
- 业务语义弱:Binlog 只表达
insert/update/delete,不表达ProductPublished、ProductOffline、OfferChanged、RefundRuleChanged。 - 强依赖表结构:字段新增、删除、顺序变化、JSON 结构变化,都可能影响同步逻辑。
- 跨表依赖复杂:一个 ES 商品文档可能依赖 item、spu、sku、offer、resource、category、stock config、refund rule 等多张表。
- 顺序不稳定:同一业务发布可能写多张表,Binlog 事件到达不同 consumer 时不一定按业务语义有序。
- 并发覆盖风险:两个表变更同时 patch 同一个 ES doc,可能出现后写基于旧 doc 覆盖前写结果。
- 版本语义不足:Binlog timestamp 或 position 不等价于商品业务版本,难以判断旧事件是否应该覆盖新事件。
- 失败补偿困难:失败消息只知道表和字段,不一定知道影响哪个商品、哪个发布版本、是否可以安全重建。
典型错误做法:按每条 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 Patch | Dirty Doc 重建 | 业务事件 + Outbox |
|---|---|---|---|
| 实现成本 | 低 | 中 | 中高 |
| 业务语义 | 弱 | 中 | 强 |
| 跨表依赖处理 | 差 | 好 | 很好 |
| 防并发覆盖 | 差 | 好 | 很好 |
| 防乱序能力 | 弱 | 中 | 强 |
| 对表结构耦合 | 强 | 中 | 弱 |
| 故障补偿 | 弱 | 好 | 很好 |
| 适合场景 | 简单索引 | 多表聚合索引 | 核心商品发布链路 |
推荐方案:
短期采用 Binlog → Dirty Doc Queue → Full Rebuild ES Doc,中长期演进到 业务事件 + Outbox + 商品快照重建 ES。
推荐落地路径:
-
定义 ES doc 聚合根
product index: doc_id = item_id carrier index: doc_id = carrier_id event index: doc_id = event_id -
维护依赖映射
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 -
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) -
Index Worker 串行处理同一个 doc
SELECT * FROM es_sync_dirty_doc WHERE status = 'PENDING' ORDER BY updated_at ASC LIMIT 100;同一个
doc_type + doc_id通过唯一键合并,Worker 抢占后重建完整文档。 -
重建时读取 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 -
写 ES 带版本
商品类索引用
publish_version;没有业务版本的对象至少使用updated_at、rebuild_seq或source_version。 -
失败进入 DLQ 和补偿
失败时记录:
doc_type doc_id source_table error_code retry_count next_retry_at -
定期 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 里猜商品到底发生了什么。
延伸思考:
- 如何设计
resolveAffectedDocIds,避免一张配置表变更导致全量商品都被标脏? - ES 写入使用 external version 有什么限制?
- Dirty Queue 堆积时,如何区分高优先级商品和普通商品?
- 全量重建和增量同步同时发生时,如何避免旧增量写到新索引?
📊 题目4:设计商品类目体系和属性管理
问题描述: 电商平台有上千个类目(如手机、服装、食品),每个类目有不同的属性(手机有内存、颜色,服装有尺码、材质)。如何设计类目体系和属性管理系统?
答案:
问题分析: 类目属性管理的核心挑战:
- 类目层级深(最多5-6级)
- 属性类型多样(文本、数值、枚举、多选)
- 属性继承和覆盖
- 属性校验规则复杂
方案一:树形类目+固定属性
核心思想: 类目按树形组织,每个类目预定义固定属性。
设计:
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查询能力有限
- 属性分组需要人工维护
方案对比:
| 维度 | 固定属性 | 动态模板 | 分组+扩展 |
|---|---|---|---|
| 灵活性 | ★★☆☆☆ | ★★★★★ | ★★★★☆ |
| 性能 | ★★★★★ | ★★★☆☆ | ★★★★☆ |
| 实施难度 | ★★★★★ | ★★☆☆☆ | ★★★☆☆ |
| 可维护性 | ★★★☆☆ | ★★★★☆ | ★★★★☆ |
推荐方案: 采用动态属性模板+继承。
实施要点:
-
类目层级设计:
建议:不超过4级 L1:大类(手机、服装、食品) L2:中类(智能手机、T恤、零食) L3:小类(iPhone、圆领T恤、膨化食品) L4:细分类(iPhone 15系列) -
属性校验:
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()); // 类型、范围、枚举值校验 } } -
属性搜索支持:
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"} } } } } } -
属性演进:
新增属性: 1. 在attribute_meta表添加属性定义 2. 关联到类目模板 3. 存量商品渐进补齐(批量任务或人工) 弃用属性: 1. 标记为deprecated 2. 新商品不展示该属性 3. 老商品保留(不删除) -
多语言支持:
attribute_i18n(属性国际化) ├── attribute_id ├── locale(zh_CN/en_US) ├── name └── description
延伸思考:
- 如何处理类目合并和拆分?
- 属性过多时如何优化详情页加载性能?
- 跨类目搜索时属性如何映射?
🔧 题目5:商品图片的存储和CDN方案
问题描述: 电商平台商品图片数量巨大(百万级),每天上传图片数万张。如何设计图片存储和CDN方案,保证加载速度和成本可控?
答案:
问题分析: 图片存储的核心挑战:
- 存储成本高(TB级数据)
- 访问量大(详情页、列表页都需要图片)
- 需要支持多种尺寸(缩略图、中图、大图)
- 图片上传和审核流程
方案一:自建存储+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。
实施要点:
-
图片命名规范:
{bucket}/{年}/{月}/{日}/{category}/{uuid}.{ext} 示例: product-images/2026/04/18/phone/550e8400-e29b-41d4-a716-446655440000.jpg -
多尺寸策略:
方案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 -
CDN配置:
缓存策略: - 原图:缓存7天 - 缩略图:缓存30天 - 回源策略:304协商缓存 防盗链: - Referer白名单 - 签名URL(临时访问) - IP黑名单 -
图片审核:
流程: 1. 上传到临时bucket 2. 触发审核(内容安全API) 3. 审核通过 → 移动到正式bucket 4. 审核不通过 → 标记为违规,删除 审核内容: - 色情识别 - 暴恐识别 - 二维码识别 - 文字OCR+敏感词 -
性能优化:
图片格式: - 优先WebP(体积小30%) - 降级JPEG/PNG(老浏览器) 懒加载: - 首屏图片优先加载 - 下方图片懒加载 - 占位图优化体验 压缩: - JPEG质量80%(肉眼无感知) - PNG使用TinyPNG压缩
延伸思考:
- 如何防止图片盗链?
- 商家上传违规图片如何处理?
- 图片存储成本如何优化?
💡 题目6:虚拟商品vs实物商品的设计差异
问题描述: 实物商品需要物流配送,虚拟商品(如充值卡、会员)是即时发货。两者在系统设计上有哪些差异?
答案:
问题分析: 虚拟商品的核心差异:
- 无需物流,履约方式不同
- 库存是卡密池,不是物理库存
- 发货是推送卡密,不是创建运单
- 支持自动发货
方案一:统一建模,类型区分
核心思想: 实物和虚拟商品共用一套模型,通过类型字段区分。
设计:
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. 标记卡密为已分配
优点:
- 订单模型统一
- 支持混合订单
- 履约解耦
缺点:
- 履约系统复杂度增加
方案对比:
| 维度 | 统一建模 | 拆分系统 | 统一订单+差异履约 |
|---|---|---|---|
| 模型清晰度 | ★★★☆☆ | ★★★★★ | ★★★★☆ |
| 混合订单 | ★★★★★ | ★★☆☆☆ | ★★★★★ |
| 实施难度 | ★★★★☆ | ★★☆☆☆ | ★★★☆☆ |
| 用户体验 | ★★★★★ | ★★★☆☆ | ★★★★★ |
推荐方案: 采用统一订单+差异化履约。
实施要点:
-
虚拟商品特殊字段:
virtual_product_ext ├── product_id ├── card_type(MOBILE_CHARGE/VIP_CARD/GAME_COIN) ├── face_value(面值) ├── validity_days(有效天数) └── auto_deliver(是否自动发货) -
卡密池设计:
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) -
自动发货:
触发条件: - 支付成功事件 - 商品类型=虚拟 - auto_deliver=true 发货流程: 1. 从卡密池分配卡密 2. 更新订单状态=COMPLETED 3. 推送卡密给用户(短信/App推送) 4. 记录发货日志 -
卡密补货:
监控: - 可用卡密数量 < 1000 → 告警 补货: - 供应商批量导入 - 或系统自动生成(如游戏币) -
安全控制:
- 卡密加密存储(AES) - 卡密脱敏展示(只显示后4位) - 限制查询频率(防止爬虫) - 异常查询告警
延伸思考:
- 如何防止卡密被盗刷?
- 卡密分配失败如何处理?
- 虚拟商品是否需要支持退款?
📊 题目7:商品上架流程的工作流设计
问题描述: 商品从创建到上架需要经过多个环节(信息录入、图片上传、价格设置、审核)。请设计商品上架的工作流系统。
答案:
问题分析: 商品上架工作流的核心挑战:
- 流程长,涉及多个环节和角色
- 需要支持驳回和重新提交
- 审核规则复杂(机审+人审)
- 大批量商品上架性能
方案一:状态机模式
核心思想: 商品的状态流转按状态机管理。
状态定义:
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可视化
- 需要自己维护
方案对比:
| 维度 | 状态机 | 工作流引擎 | 轻量引擎 |
|---|---|---|---|
| 实施难度 | ★★★★★ | ★★☆☆☆ | ★★★★☆ |
| 流程表达力 | ★★☆☆☆ | ★★★★★ | ★★★★☆ |
| 维护成本 | ★★★★☆ | ★★★☆☆ | ★★★★☆ |
| 适用场景 | 简单流程 | 复杂流程 | 中等流程 |
推荐方案: 对于商品上架,推荐轻量级流程引擎。
实施要点:
-
审核规则设计:
机器审核: - 图片审核(色情、暴恐) - 标题敏感词检测 - 价格合理性检测(异常低价) - 类目属性完整性检测 人工审核: - 机器审核不通过 → 必须人审 - 高风险类目(药品、食品) → 必须人审 - 新商家首批商品 → 必须人审 - 其他商品 → 机审通过直接上架 -
批量上架优化:
单个上架: - 提交 → 立即审核 → 立即上架 批量上架: - 提交100个商品 - 异步审核(队列) - 审核完成后批量回调 - 生成审核报告 -
驳回重审:
驳回原因分类: - 图片问题(重新上传图片即可) - 价格问题(重新设置价格) - 类目错误(重新选择类目,属性重填) 重审流程: - 修改后自动重新提审 - 或需要人工重新提交 -
工作流监控:
指标: - 待审核商品数量 - 平均审核时长 - 审核通过率 - 驳回原因分布 告警: - 待审核积压 > 1000 - 审核通过率 < 80%
延伸思考:
- 如何设计商品的定时上架功能?
- 批量上架如何保证事务性?
- 审核规则如何动态配置?
🔧 题目8:如何支持商品的多规格选择(颜色、尺码等)?
问题描述: 服装类商品有多个规格(颜色、尺码),用户需要先选择规格再下单。如何设计商品规格和SKU的选择逻辑?
答案:
问题分析: 多规格选择的核心挑战:
- 规格组合爆炸(3个颜色×5个尺码=15个SKU)
- 无效组合处理(某颜色没有某尺码)
- 库存关联(每个SKU独立库存)
- 价格差异(不同规格价格不同)
方案一:预生成所有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. 加载所有有效SKU 2. 构建规格树 3. 根据已选规格,计算可选项 4. 禁用无货或无效选项 示例(用户已选"黑色"): 可选尺码 = 筛选(所有SKU, color="黑色" && stock>0) 禁用尺码 = 筛选(所有SKU, color="黑色" && stock=0) -
规格约束表达:
方案A:黑名单 "不存在黑色XL" 方案B:白名单 "只有这些组合:黑色+M, 黑色+L, 白色+S, ..." 推荐:黑名单(灵活) -
SKU图片:
商品主图:展示默认规格 规格图:每个颜色独立图片 用户选择颜色 → 切换主图 -
性能优化:
缓存: - 缓存商品的所有SKU(减少查询) - 缓存规格树(减少计算) 压缩: - 规格数据压缩传输
延伸思考:
- 如何支持规格变更(新增颜色、下架尺码)?
- 用户加购时记录SKU还是规格组合?
- 如何优化规格选择的用户体验?
💡 题目9:商品快照在订单中的应用
问题描述: 用户下单后,商家可能修改商品标题、价格、图片。为了避免纠纷,需要在订单中保存商品快照。请设计商品快照方案。
答案:
问题分析: 商品快照的核心挑战:
- 快照内容:保存哪些字段
- 存储成本:每个订单都存快照,数据量大
- 快照时机:下单时还是支付时
- 快照更新:商品变更后订单快照是否更新
方案一:订单表冗余字段
核心思想: 在订单明细表中冗余商品关键字段。
设计:
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. 如果商品已删除,快照为空
优点:
- 存储成本低
- 按需生成
缺点:
- 延迟生成可能获取不到准确信息
- 商品删除后无法生成
方案对比:
| 维度 | 冗余字段 | 独立快照表 | 按需快照 |
|---|---|---|---|
| 快照完整性 | ★★☆☆☆ | ★★★★★ | ★★★☆☆ |
| 存储成本 | ★★★☆☆ | ★★☆☆☆ | ★★★★★ |
| 查询性能 | ★★★★★ | ★★★★☆ | ★★★☆☆ |
| 准确性 | ★★★★★ | ★★★★★ | ★★★☆☆ |
推荐方案: 采用独立快照表+去重优化。
实施要点:
-
快照内容设计:
必须包含: - 商品标题、主图 - SKU规格、价格 - 品牌、类目 可选包含: - 商品详情图(占用空间大) - 营销信息(优惠券、满减) - 服务承诺(七天无理由退货) -
快照去重:
生成流程: 1. 计算快照内容的MD5: content_hash 2. 查询是否已存在相同hash的快照 3. 如果存在,复用snapshot_id 4. 如果不存在,创建新快照 收益: - 相同商品的订单共享快照 - 存储成本降低50%+ -
快照压缩:
JSON压缩: - 使用gzip压缩snapshot_data - 读取时解压 字段裁剪: - 只保留关键字段 - 详情图等大字段不保存 -
快照过期清理:
策略: - 订单完成后保留2年(法律要求) - 2年后匿名化处理(删除用户信息,保留快照) - 5年后归档到对象存储 -
快照版本化:
快照schema版本: V1: {title, price, image} V2: {title, price, images[], brand, specs} 读取时兼容: if (snapshot.version == 1) { return convertV1ToV2(snapshot) }
延伸思考:
- 商品快照如何支持营销信息(如“限时折扣“)?
- 快照生成失败如何处理?
- 如何设计快照的版本兼容?
📊 题目10:设计商品推荐系统的架构
问题描述: 电商平台需要在详情页、列表页、首页展示个性化推荐商品。请设计商品推荐系统的架构。
答案:
问题分析: 推荐系统的核心挑战:
- 推荐算法复杂(协同过滤、深度学习)
- 实时性要求(用户行为实时影响推荐)
- 冷启动问题(新用户、新商品)
- 性能要求高(毫秒级响应)
方案一:基于规则的推荐
核心思想: 使用人工配置的规则进行推荐。
规则示例:
规则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获取实时推荐结果
优点:
- 实时性好(秒级)
- 支持个性化
- 反馈快
缺点:
- 架构复杂
- 成本高
- 算法受限(不能用复杂模型)
方案对比:
| 维度 | 规则推荐 | 离线+在线 | 实时推荐 |
|---|---|---|---|
| 推荐效果 | ★★☆☆☆ | ★★★★☆ | ★★★★★ |
| 实时性 | ★★★★★ | ★★☆☆☆ | ★★★★★ |
| 实施难度 | ★★★★★ | ★★★☆☆ | ★★☆☆☆ |
| 成本 | ★★★★★ | ★★★☆☆ | ★★☆☆☆ |
推荐方案: 采用离线推荐+实时规则补充的混合方案。
实施要点:
-
推荐场景分类:
首页推荐: - 个性化推荐(基于用户画像) - 热门推荐(兜底) 详情页推荐: - 看了还看(基于商品相似度) - 买了还买(基于订单关联) 购物车推荐: - 凑单推荐(基于购物车商品关联) - 优惠推荐(基于满减规则) -
推荐召回链路:
第一层:个性化召回(离线计算) - 协同过滤召回 - 内容召回(基于用户兴趣标签) 第二层:规则召回(在线计算) - 热门商品 - 运营配置 第三层:排序 - 点击率预估 - 转化率预估 - 业务规则调权(如新品扶持) 第四层:过滤 - 去重 - 过滤下架/无货商品 - 多样性(不全是同一类目) -
冷启动处理:
新用户: - 展示热门商品 - 根据注册信息推断兴趣(地域、年龄) - 引导用户选择兴趣标签 新商品: - 基于类目和属性推荐给相关用户 - 运营人工推送给种子用户 - 根据早期反馈调整推荐策略 -
A/B测试:
实验: - 对照组:规则推荐 - 实验组:算法推荐 指标: - 点击率(CTR) - 转化率(CVR) - 人均订单金额 -
监控指标:
业务指标: - 推荐位点击率 - 推荐商品转化率 - 推荐覆盖度(多少用户有推荐) 技术指标: - 推荐响应时间 - 推荐服务可用性 - 离线计算任务成功率
延伸思考:
- 如何评估推荐系统的效果?
- 推荐系统如何防止马太效应(热门更热,冷门更冷)?
- 如何保护用户隐私(不过度使用用户数据)?
🔧 题目11:商品搜索的倒排索引设计
问题描述: 搜索引擎的核心是倒排索引。请说明电商商品搜索的倒排索引如何设计,包括分词、索引结构、查询优化等。
答案:
问题分析: 倒排索引的核心要点:
- 分词策略(中文分词难点)
- 索引字段选择(哪些字段需要索引)
- 相关性打分(如何排序)
- 性能优化(索引大小、查询速度)
方案一:基于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分词+多字段权重。
实施要点:
-
自定义词典:
品牌词:小米、iPhone、华为 型号词:13Ultra、15Pro、Mate60 行业词:闪充、快充、护眼屏 维护: - 定期更新词典 - 新品牌/新词及时添加 -
同义词处理:
{ "filter": { "synonym_filter": { "type": "synonym", "synonyms": [ "手机,移动电话", "充电器,充电头", "iPhone,苹果手机" ] } } } -
拼音搜索:
支持拼音搜索: "xiaomi" → 小米 "pingguo" → 苹果 实现: - 使用pinyin分词插件 - 或维护拼音映射表 -
搜索建议(suggest):
输入"xiao" → 建议:[小米, 小天才, 小度] 输入"iphone" → 建议:[iPhone 15, iPhone 14, iPhone 13] 实现: - 使用ES的completion suggester - 基于前缀匹配 -
性能优化:
索引优化: - 只索引需要搜索的字段 - 使用doc_values减少内存占用 - 定期合并段(segment merge) 查询优化: - 结果分页(from+size < 10000) - 深度分页用scroll或search_after - 热门查询结果缓存
延伸思考:
- 如何实现搜索纠错(“小米手及” → “小米手机”)?
- 如何优化长尾查询的性能?
- 搜索结果如何排序(相关性、销量、价格)?
💡 题目12:如何处理商品数据的历史版本?
问题描述: 商品信息会不断变更(价格调整、标题修改、图片更换)。为了审计和纠纷处理,需要保留商品的历史版本。如何设计商品版本管理?
答案:
问题分析: 商品版本管理的核心挑战:
- 版本数据量大(每次变更都存储)
- 查询历史版本(某个时间点的商品信息)
- 版本对比(对比两个版本的差异)
- 存储成本
方案一:全量版本存储
核心思想: 每次变更都保存完整的商品数据。
设计:
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. 得到目标版本
优点:
- 平衡存储和查询性能
- 快照恢复快
- 增量节省空间
缺点:
- 实现复杂度中等
方案对比:
| 维度 | 全量版本 | 增量版本 | 混合模式 |
|---|---|---|---|
| 存储成本 | ★★☆☆☆ | ★★★★★ | ★★★★☆ |
| 查询性能 | ★★★★★ | ★★☆☆☆ | ★★★★☆ |
| 实施难度 | ★★★★★ | ★★★☆☆ | ★★★☆☆ |
| 审计能力 | ★★★★★ | ★★★★★ | ★★★★★ |
推荐方案: 对于电商系统,推荐混合模式。
实施要点:
-
快照策略:
触发快照的时机: - 商品上架时(V1) - 每周日凌晨(定期快照) - 重大变更时(价格变动>20%) -
变更日志记录:
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); } -
版本查询API:
GET /api/products/{productId}/versions → 返回所有版本列表 GET /api/products/{productId}/versions/{version} → 返回指定版本数据 GET /api/products/{productId}/diff?from=10&to=12 → 返回版本差异 -
存储优化:
- 快照使用压缩存储(gzip) - 超过1年的版本归档到对象存储 - 变更日志保留2年(法律要求)
延伸思考:
- 如何支持版本回滚(恢复到历史版本)?
- 版本数据如何支持跨表查询(如关联订单)?
- 大批量商品版本查询如何优化?
📊 题目13:多租户场景下的商品数据隔离
问题描述: 在B2B2C平台中,多个商家共用一套系统。如何设计商品数据的租户隔离,保证数据安全和性能?
答案:
问题分析: 多租户隔离的核心挑战:
- 数据隔离:商家A看不到商家B的商品
- 性能隔离:商家A的流量不影响商家B
- 成本优化:共享基础设施降低成本
- 个性化:支持商家自定义配置
方案一:独立数据库(物理隔离)
核心思想: 每个租户独立数据库。
设计:
租户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 | 混合隔离 |
|---|---|---|---|
| 隔离性 | ★★★★★ | ★★☆☆☆ | ★★★★☆ |
| 成本 | ★★☆☆☆ | ★★★★★ | ★★★★☆ |
| 运维复杂度 | ★★☆☆☆ | ★★★★★ | ★★★☆☆ |
| 扩展性 | ★★★★★ | ★★★☆☆ | ★★★★☆ |
推荐方案: 采用混合隔离(分库分表)。
实施要点:
-
租户分级:
VIP租户(月GMV>1000万): - 独立数据库 - 独立Redis - 独立ES索引 普通租户: - 共享分片数据库 - 共享Redis(按tenant_id前缀隔离) - 共享ES索引(按tenant_id过滤) -
数据源路由:
@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(); } } -
租户升降级:
普通→VIP(升级): 1. 创建独立数据库 2. 数据迁移(双写验证) 3. 切换路由 4. 清理旧数据 VIP→普通(降级): 1. 迁移到共享分片 2. 切换路由 3. 删除独立数据库 -
安全控制:
- 强制tenant_id过滤(ORM拦截器) - 禁止跨租户查询 - API鉴权(JWT包含tenant_id) - 审计日志(记录租户操作)
延伸思考:
- 如何防止误查询跨租户数据(ORM层面)?
- 租户数据如何备份和恢复?
- 如何支持租户级别的功能开关?
🔧 题目14:商品导入的批量处理优化
问题描述: 商家需要批量导入商品(一次导入1000-10000个)。如何设计批量导入功能,保证性能和数据正确性?
答案:
问题分析: 批量导入的核心挑战:
- 数据量大,处理时间长
- 需要校验每个商品(格式、必填项、业务规则)
- 部分成功部分失败如何处理
- 导入进度如何实时反馈
方案一:同步导入
核心思想: 用户上传文件,服务端同步处理,处理完返回结果。
流程:
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连接
- 实现最复杂
方案对比:
| 维度 | 同步导入 | 异步导入 | 流式导入 |
|---|---|---|---|
| 用户体验 | ★★☆☆☆ | ★★★★☆ | ★★★★★ |
| 支持规模 | ★★☆☆☆ | ★★★★★ | ★★★★☆ |
| 实施难度 | ★★★★★ | ★★★☆☆ | ★★☆☆☆ |
| 实时反馈 | ★★★★★ | ★★☆☆☆ | ★★★★★ |
推荐方案: 采用异步导入+进度查询。
实施要点:
-
文件解析:
支持格式: - Excel(.xlsx) - CSV - JSON 解析优化: - 流式解析(不一次加载全文件) - 分批处理(每100条一批) -
数据校验:
校验层级: L1:格式校验(必填字段、字段类型) L2:业务校验(价格合理性、类目有效性) L3:关联校验(品牌是否存在、图片URL是否有效) 快速失败: - 格式错误直接返回,不处理后续数据 -
事务处理:
方案A:全量事务 - 全部成功才提交,任一失败全部回滚 - 适合小批量、关联性强的数据 方案B:分批事务(推荐) - 每100条一个事务 - 部分失败不影响其他批次 - 生成失败报告 -
性能优化:
- 批量INSERT(100条一次) - 异步同步ES(不阻塞导入) - 限流(防止导入占用所有资源) - 分时段(凌晨处理大批量) -
失败处理:
失败记录: - 生成Excel文件,标注失败原因 - 用户下载修改后重新导入 部分成功: - 成功的商品已入库 - 失败的记录在error_file中
延伸思考:
- 如何支持导入任务的取消?
- 导入过程中商品数据变更如何处理?
- 如何设计商品导入的幂等性?
💡 题目15:商品审核流程的设计
问题描述: 商家上传的商品需要经过审核才能上架(防止违规商品)。请设计商品审核系统,包括机审和人审。
答案:
问题分析: 商品审核的核心挑战:
- 审核效率:大量商品等待审核
- 审核准确性:机审误报,人审成本高
- 审核优先级:重点类目优先审核
- 申诉流程:商家对审核结果不满
方案一:纯人工审核
核心思想: 所有商品都由审核人员人工审核。
流程:
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审核。
实施要点:
-
审核规则配置化:
审核规则表: 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" } -
审核任务队列:
优先级队列: P0:付费商家、大商家 P1:普通商家 P2:新商家 分配策略: - P0优先分配 - 同优先级按提交时间 - 负载均衡(每个审核员任务量相当) -
审核SLA:
目标: - 机审:5秒内完成 - 人审:2小时内完成(工作时间) 超时告警: - 待审核任务积压 > 500 - 人审超时 > 50个 -
申诉流程:
商家不满审核结果: 1. 点击"申诉" 2. 填写申诉理由 3. 转高级审核员复审 4. 复审结果通知商家
延伸思考:
- 如何设计审核人员的绩效考核?
- 机审规则如何动态调整(根据审核质量)?
- 如何防止商家恶意提交违规商品?
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_balance | 写 INIT/INBOUND 流水,不能绕过账本直接改 stock |
| 门店数量库存 | 按 sku_id + store_id 创建库存行 | 门店上下线要支持锁定、迁移和审计 |
| 日期 / 时段库存 | 按 sku_id + store_id + date + slot 创建切片 | 高流量品类提前物化,长尾门店懒创建 |
| 导入券码库存 | 创建 inventory_code_batch,逐行写 inventory_code_pool_XX | 加密存储、哈希去重、Redis LIST 只预热 code_id |
| 系统生成券码 | 预生成批次,或按订单幂等生成后落库 | 返回给用户前必须先有 MySQL 权威行 |
| 供应商库存 | 创建供应商映射和本地快照 | 本地快照不是最终承诺,下单前需要强刷或预订 |
面试时可以强调三个原则:
- 库存创建要任务化:商品发布事务不应该同步创建海量券码或未来 365 天日历库存,否则发布链路会被库存写放大拖垮。
- 库存创建要幂等:同一个发布版本、导入批次或供应商快照重复投递时,不能重复入库或重复生成券码。
- 库存创建要能解释来源:每一次初始化、导入、补货、系统生码都要有任务、批次和账本流水,否则后续对账只能看到“库存变了”,无法解释为什么变。
对于券码制,最容易踩坑的是把 Redis 当成码池权威。正确做法是:
导入或生成券码
→ 加密写入 inventory_code_pool_XX
→ status=AVAILABLE
→ Redis LIST 只灌入 code_id
→ 下单时弹出 code_id
→ MySQL CAS: AVAILABLE -> BOOKING
只有 MySQL 状态机更新成功,才算真正锁码成功。Redis 可以丢、可以重建,但不能成为唯一账本。
延伸思考:
- 库存创建任务部分成功时,哪些数据可以继续保留,哪些必须回滚?
- 系统生成券码如何防止被猜测和批量撞库?
- 酒店或门店预约类库存,未来多久的日历切片应该提前物化?
🔧 题目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 重试
要避免的反模式:
- 供给后台直接改
stock字段,绕过库存账本; - 商品
ONLINE后默认可卖,忽略库存、价格、履约和搜索刷新状态; - 下架时删除库存行,导致历史订单、售后和券码核销不可追溯;
- 供应商同步直接覆盖运营手工修复的库存策略;
- 库存系统直接改商品生命周期,绕过审核和发布版本。
一句话总结:
供给平台治理变更,生命周期控制线上状态,库存系统提供可承诺资源,可售投影把这些状态合成用户能否下单。它们通过命令、事件、版本和幂等键协作,而不是互相直接改库。
延伸思考:
- 商品已发布但库存初始化失败,是否允许展示“售罄”?
- 运营手工补货和供应商同步库存冲突时,字段主导权怎么判定?
- 下架后已有预占订单是否继续履约,谁来仲裁?
📊 题目1:设计防止库存超卖的方案
问题描述: 电商大促时,热门商品库存100件,但短时间涌入1000个订单。如何设计库存扣减方案,防止超卖?
答案:
问题分析: 库存超卖的核心原因:
- 并发扣减:多个请求同时扣减库存
- 分布式环境:库存分散在多个节点
- 缓存不一致:Redis和DB库存不同步
- 库存回滚:订单取消后库存未释放
方案一:数据库悲观锁
核心思想: 使用数据库行锁保证原子性。
实现:
-- 查询并锁定
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。
实施要点:
-
双层库存设计:
Redis(实时库存): - 用于扣减判断 - 高性能 - 可能丢失 MySQL(权威库存): - 定期同步Redis - 数据持久化 - 对账基准 -
库存同步:
Redis → MySQL: - 定时任务(每10秒) - 批量更新(减少DB压力) - 增量同步(只同步变更的SKU) MySQL → Redis: - 商品上架时初始化Redis - 运营调整库存时更新Redis - Redis故障恢复时从MySQL加载 -
库存预热:
大促前: 1. 识别热门商品(预测销量) 2. 提前加载到Redis 3. 设置永不过期 4. 多副本(主从) -
降级方案:
Redis故障: - 降级到MySQL悲观锁 - 限流(降低并发度) - 提示用户(商品火爆) -
监控告警:
指标: - Redis和MySQL库存差异 - 库存扣减QPS - 库存不足次数 - 超卖告警(库存为负) 告警: - 库存差异 > 100 - 超卖发生 - Redis同步延迟 > 1分钟
延伸思考:
- 秒杀场景如何进一步优化(如库存分段、令牌桶)?
- Redis故障导致库存丢失如何恢复?
- 如何处理订单取消后的库存回补?
🔧 题目2:如何设计分布式库存系统?
问题描述: 电商平台有多个仓库(北京、上海、深圳),商品在不同仓库有不同库存。如何设计分布式库存系统?
答案:
问题分析: 分布式库存的核心挑战:
- 库存分布:如何在多仓库间分配库存
- 库存查询:如何快速查询总库存
- 库存分配:用户下单时选择哪个仓库发货
- 库存调拨:仓库间库存转移
方案一:集中式库存
核心思想: 所有仓库库存汇总到一个中心库存池。
设计:
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最高的仓库
优点:
- 用户体验好(总库存可见)
- 灵活分配(后台优化)
- 支持复杂路由
缺点:
- 实现复杂
- 需要智能分配算法
方案对比:
| 维度 | 集中式 | 分布式 | 虚拟池 |
|---|---|---|---|
| 用户体验 | ★★★★★ | ★★★☆☆ | ★★★★★ |
| 性能 | ★★★☆☆ | ★★★★★ | ★★★★☆ |
| 库存利用率 | ★★★★★ | ★★★☆☆ | ★★★★★ |
| 实施难度 | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
推荐方案: 采用虚拟库存池。
实施要点:
-
库存聚合:
实时聚合(Redis): total_stock:sku:123 = stock:warehouse:1:sku:123 + stock:warehouse:2:sku:123 + stock:warehouse:3:sku:123 更新触发: - 仓库库存变更 → 更新总库存 - 使用Redis Pipeline批量更新 -
仓库选择算法:
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; } -
库存预占:
预占流程: 1. 用户下单 → 预占库存(reserved_stock +quantity) 2. 用户支付 → 确认扣减(stock -quantity, reserved_stock -quantity) 3. 用户取消 → 释放库存(reserved_stock -quantity) 超时释放: - 未支付订单30分钟后自动取消 - 定时任务扫描超时预占,自动释放 -
库存调拨:
场景: - 北京仓库存100,上海仓库存0 - 上海用户下单,需要从北京调拨 调拨流程: 1. 创建调拨单 2. 北京仓库:stock -10 3. 运输中... 4. 上海仓库:stock +10 -
安全库存:
设计: available_stock = physical_stock - reserved_stock - safety_stock 作用: - 预留库存应对盘点误差 - 预留库存应对损坏、丢失 - 建议:safety_stock = physical_stock * 5%
延伸思考:
- 如何设计库存预警机制(库存不足提醒)?
- 多仓库场景下如何最优化运费成本?
- 如何处理商品跨仓拆单(一单多仓发货)?
💡 题目3:大促场景下的库存预热和削峰方案
问题描述: 双11大促,预计订单量是平时的100倍。如何对库存系统进行预热和削峰,保证不超卖且性能可控?
答案:
问题分析: 大促库存的核心挑战:
- 瞬时流量暴增(平时1000 QPS → 10万 QPS)
- 热点商品集中(TOP 100商品占80%流量)
- Redis/DB压力大
- 需要防止库存击穿
方案一:库存分段+令牌桶
核心思想: 将库存分为多段,每段独立扣减,最后汇总。
设计:
库存分段:
总库存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. 扣减失败(无货)通知用户
优点:
- 削峰效果好
- 库存系统压力可控
- 用户体验可接受(秒杀场景)
缺点:
- 用户等待时间长
- 需要排队机制
- 实现复杂
方案对比:
| 方案 | 性能 | 削峰效果 | 用户体验 | 实施难度 |
|---|---|---|---|---|
| 库存分段 | ★★★★☆ | ★★★☆☆ | ★★★★★ | ★★★☆☆ |
| 本地库存 | ★★★★★ | ★★★★★ | ★★★★★ | ★★★☆☆ |
| 队列削峰 | ★★★☆☆ | ★★★★★ | ★★★☆☆ | ★★☆☆☆ |
推荐方案: 采用库存分段+本地库存的组合。
实施要点:
-
库存预热:
大促前3天: 1. 识别热销商品(TOP 1000) 2. Redis预加载: - 库存数据 - 商品信息 - 价格信息 3. 本地缓存预加载 4. 压测验证 -
分段策略:
分段数量 = max(库存数量 / 100, 服务器数量) 示例:库存10000,服务器100台 → 分段数 = max(10000/100, 100) = 100段 → 每段100件 优点: - 降低单key热度 - 并发度=分段数 -
本地库存管理:
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; } } -
监控大盘:
实时监控: - 总库存水位 - 扣减QPS - 成功率 - Redis热key - 本地库存分布 告警: - 库存水位 < 20% - 扣减失败率 > 5% - Redis单key QPS > 10万
延伸思考:
- 秒杀开始前如何预热(避免冷启动)?
- 大促结束后如何回收本地库存?
- 如何应对恶意刷单占用库存?
📊 题目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分钟
}
优点:
- 差异化服务
- 库存利用率高
- 灵活调整
缺点:
- 规则复杂
- 实现成本高
方案对比:
| 方案 | 用户体验 | 库存利用率 | 超卖风险 | 实施难度 |
|---|---|---|---|---|
| 下单预占 | ★★★☆☆ | ★★★★★ | ★★★☆☆ | ★★★★★ |
| 结算预占 | ★★★★★ | ★★★★☆ | ★★★★★ | ★★★☆☆ |
| 分级预占 | ★★★★★ | ★★★★★ | ★★★★★ | ★★☆☆☆ |
推荐方案: 采用结算时预占。
实施要点:
-
预占时长设置:
考虑因素: - 支付流程耗时(通常2-3分钟) - 用户犹豫时间(5-10分钟) - 库存周转率(紧俏商品缩短) 建议: - 默认15分钟 - 库存<10%时缩短到5分钟 - VIP用户延长到30分钟 -
预占幂等性:
使用order_id作为幂等键: INSERT INTO reservation (reservation_id, order_id, ...) ON DUPLICATE KEY UPDATE updated_at=NOW(); 防止重复预占: - 同一订单多次预占,使用相同reservation记录 - 延长expire_at即可 -
超时释放优化:
方案A:定时任务扫描 - 每分钟扫描一次 - 查询expire_at < NOW() - 批量释放 方案B:延迟队列(推荐) - 预占时发送延迟消息(延迟15分钟) - 消息到期时检查状态 - 如果未支付,释放库存 优点:精确释放,无需轮询 -
库存保护:
最大预占比例: - 允许预占库存 <= total_stock * 90% - 保留10%库存应对预占释放后的瞬时需求 预占限流: - 单用户最多预占5个订单 - 单商品最多被预占total_stock * 80%
延伸思考:
- 用户在结算页停留很久不支付,如何处理?
- 预占释放后其他用户如何得知库存恢复?
- 如何设计库存预占的监控指标?
🔧 题目5:如何设计库存的分级管理(前台可售vs仓库实际)?
问题描述: 仓库实际库存100件,但前台可售库存只有80件(预留20件应对售后、损耗)。如何设计库存的分级管理?
答案:
问题分析: 库存分级的核心挑战:
- 不同层级库存含义不同
- 层级间库存同步
- 安全库存设置
- 库存占用追踪
方案一:单一库存(简化版)
核心思想: 只维护一个库存字段,不区分层级。
设计:
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)
优点:
- 灵活,支持多种占用类型
- 可追溯所有占用历史
- 易于扩展
缺点:
- 查询需要聚合计算
- 性能较差
方案对比:
| 维度 | 单一库存 | 多级库存 | 占用日志 |
|---|---|---|---|
| 清晰度 | ★★☆☆☆ | ★★★★★ | ★★★★☆ |
| 性能 | ★★★★★ | ★★★★☆ | ★★★☆☆ |
| 灵活性 | ★★☆☆☆ | ★★★☆☆ | ★★★★★ |
| 实施难度 | ★★★★★ | ★★★☆☆ | ★★☆☆☆ |
推荐方案: 采用多级库存。
实施要点:
-
安全库存设置:
策略: - 标准:safety_stock = 5% * physical_stock - 易损商品:safety_stock = 10% * physical_stock - 高价商品:safety_stock = 2% * physical_stock 动态调整: - 根据历史损耗率调整 - 旺季增加,淡季减少 -
库存同步检查:
不变量检查: physical_stock = available_stock + reserved_stock + sold_stock + safety_stock 定期对账: 如果不等式不成立,说明库存有问题 -
库存调整接口:
运营调整物理库存: adjustPhysicalStock(skuId, delta, reason) 自动调整安全库存: adjustSafetyStock(skuId, percentage) -
库存报表:
库存健康度: - 库存周转率 = 销量 / 平均库存 - 滞销率 = 30天未售商品数 / 总商品数 - 缺货率 = 用户下单失败次数 / 总下单次数
延伸思考:
- 如何设计库存盘点功能(盘点期间库存锁定)?
- 安全库存不足时如何处理?
- 已售库存发货后如何核减?
💡 题目6:库存扣减失败的补偿机制
问题描述: 在订单创建流程中,扣减库存可能失败(并发冲突、网络超时、服务故障)。如何设计补偿机制,保证数据一致性?
答案:
问题分析: 库存扣减失败的核心场景:
- 网络超时:不知道是否扣减成功
- 服务故障:库存服务不可用
- 并发冲突:乐观锁更新失败
- 数据不一致:订单已创建但库存未扣减
方案一:同步重试
核心思想: 扣减失败时立即重试,最多重试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次仍失败
- 转人工处理
- 排查根本原因
优点:
- 多层保障
- 可靠性高
- 覆盖各种异常
缺点:
- 实现最复杂
方案对比:
| 方案 | 实时性 | 可靠性 | 用户体验 | 实施难度 |
|---|---|---|---|---|
| 同步重试 | ★★★★★ | ★★★☆☆ | ★★★☆☆ | ★★★★★ |
| 异步补偿 | ★★★☆☆ | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
| 补偿+对账 | ★★★☆☆ | ★★★★★ | ★★★★☆ | ★★☆☆☆ |
推荐方案: 采用补偿+对账。
实施要点:
-
幂等性保证:
public void deductInventory(DeductRequest req) { // 使用orderId作为幂等键 if (isAlreadyDeducted(req.getOrderId())) { return; // 已扣减,直接返回 } // 执行扣减 doDeduct(req); // 记录已扣减 markDeducted(req.getOrderId()); } -
补偿任务重试策略:
指数退避: 第1次:立即重试 第2次:1分钟后 第3次:5分钟后 第4次:15分钟后 第5次:1小时后 超过5次 → 转人工 -
补偿任务优先级:
P0:已支付订单(优先处理) P1:待支付订单 P2:其他 -
对账规则:
检查项: 1. 订单状态=PAID → 库存必须已扣减 2. 订单金额 = 商品价格 × 数量 3. 库存不能为负数 差异处理: - 自动补偿(低风险) - 人工介入(高风险)
延伸思考:
- 如果补偿重试多次仍失败,如何处理?
- 补偿过程中订单状态如何展示给用户?
- 如何监控补偿任务的执行情况?
📊 题目7:设计库存盘点系统
问题描述: 仓库需要定期盘点库存,核对系统库存和实际库存是否一致。如何设计库存盘点系统?
答案:
问题分析: 库存盘点的核心挑战:
- 盘点期间如何处理库存变更
- 盘点差异如何调整
- 大规模商品盘点效率
- 盘点结果审核
方案一:冻结盘点
核心思想: 盘点期间冻结库存,禁止出入库。
流程:
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. 正常差异汇总报告
优点:
- 分散盘点,效率高
- 重点商品关注度高
- 不影响业务
缺点:
- 需要分类管理
- 全盘点周期长
方案对比:
| 方案 | 对业务影响 | 准确性 | 效率 | 适用场景 |
|---|---|---|---|---|
| 冻结盘点 | ★★☆☆☆ | ★★★★★ | ★★☆☆☆ | 小仓库 |
| 动态盘点 | ★★★★★ | ★★★★★ | ★★★★☆ | 大仓库 |
| 循环盘点 | ★★★★★ | ★★★★☆ | ★★★★★ | 商品多 |
推荐方案: 采用动态盘点+循环盘点的组合。
实施要点:
-
盘点任务生成:
创建盘点单: 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(是否已调整) -
盘点APP设计:
功能: - 扫码盘点(扫条码自动录入) - 语音录入(解放双手) - 拍照记录(有问题的商品拍照) - 离线模式(网络不好时) 优化: - 按货架号排序(减少走动) - 实时同步(避免数据丢失) -
差异分析:
差异原因分类: - 损耗(DAMAGE):商品破损 - 丢失(LOSS):商品丢失 - 错发(WRONG_SHIP):发错货 - 漏记(MISSING_RECORD):出入库漏记 - 系统bug(SYSTEM_ERROR) 自动调整规则: - diff < 5% → 自动调整 - diff >= 5% → 需要审核 - diff > 20% → 必须复盘(可能系统bug) -
盘点报告:
报告内容: - 盘点汇总:总商品数、差异数、差异金额 - 差异TOP 10:差异最大的商品 - 差异原因分布:损耗X件、丢失Y件 - 仓库对比:各仓库差异率
延伸思考:
- 如何设计盘点的权限控制(防止作弊)?
- 盘点差异过大时如何追责?
- 如何设计移动盘点的离线模式?
🔧 题目8:如何处理库存的并发更新?
问题描述: 多个订单同时扣减同一商品库存,如何处理并发冲突,保证库存不超卖?
答案:
问题分析: 并发更新的核心场景:
- 秒杀场景:1万人抢100件商品
- 正常场景:多个用户同时下单
- 分布式场景:多个服务器同时扣减
方案一:数据库行锁(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+Lua | 100K+ | 无 | 最终一致 | ★★★☆☆ |
推荐方案: 根据场景选择:
- 普通商品:乐观锁(MySQL)
- 秒杀商品:Redis+Lua
- 低并发:悲观锁
实施要点:
-
Redis高可用:
- Redis主从+哨兵 - 双机房部署 - 持久化:AOF every second -
库存同步:
Redis → MySQL: - 定时任务(每10秒) - 批量更新(减少DB压力) - 对账纠偏(每小时) -
降级方案:
Redis故障 → 降级到MySQL乐观锁 MySQL故障 → 停止扣减,返回系统繁忙 -
监控:
- Redis和MySQL库存差异 - 扣减成功率 - 扣减耗时P99 - 并发冲突次数
延伸思考:
- 如何设计秒杀的库存扣减(更极端的高并发)?
- 分库分表场景下如何扣减库存?
- Redis和MySQL数据不一致如何恢复?
💡 题目9:虚拟库存vs实物库存的差异
问题描述: 实物商品有物理库存限制,虚拟商品(如充值卡、游戏币)可以无限生成。两者在库存设计上有什么差异?
答案:
问题分析: 虚拟库存的核心特点:
- 可按需生成(理论无限)
- 实际受限于供应商配额
- 卡密池管理(有卡密才能售卖)
- 即时发货(无需物流)
方案一:无限库存模式
核心思想: 虚拟商品库存设为无限大,不限制购买。
设计:
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. 发货给用户
优点:
- 无需提前准备卡密
- 按需申请
- 库存灵活
缺点:
- 实时性依赖供应商
- 供应商故障风险
方案对比:
| 方案 | 准确性 | 供应商依赖 | 实施难度 | 适用场景 |
|---|---|---|---|---|
| 无限库存 | ★★☆☆☆ | ★★★★★ | ★★★★★ | 可生成虚拟品 |
| 卡密池 | ★★★★★ | ★★★☆☆ | ★★★☆☆ | 充值卡、券码 |
| 配额模式 | ★★★★☆ | ★★☆☆☆ | ★★★☆☆ | 供应商直连 |
推荐方案: 根据虚拟商品类型选择:
- 可生成(游戏币、积分):无限库存
- 卡密类(充值卡、激活码):卡密池
- 供应商直连(机票、酒店):配额模式
实施要点:
-
卡密安全:
- 卡密加密存储(AES-256) - 卡密传输加密(HTTPS) - 卡密脱敏展示(**** **** **** 1234) - 限制查询频率(防止批量获取) -
卡密补货:
补货触发: - 可用卡密 < 安全阈值(如1000张) - 自动告警 补货方式: - 供应商API自动拉取 - 或人工Excel导入 -
卡密有效期:
过期处理: - 定时任务扫描过期卡密 - 状态更新为INVALID - 库存减少(不可售) - 向供应商申请补卡 -
虚拟发货:
自动发货: - 支付成功 → 立即分配卡密 - 推送给用户(短信/App) - 订单状态 → COMPLETED 发货耗时:< 30秒
延伸思考:
- 卡密被盗用如何防范?
- 虚拟商品是否需要支持退款?
- 供应商配额不足时如何处理?
📊 题目10:多仓库场景下的库存分配策略
问题描述: 电商平台有5个仓库(华北、华东、华南、西南、西北),用户下单时如何选择仓库发货?请设计库存分配策略。
答案:
问题分析: 仓库选择的核心考量:
- 配送时效:就近仓库配送快
- 运费成本:距离影响运费
- 库存充足度:优先选择库存多的仓库
- 仓库负载:避免单仓库压力过大
方案一:就近原则
核心思想: 根据用户地址,选择最近的仓库。
设计:
仓库覆盖范围:
- 北京仓:北京、天津、河北
- 上海仓:上海、江苏、浙江
- 深圳仓:广东、广西、福建
- 成都仓:四川、重庆、云南
- 西安仓:陕西、甘肃、新疆
路由逻辑:
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. 目标仓库入库
优点:
- 库存均衡,利用率高
- 减少缺货
- 优化全局
缺点:
- 调拨成本高
- 调拨周期长(天级)
- 需要预测算法
方案对比:
| 方案 | 配送时效 | 成本 | 库存利用率 | 复杂度 |
|---|---|---|---|---|
| 就近原则 | ★★★★★ | ★★★★☆ | ★★★☆☆ | ★★★★★ |
| 智能调度 | ★★★★☆ | ★★★★★ | ★★★★☆ | ★★★☆☆ |
| 均衡策略 | ★★★☆☆ | ★★★☆☆ | ★★★★★ | ★★☆☆☆ |
推荐方案: 采用智能调度。
实施要点:
-
仓库路由服务:
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); } -
拆单策略:
场景:用户购买商品A、B、C - 商品A:北京仓有货 - 商品B:上海仓有货 - 商品C:两个仓库都有货 策略1:优先合单 - 查找能满足所有商品的仓库 - 减少拆单,降低运费 策略2:就近发货 - 每个商品从最近仓库发货 - 可能拆多单,但配送快 策略3:混合 - 大件商品就近发货 - 小件商品合单发货 -
库存预测:
预测模型: - 输入:历史销量、季节、促销活动 - 输出:未来7天各仓库销量预测 预分配: - 根据预测提前调拨库存 - 避免大促时调拨来不及 -
负载均衡:
仓库容量管理: - 每个仓库设置日处理能力(如1万单/天) - 接近容量时降低选择权重 - 超过容量时停止分配 动态调整: - 实时监控各仓库订单量 - 动态调整路由权重
延伸思考:
- 如何处理跨仓拆单的运费计算?
- 用户能否指定发货仓库?
- 仓库之间如何协同(库存调拨、应急支援)?
🔧 题目11:如何设计库存安全水位和补货机制?
问题描述: 电商系统需要设置库存安全水位,当库存低于安全水位时自动触发补货。如何设计这套机制?
答案:
问题分析: 库存安全水位的核心要素:
- 安全水位如何设置(太高占用资金,太低容易缺货)
- 补货时机和数量
- 补货周期(供应商交付时间)
- 多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分类。
实施要点:
-
销量预测模型:
简单移动平均: 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开源) - 考虑季节性、趋势、促销影响 -
补货决策表:
replenishment_rule ├── sku_id ├── category(ABC分类) ├── safety_stock ├── reorder_point ├── lead_time(补货周期) ├── order_quantity(建议补货量) ├── max_stock(最大库存) └── updated_at -
自动补货流程:
定时任务(每天凌晨): 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 -
补货优先级:
优先级计算: 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) - 高销量 - 高利润 -
监控告警:
告警条件: - 库存 < 安全库存 → 缺货预警 - 库存 > 最大库存 * 1.5 → 积压告警 - 补货单超期未到货 → 交付延迟告警 报表: - 缺货率(SKU缺货天数 / 总天数) - 库存周转率(销量 / 平均库存) - 补货及时率(按时到货 / 总补货单)
延伸思考:
- 促销活动前如何调整补货策略?
- 供应商交付不稳定如何应对?
- 新品如何设置安全库存(无历史数据)?
💡 题目12:库存快照在订单中的应用
问题描述: 订单下单时需要记录当时的库存状态,用于售后和数据分析。如何设计库存快照机制?
答案:
问题分析: 库存快照的核心目的:
- 售后分析(为何超卖、缺货)
- 数据审计(库存变更追溯)
- 报表统计(某时刻库存状态)
- 性能要求(不能影响下单)
方案一:订单表冗余库存字段
核心思想: 在订单表记录下单时的库存数量。
设计:
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
优点:
- 平衡性能和存储
- 快照恢复快
- 审计能力强
缺点:
- 实现复杂度中等
方案对比:
| 方案 | 查询性能 | 存储成本 | 审计能力 | 实施难度 |
|---|---|---|---|---|
| 冗余字段 | ★★★★★ | ★★★★★ | ★★☆☆☆ | ★★★★★ |
| 变更日志 | ★★★☆☆ | ★★☆☆☆ | ★★★★★ | ★★★☆☆ |
| 快照+日志 | ★★★★☆ | ★★★★☆ | ★★★★★ | ★★★☆☆ |
推荐方案: 采用定期快照+增量日志。
实施要点:
-
快照生成策略:
定时快照: - 每小时生成一次快照 - 或库存变更超过1000次时生成 快照内容: - SKU ID - 物理库存 - 预占库存 - 已售库存 - 可售库存 - 快照时间 -
变更日志记录:
@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; } } -
历史库存查询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" } -
数据归档:
归档策略: - 变更日志保留90天 - 90天后归档到对象存储(OSS) - 快照保留1年 - 1年后删除(保留年度快照) -
应用场景:
场景1:售后分析 用户投诉超卖 → 查询下单时库存 → 分析扣减日志 → 定位问题 场景2:数据对账 每日对账:今日库存 = 昨日库存 + 今日入库 - 今日出库 不一致 → 查询变更日志 → 找出差异 场景3:报表统计 生成"每日库存报表" → 查询每日0点快照 → 生成报表
延伸思考:
- 如何设计库存变更的审计流程?
- 变更日志如何支持回滚操作?
- 大批量商品的快照如何优化存储?
📊 题目13:库存的实时性vs一致性权衡
问题描述: 库存系统中,Redis提供高性能但可能丢失数据,MySQL提供强一致但性能较低。如何在实时性和一致性之间权衡?
答案:
问题分析: 实时性vs一致性的核心矛盾:
- 用户期望实时看到库存
- 系统要保证不超卖
- 高并发下性能压力大
- 数据一致性难保证
方案一:强一致性优先(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();
}
优点:
- 灵活权衡
- 性能和一致性兼顾
- 差异化服务
缺点:
- 实现复杂
- 需要商品分类
方案对比:
| 方案 | 一致性 | 性能 | 实现难度 | 适用场景 |
|---|---|---|---|---|
| 强一致 | ★★★★★ | ★★☆☆☆ | ★★★★☆ | 高价商品 |
| 最终一致 | ★★★☆☆ | ★★★★★ | ★★★☆☆ | 秒杀 |
| 分层一致 | ★★★★☆ | ★★★★☆ | ★★☆☆☆ | 综合场景 |
推荐方案: 采用分层一致性。
实施要点:
-
一致性级别定义:
强一致(Strong Consistency): - MySQL事务 - 悲观锁或串行化 - 实时一致 最终一致(Eventual Consistency): - Redis扣减 + 异步同步 - 秒级延迟 - 需要对账 因果一致(Causal Consistency): - 同一用户操作有序 - 不同用户可能看到不同状态 -
降级策略:
正常模式: - 秒杀商品:Redis(最终一致) - 普通商品:MySQL乐观锁(强一致) 降级模式(Redis故障): - 秒杀商品:暂停售卖或限流到MySQL - 普通商品:MySQL悲观锁 极端模式(MySQL故障): - 只读Redis,禁止扣减 - 提示用户稍后再试 -
一致性检查:
实时检查: - 扣减后检查Redis和MySQL差异 - 差异 > 阈值(如100)→ 告警 定期对账: - 每小时全量对账 - 自动纠正小差异(< 5) - 大差异(> 10)→ 人工介入 -
监控指标:
一致性指标: - Redis-MySQL差异数量 - 差异持续时间 - 对账修复次数 性能指标: - 扣减TPS - 扣减耗时P99 - Redis命中率
延伸思考:
- 如何设计Redis的持久化策略(AOF/RDB)?
- 分布式场景下如何保证Redis和MySQL一致性?
- CAP理论在库存系统中如何权衡?
🔧 题目14:库存回滚机制的设计
问题描述: 用户下单后未支付,或者订单取消,需要回滚库存。如何设计库存回滚机制,保证幂等性和正确性?
答案:
问题分析: 库存回滚的核心场景:
- 订单取消(用户主动取消)
- 超时未支付(30分钟自动取消)
- 支付失败(扣款失败)
- 售后退货(订单完成后退货)
核心挑战:
- 幂等性:重复回滚不能多加库存
- 并发安全:多个回滚请求同时执行
- 部分回滚:一单多商品部分退货
- 补偿机制:回滚失败如何处理
方案一:直接加库存
核心思想: 取消订单时直接增加库存。
实现:
-- 订单取消
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();
}
优点:
- 幂等性强
- 可追溯
- 支持重试
- 审计友好
缺点:
- 实现复杂度高
- 需要额外表
方案对比:
| 方案 | 幂等性 | 并发安全 | 可追溯 | 实施难度 |
|---|---|---|---|---|
| 直接加库存 | ★☆☆☆☆ | ★★☆☆☆ | ★☆☆☆☆ | ★★★★★ |
| 基于状态 | ★★★★☆ | ★★★★☆ | ★★★☆☆ | ★★★☆☆ |
| 回滚记录 | ★★★★★ | ★★★★★ | ★★★★★ | ★★☆☆☆ |
推荐方案: 采用回滚记录表。
实施要点:
-
回滚类型设计:
CANCEL:订单取消 - 释放预占库存 - 回补可售库存 REFUND:售后退货 - 增加物理库存 - 增加可售库存 TIMEOUT:超时未支付 - 释放预占库存 ADJUST:库存调整(人工) -
回滚执行逻辑:
@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件商品,退货1件 处理: 1. 创建部分回滚记录 2. 回滚数量 = 退货数量(1件) 3. 更新订单项状态(2件已发货,1件已退货) -
失败重试:
补偿Worker: 1. 定时扫描FAILED状态的回滚记录 2. 重试执行回滚 3. 最多重试5次 4. 仍失败 → 转人工处理 -
监控告警:
指标: - 回滚成功率 - 回滚延迟(下单到回滚的时间) - 失败回滚数量 告警: - 回滚成功率 < 99% - 失败回滚 > 100条
延伸思考:
- 如何防止恶意下单占用库存?
- 库存回滚失败如何人工介入?
- 大批量订单取消如何优化回滚性能?
💡 题目15:跨境电商的库存管理(多国库存)
问题描述: 跨境电商在中国、美国、欧洲都有仓库,同一商品在不同地区有库存。如何设计全球库存管理系统?
答案:
问题分析: 跨境库存的核心挑战:
- 时区差异(中国和美国相差12小时)
- 币种不同(人民币、美元、欧元)
- 清关周期长(跨境物流10-30天)
- 库存调拨困难
方案一:独立库存池
核心思想: 每个国家/地区独立管理库存,互不共享。
设计:
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. 都无货 → 缺货
优点:
- 平衡速度和成本
- 灵活
- 用户可选
缺点:
- 需要智能路由
- 用户决策成本
方案对比:
| 方案 | 库存利用率 | 配送速度 | 用户体验 | 实施难度 |
|---|---|---|---|---|
| 独立池 | ★★☆☆☆ | ★★★★★ | ★★★☆☆ | ★★★★★ |
| 全球池 | ★★★★★ | ★★☆☆☆ | ★★★★★ | ★★★☆☆ |
| 混合模式 | ★★★★☆ | ★★★★☆ | ★★★★☆ | ★★☆☆☆ |
推荐方案: 采用混合模式。
实施要点:
-
库存数据结构:
global_inventory ├── sku_id ├── region_code(US/CN/EU/JP) ├── warehouse_id ├── stock ├── currency ├── local_price(本地售价) └── shipping_cost_to_other(跨境运费) -
库存分配策略:
初始分配(新品上架): - 根据各地区历史销量预测 - US: 40%, EU: 30%, CN: 20%, JP: 10% 动态调整(运营中): - 每周根据销量调整 - 滞销地区调拨到热销地区 -
跨境发货流程:
用户下单: 1. 显示配送选项: - 本地发货(2-3天,免运费) - 跨境发货(10-15天,运费$20) 2. 用户选择跨境发货 3. 扣减源国库存 4. 清关、物流 5. 配送到用户 -
币种和价格:
价格策略: - 每个地区独立定价(考虑关税、运费) - 实时汇率转换 示例: 商品成本:$100 - 美国售价:$150(含税15%,利润$35) - 中国售价:¥1200(含税13%,利润约$40) - 欧洲售价:€140(含税20%,利润约$30) -
库存同步:
同步机制: - 各地区库存独立数据库 - 聚合到全球视图(Redis缓存) - 更新延迟 < 1秒 时区处理: - 所有时间戳使用UTC - 本地展示转换为用户时区
延伸思考:
- 如何设计跨境库存调拨的审批流程?
- 清关失败如何处理库存回滚?
- 不同国家的退货政策如何影响库存管理?
2.3 营销与计价系统(10题)
📊 题目1:设计支持多种促销规则的价格计算引擎
问题描述: 电商平台有多种促销(满减、折扣、优惠券、满赠、阶梯价),用户下单时需要计算最终价格。如何设计灵活的价格计算引擎?
答案:
问题分析: 价格计算的核心挑战:
- 规则类型多(满减、折扣、优惠券、积分抵扣)
- 规则可组合(同时使用多种优惠)
- 优先级和互斥(有些优惠不能同时用)
- 实时计算性能
方案一:硬编码规则
核心思想: 在代码中直接编写每种促销规则的计算逻辑。
实现:
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();
}
优点:
- 极致灵活(可写任意逻辑)
- 无需发布代码
缺点:
- 安全风险(脚本注入)
- 调试困难
- 性能开销大
方案对比:
| 方案 | 灵活性 | 性能 | 运营友好 | 安全性 | 实施难度 |
|---|---|---|---|---|---|
| 硬编码 | ★★☆☆☆ | ★★★★★ | ★☆☆☆☆ | ★★★★★ | ★★★★★ |
| 规则引擎 | ★★★★☆ | ★★★★☆ | ★★★★★ | ★★★★☆ | ★★★☆☆ |
| 脚本引擎 | ★★★★★ | ★★★☆☆ | ★★★☆☆ | ★★☆☆☆ | ★★☆☆☆ |
推荐方案: 采用规则引擎。
实施要点:
-
规则抽象:
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); } } -
规则组合策略:
互斥规则: - 满减和折扣互斥(选优惠力度大的) - 用户只能使用一张优惠券 可叠加规则: - 满减 + 积分抵扣 - 会员折扣 + 优惠券 执行顺序: 1. 商品级促销(商品折扣) 2. 订单级促销(满减) 3. 用户级促销(会员折扣) 4. 优惠券 5. 积分抵扣 -
价格明细:
原价:¥500 - 商品折扣:-¥50(9折) - 满减优惠:-¥30(满200减30) - 会员折扣:-¥42(额外9折) - 优惠券:-¥20 = 实付:¥358 用户可见每项优惠的金额 -
性能优化:
规则缓存: - 缓存活跃的促销规则(Redis) - TTL 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 }
延伸思考:
- 如何设计促销规则的AB测试?
- 多种促销组合时如何选择最优组合?
- 促销规则变更如何保证已下单的订单价格不变?
🔧 题目2:优惠券系统的设计
问题描述: 电商平台需要支持优惠券(满减券、折扣券、品类券)。如何设计优惠券系统,包括发放、使用、核销?
答案:
问题分析: 优惠券的核心要素:
- 发放方式(批量发放、用户领取、定向发放)
- 使用规则(满减、折扣、品类限制、商品限制)
- 并发领取(秒杀券,1万人抢100张)
- 防刷机制(防止用户重复领取)
方案一:简单优惠券
核心思想: 优惠券模板+用户优惠券实例。
设计:
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. 检查用户是否已领取(防重复) 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(); } -
用券流程:
下单时使用优惠券: 1. 检查优惠券是否属于当前用户 2. 检查优惠券状态(UNUSED) 3. 检查是否过期 4. 检查订单是否满足使用条件(品类、金额) 5. 锁定优惠券(防止重复使用) 6. 计算优惠金额 支付成功: - 核销优惠券(status=USED) 订单取消: - 释放优惠券(status=UNUSED, lock_order_id=NULL) -
防刷策略:
策略1:用户限制 - 每人限领1张 - 同一手机号/设备ID限领 策略2:行为检测 - 短时间多次领取 → 拉黑 - 领取后不使用 → 降低权重 策略3:风控 - 新注册用户限制 - 异常IP拦截 -
券叠加规则:
规则: - 单笔订单最多使用1张优惠券 - 优惠券和满减活动可叠加 - 优惠券和积分抵扣可叠加 选券策略: - 自动选择优惠最大的券 - 或用户手动选择 -
券过期处理:
定时任务(每天凌晨): 1. 扫描即将过期的券(expire_at < NOW() + 3天) 2. 发送提醒通知(App推送、短信) 3. 扫描已过期的券 4. 状态更新为EXPIRED
延伸思考:
- 如何设计优惠券的转赠功能?
- 优惠券如何支持多次使用(如月卡券)?
- 如何设计优惠券的效果分析(发放ROI)?
💡 题目3:阶梯价和批发价的设计
问题描述: 电商平台支持批发场景,购买数量越多价格越低(如买1件100元,买10件90元,买100件80元)。如何设计阶梯价系统?
答案:
问题分析: 阶梯价的核心要素:
- 阶梯定义(数量区间和对应价格)
- 混合SKU计算(多个商品如何累计数量)
- 拆单问题(阶梯内和阶梯外商品分开发货)
- 实时计算性能
方案一: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级 | ★★☆☆☆ | ★★☆☆☆ | ★★★★★ | 零售 |
| 品类级 | ★★★★☆ | ★★★★☆ | ★★★☆☆ | 批发 |
| 订单级 | ★★★★★ | ★★★★★ | ★★★★☆ | 混合 |
推荐方案: 采用订单级阶梯价。
实施要点:
-
阶梯计算引擎:
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()); } } -
实时试算:
购物车实时显示: - 当前数量:8件 - 当前价格:¥1000 - 提示:"再买2件,享受95折,可省¥50" 动态提示: 引导用户凑单,提高客单价 -
拆单策略:
场景:用户购买120件商品 - 100件享受阶梯价(¥80/件) - 20件普通价(¥100/件) 方案A:不拆单 - 所有商品按最高阶梯价 - 用户体验好 方案B:拆单 - 100件一单,20件一单 - 复杂,不推荐 -
会员叠加:
规则: - 阶梯价和会员折扣可叠加 - 先应用阶梯价,再应用会员折扣 示例: 原价:¥10000 阶梯价(95折):¥9500 会员折扣(98折):¥9310 -
报表分析:
阶梯价效果分析: - 各阶梯成交订单数 - 平均客单价提升 - 转化率(凑单率)
延伸思考:
- 阶梯价如何与优惠券组合?
- 用户退货部分商品如何重新计算价格?
- 大促期间阶梯价如何调整?
📊 题目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.5倍) 行为积分: - 签到:5积分/天 - 分享:10积分/次 - 评价:20积分/次(带图50积分) - 首次购买:100积分 -
积分消费:
抵扣规则: - 100积分 = 1元 - 单笔订单最多抵扣订单金额的50% - 部分品类不支持积分抵扣(如iPhone) 兑换商品: - 积分商城 - 固定积分兑换商品 -
等级维护:
升级: - 成长值达到阈值立即升级 - 发送升级通知 降级: - 每年12月31日统计年度成长值 - 未达标的会员降级 - 降级前1个月提醒 - 保级活动(充值、消费保级) -
积分过期:
策略: - 积分有效期1年 - 每年12月31日清零即将过期积分 - 提前3个月、1个月、1周提醒 -
防刷策略:
- 签到积分:每天限1次 - 分享积分:每天限3次 - 评价积分:每订单限1次 - 异常行为检测(短时间大量操作)
延伸思考:
- 如何设计会员等级的有效期(年度会员)?
- 积分如何支持转赠功能?
- 会员权益如何动态调整(AB测试)?
🔧 题目5:秒杀活动的价格和库存设计
问题描述: 秒杀活动商品价格远低于平时,流量集中,如何设计秒杀的价格和库存系统,保证不超卖且性能可控?
答案:
问题分析: 秒杀的核心挑战:
- 瞬时高并发(10万+ QPS)
- 库存精准控制(100件商品,10万人抢)
- 价格隔离(秒杀价和正常价不能混淆)
- 防黄牛(防止脚本抢购)
方案一:独立秒杀表
核心思想: 秒杀商品和库存独立存储,与正常商品隔离。
设计:
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)
- 秒杀订单(独立表)
- 秒杀队列(削峰)
正常系统:
- 商品表
- 库存表
- 订单表
数据同步:
- 秒杀结束后同步到正常订单表
- 库存变更同步
优点:
- 完全隔离
- 互不影响
- 可针对性优化
缺点:
- 架构复杂
- 数据同步成本
方案对比:
| 方案 | 隔离性 | 性能 | 实施难度 | 适用场景 |
|---|---|---|---|---|
| 独立秒杀表 | ★★★★☆ | ★★★★☆ | ★★★☆☆ | 中小型 |
| 共享表 | ★★☆☆☆ | ★★★☆☆ | ★★★★★ | 小型 |
| 分层架构 | ★★★★★ | ★★★★★ | ★★☆☆☆ | 大型 |
推荐方案: 采用独立秒杀表。
实施要点:
-
秒杀库存:
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 -
限购控制:
Redis Set记录已购买用户: key: seckill:bought:{seckill_id} value: Set<user_id> 检查: if (redis.sismember(key, user_id)) { return "已购买,不能重复购买"; } 记录: redis.sadd(key, user_id); -
排队机制:
流程: 1. 用户点击"立即抢购" 2. 请求进入队列(Kafka) 3. 显示排队位置 4. Worker消费队列,限速扣减库存 5. 扣减成功,通知用户 6. 扣减失败,提示已售罄 优点: - 削峰 - 用户体验可控 - 系统稳定 -
防黄牛:
策略1:验证码 - 点击抢购后弹出验证码 - 通过验证才能提交订单 策略2:实人认证 - 首次参与秒杀需要实人认证 - 人脸识别 策略3:行为分析 - 检测异常高频请求 - IP黑名单 - 设备指纹 -
价格展示:
商品详情页: - 正常价:¥999(划线价) - 秒杀价:¥199(红色突出显示) - 倒计时:距开始还剩 01:23:45 - 提醒:每人限购1件
延伸思考:
- 秒杀订单未支付如何处理(是否释放库存)?
- 秒杀活动如何预热(提前加载数据)?
- 秒杀流量如何监控和应急处理?
💡 题目6:动态定价系统的设计
问题描述: 电商平台希望实现动态定价(如机票、酒店根据供需实时调价)。如何设计动态定价系统?
答案:
问题分析: 动态定价的核心要素:
- 定价因子(库存、时间、竞争对手、需求)
- 定价策略(规则还是算法)
- 价格变动频率
- 用户体验(频繁变价影响用户信任)
方案一:规则引擎定价
核心思想: 根据预设规则调整价格。
规则示例:
规则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测试 | ★★★★☆ | ★★★★☆ | ★★★★☆ | 新品 |
推荐方案: 采用规则引擎+算法定价的混合方案。
实施要点:
-
定价数据收集:
price_history(价格历史) ├── sku_id ├── price ├── stock ├── sales_quantity(该价格下的销量) ├── conversion_rate(转化率) ├── start_time └── end_time competitor_price(竞对价格) ├── sku_id ├── competitor_name ├── price ├── crawled_at └── ... -
定价决策流程:
定时任务(每小时): 1. 收集数据(库存、销量、竞对价格) 2. 输入定价模型 3. 模型输出推荐价格 4. 人工审核(可选) 5. 更新商品价格 6. 记录价格变更日志 -
价格锁定:
用户加购物车: - 锁定当前价格15分钟 - 15分钟内下单按锁定价 - 超时按最新价 或: - 不锁定价格 - 下单时实时计算(用户体验差) -
价格展示:
对用户: - 显示当前价 - 历史最低价(增加紧迫感) - 降价通知(用户订阅) 对运营: - 价格趋势图 - 竞对价格对比 - 销量-价格关系 -
价格保护:
规则: - 单次调价幅度 <= 20% - 每天最多调价3次 - 价格不低于成本价 × 1.1(保证毛利) - 价格不高于市场价 × 1.5(防止离谱)
延伸思考:
- 如何处理用户对频繁变价的不满?
- 价格歧视(同一商品不同用户不同价)的法律风险?
- 如何设计价格保护机制(买贵退差价)?
📊 题目7:跨境电商的汇率和税费计算
问题描述: 跨境电商需要处理多币种和不同国家的税费。如何设计汇率转换和税费计算系统?
答案:
问题分析: 跨境价格的核心要素:
- 汇率实时变动
- 不同国家税率不同(关税、增值税)
- 币种展示(用户看到本地币种)
- 结算币种(实际收款币种)
方案一:实时汇率
核心思想: 每次计算价格时查询实时汇率。
设计:
价格计算:
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
优点:
- 平衡稳定性和准确性
- 减少价格变化频率
缺点:
- 实现复杂度高
方案对比:
| 方案 | 准确性 | 稳定性 | 用户体验 | 实施难度 |
|---|---|---|---|---|
| 实时汇率 | ★★★★★ | ★★☆☆☆ | ★★☆☆☆ | ★★★☆☆ |
| 固定汇率 | ★★★☆☆ | ★★★★★ | ★★★★★ | ★★★★☆ |
| 浮动区间 | ★★★★☆ | ★★★★☆ | ★★★★☆ | ★★☆☆☆ |
推荐方案: 采用固定汇率(每日更新)。
实施要点:
-
汇率管理:
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); } } -
税费计算:
tax_rule(税费规则) ├── country_code ├── category_id ├── import_duty_rate(关税率) ├── vat_rate(增值税率) ├── min_tax_free_amount(免税额) └── ... 示例: 中国: - 关税:10% - 增值税:13% - 免税额:¥5000以下免税 美国: - 关税:0% - 州税:0-10%(各州不同) -
价格展示:
商品页展示: - 商品价格:$100 - 运费:$20 - 关税:$10(预估) - 总计:$130(约¥936) 结算页: - 确认最终价格(包含税费) - 币种选择(CNY/USD) -
结算币种:
策略1:统一结算币种 - 平台统一收USD - 用户支付CNY → 银行自动换汇 策略2:多币种账户 - 平台有USD、CNY、EUR账户 - 用户付CNY → 直接入CNY账户 - 减少汇兑成本 -
汇率风险对冲:
风险: - 用户下单时汇率7.2 - 商家收款时汇率7.0 - 平台损失2% 对冲策略: - 购买外汇期货 - 设置汇率浮动保护(±1%) - 及时结汇
延伸思考:
- 如何设计多币种支付(用户用USD支付CNY订单)?
- 汇率变化导致退款金额不一致如何处理?
- 跨境税费如何合规申报?
🔧 题目8:组合促销的价格计算(满减+折扣+券)
问题描述: 用户下单时同时享受满减(满200减30)、商品折扣(9折)、优惠券(20元)。如何设计组合促销的价格计算逻辑?
答案:
问题分析: 组合促销的核心挑战:
- 计算顺序(先满减还是先折扣影响最终价)
- 规则冲突(有些促销不能同时用)
- 最优组合(如何选择让用户优惠最大)
- 性能(实时计算)
方案一:固定计算顺序
核心思想: 规定促销的固定计算顺序。
计算顺序:
原价:¥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
优点:
- 平衡灵活性和性能
- 运营可配置
缺点:
- 可能不是全局最优
方案对比:
| 方案 | 最优性 | 性能 | 灵活性 | 实施难度 |
|---|---|---|---|---|
| 固定顺序 | ★★☆☆☆ | ★★★★★ | ★★☆☆☆ | ★★★★★ |
| 最优组合 | ★★★★★ | ★★★☆☆ | ★★★★★ | ★★☆☆☆ |
| 优先级+互斥 | ★★★★☆ | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
推荐方案: 采用优先级+互斥,必要时计算最优组合。
实施要点:
-
促销配置:
promotion ├── promotion_id ├── name ├── type(DISCOUNT/FULL_REDUCE/COUPON) ├── priority(优先级) ├── exclusive_group(互斥组,NULL表示可叠加) ├── stackable(是否可叠加) └── ... -
价格明细:
订单价格明细: { "originalPrice": 500, "appliedPromotions": [ { "name": "商品9折", "discountAmount": 50, "afterPrice": 450 }, { "name": "满200减30", "discountAmount": 30, "afterPrice": 420 }, { "name": "优惠券", "discountAmount": 20, "afterPrice": 400 } ], "finalPrice": 400 } 用户可见每一步的优惠 -
试算接口:
POST /api/price/preview { "items": [...], "promotions": [...], "coupon": "SUMMER20" } 响应: { "scenarios": [ { "name": "推荐方案", "finalPrice": 400, "savings": 100, "appliedPromotions": [...] }, { "name": "仅用优惠券", "finalPrice": 480, "savings": 20, "appliedPromotions": [...] } ] } 让用户选择方案 -
性能优化:
缓存: key: price:calculate:{商品ID}:{促销IDs哈希} value: 计算结果 TTL: 5分钟 避免重复计算
延伸思考:
- 如何向用户推荐最优促销组合?
- 促销规则变更如何保证已下单的订单价格不变?
- 如何设计促销的AB测试?
💡 题目9:预售和定金膨胀的设计
问题描述: 预售活动中,用户支付定金(如50元),尾款时定金可抵100元。如何设计预售和定金膨胀系统?
答案:
问题分析: 预售定金的核心要素:
- 定金不可退(锁定用户)
- 定金膨胀(50元抵100元)
- 尾款支付期限(超时定金不退)
- 库存预占
方案一:双订单模式
核心思想: 定金订单和尾款订单分开。
设计:
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. 实付 = 商品价格 - 抵扣券金额
优点:
- 复用现有优惠券系统
- 灵活
缺点:
- 定金和尾款割裂
- 用户可能不理解
方案对比:
| 方案 | 清晰度 | 实施难度 | 用户体验 | 适用场景 |
|---|---|---|---|---|
| 双订单 | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ | 复杂预售 |
| 单订单分阶段 | ★★★★★ | ★★★★☆ | ★★★★★ | 通用 |
| 虚拟商品 | ★★☆☆☆ | ★★★★☆ | ★★☆☆☆ | 简单预售 |
推荐方案: 采用单订单分阶段。
实施要点:
-
定金膨胀计算:
商品总价:¥999 定金:¥50 定金膨胀:¥100(2倍膨胀) 尾款:¥999 - ¥100 = ¥899 用户总共支付:¥50 + ¥899 = ¥949(省¥50) -
库存管理:
定金支付成功: - 预占库存(reserved_stock +1) - 锁定到该订单 尾款支付成功: - 确认库存(sold_stock +1, reserved_stock -1) 超时未付尾款: - 释放库存(reserved_stock -1) - 定金不退 -
尾款提醒:
提醒策略: - 尾款开始:立即推送 - 尾款截止前3天:提醒 - 尾款截止前1天:紧急提醒 - 尾款截止前1小时:最后提醒 提醒渠道: - App推送 - 短信 - 站内信 -
超时处理:
定时任务(每小时): 1. 扫描超时未付尾款的订单 2. 订单状态 → CLOSED 3. 释放库存 4. 定金记为平台收入(不退) 5. 通知用户 -
退款规则:
规则: - 支付定金后,不可取消订单 - 定金不退 - 尾款支付后,可申请退款 - 退款金额 = 定金 + 尾款
延伸思考:
- 如何防止用户恶意付定金占用库存?
- 预售商品如何设置发货时间?
- 定金膨胀活动如何设计ROI分析?
📊 题目10:价格歧视与个性化定价的设计
问题描述: 电商平台希望根据用户画像(新老用户、购买力、价格敏感度)实现个性化定价。如何设计价格歧视系统?同时如何规避法律风险?
答案:
问题分析: 个性化定价的核心要素:
- 用户分层(高价值、普通、价格敏感)
- 定价策略(不同用户看到不同价格)
- 法律风险(价格歧视在某些国家违法)
- 用户信任(发现差价后的负面影响)
方案一:明面价格歧视(不推荐)
核心思想: 不同用户直接看到不同价格。
示例:
用户A(新用户):¥99
用户B(老用户):¥129
用户C(高价值用户):¥149
价格查询:
price = getPriceByUser(skuId, userId);
优点:
- 简单直接
- 收益最大化
缺点:
- 法律风险大(违反价格法)
- 用户信任崩塌(发现后口碑崩盘)
- 媒体曝光风险
方案二:差异化优惠(推荐)
核心思想: 价格统一,但不同用户获得不同优惠。
设计:
基础价格:统一¥129
新用户:
- 新人专享券:¥30
- 实付:¥99
普通用户:
- 无优惠
- 实付:¥129
高价值用户:
- 会员折扣:9折
- 实付:¥116
关键:
- 价格统一展示
- 优惠透明(标注"新人专享"、"会员专享")
优点:
- 合法合规
- 用户可接受
- 价格透明
缺点:
- 收益优化程度不如价格歧视
方案三:隐性定价(灰色地带)
核心思想: 通过算法展示不同的商品推荐和排序。
策略:
高价值用户:
- 推荐高价商品
- 搜索结果优先展示高价商品
价格敏感用户:
- 推荐促销商品
- 搜索结果优先展示低价商品
不直接改价格,但影响用户选择
优点:
- 间接影响购买
- 法律风险小
缺点:
- 效果不如直接定价
- 算法复杂
方案对比:
| 方案 | 收益 | 合规性 | 用户信任 | 风险 |
|---|---|---|---|---|
| 明面歧视 | ★★★★★ | ★☆☆☆☆ | ★☆☆☆☆ | ★★★★★ |
| 差异化优惠 | ★★★★☆ | ★★★★★ | ★★★★☆ | ★★☆☆☆ |
| 隐性定价 | ★★★☆☆ | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
推荐方案: 采用差异化优惠。
实施要点:
-
用户分层:
基于RFM模型: - R(最近一次购买) - F(购买频次) - M(购买金额) 用户分层: - 高价值用户(VIP):R<30天, F>10次, M>1万 - 活跃用户:R<90天, F>3次 - 沉睡用户:R>90天 - 新用户:注册<30天,F=0 - 价格敏感用户:经常搜索低价、使用优惠券 -
差异化优惠策略:
新用户: - 新人专享券(大额) - 首单免运费 - 新人专区(低价引流商品) 沉睡用户: - 唤醒券(定向发放) - "好久不见,给你优惠" 高价值用户: - 会员折扣 - 生日礼包 - 专属客服 价格敏感用户: - 推荐促销商品 - 凑单优惠 -
透明化展示:
商品页: - 价格:¥129(统一价格) - 您的优惠: ✓ 新人券:-¥30 ✓ 首单免运费 - 实付:¥99 标注优惠来源,避免误解 -
法律合规:
避免: - 同一商品同一时间不同价格(价格歧视) - 隐藏真实价格 - 大数据杀熟 合法: - 不同用户不同优惠(促销活动) - 会员专享价(明确标注) - 新人优惠(限定条件) -
监控与风控:
监控指标: - 用户投诉率(价格差异投诉) - 媒体舆情 - 价格离散度(同商品价格差异) 风控: - 价格差异 < 30% - 优惠透明化 - 避免同一用户看到不同价格
延伸思考:
- 如何平衡个性化定价和用户信任?
- 用户发现价格差异后如何应对?
- 如何设计价格歧视的AB测试(避免法律风险)?
第三部分:交易核心链路(50题)
3.1 搜索与导购(10题)
📊 题目1:电商搜索引擎的架构设计
问题描述: 电商平台每天有百万级搜索请求,需要支持全文搜索、属性筛选、排序。如何设计电商搜索引擎的整体架构?
答案:
问题分析: 电商搜索的核心要素:
- 海量数据(千万级商品)
- 复杂查询(关键词+品类+价格区间+品牌)
- 实时性(商品上下架实时更新)
- 相关性排序(搜索“手机“优先展示热门手机)
- 性能要求(毫秒级响应)
方案一:基于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。
实施要点:
-
索引设计:
索引名称:products_v1 分片数:5(根据数据量调整) 副本数:2(高可用) 字段类型选择: - keyword:不分词(品牌、类目ID) - text:分词(标题、描述) - nested:嵌套对象(属性列表) -
数据同步:
实时同步: - 商品创建/更新 → 发送Kafka消息 - 同步Worker消费消息 → 更新ES - 延迟 < 5秒 全量同步(兜底): - 每天凌晨全量同步 - 对比MySQL和ES差异 - 修复不一致数据 -
搜索优化:
查询缓存: - 热门搜索词缓存(Redis) - TTL 5分钟 搜索建议: - 输入"iph" → 建议"iPhone 15" - 使用completion suggester 拼写纠错: - 输入"ipone" → 自动纠正为"iPhone" -
性能优化:
分页优化: - 浅分页:from+size(前10页) - 深分页:search_after(10页以后) 字段裁剪: - 只返回必要字段 - _source: ["productId", "title", "price"] 路由优化: - 按类目路由到不同分片 -
监控告警:
监控指标: - 搜索QPS - 搜索延迟P99 - ES集群健康度 - 索引大小 告警: - 搜索延迟 > 500ms - ES集群RED状态 - 数据同步延迟 > 1分钟
延伸思考:
- 如何设计搜索的AB测试(不同排序策略)?
- 搜索无结果时如何处理(推荐、纠错)?
- 如何防止恶意搜索(刷流量、爬虫)?
🔧 题目2:搜索相关性排序算法设计
问题描述: 用户搜索“手机“,返回1000个结果,如何排序保证用户最想要的商品排在前面?请设计相关性排序算法。
答案:
问题分析: 相关性排序的核心要素:
- 文本相关性(标题匹配度)
- 商品热度(销量、点击量)
- 商品质量(评分、评价数)
- 商品新鲜度(新品)
- 个性化(用户偏好)
方案一:单一得分排序
核心思想: 只按一个维度排序(如销量)。
实现:
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. 在线预测:
- 搜索返回候选商品
- 模型预测点击率
- 按预测得分排序
优点:
- 效果最优
- 自动学习最优权重
- 支持个性化
缺点:
- 需要算法团队
- 需要大量训练数据
- 冷启动问题
方案对比:
| 方案 | 效果 | 实施难度 | 计算成本 | 个性化 |
|---|---|---|---|---|
| 单一得分 | ★★☆☆☆ | ★★★★★ | ★★★★★ | ★☆☆☆☆ |
| 多因子加权 | ★★★★☆ | ★★★☆☆ | ★★★★☆ | ★★☆☆☆ |
| 机器学习 | ★★★★★ | ★★☆☆☆ | ★★★☆☆ | ★★★★★ |
推荐方案: 采用多因子加权,逐步引入机器学习。
实施要点:
-
初期(多因子加权):
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; } -
权重调优:
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) - 用户停留时长 选择效果最好的权重 -
个性化因子:
用户偏好品牌: 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; } -
排序规则:
规则1:置顶广告位 - 前3个位置:竞价广告 - 标注"广告" 规则2:新品扶持 - 7天内新品得分 × 1.5 规则3:库存保护 - 库存 < 10件,降权(× 0.8) - 避免缺货商品排前面 -
监控与迭代:
监控指标: - 搜索结果点击率 - 搜索转化率 - 平均点击位置 定期优化: - 每月分析数据 - 调整权重 - 新增因子
延伸思考:
- 如何处理搜索作弊(刷销量、刷好评)?
- 长尾商品如何获得曝光机会?
- 如何设计搜索排序的解释性(为何这个商品排第一)?
💡 题目3:搜索建议(Suggest)的实现
问题描述: 用户输入“iph“,搜索框下方实时展示“iPhone 15“、“iPhone 14“等建议。如何实现搜索建议功能?
答案:
问题分析: 搜索建议的核心要素:
- 实时性(输入即显示)
- 准确性(建议与输入相关)
- 热度排序(热门建议优先)
- 性能(毫秒级响应)
方案一:数据库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。
实施要点:
-
数据准备:
建议词来源: - 热门搜索词(用户历史搜索) - 商品标题(高销量商品) - 品牌名称 - 类目名称 - 运营配置词(促销活动) 权重设置: - 用户搜索频次作为权重 - 权重 = log(search_count + 1) -
拼音支持:
安装pinyin分词器: - elasticsearch-analysis-pinyin 索引配置: { "keyword": { "type": "completion", "analyzer": "pinyin_analyzer" } } 输入"pingguo" → 建议"苹果"、"iPhone" -
个性化建议:
用户维度: - 记录用户搜索历史(Redis) - 优先展示用户历史搜索 示例: 用户输入"ip" → ES返回:["iPhone 15", "iPad Pro", "iPod"] → 叠加用户历史:["iPhone 14"(历史搜索), "iPhone 15", "iPad Pro"] → 最终展示前10个 -
缓存策略:
热门建议缓存: - 缓存TOP 1000热门前缀的建议结果 - key: suggest:iph - value: ["iPhone 15", "iPhone 14", ...] - TTL: 10分钟 减少ES压力 -
建议词更新:
实时更新: - 用户搜索 → Kafka → 统计Worker → 更新ES 定时更新(每小时): - 统计最近1小时热搜词 - 更新权重 - 新增热搜词
延伸思考:
- 如何防止建议词中的敏感词?
- 搜索建议如何支持纠错(ipone → iPhone)?
- 如何设计多语言的搜索建议?
📊 题目4:商品筛选和多维度过滤的设计
问题描述: 用户搜索“手机“后,可以按品牌、价格区间、屏幕尺寸、内存等多个维度筛选。如何设计筛选系统?
答案:
问题分析: 筛选系统的核心要素:
- 动态筛选项(不同类目的筛选项不同)
- 多条件组合(品牌AND价格区间AND内存)
- 筛选项计数(显示每个选项的商品数量)
- 性能(实时计算筛选结果)
方案一:前端筛选
核心思想: 一次性返回所有结果,前端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)。
实施要点:
-
筛选项配置:
category_filter_config(类目筛选配置) ├── category_id ├── filter_name(品牌、价格、属性名) ├── filter_type(TERM/RANGE/NESTED) ├── display_order(展示顺序) └── ... 示例: 手机类目: - 品牌(TERM) - 价格(RANGE: 0-1000, 1000-3000, ...) - 屏幕尺寸(NESTED: attrs.屏幕尺寸) - 内存(NESTED: attrs.内存) -
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); } -
前端交互:
URL设计: /search?q=手机&brand=Apple,小米&price=5000-10000&memory=8GB 前端: - 用户点击筛选项 → 更新URL → 请求后端 - 后端返回筛选结果 + 筛选项计数 - 前端更新展示 已选筛选展示: - 品牌:Apple × 小米 × - 价格:5000-10000 × - 内存:8GB × 点击 × 取消该筛选 -
性能优化:
筛选缓存: key: search:q=手机&brand=Apple&price=5000-10000 value: {商品列表, 筛选项计数} TTL: 5分钟 热门筛选组合预加载 -
筛选项排序:
排序规则: 1. 按配置的display_order 2. 品牌按热度(商品数量) 3. 价格区间固定顺序(低到高) 4. 属性按字母顺序
延伸思考:
- 如何设计筛选项的动态展示(只显示有商品的筛选项)?
- 筛选条件过多时如何优化性能?
- 如何设计筛选的撤销和重置功能?
🔧 题目5:搜索结果的无结果优化
问题描述: 用户搜索“iPhne 15“(拼写错误),没有结果。如何优化无结果页,提升用户体验?
答案:
问题分析: 无结果场景:
- 拼写错误(iPhne → iPhone)
- 搜索词过于精确(“iPhone 15 Pro Max 256GB 深空黑色”)
- 商品确实不存在
- 分词问题
优化策略:
- 自动纠错
- 模糊搜索
- 推荐相关商品
- 引导用户
方案一:简单提示
核心思想: 直接提示“没有找到相关商品“。
优点:
- 实现简单
缺点:
- 用户体验差
- 流失率高
方案二:拼写纠错(推荐)
核心思想: 检测拼写错误,自动纠正或建议正确词。
算法:
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
推荐商品:
[展示热销手机]
方案对比:
| 方案 | 用户体验 | 转化率 | 实施难度 |
|---|---|---|---|
| 简单提示 | ★☆☆☆☆ | ★☆☆☆☆ | ★★★★★ |
| 拼写纠错 | ★★★★☆ | ★★★★☆ | ★★★☆☆ |
| 模糊搜索+推荐 | ★★★★★ | ★★★★★ | ★★☆☆☆ |
| 引导式 | ★★★★☆ | ★★★☆☆ | ★★★★☆ |
推荐方案: 采用拼写纠错+模糊搜索+推荐的组合。
实施要点:
-
纠错流程:
用户搜索 → 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); } -
纠错词库:
来源: - 用户搜索日志(搜索A无结果,搜索B有结果) - 商品标题词库 - 品牌名称 - 常见错误(人工维护) 存储: spell_correction ├── wrong_word(错误词) ├── correct_word(正确词) ├── correction_count(纠正次数) └── ... -
模糊搜索策略:
策略1:降低匹配度要求 minimum_should_match: "75%"(原本100%) 策略2:增加同义词 "手机" = "智能手机" = "移动电话" 策略3:分词后部分匹配 "iPhone 15 Pro Max" → ["iPhone", "15", "Pro", "Max"] 匹配任意3个词即可 -
推荐策略:
推荐来源: 1. 类目热销(如果能识别类目) 2. 全站热销(兜底) 3. 相关搜索("其他用户还搜索了...") 4. 促销商品(引导转化) -
监控优化:
监控指标: - 无结果搜索率(无结果搜索数/总搜索数) - 无结果页跳出率 - 纠错成功率 目标: - 无结果搜索率 < 5% - 无结果页跳出率 < 50%
延伸思考:
- 如何处理恶意搜索(脏词、广告)?
- 无结果搜索如何用于商品补货建议?
- 如何设计多语言搜索的纠错?
(继续生成后续5题…)
由于内容较长,我将分批次完成。继续生成3.1的剩余5题:
📊 题目6:搜索日志分析与优化
问题描述: 电商平台每天产生百万级搜索日志,如何分析搜索日志,发现问题并优化搜索体验?
答案:
问题分析: 搜索日志分析的核心目标:
- 发现热门搜索词
- 识别无结果搜索
- 分析用户搜索路径
- 优化搜索排序
推荐方案:
数据收集:
搜索日志表:
search_log
├── log_id
├── user_id
├── keyword(搜索词)
├── result_count(结果数量)
├── clicked_products(点击的商品ID列表)
├── converted(是否转化购买)
├── search_time
└── session_id
分析维度:
-
热门搜索词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; 用途: - 运营决策(备货) - 搜索建议(热词优先展示) - 广告投放 -
无结果搜索分析:
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; 优化方向: - 拼写纠错词库补充 - 商品补货建议 - 同义词扩展 -
点击率分析:
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关键词 → 排序策略需要优化 -
转化漏斗:
搜索 → 点击 → 加购 → 下单 → 支付 分析每个环节的转化率,找到瓶颈
延伸思考:
- 如何识别恶意搜索(刷流量)?
- 搜索日志如何用于个性化推荐?
- 如何设计搜索AB测试平台?
🔧 题目7:跨境电商的多语言搜索
问题描述: 跨境电商支持中文、英文、日文搜索。如何设计多语言搜索系统?
答案:
问题分析: 多语言搜索的核心挑战:
- 不同语言分词规则不同
- 用户可能用中文搜英文商品
- 同义词跨语言匹配
推荐方案:
-
多语言索引:
{ "mappings": { "properties": { "title": { "properties": { "zh": {"type": "text", "analyzer": "ik_max_word"}, "en": {"type": "text", "analyzer": "english"}, "ja": {"type": "text", "analyzer": "kuromoji"} } } } } } -
语言检测:
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); } -
跨语言搜索:
用户输入中文"手机",也能搜到英文标题"phone" 方案:翻译API - 调用翻译API(Google Translate) - keyword="手机" → translate → "phone" - 搜索中文字段 OR 英文翻译
延伸思考:
- 如何处理多语言同义词?
- 不同国家的搜索习惯差异如何处理?
💡 题目8:搜索性能优化
问题描述: 搜索响应时间P99达到2秒,用户体验差。如何优化搜索性能到100ms以内?
答案:
问题分析: 搜索慢的常见原因:
- ES查询复杂(深度分页、大量聚合)
- 索引设计不合理
- 数据量大
- 网络延迟
优化方案:
-
查询优化:
避免深度分页: ❌ from=10000, size=20(跳过1万条数据) ✅ search_after(游标分页) 减少聚合计算: ❌ 聚合100个字段 ✅ 聚合最常用的10个字段 字段裁剪: ❌ 返回所有字段 ✅ _source: ["id", "title", "price"] -
缓存策略:
热门搜索缓存: key: search:q=iPhone&page=1 value: {商品列表} TTL: 5分钟 命中率:70%+ -
索引优化:
分片数量: - 单分片大小:20-50GB - 过多分片影响性能 副本数量: - 副本数=2(高可用+读负载均衡) Segment合并: - 定期force_merge减少segment数量
延伸思考:
- 如何设计搜索的降级方案(ES故障)?
- 搜索性能如何监控和告警?
📊 题目9:智能搜索(NLP+AI)
问题描述: 用户搜索“适合送女朋友的礼物“,如何理解用户意图,推荐合适商品?
答案:
问题分析: 传统搜索只能匹配关键词,无法理解语义。
解决方案:
-
意图识别:
NLP分析: "适合送女朋友的礼物" → 意图:礼物推荐 → 对象:女性 → 场景:送礼 映射到类目: - 珠宝首饰 - 化妆品 - 鲜花 -
语义搜索:
使用BERT等模型: - 将搜索词编码为向量 - 商品标题也编码为向量 - 计算向量相似度 - 按相似度排序
延伸思考:
- 如何训练电商领域的语义模型?
- 语义搜索如何与传统搜索结合?
🔧 题目10:搜索结果的多样性优化
问题描述: 用户搜索“手机“,前10个结果都是iPhone,缺乏多样性。如何优化搜索结果的多样性?
答案:
问题分析: 多样性不足的问题:
- 马太效应(热门商品更热门)
- 用户需求多样,不都想要iPhone
- 影响长尾商品曝光
优化方案:
-
品牌打散:
规则:前10个结果中,同一品牌最多出现3次 算法: 1. 按相关性排序 2. 遍历结果,统计品牌出现次数 3. 如果某品牌超过阈值,跳过该商品,选下一个 -
MMR算法(最大边际相关性):
score = λ × relevance - (1-λ) × max_similarity relevance: 与查询的相关性 max_similarity: 与已选结果的最大相似度 λ: 权衡参数(0.7) 每次选择score最高的商品,保证相关性和多样性 -
类目多样性:
前10个结果覆盖2-3个子类目 - 智能手机(5个) - 老人机(3个) - 游戏手机(2个)
延伸思考:
- 多样性和相关性如何权衡?
- 如何评估搜索结果的多样性?
3.2 购物车与结算(15题)
📊 题目1:购物车的数据存储设计
问题描述: 用户将商品加入购物车,需要跨设备同步(手机APP、Web、小程序)。如何设计购物车的存储方案?
答案:
问题分析: 购物车的核心要素:
- 跨设备同步
- 用户未登录也能加购
- 数据持久化
- 高并发读写
方案一: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双写。
实施要点:
-
未登录用户:
未登录: - 生成临时cart_id(存Cookie) - 购物车数据存Redis - key: cart:temp:{cart_id} 登录后: - 合并临时购物车到用户购物车 - 删除临时购物车 -
购物车合并:
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); } -
失效商品处理:
商品失效场景: - 商品下架 - 商品删除 - 库存不足 展示: - 失效商品置灰 - 提示"商品已下架" - 提供"删除"或"移入收藏"选项 -
购物车清理:
定时任务(每天凌晨): - 删除90天未更新的购物车 - 减少存储成本 -
购物车同步:
跨设备同步: - 用户在APP加购 → 写Redis+MySQL - 用户在Web打开 → 读Redis → 显示购物车 实时同步(WebSocket): - 用户在设备A加购 - 推送到设备B - 设备B实时更新购物车数量
延伸思考:
- 购物车数量显示在导航栏,如何实时更新?
- 如何处理购物车中的促销信息过期?
- 购物车数据如何备份和恢复?
🔧 题目2:购物车的价格计算
问题描述: 购物车中有多个商品,每个商品可能有不同促销(满减、折扣、优惠券)。如何设计购物车的实时价格计算?
答案:
问题分析: 购物车价格计算的复杂性:
- 多商品组合
- 多种促销叠加
- 实时计算(用户修改数量即刻更新)
- 价格明细展示
推荐方案:
价格计算引擎:
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分钟)
延伸思考:
- 购物车价格和下单后价格不一致如何处理?
- 大促时购物车价格计算如何优化性能?
💡 题目3:购物车的推荐功能
问题描述: 用户购物车中有商品A,如何推荐相关商品B,提升客单价?
答案:
推荐策略:
-
关联推荐:
"买了还买": - 统计购买商品A的用户还购买了哪些商品 - 推荐高频商品 示例: 购物车有"iPhone 15" → 推荐"手机壳"、"钢化膜"、"充电器" -
凑单推荐:
购物车总价¥180 满¥200减¥30 推荐:再买¥20-30的商品,即可享受优惠 -
替代推荐:
购物车中商品缺货 → 推荐同类商品
延伸思考:
- 购物车推荐如何避免打扰用户?
- 推荐商品点击率如何提升?
📊 题目4:购物车的库存校验
问题描述: 用户加购物车时商品有货,结算时可能已无货。如何设计购物车的库存校验机制?
答案:
校验时机:
-
加购时校验:
用户点击"加入购物车" → 检查库存 库存充足 → 允许加购 库存不足 → 提示"库存不足" -
结算时校验:
用户点击"去结算" → 1. 批量查询购物车所有商品库存 2. 标记缺货商品 3. 展示: - 有货商品(可结算) - 缺货商品(置灰,不可结算) -
实时推送:
商品库存变化(如售罄) → WebSocket推送 前端实时更新购物车状态
延伸思考:
- 购物车中的商品是否需要预占库存?
- 库存不足时如何引导用户?
🔧 题目5:购物车的性能优化
问题描述: 大促期间,购物车服务QPS达10万+,如何优化购物车性能?
答案:
优化方案:
-
读写分离:
写操作(加购、删除): - 写MySQL主库 - 异步同步到Redis 读操作(查询购物车): - 读Redis(快) - 未命中读MySQL从库 -
批量操作:
❌ 单个加购:N次请求 ✅ 批量加购:1次请求 POST /api/cart/batch-add { "items": [ {"skuId": "123", "quantity": 2}, {"skuId": "456", "quantity": 1} ] } -
本地缓存:
热点用户购物车: - 加载到应用服务器内存 - 减少Redis访问 -
限流降级:
限流: - 单用户购物车操作频率限制(10次/分钟) 降级: - Redis故障 → 降级到MySQL - MySQL故障 → 只读模式(不能加购)
延伸思考:
- 购物车数据如何分片(sharding)?
- 购物车服务如何实现高可用?
📊 题目6:购物车商品失效的处理策略
问题描述: 用户购物车中的商品可能因为下架、删除、库存清零而失效。如何设计失效商品的处理策略,优化用户体验?
答案:
问题分析: 商品失效场景:
- 商品下架(运营操作)
- 商品删除(商品不再销售)
- 库存售罄(暂时缺货)
- 商品涨价(价格变动)
- 促销过期(活动结束)
方案一:定时批量检测
核心思想: 定时任务扫描购物车,标记失效商品。
实现:
定时任务(每小时):
1. 查询所有购物车商品
2. 批量查询商品状态
3. 标记失效商品
4. 更新购物车
优点:
- 批量处理,效率高
- 服务器压力均匀
缺点:
- 实时性差(最长延迟1小时)
- 用户可能看到失效商品
方案二:实时校验(推荐)
核心思想: 用户打开购物车时,实时校验商品状态。
流程:
用户打开购物车 →
1. 查询购物车商品列表
2. 批量查询商品最新状态(Redis缓存)
3. 分类展示:
- 正常商品(可结算)
- 失效商品(置灰,不可结算)
4. 标注失效原因
失效商品展示:
[置灰显示]
iPhone 15 Pro 256GB
¥7999
状态:该商品已下架
操作:[删除] [移入收藏夹]
优点:
- 实时性好
- 用户体验清晰
缺点:
- 每次打开购物车都校验
- QPS增加
方案三:消息推送
核心思想: 商品状态变化时,主动推送更新购物车。
架构:
商品下架 →
发布事件(Kafka)→
购物车Worker消费 →
1. 查询包含该商品的购物车
2. 标记商品为失效
3. WebSocket推送用户(如果在线)
优点:
- 实时性最好
- 用户感知及时
缺点:
- 架构复杂
- 需要消息队列
方案对比:
| 方案 | 实时性 | 用户体验 | 实施难度 | 系统负载 |
|---|---|---|---|---|
| 定时检测 | ★★☆☆☆ | ★★★☆☆ | ★★★★★ | ★★★★☆ |
| 实时校验 | ★★★★☆ | ★★★★★ | ★★★★☆ | ★★★☆☆ |
| 消息推送 | ★★★★★ | ★★★★★ | ★★☆☆☆ | ★★★★☆ |
推荐方案: 采用实时校验+消息推送的组合。
实施要点:
-
商品状态缓存:
Redis存储商品状态: key: product:status:{skuId} value: { "onSale": true, "stock": 100, "price": 7999, "promotionId": "xxx", "updatedAt": 1679800000 } TTL: 10分钟 商品变更时主动刷新 -
批量校验优化:
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); } -
失效商品操作:
用户操作: 1. 删除:直接从购物车删除 2. 移入收藏夹: - 加入收藏 - 从购物车删除 - 商品恢复上架时通知用户 3. 查看替代品: - 推荐同类商品 - 一键替换 -
主动通知:
通知策略: - 商品下架 → App推送 "您购物车中的【iPhone 15】已下架" - 商品降价 → App推送 "您购物车中的【iPhone 15】降价了" - 库存恢复 → 收藏夹商品有货通知 -
失效原因分类:
原因分类: - 已下架:运营下架 - 已售罄:库存为0 - 已删除:商品不存在 - 已涨价:价格变动超过10% - 活动结束:促销过期 针对性提示: - 已售罄 → "补货中,可先收藏" - 已涨价 → "当前价格¥xxx,加购时¥xxx"
延伸思考:
- 如何设计购物车的自动清理(失效商品30天后自动删除)?
- 失效商品是否计入购物车数量显示?
- 如何处理部分失效(如只有某个规格缺货)?
🔧 题目7:购物车的跨平台同步设计
问题描述: 用户在手机APP加购商品,打开电脑Web也能看到。如何实现购物车的跨平台实时同步?
答案:
问题分析: 跨平台同步的核心要素:
- 数据一致性(同一购物车)
- 实时性(秒级同步)
- 冲突处理(同时操作)
- 离线支持
方案一:轮询同步
核心思想: 客户端定时轮询服务器,获取最新购物车。
实现:
// 前端定时轮询
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)+ 轮询兜底(不支持时降级)。
实施要点:
-
连接管理:
// 用户连接映射 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))); } } } } -
版本控制:
购物车版本号: - 每次修改version+1 - 客户端记录本地version - 接收推送时检查version - 如果本地version更新,忽略旧推送 冲突解决: - 客户端操作携带version - 服务端CAS更新 - 失败则拉取最新数据重试 -
心跳保活:
// 客户端定时发送心跳 setInterval(() => { if (websocket.readyState === WebSocket.OPEN) { websocket.send(JSON.stringify({type: 'PING'})); } }, 30000); // 每30秒 // 服务端响应心跳 if (message.type === 'PING') { session.sendMessage(new TextMessage('{"type":"PONG"}')); } -
降级策略:
// 检测WebSocket支持 if ('WebSocket' in window) { connectWebSocket(); } else { // 降级到轮询 setInterval(pollCart, 10000); } // WebSocket断开时降级 websocket.onclose = () => { console.log('WebSocket断开,降级到轮询'); setInterval(pollCart, 10000); }; -
离线支持:
离线操作: 1. 用户离线时,操作保存到本地队列 2. 用户上线后,批量同步到服务器 3. 服务器合并操作,返回最终购物车 冲突处理: - 添加:合并数量 - 删除:以最新操作为准 - 修改:以最新操作为准
延伸思考:
- 如何处理网络不稳定导致的频繁重连?
- 跨平台同步如何支持多账号(家庭共享)?
- WebSocket服务如何实现横向扩展?
💡 题目8:购物车推荐算法设计
问题描述: 用户购物车有“iPhone 15“,如何推荐相关商品(配件、保险、AppleCare)提升客单价?
答案:
问题分析: 购物车推荐的核心目标:
- 提升客单价(关联销售)
- 提升转化率(凑单满减)
- 提升用户体验(需要的商品)
推荐策略:
-
关联推荐(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%) -
凑单推荐:
购物车总价:¥180 满减活动:满¥200减¥30 推荐策略: - 推荐价格在¥20-¥50的商品 - 优先推荐与购物车商品相关的 - 标注"再买¥20即享满减" -
类目互补推荐:
购物车有"相机" → 推荐: - 存储卡 - 相机包 - 三脚架 购物车有"婴儿奶粉" → 推荐: - 奶瓶 - 尿不湿 - 湿巾 -
个性化推荐:
基于用户历史: - 用户A经常买Apple产品 → 推荐AppleCare+、AirPods - 用户B价格敏感 → 推荐高性价比配件
实施要点:
-
关联规则挖掘:
# 使用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) -
推荐展示位置:
位置1:购物车下方 "买了还买":展示3-5个商品 位置2:结算页 "凑单优惠":满减差额商品 位置3:加购弹窗 用户加购商品A → 弹窗推荐配件B -
推荐排序:
score = w1 × 关联度 + w2 × 利润率 + w3 × 库存充足度 + w4 × 用户个性化得分 w1=0.4, w2=0.3, w3=0.2, w4=0.1 -
AB测试:
测试维度: - A组:展示3个推荐 - B组:展示5个推荐 - C组:不展示推荐 评估指标: - 推荐点击率 - 推荐加购率 - 客单价提升
延伸思考:
- 推荐商品如何避免干扰用户(显得推销)?
- 推荐算法如何冷启动(新商品无关联数据)?
- 推荐效果如何评估和持续优化?
📊 题目9:购物车的结算流程设计
问题描述: 用户点击“去结算“,进入结算页面,需要选择地址、优惠券、支付方式。如何设计结算流程?
答案:
问题分析: 结算流程的核心环节:
- 确认商品(数量、价格)
- 选择收货地址
- 选择配送方式
- 应用优惠(优惠券、积分)
- 选择支付方式
- 提交订单
方案一:单页结算
核心思想: 所有信息在一个页面完成。
页面布局:
结算页:
┌─────────────────┐
│ 1. 收货地址 │
│ [北京市朝阳区...] │
├─────────────────┤
│ 2. 商品清单 │
│ iPhone 15 × 1 │
│ ¥7999 │
├─────────────────┤
│ 3. 配送方式 │
│ ○ 标准配送(免费)│
│ ○ 次日达(¥10) │
├─────────────────┤
│ 4. 优惠 │
│ 优惠券:¥30 │
│ 积分抵扣:¥10 │
├─────────────────┤
│ 5. 支付方式 │
│ ○ 支付宝 │
│ ○ 微信支付 │
├─────────────────┤
│ 总计:¥7959 │
│ [提交订单] │
└─────────────────┘
优点:
- 流程简洁
- 一目了然
- 减少跳转
缺点:
- 页面信息多
- 移动端显示困难
方案二:分步结算(推荐)
核心思想: 分多个步骤完成结算。
流程:
步骤1:选择地址
→ 步骤2:确认商品和配送
→ 步骤3:选择优惠
→ 步骤4:支付
优点:
- 逻辑清晰
- 移动端友好
- 可保存中间状态
缺点:
- 步骤多
- 可能流失
推荐方案: PC端使用单页结算,移动端使用分步结算。
实施要点:
-
结算前校验:
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); } -
地址选择:
展示用户地址列表: - 默认地址(置顶) - 最近使用地址 - 其他地址 新增地址: - 省市区三级联动 - 详细地址输入 - 联系人和电话 - 设为默认地址 -
优惠券选择:
展示可用优惠券: - 按优惠力度排序 - 标注"最优"推荐 - 显示使用门槛 自动选择: - 默认选择优惠最大的券 - 用户可手动切换 不可用优惠券: - 置灰显示 - 标注不可用原因(如"不满足使用条件") -
价格实时计算:
// 监听用户操作 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 }); }; -
订单确认信息:
最终确认页展示: - 收货人:张三 138****1234 - 收货地址:北京市朝阳区xxx - 商品清单:iPhone 15 × 1 - 配送方式:标准配送(预计3天送达) - 优惠明细: * 商品折扣:-¥100 * 满减优惠:-¥30 * 优惠券:-¥20 - 实付金额:¥7849 用户确认无误后点击"提交订单"
延伸思考:
- 如何设计结算页的防重复提交?
- 结算过程中价格变动如何处理?
- 结算流程如何优化转化率?
🔧 题目10:购物车的分享功能设计
问题描述: 用户想分享购物车给朋友(如“帮我看看这些商品怎么样“),如何设计购物车分享功能?
答案:
问题分析: 购物车分享的核心场景:
- 征求意见(送礼选择)
- 代购(帮朋友买)
- 拼单(一起买更便宜)
方案一:生成分享链接
核心思想: 生成唯一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:只读分享 - 生成分享链接 - 朋友只能查看,不能修改 - 可一键导入到自己购物车 类型2:协同编辑 - 创建共享购物车 - 邀请成员 - 成员可添加/删除商品 -
分享页面设计:
分享页头部: "张三分享了购物车给你" 商品列表: [展示所有商品] 操作按钮: - [全部加入我的购物车] - [选择部分加入] - [保存为我的收藏清单] -
隐私控制:
隐私选项: - 公开:任何人都可查看 - 仅好友:需要登录且是好友 - 密码保护:需要输入密码 敏感信息隐藏: - 不显示价格(可选) - 不显示数量(可选) -
分享统计:
统计指标: - 分享次数 - 查看人数 - 转化人数(查看后购买) - 传播路径(A分享给B,B分享给C) -
场景化推荐:
场景1:送礼征询 "想送女朋友礼物,帮我选一个" → 展示多个候选商品 → 朋友投票或评论 场景2:拼单 "一起买,更便宜" → 共享购物车 → 凑满减金额 → 分摊运费
延伸思考:
- 如何设计购物车的协同冲突解决(同时删除同一商品)?
- 分享购物车如何防止恶意刷单?
- 共享购物车如何拆单结算(各付各的)?
💡 题目11:购物车的满减凑单提示
问题描述: 购物车总价¥180,有满¥200减¥30活动。如何设计智能凑单提示,引导用户加购?
答案:
推荐方案:
-
差额计算:
当前金额:¥180 满减门槛:¥200 差额:¥20 提示:"再买¥20,立减¥30" -
智能商品推荐:
推荐商品筛选条件: - 价格在¥20-¥50之间(差额附近) - 与购物车商品相关(配件、同类目) - 库存充足 - 高评分 排序: - 优先推荐价格接近差额的 - 优先推荐关联度高的 -
视觉引导:
进度条展示: [████████░░] 90% (¥180/¥200) "再买¥20,立减¥30,相当于打8.5折" 推荐商品卡片: ┌───────────┐ │ 手机壳 │ │ ¥29 │ │ [加入购物车]│ └───────────┘ -
多档位满减:
满减档位: - 满¥100减¥10(已达成✓) - 满¥200减¥30(差¥20) - 满¥500减¥100(差¥320) 提示优先显示最接近的下一档
延伸思考:
- 凑单推荐如何避免过度营销(让用户反感)?
- 多个满减活动同时存在时如何提示?
📊 题目12:购物车的批量操作设计
问题描述: 用户购物车有50个商品,想批量删除、批量加入收藏。如何设计批量操作功能?
答案:
推荐方案:
-
批量选择:
界面设计: [全选] 已选0件 ☑ 商品A ¥100 ☑ 商品B ¥200 ☐ 商品C ¥300 批量操作: [删除选中] [加入收藏] [移除失效商品] -
批量接口:
POST /api/cart/batch-delete { "skuIds": ["123", "456", "789"] } POST /api/cart/batch-move-to-favorite { "skuIds": ["123", "456"] } -
事务处理:
批量操作的事务性: - 部分成功部分失败如何处理? 方案A:全量事务 - 全部成功才提交 - 任一失败全部回滚 方案B:部分成功(推荐) - 成功的操作提交 - 失败的返回错误信息 - 前端展示"成功X件,失败Y件" -
性能优化:
批量删除50个商品: ❌ for循环50次DELETE ✅ 一次DELETE WHERE sku_id IN (...) 批量更新库存: ❌ 50次UPDATE ✅ 批量UPDATE CASE WHEN
延伸思考:
- 批量操作如何支持撤销(Undo)?
- 批量操作的进度如何展示?
🔧 题目13:购物车的收藏夹联动
问题描述: 购物车和收藏夹如何联动?商品从购物车移入收藏,或从收藏加入购物车。
答案:
推荐方案:
-
数据模型:
favorite ├── favorite_id ├── user_id ├── sku_id ├── source(CART/BROWSE) ├── added_at └── ... -
互相转换:
购物车 → 收藏夹: 1. 用户点击"移入收藏" 2. 加入收藏夹 3. 从购物车删除 4. 提示"已移入收藏夹" 收藏夹 → 购物车: 1. 用户点击"加入购物车" 2. 加入购物车 3. 保留在收藏夹(不删除) -
降价提醒:
收藏商品降价: - 监控收藏商品价格 - 降价时推送通知 - 引导用户加购
延伸思考:
- 收藏夹和购物车的区别是什么?
- 如何设计收藏夹的分组功能?
💡 题目14:购物车的历史记录
问题描述: 用户删除了购物车商品,想恢复。如何设计购物车的历史记录功能?
答案:
推荐方案:
-
软删除:
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 -
恢复功能:
历史记录页面: 最近删除: - 商品A(3天前删除)[恢复] - 商品B(7天前删除)[恢复] 恢复操作: UPDATE shopping_cart SET deleted=0, deleted_at=NULL WHERE cart_id=? -
自动清理:
定时任务: - 删除30天后的历史记录 - 减少存储成本
延伸思考:
- 购物车历史记录是否需要版本控制(记录每次修改)?
- 如何设计购物车的快照功能(保存多个购物清单)?
📊 题目15:购物车的AB测试设计
问题描述: 想测试新的购物车布局对转化率的影响。如何设计购物车的AB测试?
答案:
推荐方案:
-
分流策略:
public String getCartVersion(Long userId) { // 基于用户ID哈希分流 int hash = userId.hashCode(); if (hash % 2 == 0) { return "A"; // 对照组 } else { return "B"; // 实验组 } } -
实验设计:
对照组A(50%用户): - 旧购物车布局 实验组B(50%用户): - 新购物车布局(优化后) 评估指标: - 加购率 - 结算率 - 转化率 - 客单价 -
数据埋点:
// 购物车页面浏览 track('cart_view', { version: 'A', // 或 'B' cartItemCount: 5 }); // 点击结算 track('cart_checkout_click', { version: 'A', cartTotal: 1000 }); // 完成下单 track('order_created', { version: 'A', orderAmount: 1000 }); -
结果分析:
结果对比: | 指标 | A组 | B组 | 提升 | |------|-----|-----|------| | 结算率 | 60% | 65% | +8.3% | | 转化率 | 40% | 45% | +12.5% | | 客单价 | ¥800 | ¥850 | +6.25% | 结论:B组效果更好,全量发布
延伸思考:
- AB测试如何保证结果的统计显著性?
- 多个AB测试同时进行时如何隔离影响?
- 如何设计购物车的渐进式发布(灰度发布)?
3.3 订单系统(15题)
📊 题目1:订单状态机的设计
问题描述: 订单从创建到完成,经历多个状态(待支付、待发货、待收货、已完成)。如何设计订单状态机,保证状态流转的正确性?
答案:
问题分析: 订单状态流转的核心要素:
- 状态定义清晰
- 流转规则明确
- 防止非法跳转
- 支持异常流程(取消、退款)
状态定义:
正向流程:
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
延伸思考:
- 如何设计订单的子状态(如待发货细分为待拣货、待打包、待出库)?
- 订单状态变更如何触发后续操作(如发货后通知物流)?
- 如何处理状态流转的并发冲突?
🔧 题目2:订单号生成规则
问题描述: 订单号需要唯一、有序、不易被猜测。如何设计订单号生成规则?
答案:
订单号设计要求:
- 全局唯一
- 趋势递增(便于分库分表)
- 信息可读(包含时间、业务类型)
- 安全性(不易被遍历)
- 长度适中(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;
}
延伸思考:
- 如何设计订单号的校验规则(防止伪造)?
- 订单号如何支持多业务类型(普通订单、预售订单、拼团订单)?
- 分库分表场景下订单号如何设计路由键?
💡 题目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)。
实施要点:
-
幂等性保证:
@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()); } -
异常重试:
取消失败的处理: - 消息重新入队,稍后重试 - 最多重试3次 - 仍失败则记录告警,人工处理 -
监控告警:
监控指标: - 超时订单数量 - 取消成功率 - 延迟队列堆积量 告警: - 取消失败率 > 1% - 延迟队列堆积 > 10000
延伸思考:
- 如何设计不同订单类型的不同超时时间(普通30分钟,秒杀10分钟)?
- 订单超时取消如何通知用户?
- 大促期间超时订单激增如何处理?
📊 题目4:订单拆单与合单策略
问题描述: 用户购买多个商品,可能来自不同仓库或不同商家。如何设计订单拆单与合单策略?
答案:
问题分析: 拆单场景:
- 多仓库发货(就近发货)
- 多商家发货(平台+第三方卖家)
- 预售+现货(发货时间不同)
- 自营+跨境(清关时间不同)
合单场景:
- 同一地址多笔订单(节省运费)
- 同一商家商品(方便发货)
方案一:用户下单时拆单
核心思想: 用户提交订单时,系统自动拆分为多个子订单。
流程:
用户购物车:
- 商品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
优点:
- 用户无感知(看到的是一个订单)
- 退款简单(按订单退)
- 灵活(可随时调整拆单规则)
缺点:
- 实现复杂
- 需要维护订单和发货单的关系
拆单规则:
-
按仓库拆分:
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; } -
按商家拆分:
平台订单包含: - 自营商品(平台发货) - 第三方商品(商家发货) 拆分: - 子订单1:自营商品 - 子订单2:商家A的商品 - 子订单3:商家B的商品 -
按发货时间拆分:
订单包含: - 现货商品(立即发货) - 预售商品(15天后发货) 拆分: - 发货单1:现货(立即发) - 发货单2:预售(延迟发)
合单策略:
-
同地址合并:
用户A在1小时内下了3笔订单: - 订单1:商品A(北京仓) - 订单2:商品B(北京仓) - 订单3:商品C(上海仓) 合单: - 发货单1:订单1+订单2的商品(北京仓合并发货) - 发货单2:订单3的商品(上海仓单独发货) 好处: - 节省运费 - 减少包裹数量 -
运费优化:
规则: - 同一仓库、同一地址、24小时内的订单 - 自动合并发货 - 运费退还到用户余额
推荐方案: 采用后台自动拆单。
实施要点:
-
拆单时机:
时机选择: - 订单支付后立即拆单(推荐) - 发货前拆单(更灵活) -
用户展示:
订单详情页: 订单号:OR123456 总金额:¥1000 发货信息: - 包裹1:商品A+B(运单号:SF123) 状态:已发货 - 包裹2:商品C(运单号:SF456) 状态:待发货 -
退款处理:
部分商品退款: - 用户申请退商品A - 计算退款金额(商品价 + 分摊运费) - 只退部分金额 - 其他商品正常履约
延伸思考:
- 如何设计拆单的运费分摊规则?
- 拆单后如何保证库存一致性?
- 跨境订单的拆单有何特殊性?
🔧 题目5:订单的并发创建与幂等性
问题描述: 用户可能重复点击“提交订单“按钮,导致创建多个订单。如何保证订单创建的幂等性?
答案:
问题分析: 重复下单的原因:
- 用户重复点击
- 网络超时重试
- 前端未防抖
- 恶意刷单
方案一:前端防抖
核心思想: 前端限制用户短时间内多次点击。
实现:
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机制的组合。
实施要点:
-
多层防护:
L1:前端防抖(用户体验) L2:Token机制(防恶意) L3:唯一索引(最后防线) -
幂等键设计:
幂等键组成: userId + cartVersion + timestamp 例如: 123_v10_1679800000 说明: - userId:用户ID - cartVersion:购物车版本(购物车内容变化版本号+1) - timestamp:提交时间戳(精确到秒) -
异常处理:
try { return createOrder(request, token); } catch (DuplicateKeyException e) { // 唯一索引冲突,查询已存在的订单 Order existingOrder = findByIdempotentKey(idempotentKey); return existingOrder; } catch (BizException e) { // Token无效等业务异常 throw e; }
延伸思考:
- 如何设计订单创建的限流(防止刷单)?
- 订单创建失败如何回滚库存?
- 分布式事务下如何保证订单创建的一致性?
📊 题目6:订单的分布式事务设计(Saga模式)
问题描述: 订单创建涉及多个服务(订单服务、库存服务、优惠券服务、积分服务)。如何使用Saga模式保证分布式事务一致性?
答案:
问题分析: 订单创建的分布式事务流程:
- 扣减库存(库存服务)
- 核销优惠券(营销服务)
- 扣减积分(会员服务)
- 创建订单(订单服务)
任一环节失败,已执行的操作需要回滚。
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)
}
优点:
- 逻辑清晰(正向+补偿)
- 解耦各服务
- 支持长事务
缺点:
- 实现复杂
- 补偿可能失败(需要人工介入)
- 中间状态可见(不是强一致性)
延伸思考:
- Saga补偿失败如何处理?
- 如何设计Saga的可视化监控?
- Saga vs 2PC(两阶段提交)如何选择?
🔧 题目7:订单数据的分库分表设计
问题描述: 订单表数据量达到亿级,单表查询性能下降。如何设计订单的分库分表方案?
答案:
问题分析: 订单分库分表的核心要素:
- 分片键选择(user_id还是order_id)
- 分片数量(16、32、64、128)
- 跨片查询(如运营查询某时间段订单)
- 数据扩容
方案一:按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实现):
-
分片路由中间件:
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 } -
订单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) } -
订单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 } -
扩容方案:
扩容策略(64 → 128分片): 方案A:双写期 1. 新建64个分片(总共128个) 2. 新订单写入新分片规则 3. 老订单保留在老分片 4. 查询时先查新分片,未命中再查老分片 方案B:一致性哈希 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)
}
延伸思考:
- 履约流程如何支持异常处理(缺货、商品损坏)?
- 多个发货单如何协调履约进度?
- 履约时效如何监控和告警?
📊 题目9:订单的退款和售后流程设计
问题描述: 用户申请退款(仅退款、退货退款),如何设计售后流程,保证资金安全和用户体验?
答案:
退款场景:
- 仅退款(未发货)
- 退货退款(已发货)
- 部分退款(退部分商品)
- 售后退款(商品质量问题)
推荐方案(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
}
延伸思考:
- 退款失败如何重试和补偿?
- 恶意退款如何识别和防范?
- 部分退款如何计算退款金额(商品价+运费分摊)?
🔧 题目10:订单的异常处理(缺货、地址错误)
问题描述: 订单履约过程中可能出现异常(缺货、地址无法送达、商品损坏)。如何设计异常处理流程?
答案:
异常场景及处理方案:
-
库存不足(超卖):
// 发现超卖 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) } -
地址无法送达:
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 } -
商品损坏:
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) }
延伸思考:
- 异常订单如何统计和分析?
- 如何设计异常的自动化处理规则?
💡 题目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年前的订单)?
- 分库分表+ES同步如何保证一致性?
📊 题目12:订单的消息通知设计
问题描述: 订单状态变化时需要通知用户(下单成功、发货、签收)。如何设计消息通知系统?
答案:
通知渠道:
- App推送
- 短信
- 微信公众号/服务号
- 站内信
- 邮件
推荐方案(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
}
延伸思考:
- 通知失败如何重试?
- 如何设计通知的用户偏好设置(关闭某些通知)?
- 大批量通知如何限流(避免骚扰)?
🔧 题目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
}
延伸思考:
- 归档订单如何支持查询?
- 冷数据恢复到热库的策略?
💡 题目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, "订单完成")
}
}
延伸思考:
- 如何识别黄牛和恶意用户?
- 限流策略如何针对不同用户等级差异化?
📊 题目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
}
延伸思考:
- 实时统计如何保证准确性(与离线对账)?
- 多维度统计(按类目、品牌)如何设计?
3.4 支付系统(10题)
📊 题目1:支付系统的整体架构设计
问题描述: 电商平台需要支持多种支付方式(支付宝、微信、银行卡)。如何设计支付系统的整体架构?
答案:
问题分析: 支付系统的核心要素:
- 多渠道接入(支付宝、微信、银联)
- 支付安全性
- 异步回调处理
- 对账和资金安全
架构设计(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)
}
延伸思考:
- 支付系统如何实现高可用?
- 支付渠道故障如何降级?
- 支付回调丢失如何处理?
🔧 题目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()
}
延伸思考:
- 如何设计支付回调的重试机制?
- 回调处理失败如何人工介入?
💡 题目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
}
延伸思考:
- 对账差异如何自动修复?
- 对账失败如何告警和处理?
- 实时对账和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)
}
延伸思考:
- 回调接口如何防止伪造(恶意请求)?
- 回调处理超时如何设置?
🔧 题目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:月结算(新商家)
延伸思考:
- 如何设计结算的对账机制?
- 商家提现如何设计?
- 结算失败如何处理?
📊 题目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
}
延伸思考:
- 支付密码如何加密存储?
- 如何设计支付的二次确认(大额支付)?
- 支付安全如何平衡用户体验?
🔧 题目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},
},
}
}
延伸思考:
- 如何设计支付渠道的成本优化(选择手续费低的)?
- 支付渠道限额如何处理?
💡 题目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: "部分退货",
})
}
延伸思考:
- 退款失败如何通知用户?
- 如何设计退款的限额控制(防止洗钱)?
🔧 题目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("熔断器关闭,恢复正常")
}
}
延伸思考:
- 如何设计支付系统的多机房容灾?
- 支付降级后如何通知用户?
📊 题目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
}
延伸思考:
- 预授权过期如何自动解冻?
- 预授权场景下的对账如何设计?
第四部分:综合实战案例(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
}
性能优化点:
- 多级缓存:本地内存(1ms)→ Redis(5ms)→ MySQL(50ms)
- CDN静态化:核心商品预生成HTML,加载时间<100ms
- 缓存预热:大促前1小时预热热门商品
- 异步刷新:Cache Aside模式,回源不阻塞请求
- 降级策略:Redis故障时降级到MySQL+限流
容量规划:
QPS:100万
平均响应时间:50ms
并发连接数:100万 * 0.05 = 5万
服务器配置:
- 应用服务器:100台(每台1万QPS)
- Redis集群:50个主节点(每节点2万QPS)
- MySQL:主从+分库分表(32个分片)
延伸思考:
- 商品详情页的AB测试如何设计?
- 图片加载优化(WebP、懒加载)如何实现?
- 缓存雪崩如何防范?
💡 案例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)
}
}
}
架构要点:
- 前端限流:按钮置灰、验证码、排队页
- 网关限流:Nginx限流(10万QPS)
- Redis预扣库存:Lua脚本原子操作
- 异步下单:MQ削峰,提升吞吐
- 超时取消:15分钟未支付自动取消+回补库存
延伸思考:
- 秒杀如何防止黄牛?
- 分布式锁如何选型(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
}
延伸思考:
- 如何设计告警规则(P95延迟>500ms告警)?
- 如何追踪跨语言调用链(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
延伸思考:
- 如何设计压测数据构造?
- 压测导致的脏数据如何清理?
🔧 案例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
}
延伸思考:
- 汇率波动如何处理(订单创建时汇率 vs 支付时汇率)?
- 跨境支付的关税如何计算?
💡 案例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),
// ... 更多特征
}
}
延伸思考:
- 如何设计AB测试验证排序效果?
- 如何平衡新品曝光和热销商品?
🚀 案例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",
},
}
延伸思考:
- 如何设计自动化的应急响应系统?
- 如何平衡防御和用户体验(误封正常用户)?
🔧 案例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
}
延伸思考:
- 本地消息表 vs Saga vs TCC如何选择?
- 消息发送失败如何保证最终一致性?
📊 案例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 "低"
}
}
延伸思考:
- 如何保护用户隐私(GDPR合规)?
- 画像准确性如何评估?
💡 案例10:大促后的系统复盘
问题描述: 618大促结束后,需要对系统表现进行复盘。如何设计复盘报告,总结经验和改进点?
答案:
复盘维度:
-
业务指标:
- GMV:50亿
- 订单量:1000万
- 转化率:3.5%
- 客单价:500元
-
技术指标:
- 峰值QPS:50万
- 平均响应时间:200ms
- P99响应时间:800ms
- 可用性:99.95%
-
故障复盘:
- 23:00-23:15 订单服务QPS突增导致响应变慢
- 根因:数据库连接池不足
- 影响:15分钟内订单延迟,影响1000笔订单
- 改进:增加连接池大小,增加熔断降级
-
优化建议:
- 缓存命中率从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
}
延伸思考:
- 如何设计大促演练(压测、故障演练)?
- 如何量化技术优化的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_tab 和 product_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 最终一致。这样才能支撑多品类、多供应商和强运营平台的长期演进。
如果只能做三件事:
- 收敛写入口:所有商品变更必须经过命令 API、Task、Staging 和发布事务。
- 建立版本和审核:所有高风险变更必须有 Diff、风险理由、审核记录和
publish_version。 - 补齐补偿闭环:错误文件、DLQ、Outbox、质量巡检和运营看板必须能让失败被发现、被修复、被重新投递。
11.16 答辩材料:常见面试题
11.16.1 基础边界
问题 1:为什么商品供给与运营平台不能设计成商品中心的后台 CRUD?
参考要点:商品中心负责正式主数据和查询契约,供给平台负责 Draft、Task、Staging、审核、发布、补偿和审计。后台直接 CRUD 会让未审核数据污染线上,也会造成搜索、缓存、订单快照和发布版本不可追溯。
问题 2:商品中心和供给运营平台的边界是什么?
参考要点:商品中心保存正式 Resource / SPU / SKU / Offer / Rule 和 publish_version;供给运营平台保存供给流程对象,例如 Draft、Staging、QC、Task、DLQ。供给平台通过发布事务写商品中心,不直接让运营后台改正式表。
问题 3:供应商同步和人工上传要分成两套系统吗?
参考要点:不要简单回答“分开”或“不分开”。更准确的设计是:入口层、执行层要分开,标准化之后的治理和发布层要合流。
供应商同步和人工上传的执行问题完全不同:
| 维度 | 人工上传 / 批量导入 | 供应商同步 |
|---|---|---|
| 触发方式 | 用户点击、上传 Excel、保存 Draft | 定时任务、全量同步、增量同步、Push |
| 数据来源 | 本地运营、商家 | 外部供应商 API / 消息 |
| 失败原因 | 字段填错、模板错误、图片不合规 | 超时、限流、5xx、游标失效、字段漂移 |
| 进度模型 | 文件行号、导入 item、错误文件 | city / page / cursor / checkpoint |
| 恢复机制 | 重传文件、失败行重试 | checkpoint 续跑、lease 抢占、DLQ |
| 证据保存 | 上传文件、表单 payload | Raw 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_id 和 temporary_object_key。发布成功后生成正式 item_id,再写 product_supply_object_mapping,后续可以用 item_id 反查 supply_trace_id。
问题 7:商品已经在线后再次编辑,哪些 ID 会新建,哪些 ID 会复用?
参考要点:item_id 和 supply_trace_id 复用;operation_id、draft_id、staging_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_review 和 product_qc_review_item 两张表?
参考要点:product_qc_review 是审核工单主表,记录整体状态、审核人、结论;product_qc_review_item 是审核项明细,记录字段 Diff、风险原因、分项结论和驳回原因。批量任务和字段级驳回都需要明细表。
问题 11:product_change_request、product_supply_operation_log、product_field_ownership 分别解决什么问题?
参考要点:product_change_request 记录改了什么、风险多高、是否需要 QC;product_supply_operation_log 记录从 Draft 到下线全过程发生了什么;product_field_ownership 记录字段由谁主导,防止供应商同步覆盖人工治理结果。
问题 12:product_publish_record、product_publish_snapshot、product_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_task 和 product_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_id 和 publish_version 幂等处理,失败进入重试、DLQ 或 product_compensation_task。供给平台不直接写搜索索引、不直接写最终价格,也不直接改订单。营销活动是控制面协同:供给平台可以发起圈品、活动资格和活动绑定命令,营销系统负责活动规则、预算、券、补贴、营销库存和最终优惠计算。订单创建只相信当时的商品、报价、履约和退款快照,不回读最新商品解释历史订单。
问题 21:商品供给运营平台、商品生命周期和库存系统如何联动?
参考要点:创建和修改库存属于供给运营平台的业务工作,但不属于供给运营平台的数据事实。供给平台是库存变更的控制面,负责表单、导入、审批、任务、错误文件、可售诊断和审计;库存系统是库存事实的数据面,负责 inventory_config、inventory_balance、券码池、预占记录、状态机和账本流水。供给平台治理变更是否可以发布,商品生命周期控制正式商品何时在线、下线、结束和封禁,库存系统控制某个范围内是否有可承诺资源,营销系统控制活动、券、补贴、预算和优惠规则。发布成功不等于可售成功,Publish 只写正式商品、发布版本、交易契约和 Outbox;库存通过 CreateInventory、AdjustInventory、ImportCodeBatch、GenerateCodeBatch 等命令异步创建或调整库存实例,再发布 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=PENDING 或 status=RUNNING AND lease_until < NOW() 的 batch 才能被抢占。rows_affected=1 才说明抢占成功,其他 worker 必须退出。
问题 5:worker_id 和 lease_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_code 的 PENDING/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_id、base_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章 支付系统
子系统职责对照(面试与评审常用):
| 子系统 | 核心职责 | 关键数据 | 典型反模式 |
|---|---|---|---|
| 账户系统 | 余额、冻结、流水、充值提现 | 账户余额、冻结单、账务流水 | 把渠道手续费写进用户余额宽表 |
| 支付网关 | 路由、报文、签名、重试 | 渠道请求 / 响应摘要、路由决策 | 在网关里改支付单状态机 |
| 支付核心 | 支付单、退款单、幂等、Outbox | payment、refund、outbox | Handler 直连第三方 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
结语
本面试题精选覆盖了电商系统的核心技术栈,从系统架构到具体实现,从理论到实践。建议读者:
- 系统学习:按章节顺序学习,建立完整知识体系
- 动手实践:核心算法和代码自己实现一遍
- 举一反三:理解原理,能够应用到实际项目
- 持续更新:技术在演进,保持学习新技术
面试技巧:
- 先理清思路,再动手画图
- 多提供几种方案,对比优缺点
- 关注非功能性需求(性能、可用性、扩展性)
- 结合实际项目经验
祝各位求职顺利!🎉