电商系统设计系列(篇次与(一)推荐阅读顺序一致)

本文是电商系统设计系列的第五篇,详述计价引擎的设计与实现。

一、背景与挑战

1.1 业务背景

在电商平台中,价格计算引擎(Pricing Engine)面临的核心挑战是:

如何在多品类、多变价因素、多营销活动的前提下,提供统一、准确、高性能的价格计算能力?

1.1.1 价格计算的复杂性

在现代电商系统中,一个商品的最终价格并非简单的”标价”,而是多个因素分层叠加计算的结果:

1
2
3
4
5
最终支付价格 = 基础价格
- 营销活动折扣(Layer 2: FlashSale、新人价、Bundle等)
+ 附加费用(Layer 4: 增值服务费、运费、平台服务费等)
- 支付优惠(Layer 3: 优惠券、积分抵扣)
+ 支付渠道费(Layer 4: 支付手续费、信用卡手续费等)

典型场景示例

用户购买一部价值 ¥1,000 的手机,完整计价流程如下:

📊 分层价格计算

1️⃣ 基础价格(Layer 1: Base Price)

1
2
3
4
商品原价:         ¥1,000
商品折扣价: ¥980 (商家设置的折扣价)
───────────────────────────
基础价格: ¥980

2️⃣ 营销价格(Layer 2: Promotion)

1
2
3
4
5
6
7
基础价格:         ¥980
- 限时抢购 (Flash Sale): -¥80 (活动价 ¥900)
- 新人专享价: -¥20 (首单优惠)
- 满减活动 (满800减50): -¥50
───────────────────────────
营销优惠合计: -¥150
订单价格(营销后): ¥830

3️⃣ 附加费用(Layer 4: Additional Charge - 第一部分)

1
2
3
4
5
6
7
订单价格:         ¥830
+ 碎屏险 (增值服务): +¥50
+ 运费: +¥10
+ 平台服务费 (供应商品类): +¥0 (自营品类无此项)
───────────────────────────
附加费用合计: +¥60
订单价格(含附加费):¥890

CreateOrder(创建订单)到此为止:生成订单快照 = ¥890
此时包含:基础价 + 营销价 + 附加费,不含券/积分/支付费


4️⃣ 支付优惠(Layer 3: Deduction)

用户进入收银台(Checkout),选择优惠券和积分:

1
2
3
4
5
6
订单价格:         ¥890
- 优惠券抵扣 (满500减100): -¥100
- 积分抵扣 (200积分×¥0.5/分):-¥100
───────────────────────────
支付优惠合计: -¥200
优惠后价格: ¥690

5️⃣ 支付渠道费(Layer 4: Additional Charge - 第二部分)

用户选择支付渠道:

1
2
3
4
5
6
优惠后价格:       ¥690
+ 信用卡手续费 (2%): +¥13.8
+ 跨境支付费 (国际卡): +¥0 (本地支付无此项)
───────────────────────────
支付渠道费合计: +¥13.8
最终支付价格: ¥703.8

Checkout(收银台)完成:生成支付快照 = ¥703.8
此时包含:订单价格 + 券/积分 + 支付手续费


💰 价格明细汇总

项目 金额 说明
基础价格 ¥980 商品折扣价(Layer 1)
营销优惠 -¥150 限时抢购 + 新人价 + 满减(Layer 2)
附加费用 +¥60 碎屏险 + 运费(Layer 4)
订单价格 ¥890 CreateOrder生成订单快照
支付优惠 -¥200 优惠券 + 积分(Layer 3)
支付渠道费 +¥13.8 信用卡手续费(Layer 4)
最终支付价格 ¥703.8 Checkout生成支付快照

🔄 不同场景的价格计算

场景 计算内容 价格
PDP(商品详情页) Layer 1 + Layer 2
(基础价 + 营销价)
¥830
Cart(购物车) Layer 1 + Layer 2 + 预估Layer 3
(基础价 + 营销价 + 预估券/积分)
预估 ¥630-690
CreateOrder(创建订单) Layer 1 + Layer 2 + Layer 4(部分)
(基础价 + 营销价 + 附加费,不含券/积分
¥890
Checkout(收银台) Layer 1-5 完整计算
(订单价格 + 券/积分 + 支付手续费)
¥703.8
Payment(支付) 零计算(快照验证) ¥703.8

1.2 传统架构的痛点

1.2.1 计价逻辑分散

在传统电商系统中,价格计算逻辑往往分散在不同服务中:

服务 职责 问题
Item Service 计算商品市场价、折扣价 不知道营销活动价
Promotion Service 计算营销活动价 不知道最终售卖价
Order Service 计算订单总价 重复计算,容易出错
Payment Service 计算支付金额 再次重复计算

核心问题

  • 重复计算:同一价格在多个服务重复计算,逻辑不一致
  • 数据不一致:前端展示价格与订单价格与支付价格不一致
  • 资损风险:价格计算错误导致商家或平台损失
  • 难以扩展:新增变价因素需要修改多个服务

1.2.2 真实案例:某电商平台资损事故

事故场景

  • 用户购买商品,前端展示价格 100 元
  • 提交订单时,后端计算价格 80 元(错误地应用了两次优惠券)
  • 用户实际支付 80 元,平台损失 20 元
  • 该 Bug 持续 2 小时,影响 5000+ 订单,总损失 10 万+

根本原因

  • 前端、订单服务、支付服务各自计算价格
  • 优惠券服务在订单创建时重复扣减
  • 缺乏统一的价格计算引擎和空跑比对机制

1.3 计价中心的核心价值

建立统一的价格计算引擎(Pricing Center)可以解决上述问题:

价值 说明 收益
计算逻辑统一 所有价格计算收敛到一个服务 避免重复开发,降低出错率
数据一致性 前端展示、订单创建、支付扣款使用同一价格 消除资损风险
空跑比对 新老逻辑并行运行,自动比对差异 安全迁移,快速发现问题
灰度放量 按品类、地区、用户分批切流 降低风险,平滑迁移
扩展性强 新增变价因素只需新增策略 快速响应业务需求
性能优化 缓存、并发、批量计算 支持高并发场景

1.4 设计目标

目标 说明 优先级
准确性 价格计算结果 100% 准确,0 资损 P0
一致性 前端/订单/支付使用同一价格源 P0
高性能 PDP 页面 P99 < 100ms,Checkout 页面 P99 < 200ms P0
多品类支持 支持 Topup/EMoney/EVoucher/GiftCard/Hotel/Movie 等 P0
灰度可控 支持按品类、地区、用户灰度 P0
空跑比对 新老逻辑并行,自动比对差异 P0
可扩展 新增变价因素成本低 P1
可观测 完善的监控、日志、告警 P1

1.5 电商计价引擎核心问题解析

电商计价引擎表面上只是”加减乘除”,但在真实业务中,它是整条交易链路中逻辑最复杂、计算量最大、且最容不得差错的核心组件。一个成熟的计价引擎需要系统性地解决以下八大核心问题:

1.5.1 多维价格体系(品类异构性)

商品在不同维度下并不是只有一个价格。引擎需要在同一请求中处理多种定价模型:

维度 说明 示例
基础定价 商品原价、划线价、折扣价 市场价 ¥100 → 折扣价 ¥90
渠道定价 App/H5/直播间/不同地区差异定价 App 专享价 ¥85
身份定价 普通用户/VIP/新客/企业账号 新人专享价 ¥80
品类定价 不同品类使用完全不同的定价模型 Topup 面额定价、Hotel 间夜定价、Flight 舱位定价
实时价盘 供应商品类价格随库存/时间动态波动 Hotel/Flight 供应商实时报价

品类异构性是计价引擎最大的架构挑战之一。不同品类的定价逻辑差异巨大:

品类 核心定价逻辑 关键计算因子
Deal/生活券 简单折扣模型 数量 × 单价 - 折扣
Topup/话费充值 面额定价 + 折扣率 面额 × 折扣率(如 100 元面值 × 0.95 = 95 元)
Bill/账单 代缴模型 + 手续费 欠费金额 + 渠道手续费 - 平台补贴
E-Money 面值 + 管理费 面值 + AdminFee(固定/比例)
Hotel 间夜 × 日历价 + 税费 入离日期、入住人数、城市税(各国税率不同)
Flight 舱位报价 + 多重税费 GDS 实时报价 + 燃油费 + 机建费 + 选座/行李附加费
Movie/演出 场次 × 票种 × 座位 场次时段定价、座位等级定价

架构对策:采用**”统一编排框架 + 品类策略插件”**模式(Strategy Pattern),通过 Calculator 接口实现品类差异化,避免 if-else 巨石代码。详见 4.3 品类特殊计算器


1.5.2 多场景计价一致性

价格计算贯穿用户购物全流程,不同场景对计算深度、性能要求、精度容忍度完全不同:

场景 核心目标 计算深度 性能要求 精度容忍度
PDP(商详页) 转化引流,展示”预估到手价” 轻量(Layer 1-2) P99 < 100ms 允许预估误差
AddToCart(加购) 提升客单价,凑单提示 轻量(Layer 1-2) P99 < 150ms 允许预估
Cart(购物车) 多品组合实时计算 中等(Layer 1-3) P99 < 200ms 预估价格
CreateOrder(创单) 锁定订单价格,生成订单快照 高(Layer 1,2,4) P99 < 300ms/1s 零容忍
Checkout(收银台) 最终价格确定,生成支付快照 完整(Layer 1-5) P99 < 200ms 零容忍
Payment(支付) 金额验证,防篡改 零计算(快照验证) P99 < 500ms 零容忍

核心挑战——价格一致性(Price Consistency)

用户最常见的投诉是”商详页看到 99 元,下单变成 105 元”。解决这个问题需要:

  1. 规则版本对齐:确保 PDP 预估时的规则集与创单时的规则集版本一致
  2. 快照锁定:CreateOrder 和 Checkout 分别生成订单快照和支付快照,锁定价格
  3. 差异校验:Checkout 与 CreateOrder 之间如果价格变化超过阈值,强提醒用户

详见 二、计价场景分析3.3 价格快照管理机制


1.5.3 营销叠加与互斥规则

当多种优惠同时存在时,系统必须决定谁先谁后、谁能叠加、谁互斥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
营销优惠处理的核心决策链:

1. 优惠层级(执行顺序):
商品级(Flash Sale/新人价) → 店铺级(满减/Bundle) → 平台级(平台券/红包)

2. 互斥逻辑:
• Flash Sale 和 新人价 互斥(取最优)
• 商品级折扣 和 平台满减 可叠加
• 优惠券 和 积分 可叠加(但有上限)
• 支付立减 与订单优惠 可叠加

3. 最优解计算:
• 系统是否需要帮用户自动选择最省钱的组合?
• 涉及动态规划/贪心算法
• 购物车场景需要实时告诉用户"再凑 ¥200 可减 ¥50"

架构对策:将营销规则抽象为可配置的策略,通过 PromotionLayer 统一处理叠加/互斥逻辑,支持灰度与回滚。详见 4.2.2 营销活动层


1.5.4 费用与补贴建模

电商系统中的”加项”远不止商品价格本身,还包含多种费用(Fee/Tax)和补贴(Subsidy),且需要标注出资方:

类型 说明 出资方 示例
运费 配送费用 用户 ¥10
增值服务费 碎屏险、延保等 用户 ¥50
平台服务费 供应商品类佣金 用户/商家 ¥20
支付手续费 信用卡/跨境支付手续费 用户 金额 × 2%
税费 增值税、城市税(Hotel) 用户 各国税率不同
平台补贴 新客红包、品类补贴 平台 -¥10
商家补贴 商家承担的活动优惠 商家 -¥20
渠道补贴 银行卡立减、支付红包 渠道/银行 -¥5

关键原则:每一个价格组成项(Price Component)都必须标注 type(类型)和 source(出资方),为后续财务对账提供基础。

架构对策:通过 ChargeLayerPriceComponent 值对象建模,详见 4.2.4 附加费用层5.2 价格领域模型


1.5.5 优惠分摊与逆向退款

当一个订单包含多个商品,且使用了订单级优惠(如”满 1000 减 100”)时,这 100 元必须按比例分摊到每个商品,否则退款将无法正确处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
分摊算法(余额递减法):

场景:3 件商品(¥500 + ¥300 + ¥200),订单级满减 ¥100

Step 1: 计算权重
商品A: 500/1000 = 50%
商品B: 300/1000 = 30%
商品C: 200/1000 = 20%

Step 2: 前 n-1 件按权重分摊(向下取整)
商品A 分摊: ¥100 × 50% = ¥50.00
商品B 分摊: ¥100 × 30% = ¥30.00

Step 3: 最后一件 = 总优惠 - 前面之和(尾差处理)
商品C 分摊: ¥100 - ¥50 - ¥30 = ¥20.00

结果:
商品A 实付: ¥500 - ¥50 = ¥450
商品B 实付: ¥300 - ¥30 = ¥270
商品C 实付: ¥200 - ¥20 = ¥180
合计: ¥450 + ¥270 + ¥180 = ¥900 ✅

逆向退款场景

退款场景 处理逻辑
退单件商品 按分摊后的实付金额退款(非原价)
退后不满足满减门槛 可能需要收回优惠(业务策略决定)
退运费 根据剩余商品重新计算运费差异
退积分/优惠券 按分摊比例退还对应的积分和券

精度问题:所有金额使用分(cent)为单位的整数运算int64),避免浮点数精度丢失。详见 5.2.2 Money 值对象


1.5.6 确定性与防资损(快照机制)

计价引擎最不能容忍的就是资损——价格计算错误导致商家或平台亏钱。防资损需要多重保护:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
防资损的五道防线:

1. 价格快照(Price Snapshot)
• CreateOrder 生成订单快照(30分钟有效)
• Checkout 生成支付快照(15分钟有效)
• Payment 只验证快照,零重算

2. 金额校验
• 前端提交金额 vs 后端计算金额必须一致
• 支付金额 vs 快照金额必须一致
• 差额超过阈值拒绝交易

3. 安全检查器(Safety Checker)
• 最终价格不能为负数
• 折扣比例不能超过品类阈值(如最多打3折)
• 优惠总额不能超过商品总额

4. 空跑比对(Dry Run)
• 新老逻辑并行运行
• 自动比对差异,差异超阈值告警
• 确认无差异后再逐步灰度

5. 实时监控与告警
• 错误率 > 0.01% 触发告警
• 差异金额 > 10 元触发告警
• 差异比例 > 5% 触发告警

供应商品类的特殊挑战:Hotel/Flight 等供应商品类的价格具有实时性,在用户浏览到支付的几分钟内价格可能已经变化。通过 BookingToken 预订机制(5-15 分钟有效期)和支付前反查校验来解决。详见 4.4 供应商价格服务


1.5.7 高并发性能

大促期间计价引擎面临每秒数万次调用。不同场景需要差异化的性能策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
性能优化分层策略:

┌─────────────────────────────────────────────────────┐
│ 前台展示场景(PDP/Cart) │
│ 策略:高缓存 + 轻量计算 + 预热 │
│ • L1 本地缓存(5min)+ L2 Redis 缓存(30min) │
│ • 缓存命中率 > 90%,命中时延 < 10ms │
│ • 大促前预热热门商品价格 │
│ • 跳过不必要的计算层(如 PDP 跳过 Layer 3-4) │
├─────────────────────────────────────────────────────┤
│ 交易场景(CreateOrder/Checkout) │
│ 策略:零缓存 + 实时计算 + 并发查询 │
│ • 不使用缓存,保证实时性和准确性 │
│ • 依赖服务并发调用(商品/营销/券/积分并发获取) │
│ • 连接池复用,减少网络开销 │
│ • 供应商品类:3秒超时 + 2次重试 + 降级 │
├─────────────────────────────────────────────────────┤
│ 批量查询场景(推荐页/列表页) │
│ 策略:批量接口 + 并发计算 + 限流 │
│ • 最多 100 个商品批量计算 │
│ • 10 并发 goroutine 计算 │
│ • 限流保护后端服务 │
├─────────────────────────────────────────────────────┤
│ 兜底策略(全场景) │
│ • 非核心服务(营销/券)失败降级,不影响主流程 │
│ • 熔断器保护外部依赖(供应商 API) │
│ • 水平扩容应对流量洪峰 │
└─────────────────────────────────────────────────────┘

详见 六、性能优化4.1.3 场景驱动的缓存策略


1.5.8 审计、合规与可解释性

计价引擎必须能够回答两个问题:**”这个价格是怎么算出来的?”** 和 “每一分钱去了哪里?”

维度 要求 实现方式
可解释性 客服能向用户解释价格构成 PriceBreakdown + Formula(如 20194 - 1060 - 500 - 5 + 372 + 50 = 19051
财务对账 每笔交易的资金流向清晰可追溯 PriceComponent 标注 type + source(出资方)
价格法合规 避免”先涨后降”等违规行为 价格变更日志 + 历史价格追溯
差异审计 新老逻辑切换时的差异可追溯 空跑比对结果存 MongoDB,差异明细可查询
退款追溯 退款金额与下单时一致 价格快照(Snapshot)保存完整计价结果

架构对策:通过 PriceBreakdownPriceDictionary(统一术语)和价格快照实现全链路可追溯。详见 5.1 统一语言5.3 价格字典服务


1.5.9 核心问题总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
电商计价引擎 8 大核心问题:

┌──────────────────────────────────────────────────────────────┐
│ │
│ ① 品类异构性 统一编排 + 品类策略插件 │
│ ② 多场景一致性 规则版本对齐 + 快照锁定 │
│ ③ 营销叠加/互斥 配置化规则引擎 + 灰度可回滚 │
│ ④ 费用与补贴建模 标准化 PriceComponent + 出资方标注 │
│ ⑤ 分摊与逆向退款 余额递减法 + 尾差处理 + 整数运算 │
│ ⑥ 确定性与防资损 双快照机制 + 五道防线 │
│ ⑦ 高并发性能 场景分层缓存 + 并发优化 + 降级熔断 │
│ ⑧ 审计与可解释性 全链路 Breakdown + 统一术语 + 价格字典 │
│ │
│ 一句话总结: │
│ 计价引擎 = 统一编排框架(Pipeline) │
│ + 品类策略插件(Strategy) │
│ + 分层计算链(Layer Chain) │
│ + 快照防资损(Snapshot) │
│ + 场景差异化(Scene-Driven) │
│ │
└──────────────────────────────────────────────────────────────┘

二、计价场景分析

2.1 完整计价链路

在电商系统中,价格计算贯穿用户购物的整个流程,从浏览商品到最终支付,每个环节都需要准确的价格信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
用户购物流程 & 计价场景:

浏览商品 → 加入购物车 → 查看购物车 → 创建订单 → 收银台 → 支付 → 支付成功
↓ ↓ ↓ ↓ ↓ ↓ ↓
PDP计价 AddToCart Cart计价 CreateOrder Checkout Payment PaySuccess
(展示价格) (凑单提示) (预估价格) (订单价格) (最终价格) (验证) (资源扣减)


[供应商品类] 供应商价格预订
(Supplier Booking)

关键节点说明:
1. PDP: 展示商品价格(市场价、折扣价、活动价)
2. AddToCart: 加购时提示凑单优惠
3. Cart: 购物车预估价格(可选优惠券/积分)
- 自营品类:本地价格计算
- 供应商品类:实时查询供应商价格(缓存1-5分钟)
4. CreateOrder: 创建订单,锁定资源,计算订单价格(基础价+营销价+附加费,不含券/积分)
- 自营品类:锁定本地库存(30分钟)
- 供应商品类:先查询供应商价格并获取BookingToken(5-15分钟),基于供应商报价计算订单价格
5. Checkout: 收银台选择优惠券/积分/支付渠道,计算最终价格并生成支付快照
6. Payment: 验证支付金额和快照,调起第三方支付
- 供应商品类:使用BookingToken确认预订
7. PaySuccess: 支付成功回调,确认资源扣减,订单完成
- 供应商品类:向供应商确认订单(Confirm Booking)

核心差异点

  • CreateOrder 在前:先创建订单
    • 自营品类:锁定本地库存,计算订单价格(基础价+营销价+附加费)
    • 供应商品类:先查询供应商实时价格获取BookingToken,基于供应商报价计算订单价格
  • Checkout 在后:在收银台选择券/积分/渠道,计算最终支付价格
  • 两次计算
    • CreateOrder: 计算订单价格(含基础+营销+附加费)
    • Checkout: 在订单价格基础上加上券/积分/手续费
  • 两个快照
    • 订单快照(30分钟,含订单价格 + 供应商BookingToken)
    • 支付快照(15分钟,含最终支付价格)
  • 供应商对接:供应商品类在CreateOrder前预订,Payment时确认

2.2 核心计价场景

2.2.1 PDP 商品详情页计价

场景描述:用户浏览商品详情页时,需要展示商品的实时价格信息。

计价要素

1
2
3
4
5
6
7
8
PricingRequest {
Scene: "PDP",
Items: [{ItemID: 123, ModelID: 456, Quantity: 1}],
UserID: 789,
Region: "ID",
IsNewUser: true,
Platform: "App",
}

价格展示

1
2
3
4
5
6
7
8
9
10
11
12
13
┌─────────────────────────────────────┐
│ iPhone 15 Pro Max 256GB │
├─────────────────────────────────────┤
│ ¥8,999 ¥9,999 │
│ (折扣价) (原价) │
│ │
│ 🔥 限时抢购价:¥8,499 │
│ 🎁 新人专享价:¥8,299 │
│ 💰 可用券:满8000减500 │
│ │
│ 预估到手价:¥7,799 │
│ (使用优惠券后) │
└─────────────────────────────────────┘

性能要求

  • P99 延迟:< 100ms
  • 缓存命中率:> 90%
  • 并发 QPS:10,000+

关键特点

  • ✅ 仅展示价格,不锁定库存
  • ✅ 支持营销活动价格展示
  • ✅ 支持可用优惠券预估
  • ✅ 高缓存命中率

2.2.2 加购时计价(Add to Cart)

场景描述:用户点击”加入购物车”时,需要计算商品加购后的价格,以便展示购物车角标和提示信息。

计价要素

1
2
3
4
5
6
7
8
9
10
PricingRequest {
Scene: "AddToCart",
Items: [
{ItemID: 123, Quantity: 1}, // 当前加购商品
],
CartItems: [
{ItemID: 456, Quantity: 2}, // 购物车已有商品
],
UserID: 789,
}

前端展示

1
2
3
4
5
6
7
8
9
10
11
┌─────────────────────────────────────┐
│ ✅ 已加入购物车 │
├─────────────────────────────────────┤
│ 购物车共 3 件商品 │
│ 小计:¥1,299 │
│ │
│ 🎁 满减活动:再凑 ¥200 减 ¥50 │
│ 💰 可用券:满1000减100 │
│ │
│ [立即结算] [继续购物] │
└─────────────────────────────────────┘

关键特点

  • ✅ 计算购物车总价
  • ✅ 提示凑单信息(满减活动)
  • ✅ 展示可用优惠
  • ⚠️ 不锁定价格(真实价格以结算为准)

2.2.3 购物车页面计价(Shopping Cart)

场景描述:用户进入购物车页面,查看所有商品的价格、优惠、预估总价。

计价要素

1
2
3
4
5
6
7
8
9
10
PricingRequest {
Scene: "Cart",
Items: [
{ItemID: 123, ModelID: 456, Quantity: 2},
{ItemID: 789, ModelID: 101, Quantity: 1},
{ItemID: 234, ModelID: 567, Quantity: 3},
],
UserID: 12345,
Region: "SG",
}

页面展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
┌─────────────────────────────────────────────────────┐
│ 购物车 (5件商品) │
├─────────────────────────────────────────────────────┤
│ ☑️ iPhone 15 Pro × 2 ¥8,999 × 2 = ¥17,998 │
│ 🔥 限时抢购 -¥1,000 │
│ │
│ ☑️ AirPods Pro × 1 ¥1,899 × 1 = ¥1,899 │
│ │
│ ☑️ 手机壳 × 3 ¥99 × 3 = ¥297 │
│ 🎁 满3件8折 │
├─────────────────────────────────────────────────────┤
│ 商品总额: ¥20,194 │
│ 活动优惠: -¥1,060 │
│ │
│ 💰 可用优惠券 (3张) [选择优惠券 >] │
│ └─ 满10000减500 │
│ │
│ 🪙 使用积分 (1000分可抵¥10) [ ] 使用 │
├─────────────────────────────────────────────────────┤
│ 预估总价:¥19,134 │
│ │
│ [去结算(5)] │
└─────────────────────────────────────────────────────┘

关键特点

  • ✅ 支持多商品联合计算
  • ✅ 支持跨店铺优惠
  • ✅ 支持优惠券预选
  • ✅ 支持积分使用
  • ✅ 实时更新价格
  • ⚠️ 价格仅供参考(以订单为准)
  • ⚠️ 供应商品类需实时查询(见2.2.3.5)

2.2.3.5 购物车-供应商品类价格查询(Supplier Price Query)

场景描述:对于Hotel、Flight、Tour等供应商品类,购物车展示的价格需要实时从外部供应商获取,确保价格准确性。

品类差异

品类类型 价格来源 是否需要供应商查询 查询时机 缓存策略
自营品类 本地数据库 ❌ 不需要 - 高缓存(5-30分钟)
供应商品类 外部供应商API ✅ 需要 购物车/创单前 低缓存(1-5分钟)

供应商品类示例

  • Hotel:房型价格需实时查询供应商库存和日历价
  • Flight:航班价格需实时查询供应商舱位和动态价格
  • Tour:旅游产品需实时查询供应商可用性和套餐价格
  • Event Ticket:演出票需实时查询供应商座位库存和价格

供应商价格查询流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// SupplierPriceRequest 供应商价格查询请求
type SupplierPriceRequest struct {
CategoryID int64 `json:"category_id"` // 品类ID
SupplierID int64 `json:"supplier_id"` // 供应商ID
Items []ItemInfo `json:"items"` // 商品信息

// 品类特定参数
CheckInDate string `json:"check_in_date"` // Hotel: 入住日期
CheckOutDate string `json:"check_out_date"` // Hotel: 退房日期
RoomCount int32 `json:"room_count"` // Hotel: 房间数

FlightDate string `json:"flight_date"` // Flight: 航班日期
Passengers int32 `json:"passengers"` // Flight: 乘客数

TourDate string `json:"tour_date"` // Tour: 出游日期
TravelerCount int32 `json:"traveler_count"` // Tour: 游客数
}

// SupplierPriceResponse 供应商价格响应
type SupplierPriceResponse struct {
Items []SupplierItemPrice `json:"items"`
Available bool `json:"available"` // 是否可订
BookingToken string `json:"booking_token"` // 预订令牌(5-15分钟有效)
ExpireAt int64 `json:"expire_at"` // 令牌过期时间
SupplierOrderID string `json:"supplier_order_id"` // 供应商订单号
}

// SupplierItemPrice 供应商商品价格
type SupplierItemPrice struct {
ItemID int64 `json:"item_id"`
SupplierPrice int64 `json:"supplier_price"` // 供应商报价
SupplierCost int64 `json:"supplier_cost"` // 供应商成本
Available bool `json:"available"` // 是否可订
Stock int32 `json:"stock"` // 剩余库存

// 供应商特定信息
SupplierSKU string `json:"supplier_sku"`
SupplierRemark string `json:"supplier_remark"` // 供应商备注(如取消政策)
}

购物车处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
func GetCartPrice(req *GetCartPriceRequest) (*CartPriceResponse, error) {
var (
selfOperatedItems []ItemInfo // 自营商品
supplierItems []ItemInfo // 供应商商品
)

// 1. 区分自营和供应商品类
for _, item := range req.Items {
if isSupplierCategory(item.CategoryID) {
supplierItems = append(supplierItems, item)
} else {
selfOperatedItems = append(selfOperatedItems, item)
}
}

var (
wg sync.WaitGroup
selfPrice *PricingResponse
supplierPrices map[int64]*SupplierItemPrice
errs []error
)

// 2. 并发查询价格
if len(selfOperatedItems) > 0 {
wg.Add(1)
go func() {
defer wg.Done()
// 自营品类:使用本地计算
selfPrice, _ = pricingEngine.Calculate(&PricingRequest{
Scene: "Cart",
Items: selfOperatedItems,
})
}()
}

if len(supplierItems) > 0 {
wg.Add(1)
go func() {
defer wg.Done()
// 供应商品类:查询供应商价格
supplierPrices, _ = supplierPriceService.BatchQuery(ctx, supplierItems)
}()
}

wg.Wait()

// 3. 合并价格结果
return mergeCartPrice(selfPrice, supplierPrices), nil
}

关键特点

  • 区分品类:自营品类本地计算,供应商品类外部查询
  • 并发查询:自营和供应商价格并发获取
  • 预订令牌:供应商返回BookingToken(5-15分钟有效)
  • 库存验证:实时验证供应商库存可用性
  • ⚠️ 低缓存:供应商价格缓存时间短(1-5分钟)
  • ⚠️ 超时降级:供应商查询超时则使用数据库缓存价格

供应商价格查询时序图

sequenceDiagram
    participant Cart as 购物车
    participant PricingService as Pricing Service
    participant SupplierService as Supplier Price Service
    participant SupplierAPI as 外部供应商API
    participant Cache as Redis Cache
    
    Cart->>PricingService: GetCartPrice(items: [hotel, phone])
    
    PricingService->>PricingService: 区分自营/供应商品类
    
    par 并发查询价格
        Note over PricingService: 自营品类 (phone)
        PricingService->>PricingService: 本地计算价格
        
        Note over SupplierService: 供应商品类 (hotel)
        PricingService->>SupplierService: QuerySupplierPrice(hotelItem)
        
        SupplierService->>Cache: Get(cache_key)
        alt 缓存命中且未过期(<5分钟)
            Cache-->>SupplierService: CachedPrice
        else 缓存过期或未命中
            SupplierService->>SupplierAPI: CheckAvailability(hotelItem)
            SupplierAPI-->>SupplierService: Available=true, Price=1200, Token=xxx
            SupplierService->>Cache: Set(cache_key, 5min)
            
            alt 供应商查询超时
                Note over SupplierService: 降级:使用数据库缓存价格
                SupplierService->>SupplierService: GetFallbackPrice()
            end
        end
        
        SupplierService-->>PricingService: SupplierPrice
    end
    
    PricingService->>PricingService: 合并价格结果
    PricingService-->>Cart: CartPriceResponse

供应商对接配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 供应商价格配置
supplier_pricing:
enabled: true

# 供应商列表
suppliers:
- supplier_id: 1001
supplier_name: "Agoda"
category_ids: [100, 101] # Hotel
api_endpoint: "https://api.agoda.com/booking"
timeout: 3000ms
retry: 2
cache_ttl: 300s # 5分钟缓存

- supplier_id: 2001
supplier_name: "Expedia"
category_ids: [100]
api_endpoint: "https://api.expedia.com/availability"
timeout: 3000ms
retry: 2
cache_ttl: 300s

# 降级策略
fallback:
enabled: true
use_db_price: true # 使用数据库缓存价格
max_price_age: 3600s # 最大允许1小时的旧价格

2.2.4 订单创建计价(CreateOrder)

场景描述:用户点击”去结算”后,系统先创建订单。对于供应商品类(Hotel/Flight/Tour),需要先从外部供应商获取预订价格和BookingToken,然后基于供应商价格计算订单价格(包含基础价格、营销活动、附加费,但不含优惠券/积分),最后引导用户进入收银台。

计价要素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
PricingRequest {
Scene: "CreateOrder",
Items: [...], // 商品列表
UserID: 12345,
Region: "SG",

// 地址相关
ShippingAddress: {...}, // 收货地址
ShippingMethod: "standard", // 配送方式

// 增值服务(可选)
Services: [
{ServiceID: 1, Type: "screen_insurance", Price: 50, Selected: true},
],

// 供应商品类特有参数
SupplierBookings: [
{
ItemID: 101,
SupplierID: 1001,
BookingToken: "TKN-AGODA-20260227-001", // 供应商预订令牌
SupplierPrice: 1200, // 供应商报价
CheckInDate: "2026-03-01",
CheckOutDate: "2026-03-05",
},
],

// 订单上下文
OrderID: "ORD202602270001",
CartSnapshot: {...}, // 购物车价格快照(可选)
}

页面展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌─────────────────────────────────────────────────────┐
│ 创建订单... │
├─────────────────────────────────────────────────────┤
│ 收货地址:北京市朝阳区xxx │
├─────────────────────────────────────────────────────┤
│ 商品清单 │
│ iPhone 15 Pro × 2 ¥17,998 │
│ AirPods Pro × 1 ¥1,899 │
│ 手机壳 × 3 ¥297 │
├─────────────────────────────────────────────────────┤
│ 商品总额 ¥20,194 │
│ 活动优惠 -¥1,060 │
│ 运费 ¥0 │
│ 增值服务(碎屏险) +¥50 │
├─────────────────────────────────────────────────────┤
│ 订单金额:¥19,184 │
│ │
│ 正在跳转到收银台... │
└─────────────────────────────────────────────────────┘

关键特点

  • 订单创建:生成订单ID和订单号
  • 品类区分:自营品类和供应商品类分别处理
    • 自营品类:使用本地库存和价格,预扣库存(30分钟)
    • 供应商品类:先查询供应商实时价格,获取BookingToken(5-15分钟有效)
  • 供应商价格预订(仅供应商品类):
    • 并发调用外部供应商API查询价格和库存
    • 获取BookingToken和供应商报价
    • 验证供应商商品可用性
    • 处理供应商查询超时和降级
  • 价格计算:包含以下价格要素
    • ✅ Layer 1: 基础价格
      • 自营品类:本地商品市场价/折扣价
      • 供应商品类:供应商实时报价(SupplierPrice from BookingToken)
    • ✅ Layer 2: 营销价格(限时抢购、新人价、Bundle等活动优惠)
    • ✅ Layer 4: 附加费用
      • 增值服务费(如碎屏险)
      • 运费
      • 平台服务费(供应商品类佣金)
      • 不含支付手续费(留到Checkout)
    • ❌ 不包含优惠券(Layer 3 - voucher)
    • ❌ 不包含积分抵扣(Layer 3 - coin)
  • 库存预扣/预订
    • 自营品类:锁定本地库存(30分钟)
    • 供应商品类:保存BookingToken(与供应商有效期一致,通常5-15分钟)
  • 生成订单快照:保存订单价格和供应商预订信息(30分钟有效)
  • 订单状态pending_payment
  • ⚠️ 性能要求
    • 自营品类:P99 < 300ms
    • 供应商品类:P99 < 1000ms(含外部API调用)
  • ⚠️ 失败回滚:供应商预订失败需释放已锁定的本地库存

处理流程(区分自营/供应商品类)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
func CreateOrder(req *CreateOrderRequest) (*Order, error) {
// 步骤0: 区分自营和供应商品类
selfOperatedItems, supplierItems := classifyItems(req.Items)

// 步骤1: 供应商品类价格查询(如果有)
var supplierBookings map[int64]*SupplierBooking
if len(supplierItems) > 0 {
// 1.1 并发查询供应商价格并预订
supplierBookings, err := queryAndBookSupplierItems(ctx, supplierItems)
if err != nil {
return nil, fmt.Errorf("supplier booking failed: %w", err)
}

// 1.2 验证所有供应商商品可用
for itemID, booking := range supplierBookings {
if !booking.Available {
return nil, fmt.Errorf("item %d not available from supplier", itemID)
}
}
}

// 步骤2: 计算订单价格
// Layer 1: 基础价格(自营品类用本地价格,供应商品类用供应商报价)
// Layer 2: 营销活动(限时抢购、新人价等)
// Layer 4: 附加费用(增值服务费、运费、平台服务费等)
// 跳过 Layer 3: 优惠券和积分抵扣(留到Checkout)
// 跳过支付手续费(留到Checkout根据支付渠道计算)
pricingReq := &PricingRequest{
Scene: "CreateOrder",
Items: req.Items,
UserID: req.UserID,
Services: req.Services,
SupplierBookings: supplierBookings, // 传入供应商预订信息
// 不传 VoucherCode 和 CoinAmount
}

priceResult := pricingEngine.Calculate(pricingReq)

// 步骤3: 预扣库存
// 自营品类:锁定本地库存
if len(selfOperatedItems) > 0 {
if err := inventoryService.Reserve(req.OrderID, selfOperatedItems, 30*time.Minute); err != nil {
// 回滚供应商预订
rollbackSupplierBookings(supplierBookings)
return nil, fmt.Errorf("reserve inventory failed: %w", err)
}
}

// 供应商品类:BookingToken已在步骤1获取,此处记录即可

// 步骤4: 创建订单
order := &Order{
OrderID: req.OrderID,
UserID: req.UserID,
Items: req.Items,
OrderPrice: priceResult.FinalPrice, // 订单价格(含基础+营销+附加费)
SupplierBookings: supplierBookings, // 供应商预订信息
Status: "pending_payment",
CreatedAt: time.Now(),
ExpireAt: time.Now().Add(30 * time.Minute),
}

// 步骤5: 保存订单快照(包含供应商预订信息)
snapshot := saveOrderSnapshot(order.OrderID, priceResult, supplierBookings)

// 步骤6: 保存订单
if err := orderRepo.Create(order); err != nil {
inventoryService.Release(req.OrderID)
rollbackSupplierBookings(supplierBookings)
return nil, err
}

return order, nil
}

// queryAndBookSupplierItems 查询供应商价格并预订
func queryAndBookSupplierItems(ctx context.Context, items []ItemInfo) (map[int64]*SupplierBooking, error) {
var (
wg sync.WaitGroup
mu sync.Mutex
bookings = make(map[int64]*SupplierBooking)
errs []error
)

for _, item := range items {
wg.Add(1)
go func(itm ItemInfo) {
defer wg.Done()

// 查询供应商价格和预订
booking, err := supplierPriceService.QueryAndBook(ctx, &SupplierPriceRequest{
ItemID: itm.ItemID,
SupplierID: itm.SupplierID,
CheckInDate: itm.CheckInDate,
CheckOutDate: itm.CheckOutDate,
Quantity: itm.Quantity,
})

mu.Lock()
if err != nil {
errs = append(errs, err)
} else {
bookings[itm.ItemID] = booking
}
mu.Unlock()
}(item)
}

wg.Wait()

if len(errs) > 0 {
return nil, errs[0]
}

return bookings, nil
}

价格响应示例

1
2
3
4
5
6
7
8
9
10
{
"order_id": "ORD202602270001",
"total_amount": 20194,
"promotion_discount": 1060,
"additional_charge": 50,
"base_price": 19184,
"snapshot_id": "ORDER-20260227-001",
"expire_at": 1709116800,
"status": "pending_payment"
}

2.2.5 收银台计价(Checkout)

场景描述:订单创建后,用户进入收银台选择优惠券、积分、支付渠道,系统基于订单快照重新计算最终应付金额。

计价要素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PricingRequest {
Scene: "Checkout",
OrderID: "ORD202602270001", // 已创建的订单ID
UserID: 12345,

// 订单快照
OrderSnapshot: {...}, // CreateOrder 生成的订单快照

// 用户在收银台选择
VoucherCode: "SAVE100", // 优惠券
UseCoin: true, // 使用积分
CoinAmount: 500, // 积分数量
ChannelID: 1, // 支付渠道 (电子钱包)
}

页面展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
┌─────────────────────────────────────────────────────┐
│ 收银台 - 订单号:ORD202602270001 │
├─────────────────────────────────────────────────────┤
│ 订单金额:¥19,184 │
├─────────────────────────────────────────────────────┤
│ 💰 优惠券 │
│ ┌───────────────────────────────────────────────┐ │
│ │ ○ 不使用优惠券 │ │
│ │ ● 满10000减500 (SAVE100) -¥500 │ │
│ │ ○ 满5000减200 (SAVE200) │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ 🪙 积分抵扣 (可用: 10000积分) │
│ ┌───────────────────────────────────────────────┐ │
│ │ [x] 使用积分 [500] 积分 = ¥5 -¥5 │ │
│ └───────────────────────────────────────────────┘ │
│ │
│ 💳 支付方式 │
│ ┌───────────────────────────────────────────────┐ │
│ │ ● 电子钱包 (手续费2%) +¥372 │ │
│ │ ○ 信用卡 (手续费3%) │ │
│ │ ○ 银行转账 (免手续费) │ │
│ └───────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────┤
│ 商品总额: ¥20,194 │
│ 活动优惠: -¥1,060 │
│ 优惠券抵扣: -¥500 │
│ 积分抵扣: -¥5 │
│ 增值服务: +¥50 │
│ 支付手续费: +¥372 │
├─────────────────────────────────────────────────────┤
│ 应付金额:¥19,051 │
│ │
│ 订单将在 29:45 后自动取消 │
│ │
│ [确认支付] │
└─────────────────────────────────────────────────────┘

处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
func GetCheckoutPrice(req *CheckoutPriceRequest) (*CheckoutPriceResponse, error) {
// 1. 获取订单快照(验证订单有效性)
orderSnapshot := getOrderSnapshot(req.OrderID)
if orderSnapshot == nil {
return nil, fmt.Errorf("order not found or expired")
}

// 2. 验证订单状态
order := orderRepo.GetByID(req.OrderID)
if order.Status != "pending_payment" {
return nil, fmt.Errorf("order status invalid: %s", order.Status)
}

// 3. 完整计算价格(Layer 1-5,包含券/积分/手续费)
pricingReq := &PricingRequest{
Scene: "Checkout",
OrderID: req.OrderID,
Items: order.Items,
UserID: req.UserID,
VoucherCode: req.VoucherCode,
UseCoin: req.UseCoin,
CoinAmount: req.CoinAmount,
ChannelID: req.ChannelID,
BasePrice: orderSnapshot.OrderPrice, // 使用订单价格(基础+营销+附加费)
}

priceResult := pricingEngine.Calculate(pricingReq)

// 4. 预核销优惠券(软锁定)
if req.VoucherCode != "" {
if err := voucherService.SoftReserve(req.OrderID, req.VoucherCode); err != nil {
return nil, fmt.Errorf("voucher reserve failed: %w", err)
}
}

// 5. 预扣积分(软锁定)
if req.UseCoin {
if err := coinService.SoftReserve(req.OrderID, req.UserID, req.CoinAmount); err != nil {
return nil, fmt.Errorf("coin reserve failed: %w", err)
}
}

// 6. 生成支付快照(15分钟有效)
paymentSnapshot := createPaymentSnapshot(&PaymentSnapshot{
SnapshotID: generateSnapshotID(),
OrderID: req.OrderID,
UserID: req.UserID,
VoucherCode: req.VoucherCode,
CoinAmount: req.CoinAmount,
ChannelID: req.ChannelID,
PriceResult: priceResult,
CreatedAt: time.Now(),
ExpireAt: time.Now().Add(15 * time.Minute),
})

// 7. 构建响应
return &CheckoutPriceResponse{
OrderID: req.OrderID,
SnapshotID: paymentSnapshot.SnapshotID,
TotalAmount: priceResult.TotalAmount,
PromotionDiscount: priceResult.PromotionDiscount,
VoucherDiscount: priceResult.VoucherDiscount,
CoinDeduction: priceResult.CoinDeduction,
HandlingFee: priceResult.HandlingFee,
AdditionalCharge: priceResult.AdditionalCharge,
FinalPrice: priceResult.FinalPrice,
Breakdown: priceResult.Breakdown,
SnapshotExpireAt: paymentSnapshot.ExpireAt.Unix(),
}, nil
}

关键特点

  • 基于订单:依赖已创建的订单和订单快照
  • 完整计算:执行所有Layer(包含券/积分/手续费)
  • 软锁定:预核销优惠券和积分(不扣减,仅锁定)
  • 生成支付快照:保存最终价格(15分钟有效)
  • 实时计算:用户切换券/积分/渠道时实时重新计算
  • ⚠️ 性能要求:P99 < 200ms

价格响应示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"order_id": "ORD202602270001",
"snapshot_id": "PAY-20260227-001",
"total_amount": 20194,
"promotion_discount": 1060,
"voucher_discount": 500,
"coin_deduction": 5,
"handling_fee": 372,
"additional_charge": 50,
"final_price": 19051,
"breakdown": {
"formula": "20194 - 1060 - 500 - 5 + 372 + 50 = 19051"
},
"snapshot_expire_at": 1709116800
}

2.2.6 支付计价(Payment)

场景描述:用户点击”确认支付”,调起第三方支付前,验证支付金额和快照有效性。

计价要素

1
2
3
4
5
6
PricingRequest {
Scene: "Payment",
OrderID: "ORD202602270001",
PaymentSnapshotID: "PAY-20260227-001", // Checkout生成的支付快照
PaymentAmount: 19051, // 用户支付金额
}

处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
func ProcessPayment(req *PaymentRequest) (*PaymentResponse, error) {
// 1. 获取支付快照
paymentSnapshot := getPaymentSnapshot(req.PaymentSnapshotID)
if paymentSnapshot == nil {
return nil, fmt.Errorf("payment snapshot not found or expired")
}

// 2. 验证订单ID
if paymentSnapshot.OrderID != req.OrderID {
return nil, fmt.Errorf("order mismatch")
}

// 3. 验证支付金额
if req.PaymentAmount != paymentSnapshot.PriceResult.FinalPrice {
return nil, fmt.Errorf("payment amount mismatch: expected %d, got %d",
paymentSnapshot.PriceResult.FinalPrice, req.PaymentAmount)
}

// 4. 验证订单状态
order := orderRepo.GetByID(req.OrderID)
if order.Status != "pending_payment" {
return nil, fmt.Errorf("order status invalid: %s", order.Status)
}

// 5. 正式锁定资源(库存已在CreateOrder锁定)
// 正式核销优惠券
if paymentSnapshot.VoucherCode != "" {
if err := voucherService.Reserve(req.OrderID, paymentSnapshot.VoucherCode); err != nil {
return nil, fmt.Errorf("voucher reserve failed: %w", err)
}
}

// 正式扣减积分
if paymentSnapshot.CoinAmount > 0 {
if err := coinService.Reserve(req.OrderID, paymentSnapshot.UserID, paymentSnapshot.CoinAmount); err != nil {
voucherService.Release(req.OrderID)
return nil, fmt.Errorf("coin reserve failed: %w", err)
}
}

// 6. 更新订单状态为 paying(支付中)
order.Status = "paying"
order.PaymentSnapshotID = req.PaymentSnapshotID
orderRepo.Update(order)

// 7. 返回支付信息(用于调起第三方支付)
return &PaymentResponse{
OrderID: req.OrderID,
PaymentAmount: paymentSnapshot.PriceResult.FinalPrice,
ChannelID: paymentSnapshot.ChannelID,
PaymentURL: generatePaymentURL(req.OrderID, paymentSnapshot),
}, nil
}

关键特点

  • 快照验证:验证支付快照有效性
  • 金额验证:防止金额被篡改
  • 资源锁定:正式核销优惠券和扣减积分
  • 订单状态更新pending_paymentpaying
  • 零计算:直接使用快照,不重新计算价格
  • ⚠️ 性能要求:P99 < 500ms

2.2.7 支付成功处理(PaySuccess)

场景描述:第三方支付成功后,系统接收回调,完成最终资源扣减和订单状态更新。

处理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
func HandlePaymentCallback(req *PaymentCallbackRequest) error {
// 1. 验证回调签名
if !validatePaymentSignature(req) {
return fmt.Errorf("invalid signature")
}

// 2. 获取订单
order := orderRepo.GetByID(req.OrderID)
if order == nil {
return fmt.Errorf("order not found")
}

// 3. 验证订单状态(防止重复回调)
if order.Status == "paid" {
return nil // 已处理,直接返回成功
}

if order.Status != "paying" {
return fmt.Errorf("invalid order status: %s", order.Status)
}

// 4. 获取支付快照(验证支付金额)
paymentSnapshot := getPaymentSnapshot(order.PaymentSnapshotID)
if paymentSnapshot == nil {
return fmt.Errorf("payment snapshot not found")
}

if req.PaymentAmount != paymentSnapshot.PriceResult.FinalPrice {
return fmt.Errorf("payment amount mismatch")
}

// 5. 扣减库存(从预扣变为实扣)
if err := inventoryService.Confirm(order.OrderID); err != nil {
return fmt.Errorf("confirm inventory failed: %w", err)
}

// 6. 确认优惠券核销(从预扣变为实扣)
if paymentSnapshot.VoucherCode != "" {
if err := voucherService.Confirm(order.OrderID); err != nil {
return fmt.Errorf("confirm voucher failed: %w", err)
}
}

// 7. 确认积分扣减(从预扣变为实扣)
if paymentSnapshot.CoinAmount > 0 {
if err := coinService.Confirm(order.OrderID); err != nil {
return fmt.Errorf("confirm coin failed: %w", err)
}
}

// 8. 标记支付快照已使用
markSnapshotUsed(order.PaymentSnapshotID)

// 9. 更新订单状态
order.Status = "paid"
order.PaymentID = req.PaymentID
order.PaymentTime = time.Now()
order.ActualAmount = req.PaymentAmount
orderRepo.Update(order)

// 10. 发送订单完成事件
publishOrderPaidEvent(order)

return nil
}

关键特点

  • 回调验证:验证支付回调签名和订单状态
  • 幂等处理:防止重复回调导致重复扣减
  • 资源确认:将预扣资源(库存/券/积分)转为实扣
  • 快照标记:标记支付快照已使用
  • 订单完成:更新订单状态为 paid
  • 零价格计算:不需要重新计算价格
  • ⚠️ 不可回滚:支付成功后资源扣减不可逆

2.3 其他计价场景

2.3.1 优惠券预览计价

场景描述:用户在选择优惠券时,需要预览使用不同优惠券后的价格。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
┌─────────────────────────────────────────┐
│ 选择优惠券 │
├─────────────────────────────────────────┤
│ ○ 满10000减500 使用后: ¥18,551│
│ 有效期: 2026-03-01 │
│ │
│ ○ 满5000减200 使用后: ¥18,851│
│ 有效期: 2026-03-15 │
│ │
│ ○ 9折券 使用后: ¥17,146│
│ 有效期: 2026-02-28 │
│ │
│ [不使用优惠券] 当前: ¥19,051 │
└─────────────────────────────────────────┘

2.3.2 活动页面计价

场景描述:营销活动页面(如限时抢购、秒杀)需要展示活动价格。

1
2
3
4
5
6
PricingRequest {
Scene: "FlashSale",
Items: [{ItemID: 123, Quantity: 1}],
PromotionID: 456, // 活动ID
UserID: 789,
}

2.3.3 批量查询计价

场景描述:推荐页、列表页需要批量查询多个商品的价格。

1
2
3
4
5
6
7
8
9
10
PricingRequest {
Scene: "BatchQuery",
Items: [
{ItemID: 123, Quantity: 1},
{ItemID: 456, Quantity: 1},
{ItemID: 789, Quantity: 1},
// ... 最多100个
],
UserID: 12345,
}

2.4 计价场景对比

场景 调用方 性能要求 库存锁定 券/积分锁定 是否快照 计算内容 特殊处理
PDP 前端 P99 < 100ms 基础价+营销价 高缓存命中(>90%)
加购 前端 P99 < 150ms 基础价+营销价+券预估 凑单提示
购物车 前端 P99 < 200ms 基础价+营销价+券预估 供应商品类实时查询
创建订单 订单服务 P99 < 300ms
(供应商1s)
✅ 锁定30min
(供应商BookingToken)
✅ 订单快照 基础价+营销价+附加费
(不含券/积分)
供应商品类:先预订价格
库存预扣
供应商价格预订
Checkout 前端/收银台 P99 < 200ms ✅ 已锁定 ✅ 软锁定 ✅ 支付快照15min 完整计算
(基础+营销+券/积分+手续费)
实时重算
支付 支付服务 P99 < 500ms ✅ 已锁定 ✅ 正式锁定 ✅ 使用快照 零计算(快照验证) 防篡改验证
批量查询 推荐服务 P99 < 200ms 基础价+营销价 并发优化

关键差异说明

  1. 购物车:展示预估价格
    • 自营品类:本地价格计算(高缓存)
    • 供应商品类:实时查询供应商价格(低缓存1-5分钟),显示实时库存和价格
  2. CreateOrder:先创建订单
    • 自营品类:锁定本地库存(30分钟),计算订单价格(基础价+营销价+附加费,不含券/积分)
    • 供应商品类:先查询供应商API获取实时价格和BookingToken(5-15分钟),基于供应商报价计算订单价格
    • 生成订单快照(含订单价格 + 供应商BookingToken)
  3. Checkout:基于已创建订单,用户在收银台选择券/积分/渠道
    • 完整计算最终价格(在订单价格基础上加上券/积分/手续费)
    • 软锁定券/积分,生成支付快照(15分钟)
  4. Payment:验证支付快照和金额
    • 正式锁定券/积分
    • 供应商品类:使用BookingToken向供应商确认预订
    • 零价格计算(仅验证快照)

2.5 计价场景核心原则

2.5.1 价格一致性原则

1
2
3
4
5
6
7
8
9
10
11
前端展示价格 ≈ 订单价格 = 支付价格

允许的差异:
1. 营销活动过期
2. 库存不足导致无法参加活动
3. 用户重新选择支付渠道

不允许的差异:
1. 计算逻辑错误
2. 价格被恶意篡改
3. 系统bug导致的价格错误

2.5.2 价格锁定和快照原则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
┌────────────────────────────────────────────────────────────────────┐
│ 资源锁定与快照管理时机 │
├────────────────────────────────────────────────────────────────────┤
│ PDP/购物车: 无锁定(实时价格) │
│ • 供应商品类:实时查询价格(缓存1-5分钟) │
│ CreateOrder: • 自营品类:锁定本地库存(30分钟) │
│ • 供应商品类:查询供应商获取BookingToken(5-15分钟)│
│ • 生成订单快照(订单价格 + BookingToken) │
│ Checkout: • 库存已锁定 │
│ • 软锁定券/积分(15分钟) │
│ • 生成支付快照(最终价格,15分钟) │
│ Payment: • 正式锁定券/积分 │
│ • 供应商品类:使用BookingToken确认预订 │
│ • 使用支付快照验证 │
│ PaySuccess: • 确认库存扣减 │
│ • 确认券/积分核销 │
│ • 供应商品类:向供应商确认订单(Confirm Booking) │
│ • 标记快照已使用 │
└────────────────────────────────────────────────────────────────────┘

两个快照的作用:
1. 订单快照(CreateOrder生成):
- 保存订单价格(基础价格 + 营销价格 + 附加费)
- ✅ 包含:
- 自营品类:商品基础价、营销活动优惠、增值服务费、运费等
- **供应商品类**:**供应商报价**(SupplierPrice)、营销活动、**BookingToken**、平台服务费等
- ❌ 不包含:优惠券、积分、支付手续费
- 有效期:30分钟(与订单有效期一致)
- **供应商BookingToken**:5-15分钟(以供应商返回为准)

2. 支付快照(Checkout生成):
- 保存最终支付价格(订单价格 + 券/积分抵扣 + 支付手续费)
- ✅ 包含用户在收银台的所有选择(券/积分/支付渠道)
- ✅ 包含供应商BookingToken(用于Payment确认)
- 有效期15分钟(用户需尽快完成支付)

2.5.3 价格计算和验证原则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
价格计算流程:
1. CreateOrder: 计算订单价格
• Layer 1: 基础价格
• Layer 2: 营销活动
• Layer 4: 附加费(不含支付手续费)
→ 生成订单快照

2. Checkout: 基于订单,用户选择券/积分/渠道
→ 完整计算(Layer 1-5)
→ 生成支付快照

3. Payment: 验证支付快照和金额
→ 零计算(直接使用快照)

4. PaySuccess: 确认资源扣减
→ 零计算

防止机制:
- CreateOrder: 库存预扣 → 防止超卖
- Checkout: 支付快照 → 防止价格篡改
- Payment: 金额验证 → 防止支付金额不一致
- PaySuccess: 幂等处理 → 防止重复扣减

三、整体架构设计

3.1 场景驱动的系统架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
┌─────────────────────────────────────────────────────────────────────┐
│ Pricing Center (价格计算中心) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 【场景驱动的调用方】 │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ 场景1: PDP 场景2: AddToCart 场景3: Cart │ │
│ │ 前端 前端 前端 │ │
│ │ ↓ (P99<100ms) ↓ (P99<150ms) ↓ (P99<200ms) │ │
│ │ GetItemPrice AddToCartPrice GetCartPrice │ │
│ │ • 高缓存90% • 凑单提示 • 多商品联合 │ │
│ │ • 不锁定 • 不锁定 • 供应商品类实时查询 │ │
│ │ │ │
│ │ 场景4: CreateOrder 场景5: Checkout 场景6: Payment │ │
│ │ 订单服务 前端/收银台 支付服务 │ │
│ │ ↓ (P99<300ms/1s) ↓ (P99<200ms) ↓ (P99<500ms) │ │
│ │ CreateOrderPrice GetCheckoutPrice PaymentPrice │ │
│ │ • 库存锁定30min • 完整计算(券/积分)• 快照验证 │ │
│ │ • 供应商价格预订 • 软锁定券/积分 • 正式锁定券/积分 │ │
│ │ • 订单快照 • 支付快照15min • 供应商确认预订 │ │
│ │ │ │
│ │ 场景7: BatchQuery (列表页/推荐页批量查询) │ │
│ │ 聚合服务/推荐服务 │ │
│ │ ↓ (P99<200ms) │ │
│ │ BatchGetPrice (最多100个商品) │ │
│ │ • 并发优化 • 批量缓存 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 【统一入口层】 │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Pricing Service API Gateway │ │
│ │ ┌──────────────────────────────────────────────────────┐ │ │
│ │ │ SceneRouter (场景路由) │ │ │
│ │ │ • PDP → PDP Handler (轻量) │ │ │
│ │ │ • Cart → Cart Handler (多商品+供应商查询) │ │ │
│ │ │ • CreateOrder → Order Handler (创建订单+供应商预订+库存锁定)│ │
│ │ │ • Checkout → Checkout Handler (收银台+完整计算) │ │ │
│ │ │ • Payment → Payment Handler (快照验证+供应商确认+支付)│ │
│ │ └──────────────────────────────────────────────────────┘ │ │
│ │ • 请求校验(参数、签名、幂等) │ │
│ │ • 灰度路由(新老逻辑切流) │ │
│ │ • 空跑比对(Dry Run & Diff Report) │ │
│ │ • 限流熔断(按场景、品类、地区分别限流) │ │
│ │ • 价格快照管理(Snapshot Manager) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 【核心计算层】(5层责任链模式) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Pricing Engine (计算引擎) │ │
│ │ │ │
│ │ Layer 1: Base Price → 获取商品基础价格 │ │
│ │ • 自营品类:本地数据库价格 │ │
│ │ • 供应商品类:供应商报价(SupplierPrice) │ │
│ │ Layer 2: Promotion → 应用营销活动(FlashSale/新人价)│ │
│ │ Layer 3: Deduction → 应用抵扣(优惠券/积分) │ │
│ │ Layer 4: Charge → 计算费用(手续费/增值服务/平台服务费)│ │
│ │ Layer 5: Final → 汇总最终价格+生成明细 │ │
│ │ │ │
│ │ 不同场景可选择性跳过某些Layer: │ │
│ │ • PDP: Layer 1-2 (基础价+营销价) │ │
│ │ • Cart: Layer 1-3 (基础价+营销价+预估券) │ │
│ │ • CreateOrder: Layer 1,2,4 (基础价+营销价+附加费,不含券/积分)│ │
│ │ - 供应商品类:Layer 1使用供应商报价(需先查询供应商) │ │
│ │ • Checkout: Layer 1-5 (完整:基础+营销+券/积分+费用) │ │
│ │ • Payment: 零计算 (直接使用快照) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 【策略引擎】(品类差异化) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Topup策略 EMoney策略 EVoucher策略 Hotel策略 Movie策略 │ │
│ │ • 面额定价 • 附加费 • 购物车 • 日历价 • 场次价 │ │
│ │ • 折扣 • 手续费 • Bundle价 • 动态价 • 票种价 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 【依赖服务】 │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Item Service Promotion Service Voucher Service Coin │ │
│ │ Payment Service Inventory Service Config Service │ │
│ │ **Supplier Price Service** (外部供应商价格查询和预订) │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ 【数据层】 │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ MySQL Redis Cache MongoDB Config │ │
│ │ • 价格快照表 • L1 本地缓存(5min) • 计算日志 • 规则 │ │
│ │ • 订单价格表 • L2 Redis缓存(30min)• 空跑比对 • 灰度 │ │
│ │ • 价格变更日志 • 热点商品预热 • 差异分析 • 快照 │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘

架构特点

  • 场景驱动:不同场景使用不同的 Handler,优化性能和逻辑
  • 分层计算:责任链模式,不同场景可选择性执行某些层
  • 品类差异化:自营品类和供应商品类分别处理
    • 自营品类:本地价格 + 库存锁定
    • 供应商品类:外部API查询 + BookingToken预订
  • 快照管理:统一管理价格快照的生成、存储、验证(含供应商BookingToken)
  • 性能优化:按场景差异化缓存策略和性能目标
  • 供应商对接:异步查询、超时降级、失败回滚

3.2 场景驱动的API设计

不同计价场景对应不同的API接口,每个接口针对场景特点进行优化。

3.2.1 API接口定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package api

// PricingAPI 价格计算API接口
type PricingAPI interface {
// GetItemPrice PDP页面商品价格(场景1)
GetItemPrice(ctx context.Context, req *GetItemPriceRequest) (*ItemPriceResponse, error)

// GetCartPrice 购物车价格计算(场景2-3)
GetCartPrice(ctx context.Context, req *GetCartPriceRequest) (*CartPriceResponse, error)

// CalculateOrderPrice 订单创建价格计算(场景4 - 先创建订单)
CalculateOrderPrice(ctx context.Context, req *OrderPriceRequest) (*OrderPriceResponse, error)

// GetCheckoutPrice 收银台价格计算(场景5 - 后进入收银台)
GetCheckoutPrice(ctx context.Context, req *CheckoutPriceRequest) (*CheckoutPriceResponse, error)

// GetPaymentPrice 支付价格验证(场景6)
GetPaymentPrice(ctx context.Context, req *PaymentPriceRequest) (*PaymentPriceResponse, error)

// BatchGetPrice 批量查询价格(场景7)
BatchGetPrice(ctx context.Context, req *BatchPriceRequest) (*BatchPriceResponse, error)

// PreviewVoucherPrice 优惠券预览
PreviewVoucherPrice(ctx context.Context, req *VoucherPreviewRequest) ([]*VoucherPriceOption, error)
}

3.2.2 场景特定请求/响应模型

**GetItemPrice (PDP场景)**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// GetItemPriceRequest PDP商品价格请求
type GetItemPriceRequest struct {
ItemID int64 `json:"item_id" binding:"required"`
ModelID int64 `json:"model_id"`
UserID int64 `json:"user_id"`
Region string `json:"region" binding:"required"`
Platform string `json:"platform"`
}

// ItemPriceResponse PDP价格响应(轻量级)
type ItemPriceResponse struct {
ItemID int64 `json:"item_id"`

// 基础价格
MarketPrice int64 `json:"market_price"` // 市场价
DiscountPrice int64 `json:"discount_price"` // 折扣价

// 活动价格(可选)
FlashSalePrice *int64 `json:"flash_sale_price"` // 限时抢购价
NewUserPrice *int64 `json:"new_user_price"` // 新人价

// 营销标签
PromotionTags []string `json:"promotion_tags"` // ["flash_sale", "new_user"]

// 优惠预估
EstimatedPrice int64 `json:"estimated_price"` // 预估到手价(使用最优优惠券)
AvailableVouchers int32 `json:"available_vouchers"` // 可用优惠券数量

// 元数据
CacheHit bool `json:"-"` // 是否缓存命中
CalculatedAt int64 `json:"calculated_at"` // 计算时间戳
}

**GetCheckoutPrice (Checkout场景)**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// CheckoutPriceRequest 订单确认页价格请求
type CheckoutPriceRequest struct {
Items []CartItem `json:"items" binding:"required"`
UserID int64 `json:"user_id" binding:"required"`
Region string `json:"region" binding:"required"`

// 用户选择
VoucherCode string `json:"voucher_code"`
UseCoin bool `json:"use_coin"`
CoinAmount int64 `json:"coin_amount"`
ChannelID int64 `json:"channel_id"`

// 增值服务
Services []ServiceSelection `json:"services"`

// 配送信息
ShippingAddress *Address `json:"shipping_address"`
}

// CheckoutPriceResponse 订单确认页价格响应(完整)
type CheckoutPriceResponse struct {
// 价格快照ID
SnapshotID string `json:"snapshot_id"` // 快照ID(15分钟有效)

// 商品价格明细
Items []ItemPricing `json:"items"`

// 价格汇总
TotalAmount int64 `json:"total_amount"` // 商品总额
PromotionDiscount int64 `json:"promotion_discount"` // 活动优惠
VoucherDiscount int64 `json:"voucher_discount"` // 优惠券抵扣
CoinDeduction int64 `json:"coin_deduction"` // 积分抵扣
HandlingFee int64 `json:"handling_fee"` // 手续费
AdditionalCharge int64 `json:"additional_charge"` // 附加费用
FinalPrice int64 `json:"final_price"` // 最终价格

// 价格拆解
Breakdown PriceBreakdown `json:"breakdown"` // 计算明细

// 锁定信息
LockedUntil int64 `json:"locked_until"` // 价格锁定到期时间
SnapshotVersion string `json:"snapshot_version"` // 快照版本号

CalculatedAt int64 `json:"calculated_at"`
}

**CalculateOrderPrice (创建订单场景)**:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// OrderPriceRequest 订单创建价格请求
type OrderPriceRequest struct {
OrderID string `json:"order_id" binding:"required"`
SnapshotID string `json:"snapshot_id"` // Checkout的快照ID

Items []CartItem `json:"items" binding:"required"`
UserID int64 `json:"user_id" binding:"required"`
VoucherCode string `json:"voucher_code"`
UseCoin bool `json:"use_coin"`
CoinAmount int64 `json:"coin_amount"`
ChannelID int64 `json:"channel_id"`
Services []ServiceSelection `json:"services"`
}

// OrderPriceResponse 订单价格响应
type OrderPriceResponse struct {
OrderID string `json:"order_id"`

// 价格验证结果
PriceValid bool `json:"price_valid"` // 价格是否一致
PriceChanged bool `json:"price_changed"` // 价格是否变化
OldPrice int64 `json:"old_price"` // 快照价格
NewPrice int64 `json:"new_price"` // 重新计算价格
PriceDiff int64 `json:"price_diff"` // 价格差异

// 如果价格一致,返回完整价格信息
FinalPrice int64 `json:"final_price"`
Breakdown PriceBreakdown `json:"breakdown"`

// 新快照ID(用于支付)
PaymentSnapshotID string `json:"payment_snapshot_id"` // 支付快照ID
SnapshotExpireAt int64 `json:"snapshot_expire_at"` // 快照过期时间

CalculatedAt int64 `json:"calculated_at"`
}

3.2.3 场景Handler实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
package handler

// PDPHandler PDP场景处理器(高性能)
type PDPHandler struct {
engine PricingEngine
cache CacheManager
itemService ItemService
promoService PromotionService
}

func (h *PDPHandler) Handle(ctx context.Context, req *GetItemPriceRequest) (*ItemPriceResponse, error) {
// 1. 构建缓存Key
cacheKey := h.buildCacheKey(req)

// 2. 查询缓存(PDP场景缓存命中率要求 > 90%)
if cached := h.cache.Get(cacheKey); cached != nil {
return cached.(*ItemPriceResponse), nil
}

// 3. 计算价格(仅执行 Layer 1-2,不计算抵扣和费用)
pricingReq := &PricingRequest{
Scene: ScenePDP,
Items: []ItemInfo{{ItemID: req.ItemID, ModelID: req.ModelID, Quantity: 1}},
UserID: req.UserID,
Region: req.Region,
Platform: req.Platform,

// PDP场景不需要计算抵扣和费用
SkipLayers: []string{"deduction", "charge"},
}

result := h.engine.Calculate(ctx, pricingReq)

// 4. 构建响应(轻量级)
resp := &ItemPriceResponse{
ItemID: req.ItemID,
MarketPrice: result.Items[0].MarketPrice,
DiscountPrice: result.Items[0].DiscountPrice,
FlashSalePrice: result.Items[0].FlashSalePrice,
NewUserPrice: result.Items[0].NewUserPrice,
PromotionTags: result.PromotionTags,
CalculatedAt: time.Now().Unix(),
}

// 5. 预估可用优惠券(异步,不阻塞)
go h.estimateVouchers(ctx, req, resp)

// 6. 写入缓存(5分钟)
h.cache.Set(cacheKey, resp, 5*time.Minute)

return resp, nil
}

// CheckoutHandler Checkout场景处理器(完整计算+快照)
type CheckoutHandler struct {
engine PricingEngine
snapshotManager *SnapshotManager
inventoryService InventoryService
}

func (h *CheckoutHandler) Handle(ctx context.Context, req *CheckoutPriceRequest) (*CheckoutPriceResponse, error) {
// 1. 预检库存(避免无库存商品计算价格)
if err := h.inventoryService.CheckAvailability(ctx, req.Items); err != nil {
return nil, fmt.Errorf("inventory check failed: %w", err)
}

// 2. 完整计算价格(执行所有 Layer)
pricingReq := &PricingRequest{
Scene: SceneCheckout,
Items: req.Items,
UserID: req.UserID,
Region: req.Region,
VoucherCode: req.VoucherCode,
UseCoin: req.UseCoin,
CoinAmount: req.CoinAmount,
ChannelID: req.ChannelID,
Services: req.Services,

// Checkout场景执行完整计算
SkipLayers: []string{},
}

result := h.engine.Calculate(ctx, pricingReq)

// 3. 生成价格快照(15分钟有效)
snapshot := h.snapshotManager.CreateSnapshot(&PriceSnapshot{
SnapshotID: generateSnapshotID(),
Scene: SceneCheckout,
UserID: req.UserID,
Items: req.Items,
PriceResult: result,
CreatedAt: time.Now(),
ExpireAt: time.Now().Add(15 * time.Minute),
})

// 4. 构建响应
resp := &CheckoutPriceResponse{
SnapshotID: snapshot.SnapshotID,
Items: result.Items,
TotalAmount: result.TotalAmount,
PromotionDiscount: result.PromotionDiscount,
VoucherDiscount: result.VoucherDiscount,
CoinDeduction: result.CoinDeduction,
HandlingFee: result.HandlingFee,
AdditionalCharge: result.AdditionalCharge,
FinalPrice: result.FinalPrice,
Breakdown: result.Breakdown,
LockedUntil: snapshot.ExpireAt.Unix(),
SnapshotVersion: snapshot.Version,
CalculatedAt: time.Now().Unix(),
}

return resp, nil
}

// OrderHandler 订单创建场景处理器(价格验证+锁定)
type OrderHandler struct {
engine PricingEngine
snapshotManager *SnapshotManager
inventoryService InventoryService
voucherService VoucherService
coinService CoinService
}

func (h *OrderHandler) Handle(ctx context.Context, req *OrderPriceRequest) (*OrderPriceResponse, error) {
// 1. 获取 Checkout 快照
checkoutSnapshot := h.snapshotManager.GetSnapshot(req.SnapshotID)
if checkoutSnapshot == nil {
return nil, fmt.Errorf("checkout snapshot expired or not found")
}

// 2. 重新计算价格(二次验证)
pricingReq := &PricingRequest{
Scene: SceneCreateOrder,
Items: req.Items,
UserID: req.UserID,
VoucherCode: req.VoucherCode,
UseCoin: req.UseCoin,
CoinAmount: req.CoinAmount,
ChannelID: req.ChannelID,
Services: req.Services,
}

newResult := h.engine.Calculate(ctx, pricingReq)

// 3. 价格比对
priceChanged := false
priceDiff := int64(0)
oldPrice := checkoutSnapshot.PriceResult.FinalPrice
newPrice := newResult.FinalPrice

if oldPrice != newPrice {
priceChanged = true
priceDiff = newPrice - oldPrice

// 价格变化超过5元,拒绝创建订单
if abs(priceDiff) > 500 {
return &OrderPriceResponse{
PriceValid: false,
PriceChanged: true,
OldPrice: oldPrice,
NewPrice: newPrice,
PriceDiff: priceDiff,
}, nil
}
}

// 4. 库存锁定
if err := h.inventoryService.Reserve(ctx, req.OrderID, req.Items, 30*time.Minute); err != nil {
return nil, fmt.Errorf("reserve inventory failed: %w", err)
}

// 5. 优惠券预核销
if req.VoucherCode != "" {
if err := h.voucherService.Reserve(ctx, req.OrderID, req.VoucherCode); err != nil {
h.inventoryService.Release(ctx, req.OrderID)
return nil, fmt.Errorf("reserve voucher failed: %w", err)
}
}

// 6. 积分预扣
if req.UseCoin {
if err := h.coinService.Reserve(ctx, req.OrderID, req.UserID, req.CoinAmount); err != nil {
h.voucherService.Release(ctx, req.OrderID)
h.inventoryService.Release(ctx, req.OrderID)
return nil, fmt.Errorf("reserve coin failed: %w", err)
}
}

// 7. 生成支付快照(30分钟有效)
paymentSnapshot := h.snapshotManager.CreateSnapshot(&PriceSnapshot{
SnapshotID: generateSnapshotID(),
Scene: ScenePayment,
OrderID: req.OrderID,
UserID: req.UserID,
Items: req.Items,
PriceResult: newResult,
CreatedAt: time.Now(),
ExpireAt: time.Now().Add(30 * time.Minute),
Version: "v1",
})

// 8. 构建响应
resp := &OrderPriceResponse{
OrderID: req.OrderID,
PriceValid: true,
PriceChanged: priceChanged,
OldPrice: oldPrice,
NewPrice: newPrice,
PriceDiff: priceDiff,
FinalPrice: newResult.FinalPrice,
Breakdown: newResult.Breakdown,
PaymentSnapshotID: paymentSnapshot.SnapshotID,
SnapshotExpireAt: paymentSnapshot.ExpireAt.Unix(),
CalculatedAt: time.Now().Unix(),
}

return resp, nil
}

3.3 价格快照管理机制

价格快照是保证价格一致性和防止价格篡改的关键机制。

3.3.1 快照数据模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
-- 价格快照表
CREATE TABLE price_snapshot_tab (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
snapshot_id VARCHAR(64) NOT NULL COMMENT '快照ID',

-- 场景信息
scene VARCHAR(50) NOT NULL COMMENT 'checkout/order/payment',
order_id VARCHAR(64) COMMENT '订单ID',
user_id BIGINT NOT NULL,

-- 价格数据(JSON)
price_data JSON NOT NULL COMMENT '完整价格计算结果',

-- 快照元数据
version VARCHAR(20) NOT NULL COMMENT '版本号',
status VARCHAR(20) DEFAULT 'active' COMMENT 'active/used/expired',

-- 时间信息
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expire_at TIMESTAMP NOT NULL COMMENT '过期时间',
used_at TIMESTAMP NULL COMMENT '使用时间',

UNIQUE KEY uk_snapshot_id (snapshot_id),
KEY idx_order_id (order_id),
KEY idx_user_expire (user_id, expire_at),
KEY idx_expire_status (expire_at, status)
) COMMENT='价格快照表';

3.3.2 快照管理器实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
package pricing

import (
"context"
"encoding/json"
"fmt"
"time"
)

// SnapshotManager 价格快照管理器
type SnapshotManager struct {
repo SnapshotRepository
cache CacheManager
}

// PriceSnapshot 价格快照
type PriceSnapshot struct {
SnapshotID string
Scene PricingScene
OrderID string
UserID int64
Items []ItemInfo
PriceResult *PricingResponse
Version string
Status string
CreatedAt time.Time
ExpireAt time.Time
UsedAt *time.Time
}

// CreateSnapshot 创建价格快照
func (sm *SnapshotManager) CreateSnapshot(snapshot *PriceSnapshot) *PriceSnapshot {
// 1. 保存到数据库
sm.repo.Create(snapshot)

// 2. 写入缓存(提高查询性能)
cacheKey := fmt.Sprintf("price_snapshot:%s", snapshot.SnapshotID)
sm.cache.Set(cacheKey, snapshot, snapshot.ExpireAt.Sub(time.Now()))

return snapshot
}

// GetSnapshot 获取价格快照
func (sm *SnapshotManager) GetSnapshot(snapshotID string) *PriceSnapshot {
// 1. 查询缓存
cacheKey := fmt.Sprintf("price_snapshot:%s", snapshotID)
if cached := sm.cache.Get(cacheKey); cached != nil {
return cached.(*PriceSnapshot)
}

// 2. 查询数据库
snapshot := sm.repo.GetByID(snapshotID)
if snapshot == nil {
return nil
}

// 3. 检查是否过期
if time.Now().After(snapshot.ExpireAt) {
sm.repo.UpdateStatus(snapshotID, "expired")
return nil
}

// 4. 回写缓存
sm.cache.Set(cacheKey, snapshot, snapshot.ExpireAt.Sub(time.Now()))

return snapshot
}

// ValidateSnapshot 验证快照(支付时使用)
func (sm *SnapshotManager) ValidateSnapshot(snapshotID string, orderID string) (*PriceSnapshot, error) {
snapshot := sm.GetSnapshot(snapshotID)

if snapshot == nil {
return nil, fmt.Errorf("snapshot not found or expired")
}

if snapshot.OrderID != orderID {
return nil, fmt.Errorf("snapshot order mismatch")
}

if snapshot.Status != "active" {
return nil, fmt.Errorf("snapshot already used or expired")
}

return snapshot, nil
}

// UseSnapshot 使用快照(标记为已使用)
func (sm *SnapshotManager) UseSnapshot(snapshotID string) error {
now := time.Now()
return sm.repo.UpdateStatus(snapshotID, "used", &now)
}

// CleanExpiredSnapshots 清理过期快照(定时任务)
func (sm *SnapshotManager) CleanExpiredSnapshots() {
sm.repo.DeleteExpired(time.Now().Add(-24 * time.Hour))
}

3.4 价格计算流程(场景驱动)

3.4.1 PDP 页面价格计算流程

sequenceDiagram
    participant FE as 前端
    participant Gateway as API Gateway
    participant PDPHandler as PDP Handler
    participant Cache as Redis Cache
    participant Engine as Pricing Engine
    participant Item as Item Service
    participant Promo as Promotion Service

    FE->>Gateway: GetItemPrice(itemId=123, userId=789)
    Note over Gateway: 场景路由 → PDP Handler
    Gateway->>PDPHandler: Handle(GetItemPriceRequest)
    
    PDPHandler->>Cache: Get(cache_key)
    alt 缓存命中(命中率 > 90%)
        Cache-->>PDPHandler: ItemPriceResponse
        Note over PDPHandler: 直接返回,耗时 < 10ms
    else 缓存未命中
        PDPHandler->>Engine: Calculate(ScenePDP, SkipLayers=[deduction,charge])
        Note over Engine: 仅执行 Layer 1-2(基础价+促销)
        
        Engine->>Item: GetItemBaseInfo(itemId)
        Item-->>Engine: marketPrice, discountPrice
        
        Engine->>Promo: GetActivePromotions(itemId, userId)
        Promo-->>Engine: flashSale, newUserPrice
        
        Engine->>Engine: 计算价格(无需抵扣/费用计算)
        Engine-->>PDPHandler: PricingResult
        
        PDPHandler->>PDPHandler: 构建轻量响应
        PDPHandler->>Cache: Set(cache_key, response, 5min)
        Note over PDPHandler: 异步预估可用优惠券
    end
    
    PDPHandler-->>Gateway: ItemPriceResponse
    Gateway-->>FE: 返回价格
    Note over FE: 展示:折扣价 + 活动价 + 可用券数

PDP场景特点

  • 高缓存命中:缓存命中率 > 90%,命中时延 < 10ms
  • 轻量计算:只执行 Layer 1-2,跳过抵扣和费用计算
  • 无锁定:不锁定库存和优惠券
  • 异步预估:不阻塞主流程,后台估算优惠券

3.4.2 订单创建价格计算流程(CreateOrder - 库存锁定+订单快照)

sequenceDiagram
    participant FE as 前端
    participant Order as Order Service
    participant Gateway as API Gateway
    participant OrderHandler as Order Handler
    participant SupplierSvc as Supplier Price Service
    participant SupplierAPI as 外部供应商API
    participant Engine as Pricing Engine
    participant Inventory as Inventory Service
    participant SnapshotMgr as Snapshot Manager
    participant DB as MySQL

    FE->>Order: 点击"去结算"
    Order->>Gateway: CalculateOrderPrice(items, userId, services)
    Note over Gateway: 场景路由 → Order Handler
    Gateway->>OrderHandler: Handle(OrderPriceRequest)
    
    rect rgb(255, 230, 230)
        Note over OrderHandler: Step 0: 区分自营/供应商品类
        OrderHandler->>OrderHandler: classifyItems() → selfOperated + supplier
    end
    
    rect rgb(255, 245, 230)
        Note over OrderHandler: Step 1: 供应商价格预订(如果有供应商品类)
        par 并发查询供应商价格
            OrderHandler->>SupplierSvc: QueryAndBook(hotelItem)
            SupplierSvc->>SupplierAPI: CheckAvailability(hotelInfo)
            alt 供应商超时
                Note over SupplierSvc: 降级:使用DB缓存价格
            else 供应商正常返回
                SupplierAPI-->>SupplierSvc: Available=true
Price=1200
BookingToken=TKN-001 end SupplierSvc-->>OrderHandler: SupplierBooking{price, token, expire} and 自营品类库存预检 OrderHandler->>Inventory: CheckAvailability(selfOperatedItems) alt 库存不足 Inventory-->>OrderHandler: ErrOutOfStock OrderHandler-->>Order: Error: 库存不足 end end end rect rgb(220, 240, 255) Note over OrderHandler: Step 2: 计算订单价格 OrderHandler->>Engine: Calculate(SceneCreateOrder, items, supplierBookings) Note over Engine: Layer 1: 基础价格
• 自营:本地价格
• 供应商:SupplierPrice
Layer 2: 营销活动
Layer 4: 附加费(含平台服务费,不含支付手续费)
跳过 Layer 3: 优惠券/积分 Engine->>Engine: GetBasePrice() (Layer 1) Note over Engine: 自营品类:本地数据库价格
供应商品类:SupplierPrice from BookingToken Engine->>Engine: ApplyPromotions() (Layer 2: 限时抢购/新人价) Engine->>Engine: CalculateAdditionalFee() (Layer 4: 增值服务费/运费/平台服务费) Engine->>Engine: CalculateFinalPrice() Engine-->>OrderHandler: PricingResult (orderPrice=19184) Note over Engine: 含基础价(供应商报价) + 营销价 + 附加费
不含券/积分/支付手续费 end rect rgb(230, 255, 230) Note over OrderHandler: Step 3: 锁定资源 par 自营品类库存锁定 OrderHandler->>Inventory: Reserve(orderID, selfOperatedItems, 30min) Inventory->>DB: UPDATE inventory SET reserved += qty alt 锁定失败 Inventory-->>OrderHandler: Error Note over OrderHandler: 回滚供应商预订 end Inventory-->>OrderHandler: OK and 供应商品类预订确认 Note over OrderHandler: 保存 BookingToken(5-15分钟有效) end end rect rgb(230, 240, 255) Note over OrderHandler: Step 4: 创建订单 OrderHandler->>DB: INSERT INTO order_tab (order_id, order_price, supplier_bookings, status='pending_payment') Note over OrderHandler: Step 5: 生成订单快照(30分钟) OrderHandler->>SnapshotMgr: CreateSnapshot(OrderSnapshot + SupplierBookings) Note over SnapshotMgr: 保存订单价格
(基础价+营销价+附加费)
+ 供应商BookingToken SnapshotMgr->>DB: INSERT INTO price_snapshot_tab
(含 supplier_booking_token) SnapshotMgr-->>OrderHandler: snapshotID="ORDER-20260227-001" end OrderHandler->>OrderHandler: 构建响应 OrderHandler-->>Gateway: OrderPriceResponse{ orderID, orderPrice=19184, supplierBookings, snapshotID, expireAt } Gateway-->>Order: 返回订单信息 Order-->>FE: 跳转到收银台 Note over FE: 携带 orderID 和 BookingToken 进入收银台

CreateOrder场景特点

  • 先创建订单:生成订单ID和订单号
  • 品类区分:自营品类和供应商品类分别处理
    • 自营品类:使用本地库存和价格
    • 供应商品类:先查询供应商实时价格并获取BookingToken
  • 供应商价格预订(仅供应商品类):
    • 并发调用外部供应商API查询价格和库存
    • 获取BookingToken(5-15分钟有效)和供应商报价
    • 处理超时降级(使用DB缓存价格)
    • 预订失败需回滚其他资源锁定
  • 价格计算:包含以下要素
    • ✅ Layer 1: 基础价格
      • 自营品类:本地商品市场价/折扣价
      • 供应商品类:供应商实时报价(SupplierPrice from BookingToken)
    • ✅ Layer 2: 营销价格(限时抢购、新人价、Bundle等活动优惠)
    • ✅ Layer 4: 附加费用
      • 增值服务费(如碎屏险)
      • 运费
      • 平台服务费(供应商品类佣金)
      • 不含支付手续费(留到Checkout)
    • ❌ 不含 Layer 3: 优惠券和积分抵扣(留到Checkout)
  • 库存预扣/预订
    • 自营品类:锁定本地库存(30分钟)
    • 供应商品类:保存BookingToken(5-15分钟)
  • 生成订单快照:保存订单价格和供应商预订信息(30分钟有效)
  • 订单状态pending_payment
  • ⚠️ 性能要求
    • 自营品类:P99 < 300ms
    • 供应商品类:P99 < 1000ms(含外部API调用)
  • ⚠️ 失败回滚:供应商预订失败需释放已锁定的本地库存

3.4.3 收银台价格计算流程(Checkout - 完整计算+支付快照)

sequenceDiagram
    participant FE as 前端/收银台
    participant Gateway as API Gateway
    participant CheckoutHandler as Checkout Handler
    participant SnapshotMgr as Snapshot Manager
    participant Engine as Pricing Engine
    participant Voucher as Voucher Service
    participant Coin as Coin Service
    participant DB as MySQL
    participant Order as Order Service

    FE->>Gateway: GetCheckoutPrice(orderID, voucherCode, useCoin, channelID)
    Note over Gateway: 场景路由 → Checkout Handler
    Gateway->>CheckoutHandler: Handle(CheckoutPriceRequest)
    
    rect rgb(255, 230, 230)
        Note over CheckoutHandler: Step 1: 获取订单快照
        CheckoutHandler->>SnapshotMgr: GetSnapshot(orderSnapshotID)
        alt 订单快照已过期
            SnapshotMgr-->>CheckoutHandler: nil
            CheckoutHandler-->>Gateway: Error: 订单已过期
            Gateway-->>FE: 提示重新下单
        end
        SnapshotMgr-->>CheckoutHandler: orderSnapshot (orderPrice=19184)
含基础价+营销价+附加费 Note over CheckoutHandler: Step 2: 验证订单状态 CheckoutHandler->>Order: GetOrder(orderID) Order-->>CheckoutHandler: order (status='pending_payment') alt 订单状态异常 CheckoutHandler-->>Gateway: Error: 订单状态无效 end end rect rgb(220, 240, 255) Note over CheckoutHandler: Step 3: 完整计算最终价格 CheckoutHandler->>Engine: Calculate(SceneCheckout, orderID, voucherCode, coin, channel) Note over Engine: 执行完整 Layer 1-5(包含券/积分/手续费) Engine->>Engine: 使用订单快照的价格(基础+营销+附加费) Engine->>Voucher: ValidateVoucher(voucherCode) Voucher-->>Engine: discount = 500 Engine->>Coin: CalculateCoinDeduction(useCoin, amount) Coin-->>Engine: coinDeduction = 5 Engine->>Engine: CalculateHandlingFee(channelID) Engine->>Engine: CalculateFinalPrice() Engine-->>CheckoutHandler: PricingResult (finalPrice=19051) end rect rgb(230, 255, 230) Note over CheckoutHandler: Step 4: 软锁定券和积分(15分钟) par 并发软锁定 CheckoutHandler->>Voucher: SoftReserve(orderID, voucherCode) Note over Voucher: 软锁定(可释放) Voucher-->>CheckoutHandler: OK CheckoutHandler->>Coin: SoftReserve(orderID, coinAmount) Note over Coin: 软锁定(可释放) Coin-->>CheckoutHandler: OK end end rect rgb(230, 240, 255) Note over CheckoutHandler: Step 5: 生成支付快照(15分钟) CheckoutHandler->>SnapshotMgr: CreateSnapshot(PaymentSnapshot) Note over SnapshotMgr: 保存最终支付价格 SnapshotMgr->>DB: INSERT INTO price_snapshot_tab SnapshotMgr-->>CheckoutHandler: paymentSnapshotID="PAY-20260227-001" end CheckoutHandler->>CheckoutHandler: 构建完整响应 CheckoutHandler-->>Gateway: CheckoutPriceResponse{ orderID, finalPrice=19051, voucherDiscount=500, coinDeduction=5, handlingFee=372, breakdown, paymentSnapshotID, expireAt } Gateway-->>FE: 返回价格 Note over FE: 展示最终价格明细 + [确认支付] 按钮

Checkout场景特点

  • 基于订单:依赖已创建的订单和订单快照
  • 完整计算:执行所有 Layer(1-5),包含券/积分/手续费
  • 软锁定:预核销优惠券和积分(不实际扣减,可释放)
  • 生成支付快照:保存最终价格(15分钟有效)
  • 实时重算:用户切换券/积分/渠道时实时重新计算
  • ⚠️ 性能要求:P99 < 200ms

3.4.4 支付价格验证流程(Payment - 快照验证+调起支付)

sequenceDiagram
    participant Payment as Payment Service
    participant Gateway as API Gateway
    participant PaymentHandler as Payment Handler
    participant SnapshotMgr as Snapshot Manager
    participant SupplierSvc as Supplier Price Service
    participant SupplierAPI as 外部供应商API
    participant VoucherSvc as Voucher Service
    participant CoinSvc as Coin Service
    participant DB as MySQL

    Payment->>Gateway: GetPaymentPrice(orderID, paymentSnapshotID)
    Note over Gateway: 场景路由 → Payment Handler
    Gateway->>PaymentHandler: Handle(PaymentPriceRequest)
    
    rect rgb(255, 245, 230)
        Note over PaymentHandler: Step 1: 验证快照
        PaymentHandler->>SnapshotMgr: ValidateSnapshot(paymentSnapshotID, orderID)
        
        SnapshotMgr->>DB: SELECT * FROM price_snapshot_tab WHERE snapshot_id=?
        DB-->>SnapshotMgr: Snapshot Data (含 BookingToken)
        
        alt 快照不存在或已过期
            SnapshotMgr-->>PaymentHandler: Error: 快照无效
            PaymentHandler-->>Payment: Error: 请重新提交订单
        end
        
        alt 订单ID不匹配
            SnapshotMgr-->>PaymentHandler: Error: 订单不匹配
            PaymentHandler-->>Payment: Error: 订单验证失败
        end
        
        alt 快照已使用
            SnapshotMgr-->>PaymentHandler: Error: 快照已使用
            PaymentHandler-->>Payment: Error: 重复支付
        end
        
        SnapshotMgr-->>PaymentHandler: Valid Snapshot (finalPrice=19051, bookingTokens)
    end
    
    rect rgb(255, 230, 230)
        Note over PaymentHandler: Step 2: 供应商确认预订(如果有)
        alt 订单包含供应商品类
            PaymentHandler->>SupplierSvc: ConfirmBooking(bookingTokens)
            SupplierSvc->>SupplierAPI: ConfirmReservation(TKN-001)
            alt BookingToken过期或无效
                SupplierAPI-->>SupplierSvc: Error: Token过期
                SupplierSvc-->>PaymentHandler: Error: 供应商预订失败
                PaymentHandler-->>Payment: Error: 请重新下单
            else 供应商确认成功
                SupplierAPI-->>SupplierSvc: Confirmed, SupplierOrderID=SP-001
                SupplierSvc-->>PaymentHandler: OK (supplierOrderID)
            end
        end
    end
    
    rect rgb(230, 255, 230)
        Note over PaymentHandler: Step 3: 正式锁定券/积分
        par 锁定优惠券
            PaymentHandler->>VoucherSvc: Reserve(orderID, voucherCode)
            VoucherSvc-->>PaymentHandler: OK
        and 锁定积分
            PaymentHandler->>CoinSvc: Reserve(orderID, coinAmount)
            CoinSvc-->>PaymentHandler: OK
        end
    end
    
    PaymentHandler->>PaymentHandler: 验证支付金额
    alt 支付金额不匹配
        PaymentHandler-->>Payment: Error: 金额不一致
    end
    
    PaymentHandler-->>Gateway: PaymentPriceResponse{
        finalPrice: 19051,
        breakdown,
        snapshotID,
        supplierOrderIDs
    }
    Gateway-->>Payment: 返回价格
    
    Note over Payment: 调用第三方支付
    Payment->>Payment: 支付成功
    
    rect rgb(230, 240, 255)
        Note over Payment: Step 4: 支付成功回调
        Payment->>Gateway: MarkSnapshotUsed(snapshotID)
        Gateway->>SnapshotMgr: UseSnapshot(snapshotID)
        SnapshotMgr->>DB: UPDATE price_snapshot_tab SET status='used', used_at=NOW()
        SnapshotMgr-->>Payment: OK
    end

支付场景特点

  • 快照验证:验证快照ID、订单ID、状态、过期时间
  • 供应商确认(仅供应商品类):
    • 使用BookingToken向供应商确认预订
    • 获取供应商订单号(SupplierOrderID)
    • 处理BookingToken过期(需用户重新下单)
  • 正式锁定资源
    • 优惠券:从软锁定转为正式锁定(Reserve)
    • 积分:从软锁定转为正式锁定(Reserve)
  • 金额验证:验证支付金额与快照金额一致
  • 防重复支付:快照使用后标记为 used
  • 零计算:直接使用快照,不重新计算价格
  • ⚠️ 性能要求:P99 < 500ms(自营),P99 < 1000ms(供应商品类)

3.5 核心数据模型

3.3.1 价格请求模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// PricingRequest 价格计算请求
type PricingRequest struct {
// 基础信息
Scene PricingScene `json:"scene"` // 计价场景:PDP/Checkout/Order/Pay
CategoryID int64 `json:"category_id"` // 品类ID
Region string `json:"region"` // 地区
Platform Platform `json:"platform"` // 平台:App/Wap/PC

// 用户信息
UserID int64 `json:"user_id"` // 用户ID
IsNewUser bool `json:"is_new_user"` // 是否新用户

// 商品信息
Items []ItemInfo `json:"items"` // 商品列表

// 供应商品类信息(CreateOrder场景)
SupplierBookings map[int64]*SupplierBooking `json:"supplier_bookings"` // 供应商预订信息

// 营销信息
VoucherCode string `json:"voucher_code"` // 优惠券码
UseCoin bool `json:"use_coin"` // 是否使用积分
CoinAmount int64 `json:"coin_amount"` // 积分数量

// 支付信息
ChannelID int64 `json:"channel_id"` // 支付渠道ID

// 增值服务
Services []Service `json:"services"` // 增值服务(如碎屏险)

// 灰度控制
DryRun bool `json:"dry_run"` // 是否空跑
UseNew bool `json:"use_new"` // 是否使用新逻辑
}

// ItemInfo 商品信息
type ItemInfo struct {
ItemID int64 `json:"item_id"`
ModelID int64 `json:"model_id"`
Quantity int32 `json:"quantity"`

// 商品价格(可选,用于验证)
MarketPrice int64 `json:"market_price"`
DiscountPrice int64 `json:"discount_price"`

// 供应商品类特有字段
IsSupplier bool `json:"is_supplier"` // 是否供应商品类
SupplierID int64 `json:"supplier_id"` // 供应商ID(仅供应商品类)
CategoryID int64 `json:"category_id"` // 品类ID
}

// SupplierBooking 供应商预订信息(供应商品类专用)
type SupplierBooking struct {
ItemID int64 `json:"item_id"`
SupplierID int64 `json:"supplier_id"`
BookingToken string `json:"booking_token"` // 供应商预订令牌
SupplierPrice int64 `json:"supplier_price"` // 供应商报价
SupplierCost int64 `json:"supplier_cost"` // 供应商成本
Available bool `json:"available"` // 是否可订
Stock int32 `json:"stock"` // 剩余库存
ExpireAt int64 `json:"expire_at"` // BookingToken过期时间
SupplierOrderID string `json:"supplier_order_id"` // 供应商订单号(确认后生成)

// 品类特定参数(Hotel/Flight/Tour等)
CheckInDate string `json:"check_in_date"` // Hotel: 入住日期
CheckOutDate string `json:"check_out_date"` // Hotel: 退房日期
RoomCount int32 `json:"room_count"` // Hotel: 房间数
FlightDate string `json:"flight_date"` // Flight: 航班日期
Passengers int32 `json:"passengers"` // Flight: 乘客数
TourDate string `json:"tour_date"` // Tour: 出游日期
TravelerCount int32 `json:"traveler_count"` // Tour: 游客数

// 供应商特定信息
SupplierSKU string `json:"supplier_sku"` // 供应商SKU
SupplierRemark string `json:"supplier_remark"` // 供应商备注(如取消政策)
}

// Service 增值服务
type Service struct {
ServiceID int64 `json:"service_id"`
ServiceType string `json:"service_type"` // screen_insurance, warranty, etc.
Price int64 `json:"price"`
Selected bool `json:"selected"`
}

3.3.2 价格响应模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// PricingResponse 价格计算响应
type PricingResponse struct {
// 基础价格
TotalAmount int64 `json:"total_amount"` // 商品总额
DiscountAmount int64 `json:"discount_amount"` // 折扣金额

// 营销优惠
PromotionDiscount int64 `json:"promotion_discount"` // 营销折扣
VoucherDiscount int64 `json:"voucher_discount"` // 优惠券折扣
CoinDeduction int64 `json:"coin_deduction"` // 积分抵扣
ChannelDiscount int64 `json:"channel_discount"` // 渠道折扣

// 附加费用
HandlingFee int64 `json:"handling_fee"` // 手续费
ServiceFee int64 `json:"service_fee"` // 服务费
AdditionalCharge int64 `json:"additional_charge"` // 附加费用

// 最终价格
FinalPrice int64 `json:"final_price"` // 最终支付金额

// 价格明细
Items []ItemPricing `json:"items"` // 商品价格明细
Breakdown PriceBreakdown `json:"breakdown"` // 价格拆解

// 营销信息
PromotionTags []string `json:"promotion_tags"` // 营销标签

// 元数据
CalculatedAt int64 `json:"calculated_at"` // 计算时间
Version string `json:"version"` // 版本号
IsMigrated bool `json:"is_migrated"` // 是否已迁移
}

// ItemPricing 单品价格明细
type ItemPricing struct {
ItemID int64 `json:"item_id"`
Quantity int32 `json:"quantity"`

// 价格层级
MarketPrice int64 `json:"market_price"` // 市场价
DiscountPrice int64 `json:"discount_price"` // 折扣价
PromotionPrice int64 `json:"promotion_price"` // 营销价
FinalPrice int64 `json:"final_price"` // 最终价

// 营销活动
FlashSalePrice *int64 `json:"flash_sale_price"` // 限时抢购价
NewUserPrice *int64 `json:"new_user_price"` // 新人价
BundlePrice *int64 `json:"bundle_price"` // 捆绑价

// 小计
Subtotal int64 `json:"subtotal"` // 小计
}

// PriceBreakdown 价格拆解
type PriceBreakdown struct {
// 按层级拆解
BaseAmount int64 `json:"base_amount"` // 基础金额
PromotionAmount int64 `json:"promotion_amount"` // 营销后金额
DeductionAmount int64 `json:"deduction_amount"` // 抵扣后金额
ChargeAmount int64 `json:"charge_amount"` // 加费后金额
FinalAmount int64 `json:"final_amount"` // 最终金额

// 计算公式
Formula string `json:"formula"` // 计算公式
}

3.4 灰度与空跑机制

3.4.1 灰度配置模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// GrayConfig 灰度配置
type GrayConfig struct {
// 品类维度
Categories map[int64]*CategoryGray `json:"categories"`

// 默认配置
DefaultGray *GrayRule `json:"default_gray"`
}

// CategoryGray 品类灰度配置
type CategoryGray struct {
CategoryID int64 `json:"category_id"`

// 平台维度
App *GrayRule `json:"app"`
Wap *GrayRule `json:"wap"`
PC *GrayRule `json:"pc"`
}

// GrayRule 灰度规则
type GrayRule struct {
// 空跑开关
DryRun bool `json:"dry_run"` // 是否开启空跑
DryRunRatio int32 `json:"dry_run_ratio"` // 空跑比例 (0-10000)

// 灰度开关
UseNew bool `json:"use_new"` // 是否使用新逻辑
GrayRatio int32 `json:"gray_ratio"` // 灰度比例 (0-10000)

// 白名单
WhitelistUsers []int64 `json:"whitelist_users"` // 用户白名单
WhitelistRegions []string `json:"whitelist_regions"`// 地区白名单
}

3.4.2 空跑比对

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// DryRunResult 空跑比对结果
type DryRunResult struct {
RequestID string `json:"request_id"`
Scene PricingScene `json:"scene"`
CategoryID int64 `json:"category_id"`

// 新老价格
NewPrice *PricingResponse `json:"new_price"`
OldPrice *PricingResponse `json:"old_price"`

// 比对结果
HasDiff bool `json:"has_diff"`
DiffFields []string `json:"diff_fields"`
DiffDetails map[string]DiffDetail `json:"diff_details"`

// 元数据
Timestamp int64 `json:"timestamp"`
Region string `json:"region"`
}

// DiffDetail 差异明细
type DiffDetail struct {
Field string `json:"field"`
OldValue interface{} `json:"old_value"`
NewValue interface{} `json:"new_value"`
Diff interface{} `json:"diff"`
DiffPercent float64 `json:"diff_percent"`
}

四、核心功能实现

4.1 价格计算引擎核心代码

4.1.1 计算引擎接口定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package pricing

import (
"context"
)

// Engine 价格计算引擎接口
type Engine interface {
// CalculatePrice 计算价格
CalculatePrice(ctx context.Context, req *PricingRequest) (*PricingResponse, error)

// CalculateWithDryRun 计算价格(带空跑比对)
CalculateWithDryRun(ctx context.Context, req *PricingRequest) (*PricingResponse, *DryRunResult, error)

// BatchCalculate 批量计算价格
BatchCalculate(ctx context.Context, reqs []*PricingRequest) ([]*PricingResponse, error)
}

// Calculator 价格计算器(策略接口)
type Calculator interface {
// Calculate 执行计算
Calculate(ctx context.Context, req *PricingRequest) (*PricingResponse, error)

// Support 是否支持该品类
Support(categoryID int64) bool

// Priority 优先级(用于策略选择)
Priority() int
}

// Layer 价格计算层(责任链模式)
type Layer interface {
// Process 处理当前层
Process(ctx context.Context, req *PricingRequest, state *PricingState) error

// Name 层名称
Name() string

// Order 执行顺序
Order() int
}

4.1.2 引擎实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
package pricing

import (
"context"
"fmt"
"sort"
"sync"
"time"
)

// engine 价格计算引擎实现
type engine struct {
// 策略注册表
calculators map[int64]Calculator
calculatorsMu sync.RWMutex

// 计算层
layers []Layer

// 依赖服务
itemService ItemService
promoService PromotionService
voucherService VoucherService
coinService CoinService
paymentService PaymentService

// 灰度配置
grayConfig *GrayConfig
grayConfigMu sync.RWMutex

// 缓存
cache Cache

// 监控
metrics Metrics
}

// NewEngine 创建价格引擎
func NewEngine(opts ...EngineOption) Engine {
e := &engine{
calculators: make(map[int64]Calculator),
layers: make([]Layer, 0),
}

for _, opt := range opts {
opt(e)
}

// 初始化计算层
e.initLayers()

return e
}

// initLayers 初始化计算层(5层责任链)
func (e *engine) initLayers() {
e.layers = []Layer{
NewBasePriceLayer(e.itemService), // Layer 1: 基础价格
NewPromotionLayer(e.promoService), // Layer 2: 营销活动
NewDeductionLayer(e.voucherService, e.coinService), // Layer 3: 抵扣
NewChargeLayer(e.paymentService), // Layer 4: 附加费用
NewFinalPriceLayer(), // Layer 5: 最终价格
}

// 按顺序排序
sort.Slice(e.layers, func(i, j int) bool {
return e.layers[i].Order() < e.layers[j].Order()
})
}

// CalculatePrice 计算价格
func (e *engine) CalculatePrice(ctx context.Context, req *PricingRequest) (*PricingResponse, error) {
startTime := time.Now()
defer func() {
e.metrics.RecordLatency("calculate_price", time.Since(startTime))
}()

// 参数校验
if err := e.validateRequest(req); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}

// 判断是否使用新逻辑
useNew := e.shouldUseNew(req)
if !useNew {
// 降级到老逻辑
return e.calculateByOldLogic(ctx, req)
}

// 检查缓存
if resp := e.checkCache(req); resp != nil {
e.metrics.Inc("cache_hit")
return resp, nil
}
e.metrics.Inc("cache_miss")

// 选择计算器
calculator := e.selectCalculator(req.CategoryID)
if calculator != nil {
// 使用品类专用计算器
resp, err := calculator.Calculate(ctx, req)
if err != nil {
return nil, fmt.Errorf("calculator failed: %w", err)
}

// 写入缓存
e.writeCache(req, resp)

return resp, nil
}

// 使用通用计算逻辑
return e.calculateByLayers(ctx, req)
}

// calculateByLayers 使用责任链模式计算(支持场景驱动的层级跳过)
func (e *engine) calculateByLayers(ctx context.Context, req *PricingRequest) (*PricingResponse, error) {
// 初始化计算状态
state := NewPricingState(req)

// 根据场景决定需要跳过哪些层
skipLayers := e.getSkipLayers(req.Scene)

// 依次执行各计算层
for _, layer := range e.layers {
// 检查是否需要跳过该层
if e.shouldSkipLayer(layer.Name(), skipLayers) {
e.metrics.Inc(fmt.Sprintf("skip_layer_%s", layer.Name()))
continue
}

if err := layer.Process(ctx, req, state); err != nil {
return nil, fmt.Errorf("layer %s failed: %w", layer.Name(), err)
}
}

// 构建响应
return state.BuildResponse(), nil
}

// getSkipLayers 获取场景对应需要跳过的层
func (e *engine) getSkipLayers(scene PricingScene) []string {
switch scene {
case ScenePDP:
// PDP 场景只需要基础价格和促销层,跳过抵扣和费用计算
return []string{"deduction", "charge"}

case SceneAddToCart:
// 加购场景需要基础价格、促销,跳过抵扣和费用计算
return []string{"deduction", "charge"}

case SceneCart:
// 购物车场景需要基础价格、促销,可预估券,跳过费用计算
return []string{"charge"}

case SceneCreateOrder:
// CreateOrder 场景:计算订单价格(含基础价+营销价+附加费,不含券/积分)
// ✅ 执行 Layer 1: 基础价格(商品市场价/折扣价)
// ✅ 执行 Layer 2: 营销活动(限时抢购、新人价等)
// ✅ 执行 Layer 4: 附加费用(增值服务费、运费等,但不含支付手续费)
// ❌ 跳过 Layer 3: 优惠券和积分抵扣
// ❌ 跳过支付手续费(留到Checkout根据支付渠道计算)
// 注:实际实现中,Layer 4需要区分增值服务费和支付手续费
return []string{"deduction", "payment_handling_fee"}

case SceneCheckout:
// Checkout 场景:收银台完整计算(包含券/积分/手续费)
// 执行所有 Layer(1-5)
return []string{}

case ScenePayment:
// Payment 场景直接使用快照,不需要重新计算
return []string{"base_price", "promotion", "deduction", "charge", "final"}

default:
// 默认执行完整计算
return []string{}
}
}

// shouldSkipLayer 判断是否跳过某个层
func (e *engine) shouldSkipLayer(layerName string, skipLayers []string) bool {
for _, skip := range skipLayers {
if skip == layerName {
return true
}
}
return false
}

// CalculateWithDryRun 计算价格(带空跑比对)
func (e *engine) CalculateWithDryRun(ctx context.Context, req *PricingRequest) (*PricingResponse, *DryRunResult, error) {
// 判断是否需要空跑
if !e.shouldDryRun(req) {
resp, err := e.CalculatePrice(ctx, req)
return resp, nil, err
}

var (
newResp *PricingResponse
oldResp *PricingResponse
wg sync.WaitGroup
newErr error
oldErr error
)

// 并发执行新老逻辑
wg.Add(2)

// 执行新逻辑
go func() {
defer wg.Done()
newReq := *req
newReq.UseNew = true
newResp, newErr = e.CalculatePrice(ctx, &newReq)
}()

// 执行老逻辑
go func() {
defer wg.Done()
oldResp, oldErr = e.calculateByOldLogic(ctx, req)
}()

wg.Wait()

// 检查错误
if newErr != nil {
return nil, nil, fmt.Errorf("new logic failed: %w", newErr)
}
if oldErr != nil {
// 老逻辑失败不影响
e.metrics.Inc("old_logic_error")
}

// 比对差异
diffResult := e.comparePrices(req, newResp, oldResp)

// 上报差异
if diffResult.HasDiff {
e.reportDiff(ctx, diffResult)
}

return newResp, diffResult, nil
}

// selectCalculator 选择品类计算器
func (e *engine) selectCalculator(categoryID int64) Calculator {
e.calculatorsMu.RLock()
defer e.calculatorsMu.RUnlock()

// 优先查找完全匹配的计算器
if calc, ok := e.calculators[categoryID]; ok {
return calc
}

// 查找支持该品类的计算器
var selected Calculator
maxPriority := -1

for _, calc := range e.calculators {
if calc.Support(categoryID) && calc.Priority() > maxPriority {
selected = calc
maxPriority = calc.Priority()
}
}

return selected
}

// shouldUseNew 判断是否使用新逻辑
func (e *engine) shouldUseNew(req *PricingRequest) bool {
if req.UseNew {
return true
}

// 获取灰度配置
gray := e.getGrayConfig(req)
if gray == nil || !gray.UseNew {
return false
}

// 白名单判断
if e.inWhitelist(req, gray) {
return true
}

// 灰度比例判断
return e.hitGrayRatio(req, gray.GrayRatio)
}

// shouldDryRun 判断是否需要空跑
func (e *engine) shouldDryRun(req *PricingRequest) bool {
if req.DryRun {
return true
}

gray := e.getGrayConfig(req)
if gray == nil || !gray.DryRun {
return false
}

return e.hitGrayRatio(req, gray.DryRunRatio)
}

// comparePrices 比对价格差异
func (e *engine) comparePrices(req *PricingRequest, newResp, oldResp *PricingResponse) *DryRunResult {
result := &DryRunResult{
RequestID: generateRequestID(),
Scene: req.Scene,
CategoryID: req.CategoryID,
NewPrice: newResp,
OldPrice: oldResp,
DiffDetails: make(map[string]DiffDetail),
Timestamp: time.Now().Unix(),
Region: req.Region,
}

if oldResp == nil {
return result
}

// 比对最终价格
if newResp.FinalPrice != oldResp.FinalPrice {
result.HasDiff = true
result.DiffFields = append(result.DiffFields, "final_price")
result.DiffDetails["final_price"] = DiffDetail{
Field: "final_price",
OldValue: oldResp.FinalPrice,
NewValue: newResp.FinalPrice,
Diff: newResp.FinalPrice - oldResp.FinalPrice,
}
}

// 比对其他关键字段
e.comparePriceField(result, "total_amount", newResp.TotalAmount, oldResp.TotalAmount)
e.comparePriceField(result, "voucher_discount", newResp.VoucherDiscount, oldResp.VoucherDiscount)
e.comparePriceField(result, "coin_deduction", newResp.CoinDeduction, oldResp.CoinDeduction)
e.comparePriceField(result, "handling_fee", newResp.HandlingFee, oldResp.HandlingFee)

return result
}

// RegisterCalculator 注册品类计算器
func (e *engine) RegisterCalculator(categoryID int64, calc Calculator) {
e.calculatorsMu.Lock()
defer e.calculatorsMu.Unlock()

e.calculators[categoryID] = calc
}

4.1.3 场景驱动的缓存策略

不同场景对缓存的需求不同,需要差异化的缓存策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
package pricing

import (
"fmt"
"time"
)

// CacheManager 缓存管理器
type CacheManager struct {
localCache *LocalCache // L1: 本地缓存
redisCache *RedisCache // L2: Redis缓存
}

// checkCache 检查缓存(场景驱动)
func (e *engine) checkCache(req *PricingRequest) *PricingResponse {
// 根据场景判断是否使用缓存
if !e.shouldUseCache(req.Scene) {
return nil
}

// 构建缓存Key
cacheKey := e.buildCacheKey(req)

// L1: 本地缓存(最快)
if resp := e.cache.GetLocal(cacheKey); resp != nil {
e.metrics.Inc("cache_l1_hit")
return resp
}

// L2: Redis缓存(稍慢)
if resp := e.cache.GetRedis(cacheKey); resp != nil {
e.metrics.Inc("cache_l2_hit")
// 回写L1缓存
e.cache.SetLocal(cacheKey, resp, e.getL1TTL(req.Scene))
return resp
}

return nil
}

// writeCache 写入缓存(场景驱动)
func (e *engine) writeCache(req *PricingRequest, resp *PricingResponse) {
if !e.shouldUseCache(req.Scene) {
return
}

cacheKey := e.buildCacheKey(req)

// 获取TTL
l1TTL := e.getL1TTL(req.Scene)
l2TTL := e.getL2TTL(req.Scene)

// 并发写入L1和L2
go e.cache.SetLocal(cacheKey, resp, l1TTL)
go e.cache.SetRedis(cacheKey, resp, l2TTL)
}

// shouldUseCache 判断是否使用缓存
func (e *engine) shouldUseCache(scene PricingScene) bool {
switch scene {
case ScenePDP:
// PDP 场景高缓存命中率
return true

case SceneAddToCart:
// 加购场景使用缓存
return true

case SceneCart:
// 购物车场景使用缓存
return true

case SceneCheckout:
// Checkout 场景不使用缓存(需要实时计算)
return false

case SceneCreateOrder:
// 订单创建不使用缓存
return false

case ScenePayment:
// 支付场景不使用缓存(使用快照)
return false

default:
return false
}
}

// getL1TTL 获取L1缓存TTL(场景驱动)
func (e *engine) getL1TTL(scene PricingScene) time.Duration {
switch scene {
case ScenePDP:
return 5 * time.Minute // PDP 5分钟

case SceneAddToCart:
return 3 * time.Minute // 加购 3分钟

case SceneCart:
return 2 * time.Minute // 购物车 2分钟

default:
return 1 * time.Minute
}
}

// getL2TTL 获取L2缓存TTL(场景驱动)
func (e *engine) getL2TTL(scene PricingScene) time.Duration {
switch scene {
case ScenePDP:
return 30 * time.Minute // PDP 30分钟

case SceneAddToCart:
return 15 * time.Minute // 加购 15分钟

case SceneCart:
return 10 * time.Minute // 购物车 10分钟

default:
return 5 * time.Minute
}
}

// buildCacheKey 构建缓存Key
func (e *engine) buildCacheKey(req *PricingRequest) string {
// 根据场景构建不同的Key
switch req.Scene {
case ScenePDP:
// PDP 场景Key:scene:itemID:modelID:userID:region
return fmt.Sprintf("price:pdp:%d:%d:%d:%s",
req.Items[0].ItemID,
req.Items[0].ModelID,
req.UserID,
req.Region)

case SceneAddToCart:
// 加购场景Key:scene:itemID:quantity:userID:region
return fmt.Sprintf("price:add_cart:%d:%d:%d:%s",
req.Items[0].ItemID,
req.Items[0].Quantity,
req.UserID,
req.Region)

case SceneCart:
// 购物车场景Key:scene:itemIDs:userID:region
itemIDs := e.joinItemIDs(req.Items)
return fmt.Sprintf("price:cart:%s:%d:%s",
itemIDs,
req.UserID,
req.Region)

default:
return fmt.Sprintf("price:%s:%s", req.Scene, e.hashRequest(req))
}
}

// CacheWarmup 缓存预热(针对热门商品)
func (e *engine) CacheWarmup(ctx context.Context, itemIDs []int64, region string) error {
for _, itemID := range itemIDs {
req := &PricingRequest{
Scene: ScenePDP,
Items: []ItemInfo{{ItemID: itemID, Quantity: 1}},
Region: region,
}

resp, err := e.CalculatePrice(ctx, req)
if err != nil {
continue
}

// 写入缓存
e.writeCache(req, resp)
}

return nil
}

// InvalidateCache 缓存失效(价格变化时)
func (e *engine) InvalidateCache(itemID int64, region string) {
// 删除所有相关的缓存Key
patterns := []string{
fmt.Sprintf("price:pdp:%d:*", itemID),
fmt.Sprintf("price:add_cart:%d:*", itemID),
fmt.Sprintf("price:cart:*:%d:*", itemID),
}

for _, pattern := range patterns {
e.cache.DeleteByPattern(pattern)
}
}

场景缓存策略对比

场景 L1缓存 L2缓存 命中率目标 失效策略
PDP 5分钟 30分钟 > 90% 价格变化时失效
AddToCart 3分钟 15分钟 > 70% 价格变化时失效
Cart 2分钟 10分钟 > 60% 价格变化时失效
Checkout ❌ 不缓存 ❌ 不缓存 - 实时计算
CreateOrder ❌ 不缓存 ❌ 不缓存 - 实时计算
Payment ❌ 不缓存 ❌ 不缓存 - 使用快照

缓存优化策略

  • 热点预热:大促前预热热门商品价格(提升命中率到 95%+)
  • 分层缓存:L1本地缓存 + L2Redis缓存(降低延迟)
  • 智能失效:价格变化时精准失效相关缓存
  • 缓存穿透保护:空值缓存防止缓存穿透

4.2 计算层实现

4.2.1 基础价格层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package pricing

import (
"context"
"fmt"
)

// BasePriceLayer 基础价格层
type BasePriceLayer struct {
itemService ItemService
}

func NewBasePriceLayer(itemService ItemService) Layer {
return &BasePriceLayer{
itemService: itemService,
}
}

func (l *BasePriceLayer) Name() string {
return "base_price"
}

func (l *BasePriceLayer) Order() int {
return 1
}

func (l *BasePriceLayer) Process(ctx context.Context, req *PricingRequest, state *PricingState) error {
// 批量获取商品信息
itemIDs := make([]int64, 0, len(req.Items))
for _, item := range req.Items {
itemIDs = append(itemIDs, item.ItemID)
}

itemInfos, err := l.itemService.BatchGetItems(ctx, itemIDs)
if err != nil {
return fmt.Errorf("get items failed: %w", err)
}

// 计算基础价格
var totalAmount int64
for i, item := range req.Items {
info, ok := itemInfos[item.ItemID]
if !ok {
return fmt.Errorf("item %d not found", item.ItemID)
}

var unitPrice int64

// 区分自营品类和供应商品类
if item.IsSupplier {
// 供应商品类:使用供应商报价
if booking, ok := req.SupplierBookings[item.ItemID]; ok {
unitPrice = booking.SupplierPrice // 使用供应商实时报价

// 保存供应商信息到状态
state.Items[i].IsSupplier = true
state.Items[i].SupplierPrice = booking.SupplierPrice
state.Items[i].BookingToken = booking.BookingToken
} else {
return fmt.Errorf("supplier booking not found for item %d", item.ItemID)
}
} else {
// 自营品类:使用本地折扣价或市场价
unitPrice = info.DiscountPrice
if unitPrice == 0 {
unitPrice = info.MarketPrice
}
}

subtotal := unitPrice * int64(item.Quantity)
totalAmount += subtotal

// 保存到状态
state.Items[i].MarketPrice = info.MarketPrice
state.Items[i].DiscountPrice = info.DiscountPrice
state.Items[i].Subtotal = subtotal
}

state.TotalAmount = totalAmount
state.CurrentAmount = totalAmount

return nil
}

4.2.2 营销活动层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package pricing

import (
"context"
"fmt"
)

// PromotionLayer 营销活动层
type PromotionLayer struct {
promoService PromotionService
}

func NewPromotionLayer(promoService PromotionService) Layer {
return &PromotionLayer{
promoService: promoService,
}
}

func (l *PromotionLayer) Name() string {
return "promotion"
}

func (l *PromotionLayer) Order() int {
return 2
}

func (l *PromotionLayer) Process(ctx context.Context, req *PricingRequest, state *PricingState) error {
// 获取营销活动
promoReq := &GetPromotionRequest{
Items: req.Items,
UserID: req.UserID,
IsNewUser: req.IsNewUser,
Region: req.Region,
CategoryID: req.CategoryID,
}

promoResp, err := l.promoService.GetPromotion(ctx, promoReq)
if err != nil {
// 营销活动失败不影响主流程
return nil
}

var totalDiscount int64

// 应用营销活动
for i, item := range req.Items {
promo, ok := promoResp.Items[item.ItemID]
if !ok {
continue
}

itemState := &state.Items[i]

// Flash Sale
if promo.FlashSalePrice != nil && *promo.FlashSalePrice > 0 {
itemState.FlashSalePrice = promo.FlashSalePrice
itemState.PromotionPrice = *promo.FlashSalePrice * int64(item.Quantity)
state.PromotionTags = append(state.PromotionTags, "flash_sale")
}

// New User Price
if req.IsNewUser && promo.NewUserPrice != nil && *promo.NewUserPrice > 0 {
itemState.NewUserPrice = promo.NewUserPrice
itemState.PromotionPrice = *promo.NewUserPrice * int64(item.Quantity)
state.PromotionTags = append(state.PromotionTags, "new_user_price")
}

// Bundle Price
if promo.BundlePrice != nil && *promo.BundlePrice > 0 {
itemState.BundlePrice = promo.BundlePrice
itemState.PromotionPrice = *promo.BundlePrice * int64(item.Quantity)
state.PromotionTags = append(state.PromotionTags, "bundle_price")
}

// 计算折扣
if itemState.PromotionPrice > 0 {
discount := itemState.Subtotal - itemState.PromotionPrice
totalDiscount += discount
itemState.FinalPrice = itemState.PromotionPrice
} else {
itemState.FinalPrice = itemState.Subtotal
}
}

// 更新状态
state.PromotionDiscount = totalDiscount
state.CurrentAmount = state.TotalAmount - totalDiscount

return nil
}

4.2.3 抵扣层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package pricing

import (
"context"
"fmt"
)

// DeductionLayer 抵扣层
type DeductionLayer struct {
voucherService VoucherService
coinService CoinService
}

func NewDeductionLayer(voucherService VoucherService, coinService CoinService) Layer {
return &DeductionLayer{
voucherService: voucherService,
coinService: coinService,
}
}

func (l *DeductionLayer) Name() string {
return "deduction"
}

func (l *DeductionLayer) Order() int {
return 3
}

func (l *DeductionLayer) Process(ctx context.Context, req *PricingRequest, state *PricingState) error {
// 1. 处理优惠券
if req.VoucherCode != "" {
voucherDiscount, err := l.calculateVoucherDiscount(ctx, req, state)
if err != nil {
// 优惠券失败降级
return nil
}
state.VoucherDiscount = voucherDiscount
state.CurrentAmount -= voucherDiscount
}

// 2. 处理积分
if req.UseCoin && req.CoinAmount > 0 {
coinDeduction, err := l.calculateCoinDeduction(ctx, req, state)
if err != nil {
// 积分失败降级
return nil
}
state.CoinDeduction = coinDeduction
state.CurrentAmount -= coinDeduction
}

return nil
}

func (l *DeductionLayer) calculateVoucherDiscount(ctx context.Context, req *PricingRequest, state *PricingState) (int64, error) {
validateReq := &ValidateVoucherRequest{
VoucherCode: req.VoucherCode,
UserID: req.UserID,
Amount: state.CurrentAmount,
Items: req.Items,
}

resp, err := l.voucherService.ValidateVoucher(ctx, validateReq)
if err != nil {
return 0, fmt.Errorf("validate voucher failed: %w", err)
}

if !resp.Valid {
return 0, fmt.Errorf("voucher invalid: %s", resp.Reason)
}

return resp.DiscountAmount, nil
}

func (l *DeductionLayer) calculateCoinDeduction(ctx context.Context, req *PricingRequest, state *PricingState) (int64, error) {
calculateReq := &CalculateCoinRequest{
UserID: req.UserID,
CoinAmount: req.CoinAmount,
MaxAmount: state.CurrentAmount,
}

resp, err := l.coinService.CalculateDeduction(ctx, calculateReq)
if err != nil {
return 0, fmt.Errorf("calculate coin failed: %w", err)
}

return resp.DeductionAmount, nil
}

4.2.4 附加费用层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package pricing

import (
"context"
)

// ChargeLayer 附加费用层
type ChargeLayer struct {
paymentService PaymentService
}

func NewChargeLayer(paymentService PaymentService) Layer {
return &ChargeLayer{
paymentService: paymentService,
}
}

func (l *ChargeLayer) Name() string {
return "charge"
}

func (l *ChargeLayer) Order() int {
return 4
}

func (l *ChargeLayer) Process(ctx context.Context, req *PricingRequest, state *PricingState) error {
var totalCharge int64

// 1. 计算手续费
if req.ChannelID > 0 {
handlingFee, err := l.calculateHandlingFee(ctx, req, state)
if err != nil {
// 手续费计算失败降级
return nil
}
state.HandlingFee = handlingFee
totalCharge += handlingFee
}

// 2. 计算增值服务费
for _, service := range req.Services {
if service.Selected {
totalCharge += service.Price
}
}
state.AdditionalCharge = totalCharge - state.HandlingFee

// 更新当前金额
state.CurrentAmount += totalCharge

return nil
}

func (l *ChargeLayer) calculateHandlingFee(ctx context.Context, req *PricingRequest, state *PricingState) (int64, error) {
feeReq := &GetHandlingFeeRequest{
ChannelID: req.ChannelID,
Amount: state.CurrentAmount,
Region: req.Region,
}

resp, err := l.paymentService.GetHandlingFee(ctx, feeReq)
if err != nil {
return 0, err
}

return resp.Fee, nil
}

4.2.5 最终价格层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package pricing

import (
"context"
"fmt"
)

// FinalPriceLayer 最终价格层
type FinalPriceLayer struct{}

func NewFinalPriceLayer() Layer {
return &FinalPriceLayer{}
}

func (l *FinalPriceLayer) Name() string {
return "final_price"
}

func (l *FinalPriceLayer) Order() int {
return 5
}

func (l *FinalPriceLayer) Process(ctx context.Context, req *PricingRequest, state *PricingState) error {
// 计算最终价格
finalPrice := state.TotalAmount -
state.PromotionDiscount -
state.VoucherDiscount -
state.CoinDeduction +
state.HandlingFee +
state.AdditionalCharge

// 价格不能为负数
if finalPrice < 0 {
return fmt.Errorf("final price is negative: %d", finalPrice)
}

state.FinalPrice = finalPrice

// 生成价格公式
state.Formula = l.buildFormula(state)

return nil
}

func (l *FinalPriceLayer) buildFormula(state *PricingState) string {
formula := fmt.Sprintf("%d", state.TotalAmount)

if state.PromotionDiscount > 0 {
formula += fmt.Sprintf(" - %d(promotion)", state.PromotionDiscount)
}
if state.VoucherDiscount > 0 {
formula += fmt.Sprintf(" - %d(voucher)", state.VoucherDiscount)
}
if state.CoinDeduction > 0 {
formula += fmt.Sprintf(" - %d(coin)", state.CoinDeduction)
}
if state.HandlingFee > 0 {
formula += fmt.Sprintf(" + %d(fee)", state.HandlingFee)
}
if state.AdditionalCharge > 0 {
formula += fmt.Sprintf(" + %d(charge)", state.AdditionalCharge)
}

formula += fmt.Sprintf(" = %d", state.FinalPrice)

return formula
}

4.3 品类特殊计算器

4.3.1 Topup 充值计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package pricing

import (
"context"
"fmt"
)

// TopupCalculator 话费充值计算器
type TopupCalculator struct {
itemService ItemService
}

func NewTopupCalculator(itemService ItemService) Calculator {
return &TopupCalculator{
itemService: itemService,
}
}

func (c *TopupCalculator) Support(categoryID int64) bool {
// 支持 101xx 品类
return categoryID >= 10100 && categoryID < 10200
}

func (c *TopupCalculator) Priority() int {
return 100
}

func (c *TopupCalculator) Calculate(ctx context.Context, req *PricingRequest) (*PricingResponse, error) {
resp := &PricingResponse{
Items: make([]ItemPricing, len(req.Items)),
}

var totalAmount int64

for i, item := range req.Items {
// 获取面额信息
itemInfo, err := c.itemService.GetItem(ctx, item.ItemID)
if err != nil {
return nil, fmt.Errorf("get item failed: %w", err)
}

// Topup 使用面额定价
faceValue := itemInfo.FaceValue
discountRate := itemInfo.DiscountRate // 如 95% = 0.95

// 计算折扣价
discountPrice := int64(float64(faceValue) * discountRate)
subtotal := discountPrice * int64(item.Quantity)

resp.Items[i] = ItemPricing{
ItemID: item.ItemID,
Quantity: item.Quantity,
MarketPrice: faceValue,
DiscountPrice: discountPrice,
FinalPrice: discountPrice,
Subtotal: subtotal,
}

totalAmount += subtotal
}

resp.TotalAmount = totalAmount
resp.FinalPrice = totalAmount
resp.IsMigrated = true
resp.CalculatedAt = time.Now().Unix()

return resp, nil
}

4.3.2 E-Money 计算器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package pricing

import (
"context"
"fmt"
)

// EMoneyCalculator E-Money 计算器
type EMoneyCalculator struct {
itemService ItemService
}

func NewEMoneyCalculator(itemService ItemService) Calculator {
return &EMoneyCalculator{
itemService: itemService,
}
}

func (c *EMoneyCalculator) Support(categoryID int64) bool {
// 支持 202xx 品类
return categoryID >= 20200 && categoryID < 20300
}

func (c *EMoneyCalculator) Priority() int {
return 100
}

func (c *EMoneyCalculator) Calculate(ctx context.Context, req *PricingRequest) (*PricingResponse, error) {
resp := &PricingResponse{
Items: make([]ItemPricing, len(req.Items)),
}

var totalAmount int64
var totalAdminFee int64

for i, item := range req.Items {
itemInfo, err := c.itemService.GetItem(ctx, item.ItemID)
if err != nil {
return nil, fmt.Errorf("get item failed: %w", err)
}

// E-Money 基础价格
basePrice := itemInfo.DiscountPrice
subtotal := basePrice * int64(item.Quantity)

// 计算管理费 (Admin Fee)
adminFee := c.calculateAdminFee(itemInfo, int64(item.Quantity))

resp.Items[i] = ItemPricing{
ItemID: item.ItemID,
Quantity: item.Quantity,
MarketPrice: itemInfo.MarketPrice,
DiscountPrice: basePrice,
FinalPrice: basePrice,
Subtotal: subtotal,
}

totalAmount += subtotal
totalAdminFee += adminFee
}

resp.TotalAmount = totalAmount
resp.AdditionalCharge = totalAdminFee
resp.FinalPrice = totalAmount + totalAdminFee
resp.IsMigrated = true
resp.CalculatedAt = time.Now().Unix()

return resp, nil
}

func (c *EMoneyCalculator) calculateAdminFee(itemInfo *ItemInfo, quantity int64) int64 {
// 从配置中获取管理费规则
if itemInfo.AdminFeeConfig != nil {
if itemInfo.AdminFeeConfig.Type == "fixed" {
return itemInfo.AdminFeeConfig.Amount * quantity
} else if itemInfo.AdminFeeConfig.Type == "percentage" {
return int64(float64(itemInfo.DiscountPrice) * itemInfo.AdminFeeConfig.Rate) * quantity
}
}
return 0
}

4.4 灰度与监控

4.4.1 灰度管理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
package pricing

import (
"context"
"hash/crc32"
"sync"
)

// GrayManager 灰度管理器
type GrayManager struct {
config *GrayConfig
mu sync.RWMutex

// 配置更新通知
updates chan *GrayConfig
}

func NewGrayManager() *GrayManager {
gm := &GrayManager{
updates: make(chan *GrayConfig, 10),
}

// 启动配置监听
go gm.watchConfig()

return gm
}

// GetGrayRule 获取灰度规则
func (gm *GrayManager) GetGrayRule(req *PricingRequest) *GrayRule {
gm.mu.RLock()
defer gm.mu.RUnlock()

if gm.config == nil {
return nil
}

// 查找品类配置
categoryGray, ok := gm.config.Categories[req.CategoryID]
if !ok {
return gm.config.DefaultGray
}

// 根据平台选择
switch req.Platform {
case PlatformWap:
if categoryGray.Wap != nil {
return categoryGray.Wap
}
case PlatformPC:
if categoryGray.PC != nil {
return categoryGray.PC
}
default:
if categoryGray.App != nil {
return categoryGray.App
}
}

return gm.config.DefaultGray
}

// ShouldUseNew 判断是否使用新逻辑
func (gm *GrayManager) ShouldUseNew(req *PricingRequest) bool {
rule := gm.GetGrayRule(req)
if rule == nil || !rule.UseNew {
return false
}

// 白名单判断
if gm.inWhitelist(req, rule) {
return true
}

// 灰度比例判断
return gm.hitRatio(req.UserID, rule.GrayRatio)
}

// ShouldDryRun 判断是否空跑
func (gm *GrayManager) ShouldDryRun(req *PricingRequest) bool {
rule := gm.GetGrayRule(req)
if rule == nil || !rule.DryRun {
return false
}

return gm.hitRatio(req.UserID, rule.DryRunRatio)
}

// inWhitelist 判断是否在白名单
func (gm *GrayManager) inWhitelist(req *PricingRequest, rule *GrayRule) bool {
// 用户白名单
for _, uid := range rule.WhitelistUsers {
if uid == req.UserID {
return true
}
}

// 地区白名单
for _, region := range rule.WhitelistRegions {
if region == req.Region {
return true
}
}

return false
}

// hitRatio 判断是否命中灰度比例
func (gm *GrayManager) hitRatio(userID int64, ratio int32) bool {
if ratio >= 10000 {
return true
}
if ratio <= 0 {
return false
}

// 使用 userID 计算哈希,保证同一用户稳定
hash := crc32.ChecksumIEEE([]byte(fmt.Sprintf("%d", userID)))
return int32(hash%10000) < ratio
}

// UpdateConfig 更新配置
func (gm *GrayManager) UpdateConfig(config *GrayConfig) {
gm.mu.Lock()
defer gm.mu.Unlock()

gm.config = config
}

// watchConfig 监听配置变更
func (gm *GrayManager) watchConfig() {
for config := range gm.updates {
gm.UpdateConfig(config)
}
}

4.4.2 差异上报

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
package pricing

import (
"context"
"encoding/json"
)

// DiffReporter 差异上报器
type DiffReporter struct {
// MongoDB 存储完整差异
mongo MongoClient

// Kafka 实时告警
kafka KafkaProducer

// 监控指标
metrics Metrics
}

func NewDiffReporter(mongo MongoClient, kafka KafkaProducer, metrics Metrics) *DiffReporter {
return &DiffReporter{
mongo: mongo,
kafka: kafka,
metrics: metrics,
}
}

// Report 上报差异
func (r *DiffReporter) Report(ctx context.Context, result *DryRunResult) error {
// 1. 记录监控指标
r.recordMetrics(result)

// 2. 存储到 MongoDB
if err := r.saveToDB(ctx, result); err != nil {
return fmt.Errorf("save to db failed: %w", err)
}

// 3. 发送告警(如果差异超过阈值)
if r.shouldAlert(result) {
if err := r.sendAlert(ctx, result); err != nil {
// 告警失败不影响主流程
return nil
}
}

return nil
}

// recordMetrics 记录监控指标
func (r *DiffReporter) recordMetrics(result *DryRunResult) {
tags := map[string]string{
"scene": string(result.Scene),
"category": fmt.Sprintf("%d", result.CategoryID),
"region": result.Region,
}

if result.HasDiff {
r.metrics.IncWithTags("dry_run_diff", tags)

// 记录差异金额
if oldPrice := result.OldPrice; oldPrice != nil {
diffAmount := result.NewPrice.FinalPrice - oldPrice.FinalPrice
r.metrics.GaugeWithTags("dry_run_diff_amount", float64(diffAmount), tags)
}
} else {
r.metrics.IncWithTags("dry_run_match", tags)
}
}

// saveToDB 保存到数据库
func (r *DiffReporter) saveToDB(ctx context.Context, result *DryRunResult) error {
doc, err := json.Marshal(result)
if err != nil {
return fmt.Errorf("marshal failed: %w", err)
}

return r.mongo.Insert(ctx, "pricing_dry_run", doc)
}

// shouldAlert 判断是否需要告警
func (r *DiffReporter) shouldAlert(result *DryRunResult) bool {
if !result.HasDiff {
return false
}

if result.OldPrice == nil {
return false
}

// 差异金额超过 10 元告警
diffAmount := abs(result.NewPrice.FinalPrice - result.OldPrice.FinalPrice)
if diffAmount > 1000 { // 10元 = 1000分
return true
}

// 差异比例超过 5% 告警
diffPercent := float64(diffAmount) / float64(result.OldPrice.FinalPrice) * 100
if diffPercent > 5.0 {
return true
}

return false
}

// sendAlert 发送告警
func (r *DiffReporter) sendAlert(ctx context.Context, result *DryRunResult) error {
alert := map[string]interface{}{
"type": "pricing_dry_run_diff",
"severity": "warning",
"request_id": result.RequestID,
"category": result.CategoryID,
"region": result.Region,
"diff_fields": result.DiffFields,
"new_price": result.NewPrice.FinalPrice,
"old_price": result.OldPrice.FinalPrice,
"timestamp": result.Timestamp,
}

msg, _ := json.Marshal(alert)
return r.kafka.Produce("pricing-alert", msg)
}

4.4 供应商价格服务(Supplier Price Service)

供应商价格服务负责与外部供应商系统对接,查询实时价格、验证库存、获取预订令牌(BookingToken),是供应商品类计价的核心依赖。

4.4.1 服务架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package supplier

import (
"context"
"fmt"
"sync"
"time"
)

// SupplierPriceService 供应商价格服务
type SupplierPriceService struct {
// 供应商客户端(多个供应商)
suppliers map[int64]SupplierClient // key: supplierID

// 缓存(供应商价格缓存时间较短,1-5分钟)
cache *redis.Client
cacheTTL time.Duration // 默认 5分钟

// 降级配置
fallbackDB *sql.DB // 降级数据库(缓存历史价格)
fallbackTTL time.Duration // 降级价格最大允许时间(1小时)

// 超时配置
timeout time.Duration // 默认 3秒
retryTimes int // 重试次数,默认 2
}

// SupplierClient 供应商客户端接口
type SupplierClient interface {
// CheckAvailability 查询供应商价格和库存
CheckAvailability(ctx context.Context, req *SupplierPriceRequest) (*SupplierPriceResponse, error)

// ConfirmBooking 确认预订(Payment时调用)
ConfirmBooking(ctx context.Context, bookingToken string) (*BookingConfirmation, error)

// CancelBooking 取消预订(订单取消/超时时调用)
CancelBooking(ctx context.Context, bookingToken string) error
}

4.4.2 核心方法实现

1. 查询供应商价格并预订
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// QueryAndBook 查询供应商价格并获取BookingToken
func (s *SupplierPriceService) QueryAndBook(ctx context.Context, req *SupplierPriceRequest) (*SupplierBooking, error) {
// 1. 检查缓存(供应商价格缓存时间短,1-5分钟)
cacheKey := s.buildCacheKey(req)
if cached, err := s.getFromCache(ctx, cacheKey); err == nil && cached != nil {
// 验证缓存是否过期(供应商品类缓存较短)
if time.Since(cached.CachedAt) < s.cacheTTL {
return cached, nil
}
}

// 2. 获取供应商客户端
client, ok := s.suppliers[req.SupplierID]
if !ok {
return nil, fmt.Errorf("supplier %d not found", req.SupplierID)
}

// 3. 调用供应商API(带超时和重试)
var (
resp *SupplierPriceResponse
err error
)

for i := 0; i <= s.retryTimes; i++ {
timeoutCtx, cancel := context.WithTimeout(ctx, s.timeout)
defer cancel()

resp, err = client.CheckAvailability(timeoutCtx, req)
if err == nil {
break
}

// 如果是超时错误,重试
if errors.Is(err, context.DeadlineExceeded) && i < s.retryTimes {
time.Sleep(time.Duration(i+1) * 100 * time.Millisecond) // 指数退避
continue
}
}

// 4. 供应商查询失败,降级处理
if err != nil {
log.Warnf("supplier %d query failed: %v, fallback to DB", req.SupplierID, err)
return s.fallbackToDBPrice(ctx, req)
}

// 5. 构建预订信息
booking := &SupplierBooking{
ItemID: req.ItemID,
SupplierID: req.SupplierID,
BookingToken: resp.BookingToken,
SupplierPrice: resp.Items[0].SupplierPrice,
SupplierCost: resp.Items[0].SupplierCost,
Available: resp.Available,
Stock: resp.Items[0].Stock,
ExpireAt: resp.ExpireAt,
CheckInDate: req.CheckInDate,
CheckOutDate: req.CheckOutDate,
SupplierSKU: resp.Items[0].SupplierSKU,
SupplierRemark: resp.Items[0].SupplierRemark,
}

// 6. 缓存供应商价格(短时间缓存,1-5分钟)
s.setToCache(ctx, cacheKey, booking, s.cacheTTL)

return booking, nil
}

// fallbackToDBPrice 降级:使用数据库缓存价格
func (s *SupplierPriceService) fallbackToDBPrice(ctx context.Context, req *SupplierPriceRequest) (*SupplierBooking, error) {
query := `
SELECT supplier_price, supplier_cost, updated_at
FROM supplier_price_cache_tab
WHERE supplier_id = ? AND item_id = ?
ORDER BY updated_at DESC LIMIT 1
`

var (
supplierPrice int64
supplierCost int64
updatedAt time.Time
)

err := s.fallbackDB.QueryRowContext(ctx, query, req.SupplierID, req.ItemID).Scan(
&supplierPrice, &supplierCost, &updatedAt,
)

if err != nil {
return nil, fmt.Errorf("fallback DB query failed: %w", err)
}

// 检查缓存价格是否过期(超过1小时)
if time.Since(updatedAt) > s.fallbackTTL {
return nil, fmt.Errorf("fallback price too old: %v", updatedAt)
}

// 构建降级预订信息(注意:BookingToken为空,需要在Payment时重新查询)
return &SupplierBooking{
ItemID: req.ItemID,
SupplierID: req.SupplierID,
SupplierPrice: supplierPrice,
SupplierCost: supplierCost,
Available: true, // 假设可用
BookingToken: "", // 降级时无BookingToken
}, nil
}
2. 批量查询供应商价格
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// BatchQuery 批量查询供应商价格(购物车场景)
func (s *SupplierPriceService) BatchQuery(ctx context.Context, items []ItemInfo) (map[int64]*SupplierItemPrice, error) {
var (
wg sync.WaitGroup
mu sync.Mutex
result = make(map[int64]*SupplierItemPrice)
errs []error
)

for _, item := range items {
wg.Add(1)
go func(itm ItemInfo) {
defer wg.Done()

// 查询供应商价格
req := &SupplierPriceRequest{
ItemID: itm.ItemID,
SupplierID: itm.SupplierID,
CheckInDate: itm.CheckInDate,
CheckOutDate: itm.CheckOutDate,
Quantity: itm.Quantity,
}

booking, err := s.QueryAndBook(ctx, req)

mu.Lock()
if err != nil {
errs = append(errs, err)
} else {
result[itm.ItemID] = &SupplierItemPrice{
ItemID: itm.ItemID,
SupplierPrice: booking.SupplierPrice,
Available: booking.Available,
Stock: booking.Stock,
}
}
mu.Unlock()
}(item)
}

wg.Wait()

if len(errs) > 0 {
return result, fmt.Errorf("batch query failed: %v", errs[0])
}

return result, nil
}
3. 确认供应商预订(Payment场景)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ConfirmBooking 确认供应商预订(Payment时调用)
func (s *SupplierPriceService) ConfirmBooking(ctx context.Context, supplierID int64, bookingToken string) (*BookingConfirmation, error) {
// 1. 获取供应商客户端
client, ok := s.suppliers[supplierID]
if !ok {
return nil, fmt.Errorf("supplier %d not found", supplierID)
}

// 2. 调用供应商API确认预订
timeoutCtx, cancel := context.WithTimeout(ctx, s.timeout)
defer cancel()

confirmation, err := client.ConfirmBooking(timeoutCtx, bookingToken)
if err != nil {
return nil, fmt.Errorf("confirm booking failed: %w", err)
}

// 3. 返回确认信息(含供应商订单号)
return confirmation, nil
}

// BookingConfirmation 供应商预订确认
type BookingConfirmation struct {
SupplierOrderID string `json:"supplier_order_id"` // 供应商订单号
ConfirmedAt time.Time `json:"confirmed_at"` // 确认时间
SupplierStatus string `json:"supplier_status"` // 供应商订单状态
}
4. 取消供应商预订
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// CancelBooking 取消供应商预订(订单取消/超时时调用)
func (s *SupplierPriceService) CancelBooking(ctx context.Context, supplierID int64, bookingToken string) error {
// 1. 获取供应商客户端
client, ok := s.suppliers[supplierID]
if !ok {
return fmt.Errorf("supplier %d not found", supplierID)
}

// 2. 调用供应商API取消预订
timeoutCtx, cancel := context.WithTimeout(ctx, s.timeout)
defer cancel()

if err := client.CancelBooking(timeoutCtx, bookingToken); err != nil {
log.Warnf("cancel booking failed: %v", err)
// 取消失败不阻塞流程,记录日志即可
return nil
}

return nil
}

4.4.3 供应商客户端示例(Agoda)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
package supplier

import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"
)

// AgodaClient Agoda供应商客户端
type AgodaClient struct {
apiEndpoint string
apiKey string
httpClient *http.Client
}

func NewAgodaClient(endpoint, apiKey string) SupplierClient {
return &AgodaClient{
apiEndpoint: endpoint,
apiKey: apiKey,
httpClient: &http.Client{
Timeout: 5 * time.Second,
},
}
}

// CheckAvailability 查询Agoda价格和库存
func (c *AgodaClient) CheckAvailability(ctx context.Context, req *SupplierPriceRequest) (*SupplierPriceResponse, error) {
// 构建Agoda API请求
agodaReq := map[string]interface{}{
"hotel_id": req.ItemID,
"check_in": req.CheckInDate,
"check_out": req.CheckOutDate,
"room_count": req.RoomCount,
}

reqBody, _ := json.Marshal(agodaReq)

// 发送HTTP请求
httpReq, _ := http.NewRequestWithContext(ctx, "POST", c.apiEndpoint+"/availability", bytes.NewReader(reqBody))
httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
httpReq.Header.Set("Content-Type", "application/json")

resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("agoda API call failed: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("agoda API error: status=%d", resp.StatusCode)
}

// 解析响应
var agodaResp struct {
Available bool `json:"available"`
Price int64 `json:"price"`
BookingToken string `json:"booking_token"`
ExpiresAt int64 `json:"expires_at"`
RoomType string `json:"room_type"`
}

if err := json.NewDecoder(resp.Body).Decode(&agodaResp); err != nil {
return nil, fmt.Errorf("decode agoda response failed: %w", err)
}

// 转换为统一格式
return &SupplierPriceResponse{
Items: []SupplierItemPrice{
{
ItemID: req.ItemID,
SupplierPrice: agodaResp.Price,
Available: agodaResp.Available,
SupplierSKU: agodaResp.RoomType,
},
},
Available: agodaResp.Available,
BookingToken: agodaResp.BookingToken,
ExpireAt: agodaResp.ExpiresAt,
}, nil
}

// ConfirmBooking 确认Agoda预订
func (c *AgodaClient) ConfirmBooking(ctx context.Context, bookingToken string) (*BookingConfirmation, error) {
// 构建Agoda确认请求
reqBody, _ := json.Marshal(map[string]string{
"booking_token": bookingToken,
})

httpReq, _ := http.NewRequestWithContext(ctx, "POST", c.apiEndpoint+"/confirm", bytes.NewReader(reqBody))
httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
httpReq.Header.Set("Content-Type", "application/json")

resp, err := c.httpClient.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("agoda confirm failed: %w", err)
}
defer resp.Body.Close()

var agodaResp struct {
BookingID string `json:"booking_id"`
Status string `json:"status"`
}

if err := json.NewDecoder(resp.Body).Decode(&agodaResp); err != nil {
return nil, err
}

return &BookingConfirmation{
SupplierOrderID: agodaResp.BookingID,
ConfirmedAt: time.Now(),
SupplierStatus: agodaResp.Status,
}, nil
}

// CancelBooking 取消Agoda预订
func (c *AgodaClient) CancelBooking(ctx context.Context, bookingToken string) error {
// 实现取消逻辑
reqBody, _ := json.Marshal(map[string]string{
"booking_token": bookingToken,
})

httpReq, _ := http.NewRequestWithContext(ctx, "POST", c.apiEndpoint+"/cancel", bytes.NewReader(reqBody))
httpReq.Header.Set("Authorization", "Bearer "+c.apiKey)
httpReq.Header.Set("Content-Type", "application/json")

_, err := c.httpClient.Do(httpReq)
return err
}

4.4.4 供应商价格缓存表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE supplier_price_cache_tab (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
supplier_id BIGINT NOT NULL COMMENT '供应商ID',
item_id BIGINT NOT NULL COMMENT '商品ID',
supplier_price BIGINT NOT NULL COMMENT '供应商报价(分)',
supplier_cost BIGINT NOT NULL COMMENT '供应商成本(分)',
available TINYINT(1) DEFAULT 1 COMMENT '是否可用',
stock INT DEFAULT 0 COMMENT '库存数量',

-- 时间字段
cached_at DATETIME NOT NULL COMMENT '缓存时间',
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,

INDEX idx_supplier_item (supplier_id, item_id),
INDEX idx_updated_at (updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商价格缓存表(降级使用)';

4.4.5 供应商对接配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# etc/supplier_pricing.yml
supplier_pricing:
enabled: true

# 超时和重试配置
timeout: 3000ms # 查询超时时间
retry_times: 2 # 重试次数

# 缓存配置
cache_ttl: 300s # Redis缓存时间(5分钟)

# 降级配置
fallback:
enabled: true
max_price_age: 3600s # 降级价格最大允许时间(1小时)

# 供应商列表
suppliers:
- supplier_id: 1001
supplier_name: "Agoda"
category_ids: [100, 101] # Hotel品类
api_endpoint: "https://api.agoda.com/v1"
api_key: "your-agoda-api-key"
timeout: 3000ms

- supplier_id: 2001
supplier_name: "Expedia"
category_ids: [100]
api_endpoint: "https://api.expedia.com/v2"
api_key: "your-expedia-api-key"
timeout: 3000ms

- supplier_id: 3001
supplier_name: "Klook"
category_ids: [200, 201] # Tour品类
api_endpoint: "https://api.klook.com/v1"
api_key: "your-klook-api-key"
timeout: 3000ms

关键特性

  • 多供应商对接:支持Agoda、Expedia、Klook等多个供应商
  • 超时和重试:3秒超时,自动重试2次,指数退避
  • 降级处理:供应商查询失败时使用DB缓存价格(最大允许1小时旧数据)
  • 短时缓存:供应商价格缓存5分钟(比自营品类短)
  • 并发查询:支持批量并发查询多个供应商
  • BookingToken管理:5-15分钟有效期,Payment时确认预订

五、DDD 领域模型设计

本章内容已独立为专题文章,详见:领域驱动设计在电商计价系统中的实践

该文章涵盖:战略设计(统一语言、概念模型、子域划分、上下文映射)、战术设计(实体与值对象、聚合根、领域服务)、六边形架构实践、价格快照与一致性保障等内容。


六、业界价格模型演进与对比

6.1 价格模型演进史

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
┌─────────────────────────────────────────────────────────────────┐
│ 价格引擎技术演进路径 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Phase 1: 单一价格时代(2000年前) │
│ ┌─────────────────────────────┐ │
│ │ 商品一口价,总价 = 单价 × 数量 │ │
│ │ 代表: 早期零售 ERP │ │
│ └─────────────────────────────┘ │
│ ↓ │
│ Phase 2: 促销价格分离(2000-2010) │
│ ┌─────────────────────────────┐ │
│ │ original_price + sale_price │ │
│ │ 简单时间段促销 │ │
│ │ 代表: 早期淘宝、eBay │ │
│ └─────────────────────────────┘ │
│ ↓ │
│ Phase 3: 规则引擎化(2010-2015) │
│ ┌─────────────────────────────┐ │
│ │ 促销规则独立成表 │ │
│ │ 优先级 & 互斥机制 │ │
│ │ 独立优惠券系统 │ │
│ │ 代表: 淘宝天猫、京东 │ │
│ └─────────────────────────────┘ │
│ ↓ │
│ Phase 4: 智能定价(2015-2020) │
│ ┌─────────────────────────────┐ │
│ │ 动态定价(供需、库存、竞品)│ │
│ │ 个性化定价(用户画像) │ │
│ │ 价格快照 & 审计 │ │
│ │ 代表: Uber、Airbnb、Amazon │ │
│ └─────────────────────────────┘ │
│ ↓ │
│ Phase 5: AI 赋能(2020至今) │
│ ┌─────────────────────────────┐ │
│ │ ML 定价模型(需求预测) │ │
│ │ 实时竞品监控 │ │
│ │ A/B 测试自动化 │ │
│ │ 代表: Amazon ML、阿里智能 │ │
│ └─────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

6.2 各阶段对比

维度 Phase 1 Phase 2 Phase 3 Phase 4 Phase 5
计算方式 硬编码 配置化 规则引擎 ML 模型 在线学习
响应时间 <10ms <50ms <100ms <200ms <100ms
扩展性 优秀
维护成本
智能程度 极高
代表企业 早期电商 淘宝/京东 天猫/Amazon Uber/Airbnb Amazon/阿里

6.3 主流电商平台对比

淘宝/天猫

1
2
3
4
价格计算链路: 商品价格 → 营销活动 → 优惠券 → 积分 → 红包
技术栈: Drools 规则引擎 + Tair 分布式缓存 + HSF/Dubbo RPC
性能: P99 < 50ms
特色: 双11 大促复杂叠加规则(店铺券 + 品类券 + 跨店券 + 红包 + 津贴)

京东

1
2
3
4
价格计算链路: 基础价 → 会员价(Plus) → 活动 → 优惠券 → 京豆 → 运费
技术栈: 自研规则引擎 + Redis 集群
性能: P99 < 80ms
特色: Plus 会员价体系 + 京豆积分深度融合

Amazon

1
2
3
4
价格计算链路: 基础价 → ML 动态定价 → Prime 折扣 → Lightning Deal → Subscribe & Save
技术栈: 自研 ML Pipeline + ElastiCache
性能: P99 < 100ms
特色: ML 驱动定价(需求弹性 × 竞品价格 × 库存水平)

Uber(动态定价)

1
2
3
定价逻辑: base_price = distance × rate + duration × rate → surge_multiplier
技术栈: 实时供需计算 + ML 预测 + 平滑处理
特色: 供需比驱动 Surge Pricing,平滑处理避免价格突变

6.4 我们的定位与对比

特性 我们的设计 淘宝天猫 京东 Amazon
分层架构 4层(Base/Promo/Fee/Voucher) 5层(含积分/红包) 5层(含京豆) 3层(简化)
规则引擎 DB 配置 + JSON Drools + 自研 自研 自研
动态定价 规则驱动 ML 驱动 规则驱动 ML 驱动
价格快照 ✅ 完整支持 ✅ 支持 ✅ 支持 ✅ 支持
费用拆分 ✅ 详细 ✅ 详细 ✅ 详细 ✅ 详细
降级策略 ✅ 5级降级 ✅ 完善 ✅ 完善 ✅ 完善
ML 定价 ❌ 待规划 ✅ 深度应用 ⚠️ 部分 ✅ 核心能力

七、性能优化

不同场景对性能的要求不同,需要采取差异化的优化策略。

7.1 场景驱动的性能目标

场景 P99目标 关键优化策略 缓存策略 并发策略
PDP < 100ms • 高缓存命中(>90%)
• 轻量计算(Layer 1-2)
• 预热热门商品
L1(5min) + L2(30min) 单次计算
AddToCart < 150ms • 中等缓存命中(>70%)
• 跳过费用计算
• 凑单提示异步
L1(3min) + L2(15min) 单次计算
Cart < 200ms • 批量计算优化
• 并发查询依赖服务
供应商品类实时查询
• 跳过费用计算
自营:L1(2min)+L2(10min)
供应商:L1(1min)+L2(5min)
批量+并发
供应商查询
CreateOrder
自营品类
< 300ms • 库存预检
• 库存锁定(30分钟)
• 订单快照生成
• 计算订单价格(基础+营销+附加费)
❌ 不缓存 库存锁定
CreateOrder
供应商品类
< 1000ms 供应商价格查询和预订
• 获取BookingToken(5-15分钟)
• 基于供应商报价计算订单价格
• 超时降级(使用DB缓存)
• 失败回滚
❌ 不缓存 供应商并发查询
库存锁定
Checkout < 200ms • 基于订单快照
• 完整计算(含券/积分/手续费)
• 软锁定券/积分
• 支付快照生成
❌ 不缓存 并发查询
Payment
自营品类
< 500ms • 快照验证
• 零计算(使用快照)
• 正式锁定券/积分
❌ 不缓存 快照查询
Payment
供应商品类
< 1000ms • 快照验证
供应商确认预订(使用BookingToken)
• 零计算(使用快照)
• 正式锁定券/积分
❌ 不缓存 快照查询
供应商确认
BatchQuery < 200ms • 批量查询
• 并发计算
• 批量缓存
L1(5min) + L2(30min) 批量+并发

优化原则

  • 场景优先级:PDP > Cart > CreateOrder > Checkout > Payment(按业务流程顺序)
  • 计算复杂度:PDP最轻(基础价+营销价) → CreateOrder中等(基础价+营销价+附加费,供应商品类需外部查询) → Checkout最重(完整计算含券/积分)
  • 缓存策略:前端展示场景高缓存 → 供应商品类低缓存(1-5分钟) → 订单创建和收银台场景零缓存(实时计算)
  • 并发优化:批量场景并发计算 → Checkout场景并发查询 → CreateOrder场景库存锁定 → 供应商品类并发查询外部API
  • 品类差异化
    • 自营品类:本地计算,高性能(P99 < 300ms)
    • 供应商品类:外部查询,性能目标放宽(P99 < 1000ms),含超时降级和失败回滚

7.2 缓存策略(已在4.1.3详细说明)

参见第4.1.3节”场景驱动的缓存策略”,这里补充缓存管理器实现。

7.2.1 L1 本地缓存 + L2 Redis 缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package pricing

import (
"context"
"fmt"
"time"

"github.com/patrickmn/go-cache"
)

// CacheManager 缓存管理器(实现)
type CacheManager struct {
// L1 本地缓存
l1 *cache.Cache

// L2 Redis 缓存
l2 RedisClient

// 配置
l1TTL time.Duration
l2TTL time.Duration
}

func NewCacheManager(redis RedisClient) *CacheManager {
return &CacheManager{
l1: cache.New(5*time.Minute, 10*time.Minute),
l2: redis,
l1TTL: 5 * time.Minute,
l2TTL: 30 * time.Minute,
}
}

// Get 获取缓存
func (cm *CacheManager) Get(ctx context.Context, key string) (*PricingResponse, bool) {
// 1. 查询 L1
if val, found := cm.l1.Get(key); found {
return val.(*PricingResponse), true
}

// 2. 查询 L2
val, err := cm.l2.Get(ctx, key)
if err == nil && val != "" {
var resp PricingResponse
if err := json.Unmarshal([]byte(val), &resp); err == nil {
// 回写 L1
cm.l1.Set(key, &resp, cm.l1TTL)
return &resp, true
}
}

return nil, false
}

// Set 设置缓存
func (cm *CacheManager) Set(ctx context.Context, key string, resp *PricingResponse) error {
// 1. 写入 L1
cm.l1.Set(key, resp, cm.l1TTL)

// 2. 写入 L2
data, err := json.Marshal(resp)
if err != nil {
return err
}

return cm.l2.SetEx(ctx, key, string(data), cm.l2TTL)
}

// BuildKey 构建缓存Key
func (cm *CacheManager) BuildKey(req *PricingRequest) string {
// 根据请求构建唯一Key
return fmt.Sprintf("pricing:%s:%d:%d:%s:%t:%d",
req.Scene,
req.CategoryID,
req.UserID,
req.Region,
req.IsNewUser,
hashItems(req.Items),
)
}

func hashItems(items []ItemInfo) uint64 {
var hash uint64
for _, item := range items {
hash ^= uint64(item.ItemID) ^ uint64(item.Quantity)
}
return hash
}

7.3 批量计算优化(批量查询场景)

针对列表页、推荐页等批量查询场景,采用并发计算优化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package pricing

import (
"context"
"sync"
)

// BatchCalculate 批量计算价格
func (e *engine) BatchCalculate(ctx context.Context, reqs []*PricingRequest) ([]*PricingResponse, error) {
if len(reqs) == 0 {
return nil, nil
}

// 并发计算
var (
wg sync.WaitGroup
mu sync.Mutex
results = make([]*PricingResponse, len(reqs))
errs = make([]error, len(reqs))
)

// 限制并发数
semaphore := make(chan struct{}, 10)

for i, req := range reqs {
wg.Add(1)
go func(index int, request *PricingRequest) {
defer wg.Done()

semaphore <- struct{}{}
defer func() { <-semaphore }()

resp, err := e.CalculatePrice(ctx, request)

mu.Lock()
results[index] = resp
errs[index] = err
mu.Unlock()
}(i, req)
}

wg.Wait()

// 检查错误
for _, err := range errs {
if err != nil {
return results, err
}
}

return results, nil
}

7.4 并发优化(Checkout/Order场景)

针对需要查询多个依赖服务的场景(Checkout、Order),采用并发调用优化。

7.4.1 依赖服务并发调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package pricing

import (
"context"
"sync"
)

// fetchDataConcurrently 并发获取依赖数据
func (e *engine) fetchDataConcurrently(ctx context.Context, req *PricingRequest) (*fetchResult, error) {
var (
wg sync.WaitGroup
result fetchResult
errs []error
mu sync.Mutex
)

// 1. 获取商品信息
wg.Add(1)
go func() {
defer wg.Done()
items, err := e.itemService.BatchGetItems(ctx, getItemIDs(req.Items))
mu.Lock()
result.items = items
if err != nil {
errs = append(errs, err)
}
mu.Unlock()
}()

// 2. 获取营销活动
wg.Add(1)
go func() {
defer wg.Done()
promo, err := e.promoService.GetPromotion(ctx, buildPromoReq(req))
mu.Lock()
result.promo = promo
if err != nil {
errs = append(errs, err)
}
mu.Unlock()
}()

// 3. 获取优惠券信息
if req.VoucherCode != "" {
wg.Add(1)
go func() {
defer wg.Done()
voucher, err := e.voucherService.ValidateVoucher(ctx, buildVoucherReq(req))
mu.Lock()
result.voucher = voucher
if err != nil {
// 优惠券失败降级
errs = append(errs, err)
}
mu.Unlock()
}()
}

wg.Wait()

// 关键服务失败则返回错误
if len(errs) > 0 && result.items == nil {
return nil, errs[0]
}

return &result, nil
}

type fetchResult struct {
items map[int64]*ItemInfo
promo *PromotionResponse
voucher *VoucherValidateResponse
}

八、部署与运维

8.1 部署架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────────┐
│ Load Balancer │
└─────────────────────────────────────────────────────────────┘

┌────────────────────┼────────────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│ Pricing │ │ Pricing │ │ Pricing │
│ Service │ │ Service │ │ Service │
│ Pod 1 │ │ Pod 2 │ │ Pod N │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└────────────────────┼────────────────────┘

┌─────────────────────────┼─────────────────────────┐
│ │ │
┌───▼────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ Redis │ │ MySQL │ │ MongoDB │
│Cluster │ │ (Master) │ │ (Replica) │
│ │ │ │ │ │
│ L1/L2 │ │ ┌──────┐ │ │ Dry Run │
│ Cache │ │ │Slave │ │ │ Diff Log │
└────────┘ │ └──────┘ │ └─────────────┘
└─────────────┘

8.2 监控指标

8.2.1 核心指标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package pricing

// Metrics 监控指标
type Metrics interface {
// 计数器
Inc(name string)
IncWithTags(name string, tags map[string]string)

// 耗时
RecordLatency(name string, duration time.Duration)

// 值
Gauge(name string, value float64)
GaugeWithTags(name string, value float64, tags map[string]string)
}

// 关键指标
const (
// QPS
MetricRequestTotal = "pricing_request_total"
MetricRequestSuccess = "pricing_request_success"
MetricRequestError = "pricing_request_error"

// 延迟
MetricLatency = "pricing_latency"
MetricLatencyP99 = "pricing_latency_p99"

// 缓存
MetricCacheHit = "pricing_cache_hit"
MetricCacheMiss = "pricing_cache_miss"

// 空跑
MetricDryRunDiff = "pricing_dry_run_diff"
MetricDryRunMatch = "pricing_dry_run_match"

// 灰度
MetricGrayNewLogic = "pricing_gray_new_logic"
MetricGrayOldLogic = "pricing_gray_old_logic"
)

8.2.2 Grafana Dashboard 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Grafana Dashboard 配置示例
dashboard:
title: "Pricing Center Monitor"
panels:
# QPS
- title: "Request QPS"
targets:
- expr: "rate(pricing_request_total[1m])"

# P99 延迟
- title: "P99 Latency"
targets:
- expr: "histogram_quantile(0.99, pricing_latency)"

# 错误率
- title: "Error Rate"
targets:
- expr: "rate(pricing_request_error[1m]) / rate(pricing_request_total[1m])"

# 缓存命中率
- title: "Cache Hit Rate"
targets:
- expr: "rate(pricing_cache_hit[1m]) / (rate(pricing_cache_hit[1m]) + rate(pricing_cache_miss[1m]))"

# 空跑差异率
- title: "Dry Run Diff Rate"
targets:
- expr: "rate(pricing_dry_run_diff[1m]) / (rate(pricing_dry_run_diff[1m]) + rate(pricing_dry_run_match[1m]))"

# 灰度流量分布
- title: "Gray Traffic Distribution"
targets:
- expr: "rate(pricing_gray_new_logic[1m])"
- expr: "rate(pricing_gray_old_logic[1m])"

8.3 告警配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# Prometheus Alert Rules
groups:
- name: pricing_alerts
rules:
# 错误率告警
- alert: PricingHighErrorRate
expr: |
rate(pricing_request_error[5m]) / rate(pricing_request_total[5m]) > 0.01
for: 2m
labels:
severity: critical
annotations:
summary: "Pricing service error rate too high"
description: "Error rate is {{ $value | humanizePercentage }}"

# P99 延迟告警
- alert: PricingHighLatency
expr: |
histogram_quantile(0.99, pricing_latency) > 200
for: 5m
labels:
severity: warning
annotations:
summary: "Pricing service P99 latency too high"
description: "P99 latency is {{ $value }}ms"

# 空跑差异告警
- alert: PricingDryRunDiff
expr: |
rate(pricing_dry_run_diff[10m]) > 10
for: 5m
labels:
severity: warning
annotations:
summary: "Pricing dry run has too many diffs"
description: "Diff count is {{ $value }} per second"

# 缓存命中率告警
- alert: PricingLowCacheHitRate
expr: |
rate(pricing_cache_hit[5m]) / (rate(pricing_cache_hit[5m]) + rate(pricing_cache_miss[5m])) < 0.8
for: 10m
labels:
severity: warning
annotations:
summary: "Pricing cache hit rate too low"
description: "Cache hit rate is {{ $value | humanizePercentage }}"

九、最佳实践

9.1 迁移策略

9.1.1 三阶段迁移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
阶段一:空跑期(2-4周)
├── 目标:验证新逻辑正确性
├── 策略:
│ ├── 前端/后端同时调用新老接口
│ ├── 使用老逻辑返回结果
│ ├── 新逻辑结果仅用于比对
│ └── 差异自动上报监控
└── 验收标准:
├── 差异率 < 0.1%
├── 关键品类差异率 = 0
└── P99 延迟 < 200ms

阶段二:灰度期(2-4周)
├── 目标:逐步切流到新逻辑
├── 策略:
│ ├── Week 1: 1% 流量
│ ├── Week 2: 10% 流量
│ ├── Week 3: 50% 流量
│ └── Week 4: 100% 流量
└── 验收标准:
├── 无资损事故
├── 错误率 < 0.01%
└── P99 延迟无明显上升

阶段三:清理期(1-2周)
├── 目标:下线老逻辑
├── 策略:
│ ├── 观察稳定运行 1 周
│ ├── 下线老逻辑代码
│ └── 清理冗余配置
└── 验收标准:
└── 代码完全切换

9.1.2 灰度流程

graph TB
    A[开始迁移] --> B{选择品类}
    B --> C[开启空跑]
    C --> D{空跑比对}
    D -->|有差异| E[修复问题]
    E --> C
    D -->|无差异| F[灰度 1%]
    F --> G{观察指标}
    G -->|异常| H[回滚]
    H --> E
    G -->|正常| I[灰度 10%]
    I --> J{观察指标}
    J -->|异常| H
    J -->|正常| K[灰度 50%]
    K --> L{观察指标}
    L -->|异常| H
    L -->|正常| M[灰度 100%]
    M --> N[观察 1 周]
    N --> O[下线老逻辑]
    O --> P[完成迁移]

9.2 资损防控

9.2.1 多重保护机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
package pricing

// SafetyChecker 安全检查器
type SafetyChecker struct {
// 价格阈值配置
thresholds map[int64]*PriceThreshold
}

type PriceThreshold struct {
MinPrice int64 // 最低价格
MaxPrice int64 // 最高价格
MaxDiscount float64 // 最大折扣比例
}

// Check 安全检查
func (sc *SafetyChecker) Check(req *PricingRequest, resp *PricingResponse) error {
// 1. 价格合理性检查
if resp.FinalPrice < 0 {
return fmt.Errorf("final price is negative: %d", resp.FinalPrice)
}

// 2. 价格范围检查
threshold := sc.thresholds[req.CategoryID]
if threshold != nil {
if resp.FinalPrice < threshold.MinPrice {
return fmt.Errorf("final price %d below min %d", resp.FinalPrice, threshold.MinPrice)
}
if resp.FinalPrice > threshold.MaxPrice {
return fmt.Errorf("final price %d above max %d", resp.FinalPrice, threshold.MaxPrice)
}
}

// 3. 折扣合理性检查
if resp.TotalAmount > 0 {
discountRate := float64(resp.TotalAmount-resp.FinalPrice) / float64(resp.TotalAmount)
if threshold != nil && discountRate > threshold.MaxDiscount {
return fmt.Errorf("discount rate %.2f%% exceeds max %.2f%%",
discountRate*100, threshold.MaxDiscount*100)
}
}

// 4. 优惠叠加检查
totalDiscount := resp.PromotionDiscount + resp.VoucherDiscount + resp.CoinDeduction
if totalDiscount > resp.TotalAmount {
return fmt.Errorf("total discount %d exceeds total amount %d",
totalDiscount, resp.TotalAmount)
}

return nil
}

9.3 性能优化清单

优化项 方案 收益
缓存 L1本地 + L2Redis 缓存命中率 90%+,RT降低 80%
并发 依赖服务并发调用 RT降低 50%
批量 批量接口优化 QPS提升 10x
连接池 复用HTTP/RPC连接 RT降低 30%
熔断降级 非核心服务降级 可用性 99.9%
异步化 非实时任务异步 RT降低 60%

9.4 常见问题

Q1: 如何保证新老逻辑价格一致?

A: 通过空跑比对机制:

  1. 新老逻辑并行运行
  2. 自动比对结果差异
  3. 差异超过阈值告警
  4. 人工审核差异原因
  5. 修复问题后重新验证

Q2: 如何处理高并发场景?

A: 多层优化:

  1. 本地缓存 + Redis 缓存
  2. 依赖服务并发调用
  3. 连接池复用
  4. 限流熔断
  5. 水平扩容

Q3: 如何防止资损?

A: 多重保护:

  1. 价格合理性检查
  2. 价格范围限制
  3. 折扣比例限制
  4. 空跑比对验证
  5. 灰度逐步放量
  6. 实时监控告警

Q4: 如何支持新品类?

A: 两种方式:

  1. 实现 Calculator 接口注册专用计算器
  2. 使用通用计算层 + 配置差异化规则

Q5: 如何回滚?

A: 快速回滚机制:

  1. 配置中心调整灰度比例为 0
  2. 流量立即切回老逻辑
  3. 无需重新发布

十、总结

10.1 核心价值

通过建设统一的价格计算引擎并引入DDD设计思想,我们实现了:

维度 改进前 改进后 提升
开发效率 新增变价因素需修改 5+ 服务 只需新增一个策略/组件 ↑ 5x
准确性 年均 3-5 次资损事故 0 资损(Money值对象保证) ↑ 100%
性能 P99 延迟 500ms+ P99 延迟 < 200ms ↑ 2.5x
扩展性 每个品类独立实现 统一模型 + 策略适配 ↑ 10x
可维护性 逻辑分散,难以追溯 领域模型封装,完整链路 ↑ 5x
代码可读性 价格字段分散,难以理解 统一术语 + 值对象,语义清晰 ↑ 80%
团队协作 术语不统一,沟通成本高 价格字典 + 统一语言 ↑ 60%
测试性 依赖复杂,难以单元测试 领域对象独立,易于测试 ↑ 3x

10.2 技术亮点

  1. DDD领域驱动设计:引入统一语言、值对象、聚合根、领域服务,代码可读性提升80%
  2. 价格字典体系:建立完整的价格术语字典,统一团队沟通语言,降低理解成本
  3. 分层架构:基础价格、营销、抵扣、费用、最终价格五层清晰
  4. 策略模式:品类差异通过策略适配,避免 if-else 地狱
  5. 灰度机制:细粒度灰度控制,安全迁移
  6. 空跑比对:新老逻辑并行,自动发现差异
  7. 性能优化:多级缓存 + 并发优化,支持高并发
  8. 资损防控:多重检查机制,0 资损目标
  9. Money值对象:封装金额运算,避免浮点数精度问题,提升安全性
  10. 领域模型封装:业务逻辑内聚,修改影响范围可控

10.3 未来演进

  1. AI 定价:引入机器学习,动态调整定价策略
  2. 实时营销:基于用户行为实时调整优惠
  3. 跨境定价:支持多币种、汇率实时转换
  4. 个性化定价:千人千面的价格策略
  5. 成本优化:基于成本模型的智能定价

相关文章

参考资料

  1. Martin Fowler - 策略模式
  2. 高性能 MySQL - 缓存优化
  3. 微服务设计 - 服务拆分与治理
  4. 领域驱动设计 - Eric Evans

日期:2026-02-27
标签:#价格引擎 #电商系统 #计价中心 #系统设计


系列导航
计价系统的领域建模和 DDD 实践,详见(六)计价系统 DDD 实践

数据结构

1. 序列、子序列与子数组 (核心模式归类)

A. 子数组 (Subarray - 连续性)

B. 子串 (Substring - 连续性)

C. 子序列 (Subsequence - 相对顺序)

2. 数组与矩阵 (核心模式归类)

A. 原地修改 / 数组作为哈希表 (实现 $O(1)$ 额外空间)

B. 矩阵遍历与坐标变换

  • 54. 螺旋矩阵【模式:四边界收缩;$(u, d, l, r)$ 指针随遍历向内挤压】
  • 36. 有效的数独【模式:一维化索引;利用 (r/3)*3 + c/3 映射九宫格】
  • 189. 轮转数组【模式:三次翻转;通过 reverse 实现 $O(1)$ 空间位移】
  • 31. 下一个排列【模式:标准算法;找 pivot -> 找更大数 -> 交换并反转】

C. 双指针、贪心与接雨水 (处理单调性或边界)

  • 11. 盛最多水的容器【模式:对撞指针;每次移动较短边以求更大容积】
  • 42. 接雨水【模式:双指针/单调栈;核心是“木桶原理”,高度由短板决定】
  • 407. 接雨水 II【模式:BFS + 优先队列;从外向内收缩 3D 边界】
  • 283. 移动零【模式:快慢指针;一个负责遍历,一个负责存放非零值】
  • 228. 汇总区间【模式:分组循环 / 双指针;核心:通过 nums[j+1] != nums[j]+1 识别连续区间断点】
  • 134. 加油站【模式:贪心;记录总收益与局部余量判断起点】
  • 135. 分发糖果【模式:双向遍历;确保同时满足左右邻居约束】

D. 前缀和与子数组 (处理区间和/积)

3. 排列与字典序 (Permutations & Lexicographical Order)

核心思想:处理元素的顺序变换。从“下一个排列”的局部演变,到“全排列”的全局搜索,再到“第 K 个排列”的数学定位。

A. 下一个排列 (标准字典序算法)

B. 全排列生成 (回溯搜索)

  • 46. 全排列【模式:基础回溯;核心:【口诀】排列靠 used:全员参与,位掩码标记】
  • 47. 全排列 II【模式:去重回溯;核心:排序 + 剪枝;【口诀】重复靠排序:前人未用,后人莫入】

C. 排列数学定位 (阶乘计数法)

  • 60. 第 K 个排列【模式:数学构造;核心:利用阶乘分桶原理,逐位确定数字,时间 $O(n^2)$】

4. 区间与扫描线 (Intervals & Sweep Line)

A. 区间合并与插入 (Merge & Insert)

  • 56. 合并区间【模式:区间合并;核心:按起点排序,维护 [L, R],利用 l <= cur_right 动态扩展右边界】
  • 57. 插入区间【模式:分类讨论;核心:将区间分为“左侧不重叠”、“中间重叠合并”、“右侧不重叠”三部分处理】
  • 228. 汇总区间【模式:分组循环 / 双指针;核心:通过 nums[j+1] != nums[j]+1 识别连续区间断点】
  • 2580. 统计将重叠区间合并成组的方案数【模式:区间合并 + 组合数学;核心:合并后得到 m 个独立连通块,结果为 $2^m$】

B. 区间重叠与贪心 (Non-overlapping & Greedy)

  • 435. 无重叠区间【模式:贪心留空;核心:按终点排序,尽量保留先结束的区间,以给后续留出更多空间】
  • 452. 用最少数量的箭引爆气球【模式:区间交集;核心:按终点排序,贪心选择重叠区域的边缘】
  • 646. 最长数对链【模式:贪心;核心:按第二个数排序,贪心选择结束最早的区间,同 435 题】
  • 253. 会议室 II【模式:上下车/差分思想;核心:将起点看作 +1,终点看作 -1,求最大并发数;或利用小顶堆维护当前结束时间】
  • 218. 天际线问题【模式:区间更新 + 单点查询;核心:区间范围大且稀疏,需要动态开点 + 懒标记】
  • 1851. 包含每个查询的最小区间【模式:离线查询 + 扫描线;核心:将区间和查询排序,利用优先队列维护当前覆盖的区间】

C. 区间查询与更新 (Range Query & Update - 线段树/树状数组)

核心思想:高效处理区间操作的数据结构家族。从静态的前缀和到动态的线段树,根据问题的查询/更新需求选择合适的工具。

技术演进路径:前缀和(静态查询)→ 差分(批量更新)→ 树状数组(动态单点)→ 线段树(动态区间)

1. 前缀和与后缀和 (Prefix Sum & Suffix Sum)

场景:静态数组的快速区间查询;预处理 $O(n)$,查询 $O(1)$
核心:sum[l, r] = prefix[r+1] - prefix[l],支持一维/二维/前缀积等变体

2. 差分数组 (Difference Array)

场景:多次区间更新 + 一次性查询;时间 $O(n + q)$,空间 $O(n)$
核心:diff[l] += val, diff[r+1] -= val,最后前缀和还原;是前缀和的”逆运算”

3. 树状数组 (Binary Indexed Tree / Fenwick Tree)

场景:单点更新 + 区间查询(前缀和);时间 $O(\log n)$,空间 $O(n)$
核心:利用 lowbit(x) = x & -x 实现树状结构,是动态版本的前缀和

4. 线段树 (Segment Tree)

场景:区间更新 + 区间查询(最大/最小/和);时间 $O(\log n)$,空间 $O(n)$ 或动态开点
核心:完全二叉树结构,支持懒标记(Lazy Propagation)批量更新,是最通用的区间数据结构

4. 栈与单调栈 (核心模式归类)

A. 基础栈应用与模拟 (处理嵌套、撤销与状态存取)

  • 20. 有效的括号【模式:括号匹配;核心:利用栈的 LIFO 特性处理嵌套关系】
  • 150. 逆波兰表达式求值【模式:后缀表达式计算;核心:遇到运算符弹出两数计算,注意减/除顺序】
  • 71. 简化路径【模式:路径模拟;核心:遇到 .. 执行出栈,配合 stringstream 拆分单词】
  • 155. 最小栈【模式:双栈/辅助栈;核心:同步维护一个“当前的最小值”栈】
  • 232. 用栈实现队列【模式:双栈翻转;核心:利用入栈和出栈两个容器实现 FIFO】
  • 394. 字符串解码【模式:多栈状态存取;核心:分别用栈存储当前的倍数 cnt 和已拼出的 string
  • 224. 基本计算器【模式:符号栈模拟;核心:利用栈维护当前括号层级的“全局正负号”,实现 $O(n)$ 一次遍历展开括号】
  • 227. 基本计算器 II【模式:栈模拟 / 优先级处理;核心:遇到乘除立即运算,加减则入栈最后求和】
  • 772. 基本计算器 III【模式:通用表达式求值;核心:结合 224 和 227,利用递归或双栈处理括号与四则运算优先级】

B. 单调栈基础 (在线性时间内寻找左右最近的极值)

C. 单调栈进阶 (处理区间面积与贡献度计算)

  • 84. 柱状图中最大的矩形【模式:左右扩展边界;核心:利用单调栈一次性确定每个柱子的左、右边界,求最大矩形面积】
  • 907. 子数组的最小值之和【模式:贡献度法;核心:计算每个元素作为最小值的区间覆盖范围 $(i-L)*(R-i)$】
  • 2866. 美丽塔 II【模式:前后缀单调栈;核心:分别计算左侧和右侧的单调递增和,最后枚举顶点取 Max】
  • 768. 最多能完成排序的块 II【模式:单调栈维护块极值;核心:栈中每个元素代表一个“块”的最大值,重叠则合并】

D. 栈与贪心/其他

3. 队列与双端队列 (Queue & Deque)

4. 优先队列与堆 (Priority Queue & Heap)

A. 基础堆应用 (Top K / 动态极值 / 中位数)

B. 反悔贪心 (Regret Greedy - 核心模式)

  • 630. 课程表 III【模式:大顶堆维护耗时;遇到冲突时“反悔”替换掉耗时最长的课程】
  • LCP 30. 魔塔游戏【模式:小顶堆维护负值;血量不足时“反悔”将之前扣血最多的移到最后】
  • 871. 最低加油次数【模式:大顶堆维护油量;油不够时“反悔”在之前经过的油量最大的站加油】
  • 502. IPO【模式:双堆;按资本排序 + 大顶堆选利润最大的项目】

C. 最短路径与图搜索 (Dijkstra 及其变体)

D. 区间与会议室 (扫描线 / 堆优化)

  • 252. 会议室【模式:贪心排序;按开始时间排序后判断是否存在时间重叠】
  • 253. 会议室 II【模式:小顶堆;堆顶存储最早结束的会议时间,判断是否需开新房】
  • 2402. 会议室 III【模式:双堆模拟;小顶堆分别维护空闲会议室和占用会议室,带编号分配策略】

5. 链表 (核心模式归类)

A. 基础操作与反转 (双指针、递归与 Dummy Node)

B. 快慢指针与环形检测

  • 141. 环形链表【模式:快慢指针;核心:利用步长差 $(2-1=1)$,在 $O(n)$ 时间 $O(1)$ 空间内检测链表是否有环】
  • 142. 环形链表 II【模式:双指针追赶;核心:相遇后将一指针归零,同步慢走寻找环入口】
  • 19. 删除链表的倒数第 N 个结点【模式:快慢指针;核心:利用 $n$ 步位移差定位倒数第 $n+1$ 个节点(前驱节点)】
  • 61. 旋转链表【模式:成环解环;核心:先连成环再在 $n-(k%n)$ 处断开,简化指针操作】
  • 876. 链表的中间结点【模式:快慢指针;核心:fast 走两步 slow 走一步,fast 到头时 slow 在中点】
  • 287. 寻找重复数【模式:映射找环;将数组索引视为链表指针,转化为环入口问题】

C. 合并、排序与分隔

D. 复杂链表与采样

  • 138. 随机链表的复制【模式:原地克隆;核心:A->A'->B->B' 插入法,实现 $O(1)$ 空间拷贝随机指针】
  • 382. 链表随机节点【模式:水塘抽样;核心:从未知长度流中等概率采样,确保概率为 $1/i$】
  • 146. LRU 缓存【模式:哈希表 + 双向链表;实现 $O(1)$ 的访问与淘汰】
  • 460. LFU 缓存【模式:双哈希表 + 频次链表;实现 $O(1)$ 的频率敏感淘汰】

6. 二叉树与树形结构 (核心模式归类)

A. 遍历、属性与结构基础 (递归与迭代)

B. 路径、祖先与贡献度计算 (DFS 进阶)

C. 构造、变换与序列化

D. 二叉搜索树 (BST 专项)

7. 树与图论 (Tree & Graph - 核心模式归类)

A. 树的基础与进阶 (Tree)

B. 网格搜索与连通性 (DFS/BFS)

  • 200. 岛屿数量【模式:DFS/BFS;核心:原地修改标记(沉岛)实现 $O(1)$ 空间】
  • 305. 岛屿数量 II【模式:并查集 (Union-Find);核心:动态维护连通分量,将”陆地化”转化为”集合合并”】
  • 130. 被围绕的区域【模式:逆向思维;从边界 'O' 开始标记,未被标记的内部 'O' 均需填充】
  • 133. 克隆图【模式:哈希表 + DFS/BFS;核心:利用 Map 存储 [原节点 -> 新节点] 防止死循环】
  • 399. 除法求值【模式:带权图搜索;将变量视为节点,比值视为边权,通过 DFS 或并查集求解】

C. 拓扑排序 (有向无环图 DAG)

D. 广度优先搜索进阶 (最短路径/步数)

  • 909. 蛇梯棋【模式:BFS;核心:一维坐标与二维矩阵的映射转换】
  • 433. 最小基因变化【模式:单向/双向 BFS;寻找状态空间的最短路径】
  • 127. 单词接龙【模式:双向 BFS;核心:利用中间态(如 h*t)优化状态转移搜索】

E. 最短路径算法 (Dijkstra/Floyd/Bellman)

F. 高级图论与连通性 (并查集/基环树/网络流)

8. 平衡二叉搜索树 (std::map/set)

9. 区间查询与更新 (Range Query & Update)

核心思想:高效处理区间操作的数据结构家族。从静态的前缀和到动态的线段树,根据问题的查询/更新需求选择合适的工具。

技术演进路径:前缀和(静态查询)→ 差分(批量更新)→ 树状数组(动态单点)→ 线段树(动态区间)

A. 前缀和与后缀和 (Prefix Sum & Suffix Sum)

场景:静态数组的快速区间查询;预处理 $O(n)$,查询 $O(1)$
核心:sum[l, r] = prefix[r+1] - prefix[l],支持一维/二维/前缀积等变体

B. 差分数组 (Difference Array)

场景:多次区间更新 + 一次性查询;时间 $O(n + q)$,空间 $O(n)$
核心:diff[l] += val, diff[r+1] -= val,最后前缀和还原;是前缀和的”逆运算”

C. 树状数组 (Binary Indexed Tree / Fenwick Tree)

场景:单点更新 + 区间查询(前缀和);时间 $O(\log n)$,空间 $O(n)$
核心:利用 lowbit(x) = x & -x 实现树状结构,是动态版本的前缀和

D. 线段树 (Segment Tree)

场景:区间更新 + 区间查询(最大/最小/和);时间 $O(\log n)$,空间 $O(n)$ 或动态开点
核心:完全二叉树结构,支持懒标记(Lazy Propagation)批量更新,是最通用的区间数据结构

11. 字典树 (Trie)

12. 并查集 (Union Find)

13. 数据结构设计与实现 (Consolidated)

A. 基础结构实现

B. 缓存与高级哈希

C. 树与图的高级结构

14. 离线query

15. 回文串 (Palindrome)

基本算法

18. 双指针 (Two Pointers)

核心:关注两个“点”的博弈,利用有序性或特征缩减搜索空间。

A. 对撞指针 (相向移动)

场景:有序数组求和、反转、容积问题

B. 快慢指针 (同向移动)

场景:原地修改、子序列匹配、区间归并

19. 滑动窗口 (Sliding Window)

核心:关注两个指针中间的“区域/区间”的性质维护。

A. 不定长窗口:求最长/最大 (Variable Length - Max)

逻辑:Right 扩张 -> 满足条件? -> 更新结果

B. 不定长窗口:求最短/最小 (Variable Length - Min)

逻辑:Right 扩张 -> 满足条件? -> Left 收缩 (Update Min) -> 循环直到不满足

C. 固定窗口 (Fixed Length)

逻辑:窗口大小固定为 K,RightLeft

D. 窗口内的特殊统计 (结合其他数据结构)

21. Top K / 中位数 / 第 K 小元素 (Top K & Selection Problems)

核心思想:从海量数据中高效选取第 K 个元素或维护动态极值,是算法面试的高频考点。

A. 快速选择与堆 (QuickSelect & Heap)

场景:无序数组中找第 K 大/小元素

B. 对顶堆与数据流 (Dual Heap & Stream)

场景:动态数据流中维护中位数或极值

  • 295. 数据流的中位数【模式:对顶堆;核心:最大堆维护左半部(较小值),最小堆维护右半部(较大值),保持两堆大小平衡】
  • 480. 滑动窗口中位数【模式:对顶堆 + 延迟删除;核心:在滑动窗口场景下维护动态中位数】

C. 二分答案与有序结构 (Binary Search & Sorted Structure)

场景:有序矩阵、多数组归并中的第 K 小问题

D. 多路归并与有序序列生成 (Multi-way Merge)

场景:从多个有序源中生成第 K 个元素

  • 面试题 17.09. 第 k 个数【模式:三指针 DP / 多路归并;核心:维护 p3, p5, p7 三个指针,每次选择 min(dp[p3]*3, dp[p5]*5, dp[p7]*7) 作为下一个魔法数,相等时多指针同时前移去重;最优 $O(k)$ 时间复杂度,类似 264 题丑数 II】
  • 264. 丑数 II【模式:三指针 DP / 小顶堆;核心:按序生成仅含质因子 2, 3, 5 的数字】
  • 23. 合并 K 个升序链表【模式:最小堆 / 分治归并;核心:维护 K 个链表头的最小值,或两两分治合并】

22. 排序算法与应用 (Sorting Algorithms & Applications)

核心思想:排序是算法的基础工具,通过消除无序性简化问题,常与双指针、贪心、二分等技巧结合。

A. 排序算法实现 (Algorithm Implementation)

场景:手撕排序算法、理解排序原理

  • 912. 排序数组【模式:快速排序 / 归并排序;核心:快排三段式 (less, equal, more),归并分治合并】
  • 148. 排序链表【模式:链表归并排序;核心:快慢指针找中点 + 递归拆分 + 有序链表合并;注意:断开中点连接以防止死循环】
  • 147. 对链表进行插入排序【模式:插入排序;核心:维护已排序部分,将新节点插入合适位置】

B. 自定义排序 (Custom Comparator)

场景:需要特殊排序规则的问题

C. 有序数组/链表合并 (Merge Sorted Sequences)

场景:多个有序序列的合并

D. 排序 + 双指针 (Sorting + Two Pointers)

场景:排序后利用有序性进行双指针搜索

E. 排序 + 贪心 (Sorting + Greedy)

场景:排序后贪心选择、区间处理

F. 排序 + 枚举/分界线 (Sorting + Enumeration)

场景:排序后枚举关键点或分界线

场景:排序后利用单调性进行二分或 LIS

H. 排序块问题 (Chunk Sorting)

场景:判断数组能否分块排序

I. 有序结构与 BST (Sorted Structure)

场景:利用有序性质的特殊问题

J. 拓扑排序 (Topological Sort)

场景:有向无环图的线性排序

23. 字符串匹配 (KMP / AC 自动机)

24. 枚举

25. 模拟/分组/循环 (group/cycle arrray/模拟/易错)

26. 贪心算法 (Greedy Algorithm - 核心模式归类)

A. 基础贪心与排序 (利用排序消除维度影响)

B. 反悔贪心 (结合优先队列动态调整)

  • 630. 课程表 III【模式:反悔贪心;核心:先按截止时间排序,若当前无法加入则替换掉之前耗时最长的课程】
  • 502. IPO【模式:双堆/排序+大顶堆;核心:动态选择当前资金下利润最大的项目】
  • LCP 30. 魔塔游戏【模式:反悔贪心;核心:血量不足时将之前扣血最多的房间移到最后】
  • 871. 最低加油次数【模式:反悔贪心;核心:油不够时从经过的加油站中选油最多的加】

C. 贡献度法与数学贪心

D. 区间处理 (排序 + 边界维护)

E. 邻居约束与多遍遍历

F. 贪心 + 二分/DP 结合

27. 分治 (Divide and Conquer)

核心逻辑

  1. **分解 (Divide)**:将原问题拆分为规模较小、相互独立的子问题(如左右子树、数组半区)。
  2. **解决 (Conquer)**:递归解决子问题,直到触及边界。
  3. **合并 (Combine)**:将子问题的解合并为原问题的解(如归并排序的 merge 或 LCA 的状态上传)。

28. 贡献法 (Contribution Method)

核心思想:不枚举所有可能的子集/子数组,而是枚举每个元素,计算该元素对最终答案的“贡献”次数或值。

A. 单调栈 + 贡献法 (区间极值贡献)

B. 数学/位运算/点对贡献

C. 树上贡献 (边/点贡献)

数学 (Mathematics - 核心模式归类)

28. 数论基础 (Number Theory)

A. 质数、约数与筛选法

B. 最大公约数 (GCD) 与 最小公倍数 (LCM)

C. 数字处理与投票算法

  • 9. 回文数【模式:数学反转;核心:反转一半数字与前半部分比较,避免溢出】
  • 169. 多数元素【模式:Boyer-Moore 摩尔投票法;$O(n)$ 时间 $O(1)$ 空间找众数】
  • 229. 多数元素 II【模式:进阶摩尔投票;统计出现次数超过 $n/3$ 的元素】
  • 400. 第 N 位数字【模式:数学模拟;按位数区间(个位、十位…)定位数字】
  • 343. 整数拆分【模式:数学推导;核心:尽可能拆分成 3 以获得最大乘积】

28. 组合数学与概率 (Combinatorics & Probability)

A. 排列组合与大数取模

B. 模运算与乘法逆元

C. 随机采样 (Sampling)

29. 位运算 (Bit Manipulation)

A. 基础技巧与 Lowbit

  • 核心性质n & (n-1) 消除最低位 1;n & -n 获取最低位 1 (lowbit)】
  • 191. 位 1 的个数【模式:__builtin_popcountn & (n-1) 迭代】
  • 190. 颠倒二进制位【模式:位操作;逐位反转 ans = (ans << 1) | (n & 1) 或 分治法】
  • 231. 2 的幂【模式:n > 0 && (n & (n-1)) == 0
  • 201. 数字范围按位与【模式:公共前缀;寻找 leftright 的二进制公共前缀】

B. 异或 (XOR) 专项

30. 快速幂与几何 (Fast Power & Geometry)

搜索问题核心分类与总结 (Search Strategies)

31. DFS 与回溯:全量枚举与约束满足 (DFS & Backtracking)

核心区别

  • DFS(深度优先搜索):遍历所有状态,通常用于连通性判断、路径查找、拓扑排序
  • 回溯(Backtracking):在 DFS 基础上增加”撤销选择”,用于求解组合优化问题(排列、组合、子集)

A. 基础 DFS:遍历与连通性 (Traversal & Connectivity)

场景:图/树的遍历、连通分量统计、环检测

  • 200. 岛屿数量【模式:连通分量统计;核心:DFS 标记连通区域,原地修改避免 visited 数组】
  • 133. 克隆图【模式:哈希表 + DFS;核心:用 Map 存储 [原节点 -> 新节点] 防止死循环】
  • 2101. 引爆最多的炸弹【模式:有向图 DFS;核心:枚举起点,DFS 统计可达节点数】

B. 树上 DFS:路径与状态聚合 (Tree DFS)

场景:路径和、路径记录、子树信息聚合

  • 112. 路径总和【模式:DFS 递归;核心:判断是否存在根到叶子路径和等于目标值,叶子节点定义为左右子树均为空】
  • 113. 路径总和 II【模式:回溯 + 路径记录;核心:DFS 遍历时维护 path 数组,到达叶子节点且路径和等于目标值时保存当前路径的副本,回溯时弹出节点】
  • 437. 路径总和 III【模式:前缀和 + 哈希表;核心:路径不必从根开始,利用 prefixSum - targetSum 统计满足条件的路径数,类似 560 题】
  • 2477. 到达首都的最少油耗【模式:后序 DFS / 树形 DP;核心:从根节点开始后序遍历,递归返回子树总人数,每个节点计算 ⌈人数/seats⌉ 辆车的油耗;也可用拓扑排序 BFS 从叶子向根逐层合并(见 BFS 章节)】

C. 回溯:组合与排列 (Backtracking - Combinations & Permutations)

场景:求所有方案、排列组合、子集生成

💡 回溯去重口诀
  • 组合start:不回头看,一路向右。
  • 排列used:全员参与,位掩码标记。
  • 重复排序:前人未用,后人莫入(!used[i-1])。
  • 17. 电话号码的字母组合【模式:基础回溯;核心:递归深度控制数字索引,for 循环遍历字母映射】
  • 77. 组合【模式:组合回溯;核心:【口诀】组合靠 start:不回头看,一路向右】
  • 39. 组合总和【模式:重复选组合;核心:【原理】传递当前索引 i 而非 i+1 实现元素可重复选取】
  • 40. 组合总和 II【模式:有重复元素的组合;核心:排序去重,【口诀】重复靠排序:前人未用,后人莫入(!used[i-1])】
  • 216. 组合总和 III【模式:限定范围组合;核心:1-9 数字中选 k 个数和为 n,剪枝优化】
  • 377. 组合总和 Ⅳ【模式:排列数 DP;核心:求和为 target 的排列个数,dp[i] = sum(dp[i - num]),完全背包求排列问题】
  • 46. 全排列【模式:排列回溯;核心:【口诀】排列靠 used:全员参与,位掩码标记】
  • 47. 全排列 II【模式:有重排列;核心:【原理】重复靠排序:前人未用,后人莫入(!used[i-1])】
  • LCR 086. 分割回文串【模式:子串划分;核心:枚举分割点,预处理回文表优化判断】

D. 回溯:约束满足问题 (Constraint Satisfaction Problems)

场景:棋盘问题、数独、复杂约束条件

💡 约束优化口诀
  • 棋盘靠标记:列号、和、差,三位一体定乾坤。
  • 括号看余额:左括号不超标,右括号不透支。
  • 矩阵靠沉岛:先占位再递归,事后记得还原。
  • 51. N 皇后【模式:棋盘回溯;核心:【口诀】棋盘靠标记:列号、和、差,三位一体定乾坤】
  • 52. N 皇后 II【模式:棋盘回溯;核心:利用 cols[c]diag1[r+c]diag2[r-c+n] 三个数组实现 O(1) 冲突检测】
  • 22. 括号生成【模式:配对回溯;核心:【口诀】括号看余额:左括号不超标,右括号不透支】
  • 79. 单词搜索【模式:矩阵回溯;核心:【口诀】矩阵靠沉岛:先占位再递归,事后记得还原】
  • 212. 单词搜索 II【模式:Trie + 回溯;核心:将词典建模为 Trie,在网格回溯中同步移动 Trie 指针,实现多模式高效匹配】

E. 记忆化搜索 (Memoization DFS)

场景:重叠子问题、状态空间搜索、自顶向下 DP

DP 问题 (Dynamic Programming - 核心模式归类)

DP 类问题处理五部曲总结

步骤 核心任务 (Key Action) 你的代码体现 (Example) 空间优化思路 (Space Optimization)
1. 状态定义 明确 dp 数组各维度的物理含义(是长度、最值还是布尔值?对应什么区间?) dp[i][j] 表示到达坐标 (i,j) 的最小路径和 维度压缩:若当前状态只依赖前一状态,可将二维数组降为一维(或常数个变量)。
2. 转移方程 逻辑推导过程,包括不同条件下的决策 dp[i][j] = min(dp[i-1][j], dp[i][j-1]) + grid[i][j] 原地修改:如果输入数组(如 grid)后续不再使用,可以直接在原数组上操作实现 $O(1)$ 额外空间。
3. 初始边界 算法开始的基石(如单字符情况、空串情况),确定无需推导的“种子”值 初始化 dp[0][0],并单独处理首行 dp[0][i] 和首列 dp[i][0] 虚拟边界:有时可以多申请一行/一列(如 dp[m+1][n+1])并填入占位值,从而统一循环内的逻辑。
4. 计算顺序 确定循环的方向(Top-down vs Bottom-up),由状态依赖关系决定 使用双重 for 循环,从左到右、从上到下遍历 倒序遍历:在 0/1 背包等问题中,通过倒序遍历一维 DP 数组,可以防止当前层的计算污染待使用的旧数据。
5. 最终结果 确定答案在 dp 表中的存储位置 返回 dp[m-1][n-1] 状态追踪:如果不仅要结果还要路径,通常需要额外的 parent 数组记录来源,空间优化此时会受限。
6. 复杂度分析 分析时间与空间开销 时间 $O(M \times N)$,空间 $O(M \times N)$ 时空权衡:有时为了降低时间复杂度(如利用前缀和优化转移),可能会增加空间复杂度。

33. 状态转移流派:局部依赖 vs 全局扫描

核心逻辑:识别“依赖范围”是掌握 DP 的精髓。你可以把 DP 想象成“在过去中寻找最优解”

维度 局部依赖型 (Local Dependency) 全局扫描型 (Global Scanning)
转移特征 dp[i] = f(dp[i-1], dp[i-2], ...) dp[i] = f(dp[0...i-1])
暴力复杂度 $O(n)$ $O(n^2)$
掌握重点 空间优化 (滚动数组/变量替代) 时间优化 (数据结构/二分/单调性)
一眼辨别 “我只能从前一个或前几个状态跳过来” “我可以从前面任意一个符合条件的点跳过来”

A. 第一流派:局部依赖 (局部记忆型)

核心特点:只需要知道“最近”的几步,就能决定下一步。如果发现 dp[i] 只依赖 dp[i-1],通常可以压缩至 $O(1)$ 空间。

B. 第二流派:全局扫描 (区间/全历史扫描型)

核心特点:需要纵观所有历史状态才能决定下一步。面试常考“如何将 $O(n^2)$ 降到 $O(n \log n)$ 或 $O(n)$”。

  • 300. 最长递增子序列【模式:$dp[i] = max(dp[j]) + 1$,需遍历所有 $j < i$】
  • 139. 单词拆分【模式:需检查 $0$ 到 $i-1$ 所有可能的断点 $j$】
  • 85. 最大矩形【模式:需要结合单调栈进行全局扫描】
  • 区间 DP:如矩阵链乘法,需要遍历所有可能的切分点。

34. 基础线性 DP (1D/2D 填表)

最基础的递推,dp[i] 只依赖于前面几个状态

A. 斐波那契/爬楼梯模型

B. 网格路径模型 (Grid)

C. 简单一维推导

35. 状态机 DP (State Machine)

核心在于定义“持有”、“冷冻”、“卖出”等有限状态,画状态转移图

A. 股票系列

B. 其他状态机

C. 打家劫舍系列汇总 (House Robber)

36. 序列 DP (双串/单串)

处理字符串或数组子序列问题,核心是 LCS/LIS 模型

A. 单串 LIS 模型 ($O(n^2)$ 或 $O(n \log n)$)

B. 双串 LCS 模型 (二维表,m + 1,n + 1 处理空串的情况)

  • 1143. 最长公共子序列【模式:双串 DP;dp[i][j] = s1[i]==s2[j] ? dp[i-1][j-1]+1 : max(左, 上)注意:DP 数组大小为 (M+1)*(N+1) 处理空串
  • 72. 编辑距离【模式:增删改三选一;dp[i][j] = min(插入, 删除, 替换) + 1注意:DP 数组大小为 (M+1)*(N+1) 处理空串
  • 97. 交错字符串【模式:双串 DP;dp[i][j] 表示 s1[0..i]s2[0..j] 能否交错组成 s3[0..i+j]注意:DP 数组大小为 (M+1)*(N+1) 处理空串
  • 583. 两个字符串的删除操作【模式:LCS 变体;结果为 m + n - 2 * LCS注意:DP 数组大小为 (M+1)*(N+1) 处理空串
  • 1035. 不相交的线【模式:LCS 本质;完全等同于最长公共子序列;注意:DP 数组大小为 (M+1)*(N+1) 处理空串
  • 115. 不同的子序列【模式:计数 DP;s[i]==t[j] 时可选匹配或不匹配,dp[i][j] = dp[i-1][j-1] + dp[i-1][j]注意:DP 数组大小为 (M+1)*(N+1) 处理空串
  • 10. 正则表达式匹配【模式:双串 DP;核心:dp[i][j] 匹配 s[0..i]p[0..j];遇到 * 时可匹配 0 次或多次前字符;*注意:DP 数组大小为 (M+1)(N+1)**】
  • 44. 通配符匹配【模式:双串 DP;核心:* 可匹配任意序列;dp[i][j] = dp[i][j-1] || dp[i-1][j]

C. 回文串模型

37. 划分型 DP (Partition)

将数组/字符串切分为 k 段,求最优解

37. 背包 DP (Knapsack)

核心思想:给定一组物品(有重量和价值),在限定的总容量内,如何选择物品使得总价值最大。

解题三部曲

  1. 状态定义dp[i][j] 表示前 i 个物品,在容量为 j 时的最优解。
  2. 转移方程dp[i][j] = max(不选, 选)
    • 不选:dp[i-1][j]
    • 选(容量够):dp[i-1][j - weight[i]] + value[i]
  3. 空间优化:利用滚动数组将二维压缩至一维 dp[j]
    • 0/1 背包j 必须从大到小(倒序)遍历,防止同一物品被重复计算。
    • 完全背包j 必须从小到大(正序)遍历,允许同一物品多次选取。

A. 0/1 背包 (每个物品仅 1 件)

  • 416. 分割等和子集【模式:转化为容量为 sum/2 的 0/1 背包是否能装满】
  • 494. 目标和【模式:转化为 0/1 背包组合数问题】
  • 474. 一和零【模式:二维费用背包;dp[i][j] 表示 i 个 0 和 j 个 1 的最大字符串数】

B. 完全背包 (每个物品无限件)

C. 多重/分组背包

38. 区间 DP (Interval)

从小区间合并到大区间,枚举分割点 k

39. 树形 DP (Tree DP)

自底向上汇总信息,或换根 DP

A. 子树贡献/直径

B. 换根 DP

40. 状压 DP (Bitmask)

数据范围 n < 20,用二进制表示集合

  • 464. 我能赢吗【模式:记忆化搜索 + 状压;核心:利用二进制 mask 记录数字使用状态,博弈论必胜态判断】
  • 526. 优美的排列【模式:状压 DP / 回溯;核心:dp[mask] 表示选取状态为 mask 时的方案数,或 DFS 剪枝】
  • 847. 访问所有节点的最短路径【模式:BFS + 状压;核心:状态定义为 (node, mask),求覆盖所有节点的最短路】
  • 698. 划分为k个相等的子集【模式:状压 DP / 回溯;核心:dp[mask] 记录当前累加和,或 DFS 每次凑齐一个桶】
  • 2741. 特别的排列【模式:状压 DP;核心:dp[mask][last_val] 记录状态与上一个元素,满足整除关系转移】

41. 数位 DP (Digit DP)

按位填数,通常配合记忆化搜索

42. 其他/高级 DP

C++ 字符处理函数速查

函数名 检查内容 说明
isdigit(c) 是否为数字 (0-9) 数字字符
isalpha(c) 是否为字母 (a-z, A-Z) 纯字符/字母
isalnum(c) 是否为字母或数字 字母数字混合
tolower(c) 转换为小写 字符转换
toupper(c) 转换为大写 字符转换

参考

电商系统设计系列(篇次与(一)推荐阅读顺序一致)

电商系统设计:营销系统深度解析

营销系统是电商平台的增长引擎,通过优惠券、积分、活动等手段实现用户拉新、促活、留存和 GMV 提升。本文深入解析营销系统的架构设计、核心模块、高并发场景处理和工程实践,适合系统设计面试和电商后端工程师阅读。

Read more »

电商系统设计系列(篇次与(一)推荐阅读顺序一致)

本文是电商系统设计系列的第三篇,聚焦库存系统的设计与实现。

一、背景与挑战

1.1 多品类库存差异

在数字电商/本地生活平台中,不同品类的库存特性差异极大:

品类 库存特点 扣减时机 典型示例
电子券 (Deal) 券码制,每个券码唯一 下单预订 星巴克电子券
虚拟服务券 (OPV) 数量制,分平台统计 下单预订 美甲/按摩服务券
酒店 时间维度,按日期管理 支付成功 Agoda 酒店房间
机票/票务 座位/场次制 支付成功 航班座位、电影票
礼品卡 (Giftcard) 实时生成或预采购卡密 支付成功 Google Play 充值卡
话费充值 (TopUp) 无限库存 无需扣减 手机话费
本地生活套餐 组合型,多子项联动 下单预订 火锅双人套餐

1.2 核心痛点

  1. 模型割裂:每个品类独立设计库存逻辑,无法复用。
  2. 数据不一致:Redis 与 MySQL 之间、预订数量 (booking) 与实际状态脱节。
  3. 供应商策略不统一:有的实时查询,有的定时同步,有的无需管理。
  4. 缺乏统一服务:各业务方直接操作 DB/Redis,维护成本高。
  5. 监控缺失:超卖、库存差异、供应商同步延迟难以发现。

1.3 设计目标

目标 说明 优先级
统一模型 多品类共用一套库存模型 P0
高性能 支持万级 QPS 秒杀场景 P0
灵活扩展 新品类接入无需修改核心代码 P0
最终一致 Redis 与 MySQL 数据最终一致 P0
供应商集成 支持实时/定时/推送多种同步策略 P1

二、库存分类体系

2.1 两个核心维度

设计统一库存模型的关键是将所有品类抽象为 两个正交维度

维度一:谁管库存?(Management Type)

1
2
3
4
5
const (
SelfManaged = 1 // 自管理:平台维护库存数据
SupplierManaged = 2 // 供应商管理:第三方维护,平台定期同步
Unlimited = 3 // 无限库存:无需库存管理
)

维度二:库存长什么样?(Unit Type)

1
2
3
4
5
6
const (
CodeBased = 1 // 券码制:每个库存是唯一券码(Deal、Giftcard)
QuantityBased = 2 // 数量制:库存是一个数字(OPV、本地服务)
TimeBased = 3 // 时间维度:按日期/时段管理(酒店、票务)
BundleBased = 4 // 组合型:多子项联动扣减(套餐)
)

2.2 品类分类矩阵

品类 管理类型 单元类型 扣减时机
电子券 (Deal) Self Code 下单
虚拟服务券 (OPV) Self Quantity 下单
本地服务 Self Quantity 下单
酒店 Supplier Time 支付
机票 Supplier Quantity 支付
话费充值 Unlimited -
礼品卡(预采购) Self Code 下单
礼品卡(实时生成) Supplier Code 支付
套餐组合 Self Bundle 下单

核心洞察:任何新品类接入时,只需确定它属于哪个 (ManagementType, UnitType) 组合,即可复用对应的库存策略,无需修改核心代码。


三、统一数据模型

3.1 库存配置表(inventory_config)

每个 SKU 一条配置,决定该商品使用哪种库存策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE TABLE inventory_config (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
item_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL DEFAULT 0,

-- 库存分类(核心)
management_type INT NOT NULL COMMENT '1=自管理,2=供应商,3=无限',
unit_type INT NOT NULL COMMENT '1=券码,2=数量,3=时间,4=组合',
deduct_timing INT NOT NULL DEFAULT 1 COMMENT '1=下单,2=支付,3=发货',

-- 供应商配置
supplier_id BIGINT NOT NULL DEFAULT 0,
sync_strategy INT NOT NULL DEFAULT 0 COMMENT '1=定时,2=实时,3=推送',
sync_interval INT NOT NULL DEFAULT 300 COMMENT '同步间隔(秒)',

-- 风控配置
oversell_allowed TINYINT NOT NULL DEFAULT 0,
low_stock_threshold INT NOT NULL DEFAULT 100,

UNIQUE KEY uk_item_sku (item_id, sku_id)
);

3.2 核心库存表(inventory)

所有品类共用一张库存表,通过不同字段组合适配不同场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE TABLE inventory (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
item_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL,
batch_id BIGINT NOT NULL DEFAULT 0 COMMENT '批次(券码制)',
calendar_date DATE DEFAULT NULL COMMENT '日期(时间维度)',

-- 核心库存字段
total_stock INT NOT NULL DEFAULT 0 COMMENT '总库存',
available_stock INT NOT NULL DEFAULT 0 COMMENT '可售库存',
booking_stock INT NOT NULL DEFAULT 0 COMMENT '预订(已下单未支付)',
locked_stock INT NOT NULL DEFAULT 0 COMMENT '锁定(营销活动)',
sold_stock INT NOT NULL DEFAULT 0 COMMENT '已售',

-- 供应商同步
supplier_stock INT NOT NULL DEFAULT 0,
supplier_sync_time BIGINT NOT NULL DEFAULT 0,

status INT NOT NULL DEFAULT 1 COMMENT '1=正常,2=缺货,3=停售',

UNIQUE KEY uk_sku_batch_date (sku_id, batch_id, calendar_date)
);

库存恒等式

1
total_stock = available_stock + booking_stock + locked_stock + sold_stock

可售库存计算(不同管理类型计算方式不同):

1
2
3
4
5
6
7
8
9
10
11
func CalcAvailable(inv *Inventory, cfg *Config) int32 {
switch cfg.ManagementType {
case SelfManaged:
return inv.TotalStock - inv.SoldStock - inv.BookingStock - inv.LockedStock
case SupplierManaged:
return inv.SupplierStock - inv.BookingStock - inv.LockedStock
case Unlimited:
return 999999
}
return 0
}

3.3 券码池表(inventory_code_pool,分 100 张表)

仅用于券码制商品(Deal、Giftcard 预采购模式):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
CREATE TABLE inventory_code_pool_00 (
id BIGINT PRIMARY KEY COMMENT '雪花算法',
item_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL,
batch_id BIGINT NOT NULL,

code VARCHAR(255) NOT NULL COMMENT '券码(唯一)',
serial_number VARCHAR(255) DEFAULT '' COMMENT '序列号/PIN',
code_url VARCHAR(500) DEFAULT '' COMMENT '兑换链接',

status INT NOT NULL DEFAULT 1 COMMENT '1=可用,2=预订,3=已售,4=已核销,5=退款,6=过期',
order_id BIGINT NOT NULL DEFAULT 0,

booking_time BIGINT DEFAULT 0,
purchase_time BIGINT DEFAULT 0,
expire_time BIGINT DEFAULT 0,

UNIQUE KEY uk_code (code),
KEY idx_status (status)
);
-- 分表规则:item_id % 100

3.4 库存操作日志表(inventory_operation_log)

所有库存变更留痕,用于对账和审计:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CREATE TABLE inventory_operation_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
item_id BIGINT NOT NULL,
sku_id BIGINT NOT NULL,
operation_type VARCHAR(50) NOT NULL COMMENT 'book/unbook/sell/refund/lock/unlock',
quantity INT NOT NULL,
order_id BIGINT NOT NULL DEFAULT 0,
before_available INT NOT NULL DEFAULT 0,
after_available INT NOT NULL DEFAULT 0,
create_time BIGINT NOT NULL DEFAULT 0,

KEY idx_order_id (order_id),
KEY idx_create_time (create_time)
);

四、策略模式:核心架构

4.1 整体架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌─────────────────────────────────────────────────┐
│ 业务层 (Order Service / Promotion Service) │
└──────────────────┬──────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ 统一库存管理器 (InventoryManager) │
│ BookStock / UnbookStock / SellStock / Refund │
└──────────────────┬──────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ 策略路由器 (StrategyRouter) │
│ 根据 inventory_config 选择策略 │
├────────┬────────┬────────┬──────────────────────┤
│ Self │Supplier│Unlimit │Estimated │
│Managed │Managed │Strategy│Strategy │
│Strategy│Strategy│ │ │
└────┬───┴────┬───┴────┬───┴──────────────────────┘
▼ ▼ ▼
┌─────────┐┌─────────┐┌──────────────┐
│ Redis ││ MySQL ││ Kafka Events │
│ (Hot) ││ (Cold) ││ (Async) │
└─────────┘└─────────┘└──────────────┘

4.2 策略接口定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// InventoryStrategy 库存管理策略接口
type InventoryStrategy interface {
CheckStock(ctx context.Context, req *CheckStockReq) (*CheckStockResp, error)
BookStock(ctx context.Context, req *BookStockReq) (*BookStockResp, error)
UnbookStock(ctx context.Context, req *UnbookStockReq) error
SellStock(ctx context.Context, req *SellStockReq) error
RefundStock(ctx context.Context, req *RefundStockReq) error
}

// StrategyFactory 策略工厂
func GetStrategy(mgmtType int) InventoryStrategy {
switch mgmtType {
case SelfManaged:
return &SelfManagedStrategy{}
case SupplierManaged:
return &SupplierManagedStrategy{}
case Unlimited:
return &UnlimitedStrategy{}
default:
return &UnlimitedStrategy{}
}
}

五、自管理策略:券码制(Deal / Giftcard)

5.1 Redis 存储结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Key:   inventory:code:pool:{itemID}:{skuID}:{batchID}
Type: LIST
Value: [codeID_1, codeID_2, codeID_3, ...]
说明: 券码池,LPOP 出货,RPUSH 补货/退还

Key: inventory:code:cursor:{itemID}:{skuID}:{batchID}
Type: STRING
Value: "lastCodeID:lockCount"
说明: 补货游标,记录上次补到哪里

Key: inventory:empty:{itemID}:{skuID}:{batchID}
Type: STRING (TTL 1h)
Value: "1"
说明: 库存空标志,避免重复查库

5.2 核心流程:出货 + 补货

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
用户下单


1. 检查库存空标志 ──── 命中 → 返回缺货
│ 未命中

2. 从 Redis LIST 原子出货 (Lua: LRANGE + LTRIM)

├── 出货成功 → 步骤 4

└── 库存不足 → 3. 补货 (从 MySQL 查可用券码 → RPUSH 到 Redis)

├── 补货成功 → 再次出货 → 步骤 4
└── DB 也无库存 → 设置空标志(1h) → 返回缺货


4. 更新 MySQL 券码状态: AVAILABLE → BOOKING (绑定 order_id)


5. 同步更新 MySQL inventory 表: booking_stock += quantity


6. 发送 Kafka 事件 (异步)

出货 Lua 脚本(原子性保证):

1
2
3
4
-- 原子取出 N 个券码
local result = redis.call('LRANGE', KEYS[1], 0, ARGV[1] - 1)
redis.call('LTRIM', KEYS[1], ARGV[1], -1)
return result

补货流程(加分布式锁防并发):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func (s *SelfManagedStrategy) replenish(ctx context.Context, itemID, skuID, batchID uint64) error {
// 1. 获取分布式锁(10s 超时)
lockKey := fmt.Sprintf("inventory:lock:replenish:%d:%d:%d", itemID, skuID, batchID)
if !acquireLock(lockKey, 10*time.Second) {
return nil // 其他进程正在补货,等待即可
}
defer releaseLock(lockKey)

// 2. 读取补货游标(上次补到哪个 codeID)
lastCodeID := getCursor(itemID, skuID, batchID)

// 3. 从 MySQL 查 3000 个可用券码
codes, err := db.Query(`
SELECT id FROM inventory_code_pool_xx
WHERE item_id=? AND sku_id=? AND batch_id=? AND status=1 AND id > ?
ORDER BY id LIMIT 3000
`, itemID, skuID, batchID, lastCodeID)

if len(codes) == 0 {
// DB 也无库存,设置空标志
redis.Set(emptyKey, "1", 1*time.Hour)
return ErrStockNotEnough
}

// 4. 原子写入 Redis LIST + 更新游标
redis.Eval(replenishScript, stockKey, cursorKey, codeIDs, newCursor)
return nil
}

六、自管理策略:数量制(OPV / 本地服务)

6.1 Redis 存储结构

1
2
3
4
5
6
7
8
Key:   inventory:qty:stock:{itemID}:{skuID}
Type: HASH
Fields:
"available" : 10000 # 可售库存
"booking" : 50 # Shopee 预订中
"issued" : 5000 # 已售
"locked" : 500 # 营销锁定
"{promotionID}": 200 # 营销活动独立库存(动态字段)

6.2 预订 Lua 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
local key = KEYS[1]
local book_num = tonumber(ARGV[1])
local promotion_id = ARGV[2] -- 空字符串表示普通库存

-- 1. 获取可用库存
local available = tonumber(redis.call('HGET', key, 'available') or 0)

-- 2. 如果有营销活动,合并计算
local promo_stock = 0
if promotion_id ~= '' then
promo_stock = tonumber(redis.call('HGET', key, promotion_id) or 0)
end
local total_available = available + promo_stock

-- 3. 检查库存
if book_num > total_available then
return -1 -- 库存不足
end

-- 4. 优先扣营销库存,不足时扣普通库存
if promo_stock > 0 then
if book_num <= promo_stock then
redis.call('HINCRBY', key, promotion_id, -book_num)
else
redis.call('HSET', key, promotion_id, 0)
redis.call('HINCRBY', key, 'available', -(book_num - promo_stock))
end
else
redis.call('HINCRBY', key, 'available', -book_num)
end

-- 5. 增加预订数
redis.call('HINCRBY', key, 'booking', book_num)

return total_available - book_num

6.3 支付成功 / 取消订单

1
2
3
4
5
6
7
8
9
-- 支付成功:booking → issued
local booking = tonumber(redis.call('HGET', key, 'booking') or 0)
if stock > booking then return -1 end -- 异常保护
redis.call('HINCRBY', key, 'booking', -stock)
redis.call('HINCRBY', key, 'issued', stock)

-- 取消订单:booking → available
redis.call('HINCRBY', key, 'booking', -stock)
redis.call('HINCRBY', key, 'available', stock)

七、供应商管理策略(酒店 / 机票)

7.1 同步策略

策略 适用场景 实时性 实现方式
实时查询 库存变化快(机票) 每次请求调供应商 API(30s 缓存)
定时同步 库存变化中等(酒店) 定时任务每 5 分钟拉取
Webhook 供应商主动推送 接收推送更新本地缓存

7.2 实时查询流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func (s *SupplierManagedStrategy) CheckStock(ctx context.Context, req *CheckStockReq) (*CheckStockResp, error) {
// 1. 查 Redis 缓存(30s TTL)
cacheKey := fmt.Sprintf("inventory:supplier:%d:%d:%s", req.ItemID, req.SKUID, req.Date)
if stock, err := redis.Get(cacheKey).Int(); err == nil {
return &CheckStockResp{Available: stock, FromCache: true}, nil
}

// 2. 缓存未命中,调供应商 API
resp, err := supplierClient.QueryStock(ctx, req.SupplierID, req.ProductID, req.Date)
if err != nil {
return nil, err
}

// 3. 写入 Redis 缓存(30s)+ 异步写快照表
redis.Set(cacheKey, resp.Stock, 30*time.Second)
go saveSnapshot(req.ItemID, resp.Stock, "api")

return &CheckStockResp{Available: resp.Stock, FromCache: false}, nil
}

7.3 预订流程(供应商管理)

7.3.1 同步预订(理想情况)

供应商 API 质量好,预订接口同步返回结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (s *SupplierManagedStrategy) BookStock(ctx context.Context, req *BookStockReq) (*BookStockResp, error) {
// 1. 调供应商预订接口(同步返回成功/失败)
resp, err := supplierClient.Book(ctx, req.SupplierID, req.ProductID, req.OrderID)
if err != nil {
return nil, err
}

// 2. 保存供应商订单号映射
saveOrderMapping(req.OrderID, resp.SupplierOrderID)

// 3. 更新本地库存表(记录 booking)
updateInventoryBooking(req.ItemID, req.SKUID, req.Quantity, +1)

// 4. 发送事件
publishEvent(&InventoryEvent{Type: "book", OrderID: req.OrderID, SupplierOrderID: resp.SupplierOrderID})

return &BookStockResp{Success: true, SupplierOrderID: resp.SupplierOrderID}, nil
}

7.3.2 异步预订(供应商系统较差)

场景:部分供应商系统不稳定,预订流程为:

  1. 创建 booking 单 → 立即返回 booking_id(状态 PENDING
  2. 轮询查询 booking 状态 → 最终返回 CONFIRMED / FAILED
  3. 只有 CONFIRMED 后才能继续下单

挑战

  • 用户不能等待轮询完成(可能需要 10-30 秒)。
  • 需要异步处理 + 状态机 + 补偿机制。

状态机设计

1
2
3
4
5
6
7
8
9
10
11
12
13
用户下单

BOOKING_INIT (初始化)

调供应商创建 booking → 返回 booking_id

BOOKING_PENDING (等待确认)

异步轮询 booking 状态(每 2s 查询一次,最多 30s)

├─ CONFIRMED → BOOKING_SUCCESS
├─ FAILED → BOOKING_FAILED (释放库存)
└─ TIMEOUT → BOOKING_TIMEOUT (人工介入)

数据库表设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CREATE TABLE supplier_booking (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL COMMENT '平台订单ID',
item_id BIGINT NOT NULL,
supplier_id BIGINT NOT NULL,

booking_id VARCHAR(100) NOT NULL COMMENT '供应商 booking ID',
booking_status VARCHAR(50) NOT NULL COMMENT 'PENDING/CONFIRMED/FAILED/TIMEOUT',

create_time BIGINT NOT NULL,
confirm_time BIGINT DEFAULT 0,
query_count INT DEFAULT 0 COMMENT '轮询次数',
last_query_time BIGINT DEFAULT 0,

error_msg TEXT,

KEY idx_order_id (order_id),
KEY idx_booking_id (booking_id),
KEY idx_status_time (booking_status, create_time)
);

实现流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
// 1. 用户下单时:创建 booking 单,立即返回"处理中"
func (s *SupplierManagedStrategy) BookStock(ctx context.Context, req *BookStockReq) (*BookStockResp, error) {
// 调供应商创建 booking
resp, err := supplierClient.CreateBooking(ctx, req.SupplierID, req.ProductID)
if err != nil {
return nil, err
}

// 保存 booking 记录(状态 PENDING)
saveSupplierBooking(&SupplierBooking{
OrderID: req.OrderID,
ItemID: req.ItemID,
SupplierID: req.SupplierID,
BookingID: resp.BookingID,
BookingStatus: "PENDING",
CreateTime: time.Now().Unix(),
})

// 发送到 MQ 异步轮询
publishToMQ(&BookingPollTask{
OrderID: req.OrderID,
BookingID: resp.BookingID,
SupplierID: req.SupplierID,
})

// 立即返回给用户(告知"预订处理中")
return &BookStockResp{
Success: false, // 尚未确认
Status: "PROCESSING",
BookingID: resp.BookingID,
EstimateTime: 30, // 预计 30 秒内确认
}, nil
}

// 2. 异步 Consumer:轮询 booking 状态
func PollBookingStatus(task *BookingPollTask) {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()

timeout := time.After(30 * time.Second)
queryCount := 0

for {
select {
case <-ticker.C:
queryCount++

// 调供应商查询接口
status, err := supplierClient.QueryBookingStatus(task.SupplierID, task.BookingID)

updateQueryRecord(task.OrderID, queryCount, time.Now().Unix())

if err != nil {
log.Error("query booking failed", err)
continue
}

switch status {
case "CONFIRMED":
// 预订成功
handleBookingSuccess(task.OrderID, task.BookingID)
return

case "FAILED":
// 预订失败
handleBookingFailed(task.OrderID, task.BookingID, "supplier rejected")
return

case "PENDING":
// 继续等待
continue
}

case <-timeout:
// 超时未确认
handleBookingTimeout(task.OrderID, task.BookingID)
return
}
}
}

// 3. 预订成功回调
func handleBookingSuccess(orderID uint64, bookingID string) {
// 更新状态
updateSupplierBooking(orderID, "CONFIRMED", time.Now().Unix())

// 更新本地库存
updateInventoryBooking(orderID, +1)

// 通知用户(Push / SMS / Email)
notifyUser(orderID, "您的订单预订成功,请尽快支付")

// 设置支付超时(15 分钟)
setPaymentTimeout(orderID, 15*time.Minute)
}

// 4. 预订失败回调
func handleBookingFailed(orderID uint64, bookingID string, reason string) {
updateSupplierBooking(orderID, "FAILED", time.Now().Unix())

// 释放本地库存(如果有预扣)
releaseInventoryBooking(orderID)

// 关闭订单
closeOrder(orderID, "supplier booking failed: " + reason)

// 通知用户
notifyUser(orderID, "抱歉,预订失败,请重新下单")
}

// 5. 预订超时回调
func handleBookingTimeout(orderID uint64, bookingID string) {
updateSupplierBooking(orderID, "TIMEOUT", time.Now().Unix())

// 记录异常,人工介入
alert("Booking timeout: order=%d, booking=%s", orderID, bookingID)

// 继续在后台轮询(降低频率:每 1 分钟查询一次,最多 24 小时)
scheduleBackgroundPoll(orderID, bookingID)

// 暂不关闭订单,等待人工处理
}

7.3.3 用户体验优化

问题:用户下单后看到”预订处理中”,体验不佳。

优化方案

  1. 前端轮询展示进度
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 用户下单后,前端每 2 秒轮询订单状态
function pollOrderStatus(orderId) {
const interval = setInterval(async () => {
const resp = await fetch(`/api/order/${orderId}/status`);
const data = await resp.json();

if (data.status === 'BOOKING_SUCCESS') {
showMessage('预订成功!请在 15 分钟内完成支付');
clearInterval(interval);
redirectToPayment(orderId);
} else if (data.status === 'BOOKING_FAILED') {
showMessage('预订失败,库存不足');
clearInterval(interval);
} else {
// 继续等待
updateProgress(data.queryCount, 15); // 进度条:已查询 X/15 次
}
}, 2000);

// 30 秒后停止轮询
setTimeout(() => clearInterval(interval), 30000);
}
  1. WebSocket / SSE 推送
1
2
3
4
5
6
7
8
9
10
11
12
13
// 服务端:booking 确认后推送消息
func handleBookingSuccess(orderID uint64) {
// ... 更新状态 ...

// 推送给前端
websocketHub.Push(orderID, &Message{
Type: "BOOKING_CONFIRMED",
Data: map[string]interface{}{
"order_id": orderID,
"status": "SUCCESS",
},
})
}
  1. 短信/Push 通知
1
2
// 预订成功后 1 分钟内发送通知
notifyUser(orderID, "您的【泰国普吉岛酒店】预订成功,请尽快支付")

7.3.4 异常场景处理

场景 1:轮询期间用户取消订单

1
2
3
4
5
6
7
8
9
10
11
12
13
func PollBookingStatus(task *BookingPollTask) {
for {
// 每次轮询前检查订单状态
order := getOrder(task.OrderID)
if order.Status == "CANCELLED" {
// 调供应商取消接口
supplierClient.CancelBooking(task.SupplierID, task.BookingID)
return
}

// ... 继续轮询 ...
}
}

场景 2:供应商 API 持续超时

1
2
3
4
5
6
7
8
// 连续 3 次查询超时 → 降级到人工处理
if queryCount >= 3 && allTimeout {
handleBookingTimeout(task.OrderID, task.BookingID)

// 发送企业微信/钉钉告警
alertOps("供应商 API 异常: supplier_id=%d, booking_id=%s",
task.SupplierID, task.BookingID)
}

场景 3:供应商确认后用户未支付

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// booking 成功后设置 15 分钟支付超时
func handleBookingSuccess(orderID uint64, bookingID string) {
// ... 更新状态 ...

// 15 分钟后自动取消
scheduleTask(&CancelBookingTask{
OrderID: orderID,
BookingID: bookingID,
Delay: 15 * time.Minute,
})
}

// 超时后调供应商取消接口
func CancelExpiredBooking(task *CancelBookingTask) {
order := getOrder(task.OrderID)
if order.Status != "PAID" {
// 调供应商取消
supplierClient.CancelBooking(order.SupplierID, task.BookingID)

// 关闭订单
closeOrder(task.OrderID, "payment timeout")
}
}

7.3.5 监控指标

指标 阈值 说明
booking 成功率 > 95% 供应商库存准确性
平均确认时长 < 10s P99 < 30s
超时率 < 1% 需要人工介入的比例
取消率 < 5% 用户等待期间取消订单
1
2
3
4
// Prometheus Metrics
bookingConfirmDuration := prometheus.NewHistogram(...)
bookingSuccessRate := prometheus.NewCounter(...)
bookingTimeoutCount := prometheus.NewCounter(...)

八、无限库存策略(TopUp / 保险)

最简单的策略,只记录操作日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type UnlimitedStrategy struct{}

func (s *UnlimitedStrategy) CheckStock(ctx context.Context, req *CheckStockReq) (*CheckStockResp, error) {
return &CheckStockResp{Available: 999999, IsUnlimited: true}, nil
}

func (s *UnlimitedStrategy) BookStock(ctx context.Context, req *BookStockReq) (*BookStockResp, error) {
// 仅记录日志(用于统计销量)
logOperation("book", req.ItemID, req.Quantity, req.OrderID)
return &BookStockResp{Success: true}, nil
}

func (s *UnlimitedStrategy) UnbookStock(ctx context.Context, req *UnbookStockReq) error {
return nil // 无操作
}

九、核心流程汇总

9.1 统一预订流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
用户下单


1. 查 inventory_config → 获取 management_type + unit_type


2. StrategyFactory.GetStrategy(management_type)

├─ Self + Code → 券码出货 (Redis LIST LPOP)
├─ Self + Quantity → 数量扣减 (Redis HASH Lua 原子)
├─ Supplier + Time → 调供应商预订 API
├─ Unlimited → 直接成功
└─ Self + Bundle → 遍历子项,逐一扣减


3. 更新 inventory 表: booking_stock += quantity


4. 发送 Kafka 事件 → 异步消费写操作日志


5. 返回结果(券码制返回 codeIDs,供应商返回 supplierOrderID)

9.2 支付成功流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
支付回调


1. 路由到对应策略

├─ 券码制: code status BOOKING → SOLD, 设置 purchase_time/expire_time
├─ 数量制: Redis booking--, sold++
├─ 供应商: 调供应商确认接口(可选)
└─ Giftcard(实时生成): 调供应商 API 生成卡密 → 保存到 code_pool


2. 更新 inventory: booking_stock -= qty, sold_stock += qty


3. 发送事件

9.3 取消/超时释放流程

1
2
3
4
5
6
7
8
9
10
11
订单取消 / 超时未支付


1. 路由到对应策略

├─ 券码制: code status BOOKING → AVAILABLE, RPUSH 回 Redis LIST
├─ 数量制: Redis booking--, available++
└─ 供应商: 调供应商取消接口


2. 更新 inventory: booking_stock -= qty, available_stock += qty

十、数据一致性保障

10.1 Redis 与 MySQL 双写策略

操作 Redis MySQL 一致性保障
预订 (Book) 同步扣减(Lua 原子) Kafka 异步更新 最终一致
支付 (Sell) 同步更新 Kafka 异步更新 最终一致
营销锁定 (Lock) 同步 同步(DB 事务) 强一致
补货 (Replenish) 同步写入 不变 -

核心原则

  • Redis 是热路径:所有高频读写走 Redis,保证毫秒级响应。
  • MySQL 是权威数据源:故障恢复以 MySQL 为准。
  • Kafka 异步持久化:Book/Sell 等操作通过 MQ 异步落库,不阻塞主流程。

10.2 定时对账(每小时)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func Reconcile() {
configs := queryAllSelfManagedConfigs()
for _, cfg := range configs {
redisStock := getRedisAvailable(cfg.ItemID, cfg.SKUID)
mysqlStock := getMySQLAvailable(cfg.ItemID, cfg.SKUID)
diff := redisStock - mysqlStock

// 恒等式校验:total = available + booking + locked + sold
mysqlTotal := getMySQLTotal(cfg.ItemID, cfg.SKUID)
mysqlCalc := mysqlStock + mysqlBooking + mysqlLocked + mysqlSold
if mysqlCalc != mysqlTotal {
alert("MySQL 数据不一致: item=%d, total=%d, calc=%d", cfg.ItemID, mysqlTotal, mysqlCalc)
}

// Redis vs MySQL 差异
if abs(diff) > 100 || abs(diff) > mysqlStock/10 {
alert("库存差异过大: item=%d, redis=%d, mysql=%d", cfg.ItemID, redisStock, mysqlStock)
}

// 自动修复(可选,以 MySQL 为准)
if cfg.AutoReconcile {
syncRedisFromMySQL(cfg.ItemID, cfg.SKUID)
}
}
}

10.3 降级方案

1
2
3
4
5
6
7
8
9
10
Redis 可用 → 正常读写 Redis

Redis 不可用


降级到 MySQL 直接操作(性能下降但业务不中断)

├─ 券码制: SELECT ... FOR UPDATE + 状态更新
├─ 数量制: UPDATE available_stock = available_stock - ? WHERE available_stock >= ?
└─ 记录降级日志,Redis 恢复后全量同步

十一、Kafka 事件设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
message InventoryEvent {
string event_id = 1; // UUID
string event_type = 2; // book/unbook/sell/refund/lock/sync
int64 timestamp = 3;

int64 item_id = 10;
int64 sku_id = 11;
int64 batch_id = 12;
string calendar_date = 13; // 时间维度库存

int32 quantity = 20;
repeated int64 code_ids = 21; // 券码制

int64 order_id = 30;
string supplier_order_id = 31;

int32 before_available = 40; // 操作前快照
int32 after_available = 41; // 操作后快照
}

Topic 设计

  • inventory.book — 预订
  • inventory.unbook — 释放
  • inventory.sell — 售出
  • inventory.refund — 退款
  • inventory.sync — 供应商同步

十二、Giftcard 特殊设计

Giftcard 横跨三种库存模式,是统一模型的最佳验证:

模式 管理类型 流程 适用场景
预采购卡密 Self + Code 批量导入 → Redis 出货 高频热销卡
实时生成 Supplier + Code 支付成功 → 调 API 生成 → 存入 code_pool 长尾低频卡
无限库存 Unlimited 直接成功 供应商保证库存

卡密安全

  • 存储时 AES-256 加密卡号和 PIN。
  • 管理后台脱敏显示(XXXX-XXXX-XXXX-1234)。
  • 所有访问记录审计日志。

供应商 API 超时处理

  • 支付成功后异步生成,完成后推送通知用户。
  • 指数退避重试(1s, 2s, 4s),3 次失败后人工补发。

十三、监控与告警

13.1 关键指标

指标 阈值 告警级别
超卖次数 > 0 P0
Redis vs MySQL 差异 > 100 P1
库存服务错误率 > 1% P1
库存扣减 P99 > 200ms P2
补货失败率 > 5% P2
供应商同步延迟 > 10min P2
低库存商品数 > 100 P3

13.2 Prometheus Metrics

1
2
3
4
5
6
7
8
9
10
11
# 操作计数
inventory_operation_total{op="book|sell|refund", mgmt="self|supplier", status="ok|fail"}

# 操作延迟
inventory_operation_duration_seconds{op="book|sell"}

# 库存差异
inventory_reconcile_diff{item_id, sku_id}

# 缺货次数
inventory_out_of_stock_total{item_id}

十四、新品类接入指南

三步接入

  1. 评估分类:确定 (ManagementType, UnitType, DeductTiming)
  2. 写配置:在 inventory_config 表插入一条记录。
  3. 调接口:使用统一 InventoryManager.BookStock() 即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 示例:接入新品类"演唱会门票"
// 1. 评估:供应商管理 + 时间维度 + 支付成功扣减
// 2. 写配置
INSERT INTO inventory_config (item_id, management_type, unit_type, deduct_timing, supplier_id, sync_strategy)
VALUES (900001, 2, 3, 2, 700001, 2);

// 3. 调用统一接口
inventoryManager.BookStock(ctx, &BookStockReq{
ItemID: 900001,
SKUID: 0,
Quantity: 2,
OrderID: orderID,
CalendarDate: "2025-08-15",
})

十五、生产环境实战数据

15.1 业务规模

指标 数值 说明
秒杀峰值 QPS 20,000 单个爆款商品,持续 5-10 分钟
日均 QPS 50 常态流量
日均订单量 2,000,000 支付成功订单
日均库存扣减 6,700,000 含预订、支付、取消等操作
峰值/日均比 870:1 流量极度不均匀

容量规划推算

1
2
3
4
5
6
7
日均订单 2M / 86400s ≈ 23 TPS
秒杀峰值 20k QPS = 日均的 870 倍

假设订单转化率 30%(下单 → 支付成功)
日均扣减请求 = 2M / 0.3 ≈ 6.7M 次
Kafka 异步落库 MySQL TPS = 6.7M / 86400 ≈ 80 TPS(日均)
秒杀峰值 MySQL TPS ≈ 300-500 TPS(批量写入优化后)

15.2 集群配置

Redis 集群

1
2
3
4
拓扑: Redis Cluster (3 主 3 从)
分片: 按 item_id Hash 分片
单分片配置: 32GB 内存, 16 核
持久化: AOF + RDB 混合模式

容量规划

  • 券码池:100 万张券码 × 8 字节 ≈ 8 MB(单商品)
  • 热点商品预热:10 个商品 × 8MB = 80 MB
  • 数量制商品:1 万个 SKU × 1 KB ≈ 10 MB
  • 总计:**< 200 MB**(核心数据),32GB 绰绰有余

应用服务

1
2
3
4
实例数: 10 台 (Kubernetes Pod)
单实例配置: 4 核 8GB
线程池: 每实例 500 线程(IO 密集型,2N 配置)
单实例承载: 2,000 QPS(秒杀峰值)

为什么 10 台能抗 2w QPS?

  • Redis 操作 RT < 5ms,单线程 QPS = 1000/5 = 200
  • 500 线程 × 200 QPS = 100k QPS 理论上限(实际 2k QPS,留足余量)

MySQL 集群

1
2
3
4
架构: 1 主 2 从(半同步复制)
主库配置: 16 核 64GB, SSD
从库: 读流量(对账、报表)
分表: inventory_code_pool 分 100 张表

容量规划

  • 券码池:1 亿张券码 × 500 字节 ≈ 50 GB(分 100 张表,单表 500 MB)
  • inventory 表:10 万条记录 × 1 KB ≈ 100 MB
  • operation_log:日增 670 万条 × 200 字节 ≈ 1.3 GB/天(保留 30 天 ≈ 40 GB)

Kafka 集群

1
2
3
Broker: 3 台
Topic: inventory.events (6 分区)
消费者组: 6 个 Consumer(并发消费)

吞吐量验证

  • 秒杀峰值写入 Kafka: 20k TPS × 500 字节 = 10 MB/s
  • Kafka 单分区吞吐 > 50 MB/s,6 分区 = 300 MB/s 理论上限
  • 实际使用 **< 5%**,非常充裕

15.3 性能指标实测

操作 P50 P99 P999 备注
券码制预订 15ms 50ms 150ms 含 Redis + MySQL 同步更新
数量制预订 8ms 30ms 100ms 仅 Redis Lua 脚本
供应商库存查询 200ms 500ms 2s 第三方 API,30s 缓存
Redis 单次操作 1ms 5ms 10ms LIST/HASH 操作
MySQL 券码状态更新 10ms 50ms 200ms 主库写入
Kafka 异步消费延迟 50ms 200ms 1s 非秒杀场景

秒杀场景优化后

  • 券码提前预热到 Redis(活动前 1 小时)
  • P99 降至 30ms(无 DB 补货开销)

15.4 真实案例与优化

案例 1:秒杀 2w QPS 热点 Key 瓶颈

问题

  • 单个爆款商品,所有请求打到同一个 Redis Key。
  • Redis 单线程模型,QPS 上限 10 万(理论值),但 网卡带宽 先打满。
  • 实测单 Key 极限 5 万 QPS(1KB 数据 × 5w = 50 MB/s,接近千兆网卡上限)。

解决方案

  1. **本地缓存 (Caffeine)**:

    • 应用层缓存库存数(非强一致,允许轻微超卖)。
    • 本地缓存拦截 80% 读请求,Redis 只承担 4k QPS
  2. Key 分散(适用于读多写少):

    • 将热点 Key 复制 10 份:stock:item_123:0 ~ stock:item_123:9
    • 读请求随机路由,写请求同步更新所有副本。
  3. 限流前置

    • 网关层按 item_id 限流,单商品最大 2.5w QPS(留 20% 余量)。
    • 超出部分直接返回”繁忙”,避免击穿 Redis。

案例 2:券码补货锁超时

问题

  • 补货时加分布式锁(10s 超时),从 MySQL 查 3000 张券码。
  • DB 慢查询导致补货耗时 12s,锁提前过期。
  • 另一个进程拿到锁,重复补货,导致 券码重复出货

根因

  • MySQL inventory_code_pool_xx 表数据量大(千万级),status=1 索引选择性差。
  • 执行计划走了全表扫描。

解决方案

  1. 优化 SQL

    1
    2
    3
    4
    5
    6
    7
    -- 增加复合索引
    KEY idx_item_status_id (item_id, status, id)

    -- 查询改为游标分页
    SELECT id FROM inventory_code_pool_xx
    WHERE item_id=? AND status=1 AND id > ?
    ORDER BY id LIMIT 3000

    耗时从 12s 降至 50ms

  2. 锁续期

    • 补货时启动守护线程,每 5s 检查锁是否需要续期。
    • 避免长事务导致锁过期。
  3. 异步补货

    • 检测库存低于阈值(1000 张)时,提前异步补货
    • 避免用户请求阻塞在补货逻辑。

案例 3:Kafka 消费积压

问题

  • 秒杀活动结束后,Kafka 积压 50 万条消息(2.5 万 QPS × 20s)。
  • 6 个 Consumer 消费速度跟不上,MySQL 写入成为瓶颈。

瓶颈分析

  • Consumer 逐条更新 MySQL:UPDATE inventory SET booking_stock = booking_stock + 1
  • MySQL 单线程提交,TPS < 5000(主从半同步复制延迟)。

解决方案

  1. 批量写入

    1
    2
    3
    // 攒批 100 条,批量 INSERT
    INSERT INTO inventory_operation_log (item_id, operation_type, quantity, ...)
    VALUES (?, ?, ?), (?, ?, ?), ... -- 100 rows

    TPS 从 5k 提升至 8 万(提升 16 倍)。

  2. 降低一致性要求

    • inventory_operation_log 日志表改为异步从库写入
    • 主库只更新 inventory 核心表。
  3. 削峰

    • Kafka 设置 linger.ms=100ms,Producer 端攒批发送。
    • 减少消息数量。

案例 4:对账发现的典型问题

统计数据(3 个月):

  • 对账次数:2160 次(每小时 1 次)
  • 发现差异:87 次(4% 频率)
  • 差异 > 100:3 次(严重)

主要根因

原因 占比 说明
Kafka 消费延迟 60% 秒杀后消费积压,MySQL 未及时更新
Redis 补货未同步 MySQL 25% 券码补货只更新 Redis,DB 未记录
人工后台操作 10% 运营手动修改 DB 库存
Redis 重启丢数据 5% AOF 未及时刷盘(appendfsync everysec

优化措施

  1. Kafka 消费延迟告警:lag > 1000 立即告警。
  2. Redis 补货同步:补货时同步更新 MySQL total_stock
  3. 后台操作审计:所有库存修改必须通过 API,禁止直接改 DB。
  4. Redis 持久化增强:改为 appendfsync always(性能下降 30%,换取强一致)。

15.5 成本分析

资源 配置 数量 月成本(美元)
Redis Cluster 32GB × 6 节点 1 套 $800
MySQL 64GB 主库 + 32GB × 2 从库 1 套 $1,200
应用服务 4C8G Pod 10 台 $600
Kafka 8C16G Broker 3 台 $900
总计 - - $3,500/月

日均订单成本:$3,500 / 2,000,000 = $0.00175/单(0.175 美分)


15.6 核心设计决策

决策 选择 原因
统一 vs 独立 统一模型 + 策略模式 复用逻辑,新品类零代码接入
Redis vs MySQL Redis 优先,MySQL 持久化 高并发性能 + 数据可靠
同步 vs 异步 扣减同步,落库异步 热路径极速,冷路径可靠
券码出货方式 Lazy Loading(按需补货) 节省内存,避免一次性加载全量
对账策略 每小时自动对账,MySQL 为准 兜底一致性
降级策略 Redis 宕机切 MySQL 性能下降 10 倍,但业务不中断

15.7 业界对比

维度 淘宝/京东 Amazon 本设计
库存单元 SKU 数量 ASIN + FBA SKU + 批次/日期
扣减时机 下单预订 支付成功 可配置
虚拟商品 部分支持 完善 核心场景
时间维度 不支持 不支持 支持
券码管理 部分 完善 核心能力
供应商集成 少量 FBA 模式 多策略
峰值 QPS 100 万+ 50 万+ 2 万(中型平台)

系列导航
库存与价格在下单时的协作流程,详见(一)全景概览与领域划分中的 C 端用户旅程章节。

速查导航

阅读时间: 45 分钟 | 难度: ⭐⭐⭐⭐ | 面试频率: 中高

核心考点速查:


前言:怎样的系统算是稳定高可用的

首先回答另一个问题,怎样的系统算是稳定的?

Google SRE中(SRE三部曲[1])有一个层级模型来描述系统可靠性基础和高层次需求(Dickerson’s Hierarchy of Service Reliability),如下图:


该模型由Google SRE工程师Mikey Dickerson在2013年提出,将系统稳定性需求按照基础程度进行了不同层次的体系化区分,形成稳定性标准金字塔模型:

  • 金字塔的底座是监控(Monitoring),这是一个系统对于稳定性最基础的要求,缺少监控的系统,如同蒙上眼睛狂奔的野马,无从谈及可控性,更遑论稳定性。
  • 更上层是应急响应(Incident Response),从一个问题被监控发现到最终解决,这期间的耗时直接取决于应急响应机制的成熟度。合理的应急策略能保证当故障发生时,所有问题能得到有序且妥善的处理,而不是慌乱成一锅粥。
  • 事后总结以及根因分析(Postmortem&Root Caue Analysis),即我们平时谈到的“复盘”,虽然很多人都不太喜欢这项活动,但是不得不承认这是避免我们下次犯同样错误的最有效手段,只有当摸清故障的根因以及对应的缺陷,我们才能对症下药,合理进行规避。
  • 测试和发布管控(Testing&Release procedures),大大小小的应用都离不开不断的变更与发布,有效的测试与发布策略能保障系统所有新增变量都处于可控稳定区间内,从而达到整体服务终态稳定
  • 容量规划(Capacity Planning)则是针对于这方面变化进行的保障策略。现有系统体量是否足够支撑新的流量需求,整体链路上是否存在不对等的薄弱节点,都是容量规划需要考虑的问题。
  • 位于金字塔模型最顶端的是产品设计(Product)与软件研发(Development),即通过优秀的产品设计与软件设计使系统具备更高的可靠性,构建高可用产品架构体系,从而提升用户体验

系统稳定性建设概述


从金字塔模型我们可以看到构建维护一个高可用服务所需要做到的几方面工作:

  • 产品、技术、架构的设计,高可用的架构体系
  • 系统链路&业务策略梳理和维护(System & Biz Profiling)
  • 容量规划(Capacity Planning)
  • 应急响应(Incident Response)
  • 测试
  • 事后总结(Testing & Postmortem)
  • 监控(Monitoring)
  • 资损体系
  • 风控体系
  • 大促保障
  • 性能优化


监控&告警梳理 – Monitoring

站在监控的角度看,我们的系统从上到下一般可以分为三层:业务(Biz)、应用(Application)、系统(System)。系统层为最下层基础,表示操作系统相关状态;应用层为JVM层,涵盖主应用进程与中间件运行状态;业务层为最上层,为业务视角下服务对外运行状态。因此进行大促稳定性监控梳理时,可以先脱离现有监控,先从核心、资损链路开始,按照业务、应用(中间件、JVM、DB)、系统三个层次梳理需要哪些监控,再从根据这些索引找到对应的监控告警,如果不存在,则相应补上;如果存在则检查阈值、时间、告警人是否合理。

监控

监控系统一般有四项黄金指标:延时(Latency), 错误(Error),流量(Traffic), 饱和度(Situation),各层的关键性监控同样也可以按照这四项指标来进行归类,具体如下:


告警

是不是每项监控都需要告警?答案当然是否定的。建议优先设置Biz层告警,因为Biz层我们对外服务最直观业务表现,最贴切用户感受。Application&System层指标主要用于监控,部分关键&高风险指标可设置告警,用于问题排查定位以及故障提前发现。对于一项告警,我们一般需要关注级别、阈值、通知人等几个点。

  1. 级别
    即当前告警被触发时,问题的严重程度,一般来说有几个衡量点:
  • 是否关联NOC
  • 是否产生严重业务影响
  • 是否产生资损
  1. 阈值
  • 即一项告警的触发条件&时间,需根据具体场景合理制定。一般遵循以下原则:
  • 不可过于迟钝。一个合理的监控体系中,任何异常发生后都应触发相关告警。
  • 不可过于敏感。过于敏感的阈值会造成频繁告警,从而导致响应人员疲劳应对,无法筛选真实异常。若一个告警频繁出现,一般是两个原因:系统设计不合理 or 阈值设置不合理。
  • 若单一指标无法反馈覆盖整体业务场景,可结合多项指标关联构建。
  • 需符合业务波动曲线,不同时段可设置不同条件&通知策略。
  1. 通知人&方式
  • 若为业务指标异常(Biz层告警),通知人应为问题处理人员(开发、运维同学)与业务关注人员(TL、业务同学)的集合,通知方式较为实时,比如电话通知。
  • 若为应用 & 系统层告警,主要用于定位异常原因,通知人设置问题排查处理人员即可,通知方式可考虑钉钉、短信等低干扰方式。
  • 除了关联层次,对于不同级别的告警,通知人范围也可适当扩大,尤其是关联GOC故障的告警指标,应适当放宽范围,通知方式也应更为实时直接

应产出数据

完成该项梳理工作后,我们应该产出以下数据:

  1. 系统监控模型,格式同表1
  • Biz、Application、System 分别存在哪些待监控点
  • 监控点是否已全部存在指标,仍有哪些待补充
  1. 系统告警模型列表,需包含以下数据
  • 关联监控指标(链接)
  • 告警关键级别
  • 是否推送GOC
  • 是否产生资损
  • 是否关联故障
  • 是否关联预案
  1. 业务指标大盘,包含Biz层重点监控指标数据
  2. 系统&应用指标大盘,包含核心系统关键系统指标,可用于白盒监控定位问题。

高可用的架构设计

高可用架构是系统稳定性的基石。一个好的架构设计能够从根本上减少故障发生的概率,并在故障发生时将影响范围降到最低。

高可用架构核心原则

1. 冗余设计(Redundancy)

冗余是实现高可用的基础手段,通过部署多个相同的组件来消除单点故障。

冗余层级 实现方式 典型场景
应用层冗余 多实例部署 + 负载均衡 Web 服务、API 网关
数据层冗余 主从复制、多副本 MySQL 主从、Redis Cluster
机房冗余 同城双活、异地多活 核心交易系统
网络冗余 多运营商、多链路 CDN、DNS

冗余度计算
假设单节点可用性为 99%,双节点冗余后可用性 = 1 - (1-0.99)² = 99.99%

2. 解耦设计(Decoupling)

1
2
3
4
5
6
7
8
9
10
紧耦合架构                    解耦架构
┌─────────────────┐ ┌─────────────────┐
│ 服务 A │ │ 服务 A │
│ ↓ │ │ ↓ │
│ 服务 B │ → │ 消息队列 │
│ ↓ │ │ ↓ │
│ 服务 C │ │ 服务 B/C │
└─────────────────┘ └─────────────────┘
任一服务故障 服务故障不传导
导致整体不可用 其他服务正常运行

解耦手段

  • 异步化:同步调用改为消息队列异步处理
  • 服务化:单体拆分为微服务,故障隔离
  • 接口抽象:依赖抽象而非实现,便于替换

3. 隔离设计(Isolation)

隔离维度 实现方式 目的
线程池隔离 核心业务独立线程池 防止非核心业务拖垮核心链路
进程隔离 独立部署、容器化 资源隔离,故障不传导
集群隔离 按业务/用户分集群 大客户/VIP 用户独立资源
机房隔离 单元化部署 机房级故障隔离

4. 无状态设计(Stateless)

无状态服务是水平扩展和快速故障恢复的前提。

1
2
3
4
5
6
7
8
9
// 有状态设计(不推荐)
type OrderService struct {
userSession map[string]*Session // 状态存在内存
}

// 无状态设计(推荐)
type OrderService struct {
sessionStore SessionStore // 状态外置到 Redis
}

状态外置方案

  • Session → Redis/Memcached
  • 文件上传 → 对象存储(S3/OSS)
  • 定时任务状态 → 数据库/ZooKeeper

高可用架构模式

1. 主从模式(Primary-Secondary)

  • 适用场景:数据库、缓存、消息队列
  • 故障切换:主节点故障后,从节点提升为主节点
  • 数据一致性:同步复制(强一致)vs 异步复制(最终一致)
1
2
3
4
5
6
7
8
9
10
11
12
13
MySQL 主从架构:
┌──────────┐ Binlog同步 ┌──────────┐
│ Master │ ───────────────→ │ Slave1 │
│ (写) │ │ (读) │
└──────────┘ └──────────┘
│ ↑
│ Binlog同步 │
└──────────────────────────────┘

┌──────────┐
│ Slave2 │
│ (读) │
└──────────┘

2. 集群模式(Cluster)

  • 对等集群:所有节点地位相等,如 Redis Cluster
  • 分片集群:数据按规则分布到不同节点,如 Elasticsearch
1
2
3
4
5
6
7
8
9
10
11
12
13
Redis Cluster 架构(16384 槽位):
┌─────────────────────────────────────────────┐
│ 槽位 0-5460 槽位 5461-10922 槽位 10923-16383 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Node 1 │ │ Node 2 │ │ Node 3 │ │
│ │ Master │ │ Master │ │ Master │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ │
│ │ │ │ │
│ ┌────▼────┐ ┌────▼────┐ ┌────▼────┐ │
│ │ Node 1' │ │ Node 2' │ │ Node 3' │ │
│ │ Slave │ │ Slave │ │ Slave │ │
│ └─────────┘ └─────────┘ └─────────┘ │
└─────────────────────────────────────────────┘

3. 同城双活架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
             ┌─────────────┐
│ 用户请求 │
└──────┬──────┘

┌──────▼──────┐
│ DNS/GSLB │
└──────┬──────┘
┌────────────┴────────────┐
│ │
┌──────▼──────┐ ┌──────▼──────┐
│ 机房 A │ │ 机房 B │
│ ┌────────┐ │ │ ┌────────┐ │
│ │ App │ │ │ │ App │ │
│ └───┬────┘ │ │ └───┬────┘ │
│ ┌───▼────┐ │ 同步 │ ┌───▼────┐ │
│ │ MySQL │◄├──────────┤►│ MySQL │ │
│ │ Master │ │ │ │ Slave │ │
│ └────────┘ │ │ └────────┘ │
└─────────────┘ └─────────────┘

关键设计点

  • 流量调度:DNS/GSLB 按比例分配流量
  • 数据同步:MySQL 半同步复制,延迟 < 100ms
  • 故障切换:机房 A 故障时,流量全部切到机房 B

4. 异地多活架构

异地多活是最复杂也是可用性最高的架构模式,核心挑战是数据一致性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
      北京                      上海                     广州
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 单元 Cell1 │ │ 单元 Cell2 │ │ 单元 Cell3 │
│ ┌─────────┐ │ │ ┌─────────┐ │ │ ┌─────────┐ │
│ │ App │ │ │ │ App │ │ │ │ App │ │
│ └────┬────┘ │ │ └────┬────┘ │ │ └────┬────┘ │
│ ┌────▼────┐ │ │ ┌────▼────┐ │ │ ┌────▼────┐ │
│ │ MySQL │ │◄────────►│ │ MySQL │ │◄───────►│ │ MySQL │ │
│ └─────────┘ │ DTS同步 │ └─────────┘ │ DTS同步 │ └─────────┘ │
└─────────────┘ └─────────────┘ └─────────────┘
│ │ │
└────────────────────────┼───────────────────────┘

┌────────▼────────┐
│ 全局路由层 │
│ (按用户ID路由) │
└─────────────────┘

单元化设计要点

  • 分片键选择:通常按用户 ID 分片,保证同一用户请求落到同一单元
  • 全局服务:库存、支付等需要全局一致的服务单独部署
  • 数据同步:DTS/Canal 异步同步,解决跨单元数据查询

高可用保护机制

1. 限流(Rate Limiting)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 令牌桶限流器实现
type TokenBucket struct {
rate float64 // 令牌生成速率
capacity float64 // 桶容量
tokens float64 // 当前令牌数
lastUpdate time.Time
mu sync.Mutex
}

func (tb *TokenBucket) Allow() bool {
tb.mu.Lock()
defer tb.mu.Unlock()

now := time.Now()
elapsed := now.Sub(tb.lastUpdate).Seconds()
tb.tokens = math.Min(tb.capacity, tb.tokens+elapsed*tb.rate)
tb.lastUpdate = now

if tb.tokens >= 1 {
tb.tokens--
return true
}
return false
}

2. 熔断(Circuit Breaker)

熔断器三状态:关闭(正常)→ 打开(熔断)→ 半开(探测)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
                错误率 > 阈值
┌─────────┐ ──────────────────→ ┌─────────┐
│ 关闭 │ │ 打开 │
│ CLOSED │ ←────────────────── │ OPEN │
└─────────┘ 探测成功 └────┬────┘
↑ │
│ 超时后 │
│ ┌─────────────────────────────┘
│ │
│ ▼
┌────┴────────┐
│ 半开 │
│ HALF-OPEN │
└─────────────┘

3. 降级(Degradation)

降级类型 触发条件 降级策略
自动降级 错误率/RT 超阈值 返回缓存数据或默认值
手动降级 大促前/故障时 关闭非核心功能
读降级 主库压力大 读走从库或缓存
写降级 下游不可用 写入 MQ 异步重试

4. 超时控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 超时控制最佳实践
func CallDownstream(ctx context.Context) error {
// 1. 设置合理超时
ctx, cancel := context.WithTimeout(ctx, 500*time.Millisecond)
defer cancel()

// 2. 调用下游
result, err := downstream.Call(ctx, req)

// 3. 超时处理
if ctx.Err() == context.DeadlineExceeded {
// 降级逻辑
return getFromCache()
}
return result, err
}

超时设置原则

  • 上游超时 > 下游超时(避免上游已超时,下游还在执行)
  • 预留 Buffer:下游 RT P99 + 20%
  • 级联超时:A→B→C,A 超时 = B 超时 + C 超时 + Buffer

系统链路梳理和维护 System & Biz Profiling

系统链路梳理是所有保障工作的基础,如同对整体应用系统进行一次全面体检,从流量入口开始,按照链路轨迹,逐级分层节点,得到系统全局画像与核心保障点。

入口梳理盘点

一个系统往往存在十几个甚至更多流量入口,包含HTTP、RPC、消息等都多种来源。如果无法覆盖所有所有链路,可以从以下三类入口开始进行梳理:

  • 核心重保流量入口
    • 用户承诺服务SLI较高,对数据准确性、服务响应时间、可靠度具有明确要求。
    • 业务核心链路,浏览、下单、支付、履约
    • 面向企业级用户
  • 资损事件对应入口
    • 关联到公司资金收入或者客户资金收入收费服务
  • 大流量入口
    • 系统TPS&QPS TOP5~10
    • 该类入口虽然不涉及较高SLI与资损要求,但是流量较高,对整体系统负载有较大影响

节点分层判断

对于复杂场景可以做节点分层判断

流量入口就如同线团中的线头,挑出线头后就可按照流量轨迹对链路上的节点(HSF\DB\Tair\HBase等一切外部依赖)按照依赖程度、可用性、可靠性进行初级分层区分。

  1. 强弱依赖节点判断
  • 若节点不可用,链路业务逻辑被中断 or 高级别有损(存在一定耐受阈值),则为业务强依赖;反之为弱依赖。
  • 若节点不可用,链路执行逻辑被中断(return error),则为系统强依赖;反之为弱依赖。
  • 若节点不可用,系统性能受影响,则为系统强依赖;反之为弱依赖。
  • 按照快速失败设计逻辑,该类节点不应存在,但是在不变更应用代码前提下,如果出现该类节点,应作为强依赖看待。
  • 若节点无感可降级 or 存在业务轻微损伤替换方案,则为弱依赖。
  1. 低可用依赖节点判断
  • 节点服务日常超时严重
  • 节点对应系统资源不足
  1. 高风险节点判断
  • 上次大促后,节点存在大版本系统改造
  • 新上线未经历过大促的节点
  • 节点对应系统是否曾经出现高级别故障
  • 节点故障后存在资损风险

应产出数据

  • 识别核心接口(流程)调用拓扑图或者时序图(借用分布式链路追踪系统获得调用拓扑图)
  • 调用比
  • 识别资损风险
  • 识别内外部依赖

完成该项梳理工作后,我们应该产出以下数据:对应业务域所有核心链路分析,技术&业务强依赖、核心上游、下游系统、资损风险应明确标注。

业务策略&容量规划 Capacity Planning - 容量规划

业务策略

不同于高可用系统建设体系,大促稳定性保障体系与面向特定业务活动的针对性保障建设,因此,业务策略与数据是我们进行保障前不可或缺的数据。
一般大促业务数据可分为两类,全局业务形态评估以及应急策略&玩法。

全局评估

该类数据从可以帮助我们进行精准流量评估、峰值预测、大促人力排班等等,一般包含下面几类:

  • 业务量预估体量(日常X倍)
  • 预估峰值日期
  • 大促业务时长(XX日-XX日)
  • 业务场景预估流量分配

应急策略

  • 该类数据指相较于往年大促活动,本次大促业务变量,可用于应急响应预案与高风险节点评估等,一般包含下面两类:
  • 特殊业务玩法

容量规划的本质是追求计算风险最小化和计算成本最小化之间的平衡,只追求任意其一都不是合理的。为了达到这两者的最佳平衡点,需尽量精准计算系统峰值负载流量,再将流量根据单点资源负载上限换算成相应容量,得到最终容量规划模型。

流量模型评估

  1. 入口流量

对于一次大促,系统峰值入口流量一般由常规业务流量与非常规增量(比如容灾预案&业务营销策略变化带来的流量模型配比变化)叠加拟合而成。

  • 常规业务流量一般有两类计算方式:
    • 历史流量算法:该类算法假设当年大促增幅完全符合历史流量模型,根据当前&历年日常流量,计算整体业务体量同比增量模型;然后根据历年大促-日常对比,计算预估流量环比增量模型;最后二者拟合得到最终评估数据。
    • 由于计算时无需依赖任何业务信息输入,该类算法可用于保障工作初期业务尚未给出业务总量评估时使用,得到初估业务流量。
    • 业务量-流量转化算法(GMV\DAU\订单量):该类算法一般以业务预估总量(GMV\DAU\订单量)为输入,根据历史大促&日常业务量-流量转化模型(比如经典漏洞模型)换算得到对应子域业务体量评估。- 该种方式强依赖业务总量预估,可在保障工作中后期使用,在初估业务流量基础上纳入业务评估因素考虑。
  • 非常规增量一般指前台业务营销策略变更或系统应急预案执行后流量模型变化造成的增量流量。例如,NA61机房故障时,流量100%切换到NA62后,带来的增量变化.考虑到成本最小化,非常规增量P计算时一般无需与常规业务流量W一起,全量纳入叠加入口流量K,一般会将非常规策略发生概率λ作为权重
  1. 节点流量
    节点流量由入口流量根据流量分支模型,按比例转化而来。分支流量模型以系统链路为计算基础,遵循以下原则:
  • 同一入口,不同链路占比流量独立计算。
  • 针对同一链路上同一节点,若存在多次调用,需计算按倍数同比放大(比如DB\Tair等)。
  • DB写流量重点关注,可能出现热点造成DB HANG死。

容量转化

节点容量是指一个节点在运行过程中,能够同时处理的最大请求数。它反映了系统的瞬时负载能力。

1)Little Law衍生法则
不同类型资源节点(应用容器、Tair、DB、HBASE等)流量-容量转化比各不相同,但都服从Little Law衍生法则,即:
节点容量=节点吞吐率×平均响应时间

2)N + X 冗余原则

在满足目标流量所需要的最小容量基础上,冗余保留X单位冗余能力
X与目标成本与资源节点故障概率成正相关,不可用概率越高,X越高
对于一般应用容器集群,可考虑X = 0.2N

全链路压测(TODO)

  • 上述法则只能用于容量初估(大促压测前&新依赖),最终精准系统容量还是需要结合系统周期性压力测试得出。

应产出数据

  • 基于模型评估的入口流量模型 & 集群自身容量转化结果(若为非入口应用,则为限流点梳理)。
  • 基于链路梳理的分支流量模型 & 外部依赖容量转化结果。

大促保障

Incident Response - 紧急&前置预案梳理

要想在大促高并发流量场景下快速对线上紧急事故进行响应处理,仅仅依赖值班同学临场发挥是远远不够的。争分夺秒的情况下,无法给处理人员留有充足的策略思考空间,而错误的处理决策,往往会导致更为失控严重的业务&系统影响。因此,要想在大促现场快速而正确的响应问题,值班同学需要做的是选择题(Which),而不是陈述题(What)。而选项的构成,便是我们的业务&系统预案。从执行时机与解决问题属性来划分,预案可分为技术应急预案、技术前置预案、业务应急预案、业务前置预案等四大类。结合之前的链路梳理和业务评估结果,我们可以快速分析出链路中需要的预案,遵循以下原则:

  • 技术应急预案:该类预案用于处理系统链路中,某层次节点不可用的情况,例如技术/业务强依赖、弱稳定性、高风险等节点不可用等异常场景。
  • 技术前置预案:该类预案用于平衡整体系统风险与单节点服务可用性,通过熔断等策略保障全局服务可靠。例如弱稳定性&弱依赖服务提前降级、与峰值流量时间冲突的离线任务提前暂定等。
  • 业务应急预案:该类预案用于应对业务变更等非系统性异常带来的需应急处理问题,例如业务数据错误(数据正确性敏感节点)、务策略调整(配合业务应急策略)等
  • 业务前置预案:该类预案用于配和业务全局策略进行的前置服务调整(非系统性需求)

应产出数据

完成该项梳理工作后,我们应该产出以下数据:

  • 执行&关闭时间(前置预案)
  • 触发阈值(紧急预案,须关联相关告警)
  • 关联影响(系统&业务)
  • 决策&执行&验证人员
  • 开启验证方式
  • 关闭阈值(紧急预案)
  • 关闭验证方式

阶段性产出-全链路作战地图

进行完上述几项保障工作,我们基本可得到全局链路作战地图,包含链路分支流量模型、强弱依赖节点、资损评估、对应预案&处理策略等信息。大促期间可凭借该地图快速从全局视角查看应急事件相关影响,同时也可根据地图反向评估预案、容量等梳理是否完善合理。

Incident Response - 作战手册梳理

作战手册是整个大促保障的行动依据,贯穿于整个大促生命周期,可从事前、事中、事后三个阶段展开考虑。整体梳理应本着精准化、精细化的原则,理想状态下,即便是对业务、系统不熟悉的轮班同学,凭借手册也能快速响应处理线上问题。
事前
1)前置检查事项清单

  • 大促前必须执行事项checklist,通常包含以下事项:
  • 集群机器重启 or 手动FGC
  • 影子表数据清理
  • 检查上下游机器权限
  • 检查限流值
  • 检查机器开关一致性
  • 检查数据库配置
  • 检查中间件容量、配置(DB\缓存\NoSQL等)
  • 检查监控有效性(业务大盘、技术大盘、核心告警)
  • 每个事项都需包含具体执行人、检查方案、检查结果三列数据
    2)前置预案
  • 域内所有业务&技术前置预案。

事中

  1. 紧急技术&业务预案
    需要包含的内容基本同前置预案,差异点如下:
  • 执行条件&恢复条件:具体触发阈值,对应监控告警项。
  • 通知决策人。
  1. 应急工具&脚本
    常见故障排查方式、核心告警止血方式(强弱依赖不可用等),业务相关日志捞取脚本等。
  2. 告警&大盘
  • 应包含业务、系统集群及中间件告警监控梳理结果,核心业务以及系统大盘,对应日志数据源明细等数据:
  • 日志数据源明细:数据源名称、文件位置、样例、切分格式。
  • 业务、系统集群及中间件告警监控梳理结果:关联监控指标(链接)、告警关键级别、是否推送GOC、是否产生资损、是否关联故障、是否关联预案。
  • 核心业务&系统大盘:大盘地址、包含指标明细(含义、是否关联告警、对应日志)。
  1. 上下游机器分组
  • 应包含核心系统、上下游系统,在不同机房、单元集群分组、应用名,可用于事前-机器权限检查、事中-应急问题排查黑屏处理。
  1. 值班注意事项
  • 包含每班轮班同学值班必做事项、应急变更流程、核心大盘链接等。
  1. 核心播报指标
  • 包含核心系统&服务指标(CPU\LOAD\RT)、业务关注指标等,每项指标应明确具体监控地址、采集方式。
  1. 域内&关联域人员通讯录、值班
  • 包含域内技术、TL、业务方对应排班情况、联系方式(电话),相关上下游、基础组件(DB、中间件等)对应值班情况。
  1. 值班问题记录
  • 作战记录,记录工单、业务问题、预案(前置\紧急)(至少包含:时间、问题描述(截图)、影响分析、决策&解决过程等)。值班同学在值班结束前,进行记录。
    事后
  1. 系统恢复设置事项清单(限流、缩容)
    一般与事前检查事项清单对应,包含限流阈值调整、集群缩容等大促后恢复操作。
  2. 大促问题复盘记录
  • 应包含大促遇到的核心事件总结梳理。

沙盘推演和演练 Incident Response

实战沙盘演练是应急响应方面的最后一项保障工作,以历史真实故障CASE作为应急场景输入,模拟大促期间紧急状况,旨在考验值班同学们对应急问题处理的响应情况。
一般来说,一个线上问题从发现到解决,中间需要经历定位&排查&诊断&修复等过程,总体遵循以下几点原则:

  • 尽最大可能让系统先恢复服务,同时为根源调查保护现场(机器、日志、水位记录)。
  • 避免盲目搜索,依据白盒监控针对性诊断定位。
  • 有序分工,各司其职,避免一窝蜂失控乱象。
  • 依据现场情况实时评估影响范围,实在无法通过技术手段挽救的情况(例如强依赖不可用),转化为业务问题思考(影响范围、程度、是否有资损、如何协同业务方)。
  • 沙盘演练旨在检验值班同学故障处理能力,着重关注止血策略、分工安排、问题定位等三个方面:
    国际化中台双11买家域演练
    根据故障类型,常见止血策略有以下解决思路:
  • 入口限流:调低对应Provider服务来源限流值
  • 应对突发流量过高导致自身系统、下游强依赖负载被打满。
  • 下游降级:降级对应下游服务
  • 下游弱依赖不可用。
  • 下游业务强依赖经业务同意后降级(业务部分有损)。
  • 单点失败移除:摘除不可用节点
  • 单机水位飙高时,先下线不可用单机服务(无需下线机器,保留现场)。
  • 应对集群单点不可用、性能差。
  • 切换:单元切流或者切换备份

应对单库或某单元依赖因为自身原因(宿主机或网络),造成局部流量成功率下跌下跌。
Google SRE中,对于紧急事故管理有以下几点要素:

  • 嵌套式职责分离,即分确的职能分工安排
  • 控制中心\作战室
  • 实时事故状态文档
  • 明确公开的职责交接
  • 其中嵌套式职责分离,即分确的职能分工安排,达到各司其职,有序处理的效果,一般可分为下列几个角色:
    事故总控:负责协调分工以及未分配事务兜底工作,掌握全局概要信息,一般为PM/TL担任。
    事务处理团队:事故真正处理人员,可根据具体业务场景&系统特性分为多个小团队。团队内部存在域内负责人,与事故总控人员进行沟通。
    发言人:事故对外联络人员,负责对事故处理内部成员以及外部关注人员信息做周期性信息同步,同时需要实时维护更新事故文档。
    规划负责人:负责外部持续性支持工作,比如当大型故障出现,多轮排班轮转时,负责组织职责交接记录

资损体系

资损(资金损失)是互联网业务中最严重的故障类型之一,尤其在金融、电商、支付等领域。资损防控体系需要覆盖事前预防、事中发现、事后止血的全生命周期。

资损场景分类

资损类型 场景举例 影响
多扣 重复扣款、金额计算错误 用户资金损失,客诉赔付
少扣 优惠券超发、折扣计算错误 公司资金损失
错付 付款到错误账户 资金追回困难
漏扣 订单状态异常导致未收款 应收账款损失
数据不一致 账务与实际资金不符 财务风险

事前:资损风险识别与预防

1. 资损风险点识别

通过链路梳理识别所有资损风险点:

1
2
3
4
5
6
7
8
订单链路资损风险点:
┌─────────────────────────────────────────────────────────┐
│ 下单 → 计价 → 优惠 → 支付 → 扣库存 → 发货 → 结算 │
│ │ │ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ ▼ ▼ │
│ 重复 价格 优惠券 重复 超卖 错发 结算 │
│ 下单 篡改 超发 扣款 漏发 金额错 │
└─────────────────────────────────────────────────────────┘

2. 资损防控 Checklist

检查项 防控措施 责任人
接口幂等 唯一索引 + 状态机 开发
金额计算 Decimal 类型,禁用浮点 开发
参数校验 金额上下限、业务规则校验 开发
权限控制 敏感操作二次确认 产品
配置变更 双人复核、灰度发布 运维
代码审查 资损相关代码强制 Review Tech Lead

3. 防重设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 幂等性设计 - 数据库唯一索引
CREATE UNIQUE INDEX uk_order_pay ON payment_records (order_id, pay_type);

// 幂等性设计 - 状态机控制
func ProcessPayment(orderID string) error {
// 1. 加分布式锁
lock := redis.Lock(fmt.Sprintf("pay:%s", orderID), 30*time.Second)
if !lock.Acquire() {
return ErrProcessing
}
defer lock.Release()

// 2. 检查状态
order := db.GetOrder(orderID)
if order.Status != OrderStatusUnpaid {
return ErrAlreadyPaid // 幂等返回
}

// 3. 执行支付
if err := doPayment(order); err != nil {
return err
}

// 4. 更新状态(乐观锁)
affected := db.Exec(`
UPDATE orders SET status = 'PAID'
WHERE id = ? AND status = 'UNPAID'
`, orderID)

if affected == 0 {
return ErrConcurrentModify
}
return nil
}

事中:实时资损发现

1. 实时对账系统


【得物技术】浅谈资损防控

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
实时对账架构:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 交易系统 │ │ 支付系统 │ │ 账务系统 │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────────┐
│ Kafka │
└───────────────────────────┬──────────────────────────┘

┌──────▼──────┐
│ Flink 实时 │
│ 对账引擎 │
└──────┬──────┘

┌─────────────┼─────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 告警 │ │ 拦截 │ │ 报表 │
└─────────┘ └─────────┘ └─────────┘

2. 对账规则引擎

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 对账规则定义
type ReconcileRule struct {
Name string
LeftSource string // 左数据源(如:订单表)
RightSource string // 右数据源(如:支付表)
JoinKey string // 关联键
CheckItems []CheckItem
}

type CheckItem struct {
LeftField string
RightField string
Operator string // =, !=, >, <, range
Threshold float64 // 允许误差
}

// 示例:订单金额与支付金额一致性校验
var orderPaymentRule = ReconcileRule{
Name: "订单支付金额一致性",
LeftSource: "orders",
RightSource: "payments",
JoinKey: "order_id",
CheckItems: []CheckItem{
{LeftField: "total_amount", RightField: "pay_amount", Operator: "=", Threshold: 0},
{LeftField: "status", RightField: "status", Operator: "mapping"},
},
}

3. 异常告警分级

告警级别 触发条件 处理方式
P0 紧急 金额差异 > 10万 或 差异笔数 > 100 立即电话 + 自动熔断
P1 严重 金额差异 > 1万 或 差异笔数 > 10 钉钉 + 短信
P2 一般 金额差异 > 1000 或 差异笔数 > 1 钉钉通知
P3 提醒 存在差异记录 日报汇总

事后:止血与复盘

1. 资损止血 SOP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
资损事件处理流程:
┌────────────────────────────────────────────────────────┐
│ 1. 发现告警(< 1 min) │
│ ↓ │
│ 2. 确认资损(< 5 min) │
│ - 查看监控大盘 │
│ - 确认影响范围 │
│ ↓ │
│ 3. 止血操作(< 10 min) │
│ - 关闭入口(下单/支付) │
│ - 下线问题节点 │
│ - 回滚变更 │
│ ↓ │
│ 4. 影响评估(< 30 min) │
│ - 统计资损金额 │
│ - 统计影响用户数 │
│ ↓ │
│ 5. 数据修复(视情况) │
│ - 自动修复脚本 │
│ - 人工核对修复 │
│ ↓ │
│ 6. 复盘总结(24h 内) │
│ - 根因分析 │
│ - 改进措施 │
└────────────────────────────────────────────────────────┘

2. 资损修复策略

场景 修复策略 注意事项
多扣用户 原路退款 + 补偿 需财务审批
少扣用户 评估是否追缴 金额较小可放弃
优惠券超发 标记失效 + 用户通知 已使用的认亏
数据不一致 以权威数据源为准修复 保留修复日志

资损防控度量指标

指标 计算方式 目标值
资损发现率 发现资损笔数 / 实际资损笔数 > 99%
发现时效 从发生到发现的平均时间 < 5 分钟
止血时效 从发现到止血的平均时间 < 10 分钟
修复率 已修复金额 / 资损总金额 > 95%
资损率 资损金额 / 交易总金额 < 0.001%

参考学习

风控体系

风控体系是保障业务安全的重要防线,主要应对欺诈、薅羊毛、恶意攻击等风险场景。一个完善的风控体系需要覆盖事前、事中、事后全链路。

风控场景分类

风险类型 典型场景 影响
账户风险 盗号、撞库、虚假注册 用户资产损失、平台信誉受损
交易风险 刷单、套现、恶意退款 资金损失、数据失真
营销风险 薅羊毛、优惠券滥用、黄牛 营销成本浪费
内容风险 垃圾信息、违规内容 合规风险、用户体验差
信用风险 恶意欠款、信用违约 坏账损失

风控系统架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
                      ┌─────────────────┐
│ 业务系统 │
│ (订单/支付/营销) │
└────────┬────────┘
│ 风控请求
┌────────▼────────┐
│ 风控网关 │
│ (统一接入) │
└────────┬────────┘

┌─────────────────────┼─────────────────────┐
│ │ │
┌────────▼────────┐ ┌────────▼────────┐ ┌────────▼────────┐
│ 实时风控引擎 │ │ 准实时引擎 │ │ 离线分析 │
│ (毫秒级) │ │ (秒级) │ │ (小时/天级) │
│ │ │ │ │ │
│ - 规则引擎 │ │ - 特征计算 │ │ - 模型训练 │
│ - 模型推理 │ │ - 风险画像 │ │ - 策略挖掘 │
│ - 黑白名单 │ │ - 关联分析 │ │ - 报表统计 │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└─────────────────────┼─────────────────────┘

┌────────▼────────┐
│ 数据平台 │
│ (特征/画像/日志) │
└─────────────────┘

实时风控引擎设计

1. 规则引擎

规则引擎是风控系统的核心组件,支持灵活配置和动态更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 规则定义示例
type Rule struct {
ID string
Name string
Priority int
Conditions []Condition
Action Action
Enabled bool
}

type Condition struct {
Field string // 字段名
Operator string // 操作符: =, !=, >, <, in, regex
Value interface{} // 比较值
}

type Action struct {
Type string // PASS, REJECT, REVIEW, CHALLENGE
Score float64 // 风险分数
Reason string // 拒绝原因
}

// 示例规则:同一设备24小时内注册超过5个账号
var deviceRegisterRule = Rule{
Name: "设备注册频率限制",
Priority: 100,
Conditions: []Condition{
{Field: "event_type", Operator: "=", Value: "register"},
{Field: "device_register_count_24h", Operator: ">", Value: 5},
},
Action: Action{Type: "REJECT", Reason: "设备注册频率异常"},
}

2. 特征工程

特征类型 示例 计算方式
实时特征 用户当前IP、设备指纹 请求参数直接获取
滑动窗口特征 5分钟内登录次数 Redis ZSET 滑动窗口
累计特征 历史消费总额、注册天数 用户画像服务
关联特征 同设备关联账号数 图数据库查询
模型特征 欺诈概率分数 ML 模型实时推理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 滑动窗口特征计算
func GetLoginCount5Min(userID string) int {
key := fmt.Sprintf("login:count:%s", userID)
now := time.Now().Unix()
windowStart := now - 300 // 5分钟窗口

// 清理过期数据
redis.ZRemRangeByScore(key, "-inf", windowStart)

// 获取窗口内计数
count := redis.ZCount(key, windowStart, now)
return count
}

// 记录登录事件
func RecordLogin(userID string) {
key := fmt.Sprintf("login:count:%s", userID)
now := time.Now().Unix()
redis.ZAdd(key, now, fmt.Sprintf("%d", now))
redis.Expire(key, 10*time.Minute) // 双倍窗口 TTL
}

3. 机器学习模型

模型类型 适用场景 特点
规则模型 明确特征的欺诈行为 可解释性强,快速上线
有监督模型 有标签数据的场景 XGBoost、LightGBM
无监督模型 异常检测、新型欺诈 Isolation Forest
图模型 团伙欺诈、关联分析 GNN、社区发现

风控决策流程

1
2
3
4
5
用户请求 → 数据采集 → 特征计算 → 规则引擎 → 模型推理 → 决策聚合 → 返回结果
│ │ │
▼ ▼ ▼
规则命中 模型分数 最终决策
(布尔值) (0-100) PASS/REJECT/REVIEW

决策聚合策略

  • 串行策略:规则先行,命中则直接返回
  • 并行策略:规则 + 模型并行执行,综合评分
  • 熔断策略:风控服务超时/异常时,根据业务重要性决定放行或拒绝

风控运营

1. 处置动作

处置动作 适用场景 用户体验
直接通过 低风险用户 无感知
人工审核 中风险,需人工判断 延迟处理
验证码挑战 疑似机器行为 轻度打扰
短信验证 账户安全验证 中度打扰
直接拒绝 高风险/黑名单 阻断
静默观察 采集数据不阻断 无感知

2. 效果度量

指标 定义 目标
拦截率 拦截的风险事件 / 实际风险事件 > 95%
误杀率 误拦正常用户 / 正常用户总数 < 0.1%
响应时间 风控请求 RT P99 < 50ms
覆盖率 接入风控的业务场景比例 100%

3. 策略迭代

1
2
3
4
5
6
7
8
9
10
11
12
13
数据驱动的风控策略迭代闭环:
┌─────────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 日志采集 │ → │ 案例标注 │ → │ 特征分析 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ ↑ │ │
│ │ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 效果评估 │ ← │ 灰度发布 │ ← │ 策略优化 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
└─────────────────────────────────────────────────────┘

风控体系建设参考


性能优化

性能优化是稳定性建设的重要组成部分。高性能不仅带来更好的用户体验,也能在流量高峰时提供更大的容量 Buffer,降低系统压力。

性能优化方法论

1. 性能优化原则

  • 先测量,后优化:没有数据支撑的优化是盲目的
  • 抓大放小:优先优化瓶颈点,遵循 80/20 法则
  • 空间换时间:缓存、预计算、冗余存储
  • 串行改并行:利用多核、异步化
  • 减少 IO:批量处理、连接复用、数据压缩

2. 性能分析套路

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
性能问题定位流程:
┌─────────────────────────────────────────────────────┐
│ 1. 现象收集 │
│ - 接口 RT 多少?P99/P95/AVG? │
│ - CPU/内存/IO 占用? │
│ - 哪些时段出现? │
│ │
│ 2. 瓶颈定位 │
│ - 链路追踪:哪一跳耗时最长? │
│ - 火焰图:CPU 消耗在哪些函数? │
│ - 慢查询日志:哪些 SQL 耗时长? │
│ │
│ 3. 根因分析 │
│ - 是计算密集还是 IO 密集? │
│ - 是代码问题还是资源问题? │
│ - 是配置问题还是架构问题? │
│ │
│ 4. 方案制定与验证 │
│ - 制定优化方案 │
│ - 压测验证效果 │
│ - 灰度上线 │
└─────────────────────────────────────────────────────┘

各层性能优化策略

1. 网络层优化

优化点 方案 效果
减少 RTT CDN 加速、边缘计算 降低用户感知延迟
连接复用 HTTP/2、连接池 减少建连开销
数据压缩 Gzip、Brotli 减少传输数据量
协议优化 HTTP/3(QUIC) 减少队头阻塞

2. 应用层优化

a) 缓存优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
缓存层级:
┌─────────────────────────────────────────────────────┐
│ L1: 本地缓存(进程内) │
│ - 容量小、速度快(纳秒级) │
│ - 适合热点数据、配置数据 │
│ - 工具:Caffeine、Guava Cache │
│ │
│ L2: 分布式缓存 │
│ - 容量大、速度快(毫秒级) │
│ - 适合共享数据 │
│ - 工具:Redis、Memcached │
│ │
│ L3: 数据库 │
│ - 容量最大、速度较慢(10ms+) │
│ - 持久化存储 │
└─────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 多级缓存读取示例
func GetUserProfile(userID string) (*UserProfile, error) {
// L1: 本地缓存
if profile := localCache.Get(userID); profile != nil {
return profile.(*UserProfile), nil
}

// L2: Redis
profile, err := redis.Get(fmt.Sprintf("user:%s", userID))
if err == nil && profile != nil {
localCache.Set(userID, profile, 5*time.Minute)
return profile, nil
}

// L3: 数据库
profile, err = db.GetUserProfile(userID)
if err != nil {
return nil, err
}

// 回填缓存
redis.Set(fmt.Sprintf("user:%s", userID), profile, 30*time.Minute)
localCache.Set(userID, profile, 5*time.Minute)
return profile, nil
}

b) 并发优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 串行调用 → 并行调用
// Before: RT = T1 + T2 + T3 = 300ms
func GetOrderDetail_Before(orderID string) (*OrderDetail, error) {
order, _ := getOrder(orderID) // 100ms
user, _ := getUser(order.UserID) // 100ms
products, _ := getProducts(order.Items) // 100ms
return &OrderDetail{order, user, products}, nil
}

// After: RT = max(T1, T2, T3) = 100ms
func GetOrderDetail_After(ctx context.Context, orderID string) (*OrderDetail, error) {
var (
order *Order
user *User
products []*Product
)

g, ctx := errgroup.WithContext(ctx)

g.Go(func() error {
var err error
order, err = getOrder(orderID)
return err
})

g.Go(func() error {
var err error
user, err = getUser(order.UserID)
return err
})

g.Go(func() error {
var err error
products, err = getProducts(order.Items)
return err
})

if err := g.Wait(); err != nil {
return nil, err
}
return &OrderDetail{order, user, products}, nil
}

c) 批量处理

1
2
3
4
5
6
7
8
9
10
11
12
// 单条查询 → 批量查询
// Before: 100 次网络往返
for _, id := range userIDs {
user, _ := redis.Get(fmt.Sprintf("user:%s", id))
}

// After: 1 次网络往返
keys := make([]string, len(userIDs))
for i, id := range userIDs {
keys[i] = fmt.Sprintf("user:%s", id)
}
users, _ := redis.MGet(keys...)

3. 数据库优化

a) SQL 优化

问题 优化方案
无索引/索引失效 添加合适索引,避免索引失效场景
**SELECT *** 只查需要的字段
大事务 拆分小事务,减少锁持有时间
深分页 游标分页(WHERE id > last_id LIMIT 10
N+1 查询 JOIN 或批量查询

b) 索引优化原则

1
2
3
4
5
6
7
8
9
10
11
-- 联合索引最左前缀
CREATE INDEX idx_user_status_time ON orders(user_id, status, create_time);

-- 以下查询可以使用索引
SELECT * FROM orders WHERE user_id = 123;
SELECT * FROM orders WHERE user_id = 123 AND status = 'PAID';
SELECT * FROM orders WHERE user_id = 123 AND status = 'PAID' AND create_time > '2025-01-01';

-- 以下查询无法使用索引
SELECT * FROM orders WHERE status = 'PAID'; -- 缺少最左字段
SELECT * FROM orders WHERE user_id = 123 OR status = 'PAID'; -- OR 导致失效

c) 读写分离

1
2
3
4
5
6
7
8
9
10
11
12
13
14
             ┌─────────────┐
│ 应用服务 │
└──────┬──────┘

┌──────▼──────┐
│ 数据源路由 │
└──────┬──────┘
┌────────────┴────────────┐
│ 写操作 │ 读操作
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Master │ ──────→ │ Slave │
│ (写库) │ 同步 │ (读库) │
└─────────────┘ └─────────────┘

4. JVM/GC 优化(Java)

优化点 方案
堆内存 根据业务特点设置合理大小
GC 算法 G1/ZGC 替代 CMS
对象池 复用大对象,减少 GC 压力
逃逸分析 栈上分配,减少堆分配

5. Go 性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 1. sync.Pool 复用对象
var bufferPool = sync.Pool{
New: func() interface{} {
return make([]byte, 1024)
},
}

func ProcessRequest() {
buf := bufferPool.Get().([]byte)
defer bufferPool.Put(buf)
// 使用 buf
}

// 2. 减少内存分配
// Before: 每次拼接都分配新内存
func buildString_Before(items []string) string {
result := ""
for _, item := range items {
result += item
}
return result
}

// After: 预分配
func buildString_After(items []string) string {
var builder strings.Builder
builder.Grow(1024) // 预分配
for _, item := range items {
builder.WriteString(item)
}
return builder.String()
}

// 3. 避免 defer 在热路径
// defer 有一定开销,在极端性能敏感场景可手动管理

性能测试与监控

1. 压测类型

类型 目的 场景
基准测试 获取单接口性能基线 新接口上线前
负载测试 验证目标 QPS 下表现 常规压测
压力测试 找到系统极限 容量评估
稳定性测试 长时间运行是否稳定 内存泄漏排查
全链路压测 真实场景模拟 大促前

2. 性能指标

指标 含义 目标参考
QPS/TPS 每秒请求/事务数 根据业务目标
RT(P50/P95/P99) 响应时间分位值 P99 < 500ms
错误率 失败请求占比 < 0.1%
CPU 使用率 计算资源占用 < 70%(留 Buffer)
内存使用率 内存资源占用 < 80%
GC 频率/耗时 垃圾回收影响 Full GC < 1次/天

性能优化案例

案例:接口 RT 从 500ms 优化到 50ms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
问题:商品详情接口 P99 RT = 500ms

分析:
1. 链路追踪发现:
- 数据库查询:200ms
- 价格计算服务:150ms
- 库存服务:100ms
- 其他:50ms

2. 深入分析:
- 数据库:缺少索引,全表扫描
- 价格计算:串行调用 5 个下游
- 库存服务:无缓存,每次查 DB

优化方案:
1. 数据库添加索引:200ms → 10ms
2. 价格计算并行化:150ms → 50ms(max(5个下游))
3. 库存增加缓存:100ms → 5ms

优化后:P99 RT = 50ms(10倍提升)

SLI/SLO/SLA 体系

概念定义

概念 全称 定义
SLI Service Level Indicator 服务质量指标,可量化的度量值
SLO Service Level Objective 服务质量目标,SLI 的目标值
SLA Service Level Agreement 服务等级协议,对外承诺
1
2
关系:SLI(指标)→ SLO(目标)→ SLA(协议)
可用性 >99.9% 合同条款

常用 SLI 指标

指标类型 SLI 定义 SLO 示例
可用性 成功请求数 / 总请求数 > 99.9%
延迟 请求耗时分位值 P99 < 200ms
吞吐量 单位时间处理请求数 > 10000 QPS
正确性 返回正确结果的比例 > 99.99%
新鲜度 数据更新延迟 < 5 分钟

Error Budget(错误预算)

Error Budget 是 SRE 的核心概念,用于平衡可靠性与迭代速度。

1
2
3
4
5
6
7
8
Error Budget = 1 - SLO

示例:SLO = 99.9%
Error Budget = 0.1% = 每月约 43 分钟停机时间

如果本月已用 30 分钟:
- 剩余 13 分钟,可以继续发布
- 用完后应暂停非紧急发布,专注稳定性

Error Budget 策略

  • Budget 充足:允许激进发布,快速迭代
  • Budget 紧张:减缓发布节奏,增加测试
  • Budget 耗尽:冻结发布,全力修复

混沌工程

混沌工程是一种通过主动注入故障来验证系统韧性的实践方法。

混沌工程原则

  1. 建立稳态假设:定义系统正常运行的指标
  2. 多样化真实事件:模拟生产环境的各种故障
  3. 在生产环境运行:测试环境无法完全模拟生产
  4. 自动化持续运行:纳入 CI/CD 流水线
  5. 最小化爆炸半径:从小范围开始,逐步扩大

故障注入类型

故障类型 注入方式 验证目标
进程故障 Kill 进程、OOM 服务自愈、故障切换
网络故障 延迟、丢包、断网 超时处理、重试机制
依赖故障 Mock 下游返回错误 降级策略、熔断机制
资源耗尽 CPU 打满、磁盘填满 限流、告警
时钟偏移 NTP 故障模拟 时间敏感逻辑

混沌实验流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
┌─────────────────────────────────────────────────────┐
│ 1. 定义稳态 │
│ - 确定关键 SLI(可用性、RT) │
│ - 记录当前基线值 │
│ │
│ 2. 假设 │
│ - 注入 XX 故障后,系统应能在 XX 秒内恢复 │
│ │
│ 3. 设计实验 │
│ - 故障类型、注入时长 │
│ - 影响范围(单机/单机房/全局) │
│ - 回滚方案 │
│ │
│ 4. 执行实验 │
│ - 从最小范围开始 │
│ - 实时监控 SLI │
│ - 随时准备回滚 │
│ │
│ 5. 分析结果 │
│ - 系统表现是否符合预期? │
│ - 发现了哪些薄弱点? │
│ │
│ 6. 改进 │
│ - 修复发现的问题 │
│ - 扩大实验范围 │
└─────────────────────────────────────────────────────┘

混沌工程工具

工具 适用场景 特点
ChaosBlade 多平台通用 阿里开源,支持丰富故障场景
Chaos Monkey AWS/云环境 Netflix 开源,随机终止实例
LitmusChaos Kubernetes CNCF 项目,云原生
Gremlin 企业级 商业产品,功能全面

变更管理

变更是系统故障的主要来源。Google SRE 统计显示,70% 的故障与变更相关。

变更类型

变更类型 风险等级 审批要求
代码发布 CR + 灰度
配置变更 中-高 双人审批
DB Schema 变更 DBA 审批
基础设施变更 架构组审批
依赖升级 兼容性测试

安全变更原则

  1. 可回滚:任何变更必须有回滚方案
  2. 可观测:变更前后有明确的监控指标
  3. 渐进式:灰度发布,逐步放量
  4. 自动化:减少人工操作失误
  5. 时间窗口:避开业务高峰期

灰度发布策略

1
2
3
4
5
6
7
8
9
灰度发布流程:
┌─────────────────────────────────────────────────────┐
│ 1% → 5% → 10% → 30% → 50% → 100% │
│ │ │ │ │ │ │ │
│ └──────┴──────┴───────┴───────┴───────┘ │
│ 每个阶段观察 10-30 分钟 │
│ 确认无异常后进入下一阶段 │
│ 发现问题立即回滚 │
└─────────────────────────────────────────────────────┘

On-Call 实践

On-Call 是 SRE/运维团队的核心职责,负责生产环境的故障响应。

On-Call 轮值设计

维度 最佳实践
轮值周期 一周为单位,避免过长导致疲劳
人员配置 主备双人制,避免单点
交接 详细的 Handoff 文档
补偿 调休或补贴

On-Call 响应流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
告警触发


┌─────────────┐ 5 分钟内
│ 确认告警 │───────────────→ 未响应?升级到 Backup
└──────┬──────┘


┌─────────────┐ 15 分钟内
│ 初步诊断 │───────────────→ 无法定位?拉群协作
└──────┬──────┘


┌─────────────┐
│ 止血操作 │───────────────→ 优先恢复服务
└──────┬──────┘


┌─────────────┐
│ 根因分析 │───────────────→ 可后续进行
└──────┬──────┘


┌─────────────┐
│ 事后复盘 │───────────────→ 24-48 小时内完成
└─────────────┘

On-Call 减负措施

  • 消除噪音告警:合并重复告警,优化阈值
  • 自动化修复:常见问题自动处理(如磁盘清理)
  • 完善 Runbook:标准化处理流程
  • 定期演练:保持团队能力

学习资料

书籍

文章

工具

电商系统设计系列(篇次与(一)推荐阅读顺序一致)

电商系统设计:商品中心系统

商品中心是电商平台的「商品库」,负责商品全生命周期管理。本文将深入探讨商品系统的设计与实现,重点讲解 SPU/SKU 模型、异构商品治理、多级缓存三大核心技术,并通过标准实物商品、虚拟商品、服务商品、组合商品四个黄金案例,展示如何设计可扩展的商品系统。

本文既适合系统设计面试准备,也适合工程实践参考。

目录

1. 系统概览

1.1 业务场景

商品中心是电商平台的「商品库」,负责商品全生命周期管理。

核心职责:

  • 商品信息管理(PIM):SPU/SKU、属性、类目、图片、描述
  • 商品上架流程:商家上传、供应商同步、运营管理
  • 商品导购服务:搜索、详情、列表、筛选
  • 商品快照生成:为订单提供不可变的商品信息
  • 库存协同:与库存系统实时交互
  • 价格协同:为计价中心提供基础价格

业务模式:

  • B2B2C 模式(约 70%~80%):供应商商品,平台运营(机票、酒店、充值等)
  • B2C 模式(约 20%~30%):平台自营商品(礼品卡、券类等)

商品系统的职责边界:

  • 负责:商品数据管理、上架审核、搜索与缓存、快照生成
  • 不负责:具体库存扣减逻辑(由库存系统负责)、最终售价计算(由计价中心负责)

与其他系统的交互:

  • 订单系统:获取商品详情、库存校验、创建订单快照
  • 库存系统:实时库存查询、库存扣减与回补
  • 计价中心:提供基础价格、类目信息
  • 营销系统:提供商品标签、圈品规则
  • 搜索系统:同步商品索引

1.2 核心挑战

1. 异构商品

  • 实物商品:多规格 SKU 组合(服装、3C)
  • 虚拟商品:无 SKU 或简单 SKU(充值卡、会员)
  • 服务商品:时间维度库存(酒店、机票)
  • 组合商品:多 SKU 组合(套餐)

2. 多角色上架

  • 商家上传:Portal/App,人工审核,限流防刷
  • 供应商同步:Push/Pull,自动审核,幂等设计
  • 运营管理:后台上传,免审核或轻审核,批量处理

3. 高并发读

  • 商品详情页:QPS 可达万级
  • 商品列表页:QPS 可达千级
  • 多级缓存:L1 本地缓存 + L2 Redis + L3 数据库,配合 CDN

4. 数据一致性

  • 商品变更后:缓存失效、搜索索引更新、下游感知版本
  • 最终一致性:Kafka 事件、CDC
  • 补偿机制:定时对账、修复任务

5. 扩展性

  • 新品类快速接入:适配器模式、配置化平台
  • 尽量少改核心链路:开闭原则、策略模式

1.3 系统架构

商品系统在平台中承接上架写入与导购读取,经网关统一接入,核心能力按领域拆分为多个服务,并通过消息队列与订单、库存等系统解耦。

核心模块:

  1. 商品信息服务:SPU/SKU CRUD、版本管理、属性管理
  2. 类目属性服务:类目树、动态属性、品牌管理
  3. 上架审核服务:多角色上架、状态机、审核流
  4. 搜索服务:Elasticsearch 索引、多维筛选、排序
  5. 缓存服务:多级缓存(L1/L2)、智能刷新、缓存预热
  6. 快照服务:商品快照生成、Hash 复用、订单引用
  7. 同步服务:供应商数据同步、全量/增量、失败重试

技术栈:

  • 数据库:MySQL(分库分表,例如按 SPU 哈希 16 张表)、MongoDB(ExtInfo)
  • 缓存:Redis、本地缓存(Caffeine 等)
  • 搜索:Elasticsearch 7.x
  • 消息队列:Kafka(变更事件、CDC)
  • 对象存储:OSS(图片/视频)
  • 监控:Prometheus + Grafana

系统架构图

graph TB
    subgraph 客户端层
        A1[商家 Portal]
        A2[运营后台]
        A3[前端商城]
    end

    subgraph 接入层
        B[商品 API Gateway]
    end

    subgraph 服务层
        C1[商品信息服务]
        C2[上架审核服务]
        C3[搜索服务]
        C4[缓存服务]
        C5[快照服务]
        C6[同步服务]
    end

    subgraph 数据层
        D1[MySQL 分库分表]
        D2[Redis 多级缓存]
        D3[Elasticsearch]
        D4[MongoDB ExtInfo]
        D5[OSS 图片存储]
    end

    subgraph 消息队列
        E[Kafka]
    end

    subgraph 外部系统
        F1[供应商系统]
        F2[订单系统]
        F3[库存系统]
    end

    A1 --> B
    A2 --> B
    A3 --> B
    B --> C1
    B --> C2
    B --> C3
    C1 --> C4
    C1 --> C5
    C3 --> D3
    C4 --> D2
    C1 --> D1
    C1 --> D4
    C1 --> D5
    C6 --> F1
    C1 --> E
    E --> F2
    E --> F3

    style C1 fill:#e1f5ff
    style D1 fill:#ffe1e1
    style D2 fill:#ffe1e1
    style E fill:#e1ffe1

1.4 数据模型概览

核心表(逻辑名):

  • spu_tab:商品主信息(SPU)
  • sku_tab:SKU 信息
  • category_tab:类目
  • attribute_tab:属性定义
  • product_attribute_tab:商品属性值(EAV)
  • product_ext_tab 或 MongoDB 集合:扩展信息
  • product_snapshot_tab:商品快照
  • product_audit_tab:审核记录
  • product_log_tab:变更日志

ER 图

erDiagram
    SPU ||--o{ SKU : contains
    SPU ||--o| CATEGORY : belongs_to
    SPU ||--o{ PRODUCT_ATTRIBUTE : has
    SPU ||--|| PRODUCT_EXT : extends
    SPU ||--o{ PRODUCT_SNAPSHOT : generates
    SPU ||--o{ PRODUCT_LOG : tracks
    ATTRIBUTE ||--o{ PRODUCT_ATTRIBUTE : defines

    SPU {
        string spu_id PK
        string title
        int64 category_id FK
        int64 brand_id
        int status
        int64 version
        timestamp created_at
    }

    SKU {
        string sku_id PK
        string spu_id FK
        string spec_values
        int64 price
        int stock
        int status
    }

    CATEGORY {
        int64 category_id PK
        int64 parent_id
        string name
        int level
    }

    ATTRIBUTE {
        int64 attribute_id PK
        string attribute_name
        string attribute_type
    }

    PRODUCT_ATTRIBUTE {
        int64 id PK
        string spu_id FK
        int64 attribute_id FK
        string value
    }

    PRODUCT_SNAPSHOT {
        string snapshot_id PK
        string spu_id FK
        string sku_id FK
        string content
    }

2. 商品创建和上架流程

商品上架需要区分三种角色:商家(Merchant)、供应商(Partner)、运营(Ops)。不同角色的入口、审核策略与幂等要求不同,但底层都落在统一的 SPU/SKU 模型与状态机上。

2.1 商家上传(Merchant)

商家通过 Portal 或 App 上传商品,通常需要人工审核,并配合限流与风控,降低虚假商品与刷单风险。

业务流程:

  1. 商家在 Portal/App 填写商品信息
  2. 提交后进入「待审核」状态
  3. 审核通过后才能上架
  4. 需要人工审核(防止虚假商品)

技术要点:

  • 表单验证(前后端双重校验)
  • 限流(防止恶意刷单)
  • 审核队列(异步处理)
  • 审核历史可追溯

流程图:

sequenceDiagram
    participant M as 商家
    participant API as 商品 API
    participant DB as 数据库
    participant MQ as Kafka
    participant Audit as 审核服务

    M->>API: 创建商品
    API->>API: 身份验证
    API->>API: 限流检查
    API->>DB: 保存商品(草稿)
    API->>MQ: 发布审核事件
    API-->>M: 返回商品 ID

    MQ->>Audit: 消费审核事件
    Audit->>Audit: 人工审核
    Audit->>DB: 更新状态
    Audit->>MQ: 发布审核结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// 商家创建商品
func MerchantCreateProduct(ctx context.Context, req *MerchantProductRequest) (*Product, error) {
merchant, err := ValidateMerchant(ctx, req.MerchantID)
if err != nil {
return nil, ErrUnauthorized
}

limiterKey := fmt.Sprintf("merchant_create:%d", req.MerchantID)
if !rateLimiter.Allow(limiterKey, 10, time.Minute) {
return nil, ErrRateLimitExceeded
}

if err := ValidateProductRequest(req); err != nil {
return nil, err
}

product := &Product{
SPUID: GenerateSPUID(),
Title: req.Title,
CategoryID: req.CategoryID,
Status: ProductStatusDraft,
Source: SourceMerchant,
MerchantID: req.MerchantID,
Version: 1,
CreatedAt: time.Now(),
}

if err := db.InsertProduct(ctx, product); err != nil {
return nil, err
}

audit := &AuditTask{
TaskID: GenerateAuditID(),
ProductID: product.SPUID,
Type: AuditTypeMerchant,
Priority: AuditPriorityNormal,
Status: AuditStatusPending,
}

if err := db.InsertAuditTask(ctx, audit); err != nil {
return nil, err
}

event := &AuditEvent{
TaskID: audit.TaskID,
ProductID: product.SPUID,
EventType: "audit.created",
}
PublishAuditEvent(ctx, event)

RecordProductLog(ctx, product.SPUID, "商家创建商品", merchant.Name)

return product, nil
}

// 审核服务处理(示意:规则引擎 + 人工兜底)
func HandleAudit(ctx context.Context, event *AuditEvent) error {
task, err := db.GetAuditTask(ctx, event.TaskID)
if err != nil {
return err
}

product, _ := db.GetProduct(ctx, task.ProductID)

var approved bool
if ContainsSensitiveWords(product.Title) {
approved = false
} else {
approved = true
}

if approved {
task.Status = AuditStatusApproved
product.Status = ProductStatusApproved
} else {
task.Status = AuditStatusRejected
product.Status = ProductStatusRejected
}

db.UpdateAuditTask(ctx, task)
db.UpdateProduct(ctx, product)

resultEvent := &AuditResultEvent{
ProductID: product.SPUID,
Approved: approved,
}
PublishAuditResultEvent(ctx, resultEvent)

return nil
}

2.2 供应商同步(Partner)

供应商侧数据可通过 Push(供应商推送到平台 MQ)或 Pull(平台定时拉取)进入商品中心。自动审核可走快速通道,同时必须用幂等与版本控制避免重复写入与乱序覆盖。

技术要点:

  • 幂等性(重复推送去重)
  • 字段映射(供应商模型 → 平台模型)
  • 同步监控与告警
  • 热门商品可配合更积极的缓存刷新策略(见第 5 章)

Push 模式流程图:

sequenceDiagram
    participant P as 供应商
    participant MQ as Kafka
    participant Sync as 同步服务
    participant DB as 数据库

    P->>MQ: 推送商品数据
    MQ->>Sync: 消费消息
    Sync->>Sync: 幂等性检查
    Sync->>Sync: 数据映射
    Sync->>Sync: 自动审核
    Sync->>DB: Upsert 商品
    Sync->>MQ: 发布变更事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// 供应商 Push 模式
func PartnerPushProduct(ctx context.Context, msg *PartnerProductMessage) error {
idempotentKey := fmt.Sprintf("partner_push:%s:%s", msg.PartnerID, msg.ProductID)

record := &IdempotentRecord{
Key: idempotentKey,
Status: IdempotentProcessing,
ExpireAt: time.Now().Add(10 * time.Minute),
}

if err := db.InsertIdempotentRecord(ctx, record); err != nil {
return nil
}

product := MapPartnerProduct(msg)
product.Source = SourcePartner
product.PartnerID = msg.PartnerID

if AutoAudit(product) {
product.Status = ProductStatusOnline
} else {
product.Status = ProductStatusPendingAudit
}

existing, _ := db.GetProductByPartnerID(ctx, msg.PartnerID, msg.ProductID)
if existing != nil {
product.SPUID = existing.SPUID
product.Version = existing.Version + 1
if err := db.UpdateProductWithVersion(ctx, product, existing.Version); err != nil {
return err
}
} else {
product.SPUID = GenerateSPUID()
product.Version = 1
if err := db.InsertProduct(ctx, product); err != nil {
return err
}
}

changed := &ProductChangedEvent{
SPUID: product.SPUID,
ChangeType: "partner_sync",
}
PublishProductChangedEvent(ctx, changed)

db.UpdateIdempotentStatus(ctx, idempotentKey, IdempotentSuccess)

return nil
}

// 供应商 Pull 模式
func PartnerPullProducts(ctx context.Context, partnerID string) error {
lastSyncTime := GetLastSyncTime(partnerID)

products, err := partnerClient.GetProducts(partnerID, lastSyncTime)
if err != nil {
return err
}

for _, p := range products {
msg := ConvertToMessage(p)
if err := PartnerPushProduct(ctx, msg); err != nil {
log.Error("failed to sync product", "partnerID", partnerID, "productID", p.ID, "error", err)
continue
}
}

UpdateLastSyncTime(partnerID, time.Now())
return nil
}

func AutoAudit(product *Product) bool {
if ContainsSensitiveWords(product.Title) {
return false
}
if product.Price < 0 || product.Price > 1000000 {
return false
}
if !CategoryExists(product.CategoryID) {
return false
}
if product.Title == "" || product.CategoryID == 0 {
return false
}
return true
}

2.3 运营上传(Ops)

运营在后台可单品录入或批量导入(如 Excel)。通常免人工审核或仅做抽检,要求批量任务可观测、单行失败可定位。

技术要点:

  • Excel 流式解析,控制内存
  • 行级校验与错误汇总
  • 写库与发事件的一致策略(必要时按批次事务)
  • 操作审计日志

批量上传流程图:

sequenceDiagram
    participant Ops as 运营
    participant API as 商品 API
    participant Parser as Excel 解析器
    participant DB as 数据库
    participant MQ as Kafka

    Ops->>API: 上传 Excel 文件
    API->>Parser: 流式解析
    loop 每行数据
        Parser->>Parser: 验证数据
        Parser->>DB: 保存商品
        Parser->>MQ: 发布事件
    end
    API-->>Ops: 返回结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 运营批量上传
func OpsBatchUpload(ctx context.Context, file *ExcelFile) (*UploadResult, error) {
result := &UploadResult{
Success: []string{},
Failed: []UploadError{},
}

parser := NewExcelParser(file)
rowNum := 0
for row := range parser.Parse() {
rowNum++

product, err := ValidateRow(row)
if err != nil {
result.Failed = append(result.Failed, UploadError{Row: rowNum, Error: err.Error()})
continue
}

product.SPUID = GenerateSPUID()
product.Status = ProductStatusOnline
product.Source = SourceOps
product.Version = 1

if err := db.InsertProduct(ctx, product); err != nil {
result.Failed = append(result.Failed, UploadError{Row: rowNum, Error: err.Error()})
continue
}

evt := &ProductCreatedEvent{SPUID: product.SPUID}
PublishProductCreatedEvent(ctx, evt)

result.Success = append(result.Success, product.SPUID)
}

RecordBatchUploadLog(ctx, result)
return result, nil
}

type ExcelParser struct {
file *ExcelFile
}

func (p *ExcelParser) Parse() <-chan *ExcelRow {
ch := make(chan *ExcelRow, 100)
go func() {
defer close(ch)
f, err := excelize.OpenFile(p.file.Path)
if err != nil {
return
}
defer f.Close()

rows, _ := f.GetRows("Sheet1")
for i, row := range rows {
if i == 0 {
continue
}
ch <- &ExcelRow{RowNum: i + 1, Data: row}
}
}()
return ch
}

2.4 上架状态机与审核策略

商品状态驱动上架生命周期:草稿 → 待审核 → 通过/拒绝 → 上线/下线。转换规则应集中配置,写库时使用乐观锁或状态条件更新,避免并发覆盖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
const (
ProductStatusDraft = 0
ProductStatusPendingAudit = 1
ProductStatusApproved = 2
ProductStatusRejected = 3
ProductStatusOnline = 4
ProductStatusOffline = 5
)

var allowedTransitions = map[int]map[int]bool{
ProductStatusDraft: {ProductStatusPendingAudit: true},
ProductStatusPendingAudit: {
ProductStatusApproved: true,
ProductStatusRejected: true,
},
ProductStatusApproved: {ProductStatusOnline: true},
ProductStatusOnline: {ProductStatusOffline: true},
ProductStatusOffline: {ProductStatusOnline: true},
}

func TransitionProductStatus(ctx context.Context, spuID string, from, to int) error {
if !allowedTransitions[from][to] {
return ErrIllegalTransition
}

product, err := db.GetProduct(ctx, spuID)
if err != nil {
return err
}
if product.Status != from {
return ErrStatusMismatch
}

if err := db.UpdateProductStatus(ctx, spuID, to, product.Version); err != nil {
return err
}

RecordProductLog(ctx, spuID, fmt.Sprintf("状态变更: %d -> %d", from, to), "system")
return nil
}
stateDiagram-v2
    [*] --> 草稿
    草稿 --> 待审核: 提交审核
    待审核 --> 已拒绝: 审核不通过
    待审核 --> 已通过: 审核通过
    已通过 --> 已上线: 上架
    已上线 --> 已下线: 下架
    已下线 --> 已上线: 重新上架
    已拒绝 --> [*]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
type AuditStrategy interface {
ShouldAudit(product *Product) bool
GetPriority() int
}

type MerchantAuditStrategy struct{}

func (s *MerchantAuditStrategy) ShouldAudit(product *Product) bool {
return product.Source == SourceMerchant
}

func (s *MerchantAuditStrategy) GetPriority() int {
return AuditPriorityNormal
}

type PartnerAuditStrategy struct{}

func (s *PartnerAuditStrategy) ShouldAudit(product *Product) bool {
return product.Source == SourcePartner && !AutoAudit(product)
}

func (s *PartnerAuditStrategy) GetPriority() int {
return AuditPriorityHigh
}

type OpsAuditStrategy struct{}

func (s *OpsAuditStrategy) ShouldAudit(product *Product) bool {
return false
}

var strategies = []AuditStrategy{
&MerchantAuditStrategy{},
&PartnerAuditStrategy{},
&OpsAuditStrategy{},
}

func RouteAuditStrategy(product *Product) AuditStrategy {
for _, strategy := range strategies {
if strategy.ShouldAudit(product) {
return strategy
}
}
return nil
}

3. 商品数据模型设计专题

本章从 SPU/SKU、类目属性、动态字段存储到订单快照,串起商品中心最核心的数据面设计。

3.1 SPU/SKU 模型设计

SPU(Standard Product Unit) 描述「卖的是什么」:标题、类目、图文、共用属性。SKU(Stock Keeping Unit) 描述「可售卖的最小单元」:规格组合、价格、可售状态。一笔下单通常指向 SKU,搜索与列表常聚合在 SPU 维度展示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type SPU struct {
SPUID string
Title string
CategoryID int64
BrandID int64
MainImages []string
Description string
SpecDefs []SpecDef // 规格维度定义,如颜色、尺码
ProductType string // standard/virtual/service/bundle
Status int
Version int64
}

type SpecDef struct {
Name string
Values []string // 可选值集合
}

type SKU struct {
SKUID string
SPUID string
SpecValues map[string]string // 如 {"颜色":"黑","存储":"256G"}
Price int64
Status int
}

规格组合(笛卡尔积):给定多个规格维度及其取值,生成所有合法 SKU。实务中常加规则表剔除无效组合(例如某颜色不提供某尺码)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func CartesianSKUs(spuID string, defs []SpecDef) []*SKU {
if len(defs) == 0 {
return []*SKU{{SPUID: spuID, SKUID: GenerateSKUID(spuID, nil), SpecValues: map[string]string{}}}
}
var out []*SKU
var dfs func(i int, cur map[string]string)
dfs = func(i int, cur map[string]string) {
if i == len(defs) {
m := make(map[string]string, len(cur))
for k, v := range cur {
m[k] = v
}
out = append(out, &SKU{
SPUID: spuID,
SKUID: GenerateSKUID(spuID, m),
SpecValues: m,
})
return
}
d := defs[i]
for _, val := range d.Values {
cur[d.Name] = val
dfs(i+1, cur)
delete(cur, d.Name)
}
}
dfs(0, map[string]string{})
return out
}

示例(手机):颜色 {黑, 白} × 存储 {128G, 256G} → 4 个 SKU;若再 × 版本 {标准, Pro} → 8 个 SKU。服装类目常见「颜色 × 尺码」,SKU 数量更易膨胀,需要规格模板与批量编辑工具。

flowchart LR
    SPU[SPU 商品主体] --> SK1[SKU 规格组合 A]
    SPU --> SK2[SKU 规格组合 B]
    SPU --> SK3[SKU 规格组合 C]
    SK1 --> INV[库存单元]
    SK2 --> INV
    SK3 --> INV

3.2 类目与属性系统

类目树支持前台导航、属性继承与搜索筛选维度配置。叶子类目绑定「销售属性」与「关键属性」,非叶子类目可定义通用属性并由子类目继承或覆盖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
type Category struct {
CategoryID int64
ParentID int64
Name string
Level int
Path string // 物化路径,如 "1/10/1005"
IsLeaf bool
}

type Attribute struct {
AttributeID int64
Name string
InputType string // text/select/multi/date
Required bool
CategoryIDs []int64 // 绑定类目
}

type ProductAttribute struct {
SPUID string
AttributeID int64
Value string
}

func BuildCategoryTree(nodes []*Category) map[int64][]*Category {
children := make(map[int64][]*Category)
for _, n := range nodes {
children[n.ParentID] = append(children[n.ParentID], n)
}
return children
}

func IndexCategoriesByID(nodes []*Category) map[int64]*Category {
m := make(map[int64]*Category, len(nodes))
for _, n := range nodes {
m[n.CategoryID] = n
}
return m
}

func GetCategoryPath(byID map[int64]*Category, leafID int64) []*Category {
var path []*Category
cur, ok := byID[leafID]
for ok && cur != nil {
path = append([]*Category{cur}, path...)
if cur.ParentID == 0 {
break
}
cur, ok = byID[cur.ParentID]
}
return path
}
graph TD
    ROOT[根类目] --> A[服饰]
    ROOT --> B[数码]
    A --> A1[男装]
    A --> A2[女装]
    B --> B1[手机]
    B1 --> B1L[智能手机 叶子]
1
2
3
func ExampleCategoryPath(nodes []*Category, leafID int64) []*Category {
return GetCategoryPath(IndexCategoriesByID(nodes), leafID)
}

3.3 动态属性与 EAV 模型

不同类目字段差异大:服装要「材质、版型」,手机要「CPU、屏幕」。直接用超宽表会导致大量稀疏列;全 EAV 查询与索引压力大。工程上常见混合方案:高频筛选字段进主表或 JSON 索引,长尾属性走 EAV 或文档库。

方案 优点 缺点 适用
宽表 查询简单、性能好 schema 僵化、稀疏列浪费 属性稳定的标品类
EAV 扩展灵活 多表 Join、索引复杂 长尾属性、运营可配字段
混合 平衡性能与扩展 需要治理与同步策略 大型平台主流选择
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type ProductBase struct {
SPUID string
Title string
CategoryID int64
BrandID int64
PriceMin int64 // 列表价展示用
PriceMax int64
}

// MongoDB / JSON 存非强筛选扩展
type ProductExt struct {
SPUID string
Payload map[string]interface{}
}

func LoadProductFull(ctx context.Context, spuID string) (*SPU, map[int64]string, *ProductExt, error) {
spu, err := db.GetSPU(ctx, spuID)
if err != nil {
return nil, nil, nil, err
}
eav, _ := db.ListProductAttributes(ctx, spuID)
ext, _ := extStore.Get(ctx, spuID)
return spu, eav, ext, nil
}

3.4 商品快照生成与复用

订单需要固化「下单瞬间」的商品展示信息与价格依据,避免后续改价改图引发纠纷。快照内容建议与计价结果、税费规则版本等一并由订单域引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import (
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"time"
)

type ProductSnapshot struct {
SnapshotID string
SPUID string
SKUID string
Title string
Price int64
Image string
SpecsJSON string
AttrsJSON string
CreatedAt time.Time
}

func SnapshotContentKey(spu *SPU, sku *SKU) string {
b, _ := json.Marshal(sku.SpecValues)
return fmt.Sprintf("%s|%s|%s|%d|%s", spu.SPUID, sku.SKUID, spu.Title, sku.Price, string(b))
}

func FindOrCreateSnapshot(ctx context.Context, spu *SPU, sku *SKU) (string, error) {
key := SnapshotContentKey(spu, sku)
sum := md5.Sum([]byte(key))
snapshotID := hex.EncodeToString(sum[:])

if snap, _ := db.GetSnapshot(ctx, snapshotID); snap != nil {
return snapshotID, nil
}

specsJSON, _ := json.Marshal(sku.SpecValues)
snap := &ProductSnapshot{
SnapshotID: snapshotID,
SPUID: spu.SPUID,
SKUID: sku.SKUID,
Title: spu.Title,
Price: sku.Price,
Image: spu.MainImages[0],
SpecsJSON: string(specsJSON),
CreatedAt: time.Now(),
}
if err := db.InsertSnapshot(ctx, snap); err != nil {
return "", err
}
return snapshotID, nil
}

4. 异构商品治理

实物、虚拟、服务、组合在 SKU 形态、库存维度、价格与履约链路上差异显著。治理目标是:统一生命周期与检索体验,又允许品类在扩展点上替换实现。

4.1 异构商品的挑战

维度 标准实物 虚拟商品 服务商品 组合商品
SKU 模型 多规格 SKU 常单 SKU 或无 SKU 房型/时段颗粒度 子 SKU 组合
库存 数量库存 卡密池/核销码 日历房态/座位图 子品库存联合约束
价格 标价 + 促销 面值/折扣规则 日历价、动态溢价 打包价、分摊规则
履约 物流发货 直充/发码 预约/入住/出行 分履约合并展示

核心矛盾:一套核心表与流程 vs 品类特有字段与规则。解法是把「共性」沉到内核,把「差异」关进扩展点(适配器/策略/配置)。

4.2 统一抽象与适配器模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import "errors"

// 与第 5 章搜索文档对齐的简化结构(完整字段见 5.1)
type ProductSearchDoc struct {
SPUID string
Title string
CategoryID int64
ExtraTags []string
}

type ProductType string

const (
ProductTypeStandard ProductType = "standard"
ProductTypeVirtual ProductType = "virtual"
ProductTypeService ProductType = "service"
ProductTypeBundle ProductType = "bundle"
)

type ProductAdapter interface {
Type() ProductType
Validate(spu *SPU, skus []*SKU) error
NormalizeForSearch(doc *ProductSearchDoc) error
StockDimensions() []string
}

type StandardProductAdapter struct{}

func (a *StandardProductAdapter) Type() ProductType { return ProductTypeStandard }

func (a *StandardProductAdapter) Validate(spu *SPU, skus []*SKU) error {
if len(skus) == 0 {
return errors.New("standard product requires skus")
}
return nil
}

func (a *StandardProductAdapter) NormalizeForSearch(doc *ProductSearchDoc) error {
return nil
}

func (a *StandardProductAdapter) StockDimensions() []string {
return []string{"warehouse_sku"}
}

type ServiceProductAdapter struct{}

func (a *ServiceProductAdapter) Type() ProductType { return ProductTypeService }

func (a *ServiceProductAdapter) Validate(spu *SPU, skus []*SKU) error {
// 服务类可允许「按日库存」在扩展表维护
return nil
}

func (a *ServiceProductAdapter) NormalizeForSearch(doc *ProductSearchDoc) error {
doc.ExtraTags = append(doc.ExtraTags, "service")
return nil
}

func (a *ServiceProductAdapter) StockDimensions() []string {
return []string{"date", "room_type"}
}

var adapterRegistry = map[ProductType]ProductAdapter{
ProductTypeStandard: &StandardProductAdapter{},
ProductTypeService: &ServiceProductAdapter{},
}

func RouteAdapter(spu *SPU) ProductAdapter {
t := ProductType(spu.ProductType)
if a, ok := adapterRegistry[t]; ok {
return a
}
return adapterRegistry[ProductTypeStandard]
}
classDiagram
    class ProductAdapter {
        <>
        +Type() ProductType
        +Validate()
        +NormalizeForSearch()
        +StockDimensions()
    }
    class StandardProductAdapter
    class ServiceProductAdapter
    ProductAdapter <|.. StandardProductAdapter
    ProductAdapter <|.. ServiceProductAdapter

4.3 配置化与低代码平台

将「表单字段、校验规则、上架步骤、审核模板」配置化,新品类主要工作是配置 + 少量插件,而不是复制一套后台。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
type FormField struct {
Key string
Label string
Widget string // input/select/date/city
Required bool
Validator string // 正则或规则名
DataSource string // 枚举接口
}

type FormConfig struct {
ProductType ProductType
Fields []FormField
Steps []string
}

var HotelSPUForm = FormConfig{
ProductType: ProductTypeService,
Fields: []FormField{
{Key: "hotel.star", Label: "星级", Widget: "select", Required: true, DataSource: "/meta/stars"},
{Key: "hotel.city", Label: "城市", Widget: "city", Required: true},
{Key: "hotel.address", Label: "地址", Widget: "input", Required: true},
},
Steps: []string{"basic", "room", "policy", "media"},
}

4.4 多维度库存管理

库存可分层:平台库存网关 统一对外,GetStock 根据 ProductType 与维度路由到仓库库存、卡密池、房态服务等。

graph LR
    API[下单/详情] --> GW[库存网关]
    GW --> W[仓配库存]
    GW --> C[卡密池]
    GW --> R[房态/日历库存]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
type StockQuery struct {
SKUID string
Date string
Warehouse string
}

type InventoryGateway struct {
warehouse StockProvider
cardPool StockProvider
roomState StockProvider
}

type StockProvider interface {
Name() string
Available(ctx context.Context, q StockQuery) (int64, error)
}

func (g *InventoryGateway) GetStock(ctx context.Context, adapter ProductAdapter, q StockQuery) (int64, error) {
dims := adapter.StockDimensions()
switch {
case contains(dims, "warehouse_sku"):
return g.warehouse.Available(ctx, q)
case contains(dims, "date"):
return g.roomState.Available(ctx, q)
default:
return g.cardPool.Available(ctx, q)
}
}

func contains(arr []string, v string) bool {
for _, x := range arr {
if x == v {
return true
}
}
return false
}

5. 商品搜索与多级缓存

导购链路读多写少,典型优化是 Elasticsearch 承担检索与排序Redis + 本地缓存承担热点详情。写入路径发布变更事件,异步刷新索引与失效缓存,接受短暂最终一致。

5.1 Elasticsearch 索引设计

索引文档应同时满足:关键词检索、类目/品牌筛选、价格区间、标签过滤、排序(销量、上架时间、相关性得分)。sku_list 可用 nested 或扁平化子文档,视查询复杂度权衡。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type ProductSearchDoc struct {
SPUID string `json:"spu_id"`
Title string `json:"title"`
CategoryID int64 `json:"category_id"`
BrandID int64 `json:"brand_id"`
Tags []string `json:"tags"`
PriceMin int64 `json:"price_min"`
PriceMax int64 `json:"price_max"`
Status int `json:"status"`
Sales30d int64 `json:"sales_30d"`
OnShelfAt int64 `json:"on_shelf_at"`
ExtraTags []string `json:"extra_tags"`
SKUs []SearchSKUInline `json:"skus"`
}

type SearchSKUInline struct {
SKUID string `json:"sku_id"`
Specs map[string]string `json:"specs"`
Price int64 `json:"price"`
InStock bool `json:"in_stock"`
}

Mapping 要点title 使用 text + keyword 子字段;筛选字段 keyword;价格 longskus 使用 nested 以便按规格价查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"mappings": {
"properties": {
"spu_id": { "type": "keyword" },
"title": {
"type": "text",
"fields": { "kw": { "type": "keyword", "ignore_above": 256 } }
},
"category_id": { "type": "long" },
"brand_id": { "type": "long" },
"tags": { "type": "keyword" },
"price_min": { "type": "long" },
"price_max": { "type": "long" },
"status": { "type": "integer" },
"sales_30d": { "type": "long" },
"on_shelf_at": { "type": "long" },
"extra_tags": { "type": "keyword" },
"skus": {
"type": "nested",
"properties": {
"sku_id": { "type": "keyword" },
"specs": { "type": "flattened" },
"price": { "type": "long" },
"in_stock": { "type": "boolean" }
}
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
import (
"context"
"encoding/json"
)

type SearchRequest struct {
Keyword string
CategoryID int64
BrandID int64
PriceMin int64
PriceMax int64
Page int
PageSize int
Sort string
}

func SearchProducts(ctx context.Context, es *ESClient, req *SearchRequest) ([]ProductSearchDoc, int, error) {
must := []map[string]interface{}{
{"term": map[string]interface{}{"status": 4}},
}
if req.Keyword != "" {
must = append(must, map[string]interface{}{
"multi_match": map[string]interface{}{
"query": req.Keyword,
"fields": []string{"title^2", "title.kw"},
},
})
}

filters := []map[string]interface{}{}
if req.CategoryID > 0 {
filters = append(filters, map[string]interface{}{
"term": map[string]interface{}{"category_id": req.CategoryID},
})
}
if req.BrandID > 0 {
filters = append(filters, map[string]interface{}{
"term": map[string]interface{}{"brand_id": req.BrandID},
})
}
if req.PriceMin > 0 || req.PriceMax > 0 {
rng := map[string]interface{}{}
if req.PriceMin > 0 {
rng["gte"] = req.PriceMin
}
if req.PriceMax > 0 {
rng["lte"] = req.PriceMax
}
filters = append(filters, map[string]interface{}{
"range": map[string]interface{}{"price_min": rng},
})
}

boolQ := map[string]interface{}{"must": must}
if len(filters) > 0 {
boolQ["filter"] = filters
}
body := map[string]interface{}{
"query": map[string]interface{}{"bool": boolQ},
"from": (req.Page - 1) * req.PageSize,
"size": req.PageSize,
}
payload, err := json.Marshal(body)
if err != nil {
return nil, 0, err
}
return es.Search(ctx, "product_index", payload)
}

5.2 多级缓存策略

flowchart TB
    Client[详情/列表请求] --> L1[L1 本地缓存 Caffeine]
    L1 -->|miss| L2[L2 Redis 集群]
    L2 -->|miss| L3[L3 MySQL 主从]
    L3 --> L2
    L2 --> L1
    MQ[Kafka 变更事件] -.->|失效/异步回填| L2
    MQ -.-> L1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import (
"context"
"encoding/json"
"fmt"
"time"
)

type ProductDetailDTO struct {
SPU *SPU
SKUs []*SKU
}

func cacheKeyDetail(spuID string) string {
return fmt.Sprintf("pd:%s", spuID)
}

func GetProductDetail(ctx context.Context, spuID string) (*ProductDetailDTO, error) {
if v, ok := localCache.Get(spuID); ok {
return v.(*ProductDetailDTO), nil
}

raw, err := redis.Get(ctx, cacheKeyDetail(spuID))
if err == nil && raw != "" {
var dto ProductDetailDTO
if json.Unmarshal([]byte(raw), &dto) == nil {
localCache.Set(spuID, &dto, 5*time.Second)
return &dto, nil
}
}

spu, err := db.GetSPU(ctx, spuID)
if err != nil {
return nil, err
}
skus, err := db.ListSKUBySPU(ctx, spuID)
if err != nil {
return nil, err
}
dto := &ProductDetailDTO{SPU: spu, SKUs: skus}

b, _ := json.Marshal(dto)
_ = redis.Set(ctx, cacheKeyDetail(spuID), string(b), 10*time.Minute)
localCache.Set(spuID, dto, 5*time.Second)

return dto, nil
}

func InvalidateProductCaches(ctx context.Context, spuID string) {
_ = redis.Del(ctx, cacheKeyDetail(spuID))
localCache.Invalidate(spuID)
}

5.3 智能刷新规则

热门商品变更后应更快可见;长尾商品可降低刷新频率以节省 ES 与 Redis 成本。可结合 近实时销量、搜索曝光、运营打标 计算刷新间隔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func CalculateRefreshInterval(spuID string, score float64) time.Duration {
if score > 1000 {
return 5 * time.Second
}
if score > 100 {
return 30 * time.Second
}
if score > 10 {
return 2 * time.Minute
}
return 10 * time.Minute
}

func HotnessScore(expose1h, cart1h, sales30d int64) float64 {
return float64(expose1h)*0.1 + float64(cart1h)*2 + float64(sales30d)*0.01
}

6. 特殊商品类型(黄金案例)

以下四个案例覆盖多数面试与工程追问:规格矩阵、虚拟履约、日历库存、组合约束

6.1 标准实物商品

场景:T 恤,颜色 {红, 蓝, 黑},尺码 {S, M, L, XL},共 12 个 SKU。列表价可一致,也可按颜色区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func ExampleTShirtSPU() *SPU {
return &SPU{
SPUID: "SPU_TSHIRT_DEMO",
Title: "纯棉圆领 T 恤",
CategoryID: 5001,
ProductType: "standard",
SpecDefs: []SpecDef{
{Name: "颜色", Values: []string{"红", "蓝", "黑"}},
{Name: "尺码", Values: []string{"S", "M", "L", "XL"}},
},
}
}

// 12 个 SKU:CartesianSKUs("SPU_TSHIRT_DEMO", specDefs)
graph LR
    subgraph SKUs["12 x SKU"]
        R1[红-S] --- R2[红-M]
        B1[蓝-S] --- B2[黑-XL]
    end
    SPU[T 恤 SPU] --> SKUs

6.2 虚拟商品

场景:话费充值,SKU 对应面额;库存来自 卡密池直充渠道额度。下单后即时履约,无需物流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
type TopUpSKU struct {
SKUID string
FaceValue int64
Channel string
}

type CardPool struct {
PoolID string
SKUID string
}

type CardSecret struct {
CardID string
SKUID string
Secret string
Status int
}

func IssueCard(ctx context.Context, skuID string) (*CardSecret, error) {
card, err := db.AcquireOneCard(ctx, skuID)
if err != nil {
return nil, err
}
if err := db.MarkCardIssued(ctx, card.CardID); err != nil {
return nil, err
}
return card, nil
}
sequenceDiagram
    participant U as 用户
    participant O as 订单
    participant P as 商品/卡密
    U->>O: 下单支付成功
    O->>P: 申请卡密
    P-->>O: 返回卡密/直充结果
    O-->>U: 展示履约结果

6.3 服务类商品

场景:酒店房型,价格随 日期 波动,库存为 每日可售间夜。搜索与下单需传入住离店日期。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
type HotelRoomSKU struct {
SKUID string
RoomType string
HotelID string
}

type PriceCalendar struct {
SKUID string
Date string
Price int64
}

type StockCalendar struct {
SKUID string
Date string
Stock int64
}

func CalculateHotelPrice(ctx context.Context, skuID, checkIn, checkOut string) (int64, error) {
nights := NightsBetween(checkIn, checkOut)
var total int64
for d := range EachNight(checkIn, nights) {
p, err := db.GetDailyPrice(ctx, skuID, d)
if err != nil {
return 0, err
}
total += p
}
return total, nil
}

func NightsBetween(checkIn, checkOut string) int { return 1 }
func EachNight(checkIn string, n int) []string { return []string{checkIn} }

6.4 组合商品

场景:电影票 + 小食套餐,子商品各自有 SKU 与库存,下单需 联合校验打包售价分摊(可固定价或按比例)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import "fmt"

type BundleItem struct {
ChildSPUID string
ChildSKUID string
Quantity int
}

type BundleProduct struct {
SPUID string
Items []BundleItem
Price int64
}

func CheckBundleStock(ctx context.Context, b *BundleProduct) error {
for _, it := range b.Items {
n, err := inventory.GetStock(ctx, it.ChildSKUID, "")
if err != nil {
return err
}
if n < int64(it.Quantity) {
return fmt.Errorf("insufficient stock for %s", it.ChildSKUID)
}
}
return nil
}

func AllocateBundlePrice(items []BundleItem, total int64) map[string]int64 {
out := make(map[string]int64)
var units int64
for _, it := range items {
units += int64(it.Quantity)
}
if units == 0 {
return out
}
var allocated int64
for i, it := range items {
var share int64
if i == len(items)-1 {
share = total - allocated
} else {
share = total * int64(it.Quantity) / units
}
out[it.ChildSKUID] = share
allocated += share
}
return out
}
flowchart TB
    B[组合 SPU] --> M1[子 SKU 电影票]
    B --> M2[子 SKU 爆米花]
    M1 --> INV1[影厅座位库存]
    M2 --> INV2[卖品库存]

7. 商品版本管理与快照

商品变更频繁,需要 可追溯的版本历史面向订单的不可变快照。版本表支撑审计与回滚;快照表支撑下单展示与纠纷处理;Kafka 将变更广播给搜索与缓存等消费者。

7.1 版本控制

目标:记录每次变更、支持回滚、满足合规审计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import (
"context"
"encoding/json"
"time"
)

type ProductVersion struct {
VersionID string
SPUID string
Version int64
Content string
ChangeType string
Operator string
CreatedAt time.Time
}

func CreateProductVersion(ctx context.Context, product *Product, operator string) error {
content, _ := json.Marshal(product)
version := &ProductVersion{
VersionID: GenerateVersionID(),
SPUID: product.SPUID,
Version: product.Version,
Content: string(content),
ChangeType: "update",
Operator: operator,
CreatedAt: time.Now(),
}
return db.InsertProductVersion(ctx, version)
}

func RollbackToVersion(ctx context.Context, spuID string, targetVersion int64) error {
ver, err := db.GetProductVersion(ctx, spuID, targetVersion)
if err != nil {
return err
}
product := &Product{}
if err := json.Unmarshal([]byte(ver.Content), product); err != nil {
return err
}
current, _ := db.GetProduct(ctx, spuID)
product.Version = current.Version + 1
if err := db.UpdateProduct(ctx, product); err != nil {
return err
}
return CreateProductVersion(ctx, product, "system_rollback")
}

7.2 快照机制

订单创建时引用快照 ID,即使商品改价改图,订单详情仍展示下单时内容。内容 Hash 相同则复用一条快照记录,节省存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import (
"context"
"crypto/md5"
"encoding/hex"
"encoding/json"
"fmt"
"time"
)

type ProductSnapshotOrder struct {
SnapshotID string
SPUID string
SKUID string
Title string
Price int64
Image string
Specs string
Attributes string
CreatedAt time.Time
}

func CreateSnapshot(ctx context.Context, sku *SKU) (string, error) {
spu, err := db.GetSPU(ctx, sku.SPUID)
if err != nil {
return "", err
}
content := fmt.Sprintf("%s_%s_%s_%d_%s",
spu.SPUID, sku.SKUID, spu.Title, sku.Price, encodeSpecs(sku.SpecValues))
sum := md5.Sum([]byte(content))
snapshotID := hex.EncodeToString(sum[:])

if existing, _ := db.GetSnapshot(ctx, snapshotID); existing != nil {
return snapshotID, nil
}

snap := &ProductSnapshotOrder{
SnapshotID: snapshotID,
SPUID: spu.SPUID,
SKUID: sku.SKUID,
Title: spu.Title,
Price: sku.Price,
Image: spu.MainImages[0],
Specs: encodeSpecs(sku.SpecValues),
CreatedAt: time.Now(),
}
if err := db.InsertSnapshot(ctx, snap); err != nil {
return "", err
}
return snapshotID, nil
}

func encodeSpecs(m map[string]string) string {
b, _ := json.Marshal(m)
return string(b)
}

7.3 变更事件与最终一致性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import (
"encoding/json"
"time"
)

type ProductChangedEvent struct {
SPUID string
ChangeType string
Version int64
Timestamp time.Time
}

func PublishProductChangedEvent(ctx context.Context, event *ProductChangedEvent) error {
data, _ := json.Marshal(event)
msg := &KafkaMessage{
Topic: "product.changed",
Key: event.SPUID,
Value: data,
}
return kafkaProducer.Send(msg)
}

func ConsumeProductChangedEvent() {
consumer := kafka.NewConsumer("product.changed", "search-sync-group")
for msg := range consumer.Messages() {
event := &ProductChangedEvent{}
if err := json.Unmarshal(msg.Value, event); err != nil {
continue
}
if err := UpdateSearchIndex(event.SPUID); err != nil {
log.Error("failed to update search index", "error", err)
continue
}
InvalidateCache(event.SPUID)
consumer.CommitMessage(msg)
}
}
flowchart LR
    W[商品写库] --> E[发布 product.changed]
    E --> S[搜索同步 Worker]
    E --> C[缓存失效]
    E --> O[下游对账/监控]

8. 商品类型扩展设计

8.1 扩展点识别

  1. 商品模型扩展:品类特有属性与扩展存储(MongoDB / JSON / EAV)
  2. 上架流程扩展:审核模板、必填项、校验插件
  3. 库存扩展:库存维度与网关路由(见第 4 章)
  4. 价格扩展:计价参数、日历价、动态溢价
  5. 搜索与展示扩展:索引字段、列表卡片模板、筛选器组件

8.2 策略模式应用

1
2
3
4
5
6
7
8
9
type ProductTypeStrategy interface {
Validate(product *Product) error
GenerateSKUs(spu *SPU) []*SKU
GetStock(skuID string) (int, error)
CalculatePrice(sku *SKU, params map[string]interface{}) (int64, error)
GetExtAttributes(spu *SPU) map[string]interface{}
}

// 具体策略按品类实现,注册到表;与第 4 章 ProductAdapter 可合并或分层(适配器偏写模型,策略偏业务规则)

8.3 新品类接入指南

flowchart LR
    A[1. 分析差异] --> B[2. 定义模型]
    B --> C[3. 实现策略]
    C --> D[4. 注册路由]
    D --> E[5. 测试验证]
  1. 分析差异:相对标准实物,列出 SKU、库存、价格、履约差异
  2. 定义模型:SPU/SKU 扩展字段、Ext 文档 schema
  3. 实现策略:实现 ProductTypeStrategy(或 ProductAdapter)并补单测
  4. 注册路由:在注册表挂载 ProductType → 实现,并配置表单/索引
  5. 测试验证:集成测试覆盖上架、搜索、下单快照全链路

8.4 扩展性设计原则

  • 开闭原则:新增品类以注册策略为主,避免修改核心状态机主干
  • 单一职责:每个策略只处理一个品类或一族相似品类
  • 依赖倒置:上层依赖 ProductTypeStrategy 接口,而非具体类

9. 工程实践要点

9.1 商品 ID 生成

方案 优点 缺点 适用
Snowflake 趋势递增、高性能、全局唯一 时钟回拨需治理 大规模推荐
UUID 实现简单 无序、索引碎片化 中小流量
DB 自增 简单 分库分表扩展难 单库早期
flowchart LR
    ID[64 bit ID] --> T[41 bit 时间戳]
    ID --> M[10 bit 机器号]
    ID --> S[12 bit 序列]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import (
"fmt"
"sync"
"time"
)

type SnowflakeGenerator struct {
machineID int64
sequence int64
lastTime int64
mu sync.Mutex
}

func (g *SnowflakeGenerator) NextID() string {
g.mu.Lock()
defer g.mu.Unlock()

now := time.Now().UnixMilli()
if now < g.lastTime {
time.Sleep(time.Duration(g.lastTime-now) * time.Millisecond)
now = time.Now().UnixMilli()
}
if now == g.lastTime {
g.sequence = (g.sequence + 1) & 0xFFF
if g.sequence == 0 {
for now <= g.lastTime {
now = time.Now().UnixMilli()
}
}
} else {
g.sequence = 0
}
g.lastTime = now
id := ((now - 1609459200000) << 22) | (g.machineID << 12) | g.sequence
return fmt.Sprintf("SP%d", id)
}

9.2 商品同步任务治理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import "time"

func FullSync(ctx context.Context, partnerID string) error {
products, err := partnerClient.GetAllProducts(partnerID)
if err != nil {
return err
}
for _, p := range products {
msg := ConvertToMessage(p)
if err := PartnerPushProduct(ctx, msg); err != nil {
log.Error("full sync row failed", "err", err)
}
}
return nil
}

func IncrementalSync(ctx context.Context, partnerID string) error {
last := GetLastSyncTime(partnerID)
products, err := partnerClient.GetChangedProducts(partnerID, last)
if err != nil {
return err
}
for _, p := range products {
msg := ConvertToMessage(p)
if err := PartnerPushProduct(ctx, msg); err != nil {
log.Error("incr sync row failed", "err", err)
}
}
UpdateLastSyncTime(partnerID, time.Now())
return nil
}

热门商品刷新间隔见第 5.3 节 CalculateRefreshInterval

9.3 监控告警体系

  • 业务:上架量、审核通过率、搜索 QPS、缓存命中率
  • 应用:核心接口 P99、错误率、同步成功率
  • 依赖:MySQL 慢查询、Redis 连接、ES 查询延迟
  • 系统:CPU、内存、磁盘、网络
1
2
3
4
5
6
7
8
9
import "time"

func RecordMetrics(spuID string, operation string, latency time.Duration) {
metrics.IncrCounter("product_operation_total", "operation", operation)
metrics.ObserveHistogram("product_operation_latency", latency.Milliseconds(), "operation", operation)
if latency > 100*time.Millisecond {
metrics.IncrCounter("product_operation_slow", "operation", operation)
}
}

9.4 性能优化

  • 数据库:按 SPU ID 哈希分表;组合索引如 (category_id, status);读写分离
  • 缓存:多级缓存、预热 TOP N、布隆过滤器防穿透、空值短 TTL
  • 搜索:合理分片与副本、避免深分页、聚合用近似算法
1
2
3
4
5
6
7
8
9
10
11
12
func ShardIndex(spuID string, shards int) int {
return int(fnv32(spuID) % uint32(shards))
}

func fnv32(s string) uint32 {
var h uint32 = 2166136261
for i := 0; i < len(s); i++ {
h ^= uint32(s[i])
h *= 16777619
}
return h
}
1
2
3
4
// 缓存穿透:布隆过滤器判断「一定不存在」时再短路,避免打穿 DB
func MaybeProductExists(bloom *BloomFilter, spuID string) bool {
return bloom.MightContain(spuID)
}

9.5 故障处理

故障 处理思路
缓存雪崩 TTL 加随机抖动、热点 key 独立策略
缓存穿透 布隆过滤器、空值缓存
数据不一致 定时对账、修复任务、人工兜底
同步失败 重试队列、死信告警
ES 慢查询 优化 mapping、查询裁剪、冷热索引

总结

核心要点回顾:

商品中心是电商平台的「商品库」,核心技术要点包括:

  1. SPU/SKU 模型:标准产品单元 + 库存单位,规格组合与笛卡尔积生成 SKU
  2. 多角色上架:商家、供应商、运营三条链路,配合状态机与审核策略
  3. 异构商品治理:适配器 + 配置化 + 库存网关,隔离品类差异
  4. 多级缓存与搜索:Elasticsearch 负责检索,L1/L2 缓存扛热点读
  5. 版本与快照:版本审计与回滚,快照 Hash 复用服务订单域
  6. 事件驱动product.changed 串联搜索、缓存与下游

面试要点:

  1. 画出 SPU/SKU 关系,说明规格组合如何生成与如何剔除无效组合
  2. 说明异构商品的挑战与适配器、策略模式如何落地
  3. 描述多级缓存与热点刷新策略,以及接受怎样的最终一致窗口
  4. 对比宽表、EAV、混合存储的取舍
  5. 说明订单为何引用商品快照,以及 Hash 复用如何做

扩展阅读:

  • DDD 在商品域的建模与限界上下文划分
  • 商品中台演进与多租户隔离
  • 搜索排序:相关性、商业化、个性化权重

参考资料

业界文章与分享

  1. 淘宝商品中心技术演进相关分享
  2. 京东商品系统架构实践
  3. 亚马逊商品目录(Catalog)设计公开资料

开源项目

  1. Elasticsearch — 搜索引擎
  2. Caffeine — 本地缓存
  3. Excelize — Excel 处理

系列文章(本仓库)

  1. 20-ecommerce-overview.md — 电商总览
  2. 21-ecommerce-listing.md — 商品上架系统
  3. 22-ecommerce-inventory.md — 库存系统
  4. 26-ecommerce-order-system.md — 订单系统

设计过程与章节拆解见仓库内 docs/superpowers/specs/2026-04-07-product-center-design.md

电商系统设计系列 —— 推荐阅读顺序

本系列按照系统依赖层级组织,从基础到应用,循序渐进:

📚 第一层:基础数据层

📊 第二层:业务规则层

💰 第三层:核心交易流程

🔧 第四层:运营管理层

📎 (十一)生命周期与操作语义

一、系统全景架构(EA + 4A 视角)

1.1 业务架构(BA):基于 DDD 的领域全景

视角说明:采用 DDD(领域驱动设计)的核心域、支撑域、通用域三层结构,展示完整的电商业务能力。

graph TB
    subgraph 电商业务架构全景[电商业务架构全景 - DDD 视角]
        subgraph 核心域[🎯 核心域 Core Domain - 核心竞争力]
            direction LR
            CORE1[交易域
订单管理
订单状态
Saga编排] CORE2[支付域
支付流程
退款
清结算] CORE1 --> CORE2 end subgraph 支撑域[📦 支撑域 Supporting Domain - 支撑核心业务] direction TB subgraph 商品供给链 SUP1[商品域
SPU/SKU
类目属性
商品快照] SUP2[库存域
库存管理
预订扣减
对账] SUP3[定价域
价格计算
费用配置
DDD实践] end subgraph 营销运营链 SUP4[营销域
优惠券
活动
补贴] SUP5[商品上架域
流程编排
审核发布
状态机] SUP6[运营管理域
批量管理
配置工具
数据看板] end end subgraph 通用域[🔧 通用域 Generic Domain - 通用能力] direction TB subgraph 用户与权限 GEN1[用户域
注册登录
会员体系
账号安全] GEN2[商家域
商家入驻
商家管理
资质审核] end subgraph 基础服务 GEN3[搜索域
商品搜索
推荐系统
搜索优化] GEN4[消息域
短信/邮件
Push推送
站内信] GEN5[评价域
商品评价
评分体系
DSR] end subgraph 履约与风控 GEN6[物流域
配送
履约
物流跟踪] GEN7[风控域
反欺诈
风险识别
规则引擎] end end end %% 核心域依赖支撑域 CORE1 --> SUP1 CORE1 --> SUP2 CORE1 --> SUP3 CORE2 --> CORE1 %% 支撑域之间的关系 SUP5 --> SUP1 SUP5 --> SUP2 SUP5 --> SUP3 SUP6 --> SUP1 SUP6 --> SUP2 SUP6 --> SUP3 SUP6 --> SUP4 SUP4 --> SUP3 %% 通用域支撑所有域 GEN1 -.->|用户身份| CORE1 GEN2 -.->|商家信息| SUP5 GEN3 -.->|商品发现| SUP1 GEN5 -.->|评价数据| SUP1 GEN6 -.->|履约状态| CORE1 GEN7 -.->|风控检查| CORE1 %% 样式 classDef coreDomain fill:#ffebee,stroke:#c62828,stroke-width:4px,color:#000 classDef supportDomain fill:#e8f5e9,stroke:#2e7d32,stroke-width:3px,color:#000 classDef genericDomain fill:#e3f2fd,stroke:#1565c0,stroke-width:2px,color:#000 class CORE1,CORE2 coreDomain class SUP1,SUP2,SUP3,SUP4,SUP5,SUP6 supportDomain class GEN1,GEN2,GEN3,GEN4,GEN5,GEN6,GEN7 genericDomain

DDD 领域划分说明

域类型 定义 业务价值 投资策略 本系列覆盖
核心域 平台的核心竞争力,差异化能力 最高,决定平台成败 重点投入,自研 ✅ 订单、支付
支撑域 支撑核心业务的必要能力 中等,必须有但非差异化 适度投入,可定制 ✅ 商品、库存、计价、营销、上架、运营
通用域 通用基础能力,行业共性 低,无差异化 最小投入,采购或开源 ❌ 未详述(聚焦核心)

完整领域能力图谱

域层级 子域 核心能力 本系列文章 备注
🎯 核心域 交易域 订单创建、状态流转、Saga 编排 (七)订单系统 电商核心竞争力
支付域 支付流程、退款、清结算、对账 (八)支付系统 资金安全核心
📦 支撑域 商品域 SPU/SKU 模型、类目属性、商品快照 (二)商品中心 交易的基础数据
库存域 库存管理、预订扣减、库存对账 (三)库存系统 保证可售性
定价域 价格计算、费用配置、DDD 实践 (五)(六)计价引擎 价格准确性
营销域 优惠券、活动管理、补贴规则 (四)营销系统 促进转化
商品上架域 流程编排、审核发布、状态机 (九)商品上架 B 端供给流程
运营管理域 批量管理、配置工具、数据看板 (十)B 端运营 B 端管理工具
🔧 通用域 用户域 注册登录、会员体系、账号安全 可采购 SSO
商家域 商家入驻、商家管理、资质审核 通用商家平台
搜索域 商品搜索、推荐系统、搜索优化 可用 ES/推荐引擎
消息域 短信/邮件、Push 推送、站内信 可用消息中间件
评价域 商品评价、评分体系、DSR 通用评价系统
物流域 配送、履约、物流跟踪 可对接三方物流
风控域 反欺诈、风险识别、规则引擎 可用风控平台

本系列聚焦策略

为什么只详述核心域和支撑域?

  1. 面试高频:核心域(订单、支付)和支撑域(商品、库存、计价)是系统设计面试的高频考点
  2. 技术深度:这些域涉及分布式事务、状态机、一致性等核心技术挑战
  3. 差异化设计:不同公司的实现差异大,需要深入理解设计思路
  4. 通用域标准化:用户、搜索、消息等通用域已有成熟解决方案(SSO、ES、Kafka),无需详述

1.2 应用架构(AA):系统依赖与分层

视角说明:从应用系统的角度,展示各系统之间的依赖关系和技术分层。

graph TB
    subgraph Layer1[第一层:基础数据服务层]
        PC[商品中心系统]
    end
    
    subgraph Layer2[第二层:业务规则服务层]
        INV[库存系统]
        MKT[营销系统]
        PRICE[计价引擎]
    end
    
    subgraph Layer3[第三层:核心交易流程层]
        ORDER[订单系统]
        PAY[支付系统]
    end
    
    subgraph Layer4[第四层:B端管理应用层]
        LIST[商品上架系统]
        OPS[B端运营系统]
    end
    
    %% 依赖关系(自下而上)
    PC --> INV
    PC --> PRICE
    PC --> ORDER
    INV --> ORDER
    MKT --> PRICE
    PRICE --> ORDER
    ORDER --> PAY
    
    LIST --> PC
    LIST --> INV
    LIST --> PRICE
    
    OPS --> PC
    OPS --> INV
    OPS --> MKT
    OPS --> PRICE
    
    %% 样式
    classDef layer1 fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
    classDef layer2 fill:#e3f2fd,stroke:#2196f3,stroke-width:2px
    classDef layer3 fill:#fff3e0,stroke:#ff9800,stroke-width:2px
    classDef layer4 fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
    
    class PC layer1
    class INV,MKT,PRICE layer2
    class ORDER,PAY layer3
    class LIST,OPS layer4

应用分层原则

层级 系统 依赖的系统 核心职责 文章编号
第一层 商品中心 - SPU/SKU 数据服务 (二)
第二层 库存系统 商品中心 库存管理与扣减 (三)
营销系统 - 优惠规则与补贴 (四)
计价引擎 商品中心、营销 价格计算与快照 (五)(六)
第三层 订单系统 商品、库存、计价 订单创建与流转 (七)
支付系统 订单 支付、退款、结算 (八)
第四层 商品上架 商品、库存、计价 供给流程编排 (九)
B端运营 商品、库存、营销、计价 管理界面与工具 (十)

依赖方向:箭头指向表示依赖关系,上层依赖下层,下层不依赖上层

1.3 业务流程视角:三条核心流程

1. C端购物流程(对应文章二→七→八)

sequenceDiagram
    participant 用户
    participant 商品中心
    participant 计价引擎
    participant 订单系统
    participant 支付系统

    用户->>商品中心: 1. 浏览商品
    用户->>计价引擎: 2. 计算价格
    计价引擎->>商品中心: 查询商品
    计价引擎-->>用户: 返回价格
    
    用户->>订单系统: 3. 创建订单
    订单系统->>商品中心: 商品快照
    订单系统->>计价引擎: 价格快照
    订单系统-->>用户: 订单创建
    
    用户->>支付系统: 4. 支付
    支付系统-->>订单系统: 支付成功

2. B端供给流程(对应文章九)

sequenceDiagram
    participant 商家
    participant 商品上架
    participant 商品中心
    participant 库存系统
    participant 计价引擎

    商家->>商品上架: 1. 提交商品
    商品上架->>商品上架: 2. 审核流程
    商品上架->>商品中心: 3. 创建商品
    商品上架->>库存系统: 4. 初始化库存
    商品上架->>计价引擎: 5. 配置价格
    商品上架-->>商家: 上架成功

3. B端管理流程(对应文章十)

sequenceDiagram
    participant 运营
    participant B端运营
    participant 商品中心
    participant 计价引擎

    运营->>B端运营: 1. 批量编辑
    B端运营->>商品中心: 2. 更新商品
    B端运营->>计价引擎: 3. 调整价格
    B端运营-->>运营: 更新成功

1.4 架构设计原则

为什么采用 EA + 4A 视角?

架构视角 关注点 价值 本系列覆盖
BA(业务架构) 业务能力和业务流程 理解”做什么” ✅ 1.1 节
AA(应用架构) 系统划分和依赖关系 理解”怎么做” ✅ 1.2 节
DA(数据架构) 数据模型和数据流 理解”数据在哪” 各文章详述
TA(技术架构) 技术选型和基础设施 理解”用什么技术” 各文章详述

应用分层设计原则

  1. 单向依赖:上层依赖下层,避免循环依赖
  2. 职责清晰:每层有明确的职责边界(数据/规则/流程/界面)
  3. 可测试性:下层系统可独立测试
  4. 可扩展性:新系统遵循分层原则插入对应层级

业务与系统的映射关系

业务域 核心能力 应用系统 文章编号
C端业务域 商品发现 商品中心 (二)
购物决策 库存系统、计价引擎、营销系统 (三)(四)(五)(六)
交易履约 订单系统、支付系统 (七)(八)
B端业务域 商品供给 商品上架系统 (九)
商品管理 B端运营系统 (十)

关键洞察

  • 业务架构 回答”平台提供哪些业务能力”
  • 应用架构 回答”如何用系统实现这些能力”
  • 商品上架系统 是流程编排器(非数据存储)
  • B端运营系统 是管理界面(非业务逻辑)
  • 商品中心/库存/计价 是数据服务(被创建、被修改)

推荐学习路径

  1. 先看业务架构(1.1):理解平台的业务能力全景
  2. 再看应用架构(1.2):理解系统如何分层实现业务
  3. 按文章顺序学习:从基础数据层 → 业务规则层 → 交易流程 → 运营管理

1.5 B端商品生命周期操作全景

关键问题:商品上架、商品信息编辑、价格编辑、上传库存、库存修改,这些操作属于什么模块?

答案:这些操作分属于商品生命周期的两个阶段:

1.5.1 商品生命周期两阶段

阶段 主要系统 核心操作 操作者 特点
供给阶段 商品上架系统(LIST) 商品上架、初始信息录入、初始价格、初始库存 商家/供应商/运营 从无到有,需审核流程
管理阶段 B端运营系统(OPS) 商品信息编辑、价格编辑、库存修改 运营人员 日常维护,直接修改

1.5.2 B端操作与系统模块关系

sequenceDiagram
    participant 商家运营
    participant 商品上架系统
    participant 商品中心
    participant 库存系统
    participant 计价引擎
    participant B端运营系统

    Note over 商家运营,计价引擎: 阶段1:商品供给(从无到有)
    
    商家运营->>商品上架系统: 1. 创建商品上架任务
    Note right of 商品上架系统: 录入:商品信息 + 初始价格 + 初始库存
    
    商品上架系统->>商品上架系统: 2. 状态机流转
(草稿→待审核→审核通过) 商品上架系统->>商品中心: 3. 发布商品信息
(SPU/SKU创建) 商品上架系统->>库存系统: 4. 初始化库存
(上传库存) 商品上架系统->>计价引擎: 5. 配置基础价格 Note over 商家运营,B端运营系统: 阶段2:商品管理(日常维护) 商家运营->>B端运营系统: 6. 商品信息编辑请求 B端运营系统->>商品中心: 7. 更新商品信息 商品中心-->>B端运营系统: 更新成功 商家运营->>B端运营系统: 8. 价格编辑请求 B端运营系统->>计价引擎: 9. 更新价格配置 计价引擎-->>B端运营系统: 更新成功 商家运营->>B端运营系统: 10. 库存修改请求 B端运营系统->>库存系统: 11. 调整库存数量 库存系统-->>B端运营系统: 更新成功 B端运营系统-->>商家运营: 操作完成

1.5.3 操作详细归属

操作 所属阶段 主要系统 数据最终存储 操作特点
商品上架 供给阶段 商品上架系统 商品中心 流程编排,状态机管理,需审核
商品信息录入(上架时) 供给阶段 商品上架系统 商品中心 初始数据,通过上架流程创建
初始价格配置 供给阶段 商品上架系统 计价引擎 基础价格配置,通过上架流程创建
上传库存(初始化) 供给阶段 商品上架系统 库存系统 初始库存,通过上架流程创建
商品信息编辑 管理阶段 B端运营系统 商品中心 已上架商品的信息维护
价格编辑 管理阶段 B端运营系统 计价引擎 价格调整、促销配置
库存修改 管理阶段 B端运营系统 库存系统 库存补货、调整

1.5.4 关键设计原则

1. 职责分离

  • 商品上架系统:负责”流程编排”(状态机、审核、工作流)
  • 商品中心/库存/计价:负责”数据存储”(商品、库存、价格的数据服务)
  • B端运营系统:负责”管理界面”(统一的运营工具入口)

2. 数据流向

1
2
供给阶段:商品上架系统 → 商品中心/库存/计价(创建数据)
管理阶段:B端运营系统 → 商品中心/库存/计价(修改数据)

3. 为什么要分两个阶段?

  • 审核控制:上架阶段需要严格的审核流程(防止垃圾商品),管理阶段可以直接修改(已审核商品)
  • 权限分离:商家可以上架商品,但只有运营可以批量管理所有商品
  • 流程复杂度:上架是复杂的异步流程(状态机),编辑是简单的CRUD操作

二、背景与挑战

2.1 业务背景

在数字电商/本地生活平台中,商品管理、库存管理、价格管理是三大核心支柱系统,它们相互依赖、紧密协作,共同支撑着平台的商品从上架到售卖的完整生命周期。

1
2
3
4
商品上架 → 库存同步 → 价格配置 → 用户浏览列表 → 查看详情 → 加入购物车 → 用户下单 → 库存扣减 → 订单履约
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
商品信息 库存状态 基础定价 批量价格计算 实时价格 价格快照 订单创建 预订/售出 发货/核销
(缓存优化) (促销匹配) (30分钟)

1.2 多品类差异与挑战

不同品类在商品属性、库存特性、定价逻辑上差异极大:

品类 商品特点 库存特性 价格特点 典型示例
电子券 (Deal) 券码制,每券唯一 券码池,预订扣减 面值 vs 售价 咖啡店电子券
虚拟服务券 (OPV) 数量制,分平台统计 数量制,预订扣减 固定价 + 促销 美甲服务券
酒店 (Hotel) 房型 × 日期 时间维度库存 日历价 + 动态定价 在线酒店预订
电影票 (Movie) 场次 × 座位 × 票种 座位制库存 场次定价 + Fee IMAX 电影票
机票/票务 航班 × 舱位 座位/场次制 动态定价 航班经济舱
礼品卡 (Giftcard) 实时生成或预采购 券码制 / 无限 面值定价 应用商店充值卡
话费充值 (TopUp) 面额制 无限库存 面额 + 折扣 手机话费充值
本地生活套餐 组合型,多子项 组合库存联动 套餐价 + 子项加总 火锅双人套餐

1.3 核心痛点

1.3.1 商品管理痛点

  1. 流程不统一:每个品类上架流程各异,代码无法复用
  2. 状态管理混乱:草稿、审核、上线、下线等状态散落在不同表中
  3. 供应商对接不统一:推送/拉取/API 各自实现,缺乏标准化
  4. 审核策略不灵活:无法根据数据来源(供应商/运营/商家)动态调整审核策略

1.3.2 库存管理痛点

  1. 模型割裂:每个品类独立设计库存逻辑,无法复用
  2. 数据不一致:Redis 与 MySQL 之间、预订数量与实际状态脱节
  3. 供应商策略不统一:实时查询、定时同步、推送等策略混乱
  4. 缺乏统一服务:业务方直接操作 DB/Redis,维护成本高
  5. 监控缺失:超卖、库存差异、供应商同步延迟难以发现

1.3.3 价格管理痛点

  1. 价格散落多表:基础价、营销价、费用、优惠券分散在不同模块
  2. 计算逻辑分散:各品类各自实现价格计算,重复代码多
  3. 营销活动隔离:促销规则硬编码在业务逻辑中,扩展性差
  4. Fee 管理混乱:平台手续费、商户服务费、合作方费用等缺乏统一配置
  5. 优惠券叠加复杂:多种优惠方式叠加规则不清晰
  6. 审计困难:价格变更历史难以追溯,无法准确还原计算过程

1.4 设计目标

目标 说明 优先级
统一模型 商品、库存、价格共用一套统一模型,多品类复用 P0
高性能 支持万级 QPS 秒杀场景,P99 < 100ms P0
灵活扩展 新品类接入无需修改核心代码 P0
最终一致 Redis 与 MySQL 数据最终一致 P0
异步化 上传、审核、发布、价格快照异步化 P0
状态可追溯 完整的状态变更历史记录 P0
供应商集成 支持实时/定时/推送多种同步策略 P1
多级降级 促销/优惠券服务不可用时,仍能返回基础价格 P1

电商系统整体架构设计

三大系统协作架构

2.1 三大系统总览

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
┌─────────────────────────────────────────────────────────────────────────────┐
│ 统一商品·库存·价格管理平台 │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ 商品上架管理系统 (Listing) │ │
│ │ 数据来源: 运营/商家/供应商Push/供应商Pull/API │ │
│ │ 核心流程: DRAFT → Pending Audit → Approved → Online │ │
│ │ 策略: 审核策略路由(免审/自动审核/人工审核/快速通道) │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ 统一库存管理系统 (Inventory) │ │
│ │ 管理类型: 自管理/供应商管理/无限库存 │ │
│ │ 单元类型: 券码制/数量制/时间维度/组合型 │ │
│ │ 核心操作: BookStock / UnbookStock / SellStock / RefundStock │ │
│ │ 存储: Redis(热) + MySQL(冷) + Kafka(事件) │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ 统一价格管理系统 (Pricing) │ │
│ │ 四层架构: Base Price → Promotion → Fee → Voucher │ │
│ │ 核心能力: 价格计算引擎 / 营销匹配器 / 费用计算器 / 优惠券应用器 │ │
│ │ 降级策略: 5级降级(促销/费用/优惠券可降级) │ │
│ │ 审计: 价格快照 + 变更日志 + 人类可读公式 │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ 订单服务 (Order Service) │ │
│ │ 下单 → 价格锁定(快照) → 库存预订 → 支付 → 库存售出 → 履约 │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘

2.2 分层服务架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
┌───────────────────────────────────────────────────────────────────────────┐
│ API Gateway / BFF │
└───────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────┼─────────────────────────────┐
│ │ │
▼ ▼ ▼
┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐
│ Listing Service │ │ Inventory Service │ │ Pricing Service │
│ ───────────── │ │ ─────────────── │ │ ───────────── │
│ • 上架API │ │ • 库存查询 │ │ • 价格计算API │
│ • 审核API │ │ • 库存预订 │ │ • 快照API │
│ • 发布API │ │ • 库存售出 │ │ • 审计API │
│ • 状态机引擎 │ │ • 库存退还 │ │ • 价格公式 │
│ │ │ │ │ │
│ Workers: │ │ Strategies: │ │ Calculators: │
│ • ExcelParser │ │ • SelfManaged │ │ • BasePriceCalc │
│ • AuditWorker │ │ • SupplierManaged │ │ • PromotionMatch │
│ • PublishWorker │ │ • Unlimited │ │ • FeeCalculator │
│ • Watchdog │ │ • Estimated │ │ • VoucherApplier │
└───────────────────┘ └───────────────────┘ └───────────────────┘
│ │ │
└─────────────────────────────┼─────────────────────────────┘

┌───────────────────────────────────────────────────────────────────────────┐
│ Infrastructure & Data Layer │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ MySQL │ │ Redis │ │ Kafka │ │ ES │ │ OSS │ │
│ │ (分库表) │ │ Cluster │ │ Events │ │ Search │ │ Files │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
└───────────────────────────────────────────────────────────────────────────┘

2.3 核心设计思想

  1. 统一模型 + 策略模式

    • 商品管理:统一状态机 + 审核策略路由
    • 库存管理:(ManagementType, UnitType) 二维分类 + 策略接口
    • 价格管理:四层计算架构 + 可插拔规则引擎
  2. 异步化 + 事件驱动

    • 所有耗时操作(文件解析、审核、发布、快照)通过 Kafka + Worker 异步处理
    • 每个状态变更都发送 Kafka 事件,下游消费者解耦处理
  3. 多级缓存 + 降级保障

    • L1 本地缓存 + L2 Redis + L3 MySQL,保证高性能
    • 5级降级策略,保证核心链路不中断
  4. 数据一致性保障

    • Redis 是热路径,MySQL 是权威数据源
    • Kafka 异步持久化,定时对账修复
  5. 审计与追溯

    • 价格快照保留完整计算明细
    • 库存操作日志留痕
    • 状态变更历史完整记录

2.4 核心业务流

平台业务可以划分为三大核心流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
┌────────────────────────────────────────────────────────────────────────┐
│ 三大核心业务流 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ 流程一:商品管理 B 端流程 (Listing Management - B2B Operations) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 【商品供给侧】 │ │
│ │ 供应商/运营/商家 → 批量上传/API推送 → 审核 → 发布 → 商品上线 │ │
│ │ • Excel 批量导入(单次最多 10000 SKU) │ │
│ │ • 供应商 Push/Pull(实时同步/定时拉取) │ │
│ │ • 运营后台表单(单品/批量编辑) │ │
│ │ │ │
│ │ 【运营管理侧】 │ │
│ │ 商品编辑 → 价格调整 → 库存管理 → 类目维护 → 首页配置 │ │
│ │ • 价格批量调整(促销价、成本价) │ │
│ │ • 库存批量设置(导入券码、设置库存数) │ │
│ │ • Entrance/Group 首页入口配置 │ │
│ │ • Tag 标签管理(推荐、热门、新品) │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ 流程二:用户交易流 (User Journey - C2C Customer Facing) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 首页浏览 → 搜索/筛选 → 查看详情 → 加购 → 下单 → 支付 → 查看订单│ │
│ │ • 列表页:批量价格计算 + 库存展示 │ │
│ │ • 详情页:实时价格 + 促销匹配 + 库存校验 │ │
│ │ • 购物车:价格快照锁定(30分钟) │ │
│ │ • 下单:价格验证 + 库存预订 + 订单创建 │ │
│ │ • 支付:支付中台 + 优惠券核销 + 积分抵扣 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ 流程三:系统履约流 (System Fulfillment - Backend Processing) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 支付回调 → 库存确认 → 供应商履约 → 券码发放 → 订单完成 │ │
│ │ • 库存售出:booking → sold,Kafka 异步落库 │ │
│ │ • 供应商履约:调供应商平台 API 创建订单/出票 │ │
│ │ • 券码发放:电子券/礼品卡卡密展示 │ │
│ │ • 退款处理:库存回退 + 优惠券/配额归还 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────┘

2.4.1 流程职责划分

业务流 核心系统 主要用户 职责范围 关键指标
商品管理 B 端流程 Listing + Inventory + Pricing 供应商、运营、商家 商品供给(上架/审核/发布)
运营管理(批量编辑/价格调整/库存管理/配置发布)
上架成功率、审核通过率
供应商同步延迟、操作效率
用户交易 C 端流程 Pricing + Inventory + Order 终端用户(消费者) 商品浏览、价格展示、库存查询
下单、支付、订单查询
转化率、下单成功率
支付成功率
系统履约流 Order + Inventory + Supplier Platform 系统自动化 支付回调处理、库存确认
供应商履约、券码发放、退款处理
履约成功率、履约时长
退款处理时长

三大流程的关系

  • B 端流程:负责”供给”,确保平台有丰富的商品可售
  • C 端流程:负责”销售”,为用户提供流畅的购买体验
  • 履约流程:负责”交付”,确保订单正确履约和售后处理

业务流 (business process)


E-commerce process

系统流 (system process)


E-commerce whole process of system

统一术语和关键实体

核心业务术语

商品相关术语

  • **SPU (Standard Product Unit)**:标准化产品单元,描述商品的通用属性(如”iPhone 15”)
  • **SKU (Stock Keeping Unit)**:库存量单位,最小销售单元(如”iPhone 15 256GB 黑色”)
  • **商品 (Item/Product)**:平台上可销售的实体
  • **类目 (Category)**:商品分类体系,通常为多级树状结构
  • **品牌 (Brand)**:商品品牌标识
  • **属性 (Attribute)**:商品的描述信息,分为销售属性(影响SKU)和非销售属性

交易相关术语

  • **订单 (Order)**:用户购买商品的请求记录
  • **子订单 (Sub-Order)**:按店铺或仓库拆分的订单
  • **购物车 (Cart)**:用户临时存放待购商品的容器
  • **支付单 (Payment)**:订单的支付凭证
  • **退款单 (Refund)**:退款请求和处理记录

库存相关术语

  • **库存 (Inventory/Stock)**:商品的可售数量
  • **占用库存 (Reserved Stock)**:已下单未支付占用的库存
  • **真实库存 (Available Stock)**:实际可销售的库存
  • **安全库存 (Safety Stock)**:为防止缺货设置的最低库存量

用户相关术语

  • **买家 (Buyer/Customer)**:购买商品的用户
  • **卖家 (Seller/Merchant)**:商品的提供方
  • **会员等级 (Membership Level)**:用户等级体系
  • **收货地址 (Shipping Address)**:订单配送地址

营销相关术语

  • **优惠券 (Coupon)**:折扣凭证
  • **促销活动 (Promotion)**:限时折扣、满减等营销活动
  • **秒杀 (Flash Sale)**:限时限量抢购活动
  • **拼团 (Group Buy)**:多人成团购买

物流相关术语

  • **物流单 (Logistics Order)**:配送信息记录
  • **运单号 (Tracking Number)**:快递追踪编号
  • **仓库 (Warehouse)**:商品存储和配送中心
  • **配送状态 (Delivery Status)**:待发货、已发货、配送中、已签收等

关键实体定义

1. 用户实体 (User)

1
2
3
4
5
6
7
8
9
属性:
- 用户ID (user_id)
- 用户名 (username)
- 手机号 (mobile)
- 邮箱 (email)
- 用户类型 (user_type): 买家/卖家
- 会员等级 (membership_level)
- 注册时间 (created_at)
- 状态 (status): 正常/冻结/注销

2. 商品实体 (Product/Item)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
SPU 属性:
- SPU ID (spu_id)
- 商品名称 (product_name)
- 类目ID (category_id)
- 品牌ID (brand_id)
- 商品描述 (description)
- 商品图片 (images)
- 基本属性 (base_attributes)

SKU 属性:
- SKU ID (sku_id)
- SPU ID (spu_id)
- 销售属性 (sale_attributes): 颜色、尺寸等
- 价格 (price)
- 库存 (stock)
- SKU编码 (sku_code)
- 状态 (status): 上架/下架/售罄

3. 订单实体 (Order)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
属性:
- 订单ID (order_id)
- 用户ID (user_id)
- 订单编号 (order_no)
- 订单总额 (total_amount)
- 实付金额 (paid_amount)
- 优惠金额 (discount_amount)
- 订单状态 (status): 待支付/已支付/已发货/已完成/已取消
- 收货地址ID (address_id)
- 支付方式 (payment_method)
- 下单时间 (created_at)
- 支付时间 (paid_at)

订单明细 (Order Item):
- 明细ID (item_id)
- 订单ID (order_id)
- SKU ID (sku_id)
- 商品名称 (product_name)
- 数量 (quantity)
- 单价 (price)
- 小计 (subtotal)

4. 库存实体 (Inventory)

1
2
3
4
5
6
7
8
9
属性:
- 库存ID (inventory_id)
- SKU ID (sku_id)
- 仓库ID (warehouse_id)
- 总库存 (total_stock)
- 可用库存 (available_stock)
- 占用库存 (reserved_stock)
- 安全库存 (safety_stock)
- 更新时间 (updated_at)

5. 支付实体 (Payment)

1
2
3
4
5
6
7
8
9
属性:
- 支付ID (payment_id)
- 订单ID (order_id)
- 支付编号 (payment_no)
- 支付金额 (amount)
- 支付方式 (method): 微信/支付宝/银行卡
- 支付状态 (status): 待支付/已支付/已退款
- 第三方流水号 (transaction_id)
- 支付时间 (paid_at)

6. 物流实体 (Logistics)

1
2
3
4
5
6
7
8
9
属性:
- 物流ID (logistics_id)
- 订单ID (order_id)
- 物流公司 (logistics_company)
- 运单号 (tracking_number)
- 发货时间 (shipped_at)
- 签收时间 (received_at)
- 物流状态 (status)
- 物流轨迹 (tracking_info)

核心领域模型和 ER 图

电商系统核心实体关系图 (ER Diagram)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
┌─────────────┐         ┌─────────────┐         ┌─────────────┐
│ Category │ │ Brand │ │ Attribute │
│ (类目) │ │ (品牌) │ │ (属性) │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ 1:N │ 1:N │ 1:N
│ │ │
└───────────────────────┴────────────────────────┘


┌──────────────┐
│ SPU │◄──────────┐
│ (标准品) │ │
└──────┬───────┘ │
│ 1:N │
▼ │ N:N
┌──────────────┐ │
│ SKU │───────────┘
│ (库存单位) │
└──────┬───────┘

┌──────────────┼──────────────┐
│ │ │
▼ N:1 ▼ 1:1 ▼ N:N
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ OrderItem │ │ Inventory │ │ Promotion │
│ (订单明细) │ │ (库存) │ │ (促销) │
└──────┬───────┘ └──────────────┘ └──────────────┘
│ N:1

┌──────────────┐
│ Order │
│ (订单) │
└──────┬───────┘

┌───────┼───────┬─────────┐
│ 1:1 │ 1:1 │ N:1 │
▼ ▼ ▼ ▼
┌────────┐ ┌────┐ ┌──────┐ ┌──────┐
│Payment │ │Logis│ │ User │ │Addr │
│ (支付) │ │tics │ │(用户)│ │ess │
└────────┘ └─────┘ └──────┘ └──────┘

详细 ER 关系说明

1. 商品域关系

  • Category → SPU (1:N): 一个类目包含多个SPU
  • Brand → SPU (1:N): 一个品牌包含多个SPU
  • SPU → SKU (1:N): 一个SPU对应多个SKU
  • Attribute ← → SKU (N:N): SKU与属性多对多关系

2. 交易域关系

  • User → Order (1:N): 一个用户可以有多个订单
  • Order → OrderItem (1:N): 一个订单包含多个商品明细
  • SKU → OrderItem (1:N): 一个SKU可以在多个订单中
  • Order → Payment (1:1): 一个订单对应一个支付记录
  • Order → Logistics (1:1或1:N): 一个订单对应一个或多个物流单

3. 库存域关系

  • SKU → Inventory (1:1或1:N): 一个SKU在一个或多个仓库有库存
  • Warehouse → Inventory (1:N): 一个仓库管理多个SKU库存

4. 营销域关系

  • Promotion ← → SKU (N:N): 促销活动与商品多对多
  • Coupon → User (N:N): 优惠券与用户多对多
  • Order → Coupon (N:N): 订单可使用多张优惠券

领域驱动设计架构 (DDD)

电商系统领域划分

1. 用户域 (User Domain)

  • 核心功能: 用户注册、登录、个人信息管理、会员体系
  • 聚合根: User (用户)
  • 实体: 用户、用户资料、收货地址、会员等级
  • 值对象: 手机号、邮箱、身份证号
  • 领域服务: 用户认证服务、会员升级服务

2. 商品域 (Product Domain)

  • 核心功能: 商品创建、上下架、属性管理、类目管理
  • 聚合根: SPU (标准商品单元)
  • 实体: SPU、SKU、类目、品牌、属性
  • 值对象: 商品编码、价格、商品图片
  • 领域服务: 商品搜索服务、商品审核服务、价格计算服务

3. 交易域 (Order Domain)

  • 核心功能: 购物车、下单、支付、订单管理
  • 聚合根: Order (订单)
  • 实体: 订单、订单明细、购物车、支付记录
  • 值对象: 订单号、金额、订单状态
  • 领域服务: 订单创建服务、价格计算服务、订单状态机

4. 库存域 (Inventory Domain)

  • 核心功能: 库存管理、库存扣减、库存同步
  • 聚合根: Inventory (库存)
  • 实体: 库存记录、库存流水、仓库
  • 值对象: 库存数量、仓库编码
  • 领域服务: 库存扣减服务、库存预占服务、库存释放服务

5. 营销域 (Marketing Domain)

  • 核心功能: 优惠券、促销活动、满减、秒杀
  • 聚合根: Promotion (促销活动)
  • 实体: 优惠券、促销规则、活动商品
  • 值对象: 折扣规则、使用条件
  • 领域服务: 优惠计算服务、活动资格校验服务

6. 物流域 (Logistics Domain)

  • 核心功能: 发货、配送、物流追踪
  • 聚合根: LogisticsOrder (物流单)
  • 实体: 物流单、物流轨迹、快递公司
  • 值对象: 运单号、收货地址
  • 领域服务: 物流路由服务、配送状态更新服务

7. 支付域 (Payment Domain)

  • 核心功能: 支付、退款、对账
  • 聚合根: Payment (支付单)
  • 实体: 支付记录、退款记录、支付渠道
  • 值对象: 支付流水号、支付金额
  • 领域服务: 支付路由服务、退款服务、对账服务

领域架构分层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
┌──────────────────────────────────────────────────────┐
│ 用户接口层 (User Interface) │
│ Web、Mobile、API Gateway、Admin Console │
└────────────────────┬─────────────────────────────────┘

┌────────────────────┴─────────────────────────────────┐
│ 应用服务层 (Application Layer) │
│ OrderAppService、ProductAppService、UserAppService │
│ - 编排领域服务 │
│ - 事务管理 │
│ - 权限控制 │
└────────────────────┬─────────────────────────────────┘

┌────────────────────┴─────────────────────────────────┐
│ 领域层 (Domain Layer) │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 商品域 │ │ 交易域 │ │ 库存域 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ 用户域 │ │ 营销域 │ │ 物流域 │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
│ - 领域模型 (Entities、Value Objects、Aggregates) │
│ - 领域服务 (Domain Services) │
│ - 领域事件 (Domain Events) │
│ - 仓储接口 (Repository Interfaces) │
└────────────────────┬─────────────────────────────────┘

┌────────────────────┴─────────────────────────────────┐
│ 基础设施层 (Infrastructure Layer) │
│ - 仓储实现 (MySQL、Redis、MongoDB) │
│ - 消息队列 (Kafka、RabbitMQ) │
│ - 缓存 (Redis、Memcached) │
│ - 第三方服务 (支付网关、物流接口) │
│ - 配置中心、服务注册发现 │
└──────────────────────────────────────────────────────┘

领域事件驱动

核心领域事件

  1. OrderCreatedEvent (订单创建事件)

    • 触发: 用户下单成功
    • 订阅者: 库存服务(扣减库存)、营销服务(更新活动数据)、消息服务(发送通知)
  2. OrderPaidEvent (订单支付事件)

    • 触发: 支付成功
    • 订阅者: 订单服务(更新状态)、物流服务(准备发货)、会员服务(积分增加)
  3. OrderCancelledEvent (订单取消事件)

    • 触发: 用户或系统取消订单
    • 订阅者: 库存服务(释放库存)、支付服务(退款)、营销服务(返还优惠券)
  4. InventoryInsufficientEvent (库存不足事件)

    • 触发: 库存检查失败
    • 订阅者: 订单服务(取消订单)、消息服务(通知用户)
  5. ProductUpdatedEvent (商品更新事件)

    • 触发: 商品信息变更
    • 订阅者: 搜索服务(更新索引)、缓存服务(清除缓存)、推荐服务(更新模型)

限界上下文 (Bounded Context)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
电商系统限界上下文划分:

┌──────────────────────────────────────────────────────┐
│ 前台购物上下文 │
│ - 商品展示、搜索、购物车、下单 │
└──────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────┐
│ 商家管理上下文 │
│ - 商品发布、订单管理、营销活动配置 │
└──────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────┐
│ 平台运营上下文 │
│ - 商品审核、类目管理、数据分析、风控 │
└──────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────┐
│ 履约配送上下文 │
│ - 仓储管理、库存管理、物流配送 │
└──────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────┐
│ 财务结算上下文 │
│ - 支付、退款、对账、结算 │
└──────────────────────────────────────────────────────┘

产品架构 (Product Structure/组织架构)


E-commerce product structure

应用架构

技术架构tech architecture

数据架构

C 端用户旅程

本章涵盖:本章描述面向终端用户(消费者)的 C 端交易流程,从首页浏览到支付完成的完整用户旅程,包括商品展示、价格计算、库存校验、下单支付等核心环节。

5.1 完整用户旅程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
┌────────────────────────────────────────────────────────────────────────┐
│ 用户交易完整旅程 (User Journey) │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ Phase 1: 首页浏览 (Homepage Browsing) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 用户打开 APP → 加载首页 Entrance/Group 配置 │ │
│ │ • 拉取首页配置(CDN + Redis,分散热 Key) │ │
│ │ • 展示品类卡片(电影/酒店/美食/娱乐/充值) │ │
│ │ • 展示营销 Banner(秒杀/新人专享/限时特惠) │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Phase 2: 商品列表 (Product List) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 用户点击品类 → 进入商品列表 │ │
│ │ • ES 搜索(按类目/Tag/筛选条件) │ │
│ │ • 批量价格计算(BatchCalculate,20-50 商品/页) │ │
│ │ • 库存状态展示(有货/缺货/少量库存) │ │
│ │ • 促销标签展示(限时特惠/新人专享/买一送一) │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Phase 3: 商品详情 (Item Detail) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 用户点击商品 → 进入详情页 │ │
│ │ • 查询商品详情(L1 本地缓存 → L2 Redis → L3 MySQL) │ │
│ │ • 实时价格计算(Calculate API) │ │
│ │ - Base Price: 450฿ │ │
│ │ - Promotion: -50฿ (新人立减) │ │
│ │ - Fee: +15฿ (平台手续费) │ │
│ │ - Final: 415฿ │ │
│ │ • 库存实时查询(Redis CheckStock) │ │
│ │ • SKU 切换(规格选择) │ │
│ │ • 推荐商品("你可能还喜欢") │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Phase 4: 加入购物车 (Add to Cart) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 用户点击"加入购物车" │ │
│ │ • 创建购物车项(cart_item_tab) │ │
│ │ • 生成价格快照(30 分钟有效期) │ │
│ │ • 锁定价格公式和优惠明细 │ │
│ │ • 展示"已加入购物车,共 N 件商品" │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Phase 5: 购物车结算 (Cart Checkout) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 用户进入购物车 → 点击"去结算" │ │
│ │ • 批量验证价格快照(是否过期?) │ │
│ │ - 未过期:使用快照价格 │ │
│ │ - 已过期:重新计算 → 价格变动提示用户 │ │
│ │ • 优惠券选择(展示可用券列表) │ │
│ │ • 实时校验库存(批量 CheckStock) │ │
│ │ • 计算订单总价(Subtotal + Fee - Voucher) │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Phase 6: 创建订单 (Create Order) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 用户点击"提交订单" │ │
│ │ • 验证价格快照(最后一次检查) │ │
│ │ • 库存预订(BookStock,Redis 原子扣减) │ │
│ │ - 成功:booking_stock += quantity │ │
│ │ - 失败:返回"库存不足,请选择其他商品" │ │
│ │ • 营销配额扣减(促销活动/优惠券配额) │ │
│ │ • 创建订单(order_tab,status=PENDING_PAYMENT) │ │
│ │ • 返回订单号 + 支付二维码 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Phase 7: 支付 (Payment) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 用户扫码支付 / 使用电子钱包 / 积分抵扣 │ │
│ │ • 跳转支付中台(统一收银台) │ │
│ │ • 选择支付方式(钱包余额/信用卡/借记卡) │ │
│ │ • 积分部分抵扣(100 积分 = 1฿) │ │
│ │ • 优惠券最终核销(锁定优惠券,扣减配额) │ │
│ │ • 支付成功 → Webhook 回调订单服务 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Phase 8: 查看订单 (View Order) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 支付成功 → 跳转订单详情页 │ │
│ │ • 订单状态:PAID → PROCESSING → COMPLETED │ │
│ │ • 展示价格明细(Base/Promotion/Fee/Voucher/Final) │ │
│ │ • 券码展示(电子券直接可用) │ │
│ │ • 履约进度(订单创建 → 供应商确认 → 券码发放) │ │
│ │ • 订单操作:申请退款 / 联系客服 / 查看详情 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────┘

5.2 关键节点详细流程

5.2.1 列表页批量价格计算

场景:用户浏览商品列表,单页展示 20-50 件商品,需要批量计算价格并展示促销信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 列表页批量价格计算(优化版)
func (s *ListPageService) LoadProductList(ctx context.Context, req *ListPageRequest) (*ListPageResponse, error) {
// 1. ES 搜索商品(按类目/Tag/筛选条件)
esResp, _ := s.esClient.Search(ctx, &ESSearchRequest{
CategoryID: req.CategoryID,
Tags: req.Tags,
Filters: req.Filters,
Page: req.Page,
PageSize: 20,
})

// 2. 提取 SKU ID 列表
skuIDs := make([]int64, 0)
for _, item := range esResp.Items {
skuIDs = append(skuIDs, item.DefaultSKUID)
}

// 3. 批量价格计算(单次调用)
priceReqs := make([]*PriceRequest, len(skuIDs))
for i, skuID := range skuIDs {
priceReqs[i] = &PriceRequest{
SKUID: skuID,
Quantity: 1,
UserID: req.UserID,
}
}
priceResults, _ := s.pricingEngine.BatchCalculate(ctx, priceReqs)

// 4. 批量库存查询(Redis Pipeline)
stockResults, _ := s.inventoryService.BatchCheckStock(ctx, skuIDs)

// 5. 组装返回结果
products := make([]*ProductCard, 0)
for i, item := range esResp.Items {
products = append(products, &ProductCard{
ItemID: item.ItemID,
Title: item.Title,
ImageURL: item.ImageURL,
BasePrice: priceResults[i].BasePrice,
FinalPrice: priceResults[i].FinalPrice,
PromotionTag: priceResults[i].PromotionTag, // "限时特惠", "新人专享"
StockStatus: stockResults[i].Status, // "有货", "缺货", "少量库存"
})
}

return &ListPageResponse{Products: products}, nil
}

性能优化

  • ES 搜索 + 批量价格计算:P99 < 150ms
  • 本地缓存命中:80%+,P99 < 50ms
  • Redis Pipeline 批量查库存:10ms/次

5.2.2 详情页实时价格计算

场景:用户进入详情页,需要实时计算价格并展示促销活动、优惠券、费用明细。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 详情页实时价格计算
func (s *DetailPageService) LoadItemDetail(ctx context.Context, req *DetailPageRequest) (*DetailPageResponse, error) {
// 1. 查询商品详情(多级缓存)
item, _ := s.itemService.GetByID(ctx, req.ItemID)

// 2. 实时价格计算
priceResp, _ := s.pricingEngine.Calculate(ctx, &PriceRequest{
ItemID: req.ItemID,
SKUID: req.SKUID,
Quantity: req.Quantity,
UserID: req.UserID,
VoucherCodes: req.VoucherCodes, // 用户选择的优惠券
})

// 3. 库存实时查询
stockResp, _ := s.inventoryService.CheckStock(ctx, req.SKUID)

// 4. 查询可用优惠券
vouchers, _ := s.voucherService.GetAvailableVouchers(ctx, req.UserID, req.ItemID)

// 5. 推荐商品(协同过滤)
recommendations, _ := s.recommendService.GetRecommendations(ctx, req.UserID, req.ItemID)

return &DetailPageResponse{
Item: item,
Price: &PriceDetail{
BasePrice: priceResp.BasePrice,
PromotionDiscount: priceResp.PromotionDiscount,
PromotionDetails: priceResp.PromotionDetails, // 具体促销活动列表
TotalFee: priceResp.TotalFee,
FeeDetails: priceResp.FeeDetails, // 平台手续费, 商户服务费, 其他费用
VoucherDiscount: priceResp.VoucherDiscount,
FinalPrice: priceResp.FinalPrice,
PriceFormula: priceResp.PriceFormula, // "450฿ - 50฿ + 15฿ = 415฿"
},
Stock: &StockInfo{
Available: stockResp.AvailableStock,
Status: stockResp.Status, // "有货" / "仅剩 5 件" / "缺货"
},
Vouchers: vouchers,
Recommendations: recommendations,
}, nil
}

5.2.3 购物车价格快照

场景:用户加入购物车,需要锁定价格 30 分钟,避免结算时价格变动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 加入购物车 + 价格快照
func (s *CartService) AddToCart(ctx context.Context, req *AddToCartRequest) (*AddToCartResponse, error) {
// 1. 实时计算价格
priceResp, _ := s.pricingEngine.Calculate(ctx, &PriceRequest{
ItemID: req.ItemID,
SKUID: req.SKUID,
Quantity: req.Quantity,
UserID: req.UserID,
})

// 2. 生成价格快照(30 分钟有效)
snapshot := &PriceSnapshot{
SnapshotCode: generateSnapshotCode(),
UserID: req.UserID,
SKUID: req.SKUID,
Quantity: req.Quantity,
BasePrice: priceResp.BasePrice,
PromotionDiscount: priceResp.PromotionDiscount,
PromotionDetails: priceResp.PromotionDetails,
TotalFee: priceResp.TotalFee,
FeeDetails: priceResp.FeeDetails,
FinalPrice: priceResp.FinalPrice,
PriceFormula: priceResp.PriceFormula,
ExpiredAt: time.Now().Add(30 * time.Minute),
}
s.snapshotRepo.Create(ctx, snapshot)

// 3. 创建购物车项
cartItem := &CartItem{
UserID: req.UserID,
ItemID: req.ItemID,
SKUID: req.SKUID,
Quantity: req.Quantity,
SnapshotCode: snapshot.SnapshotCode, // 关联价格快照
}
s.cartRepo.Create(ctx, cartItem)

return &AddToCartResponse{
Success: true,
SnapshotCode: snapshot.SnapshotCode,
FinalPrice: priceResp.FinalPrice,
}, nil
}

5.2.4 下单流程(核心)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 创建订单(含库存预订 + 配额扣减)
func (s *OrderService) CreateOrder(ctx context.Context, req *CreateOrderRequest) (*CreateOrderResponse, error) {
// Step 1: 验证价格快照
snapshot, err := s.snapshotRepo.GetByCode(ctx, req.SnapshotCode)
if err != nil {
return nil, errors.New("价格快照不存在")
}
if snapshot.ExpiredAt.Before(time.Now()) {
// 快照已过期,重新计算价格
newPrice, _ := s.pricingEngine.Calculate(ctx, &PriceRequest{
ItemID: snapshot.ItemID, SKUID: snapshot.SKUID, Quantity: snapshot.Quantity,
})
if !newPrice.FinalPrice.Equal(snapshot.FinalPrice) {
return nil, errors.New("价格已变动,请重新确认")
}
}

// Step 2: 库存预订(原子操作)
bookResp, err := s.inventoryService.BookStock(ctx, &BookStockReq{
ItemID: snapshot.ItemID,
SKUID: snapshot.SKUID,
Quantity: snapshot.Quantity,
OrderID: generateOrderID(),
})
if err != nil || !bookResp.Success {
return nil, errors.New("库存不足")
}

// Step 3: 营销配额扣减(Redis Lua 原子操作)
if len(snapshot.PromotionDetails) > 0 {
for _, promo := range snapshot.PromotionDetails {
if err := s.promotionService.ConsumeQuota(ctx, promo.ActivityID, 1); err != nil {
// 配额不足,回滚库存
s.inventoryService.UnbookStock(ctx, bookResp.BookingID)
return nil, errors.New("活动配额已用完")
}
}
}

// Step 4: 优惠券锁定(待支付后核销)
if req.VoucherCode != "" {
if err := s.voucherService.LockVoucher(ctx, req.VoucherCode, req.UserID); err != nil {
// 优惠券锁定失败,回滚库存和配额
s.inventoryService.UnbookStock(ctx, bookResp.BookingID)
return nil, errors.New("优惠券不可用")
}
}

// Step 5: 创建订单
order := &Order{
OrderID: bookResp.OrderID,
UserID: req.UserID,
ItemID: snapshot.ItemID,
SKUID: snapshot.SKUID,
Quantity: snapshot.Quantity,
SnapshotCode: snapshot.SnapshotCode,
Status: OrderStatusPendingPayment,
TotalAmount: snapshot.FinalPrice,
}
s.orderRepo.Create(ctx, order)

// Step 6: 发送 Kafka 事件
s.eventPublisher.Publish(&OrderCreatedEvent{OrderID: order.OrderID})

return &CreateOrderResponse{
OrderID: order.OrderID,
PaymentURL: s.generatePaymentURL(order.OrderID),
}, nil
}

5.3 用户体验优化

5.3.1 价格变动提示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 购物车结算时检查价格变动
func (s *CartService) CheckPriceChange(ctx context.Context, snapshotCode string) (*PriceChangeAlert, error) {
snapshot, _ := s.snapshotRepo.GetByCode(ctx, snapshotCode)

// 重新计算当前价格
currentPrice, _ := s.pricingEngine.Calculate(ctx, &PriceRequest{
ItemID: snapshot.ItemID, SKUID: snapshot.SKUID, Quantity: snapshot.Quantity,
})

priceDiff := currentPrice.FinalPrice.Sub(snapshot.FinalPrice)

if priceDiff.Abs().GreaterThan(decimal.NewFromFloat(0.01)) {
return &PriceChangeAlert{
HasChange: true,
OldPrice: snapshot.FinalPrice,
NewPrice: currentPrice.FinalPrice,
ChangeReason: "促销活动已结束", // 或 "基础价格调整"
Message: fmt.Sprintf("价格已从 %s฿ 变为 %s฿",
snapshot.FinalPrice.String(), currentPrice.FinalPrice.String()),
}, nil
}

return &PriceChangeAlert{HasChange: false}, nil
}

5.3.2 缺货降级提示

1
2
3
4
当前商品暂时缺货,您可以:
1. 查看相似商品(推荐 3 款同类商品)
2. 到货通知(输入手机号,库存补充时短信提醒)
3. 联系客服咨询(跳转客服对话)

商品管理 Product Center

商品上架系统的完整设计(状态机、审核策略、Saga 事务),详见(九)商品上架系统
库存系统的完整设计(二维分类模型、策略模式、Redis/MySQL 双写),详见(三)库存系统

商品信息包括哪些内容

商品系统的演进

阶段 主要特征/能力 技术架构/数据模型 适用场景/目标 实现方式简单说明
初始阶段 - 商品信息简单,字段少
- SKU/SPU未严格区分
- 价格库存直接在商品表
- 仅支持基本的增删改查
单表/简单表结构 小型电商、业务初期,SKU数量少 单体应用,单表存储,简单业务逻辑,直接数据库操作
成长阶段 - 引入SPU/SKU模型
- 属性、类目、品牌等实体独立
- 支持多规格商品
- 价格库存可拆分为独立表
关系型数据库,ER模型优化 SKU多样化,品类扩展,业务快速增长 关系型数据库,ER模型优化,多表存储,业务逻辑复杂
成熟阶段 - 商品中台化,支持多业务线/多渠道
- 属性体系灵活可扩展
- 多级类目、标签、图片、描述等丰富
- 商品快照、操作日志、版本控制
中台架构,微服务/多表/NoSQL 大型平台,业务复杂,需支撑多业务场景 分布式服务,插件化/配置化流程,状态机驱动,异步消息,灵活数据模型
未来演进 - 多语言多币种支持
- 商品内容多媒体化(视频、3D等)
- AI智能标签/推荐
- 商品数据实时分析与洞察
分布式/云原生/大数据平台 国际化、智能化、数据驱动的电商生态 云原生架构,AI/大数据分析,自动化运维,弹性伸缩,智能路由与风控

什么是SPU、SKU

方案一:同时创建多个SKU,并同步生成关联的SPU。整体方案是直接创建SKU,并维护多个不同的属性;该方案适用于大多数C2C综合电商平台(例如,阿里巴巴就是采用这种方式创建商品)。
方案二:先创建SPU,再根据SPU创建SKU。整体方案是由平台的主数据团队负责维护SPU,商家(包括自营和POP)根据SPU维护SKU。在创建SKU时,首先选择SPU(SPU中的基本属性由数据团队维护),然后基于SPU维护销售属性和物流属性,最后生成SKU;该方案适用于高度专业化的垂直B2B行业,如汽车、医药等。
这两种方案的原因是:垂直B2B平台上的业务(传统行业、年长的商家)操作能力有限,维护产品属性的错误率远高于C2C平台,同时平台对产品结构控制的要求较高。为了避免同一产品被不同商家维护成多个不同的属性(例如,汽车轮胎的胎面宽度、尺寸等属性),平台通常选择专门的数据团队来维护产品的基本属性,即维护SPU。
此外,B2B垂直电商的品类较少,SKU数量相对较小,品类标准化程度高,平台统一维护的可行性较高。
对于拥有成千上万品类的综合电商平台,依靠平台数据团队的统一维护是不现实的,或者像服装这样非标准化的品类对商品结构化管理的要求较低。因此,综合平台(阿里巴巴和亚马逊)的设计方向与垂直平台有所不同。
实际上,即使对于综合平台,不同的品类也会有不同的设计方法。一些品类具有垂直深度,因此也采用平台维护SPU和商家创建SKU的方式

数据库模型

  • 类目category
  • 品牌brand
  • 属性attribute
  • 标签tag
  • 商品主表/spu表/item表、item_stat 统计表、item属性值表
  • 商品变体表/variant表/sku表、sku attribute表
  • 其它实体表、其它实体和商品表的关联表


E-commerce product center

模型说明:

  • 商品(item/SPU)与商品变体(sku)分离,便于管理不同规格、价格、库存的商品。
  • 属性(attribute)、类目(category)、品牌(brand)等实体独立,便于扩展和维护
  • 商品分类体系如何设计?采用多级分类?分类的动态扩展只需插入新分类,指定其 parent_id,即可动态扩展任意层级
  • 灵活的属性体系。通过 category_attribute 和 spu_attr_value 支持不同类目下的不同属性,适应多样化商品需求。属性值与商品解耦,支持动态扩展
  • item_stat 单独存储统计信息,便于高并发下的读写优化。
  • 可以方便地增加标签(tag)、图片、描述、规格等字段,适应业务变化

商品信息录入JSON示例

实体商品
1、实体商品男士T恤 JSON 数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
{
"categoryId": 1003001,
"spu": {
"name": "经典圆领男士T恤",
"brandId": 2001,
"description": "柔软舒适,100%纯棉"
},
"basicAttributes": [
{
"attributeId": 101, // 品牌
"attributeName": "品牌",
"value": "NIKE"
},
{
"attributeId": 102, // 材质
"attributeName": "材质",
"value": "棉"
},
{
"attributeId": 103, // 产地
"attributeName": "产地",
"value": "中国"
},
{
"attributeId": 104, // 袖型
"attributeName": "袖型",
"value": "短袖"
}
],
"skus": [
{
"skuName": "黑色 L",
"price": 79.00,
"stock": 100,
"salesAttributes": [
{
"attributeId": 201,
"attributeName": "颜色",
"value": "黑色"
},
{
"attributeId": 202,
"attributeName": "尺码",
"value": "L"
}
]
},
{
"skuName": "白色 M",
"price": 79.00,
"stock": 150,
"salesAttributes": [
{
"attributeId": 201,
"attributeName": "颜色",
"value": "白色"
},
{
"attributeId": 202,
"attributeName": "尺码",
"value": "M"
}
]
}
]
}
虚拟商品
2、虚拟商品流量充值 JSON 数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
{
"categoryId": 1005002,
"spu": {
"name": "中国移动流量包充值",
"brandId": 3001,
"description": "全国通用流量包充值,按需选择,自动到账"
},
"basicAttributes": [
{
"attributeId": 301,
"attributeName": "运营商",
"value": "中国移动"
},
{
"attributeId": 302,
"attributeName": "适用网络",
"value": "4G/5G"
}
],
"skus": [
{
"skuName": "中国移动1GB全国流量包(7天)",
"price": 5.00,
"stock": 9999,
"salesAttributes": [
{
"attributeId": 401,
"attributeName": "流量容量",
"value": "1GB"
},
{
"attributeId": 402,
"attributeName": "有效期",
"value": "7天"
},
{
"attributeId": 403,
"attributeName": "流量类型",
"value": "全国通用"
}
]
},
{
"skuName": "中国移动5GB全国流量包(30天)",
"price": 20.00,
"stock": 9999,
"salesAttributes": [
{
"attributeId": 401,
"attributeName": "流量容量",
"value": "5GB"
},
{
"attributeId": 402,
"attributeName": "有效期",
"value": "30天"
},
{
"attributeId": 403,
"attributeName": "流量类型",
"value": "全国通用"
}
]
},
{
"skuName": "中国移动10GB全国流量包(90天)",
"price": 38.00,
"stock": 9999,
"salesAttributes": [
{
"attributeId": 401,
"attributeName": "流量容量",
"value": "10GB"
},
{
"attributeId": 402,
"attributeName": "有效期",
"value": "90天"
},
{
"attributeId": 403,
"attributeName": "流量类型",
"value": "全国通用"
}
]
}
]
}

商品的价格和库存

方案1. 价格和库存直接放在sku表中 (变化小)

在这种方案中,SKU(Stock Keeping Unit) 表包含商品的所有信息,包括价格和库存数量。每个 SKU 记录一个独立的商品实例,它有唯一的标识符,直接关联价格和库存。

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE sku_tab (
sku_id INT PRIMARY KEY, -- SKU ID
product_id INT, -- 商品ID (外键,指向商品表)
sku_name VARCHAR(255), -- SKU 名称
original_price DECIMAL(10, 2), -- 原始价格
price DECIMAL(10, 2), -- 销售价格
discount_price DECIMAL(10, 2), -- 折扣价格(如果有)
stock_quantity INT, -- 库存数量
warehouse_id INT, -- 仓库ID(如果有多个仓库)
created_at TIMESTAMP, -- 创建时间
updated_at TIMESTAMP -- 更新时间
);

优点:

  • 简单:所有信息都集中在一个表中,查询和管理都很方便。
  • 查询效率:查询某个商品的价格和库存不需要多表联接,减少了数据库查询的复杂度。
  • 维护方便:商品的所有信息(包括价格和库存)都在一个地方,减少了冗余数据和数据不一致的可能性。

缺点:

  • 灵活性差:如果价格和库存的管理策略较复杂(如促销、库存管理、动态定价等),这种方式可能不太适用。修改价格或库存时需要直接更新 SKU 表。
  • 扩展性差:对于一些复杂的定价和库存管理需求(如多层次的定价结构、分仓库管理等),直接放在 SKU 表中可能不够灵活。

适用场景:

  • 商品种类较少,SKU 数量相对固定且不复杂的场景。
  • 价格和库存变动较少,不涉及复杂的促销或动态定价的场景
方案2. 价格和库存单独管理(变化大)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

CREATE TABLE price_tab (
price_id INT PRIMARY KEY, -- 价格ID
sku_id INT, -- SKU ID (外键)
price DECIMAL(10, 2), -- 商品价格
discount_price DECIMAL(10, 2), -- 折扣价格
effective_date TIMESTAMP, -- 价格生效时间
expiry_date TIMESTAMP, -- 价格失效时间
price_type VARCHAR(50), -- 价格类型(如标准价、促销价等)
FOREIGN KEY (sku_id) REFERENCES ProductSKUs(sku_id)
);

CREATE TABLE inventory_tab (
inventory_id INT PRIMARY KEY, -- 库存ID
sku_id INT, -- SKU ID (外键)
quantity INT, -- 库存数量
warehouse_id INT, -- 仓库ID(如果有多个仓库)
updated_at TIMESTAMP, -- 库存更新时间
FOREIGN KEY (sku_id) REFERENCES ProductSKUs(sku_id)
);

优点:

  • 灵活性高:价格和库存信息可以独立管理,更容易支持多样化的定价策略、促销活动、库存管理等。
  • 可扩展性强:对于需要频繁更新价格、库存、促销等信息的商品,这种方案更容易扩展和适应变化。例如,可以灵活地增加新的价格策略或库存仓库。
  • 数据结构清晰:避免了价格和库存在 SKU 表中的冗余存储,使得数据结构更清晰。

缺点:

  • 查询复杂:获取某个商品的价格和库存信息时,需要联接多个表,查询效率可能会降低,尤其是在数据量大时。
  • 管理复杂:需要更多的表和关系,增加了维护成本和系统复杂度。

适用场景:

  • 商品种类繁多,SKU 数量较大,且需要支持动态定价、促销、库存管理等复杂需求的场景。
  • 需要频繁变动价格或库存的商品,且这些信息与 SKU 无法紧密绑定的场景

商品快照 item_snapshots

  1. 商品编辑时生成快照:
  • 每次商品信息(如价格、描述、属性等)发生编辑时,生成一个新的商品快照。
  • 将快照信息存储在 item_snapshots 表中,并生成一个唯一的 snapshot_id。
  1. 订单创建时使用快照:
    在用户下单时,查找当前商品的最新 snapshot_id。
    在 order_items 表中记录该 snapshot_id,以确保订单项反映下单时的商品状态
    1
    2
    3
    4
    5
    6
    7
    8
    9
    CREATE TABLE `snapshot_tab` (
    `snapshot_id` int(11) NOT NULL AUTO_INCREMENT,
    `snapshot_type` int(11) NOT NULL,
    `create_time` int(11) NOT NULL DEFAULT '0',
    `data` text NOT NULL,
    `entity_id` int(11) DEFAULT NULL,
    PRIMARY KEY (`snapshot_id`),
    KEY `idx_entity_id` (`entity_id`)
    )

用户操作日志

1
2
3
4
5
6
7
8
9
10
CREATE TABLE user_operation_logs (
log_id INT PRIMARY KEY AUTO_INCREMENT, -- Unique identifier for each log entry
user_id INT NOT NULL, -- ID of the user who made the edit
entity_id INT NOT NULL, -- ID of the entity being edited
entity_type VARCHAR(50) NOT NULL, -- Type of entity (e.g., SPU, SKU, Price, Stock)
operation_type VARCHAR(50) NOT NULL, -- Type of operation (e.g., CREATE, UPDATE, DELETE)
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Time of the operation
details TEXT, -- Additional details about the operation
FOREIGN KEY (user_id) REFERENCES users(id) -- Assuming a users table exists
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

商品的统计信息

缓存的使用

核心流程

B端:商品创建和发布的流程

  • 批量上传、批量编辑
  • 单个上传、编辑
  • 审核、发布
  • OpenAPI,支持外部同步API push 商品
  • auto-sync,自动同步外部商品

C端:商品搜索、商品详情

  • 商品搜索
    • elastic search 索引构建。获取商品列表(首页索引)
    • 如何处理商品的SEO优化?
      1、item index
      
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      POST /products/_doc/1
      {
      "product_id": "123456",
      "name": "Wireless Bluetooth Headphones",
      "description": "High-quality wireless headphones with noise-cancellation.",
      "price": 99.99,
      "stock": 50,
      "category": "Electronics",
      "brand": "SoundMax",
      "sku": "SM-123",
      "spu": "SPU-456",
      "image_urls": [
      "http://example.com/images/headphones1.jpg",
      "http://example.com/images/headphones2.jpg"
      ],
      "ratings": 4.5,
      "seller_info": {
      "seller_id": "78910",
      "seller_name": "BestSeller"
      },
      "attributes": {
      "color": "Black",
      "size": "Standard",
      "material": "Plastic"
      },
      "release_date": "2023-01-15",
      "location": {
      "lat": 40.7128,
      "lon": -74.0060
      },
      "tags": ["headphones", "bluetooth", "wireless"],
      "promotional_info": "20% off for a limited time"
      }
  • 商品推荐
    • 商品的A/B测试如何设计?
    • 如何设计商品的推荐算法?
    • 商品的个性化定制如何实现?
  • 获取商品详情

订单管理 Order Center

订单系统,平台的”生命中轴线”

订单中需要包含哪些信息

常见的订单类型

  1. 实物订单
    典型场景:电商平台购物(如买衣服、家电)
    核心特征:
    需要物流配送,涉及收货地址、运费、物流跟踪
    需要库存校验与扣减
    售后流程(退货、换货、退款)复杂
    订单状态多(待发货、已发货、已收货等)

  2. 虚拟订单
    典型场景:会员卡、电子券、游戏点卡、电影票等
    核心特征:
    无物流配送,不需要收货地址和运费
    通常无需库存(或库存为虚拟库存)
    订单完成后直接发放虚拟物品或凭证
    售后流程简单或无售后

  3. 预售订单
    典型场景:新品预售、定金膨胀、众筹等
    核心特征:
    订单分为定金和尾款两阶段
    需校验定金支付、尾款支付的时效
    可能涉及定金不退、尾款未付订单自动关闭等规则
    发货时间通常在尾款支付后

  4. O2O订单,外卖订单
    典型场景:酒店预订
    核心特征:
    需选择入住/离店日期、房型、入住人信息
    需对接第三方酒店系统实时查房、锁房
    取消、变更政策复杂,可能涉及违约金
    无物流,但有电子凭证或入住确认

订单系统的演进

阶段 主要特征/能力 技术架构/数据模型 适用场景/目标 实现方式简单说明
初始阶段 - 实现订单基本流转(下单、支付、发货、收货、取消)
- 单一订单类型(实物订单)
- 订单与商品、用户简单关联
单体应用/单表或少量表结构 业务初期,订单量小,流程简单,SKU/商家数量有限 单体应用,单表存储,简单业务逻辑,直接数据库操作
成长阶段(订单中心) - 支持订单拆单、合单(如多仓发货、合并支付)
- 支持多品类订单(如实物+虚拟)
- 订单中心化,订单与支付、配送、售后等子系统解耦
- 订单与商品快照、操作日志关联
微服务/多表/订单中心架构 平台型电商,业务扩展,需支持多商家、多类型订单,订单量大幅增长 订单中心服务,微服务拆分,多表关联,服务间接口调用,快照与日志表设计
成熟期(平台化) - 支持多样化订单类型(预售、虚拟、O2O、定制、JIT等)
- 订单流程可配置/插件化/工作流引擎/状态机框架/规则引擎等
- 订单状态机、履约、支付、退款等子流程解耦
- 支持复杂的促销、分账、履约模式
分布式/服务化/灵活数据模型 大型/综合电商,业务复杂,需快速适应新业务模式和高并发场景 分布式服务,插件化/配置化流程,状态机驱动,异步消息,灵活数据模型
未来智能化 - 订单智能路由与分配(如智能分仓、智能客服)
- 实时风控与反欺诈
- 订单数据实时分析与洞察
- 高可用、弹性伸缩、自动化运维
云原生/大数据/AI驱动架构 超大规模平台,国际化、智能化、数据驱动,需极致稳定与创新能力 云原生架构,AI/大数据分析,自动化运维,弹性伸缩,智能路由与风控

常见的订单模型设计

订单表(order_tab):记录用户的购买订单信息。主键为 order_id。

  • pay_order_id:支付订单ID,作为外键关联支付订单。
  • user_id:用户ID,标识购买订单的用户。
  • total_amount:订单的总金额。
  • order_status:订单状态,如已完成、已取消等。
  • payment_status:支付状态,与支付订单相关。
  • fulfillment_status:履约状态,表示订单的配送或服务状态。
  • refund_status:退款状态,用于标识订单是否有退款

订单商品表(order_item_tab:记录订单中具体商品的信息。主键为 order_item_id。

  • order_id:订单ID,作为外键关联订单。
  • item_id:商品ID,表示订单中的商品。
  • item_snapshot_id:商品快照ID,记录当时购买时的商品信息快照。
  • item_status:商品状态,如已发货、退货等。
  • quantity:购买数量。
  • price:商品单价。
  • discount:商品折扣金额

订单支付表(pay_order_tab):主要用于记录用户的支付信息。主键为 pay_order_id,标识唯一的支付订单。

  • user_id:用户ID,标识支付的用户。
  • payment_method:支付方式,如信用卡、支付宝等。
  • payment_status:支付状态,如已支付、未支付等。
  • pay_amount、cash_amount、coin_amount、voucher_amount:支付金额、现金支付金额、代币支付金额、优惠券使用金额。
  • 时间戳字段包括创建时间、初始化时间和更新时间

退款表(refund_tab):记录订单或订单项的退款信息。主键为 refund_id。

  • order_id:订单ID,作为外键关联订单。
  • order_item_id:订单项ID,标识具体商品的退款。
  • refund_amount:退款金额。
  • reason:退款原因。
  • quantity:退款的商品数量。
  • refund_status:退款状态。
  • refund_time:退款操作时间。

实体间关系:

  • 支付订单与订单:
  • 一个支付订单可能关联多个购买订单,形成 一对多 关系。
    例如,用户可以通过一次支付购买多个不同的订单。
  • 订单与订单商品:
    一个订单可以包含多个订单项,形成 一对多 关系。
    订单项代表订单中所购买的每个商品的详细信息。
  • 订单与退款:
    • 一个订单可能包含多个退款,形成 一对多 关系。
    • 退款可以是针对订单整体,也可以针对订单中的某个商品

订单状态机设计

Order 主状态机

支付状态机

履约状态机

退货退款状体机

异常单人工介入

  • 用户发起退款单拒绝
  • 退货失败,订单状态无法流转
  • 退款失败
  • 退营销失败

订单ID 生成策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
  # 时间戳 + 机器id + uid % 1000 + 自增序号

import time
import threading
from typing import Union

class OrderNoGenerator:
def __init__(self, machine_id: int):
"""
初始化订单号生成器
:param machine_id: 机器ID (0-999)
"""
if not 0 <= machine_id <= 999:
raise ValueError("机器ID必须在0-999之间")

self.machine_id = machine_id
self.sequence = 0
self.last_timestamp = -1
self.lock = threading.Lock() # 线程锁,保证线程安全

def _wait_next_second(self, last_timestamp: int) -> int:
"""
等待下一秒
:param last_timestamp: 上次时间戳
:return: 新的时间戳
"""
timestamp = int(time.time())
while timestamp <= last_timestamp:
timestamp = int(time.time())
return timestamp

def generate_order_no(self, user_id: int) -> Union[int, str]:
"""
生成订单号
:param user_id: 用户ID
:return: 订单号(整数或字符串形式)
"""
with self.lock: # 使用线程锁保证线程安全
# 获取当前时间戳(秒级)
timestamp = int(time.time())

# 处理时间回拨
if timestamp < self.last_timestamp:
raise RuntimeError("系统时间回拨,拒绝生成订单号")

# 如果是同一秒,序列号自增
if timestamp == self.last_timestamp:
self.sequence = (self.sequence + 1) % 1000
# 如果序列号用完了,等待下一秒
if self.sequence == 0:
timestamp = self._wait_next_second(self.last_timestamp)
else:
# 不同秒,序列号重置
self.sequence = 0

self.last_timestamp = timestamp

# 获取用户ID的后3位
user_id_suffix = user_id % 1000

# 组装订单号
order_no = (timestamp * 1000000000 + # 时间戳左移9位
self.machine_id * 1000000 + # 机器ID左移6位
user_id_suffix * 1000 + # 用户ID左移3位
self.sequence) # 序列号

return order_no

def generate_order_no_str(self, user_id: int) -> str:
"""
生成字符串形式的订单号
:param user_id: 用户ID
:return: 字符串形式的订单号
"""
order_no = self.generate_order_no(user_id)
return f"{order_no:019d}" # 补零到19位

# 使用示例
def main():
# 创建订单号生成器实例
generator = OrderNoGenerator(machine_id=1)

# 生成订单号
user_id = 12345
order_no = generator.generate_order_no(user_id)
order_no_str = generator.generate_order_no_str(user_id)

print(f"整数形式订单号: {order_no}")
print(f"字符串形式订单号: {order_no_str}")

# 测试并发
def test_concurrent():
for _ in range(5):
order_no = generator.generate_order_no(user_id)
print(f"并发生成的订单号: {order_no}")

# 创建多个线程测试并发
threads = []
for _ in range(3):
t = threading.Thread(target=test_concurrent)
threads.append(t)
t.start()

# 等待所有线程完成
for t in threads:
t.join()

if __name__ == "__main__":
main()

订单商品快照

方案1. 直接使用商品系统的item snapshot。(由商品系统维护快照)

  • 商品系统负责维护商品快照
  • 订单系统通过引用商品快照ID来关联商品信息
  • 商品信息变更时,商品系统生成新的快照版本
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    -- 商品系统维护的快照表
    CREATE TABLE item_snapshot_tab (
    snapshot_id BIGINT PRIMARY KEY,
    item_id BIGINT NOT NULL,
    version INT NOT NULL,
    data JSON NOT NULL, -- 存储商品完整信息
    created_at TIMESTAMP NOT NULL,
    INDEX idx_item_version (item_id, version)
    );

    -- 订单系统引用快照
    CREATE TABLE order_item_tab (
    order_id BIGINT,
    item_id BIGINT,
    snapshot_id BIGINT, -- 引用商品快照
    quantity INT,
    price DECIMAL(10,2),
    FOREIGN KEY (snapshot_id) REFERENCES item_snapshot(snapshot_id)
    );
    优点
  • 数据一致性高:商品系统统一管理快照,避免数据不一致
  • 存储效率高:多个订单可以共享同一个快照版本
  • 维护成本低:订单系统不需要关心快照的生成和管理
  • 查询性能好:可以直接通过快照ID获取完整商品信息

缺点

  • 系统耦合度高:订单系统强依赖商品系统的快照服务
  • 扩展性受限:商品系统需要支持所有订单系统可能需要的商品信息
  • 版本管理复杂:需要处理快照的版本控制和清理
  • 跨系统调用:订单系统需要调用商品系统获取快照信息

方案2. 创单时提供商品详情信息。(由订单维护商品快照)

1
2
3
4
5
6
7
8
CREATE TABLE order_item (
order_id BIGINT,
item_id BIGINT,
quantity INT,
price DECIMAL(10,2),
snapshot_data JSON NOT NULL, -- 存储下单时的商品信息
FOREIGN KEY (order_id, item_id) REFERENCES order_item_snapshot(order_id, item_id)
);

方案3. 创单时提供商品详情信息。(由订单维护商品快照)+ 快照复用

设计思路:

  • 订单系统维护自己的快照表,但增加快照复用机制
  • 使用商品信息的摘要(摘要算法如MD5)作为快照的唯一标识
  • 相同摘要的商品信息共享同一个快照记录
  • 创单时先检查摘要是否存在,存在则复用,不存在则创建新快照
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 订单系统维护的快照表
CREATE TABLE order_item_snapshot (
snapshot_id BIGINT PRIMARY KEY,
item_id BIGINT NOT NULL,
item_hash VARCHAR(32) NOT NULL COMMENT '商品信息摘要',
snapshot_data JSON NOT NULL COMMENT '存储下单时的商品信息',
created_at TIMESTAMP NOT NULL,
INDEX idx_item_hash (item_hash),
INDEX idx_item_id (item_id)
);
-- 订单商品表
CREATE TABLE order_item (
order_id BIGINT,
item_id BIGINT,
snapshot_id BIGINT,
quantity INT,
price DECIMAL(10,2),
FOREIGN KEY (snapshot_id) REFERENCES order_item_snapshot(snapshot_id)
);

适用场景:

  • 商品模型比较固定,项目初期,团队比较小,能接受系统之间的耦合,可以考虑用1
  • 不同商品差异比较大,商品信息结构复杂,考虑用2
  • 订单量太大,考虑复用快照

核心流程

正常流程和逆向流程

创单

核心步骤
  1. 参数校验。用户校验,是否异常用户。
  2. 商品与价格校验。校验商品是否存在、是否上架、价格是否有效
  3. 库存校验与预占。检查库存是否充足,部分场景下进行库存预占(锁库存)。
  4. 营销信息校验。校验优惠券、积分等是否可用,计算优惠金额。
  5. 订单金额计算。计算订单总金额、应付金额、各项明细。
  6. 生成订单号。生成全局唯一订单号,保证幂等性。
  7. 订单数据落库。写入订单主表、订单明细表、扩展表等。
  8. 扣减库存、扣减实际库存(有的系统在支付后扣减)。
  9. 发送消息/异步处理。发送订单创建成功消息,通知库存、物流、营销等系统。
  10. 返回下单结果。返回订单号、支付信息等给前端。
实现思路
  • 接口定义:通过OrderCreationStep接口定义了每个步骤必须实现的方法
  • 上下文共享:使用OrderCreationContext在步骤间共享数据
  • 步骤独立:每个步骤都是独立的,便于维护和测试
  • 回滚机制:每个步骤都实现了回滚方法
  • 流程管理:通过OrderCreationManager统一管理步骤的执行和回滚
  • 错误处理:统一的错误处理和回滚机制
  • 可扩展性:易于添加新的步骤或修改现有步骤
  • 如何解决不同category 创单差异较大的问题?
    • 插件化/策略模式。将订单处理流程拆分为多个步骤(如校验、支付、通知等)。不同订单类型实现各自的处理逻辑,通过策略模式动态选择。
    1. 订单类型标识。在订单主表中增加订单类型字段,根据类型选择不同的处理流程。
    2. 扩展字段。使用JSON或扩展表存储特定订单类型的特殊字段(如酒店的入住日期、机票的航班信息)。
    3. 流程引擎。使用流程引擎(如BPMN)定义和管理复杂的订单处理流程,支持动态调整。
点击查看创单核心逻辑代码实现


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
package order

import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"time"
)

// OrderType 订单类型
type OrderType string

const (
OrderTypePhysical OrderType = "physical" // 实物订单
OrderTypeVirtual OrderType = "virtual" // 虚拟订单
OrderTypePresale OrderType = "presale" // 预售订单
OrderTypeHotel OrderType = "hotel" // 酒店订单
OrderTypeTopUp OrderType = "topup" // 充值订单
)

// OrderStatus 订单状态
type OrderStatus string

const (
OrderStatusInit OrderStatus = "init" // 初始化
OrderStatusPending OrderStatus = "pending" // 待支付
OrderStatusPaid OrderStatus = "paid" // 已支付
OrderStatusShipping OrderStatus = "shipping" // 发货中
OrderStatusSuccess OrderStatus = "success" // 成功
OrderStatusFailed OrderStatus = "failed" // 失败
OrderStatusCanceled OrderStatus = "canceled" // 已取消
)

// Order 订单基础信息
type Order struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Type OrderType `json:"type"`
Status OrderStatus `json:"status"`
Amount float64 `json:"amount"`
Detail json.RawMessage `json:"detail"` // 不同类型订单的特殊字段
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}

// OrderCreationContext 创单上下文
type OrderCreationContext struct {
Ctx context.Context
Order *Order
Params map[string]interface{} // 创单参数
Cache map[string]interface{} // 步骤间共享数据
Errors []error // 错误记录
StepResults map[string]StepResult // 每个步骤的执行结果
RollbackFailedSteps []string // 记录回滚失败的步骤
}

// StepResult 步骤执行结果
type StepResult struct {
Success bool
Error error
Data interface{}
CompensateData interface{} // 用于补偿的数据
}

// OrderCreationStep 创单步骤接口
type OrderCreationStep interface {
Execute(ctx *OrderCreationContext) error
Rollback(ctx *OrderCreationContext) error
Compensate(ctx *OrderCreationContext) error // 异步补偿
Name() string
}

// 错误定义
var (
ErrInvalidParams = errors.New("invalid parameters")
ErrProductNotFound = errors.New("product not found")
ErrProductOffline = errors.New("product is offline")
ErrStockInsufficient = errors.New("stock insufficient")
ErrUserBlocked = errors.New("user is blocked")
ErrSystemBusy = errors.New("system is busy")
)

// OrderError 订单错误
type OrderError struct {
Step string
Message string
Err error
}

func (e *OrderError) Error() string {
return fmt.Sprintf("step: %s, message: %s, error: %v", e.Step, e.Message, e.Err)
}

// 参数校验步骤
type ParamValidationStep struct{}

func (s *ParamValidationStep) Execute(ctx *OrderCreationContext) error {
// 通用参数校验
if ctx.Order.UserID == "" || ctx.Order.Type == "" {
return &OrderError{Step: s.Name(), Message: "missing required fields", Err: ErrInvalidParams}
}

// 订单类型特殊参数校验
switch ctx.Order.Type {
case OrderTypePhysical:
if addr, ok := ctx.Params["address"].(string); !ok || addr == "" {
return &OrderError{Step: s.Name(), Message: "missing address for physical order", Err: ErrInvalidParams}
}
case OrderTypeHotel:
if _, ok := ctx.Params["check_in_date"].(time.Time); !ok {
return &OrderError{Step: s.Name(), Message: "missing check-in date for hotel order", Err: ErrInvalidParams}
}
}
return nil
}

func (s *ParamValidationStep) Rollback(ctx *OrderCreationContext) error {
// 参数校验步骤无需回滚
return nil
}

func (s *ParamValidationStep) Compensate(ctx *OrderCreationContext) error {
// 参数校验步骤无需补偿
return nil
}

func (s *ParamValidationStep) Name() string {
return "param_validation"
}

// Product 商品信息
type Product struct {
ID string
Name string
Price float64
IsOnSale bool
}

// ProductService 商品服务接口
type ProductService interface {
GetProduct(ctx context.Context, productID string) (*Product, error)
}

// StockService 库存服务接口
type StockService interface {
LockStock(ctx context.Context, productID string, quantity int) (string, error)
UnlockStock(ctx context.Context, lockID string) error
DeductStock(ctx context.Context, productID string, quantity int) error
RevertDeductStock(ctx context.Context, productID string, quantity int) error
}

// PromotionService 营销服务接口
type PromotionService interface {
ValidateCoupon(ctx context.Context, couponCode string, userID string, orderAmount float64) (*Coupon, error)
UseCoupon(ctx context.Context, couponCode string, userID string, orderID string) error
RevertCouponUsage(ctx context.Context, couponCode string, userID string, orderID string) error
DeductPoints(ctx context.Context, userID string, points int) error
RevertPointsDeduction(ctx context.Context, userID string, points int) error
}

// Coupon 优惠券信息
type Coupon struct {
Code string
Type string
Amount float64
Threshold float64
ExpireTime time.Time
}

// 商品校验步骤
type ProductValidationStep struct {
productService ProductService
}

func (s *ProductValidationStep) Execute(ctx *OrderCreationContext) error {
productID := ctx.Params["product_id"].(string)
product, err := s.productService.GetProduct(ctx.Ctx, productID)
if err != nil {
return &OrderError{Step: s.Name(), Message: "failed to get product", Err: err}
}

if !product.IsOnSale {
return &OrderError{Step: s.Name(), Message: "product is offline", Err: ErrProductOffline}
}

ctx.Cache["product"] = product
return nil
}

func (s *ProductValidationStep) Rollback(ctx *OrderCreationContext) error {
return nil
}

func (s *ProductValidationStep) Compensate(ctx *OrderCreationContext) error {
return nil
}

func (s *ProductValidationStep) Name() string {
return "product_validation"
}

// 库存校验步骤
type StockValidationStep struct {
stockService StockService
}

func (s *StockValidationStep) Execute(ctx *OrderCreationContext) error {
if ctx.Order.Type == OrderTypeVirtual || ctx.Order.Type == OrderTypeTopUp {
return nil
}

productID := ctx.Params["product_id"].(string)
quantity := ctx.Params["quantity"].(int)

lockID, err := s.stockService.LockStock(ctx.Ctx, productID, quantity)
if err != nil {
return &OrderError{Step: s.Name(), Message: "failed to lock stock", Err: err}
}

ctx.Cache["stock_lock_id"] = lockID
return nil
}

func (s *StockValidationStep) Rollback(ctx *OrderCreationContext) error {
if lockID, ok := ctx.Cache["stock_lock_id"].(string); ok {
return s.stockService.UnlockStock(ctx.Ctx, lockID)
}
return nil
}

func (s *StockValidationStep) Compensate(ctx *OrderCreationContext) error {
return nil
}

func (s *StockValidationStep) Name() string {
return "stock_validation"
}

// 库存扣减步骤
type StockDeductionStep struct {
stockService StockService
}

func (s *StockDeductionStep) Execute(ctx *OrderCreationContext) error {
// 虚拟商品和充值订单跳过库存扣减
if ctx.Order.Type == OrderTypeVirtual || ctx.Order.Type == OrderTypeTopUp {
return nil
}

productID := ctx.Params["product_id"].(string)
quantity := ctx.Params["quantity"].(int)

// 执行库存扣减
if err := s.stockService.DeductStock(ctx.Ctx, productID, quantity); err != nil {
return &OrderError{
Step: s.Name(),
Message: "failed to deduct stock",
Err: err,
}
}

// 记录扣减信息,用于回滚
ctx.Cache["stock_deducted"] = map[string]interface{}{
"product_id": productID,
"quantity": quantity,
}

return nil
}

func (s *StockDeductionStep) Rollback(ctx *OrderCreationContext) error {
deducted, ok := ctx.Cache["stock_deducted"].(map[string]interface{})
if !ok {
return nil
}

productID := deducted["product_id"].(string)
quantity := deducted["quantity"].(int)

return s.stockService.RevertDeductStock(ctx.Ctx, productID, quantity)
}

func (s *StockDeductionStep) Compensate(ctx *OrderCreationContext) error {
deducted, ok := ctx.Cache["stock_deducted"].(map[string]interface{})
if !ok {
return nil
}

productID := deducted["product_id"].(string)
quantity := deducted["quantity"].(int)

// 创建补偿消息
compensationMsg := StockCompensationMessage{
OrderID: ctx.Order.ID,
ProductID: productID,
Quantity: quantity,
Timestamp: time.Now(),
}

// TODO: 实现发送到补偿队列的逻辑
// return sendToCompensationQueue("stock_compensation", compensationMsg)
return nil
}

func (s *StockDeductionStep) Name() string {
return "stock_deduction"
}

// 营销活动扣减步骤
type PromotionDeductionStep struct {
promotionService PromotionService
}

func (s *PromotionDeductionStep) Execute(ctx *OrderCreationContext) error {
// 处理优惠券
if couponCode, ok := ctx.Params["coupon_code"].(string); ok {
// 验证优惠券
coupon, err := s.promotionService.ValidateCoupon(
ctx.Ctx,
couponCode,
ctx.Order.UserID,
ctx.Order.Amount,
)
if err != nil {
return &OrderError{
Step: s.Name(),
Message: "invalid coupon",
Err: err,
}
}

// 使用优惠券
if err := s.promotionService.UseCoupon(ctx.Ctx, couponCode, ctx.Order.UserID, ctx.Order.ID); err != nil {
return &OrderError{
Step: s.Name(),
Message: "failed to use coupon",
Err: err,
}
}

// 记录优惠券使用信息,用于回滚
ctx.Cache["used_coupon"] = couponCode

// 更新订单金额
ctx.Order.Amount -= coupon.Amount
}

// 处理积分抵扣
if points, ok := ctx.Params["use_points"].(int); ok && points > 0 {
// 扣减积分
if err := s.promotionService.DeductPoints(ctx.Ctx, ctx.Order.UserID, points); err != nil {
return &OrderError{
Step: s.Name(),
Message: "failed to deduct points",
Err: err,
}
}

// 记录积分扣减信息,用于回滚
ctx.Cache["deducted_points"] = points

// 更新订单金额(假设1积分=0.01元)
ctx.Order.Amount -= float64(points) * 0.01
}

return nil
}

func (s *PromotionDeductionStep) Rollback(ctx *OrderCreationContext) error {
// 回滚优惠券使用
if couponCode, ok := ctx.Cache["used_coupon"].(string); ok {
if err := s.promotionService.RevertCouponUsage(ctx.Ctx, couponCode, ctx.Order.UserID, ctx.Order.ID); err != nil {
return err
}
}

// 回滚积分扣减
if points, ok := ctx.Cache["deducted_points"].(int); ok {
if err := s.promotionService.RevertPointsDeduction(ctx.Ctx, ctx.Order.UserID, points); err != nil {
return err
}
}

return nil
}

func (s *PromotionDeductionStep) Compensate(ctx *OrderCreationContext) error {
// 优惠券补偿
if couponCode, ok := ctx.Cache["used_coupon"].(string); ok {
// TODO: 实现优惠券补偿逻辑
// 1. 发送到补偿队列
// 2. 记录补偿日志
// 3. 通知运营人员
}

// 积分补偿
if points, ok := ctx.Cache["deducted_points"].(int); ok {
// TODO: 实现积分补偿逻辑
// 1. 发送到补偿队列
// 2. 记录补偿日志
// 3. 通知运营人员
}

return nil
}

func (s *PromotionDeductionStep) Name() string {
return "promotion_deduction"
}

// OrderFactory 订单工厂
type OrderFactory struct {
commonSteps []OrderCreationStep
typeSteps map[OrderType][]OrderCreationStep
}

func NewOrderFactory() *OrderFactory {
f := &OrderFactory{
commonSteps: []OrderCreationStep{
&ParamValidationStep{},
&ProductValidationStep{},
&PromotionDeductionStep{},
},
typeSteps: make(map[OrderType][]OrderCreationStep),
}

// 实物订单特有步骤
f.typeSteps[OrderTypePhysical] = []OrderCreationStep{
&StockValidationStep{},
&StockDeductionStep{},
}

// 虚拟订单特有步骤
f.typeSteps[OrderTypeVirtual] = []OrderCreationStep{}

// 预售订单特有步骤
f.typeSteps[OrderTypePresale] = []OrderCreationStep{
&StockValidationStep{},
}

// 酒店订单特有步骤
f.typeSteps[OrderTypeHotel] = []OrderCreationStep{}

return f
}

func (f *OrderFactory) GetSteps(orderType OrderType) []OrderCreationStep {
steps := make([]OrderCreationStep, 0)
steps = append(steps, f.commonSteps...)
if typeSteps, ok := f.typeSteps[orderType]; ok {
steps = append(steps, typeSteps...)
}
return steps
}

// Logger 日志接口
type Logger interface {
Info(msg string, args ...interface{})
Error(msg string, args ...interface{})
}

// OrderCreationManager 订单创建管理器
type OrderCreationManager struct {
factory *OrderFactory
logger Logger
}

func (m *OrderCreationManager) CreateOrder(ctx context.Context, params map[string]interface{}) (*Order, error) {
orderCtx := &OrderCreationContext{
Ctx: ctx,
Params: params,
Cache: make(map[string]interface{}),
StepResults: make(map[string]StepResult),
RollbackFailedSteps: make([]string, 0),
}

// 初始化订单
order := &Order{
ID: generateOrderID(),
UserID: params["user_id"].(string),
Type: OrderType(params["type"].(string)),
Status: OrderStatusInit,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
orderCtx.Order = order

// 获取订单类型对应的处理步骤
steps := m.factory.GetSteps(order.Type)

// 执行步骤
executedSteps := make([]OrderCreationStep, 0)
for _, step := range steps {
stepName := step.Name()
m.logger.Info("executing step", "step", stepName)

err := step.Execute(orderCtx)
if err != nil {
m.logger.Error("step execution failed", "step", stepName, "error", err)

orderCtx.Errors = append(orderCtx.Errors, err)

// 执行回滚,并记录回滚失败的步骤
m.rollbackSteps(orderCtx, executedSteps)

// 只对回滚失败的步骤进行补偿
if len(orderCtx.RollbackFailedSteps) > 0 {
go m.compensateFailedRollbacks(orderCtx)
}

return nil, err
}

executedSteps = append(executedSteps, step)
m.logger.Info("step executed successfully", "step", stepName)
}

return order, nil
}

// 修改回滚逻辑,记录回滚失败的步骤
func (m *OrderCreationManager) rollbackSteps(ctx *OrderCreationContext, steps []OrderCreationStep) {
for i := len(steps) - 1; i >= 0; i-- {
step := steps[i]
stepName := step.Name()

if err := step.Rollback(ctx); err != nil {
m.logger.Error("step rollback failed", "step", stepName, "error", err)
// 记录回滚失败的步骤
ctx.RollbackFailedSteps = append(ctx.RollbackFailedSteps, stepName)
}
}
}

// 新的补偿方法,只处理回滚失败的步骤
func (m *OrderCreationManager) compensateFailedRollbacks(ctx *OrderCreationContext) {
m.logger.Info("starting compensation for failed rollbacks",
"failed_steps", ctx.RollbackFailedSteps)

// 获取所有步骤的映射
allSteps := make(map[string]OrderCreationStep)
for _, step := range m.factory.GetSteps(ctx.Order.Type) {
allSteps[step.Name()] = step
}

// 只对回滚失败的步骤进行补偿
for _, failedStepName := range ctx.RollbackFailedSteps {
if step, ok := allSteps[failedStepName]; ok {
if err := step.Compensate(ctx); err != nil {
m.logger.Error("step compensation failed",
"step", failedStepName,
"error", err)

// 补偿失败处理
m.handleCompensationFailure(ctx, failedStepName, err)
}
}
}
}

// 处理补偿失败的情况
func (m *OrderCreationManager) handleCompensationFailure(ctx *OrderCreationContext, stepName string, err error) {
// 创建补偿任务
compensationTask := CompensationTask{
OrderID: ctx.Order.ID,
StepName: stepName,
Params: ctx.Params,
Cache: ctx.Cache,
RetryCount: 0,
MaxRetries: 3,
CreatedAt: time.Now(),
}

// 记录错误日志
m.logger.Error("compensation task created for failed step",
"order_id", compensationTask.OrderID,
"step", compensationTask.StepName,
"error", err)

// TODO: 实现具体的补偿任务处理逻辑
// 1. 将任务保存到数据库
// 2. 发送到消息队列
// 3. 触发告警
}

// DefaultLogger 默认日志实现
type DefaultLogger struct{}

func NewDefaultLogger() Logger {
return &DefaultLogger{}
}

func (l *DefaultLogger) Info(msg string, args ...interface{}) {
log.Printf("INFO: "+msg, args...)
}

func (l *DefaultLogger) Error(msg string, args ...interface{}) {
log.Printf("ERROR: "+msg, args...)
}

// 辅助函数
func generateOrderID() string {
return fmt.Sprintf("ORDER_%d", time.Now().UnixNano())
}

// CompensationTask 补偿任务结构
type CompensationTask struct {
OrderID string
StepName string
Params map[string]interface{}
Cache map[string]interface{}
RetryCount int
MaxRetries int
CreatedAt time.Time
}

// StockCompensationMessage 库存补偿消息
type StockCompensationMessage struct {
OrderID string
ProductID string
Quantity int
Timestamp time.Time
}

支付

支付流程

1. 支付校验。用户校验,订单状态校验等 2. 营销活动扣减deduction、回滚rollback、补偿compensation. 3. 支付初始化 4. 支付回调 5. 补偿队列 6. OrderBus 订单事件
支付状态的设计
1
2
3
4
5
6
7
8
9
10
11
12
13
14
P0: PAYMENT_NOT_STARTED - 未开始
P1: PAYMENT_PENDING - 支付中,用户点击了pay按钮,等待支付)

P2: MARKETING_Init - 营销初始化
P3: MARKETING_FAILED - 营销扣减失败
P4: MARKETING_SUCCESS - 营销扣减成功

P5: PAYMENT_INITIALIZED - 支付初始化
P6: PAYMENT_INITIALIZED_FAILED - 支付初始化失败
P7: PAYMENT_PROCESSING - 支付处理中。(支付系统正在处理支付请求)
P8: PAYMENT_SUCCESS - 支付成功
P9: PAYMENT_FAILED - 支付失败
P10: PAYMENT_CANCELLED - 支付取消
P11: PAYMENT_TIMEOUT - 支付超时
异常和补偿设计

常见的异常:
营销部分:

  1. 营销扣减补偿操作重复。(营销接口幂等设计)
  2. 营销已经扣减了,但是后续步骤失败,需要回滚扣减的操作。(业务代码中需要有rollback操作)
  3. 营销已经扣减了,回滚扣减失败。延时队列任务补偿。(回滚失败发送延时队列,任务补偿)
  4. 营销已经扣减了,写延时队列失败,任务没有补偿成功。(补偿任务通过扫描异常单进行补偿)
  5. 营销已经扣减了,延时队列消息重复,重复回滚。(依赖营销系统的幂等操作)
  6. 营销已经扣减了,请求已经发给了营销服务,营销服务已经扣减了,但是回包失败。(请求营销接口之前更新订单状态为P2,针对P2的订单进行补偿)

支付部分:

  1. 重复支付。(支付接口幂等设计)
  2. 支付初始化请求支付成功,但是回包失败(重续针对P5的订单进行补偿,查询支付系统是否收单,已经支付结果查询)
  3. 支付回调包重复,更新回调结果幂等。
  4. 支付回调包丢失,对于P7支付单需要补偿。

履约

履约核心流程

履约状态机的设计
1
2
3
4
5
6
F0: FULFILLMENT_NOT_STARTED - 未开始
F1: FULFILLMENT_PENDING - 履约开始
F2: FULFILLMENT_PROCESSING - 履约处理中
F3: FULFILLMENT_FAILED - 履约失败
F4: FULFILLMENT_SUCCESS - 履约成功
F5: FULFILLMENT_CANCELLED - 履约取消
异常和补偿的设计
  1. 订阅支付完成的事件O2
  2. 在请求fulfillment/init履约初始化之前,更新订单状态为F1
  3. fulfillment/init 接口的回包丢了。(针对F1订单进行补偿)
  4. fulfillment/init 重复请求(幂等设计)
  5. F2订单补偿。(fulfillment/callback 丢包,处理失败等)

return & refund

主要流程
  1. 订单服务作为协调者。与履约服务、营销服务、支付服务解耦
  2. 用 OrderBus 进行事件传递
  3. 状体机设计
  4. 异常处理
异常和补偿机制
  1. 退货环节异常
  • 退货初始化失败:直接发送退款失败事件
  • 退货回调失败:更新状态为 R7,发送失败事件
  1. 营销退款异常
  • 营销处理失败:更新状态为 R14,发送失败事件
  • 营销处理成功:更新状态为 R13,继续后续流程
  1. 支付退款异常
  • 支付退款失败:更新状态为 R11,发送失败事件
  • 支付退款成功:更新状态为 R10,发送成功事件
订单详情查询

系统挑战和解决方案

如何维护订单状态的最终一致性?

不一致的原因

  • 重复请求
  • 丢包。例如,请求发货,对方收单,回包失败。
  • 资源回滚:营销、库存
  • 并发问题

状态机

  • 设计层面,严格的状态转换规则 + 状态转换的触发事件
  • 状态转换的原子性。(事务性)

并发更新数据库前,要用乐观锁或者悲观锁,

  • 乐观锁:同时在更新时判断版本号是否是之前取出来的版本号,更新成功就结束
  • 悲观锁:先使用select for update进行锁行记录,然后更新
1
2
3
4
5
6
7
8
9
UPDATE orders 
SET status = 'NEW_STATUS',
version = version + 1
WHERE id = ? AND version = ?

BEGIN;
SELECT * FROM orders WHERE id = ? FOR UPDATE;
UPDATE orders SET status = 'NEW_STATUS' WHERE id = ?;
COMMIT;

幂等设计。比如重复支付、重复扣减营销、重复履约等

  • 支付重复支付,支付回调幂等设计。
  • 重复营销扣减,回滚,
  • 重复履约
  • 重复回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 使用支付单号作为幂等键
@Transactional
public void handlePaymentCallback(String paymentId, String status) {
// 检查是否已处理
if (isProcessed(paymentId)) {
return;
}
// 处理支付回调
processPaymentCallback(paymentId, status);
// 记录处理状态
markAsProcessed(paymentId);
}


// 使用订单号+营销资源ID作为幂等键
@Transactional
public void deductMarketingResource(String orderId, String resourceId) {
if (isDeducted(orderId, resourceId)) {
return;
}
// 扣减营销资源
deductResource(orderId, resourceId);
// 记录扣减状态
markAsDeducted(orderId, resourceId);
}

补偿机制兜底

  • 异常回滚。营销扣减回滚
  • 消息队列补偿:补偿队列,重试。(可能丢消息)
  • 定时任务补偿:扫表补偿
  • 依赖方支付查询和幂等设计

分布式事务

  • 营销扣减
  • 库存扣减
  • 支付等业务
  • 实现状态转换和业务操作在同一个事务中完成
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Transactional
    public void processOrderWithDistributedTransaction(Order order) {
    try {
    // 1. 更新订单状态
    updateOrderStatus(order);
    // 2. 扣减库存
    deductInventory(order);
    // 3. 创建物流单
    createLogistics(order);
    } catch (Exception e) {
    // 触发补偿机制
    triggerCompensation(order);
    }
    }

异常单人工介入

对账机制

商品信息缓存和数据一致性

主从架构中如何获取最新的数据,避免因为主从延时导致获得脏数据

策略 优点 缺点
1. 直接读取主库 - 一致性: 始终获取最新的数据。 - 性能: 增加主库的负载,可能导致性能瓶颈。
- 简单性: 实现简单直接,因为它直接查询可信的源。 - 可扩展性: 主库可能成为瓶颈,限制系统在高读流量下有效扩展的能力。
2. 使用VersionCache与从库 - 性能: 分散读取负载到从库,减少主库的压力。 - 复杂性: 实现更加复杂,需要进行缓存管理并处理潜在的不一致性问题。
- 可扩展性: 通过将大部分读取操作卸载到从库,实现更好的扩展性。 - 缓存管理: 需要进行适当的缓存失效处理和同步,以确保数据的一致性。
- 一致性: 通过比较版本并在必要时回退到主库,提供确保最新数据的机制。 - 潜在延迟: 从库的数据可能仍然存在不同步的可能性,导致数据更新前有轻微延迟。

常见问题1: 重复下单、支付、履约问题(重复和幂等问题)

场景:

  1. 下单、去重、DB唯一键兜底。去重逻辑是约定的
  2. 支付、checkoutid,唯一键
  3. 履约、先获取reference id,再履约

解决方案:

  1. 前端方案
    前端通过js脚本控制,无法解决用户刷新提交的请求。另外也无法解决恶意提交。
    不建议采用该方案,如果想用,也只是作为一个补充方案。

  2. 中间环节去重。根据请求参数中间去重
    当用户点击购买按钮时,渲染下单页面,展示商品、收货地址、运费、价格等信息,同时页面会埋上 Token 信息,用户提交订单时,后端业务逻辑会校验token,有且匹配才认为是合理请求。

  3. 利用数据库自身特性 “主键唯一约束”,在插入订单记录时,带上主键值,如果订单重复,记录插入会失败。
    操作过程如下:
    引入一个服务,用于生成一个”全局唯一的订单号”;
    进入创建订单页面时,前端请求该服务,预生成订单ID;
    提交订单时,请求参数除了业务参数外,还要带上这个预生成订单ID

快照和操作日志

为了保证数据的 完整性、可追溯性,写操作需要关注的问题
场景:
商品信息是可以修改的,当用户下单后,为了更好解决后面可能存在的买卖纠纷,创建订单时会同步保存一份商品详情信息,称之为订单快照

解决方案:
同一件商品,会有很多用户会购买,如果热销商品,短时间就会有上万的订单。如果每个订单都创建一份快照,存储成本太高。另外商品信息虽然支持修改,但毕竟是一个低频动作。我们可以理解成,大部分订单的商品快照信息都是一样的,除非下单时用户修改过。
如何实时识别修改动作是解决快照成本的关键所在。我们采用摘要比对的方法‍。创建订单时,先检查商品信息摘要是否已经存在,如果不存在,会创建快照记录。订单明细会关联商品的快照主键。

账户余额更新,保证事务
用户支付,我们要从买家账户减掉一定金额,再往卖家增加一定金额,为了保证数据的 完整性、可追溯性, 变更余额时,我们通常会同时插入一条 记录流水。

账户流水核心字段: 流水ID、金额、交易双方账户、交易时间戳、订单号。
账户流水只能新增,不能修改和删除。流水号必须是自增的。
后续,系统对账时,我们只需要对交易流水明细数据做累计即可,如果出现和余额不一致情况,一般以交易流水为准来修复余额数据。
更新余额、记录流水 虽属于两个操作,但是要保证要么都成功,要么都失败。要做到事务。
当然,如果涉及多个微服务调用,会用到 分布式事务。
分布式事务,细想下也很容易理解,就是 将一个大事务拆分为多个本地事务, 本地事务依然借助于数据库自身事务来解决,难点在于解决这个分布式一致性问题,借助重试机制,保证最终一致是我们常用的方案。

常见问题3: 并发更新的ABA问题 (订单表的version)

场景:
商家发货,填写运单号,开始填了 123,后来发现填错了,然后又修改为 456。此时,如果就为某种特殊场景埋下错误伏笔,具体我们来看下,过程如下:
开始「请求A」发货,调订单服务接口,更新运单号 123,但是响应有点慢,超时了;
此时,商家发现运单号填错了,发起了「请求B」,更新运单号为 456 ,订单服务也响应成功了;
这时,「请求A」触发了重试,再次调用订单服务,更新运单号 123,订单服务也响应成功了;订单服务最后保存的 运单号 是 123。

是不是犯错了!!!!,那么有什么好的解决方案吗?
数据库表引入一个额外字段 version ,每次更新时,判断表中的版本号与请求参数携带的版本号是否一致。这个版本字段可以是时间戳
复制
update order
set logistics_num = #{logistics_num} , version = #{version} + 1
where order_id= 1111 and version = #{version}

秒杀系统中的库存管理和订单蓄洪

常见的库存扣减方式有:
下单减库存: 即当买家下单后,在商品的总库存中减去买家购买数量。下单减库存是最简单的减库存方式,也是控制最精确的一种,但是有些人下完单可能并不会付款。
付款减库存: 即买家下单后,并不立即减库存,而是等到有用户付款后才真正减库存,否则库存一直保留给其他买家。但因为付款时才减库存,如果并发比较高,有可能出现买家下单后付不了款的情况,因为可能商品已经被其他人买走了。
预扣库存: 这种方式相对复杂一些,买家下单后,库存为其保留一定的时间(如 30 分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。在买家付款前,系统会校验该订单的库存是否还有保留:如果没有保留,则再次尝试预扣;
方案一:数据库乐观锁扣减库存
通常在扣减库存的场景下使用行级锁,通过数据库引擎本身对记录加锁的控制,保证数据库的更新的安全性,并且通过where语句的条件,保证库存不会被减到 0 以下,也就是能够有效的控制超卖的场景。
先查库存
然后乐观锁更新:update … set amount = amount - 1 where id = $id and amount = x
设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时 SQL 语句会报错
方案二:redis 扣减库存,异步同步到DB
redis 原子操作扣减库存
异步通过MQ消息同步到DB

购物车模块的实现和优化

技术设计并不是特别复杂,存储的信息也相对有限(用户id、商品id、sku_id、数量、添加时间)。这里特别拿出来单讲主要是用户体验层面要注意几个问题:
添加购物车时,后端校验用户未登录,常规思路,引导用户跳转登录页,待登录成功后,再添加购物车。多了一步操作,给用户一种强迫的感觉,体验会比较差。有没有更好的方式?
如果细心体验京东、淘宝等大平台,你会发现即使未登录态也可以添加购物车,这到底是怎么实现的?
细细琢磨其实原理并不复杂,服务端这边在用户登录态校验时,做了分支路由,当用户未登录时,会创建一个临时Token,作为用户的唯一标识,购物车数据挂载在该Token下,为了避免购物车数据相互影响以及设计的复杂度,这里会有一个临时购物车表。
当然,临时购物车表的数据量并不会太大,why?用户不会一直闲着添加购物车玩,当用户登录后,查看自己的购物车,服务端会从请求的cookie里查找购物车Token标识,并查询临时购物车表是否有数据,然后合并到正式购物车表里。
临时购物车是不是一定要在服务端存储?未必。
有架构师倾向前置存储,将数据存储在浏览器或者 APP LocalStorage, 这部分数据毕竟不是共享的,但是不太好的增加了设计的复杂度。

客户端需要借助本地数据索引,远程请求查完整信息;
如果是登录态,还要增加数据合并逻辑;
考虑到这两部分数据只是用户标识的差异性,所以作者还是建议统一存到服务端,日后即使业务逻辑变更,只需要改一处就可以了,毕竟自运营系统,良好的可维护性也需要我们非常关注的。

购物车是电商系统的标配功能,暂存用户想要购买的商品。

  • 分为添加商品、列表查看、结算下单三个动作。
  • 用户未登录时,将数据存储在浏览器或者 APP LocalStorage。登录后写入后端
  • 后端使用DB,为了性能考虑可以结合redis和DB联合存储
  • 存redis定期同步到DB
  • 前后端联合存储

系统中的分布式ID是怎么生成的

item ID 自增。(100w级别)
order id. 时间戳 + 机器ID + uid % 100 + sequence
DP唯一ID生成调研说明
request 生成方法:时间戳 + 机器mac地址 + sequence

系统稳定性建设

B 端运营系统的稳定性设计和监控体系,详见(六)B 端运营系统

Google 理论:怎样的系统算是稳定高可用的

Google SRE中(SRE三部曲[1])有一个层级模型来描述系统可靠性基础和高层次需求(Dickerson’s Hierarchy of Service Reliability),如下图:


该模型由Google SRE工程师Mikey Dickerson在2013年提出,将系统稳定性需求按照基础程度进行了不同层次的体系化区分,形成稳定性标准金字塔模型:

  1. 金字塔的底座是监控(Monitoring),这是一个系统对于稳定性最基础的要求,缺少监控的系统,如同蒙上眼睛狂奔的野马,无从谈及可控性,更遑论稳定性。

  2. 更上层是应急响应(Incident Response),从一个问题被监控发现到最终解决,这期间的耗时直接取决于应急响应机制的成熟度。合理的应急策略能保证当故障发生时,所有问题能得到有序且妥善的处理,而不是慌乱成一锅粥。

  3. 研发流程规范

  • 测试和发布管控(Testing&Release procedures),大大小小的应用都离不开不断的变更与发布,有效的测试与发布策略能保障系统所有新增变量都处于可控稳定区间内,从而达到整体服务终态稳定
  • 事后总结以及根因分析(Postmortem&Root Caue Analysis),即我们平时谈到的”复盘”,虽然很多人都不太喜欢这项活动,但是不得不承认这是避免我们下次犯同样错误的最有效手段,只有当摸清故障的根因以及对应的缺陷,我们才能对症下药,合理进行规避
  1. 容量规划(Capacity Planning)则是针对于这方面变化进行的保障策略。现有系统体量是否足够支撑新的流量需求,整体链路上是否存在不对等的薄弱节点,都是容量规划需要考虑的问题。

  2. 高可用性产品设计(Product)与软件研发(Development),即通过优秀的产品设计与软件设计使系统具备更高的可靠性,构建高可用产品架构体系,从而提升用户体验

  3. 除此之外,还需要系统资损防控等

下面也会从六个个方面分别介绍


可观测性设计

监控指标 - 完备性


监控类型 监控指标
业务监控 - 订单量、GMV、转化率等业务KPI
- 用户活跃度、留存率等用户指标
- 支付成功率、退款率等交易指标
- 下单失败率
- 支付超时率
- 库存不足率
应用监控 - 接口响应时间(RT)
- 接口调用量(QPS)
- 接口成功率
- 应用JVM指标
- 线程池使用情况
- 应用日志监控
系统监控 - 服务器CPU/内存/磁盘使用率
- 网络带宽使用率
- 系统负载
- 容器资源使用情况
- 网关流量/延迟/错误率
外部依赖 - RPC调用成功率/延迟
- 数据库连接池状态/慢查询
- 缓存命中率/延迟
- 消息队列积压量/消费延迟
- 第三方服务调用成功率/超时率
- 分布式锁获取成功率
- 分布式事务状态
- CDN服务质量
- 对象存储可用性
其它 - 安全相关指标(登录失败/异常IP等)
- 业务告警统计/处理率
- 系统变更记录/回滚率
- 运维操作审计
- 数据备份状态
- 证书过期监控
- 配置变更记录
- 资源成本监控
- 服务SLA达标率

告警 - 实时性和有效性

是不是每项监控都需要告警?答案当然是否定的。建议优先设置Biz层告警,因为Biz层我们对外服务最直观业务表现,最贴切用户感受。Application&System层指标主要用于监控,部分关键&高风险指标可设置告警,用于问题排查定位以及故障提前发现。对于一项告警,我们一般需要关注级别、阈值、通知人等几个点。

  1. 级别
    即当前告警被触发时,问题的严重程度,一般来说有几个衡量点:
  • 是否关联NOC.影响业务指标,事故级别
  • 是否影响核心链路,应用指标异常
  • 是否产生资损
  1. 阈值
  • 即一项告警的触发条件&时间,需根据具体场景合理制定。一般遵循以下原则:
  • 不可过于迟钝。一个合理的监控体系中,任何异常发生后都应触发相关告警。
  • 不可过于敏感。过于敏感的阈值会造成频繁告警,从而导致响应人员疲劳应对,无法筛选真实异常。若一个告警频繁出现,一般是两个原因:系统设计不合理 or 阈值设置不合理。
  • 若单一指标无法反馈覆盖整体业务场景,可结合多项指标关联构建。
  • 需符合业务波动曲线,不同时段可设置不同条件&通知策略。
  • 定期review 告警指标合理性
  1. 通知人&方式
  • 若为业务指标异常(Biz层告警),通知人应为问题处理人员(开发、运维同学)与业务关注人员(TL、业务同学)的集合,通知方式较为实时,比如电话通知。
  • 若为应用 & 系统层告警,主要用于定位异常原因,通知人设置问题排查处理人员即可,通知方式可考虑钉钉、短信等低干扰方式。
  • 除了关联层次,对于不同级别的告警,通知人范围也可适当扩大,尤其是关联GOC故障的告警指标,应适当放宽范围,通知方式也应更为实时直接

日志体系

  • 日志收集
  • 日志分析
  • 日志存储
  • 日志检索

追踪体系

  • 分布式追踪
  • 性能分析
  • 异常追踪
  • 依赖分析

异常应急策略

常用的应急策略

  • 降级策略
  • 限流策略
  • 熔断策略
  • 超时配置化(context 超时配置和传递)
  • 补偿策略
  • 其它业务策略:
    • 商品暂停销售
    • 营销券暂停发送等

降级策略

策略类型 具体场景 实现方式 应用案例 配置建议
降级策略 服务降级 降级开关 推荐服务返回本地缓存 开关粒度: 接口级
降级时长: 5min
功能降级 业务降级 搜索服务简化召回逻辑 降级优先级配置
降级比例配置

限流策略

策略类型 具体场景 实现方式 应用案例 配置建议
限流策略 单机接口限流(粗略) 令牌桶/漏桶算法 下单接口限制QPS为1000 QPS: 1000
突发流量: 1200
分布式限流(全局限流) Redis + Lua脚本 秒杀接口全局限限流 \n 用户维度限流 全局QPS: 5000
单机QPS: 1000,时间窗口: 1min
最大请求数: 5

熔断策略

策略类型 具体场景 实现方式 应用案例 配置建议
熔断策略 服务调用熔断 Hystrix-go 外部支付服务调用保护 错误阈值: 50%
最小请求数: 20
超时时间: 1s
中间件数据库、缓存熔断 Circuit Breaker 数据库访问保护,缓存访问保护 错误阈值: 30%
恢复时间: 5s

补偿策略

策略类型 具体场景 实现方式 应用案例 配置建议
补偿机制 状态机补偿 定时任务 订单状态异常修复 执行间隔: 5min
重试次数: 3
数据补偿 人工触发 库存数据不一致修复 补偿任务可回滚
补偿日志记录
业务补偿 消息队列 支付结果异步通知 重试策略配置
补偿通知方式
降级补偿 降级恢复 服务恢复后数据同步 补偿优先级
补偿超时时间

组合策略

组合策略应用场景

业务场景 组合策略 实现方式 配置建议
秒杀系统 限流 + 熔断 + 降级 1. 入口限流
2. 服务熔断
3. 降级返回
- 限流: 5000 QPS
- 熔断: 50% 错误率
- 降级: 返回售罄
订单系统 限流 + 补偿 + 熔断 1. 用户限流
2. 状态补偿
3. DB熔断
- 用户限流: 5次/分钟
- 补偿间隔: 1分钟
- DB超时: 1秒
支付系统 熔断 + 降级 + 补偿 1. 支付熔断
2. 降级支付
3. 异步补偿
- 熔断阈值: 30%
- 降级通道: 备用
- 补偿周期: 实时
库存系统 限流 + 熔断 + 补偿 1. 接口限流
2. 缓存熔断
3. 数据补偿
- QPS限制: 1000
- 熔断恢复: 5s
- 补偿策略: 定时

其它业务策略:

  1. 商品暂停销售
  2. 营销券暂停发送等

业务和容量规划

业务策略

不同于高可用系统建设体系,大促稳定性保障体系与面向特定业务活动的针对性保障建设,因此,业务策略与数据是我们进行保障前不可或缺的数据。
一般大促业务数据可分为两类,全局业务形态评估以及应急策略&玩法。

流量模型评估

  1. 入口流量
    对于一次大促,系统峰值入口流量一般由常规业务流量与非常规增量(比如容灾预案&业务营销策略变化带来的流量模型配比变化)叠加拟合而成。
  • 常规业务流量一般有两类计算方式:
    • 历史流量算法:该类算法假设当年大促增幅完全符合历史流量模型,根据当前&历年日常流量,计算整体业务体量同比增量模型;然后根据历年大促-日常对比,计算预估流量环比增量模型;最后二者拟合得到最终评估数据。
    • 由于计算时无需依赖任何业务信息输入,该类算法可用于保障工作初期业务尚未给出业务总量评估时使用,得到初估业务流量。
    • 业务量-流量转化算法(GMV\DAU\订单量):该类算法一般以业务预估总量(GMV\DAU\订单量)为输入,根据历史大促&日常业务量-流量转化模型(比如经典漏洞模型)换算得到对应子域业务体量评估。- 该种方式强依赖业务总量预估,可在保障工作中后期使用,在初估业务流量基础上纳入业务评估因素考虑。
  • 非常规增量一般指前台业务营销策略变更或系统应急预案执行后流量模型变化造成的增量流量。例如,NA61机房故障时,流量100%切换到NA62后,带来的增量变化.考虑到成本最小化,非常规增量P计算时一般无需与常规业务流量W一起,全量纳入叠加入口流量K,一般会将非常规策略发生概率λ作为权重
  1. 节点流量
    节点流量由入口流量根据流量分支模型,按比例转化而来。分支流量模型以系统链路为计算基础,遵循以下原则:
  • 同一入口,不同链路占比流量独立计算。
  • 针对同一链路上同一节点,若存在多次调用,需计算按倍数同比放大(比如DB\Tair等)。
  • DB写流量重点关注,可能出现热点造成DB HANG死。

容量转化

节点容量是指一个节点在运行过程中,能够同时处理的最大请求数。它反映了系统的瞬时负载能力。

1)Little Law衍生法则
不同类型资源节点(应用容器、Tair、DB、HBASE等)流量-容量转化比各不相同,但都服从Little Law衍生法则,即:
节点容量=节点吞吐率×平均响应时间
2)N + X 冗余原则

在满足目标流量所需要的最小容量基础上,冗余保留X单位冗余能力
X与目标成本与资源节点故障概率成正相关,不可用概率越高,X越高
对于一般应用容器集群,可考虑X = 0.2N

全链路压测

成本优化

研发流程规范

  • 研发
  • 测试
  • 发布

高可用的架构设计

整体架构

架构层面 设计类型 具体措施 说明
整体架构和部署 冗余设计 多机房部署 在不同地理位置部署多个机房,避免单点故障
服务多副本 每个服务部署多个实例,提供故障转移能力
数据多副本 数据多副本存储,保证数据可靠性
无状态服务设计 服务无状态化,便于水平扩展
容灾设计 同城双活 同一城市两个机房同时对外服务
异地多活 多地机房同时对外服务,就近访问
机房级容灾 单机房故障时可切换到其他机房
数据中心容灾 数据中心级别的容灾能力
灾备演练 演练计划 定期制定灾备演练计划
演练流程 标准化的演练流程和checklist
效果评估 对演练结果进行评估和复盘
持续改进 根据演练反馈持续优化容灾方案
安全架构 网络安全 防火墙/WAF 防御网络攻击,过滤恶意流量
VPN/专线 安全的网络连接方式
数据安全 加密存储 敏感数据加密存储
访问控制 严格的数据访问权限控制
应用安全 身份认证 多因素认证,防止账号盗用
操作审计 记录关键操作审计日志

应用层架构

设计类型 具体措施 说明
隔离设计 服务隔离 核心服务独立部署,避免互相影响
数据隔离 按业务领域划分数据存储
故障隔离 故障隔离机制,防止故障扩散
资源隔离 CPU/内存等资源隔离管理
水平扩展设计 服务无状态化 服务设计无状态,便于扩容
数据分片策略 数据分片存储,支持横向扩展
弹性伸缩 根据负载自动扩缩容
负载均衡 多实例间的负载分配策略

系统链路梳理和维护 System & Biz Profiling

系统链路梳理是所有保障工作的基础,它就像对整个应用系统进行一次全面体检。从流量入口开始,按照链路轨迹,逐级分层节点,最终得到系统全局画像与核心保障点。主要包含以下几个方面:

  1. 入口梳理盘点
    系统通常有多个流量入口,包括HTTP、RPC、消息等多种来源。建议优先从以下三类重点入口开始梳理:
  • 核心重保流量入口
    • 有明确的服务SLI承诺,对数据准确性、响应时间、可靠度有严格要求
    • 业务核心链路(浏览、下单、支付、履约等)
    • 面向企业级用户的服务
  • 资损风险入口
    • 涉及公司或客户资金收入的收费服务
    • 可能造成资金损失的关键节点
  • 大流量入口
    • 系统TPS/QPS排名前5-10的入口
    • 虽无高SLI要求但流量大,对系统整体负载影响显著
  1. 节点分层分析
    按照流量轨迹,对链路上的各类节点(HSF、DB、Tair、HBase等外部依赖)进行分层分析:
  • 强弱依赖判定

    • 业务强依赖:节点不可用会导致业务中断或严重受损
    • 系统强依赖:节点不可用会导致系统执行中断
    • 性能强依赖:节点不可用会严重影响系统性能
    • 弱依赖:有降级方案或影响较小
  • 可用性风险判定

    • 日常超时频发的节点
    • 系统资源紧张的节点
    • 历史故障频发的节点
  • 高风险节点识别

    • 近期改造过的节点
    • 首次参与大促的新节点
    • 曾出现过高级别故障的节点
    • 可能造成资损的关键节点
  1. 调用关系分析
  • 绘制核心接口调用拓扑图或时序图
  • 计算各节点间调用比例
  • 分析调用链路上的资损风险点
  • 识别内外部系统依赖关系
  1. 系统调用拓扑图(可借助分布式链路追踪系统),包含QPS、强弱依赖、高风险节点标注

  2. 接口时序图(以创建订单为例)

资损防控架构

定期review资损风险
事中及时发现


【得物技术】浅谈资损防控

事后复盘和知识沉淀

性能优化


  1. 应用层优化

    • 代码优化
    • JVM调优
    • 线程池优化
    • 连接池优化
  2. 数据层优化

    • SQL优化
    • 索引优化
    • 分库分表
    • 读写分离
  3. 缓存优化

    • 多级缓存
    • 热点缓存
    • 缓存预热
    • 缓存穿透/击穿防护

新品类接入指南

11.1 接入检查清单

检查项 商品管理 库存管理 价格管理
数据模型 确定品类属性和 SKU 结构 确定 (ManagementType, UnitType) 确定基础价格来源
流程配置 配置审核策略(运营/商家/供应商) 配置扣减时机(下单/支付) 配置费用类型
供应商对接 配置 Push/Pull 策略 配置同步策略(实时/定时) 配置动态定价规则
校验规则 注册品类校验规则 - -
自定义逻辑 实现 ValidationRule 接口 实现 Strategy 接口(如需) 实现 BasePriceCalculator(如需)

11.2 四步接入示例:演唱会门票

Step 1: 商品管理配置

1
2
3
4
5
6
-- 1. 配置审核策略
INSERT INTO listing_audit_config_tab (category_id, source_type, audit_strategy)
VALUES
(50001, 'supplier_push', 'fast_track'), -- 供应商推送:快速通道
(50001, 'operator_form', 'skip'), -- 运营上传:免审核
(50001, 'merchant_portal', 'manual'); -- 商家上传:人工审核
1
2
3
4
// 2. 注册校验规则
engine.RegisterRule("concert", &ConcertValidationRule{
// 演唱会特殊校验:场次时间、座位区域、票价范围
})

Step 2: 库存管理配置

1
2
3
4
-- 1. 配置库存策略
INSERT INTO inventory_config (item_id, management_type, unit_type, deduct_timing, supplier_id, sync_strategy)
VALUES (900001, 2, 3, 2, 700001, 2);
-- 供应商管理 + 时间维度 + 支付扣减 + 实时同步
1
2
3
4
5
6
7
8
// 2. 调用统一接口(无需修改核心代码)
inventoryManager.BookStock(ctx, &BookStockReq{
ItemID: 900001,
SKUID: 900001001,
Quantity: 2,
OrderID: orderID,
Context: map[string]interface{}{"session_id": "202608150900"},
})

Step 3: 价格管理配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 1. 配置基础价格(按场次 × 区域)
INSERT INTO sku_tab (item_id, sku_code, sku_name, price)
VALUES (900001, 'SKU_CONCERT_VIP', 'VIP区', 2800.00);

-- 2. 配置费用
INSERT INTO fee_config_tab (fee_code, fee_name, fee_type, category_id, calculation_type, calculation_config)
VALUES
('FEE_PLATFORM_CONCERT', '平台手续费', 'platform_fee', 50001, 'percentage', '{"percentage": 3}'),
('FEE_TICKET_SERVICE', '票务服务费', 'service_fee', 50001, 'fixed', '{"amount": 15}');

-- 3. 关联营销活动(在活动 category_ids 中加入 50001)
UPDATE promotion_activity_tab
SET category_ids = JSON_ARRAY_APPEND(category_ids, '$', 50001)
WHERE activity_code = 'PROMO_SUMMER_SALE';

Step 4: 验证接入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 完整流程测试
func TestConcertTicketFlow(t *testing.T) {
// 1. 创建演唱会商品
task := listingService.CreateTask(&CreateTaskRequest{
CategoryID: 50001,
SourceType: "operator_form",
ItemData: map[string]interface{}{
"title": "周杰伦2026演唱会",
"venue": "鸟巢",
"date": "2026-08-15",
"session_time": "19:00",
},
})
assert.Equal(t, StatusOnline, task.Status)

// 2. 查询价格
priceResp := pricingEngine.Calculate(ctx, &PriceRequest{
ItemID: task.ItemID,
SKUID: 900001001,
Quantity: 2,
})
assert.Equal(t, decimal.NewFromFloat(5630.00), priceResp.FinalPrice)
// 2800 × 2 + 3% 平台手续费 + 15 × 2 服务费 = 5630

// 3. 预订库存
bookResp := inventoryManager.BookStock(ctx, &BookStockReq{
ItemID: task.ItemID,
SKUID: 900001001,
Quantity: 2,
})
assert.True(t, bookResp.Success)

// 4. 支付 → 库存售出
inventoryManager.SellStock(ctx, &SellStockReq{
ItemID: task.ItemID,
SKUID: 900001001,
Quantity: 2,
OrderID: 123456,
})
}

其它常见问题

基础概念与架构设计

  • 电商后台系统的核心架构设计原则有哪些?
  • 电商后台系统与前端系统的交互方式有哪些?各自的特点是什么?
  • 如何设计电商后台系统的用户权限管理模块?
  • 电商后台系统中,微服务架构和单体架构的适用场景分别是什么?
  • 简述电商后台系统的分层架构设计,各层的主要职责是什么?
  • 如何实现电商后台系统的接口幂等性?
  • 电商后台系统中,分布式 Session 管理有哪些常见方案?
  • 设计电商后台系统时,如何考虑系统的可扩展性和可维护性?
  • 电商后台系统的 API 设计规范应包含哪些内容?
  • 如何设计电商后台系统的异常处理机制?

商品管理

  • 什么是SPU和SKU?它们之间的关系是什么?

  • 电商系统中的商品分类体系是如何设计的? category 父子类目

  • 什么是商品属性?如何区分规格属性(Sales Attributes))和非规格属性?属性,会影响商品SKU的属性直接关系到库存和价格,用户购买时需要选择的属性,例如:颜色、尺码、内存容量等。非规格属性(Basic Attributes):用于描述商品特征,产地、材质、生产日期

  • 商品的生命周期包含哪些状态?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    创建阶段
    DRAFT(0): 草稿状态
    PENDING_AUDIT(1): 待审核
    AUDIT_REJECTED(2): 审核拒绝
    AUDIT_APPROVED(3): 审核通过
    销售阶段
    ON_SHELF(10): 在售/上架
    OFF_SHELF(11): 下架
    SOLD_OUT(12): 售罄
    特殊状态
    FROZEN(20): 冻结(违规/投诉)
    DELETED(99): 删除

    什么是商品快照?为什么需要商品快照?

  • 商品的 SKU 和 SPU 概念在后台系统中如何体现?两者的关系是怎样的?

  • 电商后台系统中商品的基础信息包括哪些?如何设计商品表的数据库模型?

  • 商品的库存管理和价格管理在后台系统中是如何关联的?

  • 如何处理商品的多规格(如颜色、尺寸、型号等)信息?数据库表结构如何设计?

  • 商品详情页的信息(如描述、图片、参数)在后台系统中如何存储和管理?

  • 商品上下架的逻辑在后台系统中是如何实现的?需要考虑哪些因素(如库存、审核状态等)?

  • 商品的搜索和筛选功能在后台系统中是如何实现的?涉及哪些技术(如全文搜索、数据库索引等)?

  • 新品发布和商品淘汰在后台系统中的处理流程是怎样的?

  • 如何保证商品信息的唯一性和完整性,避免重复录入和数据错误?

  1. 唯一性保证:
    使用唯一索引
    引入商品编码系统
    查重机制
  2. 完整性保证:
    必填字段验证
    数据格式校验
    关联完整性检查
    业务规则校验

订单管理

  • 电商后台系统中订单的主要状态有哪些?状态流转的触发条件和处理逻辑是怎样的?
  • 订单的创建流程在后台系统中是如何处理的?涉及哪些模块(如库存、价格、用户信息等)的交互?
  • 如何实现订单的分单处理(如不同仓库发货、不同店铺订单拆分)?
  • 订单的支付状态如何与支付系统进行同步?后台系统需要处理哪些异常情况?
  • 订单的取消、修改(如收货地址、商品数量)在后台系统中有哪些限制和处理逻辑?
  • 如何计算订单的总价(包括商品价格、运费、优惠活动等)?优惠分摊的逻辑是怎样的?
  • 订单的物流信息在后台系统中如何获取和更新?与物流服务商的接口如何对接?
  • 历史订单的存储和查询在后台系统中如何优化?涉及大量数据时如何提高查询效率?
  • 如何设计订单的反欺诈机制,识别和防范恶意订单?
  • 订单的售后服务(如退货、换货、退款)在后台系统中的处理流程是怎样的?与库存、财务等模块如何交互?

用户与账户管理

  • 电商后台系统中用户信息通常包含哪些字段?如何设计用户表的数据库结构?
  • 如何实现用户的注册、登录(包括第三方登录)功能在后台系统中的处理逻辑?
  • 怎样处理用户密码的加密存储和找回功能?
  • 电商后台如何管理用户的收货地址?地址数据的增删改查逻辑是怎样的?
  • 用户账户余额和积分的管理在后台系统中有哪些注意事项?如何保证数据的一致性?
  • 如何实现用户权限的分级管理(如普通用户、VIP 用户、管理员等)?
  • 当用户账户出现异常登录时,后台系统应如何处理和记录?
  • 电商后台如何统计用户的活跃度、留存率等指标?数据来源和计算逻辑是怎样的?
  • 用户信息修改(如手机号、邮箱)时,后台系统需要进行哪些验证和处理?
  • 如何设计用户操作日志的记录和查询功能,以满足审计和问题排查需求?

库存管理

  • 电商后台系统中库存管理的主要目标是什么?常见的库存管理策略有哪些?
  • 如何实现库存的实时更新?在高并发场景下如何保证库存数据的一致性?
  • 库存预警机制如何设计?预警的条件(如安全库存、滞销库存等)和通知方式是怎样的?
  • 多仓库库存管理在后台系统中如何实现?库存的分配和调拨逻辑是怎样的?
  • 库存盘点功能在后台系统中的实现步骤是怎样的?如何处理盘点差异?
  • 预售商品的库存管理与普通商品有何不同?后台系统需要特殊处理哪些方面?
  • 如何防止超卖现象的发生?在库存不足时,订单的处理逻辑是怎样的?
  • 库存数据与订单、采购、物流等模块的交互接口是如何设计的?
  • 对于虚拟商品(如电子卡券),库存管理的方式与实物商品有何区别?
  • 如何统计库存的周转率、缺货率等指标?数据来源和计算方法是怎样的?

支付与结算

  • 电商后台系统支持哪些支付方式?每种支付方式的对接流程和注意事项是什么?
  • 支付系统与电商后台系统的交互接口应包含哪些关键信息?如何保证支付数据的安全性?
  • 支付过程中的异步通知机制是如何实现的?后台系统如何处理重复通知和通知失败的情况?
  • 如何实现支付订单与业务订单的关联和对账功能?
  • 结算周期和结算规则在后台系统中如何配置和管理?(如供应商结算、平台佣金结算等)
  • 支付过程中的手续费计算和分摊逻辑是怎样的?如何在后台系统中实现?
  • 对于跨境支付,后台系统需要处理哪些特殊问题(如汇率转换、支付合规性等)?
  • 如何设计支付系统的异常处理和回滚机制?
  • 支付成功后,后台系统如何触发后续的业务流程(如订单发货、积分发放等)?
  • 财务对账在后台系统中的实现方式有哪些?如何保证财务数据与业务数据的一致性?

物流与供应链

  • 电商后台系统如何与物流服务商(如快递、仓储)进行接口对接?需要获取哪些物流信息?
  • 物流单号的生成和管理在后台系统中是如何实现的?如何避免重复和错误?
  • 发货流程在后台系统中的处理逻辑是怎样的?涉及哪些部门或系统的协作(如仓库、库存、订单等)?
  • 如何实现物流信息的实时追踪和更新?在后台系统中如何展示给用户和客服?
  • 退换货的物流处理在后台系统中有哪些特殊流程?如何与原订单和库存进行关联?
  • 供应链管理在电商后台系统中包括哪些主要功能?如何实现供应商管理、采购管理和库存管理的协同?
  • 如何根据商品的特性和用户地址选择合适的物流方案(如快递类型、运费模板等)?
  • 物流异常(如包裹丢失、破损)在后台系统中的处理流程是怎样的?如何与用户和物流服务商沟通协调?
  • 如何统计物流成本和物流效率(如发货时效、配送成功率等)?数据来源和分析方法是怎样的?
  • 对于海外仓和跨境物流,后台系统需要处理哪些额外的业务逻辑(如清关、关税计算等)?

营销与促销

  • 电商后台系统中常见的促销策略有哪些(如满减、打折、优惠券、秒杀、拼团等)?如何设计支持多种促销策略的模块?
  • 优惠券的生成、发放、使用和核销在后台系统中的处理流程是怎样的?
  • 促销活动的时间管理和范围管理(如针对特定用户群体、特定商品、特定时间段)如何实现?
  • 如何避免促销活动中的超卖和优惠叠加错误?后台系统的校验逻辑是怎样的?
  • 秒杀活动在后台系统中如何应对高并发场景?需要进行哪些技术优化?
  • 营销活动的效果评估指标(如转化率、客单价提升、销售额增长等)在后台系统中如何统计和分析?
  • 如何设计推荐系统与后台营销模块的集成,实现个性化的促销推荐?
  • 会员体系(如 VIP 等级、积分兑换)在后台系统中如何与营销活动结合?
  • 促销活动的库存预留和释放逻辑是怎样的?如何与库存管理模块进行交互?
  • 营销费用的预算管理和成本核算在后台系统中如何实现?

数据统计与分析

  • 电商后台系统需要统计哪些核心业务指标(如 GMV、UV、PV、转化率、复购率等)?数据采集的方式和频率是怎样的?
  • 如何设计数据报表功能,支持不同角色(如运营、管理层、客服)的个性化报表需求?
  • 数据统计中的维度和指标如何定义和管理?如何实现多维度的交叉分析?
  • 实时数据统计和离线数据统计在后台系统中的实现方式有何不同?各自的适用场景是什么?
  • 如何保证数据统计的准确性和完整性?数据清洗和校验的流程是怎样的?
  • 数据分析结果如何反馈到业务模块(如库存调整、促销策略优化等)?
  • 数据可视化在后台系统中的实现方式有哪些(如图表、仪表盘、数据大屏等)?
  • 对于海量数据的统计分析,后台系统需要进行哪些性能优化(如分布式计算、缓存、索引等)?
  • 如何设计数据权限管理,确保不同用户只能查看和操作其权限范围内的数据?
  • 数据统计分析模块与其他业务模块(如订单、商品、用户等)的数据接口是如何设计的?

其他综合问题

  • 电商后台系统在应对大促(如双 11、618)时,需要进行哪些准备工作和技术优化?
  • 如何保障电商后台系统的高可用性和容灾能力?常见的解决方案有哪些?
  • 后台系统的代码维护和版本管理有哪些最佳实践?如何保证多人协作开发的效率和代码质量?
  • 当电商业务拓展到新的领域或增加新的业务模块时,后台系统如何进行适应性改造?
  • 如何处理不同国家和地区的电商业务在后台系统中的差异化需求(如语言、货币、法规等)?
  • 电商后台系统中的日志管理有哪些重要性?如何设计日志的记录、存储和查询功能?
  • 对于第三方服务(如短信验证码、邮件通知、数据分析工具等)的接入,后台系统需要注意哪些问题?
  • 如何评估电商后台系统的性能瓶颈?常见的性能测试工具和方法有哪些?
  • 简述你在以往项目中参与过的电商后台系统开发经验,遇到过哪些挑战,是如何解决的?
  • 对于电商后台系统的未来发展趋势(如智能化、自动化、区块链应用等),你有哪些了解和思考?

系列导航(与文首「推荐阅读顺序」一致)

参考:

速查导航

阅读时间: 35 分钟 | 难度: ⭐⭐⭐⭐ | 面试频率: 中高

核心考点速查:


前言

为什么要做设计方案

  • 设计是系统实现的蓝图
  • 设计是沟通协作的基础
  • 设计是思考的过程决定了产品的质量
    理解对齐:所有软件系统的目的都是为了实现用户需求,但实现的途径有无限种可能性(相比传统工程行业,软件的灵活性更大、知识迭代更快)。架构设计就是去选择其中一条最合适的实现途径,因此其中会涉及非常多关键的选路决策(为什么要这么拆分?为什么选择 A 技术而不是 B?)。这些重要的技术决策需要通过架构描述这种形式被记录和同步,才能让项目组所有成员对整个系统的理解对齐,形成共识。
    工作量化:项目管理最重要的步骤之一就是工时评估,它是确定项目排期和里程碑的直接依据。显然,只通过 PRD / 交互图是无法科学量化出项目工作量的,因为很难直观判断出一句简短需求或一个简单页面背后,究竟要写多少代码、实现起来难度有多大。有了清晰明确的架构之后,理论上绝大部分开发工作都能做到可见、可预测和可拆解,自然而然也就能够被更准确地量化。当然,精准的工作量评估在 IT 行业内也一直是个未解之谜,实际的工期会受太多未知因素影响,包括程序员的技能熟练度、心情好不好、有没有吃饱等。
    标准术语:编程作为一种具有创造力的工作,从某种角度看跟写科幻小说是类似的。好的科幻小说都喜欢造概念,比如三体中的智子,如果没看过小说肯定不知道这是个啥玩意儿。软件系统在造概念这一点上,相比科幻小说只有过之而无不及,毕竟小说里的世界通常还是以现实为背景,而软件中的世界就全凭造物者(程序员)的想象(建模)了。稍微复杂一点的软件系统,都会引入一些领域特定甚至全新创作的概念。为了避免在项目过程中出现鸡同鸭讲的沟通障碍和理解歧义,就必须对描述这些概念的术语进行统一。而架构的一个重要目的,就是定义和解释清楚系统中涉及的所有关键概念,并在整个架构设计和描述过程中使用标准和一致的术语,真正做到让大家的沟通都在一个频道上。
    言之有物:就跟讨论产品交互时需要对着原型图、讨论代码细节时需要直接看代码一样,架构是在讨论一些较高维技术问题时的必要实物(具体的实物化形式就是所谓架构描述)。否则,要么一堆人对着空气谈(纸上谈兵都说不上),要么每次沟通时都重新找块白板画一画(费时费力且容易遗落信息,显然不是长久之计)。
    知识沉淀 & 新人培训:架构应该被作为与代码同等重要的文档资产持续沉淀和维护,同时也是项目新人快速理解和上手系统的重要依据。不要让你的系统跟公司内某些祖传遗留系统一样 —— 只有代码遗留了下来,架构文档却没有;只能靠一些口口相传的残留设计记忆,苦苦维系着项目的生命延续。

技术方案应该包含哪些内容


  1. 背景:

    • 解决的问题:明确要解决的技术问题和产品问题的具体描述。
    • 难点和挑战:列出可能遇到的难点、挑战和限制条件。
    • 目标和关键指标:明确解决方案的目标和关键指标,例如性能要求、用户体验等。
  2. 外部依赖调研

    • 外部服务和组件:列出系统所依赖的外部服务、组件或系统,并描述其功能和接口。
    • 管理和集成策略:说明如何管理和集成外部依赖,包括版本控制、接口规范等。
  3. 业界方案调研和对比:

    • 调研结果:调研现有的业界解决方案,并总结其优缺点。
    • 对比分析:比较不同方案之间的特点、适用性和可行性。
  4. 整体设计:

    • 业务流程架构图:展示系统的业务流程和组件之间的关系。
    • 系统调用拓扑图:显示系统内部和外部的调用关系。
    • 技术架构图:描述系统的技术架构,包括各个模块、组件和数据流之间的关系。
  5. 功能设计:

    • 存储设计:定义系统中数据的存储方式和结构。
    • 接口设计:定义系统的各个模块之间的接口和通信方式。
    • 流程设计:描述系统的各个功能模块的流程和交互方式。
    • 缓存设计:确定系统中需要使用的缓存策略和机制。
  6. 非功能设计:

    • 兼容性设计:考虑系统与不同平台、浏览器或设备的兼容性。
    • 稳定性设计:定义系统的容错和恢复机制,确保系统的稳定性和可用性。
    • 扩展性设计:考虑系统的可扩展性,以便在需要时能够方便地扩展功能和容量。
    • 安全设计:定义系统的安全策略和机制,保护用户数据和系统资源。
    • 性能设计:考虑系统的性能需求,并设计相应的优化措施。
    • 部署设计:定义系统的部署架构和流程,包括服务器配置、网络拓扑等。
    • 可维护性设计:考虑系统的可维护性,包括日志记录、错误处理和调试功能。
    • 测试策略和方案:定义系统的测试策略和测试计划,包括单元测试、集成测试和系统测试等。
    • 部署和运维设计:描述系统的部署和运维策略,包括自动化部署、监控和故障处理等。
    • 风险点:识别系统设计中的潜在风险和问题,并提供相应的应对措施。
    • 监控设计和异常处理机制:
    • 监控需求:定义系统的监控需求,包括日志记录、性能监控和错误监控等。
    • 异常处理机制:描述系统对异常情况的处理方式和机制,包括错误提示、异常捕获和处理流程等。
  7. 资源清单:

    • 硬件资源:列出系统所需的硬件资源,例如服务器、存储设备等。
    • 软件资源:列出系统所需的软件资源,例如操作系统、数据库等。
    • 人力资源:确定系统开发和维护所需的人力资源,包括开发人员、测试人员等。
  8. 任务拆分和排期:

    • 任务拆分:将系统开发和实施过程分解为具体的任务和子任务。
    • 排期计划:为每个任务和子任务确定时间表和优先级。
  9. 评审记录:

    • 评审会议记录:记录技术方案评审会议的讨论和决策结果。
    • 修改和改进建议:记录评审过程中提出的修改和改进建议,并记录其处理状态。

如何评估技术方案的质量


功能性

  • 功能完整度
  • 功能正确性
  • 功能恰当性

稳定性(Dependability Criteria):

  • 可靠性(Reliability):系统处理错误和故障,保证数据完整性和可用性的能力
  • 兼容性,向前兼容性值
  • 可用性(Availability):系统在投入使用时可操作和可访问的程度。
  • 安全性(Security):系统保护用户数据和系统资源,防止未经授权的访问和恶意行为的能力

性能(Performance):

  • 响应时间(Latency):系统对请求的反应速度。
  • 吞吐量(Throughput):系统处理的工作量

成本(Cost):

  • 开发成本(Development Cost):系统的构建和开发所需的费用。
  • 部署成本(Deployment Cost):系统部署和运行所需的资源成本。
  • 升级成本(Upgrade Cost):将数据从旧系统转换到新系统,以及满足向后兼容性要求的成本。
  • 维护成本(Maintenance Cost):包括错误修复和未来功能增强的成本。
  • 运营成本(Administration Cost):运行系统的成本。

维护性(Maintainability)

  • 可扩展性(Extensibility):系统添加新功能的容易程度。
  • 可修改性(Modifiability):系统更改功能的容易程度。
  • 适应性(Adaptability):系统适应不同应用领域的能力。
  • 可移植性(Portability):系统在不同计算机平台上运行的容易程度。
  • 可读性(Readability):代码的理解难度。
  • 需求可追溯性(Traceability of Requirements):代码与需求之间的映射关系。
  • 可测试性(Testability):系统易于测试和验证的程度。

用户体验(User Experience)

  • 系统提供友好的用户界面和良好的用户交互,以提高用户满意度和使用效率。

技术方案模板

附录:设计文档模板

设计文档没有定式。即使如此,笔者参考谷歌设计文档的结构和格式,并结合实际工作经验加以完善。在此提供一个可供新手参考的设计文档模板,您可以使用此文档模板作为思考的基础。通常,无须事无巨细地填写每一部分,不相关的内容直接略过即可。

设计文档应该体现设计决策的合理性,同时也有助于日后迭代设计时,检查最初的假设是否仍然成立。

背景

我们要解决的问题是什么

为设计文档的目标读者提供理解详细设计所需的背景信息。按读者范围来提供背景。见上文关于目标读者的圈定。设计文档应该是“自足的”(self-contained),即应该为读者提供足够的背景知识,使其无需进一步的查阅资料即可理解后文的设计。保持简洁,通常以几段为宜,每段简要介绍即可。如果需要向读者提供进一步的信息,最好只提供链接。警惕知识的诅咒(知识的诅咒(Curse of knowledge)是一种认知偏差,指人在与他人交流的时候,下意识地假设对方拥有理解交流主题所需要的背景知识)

背景通常可以包括:
需求动机以及可能的例子。 如,“(tRPC) 微服务模式正在公司内变得流行,但是缺少一个通用的、封装了常用内部工具及服务接口的微服务框架”。 - 这是放置需求文档的链接的好地方。
此前的版本以及它们的问题。 如,“(tRPC) Taf 是之前的应用框架, 有以下特点,…………, 但是有以下局限性及历史遗留问题”。
其它已有方案, 如公司内其它方案或开源方案, “tRPC v.s. gRPC v.s. Arvo”
相关的项目,如 “tRPC 框架中可能会对接的其它 PCG 系统”
不要在背景中写你的设计,或对问题的解决思路。

难点和挑战

“解决这个问题的难点和挑战”

用几句话说明该设计文档的关键目的,让读者能够一眼得知自己是否对该设计文档感兴趣。 如:“本文描述 Spanner 的顶层设计”

目标和关键指标

继而,使用 Bullet Points 描述该设计试图达到的重要目标,如:

  • 可扩展性
  • 多版本
  • 全球分布
  • 同步复制
    非目标也可能很重要。非目标并非单纯目标的否定形式,也不是与解决问题无关的其它目标,而是一些可能是读者非预期的、本可作为目标但并没有的目标,如:
  • 高可用性
  • 高可靠性 如果可能,解释是基于哪些方面的考虑将之作为非目标。如:
  • 可维护性: 本服务只是过渡方案,预计寿命三个月,待 XX 上线运行后即可下线
    设计不是试图达到完美,而是试图达到平衡。 显式地声明哪些是目标,哪些是非目标,有助于帮助读者理解下文中设

总体设计

“我们如何解决这个问题?”

用一页描述高层设计。说明系统的主要组成部分,以及一些关键设计决策。应该说明该系统的模块和决策如何满足前文所列出的目标。

本设计文档的评审人应该能够根据该总体设计理解你的设计思路并做出评价。描述应该对一个新加入的、不在该项目工作的腾讯工程师而言是可以理解的。

推荐使用系统关系图描述设计。它可以使读者清晰地了解文中的新系统和已经熟悉的系统间的关系。它也可以包含新系统内部概要的组成模块。

注意:不要只放一个图而不做任何说明,请根据上面小节的要求用文字描述设计思想。

  • 一个示例体统关系图

  • 自举的文档结构图

  • 可能不太好的顶层设计
    不要在这里描述细节,放在下一章节中;不要在这里描述背景,放在上一章节中。

详细设计

在这一节中,除了介绍设计方案的细节,还应该包括在产生最终方案过程中,主要的设计思想及权衡(tradeoff)。这一节的结构和内容因设计对象(系统,API,流程等)的不同可以自由决定,可以划分一些小节来更好地组织内容,尽可能以简洁明了的结构阐明整个设计。

不要过多写实现细节。就像我们不推荐添加只是说明”代码做了什么”的注释,我们也不推荐在设计文档中只说明你具体要怎么实现该系统。否则,为什么不直接实现呢? 以下内容可能是实现细节例子,不适合在设计文档中讨论:

  • API 的所有细节
  • 存储系统的 Data Schema
  • 具体代码或伪代码
  • 该系统各模块代码的存放位置、各模块代码的布局
  • 该系统使用的编译器版本

通常可以包含以下内容(注意,小节的命名可以更改为更清晰体现内容的标题):

各子模块的设计
阐明一些复杂模块内部的细节,可以包含一些模块图、流程图来帮助读者理解。可以借助时序图进行展现,如一次调用在各子模块中的运行过程。每个子模块需要说明自己存在的意义。如无必要,勿添模块。如果没有特殊情况(例如该设计文档是为了描述并实现一个核心算法),不要在系统设计加入代码或者伪代码。

API 接口

如果设计的系统会暴露 API 接口,那么简要地描述一下 API 会帮助读者理解系统的边界。避免将整个接口复制粘贴到文档中,因为在特定编程语言中的接口通常包含一些语言细节而显得冗长,并且有一些细节也会很快变化。着重表现 API 接口跟设计最相关的主要部分即可。

存储设计
介绍系统依赖的存储设计。该部分内容应该回答以下问题,如果答案并非显而易见:

该系统对数据/存储有哪些要求?

  • 该系统会如何使用数据?
  • 数据是什么类型的?
  • 数据规模有多大?
  • 读写比是多少?读写频率有多高?
  • 对可扩展性是否有要求?
  • 对原子性要求是什么?
  • 对一致性要求是什么?是否需要支持事务?
  • 对可用性要求是什么?
  • 对性能的要求是什么?

基于上面的事实,数据库应该如何选型?

  • 选用关系型数据库还是非关系型数据库?是否有合适的中间件可以使用?
  • 如何分片?是否需要分库分表?是否需要副本?
  • 是否需要异地容灾?
  • 是否需要冷热分离?
    数据的抽象以及数据间关系的描述至关重要。可以借助 ER 图(Entity Relationshiop) 的方式展现数据关系。

回答上述问题时,尽可能提供数据,将数据作为答案或作为辅助。 不要回答“数据规模很大,读写频繁”,而是回答“预计数据规模为 300T, 3M 日读出, 0.3M 日写入, 巅峰 QPS 为 300”。这样才能为下一步的具体数据库造型提供详细的决策依据,并让读者信服。 注意:在选型时也应包括可能会造成显著影响的非技术因素,如费用。

避免将所有数据定义(data schema)复制粘贴到文档中,因为 data schema 更偏实现细节。

其他方案

“我们为什么不用另一种方式解决问题?”

在介绍了最终方案后,可以有一节介绍一下设计过程中考虑过的其他设计方案(Alternatives Considered)、它们各自的优缺点和权衡点、以及导致选择最终方案的原因等。通常,有经验的读者(尤其是方案的审阅者)会很自然地想到一些其他设计方案,如果这里的介绍描述了没有选择这些方案的原因,就避免读者带着疑问看完整个设计再来询问作者。这一节可以体现设计的严谨性和全面性。

交叉关注点

基础设施

如果基础设施的选用需要特殊考量,则应该列出。如果该系统的实现需要对基础设施进行增强或变更,也应该在此讨论。

可扩展性

你的系统如何扩展?横向扩展还是纵向扩展?注意数据存储量和流量都可能会需要扩展。

安全 & 隐私

项目通常需要在设计期即确定对安全性的保证,而难以事后补足。不同于其它部分是可选的,安全部分往往是必需的。即使你的系统不需要考虑安全和隐私,也需要显式地在本章说明为何是不必要的。

安全性如何保证?

  • 系统如何授权、鉴权和审计(Authorization, Authentication and Auditing, AAA)?
  • 是否需要破窗(break-glass)机制?
  • 有哪些已知漏洞和潜在的不安全依赖关系?
  • 是否应该与专业安全团队讨论安全性设计评审?
数据完整性

如何保证数据完整性(Data Integrity)?如何发现存储数据的损坏或丢失?如何恢复?由数据库保证即可,还是需要额外的安全措施?为了数据完整性,需要对稳定性、性能、可复用性、可维护性造成哪些影响?

延迟

声明延迟的预期目标。描述预期延迟可能造成的影响,以及相关的应对措施。

冗余 & 可靠性

是否需要容灾?是否需要过载保护、有损降级、接口熔断、轻重分离?是否需要备份?备份策略是什么?如何修复?在数据丢失和恢复之间会发生什么?

稳定性

SLA 目标是什么?如何监控?如何保证?

外部依赖

你的外部依赖的可靠性(如 SLA)如何?会对你的系统的可靠性造成何种影响?如果你的外部依赖不可用,会对你的系统造成何种影响?除了服务级的依赖外,不要忘记一些隐含的依赖,如 DNS 服务、时间协议服务、运行集群等。

任务拆分和研发排期

描述时间及人力安排(如里程碑)。这有利于相关人员了解预期,调整工作计划。

遗留的问题与未来计划

未来可能的计划会方便读者更好地理解该设计以及其定位。列出当前设计中已知但暂未解决的问题,以及未来可能的优化方向和扩展计划。

技术方案设计的规范与模板

技术设计基础

如何量化系统指标(SLA指标)

reliable


available


efficiency

latency and throughput

manageability


系统设计的权衡(Top 15 Trade-off)

  • 性能与可扩展性的权衡:提高性能可能需要牺牲一部分可扩展性,因为某些优化可能会引入复杂性或限制系统的扩展性。
  • 可维护性与性能的权衡:某些优化措施可能会降低代码的可读性和可维护性,因此需要在维护性和性能之间进行权衡。
  • 时间与成本的权衡:系统设计需要考虑开发时间和成本,以确保在给定资源限制下实现最佳的设计方案。
  • 安全性与用户体验的权衡:强大的安全措施可能会增加用户的身份验证和授权过程,从而影响用户体验。

参考阅读

架构权衡评估方法(ATAM):如何评估一个系统的质量
架构-trade-off(架构权衡
https://haomo-tech.com/project-docs/%E7%B3%BB%E7%BB%9F%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E6%96%87%E6%A1%A3/assets/%E7%B3%BB%E7%BB%9F%E4%B8%9A%E5%8A%A1%E6%9E%B6%E6%9E%84%E5%9B%BE.omnigraffle
架构-trade-off(架构权衡
架构权衡评估方法(ATAM):如何评估一个系统的质量
系统架构

面向对象系统设计的原则

SOLID 原则概述

SOLID 原则是一套比较经典且流行的架构原则(主要还是名字起得好):
单一职责:与 Unix 哲学所倡导的“Do one thing and do it well”不谋而合;
开闭原则:用新增(扩展)来取代修改(破坏现有封装),这与函数式的 immutable 思想也有异曲同工之妙;
里式替换:父类能够出现的地方子类一定能够出现,这样它们之间才算是具备继承的“Is-A”关系;
接口隔离:不要让一个类依赖另一个类中用不到的接口,简单说就是最小化组件之间的接口依赖和耦合;

  • 依赖反转(Dependency Inversion):依赖抽象类与接口,而不是具体实现;让低层次模块依赖高层次模块的稳定抽象,实现解耦。

其他架构设计原则

此外,我们做架构设计时也会尽量遵循如下一些原则(与上述 SOLID 原则在本质上也是相通的):

  • 正交性:架构同一层次拆分出的各组件之间,应该尽量保持正交,即彼此职责独立,边界清晰,没有重叠。
  • 高内聚:同一组件内部应该是高度内聚的(cohesive),像是一个不可分割的整体(否则就应该拆开)。
  • 低耦合:不同组件之间应该尽量减少耦合(coupling),既降低相互的变化影响,也能增强组件可复用性。
  • 隔离变化:许多架构原则与模式的本质都是在隔离变化 —— 将预期可能变化的部分都隔离到一块,减少发生变化时受影响(需要修改代码、重新测试或产生故障隐患)的其他稳定部分。

参考阅读

互联网系统八大谬论


数学估算

延迟数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Latency Comparison Numbers
--------------------------
L1 cache reference 0.5 ns
Branch mispredict 5 ns
L2 cache reference 7 ns 14x L1 cache
Mutex lock/unlock 25 ns
Main memory reference 100 ns 20x L2 cache, 200x L1 cache
Compress 1K bytes with Zippy 10,000 ns 10 us
Send 1 KB bytes over 1 Gbps network 10,000 ns 10 us
Read 4 KB randomly from SSD* 150,000 ns 150 us ~1GB/sec SSD
Read 1 MB sequentially from memory 250,000 ns 250 us
Round trip within same datacenter 500,000 ns 500 us
Read 1 MB sequentially from SSD* 1,000,000 ns 1,000 us 1 ms ~1GB/sec SSD, 4X memory
Disk seek 10,000,000 ns 10,000 us 10 ms 20x datacenter roundtrip
Read 1 MB sequentially from 1 Gbps 10,000,000 ns 10,000 us 10 ms 40x memory, 10X SSD
Read 1 MB sequentially from disk 30,000,000 ns 30,000 us 30 ms 120x memory, 30X SSD
Send packet CA->Netherlands->CA 150,000,000 ns 150,000 us 150 ms

基于上述数字的指标:

  • 从磁盘以 30 MB/s 的速度顺序读取
  • 以 100 MB/s 从 1 Gbps 的以太网顺序读取
  • 从 SSD 以 1 GB/s 的速度读取
  • 以 4 GB/s 的速度从主存读取
  • 每秒能绕地球 6-7 圈
  • 数据中心内每秒有 2,000 次往返

traffic estimates


memory estimates


bandwidth estimates


storage estimates


系统设计核心概念

📌 𝐒𝐲𝐬𝐭𝐞𝐦 𝐃𝐞𝐬𝐢𝐠𝐧 𝐊𝐞𝐲 𝐂𝐨𝐧𝐜𝐞𝐩𝐭𝐬

  • Scalability: lnkd.in/gpge_z76
  • CAP Theorem: lnkd.in/g3hmVamx
  • ACID Transactions: lnkd.in/gMe2JqaF
  • Consistent Hashing: lnkd.in/gd3eAQKA
  • Rate Limiting: lnkd.in/gWsTDR3m
  • API Design: lnkd.in/ghYzrr8q
  • Strong vs Eventual Consistency: lnkd.in/gJ-uXQXZ
  • Synchronous vs. asynchronous communications: lnkd.in/g4EqcckR
  • REST vs RPC: lnkd.in/gN__zcAB
  • Batch Processing vs Stream Processing: lnkd.in/gaAnP_fT
  • Fault Tolerance: lnkd.in/dVJ6n3wA
  • Consensus Algorithms: lnkd.in/ggc3tFbr
  • Gossip Protocol: lnkd.in/gfPMtrJZ
  • Service Discovery: lnkd.in/gjnrYkyF
  • Disaster Recovery: lnkd.in/g8rnr3V3
  • Distributed Tracing: lnkd.in/d6r5RdXG
  • Top 15 Tradeoffs: lnkd.in/gnM8QC-z

🛠️ 𝐒𝐲𝐬𝐭𝐞𝐦 𝐃𝐞𝐬𝐢𝐠𝐧 𝐁𝐮𝐢𝐥𝐝𝐢𝐧𝐠 𝐁𝐥𝐨𝐜𝐤𝐬

  • Horizontal vs Vertical Scaling: lnkd.in/gAH2e9du
  • Databases: lnkd.in/gti8gjpz
  • Content Delivery Network (CDN): lnkd.in/gjJrEJeH
  • Domain Name System (DNS): lnkd.in/gkMcZW8V
  • Caching: lnkd.in/gC9piQbJ
  • Distributed Caching: lnkd.in/g7WKydNg
  • Load Balancing: lnkd.in/gQaa8sXK
  • SQL vs NoSQL: lnkd.in/g3WC_yxn
  • Database Indexes: lnkd.in/dGnZiNmM
  • HeartBeats: lnkd.in/gfb9-hpN
  • Circuit Breaker: lnkd.in/gCxyFzKm
  • Idempotency: lnkd.in/gPm6EtKJ
  • Database Scaling: lnkd.in/gAXpSyWQ
  • Data Replication: lnkd.in/gVAJxTpS
  • Data Redundancy: lnkd.in/gNN7TF7n
  • Database Sharding: lnkd.in/gRHb-67m
  • Failover: lnkd.in/dihZ-cEG
  • Proxy Server: lnkd.in/gi8KnKS6
  • Message Queues: lnkd.in/gTzY6uk8
  • WebSockets: lnkd.in/g76Gv2KQ
  • Bloom Filters: lnkd.in/dt4QbSUz
  • API Gateway: lnkd.in/gnsJGJaM
  • Distributed Locking: lnkd.in/gRxNJwWE
  • Checksum: lnkd.in/gCTa4DrS

🖇️ 𝐒𝐲𝐬𝐭𝐞𝐦 𝐃𝐞𝐬𝐢𝐠𝐧 𝐀𝐫𝐜𝐡𝐢𝐭𝐞𝐜𝐭𝐮𝐫𝐚𝐥 𝐏𝐚𝐭𝐭𝐞𝐫𝐧𝐬

  • Client-Server Architecture: lnkd.in/dAARQYzq
  • Microservices Architecture: lnkd.in/gFXUrz_T
  • Serverless Architecture: lnkd.in/gQNAXKkb
  • Event-Driven Architecture: lnkd.in/dp8CPvey
  • Peer-to-Peer (P2P) Architecture: lnkd.in/di32HDu3

整体架构设计

软件架构模式(patterns)

Application Landscape Patterns

Application structure Patterns

User Interface Patterns

  • MVC
  • MVP

参考阅读:

架构 EA+4A

什么是架构 EA+4A

业务架构

应用架构

技术架构

数据架构

架构设计原则

扩展阅读

微服务架构

单体服务、微服务、Service Mesh


什么是服务治理

  • 单体服务(Monolithic Services):单体服务是指将整个应用程序作为一个单一的、紧密耦合的单元进行开发、部署和运行的架构模式。在单体服务中,应用程序的各个功能模块通常运行在同一个进程中,并共享相同的数据库和资源。单体服务的优点是开发简单、部署方便,但随着业务规模的增长,单体服务可能变得庞大且难以维护。

  • 微服务(Microservices):微服务是一种将应用程序拆分为一组小型、独立部署的服务的架构模式。每个微服务都专注于单个业务功能,并通过轻量级的通信机制(如RESTful API或消息队列)进行相互通信。微服务的优点是灵活性高、可扩展性好,每个微服务可以独立开发、测试、部署和扩展。然而,微服务架构也带来了分布式系统的复杂性和管理的挑战。

  • Service Mesh:Service Mesh是一种用于解决微服务架构中服务间通信和治理问题的基础设施层。它通过在服务之间插入一个专用的代理(称为Sidecar)来提供服务间的通信、安全性、可观察性和弹性的功能。Service Mesh可以提供流量管理、负载均衡、故障恢复、安全认证、监控和追踪等功能,而不需要在每个微服务中显式实现这些功能。常见的Service Mesh实现包括Istio、Linkerd和Consul Connect等。

微服务


gRPC 概述

与此讨论相关的话题是 微服务,可以被描述为一系列可以独立部署的小型的,模块化服务。每个服务运行在一个独立的线程中,通过明确定义的轻量级机制通讯,共同实现业务目标。1例如,Pinterest 可能有这些微服务: 用户资料、关注者、Feed 流、搜索、照片上传等。

服务发现

ZooKeeper

  • ZooKeeper是一个开源的分布式协调服务,最初由雅虎开发并后来成为Apache软件基金会的顶级项目。
  • ZooKeeper提供了一个分布式的、高可用的、强一致性的数据存储服务。它的设计目标是为构建分布式系统提供可靠的协调机制。
  • ZooKeeper使用基于ZAB(ZooKeeper Atomic Broadcast)协议的一致性算法来保证数据的一致性和可靠性。
  • ZooKeeper提供了一个类似于文件系统的层次化命名空间(称为ZNode),可以存储和管理数据,并支持对数据的读写操作。
  • ZooKeeper还提供了一些特性,如临时节点、顺序节点和观察者机制,用于实现分布式锁、选举算法和事件通知等。

etcd

  • etcd是一个开源的分布式键值存储系统,由CoreOS开发并后来成为Cloud Native Computing Foundation(CNCF)的项目之一。

  • etcd被设计为一个高可用、可靠的分布式存储系统,用于存储和管理关键的配置数据和元数据。

  • etcd使用Raft一致性算法来保证数据的一致性和可靠性,Raft是一种强一致性的分布式共识算法。

  • etcd提供了一个简单的键值存储接口,可以存储和检索键值对数据,并支持对数据的原子更新操作。

  • etcd还提供了一些高级特性,如目录结构、事务操作和观察者机制,用于构建复杂的分布式系统和应用

  • Etcd

  • Zookeeper

  • Consul

  • grpc

Service Mesh


service Mesh 是怎么工作的

远程过程调用协议(RPC)


Source: Crack the system design interview

在 RPC 中,客户端会去调用另一个地址空间(通常是一个远程服务器)里的方法。调用代码看起来就像是调用的是一个本地方法,客户端和服务器交互的具体过程被抽象。远程调用相对于本地调用一般较慢而且可靠性更差,因此区分两者是有帮助的。热门的 RPC 框架包括 ProtobufThriftAvro

RPC 是一个“请求-响应”协议:

  • 客户端程序 ── 调用客户端存根程序。就像调用本地方法一样,参数会被压入栈中。
  • 客户端 stub 程序 ── 将请求过程的 id 和参数打包进请求信息中。
  • 客户端通信模块 ── 将信息从客户端发送至服务端。
  • 服务端通信模块 ── 将接受的包传给服务端存根程序。
  • 服务端 stub 程序 ── 将结果解包,依据过程 id 调用服务端方法并将参数传递过去。

接入层设计:域名/代理/负载均衡

域名系统

Amazon Route 53域名系统


Amazon Route 53 工作原理

### 域名解析的过程


来源:DNS 安全介绍

域名系统是把 www.example.com 等域名转换成 IP 地址。域名系统是分层次的,一些 DNS 服务器位于顶层。当查询(域名) IP 时,路由或 ISP 提供连接 DNS 服务器的信息。较底层的 DNS 服务器缓存映射,它可能会因为 DNS 传播延时而失效。DNS 结果可以缓存在浏览器或操作系统中一段时间,时间长短取决于存活时间 TTL

  • A 记录(地址) ─ 指定域名对应的 IP 地址记录。
  • CNAME(规范) ─ 一个域名映射到另一个域名或 CNAME 记录( example.com 指向 www.example.com )或映射到一个 A 记录。
  • NS 记录(域名服务) ─ 指定解析域名或子域名的 DNS 服务器。
  • MX 记录(邮件交换) ─ 指定接收信息的邮件服务.

域名管理服务

常用命令

  • nslookup
  • dig

来源及延伸阅读

代理+负载均衡器

正向forward proxy

反向reverse proxy


负载均衡器和反向代理



来源:可扩展的系统设计模式

负载均衡器将传入的请求分发到应用服务器和数据库等计算资源。无论哪种情况,负载均衡器将从计算资源来的响应返回给恰当的客户端。负载均衡器的效用在于:

  • 防止请求进入不好的服务器
  • 防止资源过载
  • 帮助消除单一的故障点
  • SSL 终结 ─ 解密传入的请求并加密服务器响应,这样的话后端服务器就不必再执行这些潜在高消耗运算了。
  • 不需要再每台服务器上安装 X.509 证书
  • Session 留存 ─ 如果 Web 应用程序不追踪会话,发出 cookie 并将特定客户端的请求路由到同一实例。
  • 通常会设置采用工作─备用双工作 模式的多个负载均衡器,以免发生故障。

负载均衡器能基于多种方式来路由流量:

四层负载均衡

四层负载均衡根据监看传输层的信息来决定如何分发请求。通常,这会涉及来源,目标 IP 地址和请求头中的端口,但不包括数据包(报文)内容。四层负载均衡执行网络地址转换(NAT)来向上游服务器转发网络数据包。

七层负载均衡器

七层负载均衡器根据监控应用层来决定怎样分发请求。这会涉及请求头的内容,消息和 cookie。七层负载均衡器终结网络流量,读取消息,做出负载均衡判定,然后传送给特定服务器。比如,一个七层负载均衡器能直接将视频流量连接到托管视频的服务器,同时将更敏感的用户账单流量引导到安全性更强的服务器。

以损失灵活性为代价,四层负载均衡比七层负载均衡花费更少时间和计算资源,虽然这对现代商用硬件的性能影响甚微。

水平扩展

负载均衡器还能帮助水平扩展,提高性能和可用性。使用商业硬件的性价比更高,并且比在单台硬件上垂直扩展更贵的硬件具有更高的可用性。相比招聘特定企业系统人才,招聘商业硬件方面的人才更加容易。

缺陷:水平扩展

  • 水平扩展引入了复杂度并涉及服务器复制
  • 服务器应该是无状态的:它们也不该包含像 session 或资料图片等与用户关联的数据。
  • session 可以集中存储在数据库或持久化缓存(Redis、Memcached)的数据存储区中。
  • 缓存和数据库等下游服务器需要随着上游服务器进行扩展,以处理更多的并发连接。

缺陷:负载均衡器

  • 如果没有足够的资源配置或配置错误,负载均衡器会变成一个性能瓶颈。
  • 引入负载均衡器以帮助消除单点故障但导致了额外的复杂性。
  • 单个负载均衡器会导致单点故障,但配置多个负载均衡器会进一步增加复杂性。

反向代理(web 服务器)


资料来源:维基百科

反向代理是一种可以集中地调用内部服务,并提供统一接口给公共客户的 web 服务器。来自客户端的请求先被反向代理服务器转发到可响应请求的服务器,然后代理再把服务器的响应结果返回给客户端。

带来的好处包括:

  • 增加安全性 - 隐藏后端服务器的信息,屏蔽黑名单中的 IP,限制每个客户端的连接数。
  • 提高可扩展性和灵活性 - 客户端只能看到反向代理服务器的 IP,这使你可以增减服务器或者修改它们的配置。
  • 本地终结 SSL 会话 - 解密传入请求,加密服务器响应,这样后端服务器就不必完成这些潜在的高成本的操作。免除了在每个服务器上安装 X.509 证书的需要
  • 压缩 - 压缩服务器响应
  • 缓存 - 直接返回命中的缓存结果
  • 静态内容 - 直接提供静态内容
    • HTML/CSS/JS
    • 图片
    • 视频
    • 等等

负载均衡器与反向代理

  • 当你有多个服务器时,部署负载均衡器非常有用。通常,负载均衡器将流量路由给一组功能相同的服务器上。
  • 即使只有一台 web 服务器或者应用服务器时,反向代理也有用,可以参考上一节介绍的好处。
  • NGINX 和 HAProxy 等解决方案可以同时支持第七层反向代理和负载均衡。

不利之处:反向代理

  • 引入反向代理会增加系统的复杂度。
  • 单独一个反向代理服务器仍可能发生单点故障,配置多台反向代理服务器(如故障转移)会进一步增加复杂度。

来源及延伸阅读

应用层web网关设计


百亿规模API网关服务Shepherd的设计与实现

将 Web 服务层与应用层(也被称作平台层)分离,可以独立缩放和配置这两层。添加新的 API 只需要添加应用服务器,而不必添加额外的 web 服务器。用于完成基础的:

API 设计规范和管理

API 架构风格

  • RESTful API
  • GraphQL
  • RPC
  • SOA

RESTful API

  • 路径名称避免动词

    1
    2
    3
    4
    5
    路径名称避免动词
    # Good
    curl -X GET /orders
    # Bad
    curl -X GET /getOrders
  • GET 获取指定 URI 的资源信息

    1
    2
    3
    4
    5
    6
    7
    # 代表获取当前系统的所有订单信息
    curl -X GET /orders

    curl -X GET /users/{user_id}/orders

    # 代表获取指定订单编号为订单详情信息
    curl -X GET /orders/{order_id}
  • POST 通过指定的 URI 创建资源

    1
    2
    curl -X POST /orders \
    -d '{"name": "awesome", region: "A"}' \
  • PUT 创建或全量替换指定 URI 上的资源

    1
    2
    curl -X PUT http://httpbin.org/orders/1 \
    -d '{"name": "new awesome", region: "B"}' \
  • PATCH 执行一个资源的部分更新

    1
    2
    3
    4
    5
    # 代表将 id 为 1 的 order 中的 region 字段进行更改,其他数据保持不变
    curl -X PATCH /orders/{order_id} \
    -d '{name: "nameB"}' \
    curl -X order/{order_id}/name (用来重命名)
    curl -X /order/{order_id}/status(用来更改用户状态)
  • DELETE 通过指定的 URI 移除资源

    1
    2
    # 代表将id的 order 删除
    curl -X DELETE /orders/{order_id}

其它规则:
规则1:应使用连字符( - )来提高URI的可读性
规则2:不得在URI中使用下划线(_)
规则3:URI路径中全都使用小写字母

API 错误码设计规范

  1. 不论请求成功或失败,始终返回 200 http status code,在 HTTP Body 中包含用户账号没有找到的错误信息:
1
2
3
4
5
6
7
8
9
10
11
12
13
如: Facebook API 的错误 Code 设计,始终返回 200 http status code:
{
"error": {
"message": "Syntax error \"Field picture specified more than once. This is only possible before version 2.1\" at character 23: id,name,picture,picture",
"type": "OAuthException",
"code": 2500,
"fbtrace_id": "xxxxxxxxxxx"
}
}

缺点:
对于每一次请求,我们都要去解析 HTTP Body,从中解析出错误码和错误信息

  1. 返回 http 404 Not Found 错误码,并在 Body 中返回简单的错误信息:
1
2
3
4
5
如: Twitter API 的错误设计
根据错误类型,返回合适的 HTTP Code,并在 Body 中返回错误信息和自定义业务 Code

HTTP/1.1 400 Bad Request
{"errors":[{"code":215,"message":"Bad Authentication data."}]}
  1. 返回 http 404 Not Found 错误码,并在 Body 中返回详细的错误信息:
1
2
3
4
5
6
7
如: 微软 Bing API 的错误设计,会根据错误类型,返回合适的 HTTP Code,并在 Body 中返回详尽的错误信息
HTTP/1.1 400
{
"code": 100101,
"message": "Database error",
"reference": "https://github.com/xx/tree/master/docs/guide/faq/xxxx"
}
  1. 业务 Code 码设计
  • 纯数字表示
  • 不同部位代表不同的服务
  • 不同的模块(品类)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
如: 错误代码说明:100101
10: 服务
01: 某个服务下的某个模块
01: 模块下的错误码序号,每个模块可以注册 100 个错误
建议 http status code 不要太多:

200 - 表示请求成功执行
400 - 表示客户端出问题
500 - 表示服务端出问题

如果觉得这 3 个错误码不够用,可以加如下 3 个错误码:
401 - 表示认证失败
403 - 表示授权失败
404 - 表示资源找不到,这里的资源可以是 URL 或者 RESTful 资源

接口幂等性设计

幂等性的重要性

  • 提高可靠性:在网络不稳定的情况下,客户端可能会重试请求。幂等性确保重复请求不会导致意外的副作用。
  • 简化客户端代码:客户端不需要担心重复请求的副作用,从而简化了错误处理逻辑。
  • 改善用户体验:确保用户操作的可预测性,避免因重复提交表单等操作导致的错误或重复数据。

怎么实现幂等性

  • 幂等键(Idempotency Key: 由客户端生成一个唯一标识请求的ID,并在请求头中包含此ID。服务器端会检查此ID是否已处理过,如果是,则返回之前的响应。
  • 幂等令牌(Idempotency Token):在需要创建资源的请求中,通过幂等令牌保证幂等性。服务器端生成并验证令牌,确保同一令牌只能创建一个资源

中间件和存储

如何选择存储组件

各中间件详细专题

框架和引擎

工作流引擎与任务编排

goflow

  • 技术原理:goflow 是基于数据流编程模型的工作流引擎,采用有向无环图(DAG)描述任务节点之间的依赖关系。每个节点可独立执行,节点间通过通道(channel)传递数据,充分利用 Go 的并发特性(goroutine、channel)实现高效的任务编排和并行处理。
  • 使用场景:适用于需要复杂任务编排、数据流转、并发处理的场景,如数据处理流水线、ETL、自动化运维流程等。适合对任务依赖关系有明确建模需求的系统。

参考:goflow GitHub

go-workflow

  • 技术原理:go-workflow 是一款轻量级的工作流引擎,支持任务的定义、调度、状态管理和持久化。通过状态机管理任务流转,支持任务重试、超时、失败回调等机制。底层可集成多种存储后端(如 MySQL、Redis)以保证任务可靠性和可恢复性。
  • 使用场景:适合需要可靠任务编排、长流程管理、任务状态追踪的业务,如订单处理、审批流、异步任务调度等。适用于对任务持久化和容错有较高要求的系统。

参考:go-workflow GitHub


规则引擎与风控、资损、校验

gengine

  • 技术原理:gengine 是一款高性能、轻量级的 Go 规则引擎,采用自定义 DSL(领域特定语言)编写规则,支持动态加载和热更新。底层通过 AST(抽象语法树)解析和高效的规则匹配算法,实现复杂业务规则的快速执行。支持规则优先级、条件判断、动作执行等特性。
  • 使用场景:广泛应用于风控决策、资损防控、数据校验、营销策略等需要灵活配置和频繁变更规则的场景。适合对规则实时性和可维护性有较高要求的金融、电商等行业。

参考:gengine GitHub


脚本执行引擎与低代码平台

tengo

  • 技术原理:tengo 是一个用 Go 实现的嵌入式脚本语言,语法类似 JavaScript。支持类型安全、垃圾回收、闭包、模块化等特性。可将业务逻辑以脚本形式动态加载和执行,便于扩展和热更新。适合嵌入到 Go 应用中作为业务自定义脚本引擎。
  • 使用场景:适用于低代码平台、动态业务规则、用户自定义脚本、插件系统等场景。可用于实现灵活的业务扩展和快速迭代。

参考:tengo GitHub

anko

  • 技术原理:anko 是一个简洁的 Go 脚本解释器,支持基本的脚本语法、变量、函数、流程控制等。可与 Go 代码无缝集成,支持在运行时动态执行脚本。适合对脚本功能要求不高但需要快速集成的场景。
  • 使用场景:适合低代码平台、配置驱动、动态表达式计算、简单自动化脚本等。适用于对性能和安全性有一定要求但业务逻辑相对简单的系统。

参考:anko GitHub

好用的规范和工具

规范:

  • Go编码规范
  • api 设计规范
  • git 使用规范

工具:

云原生和服务部署CI/CD

大数据存储和计算

系统稳定性建设

详见 互联网系统稳定性建设:方法论与实践

系统设计实践

参考:

速查导航

阅读时间: 50 分钟 | 难度: ⭐⭐⭐⭐⭐ | 面试频率: 极高

核心考点速查:


一、Docker 核心原理(面试必问)

1.1 Docker vs 虚拟机

对比表格

维度 Docker 容器 虚拟机(VM)
启动速度 秒级 分钟级
资源占用 MB 级别 GB 级别
隔离级别 进程级别(共享内核) 操作系统级别(独立内核)
性能 接近原生(无虚拟化开销) 有虚拟化开销(5-10%)
镜像大小 MB 级别 GB 级别
安全性 较弱(共享内核) 较强(完全隔离)
适用场景 微服务、CI/CD、快速部署 需要完全隔离、不同OS

架构对比图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
容器架构                      虚拟机架构
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ App A │ │ App B │ │ App A │ │ App B │
├─────────┤ ├─────────┤ ├─────────┤ ├─────────┤
│ Bins │ │ Bins │ │ Bins │ │ Bins │
├─────────┼─┴─────────┤ ├─────────┤ ├─────────┤
│ Docker Engine │ │ Guest │ │ Guest │
├─────────────────────┤ │ OS │ │ OS │
│ Host OS │ ├─────────┼─┴─────────┤
├─────────────────────┤ │ Hypervisor │
│ Infrastructure │ ├─────────────────────┤
└─────────────────────┘ │ Host OS │
├─────────────────────┤
│ Infrastructure │
└─────────────────────┘

面试标准答案(30 秒):

Docker 容器通过 namespace 实现资源隔离,通过 cgroup 实现资源限制,共享宿主机内核,启动快、资源占用少。VM 通过 Hypervisor 虚拟化完整操作系统,隔离性更强但开销大。

1.2 Docker 镜像分层机制

分层原理

  • 每个 Docker 镜像由多层只读层(Read-Only Layer)叠加而成
  • 运行容器时,在最上层加一层可写层(Container Layer)
  • 多个容器可以共享相同的镜像层,节省磁盘空间

示例

1
2
3
4
5
FROM ubuntu:20.04        # 第 1 层:基础镜像
RUN apt-get update # 第 2 层:更新软件源
RUN apt-get install -y nginx # 第 3 层:安装 nginx
COPY app.conf /etc/nginx/ # 第 4 层:复制配置文件
CMD ["nginx", "-g", "daemon off;"] # 第 5 层:启动命令

分层存储结构

1
2
3
4
5
6
7
8
9
10
11
12
┌─────────────────┐ ← Container Layer(可写层)
├─────────────────┤
│ CMD nginx │ ← Layer 5(只读)
├─────────────────┤
│ COPY app.conf │ ← Layer 4(只读)
├─────────────────┤
│ RUN apt install │ ← Layer 3(只读)
├─────────────────┤
│ RUN apt update │ ← Layer 2(只读)
├─────────────────┤
│ FROM ubuntu │ ← Layer 1(只读)
└─────────────────┘

面试追问:为什么要分层?

  1. 共享存储:多个镜像可以共享相同的层(如 ubuntu:20.04 基础层)
  2. 快速构建:修改某一层时,只需重新构建该层及以上层
  3. 快速分发:只传输变化的层,不需要传输整个镜像

1.3 Namespace 隔离

Docker 使用 Linux Namespace 实现资源隔离

Namespace 隔离内容 示例
PID 进程 ID 容器内 PID=1 的进程在宿主机上是另一个 PID
NET 网络栈(网卡、IP、端口) 容器有独立的 IP 地址和端口
IPC 进程间通信(消息队列、信号量) 容器间无法直接 IPC
MNT 文件系统挂载点 容器看到的是独立的文件系统
UTS 主机名和域名 容器有独立的 hostname
USER 用户和用户组 容器内 root 可以映射为宿主机普通用户

验证 Namespace

1
2
3
4
5
6
7
8
9
# 查看容器的 namespace
docker inspect <container-id> | grep -i pid
# 输出:PID: 12345

# 查看宿主机上的 namespace
sudo ls -l /proc/12345/ns/
# 输出:
# net -> 'net:[4026532123]'
# pid -> 'pid:[4026532124]'

1.4 Cgroup 资源限制

Cgroup(Control Groups)用于限制容器的资源使用

资源 限制方式 Docker 参数
CPU 限制 CPU 使用率 --cpus=2(2 核)或 --cpu-shares=512(相对权重)
内存 限制内存使用量 --memory=1g(1GB)
磁盘 IO 限制磁盘读写速度 --device-read-bps--device-write-bps
网络 限制网络带宽 需借助 tc(traffic control)工具

示例

1
2
# 限制容器使用 1 核 CPU 和 512MB 内存
docker run -d --cpus=1 --memory=512m nginx

面试追问:如果容器内存超限会发生什么?

  • 容器会被 OOM Killer 杀死
  • Pod 状态变为 OOMKilled
  • Kubernetes 会根据 restartPolicy 决定是否重启

二、Kubernetes 核心概念(5 分钟速记)

2.1 核心组件

组件 职责 面试话术
Master 集群控制平面 “大脑”,负责调度和管理
Node 工作节点,运行容器 “手脚”,执行实际任务
Pod 最小部署单元,包含 1 个或多个容器 “豆荚”,容器的外壳
Deployment 管理 Pod 的副本数和更新策略 自动扩缩容、滚动更新
Service 为 Pod 提供固定 IP 和负载均衡 Pod 的”门牌号”
Ingress HTTP/HTTPS 路由,七层负载均衡 集群的”网关”

2.2 Master 组件

组件 职责
API Server 集群的统一入口,所有操作都通过 API Server
Scheduler 负责 Pod 调度,选择合适的 Node
Controller Manager 管理各种控制器(Deployment、ReplicaSet 等)
etcd 分布式 KV 存储,保存集群状态

2.3 Node 组件

组件 职责
kubelet Node 上的代理,负责 Pod 生命周期管理
kube-proxy 负责 Service 的网络代理和负载均衡
Container Runtime 容器运行时(Docker、containerd、CRI-O)

2.4 Pod 生命周期

Pod 状态

状态 含义
Pending 等待调度或拉取镜像
Running 至少一个容器正在运行
Succeeded 所有容器成功终止(Job/CronJob 场景)
Failed 至少一个容器失败退出
Unknown 无法获取 Pod 状态(通常是 Node 失联)

Pod 生命周期钩子

钩子 触发时机
postStart 容器启动后立即执行
preStop 容器终止前执行(用于优雅关闭)

三、K8s 网络原理(深度解析)

3.1 Linux 虚拟网络基础

Veth Pair + Bridge 实现跨 namespace 通信

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──────────────┐      ┌──────────────┐
│ Container A │ │ Container B │
│ (ns1) │ │ (ns2) │
│ 192.168.1.2 │ │ 192.168.1.3 │
└──────┬───────┘ └──────┬───────┘
│ veth-ns1 │ veth-ns2
│ │
└──────┬──────────────┘

┌────┴────┐
│ docker0 │ (Bridge)
│ 192.168.1.1
└─────────┘

实战练习

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 创建两个 network namespace
sudo ip netns add ns1
sudo ip netns add ns2

# 创建 veth pair
sudo ip link add veth-ns1 type veth peer name veth-ns1-br
sudo ip link set veth-ns1 netns ns1

# 创建网桥
sudo brctl addbr docker0
sudo brctl addif docker0 veth-ns1-br

# 配置 IP
sudo ip -n ns1 addr add 192.168.1.2/24 dev veth-ns1
sudo ip link set veth-ns1-br up
sudo ip -n ns1 link set veth-ns1 up

# 测试连通性
sudo ip netns exec ns1 ping 192.168.1.1

3.2 Docker 网络

docker0 网桥

  • Docker 默认创建 docker0 网桥(类似交换机)
  • 每个容器通过 veth pair 连接到 docker0
  • 容器间通过 docker0 通信

查看 Docker 网络

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查看网桥
brctl show
# 输出:
# bridge name interfaces
# docker0 veth91e1730
# vethc858a6a

# 查看容器路由
docker exec <container-id> route -n
# 输出:
# Destination Gateway Genmask
# 0.0.0.0 172.17.0.1 0.0.0.0 # 默认网关
# 172.17.0.0 0.0.0.0 255.255.0.0 # 本地网络

# 查看 iptables NAT 规则
sudo iptables -t nat -S | grep docker
# 输出:
# -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE

3.3 Pod 网络

Pause 容器

  • 每个 Pod 有一个 Pause 容器(registry.k8s.io/pause
  • Pause 容器负责创建 Network Namespace
  • Pod 内其他容器共享 Pause 的网络栈(IP、端口、路由)

验证 Pause 容器

1
2
3
4
5
6
7
8
9
10
# 查看 Pod 对应的容器
docker ps | grep etcd
# 输出:
# 8fd1337b0bf2 etcd:latest # 业务容器
# 1202ef34af2b registry.k8s.io/pause:3.9 # Pause 容器

# 查看业务容器的网络模式
docker inspect 8fd1337b0bf2 | grep NetworkMode
# 输出:
# "NetworkMode": "container:1202ef34af2b..."

CNI(Container Network Interface)

  • Kubernetes 通过 CNI 插件实现 Pod 网络
  • 常见 CNI:Flannel、Calico、Weave、Cilium
  • CNI 插件位置:/opt/cni/bin/
1
2
3
4
5
ls -l /opt/cni/bin/
# 输出:
# -rwxr-xr-x bridge
# -rwxr-xr-x host-local
# -rwxr-xr-x loopback

3.4 Service 网络

Service 的作用

  1. 服务发现:为 Pod 提供固定的 ClusterIP
  2. 负载均衡:将请求分发到多个 Pod

Service 实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌──────────────────────────────────────────────┐
│ API Server │
│ (Service IP: 10.96.0.1 分配并写入 etcd) │
└───────────────┬──────────────────────────────┘

┌───────┴───────┐
│ etcd │
│ (Service + │
│ Endpoints) │
└───────┬───────┘
│ watch
┌───────┴───────┐
│ kube-proxy │
│ (维护 iptables│
│ 或 ipvs 规则)│
└───────┬───────┘

┌───────┴───────┐
│ iptables/ipvs│
│ (Service IP → │
│ Pod IP 转发) │
└───────────────┘

Service 类型

类型 说明 使用场景
ClusterIP 集群内部访问(默认) 微服务间调用
NodePort 通过 Node IP + 端口访问 测试、临时对外暴露
LoadBalancer 云厂商提供的负载均衡器 生产环境对外暴露
ExternalName CNAME 记录,映射到外部服务 访问外部数据库

kube-proxy 工作模式

模式 原理 性能
userspace 用户态代理(已废弃)
iptables 内核态转发(默认) 中等
ipvs LVS 负载均衡 高(推荐)

3.5 Ingress 网络

Ingress 的作用

  • 七层(HTTP/HTTPS)负载均衡
  • 基于域名和 URL 路径路由

Ingress 架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
┌────────────────────────────────────────┐
│ External Traffic (example.com/app1) │
└─────────────┬──────────────────────────┘

┌───────┴───────┐
│ Ingress │ (Nginx/Traefik)
│ Controller │
└───────┬───────┘
│ 根据 Ingress 规则转发
┌───────┴───────┐
│ Service A │ (ClusterIP: 10.96.0.1)
└───────┬───────┘

┌───────┴───────┬───────────┐
│ Pod A1 │ Pod A2 │
└───────────────┴───────────┘

Ingress 路由规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
spec:
rules:
- host: example.com
http:
paths:
- path: /app1
pathType: Prefix
backend:
service:
name: service-a
port:
number: 80
- path: /app2
pathType: Prefix
backend:
service:
name: service-b
port:
number: 80

四、Pod 生命周期与故障排查

4.1 Pod 状态速查表

状态 原因 排查方法
Pending 资源不足 / 镜像拉取中 / 调度失败 kubectl describe pod <pod-name>
ImagePullBackOff 镜像不存在 / 镜像仓库认证失败 检查镜像名称、Secret
CrashLoopBackOff 容器启动后立即崩溃 查看日志 kubectl logs <pod-name>
OOMKilled 内存超限 增加 resources.limits.memory
Error 容器异常退出 查看退出码 kubectl describe pod

4.2 常见故障排查

故障1:ImagePullBackOff

现象

1
2
3
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
myapp-5d4b7c8f9-xyz 0/1 ImagePullBackOff 0 2m

原因

  1. 镜像名称错误(拼写错误、标签不存在)
  2. 私有镜像仓库认证失败
  3. 网络问题(无法连接镜像仓库)

排查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 查看详细错误
kubectl describe pod myapp-5d4b7c8f9-xyz
# 输出:
# Failed to pull image "myapp:v1.0": rpc error: code = Unknown desc = Error response from daemon: pull access denied

# 解决方案1:修正镜像名称
kubectl edit deployment myapp

# 解决方案2:创建 imagePullSecrets
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com \
--docker-username=user \
--docker-password=pass \
--docker-email=user@example.com

故障2:CrashLoopBackOff

现象

1
2
3
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
myapp-5d4b7c8f9-xyz 0/1 CrashLoopBackOff 5 5m

原因

  1. 应用启动失败(配置错误、依赖服务不可用)
  2. 健康检查失败
  3. 启动命令错误

排查

1
2
3
4
5
6
7
8
9
10
# 查看日志
kubectl logs myapp-5d4b7c8f9-xyz
# 输出:
# Error: Cannot connect to database at mysql:3306

# 查看上一次容器的日志(如果容器已重启)
kubectl logs myapp-5d4b7c8f9-xyz --previous

# 解决方案:修复配置
kubectl edit deployment myapp

故障3:OOMKilled

现象

1
2
3
4
5
$ kubectl describe pod myapp-5d4b7c8f9-xyz
...
State: Terminated
Reason: OOMKilled
Exit Code: 137

原因

  • 容器内存使用超过 resources.limits.memory

排查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 查看 Pod 资源限制
kubectl get pod myapp-5d4b7c8f9-xyz -o yaml | grep -A 10 resources
# 输出:
# resources:
# limits:
# memory: 128Mi # 内存限制过小
# requests:
# memory: 64Mi

# 解决方案:增加内存限制
kubectl edit deployment myapp
# 修改为:
# resources:
# limits:
# memory: 512Mi

4.3 健康检查

Liveness Probe(存活探针)

  • 检测容器是否存活
  • 失败则重启容器

Readiness Probe(就绪探针)

  • 检测容器是否准备好接收流量
  • 失败则从 Service Endpoints 中移除

Startup Probe(启动探针)

  • 用于慢启动容器
  • 启动阶段禁用 Liveness Probe

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: app
image: myapp:v1.0
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 30 # 启动后等待 30 秒
periodSeconds: 10 # 每 10 秒检查一次
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5

五、实用 YAML 配置模板

5.1 Deployment(无状态应用)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
labels:
app: nginx
spec:
replicas: 3 # 副本数
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.21
ports:
- containerPort: 80
resources:
requests:
cpu: 100m # 请求 0.1 核
memory: 128Mi
limits:
cpu: 500m # 最多使用 0.5 核
memory: 512Mi
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 5
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 5
periodSeconds: 3

5.2 Service(ClusterIP)

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
type: ClusterIP # 集群内部访问
selector:
app: nginx
ports:
- protocol: TCP
port: 80 # Service 端口
targetPort: 80 # Pod 端口

5.3 Service(NodePort)

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: nginx-nodeport
spec:
type: NodePort
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 30080 # Node 上的端口(30000-32767)

5.4 Ingress

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: example-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: example.com
http:
paths:
- path: /app1
pathType: Prefix
backend:
service:
name: service-a
port:
number: 80
- path: /app2
pathType: Prefix
backend:
service:
name: service-b
port:
number: 80
tls:
- hosts:
- example.com
secretName: example-tls

5.5 ConfigMap(配置文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
database.url: "mysql://db:3306/mydb"
log.level: "info"
---
# 使用 ConfigMap
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: app
image: myapp:v1.0
envFrom:
- configMapRef:
name: app-config # 所有 key-value 都作为环境变量

5.6 Secret(敏感信息)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
apiVersion: v1
kind: Secret
metadata:
name: db-secret
type: Opaque
data:
username: YWRtaW4= # base64("admin")
password: cGFzc3dvcmQ= # base64("password")
---
# 使用 Secret
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: app
image: myapp:v1.0
env:
- name: DB_USER
valueFrom:
secretKeyRef:
name: db-secret
key: username
- name: DB_PASS
valueFrom:
secretKeyRef:
name: db-secret
key: password

六、监控与告警

6.1 核心指标

维度 指标 含义
Node CPU 使用率 节点 CPU 使用情况
内存使用率 节点内存使用情况
磁盘使用率 节点磁盘使用情况
Pod CPU 使用率 Pod CPU 使用情况
内存使用率 Pod 内存使用情况
重启次数 Pod 重启次数(CrashLoopBackOff)
容器 OOMKilled 次数 内存超限次数
退出码 容器退出码(0 正常,非 0 异常)

6.2 常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查看 Node 资源使用情况
kubectl top nodes

# 查看 Pod 资源使用情况
kubectl top pods

# 查看 Pod 详细信息
kubectl describe pod <pod-name>

# 查看 Pod 日志
kubectl logs <pod-name>
kubectl logs <pod-name> --previous # 上一次容器的日志

# 进入 Pod 调试
kubectl exec -it <pod-name> -- /bin/bash

# 查看 Pod 事件
kubectl get events --sort-by='.metadata.creationTimestamp'

七、面试高频 20 题

1. Docker 和虚拟机有什么区别?

标准答案
Docker 容器通过 namespace 实现资源隔离,通过 cgroup 实现资源限制,共享宿主机内核,启动快(秒级)、资源占用少(MB 级)。VM 通过 Hypervisor 虚拟化完整操作系统,隔离性更强但开销大(分钟级启动、GB 级资源占用)。

追问应对

  • 为什么容器启动快? 不需要启动完整 OS,只需启动进程
  • 容器安全性如何保证? namespace 隔离 + seccomp/AppArmor 限制系统调用 + 最小化镜像

2. Docker 镜像为什么要分层?

标准答案

  1. 共享存储:多个镜像可以共享相同的层(如 ubuntu:20.04 基础层)
  2. 快速构建:修改某一层时,只需重新构建该层及以上层
  3. 快速分发:只传输变化的层,不需要传输整个镜像

3. Kubernetes 的核心组件有哪些?

标准答案

  • Master:API Server、Scheduler、Controller Manager、etcd
  • Node:kubelet、kube-proxy、Container Runtime

追问应对

  • API Server 的作用? 集群的统一入口,所有操作都通过 API Server
  • etcd 的作用? 分布式 KV 存储,保存集群状态

4. Pod 是什么?为什么需要 Pod?

标准答案
Pod 是 Kubernetes 的最小部署单元,包含 1 个或多个容器。Pod 内容器共享网络(同一个 IP)和存储(Volume)。

为什么需要 Pod?

  • 解决”紧密耦合”容器的部署问题(如 Web + Sidecar)
  • 简化调度(Pod 作为整体调度,而不是单个容器)

5. Deployment、ReplicaSet、Pod 的关系?

标准答案

  • Deployment 管理 ReplicaSet,负责滚动更新和回滚
  • ReplicaSet 管理 Pod,保证 Pod 副本数
  • Pod 是实际运行的容器
1
2
3
4
5
Deployment(管理更新)

ReplicaSet(管理副本数)

Pod(运行容器)

6. Service 是如何实现负载均衡的?

标准答案

  1. API Server 分配 ClusterIP,写入 etcd
  2. kube-proxy watch Service 和 Endpoints 变化
  3. kube-proxy 维护 iptables/ipvs 规则,实现 Service IP → Pod IP 转发
  4. 默认使用轮询(Round-Robin)负载均衡

追问应对

  • iptables vs ipvs? ipvs 性能更高(基于 LVS),支持更多负载均衡算法

7. Ingress 和 Service 有什么区别?

标准答案

  • Service:四层(TCP/UDP)负载均衡,基于 IP + 端口
  • Ingress:七层(HTTP/HTTPS)负载均衡,基于域名 + URL 路径

追问应对

  • 如何选择? 内部服务间调用用 Service,对外暴露 HTTP 服务用 Ingress

8. Pod 的 Pending 状态可能是什么原因?

标准答案

  1. 资源不足:Node 没有足够的 CPU/内存
  2. 镜像拉取中:正在拉取镜像
  3. 调度失败:没有满足 nodeSelector/亲和性的 Node

排查方法kubectl describe pod <pod-name>

9. Pod 的 CrashLoopBackOff 如何排查?

标准答案

  1. 查看日志:kubectl logs <pod-name>
  2. 查看上一次日志:kubectl logs <pod-name> --previous
  3. 查看 Pod 详情:kubectl describe pod <pod-name>

常见原因

  • 应用启动失败(配置错误、依赖服务不可用)
  • 健康检查失败

10. OOMKilled 是什么?如何解决?

标准答案
OOMKilled 表示容器内存使用超过 resources.limits.memory,被 OOM Killer 杀死。

解决方案

  1. 增加内存限制:resources.limits.memory: 512Mi
  2. 优化应用内存使用(如减少缓存、优化算法)

11. 什么是 namespace?有什么作用?

标准答案
namespace 是 Kubernetes 的资源隔离机制,用于将集群资源划分为多个虚拟集群。

作用

  • 多租户隔离(不同团队/项目)
  • 资源配额(ResourceQuota)
  • 访问控制(RBAC)

12. ConfigMap 和 Secret 有什么区别?

标准答案

  • ConfigMap:存储非敏感配置(如数据库地址、日志级别)
  • Secret:存储敏感信息(如密码、密钥),base64 编码

追问应对

  • Secret 是否安全? base64 不是加密,只是编码。生产环境建议使用外部密钥管理(如 Vault)

13. Liveness Probe 和 Readiness Probe 的区别?

标准答案

  • Liveness Probe:检测容器是否存活,失败则重启容器
  • Readiness Probe:检测容器是否准备好接收流量,失败则从 Service Endpoints 中移除

14. Kubernetes 如何实现滚动更新?

标准答案

  1. Deployment 创建新的 ReplicaSet(新版本)
  2. 逐步增加新 ReplicaSet 的副本数,减少旧 ReplicaSet 的副本数
  3. 新 Pod 就绪后,旧 Pod 才会被删除

配置

  • maxSurge:滚动更新时最多超出的 Pod 数
  • maxUnavailable:滚动更新时最多不可用的 Pod 数

15. Kubernetes 如何实现自动扩缩容?

标准答案
使用 HPA(Horizontal Pod Autoscaler),基于 CPU/内存使用率或自定义指标自动调整 Pod 副本数。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: myapp-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: myapp
minReplicas: 2
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80 # CPU 使用率超过 80% 时扩容

16. StatefulSet 和 Deployment 有什么区别?

标准答案

  • Deployment:无状态应用,Pod 名称随机、无固定存储
  • StatefulSet:有状态应用,Pod 名称有序(如 mysql-0mysql-1)、有固定存储(PVC)

适用场景

  • Deployment:Web 服务、API 服务
  • StatefulSet:数据库、消息队列、Zookeeper

17. DaemonSet 是什么?有什么作用?

标准答案
DaemonSet 保证每个 Node 上运行一个 Pod。

适用场景

  • 日志采集(Fluentd、Filebeat)
  • 监控代理(Node Exporter)
  • 存储插件(Ceph、GlusterFS)

18. Kubernetes 如何实现服务发现?

标准答案

  1. 环境变量:Pod 启动时,Kubernetes 会注入 Service 的环境变量
  2. DNS:集群内置 DNS(CoreDNS),Service 名称解析为 ClusterIP

示例

  • Service 名称:mysql-service
  • DNS 域名:mysql-service.default.svc.cluster.local

19. Kubernetes 的存储机制是怎样的?

标准答案

  • Volume:Pod 级别存储,Pod 删除后数据丢失
  • PersistentVolume(PV):集群级别存储资源(如 NFS、Ceph)
  • PersistentVolumeClaim(PVC):Pod 对存储的请求,绑定到 PV

20. Kubernetes 的安全机制有哪些?

标准答案

  1. RBAC:基于角色的访问控制
  2. Network Policy:网络隔离,限制 Pod 间通信
  3. Pod Security Policy:限制 Pod 的权限(如禁止特权容器)
  4. Secret 加密:etcd 中 Secret 数据加密存储

常用命令速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Minikube
minikube start
minikube status
minikube ssh

# Pod 操作
kubectl get pods
kubectl describe pod <pod-name>
kubectl logs <pod-name>
kubectl exec -it <pod-name> -- /bin/bash

# Deployment 操作
kubectl create deployment nginx --image=nginx:1.21
kubectl scale deployment nginx --replicas=3
kubectl rollout status deployment nginx
kubectl rollout undo deployment nginx # 回滚

# Service 操作
kubectl expose deployment nginx --port=80 --target-port=80 --type=NodePort
kubectl get svc

# 查看资源使用情况
kubectl top nodes
kubectl top pods

# 调试
kubectl describe pod <pod-name>
kubectl get events --sort-by='.metadata.creationTimestamp'

参考资料

  1. Minikube 环境安装
  2. Kubectl 命令和集群体验
  3. Linux network namespace,veth,bridge 和 路由
  4. 从0到1搭建linux虚拟网络
  5. Docker 网络:模拟docker网络
  6. Docker 网络:从docker0开始
  7. Pod网络和pause容器
  8. 认识CNI插件
  9. 深度解读CNI:容器网络接口
  10. 官方文档:服务service
  11. 创建service之后,k8s会发生什么
  12. 探究k8s service iptables 路由规则
  13. 官方文档:在minikube中使用nginx ingress 控制配置ingress
  14. 官方文档:ingress
  15. Kubernetes 官方文档

速查导航

阅读时间: 45 分钟 | 难度: ⭐⭐⭐⭐ | 面试频率: 极高

核心考点速查:


一、Kafka 核心特性(面试必答)

1.1 三句话介绍 Kafka

标准回答(45 秒内说完):
Kafka 是分布式流式消息队列,具有高吞吐(百万 TPS)、低延迟(ms 级)、持久化的特点。采用发布-订阅模式,常用于异步解耦、削峰填谷、日志采集。

加分项:提到”顺序写磁盘 + 零拷贝 + Page Cache”性能三板斧。

1.2 使用场景(带真实案例)

场景 痛点 Kafka 方案 示例
订单处理 同步调用慢 异步解耦 下单 → MQ → 库存/物流并行处理
秒杀 瞬时流量打垮 DB 削峰填谷 请求入队 → 匀速消费
日志采集 海量日志 高吞吐 Filebeat → Kafka → ES
风控系统 实时流计算 流式数据源 Kafka → Flink/Storm → 实时告警
数据同步 异构系统集成 数据管道 MySQL Binlog → Kafka → 数仓

1.3 面试追问:为什么不用 Redis 做消息队列?

对比表格

维度 Kafka Redis
吞吐量 百万级 TPS 十万级
持久化 强(磁盘) 弱(RDB/AOF 有丢失风险)
消息回溯 支持(offset) 不支持
集群 原生支持 Cluster 模式复杂
消息大小 支持 MB 级 推荐 KB 级

结论:核心业务用 Kafka,轻量级任务可用 Redis List。


二、核心概念(5 分钟速记)

2.1 核心组件速查表

组件 面试关键点 记忆口诀
Broker 消息代理服务器,负责存储和转发 “银行柜台”,存取消息
Topic 消息主题/分类,类似数据库表 按业务分类
Partition Topic 物理分片,实现并行与扩展 分区内有序,全局无序
Offset 消费位点,每条消息的唯一编号 类似数组下标
Producer 消息生产者 往银行存钱
Consumer 消息消费者 从银行取钱
Consumer Group 消费者组,一个分区只能被组内一个消费者消费 多人共同分账单

2.2 副本机制(高可用核心)

面试必问点:

概念 解释 面试话术
Leader 负责读写的主副本 “所有读写请求都打在 Leader 上”
Follower 从 Leader 同步的备份副本 “只负责同步,不对外服务”
ISR In-Sync Replicas,同步副本集合 “能跟上 Leader 的副本才能入选”
AR Assigned Replicas,所有副本 AR = ISR + OSR
OSR Out-of-Sync Replicas,落后的副本 “掉队的副本会被踢出 ISR”

追问:Leader 挂了怎么办?

  • ISR 中选举新 Leader(保证数据不丢)
  • 如果 ISR 为空,是否允许从 OSR 选举?取决于 unclean.leader.election.enable(默认 false,不允许)

2.3 Partition 与顺序性

面试标准答案(30 秒):

  1. 同一 Partition 内严格有序(按 offset 递增)
  2. 不同 Partition 之间无序
  3. 需要全局有序? 设置 Partition = 1(牺牲并行度)
  4. 常见方案:按业务 key(如订单 ID)Hash 到同一分区

代码示例(Partition 策略):

1
2
3
4
5
6
7
// 自定义分区器:按订单 ID 保证同订单消息有序
import "hash/crc32"

func OrderPartitioner(key []byte, numPartitions int) int {
orderID := string(key)
return int(crc32.ChecksumIEEE([]byte(orderID))) % numPartitions
}

2.4 ZooKeeper vs KRaft

维度 ZooKeeper 模式(旧) KRaft 模式(新)
元数据存储 外部 ZK 集群 Kafka 内部 Raft 日志
Controller 选举 ZK 选举 Raft 协议选举
部署复杂度 高(需额外维护 ZK) 低(无外部依赖)
启动速度 慢(ZK session 超时)
生产可用 稳定(旧版默认) Kafka 3.3+ 推荐

面试加分项:新项目推荐 KRaft,减少外部依赖,简化运维。


三、数据流与架构(高频考点)

3.1 Producer → Broker → Consumer 完整流程

1
2
3
4
5
6
7
8
9
10
┌─────────────┐                ┌─────────────┐                ┌─────────────┐
│ Producer │──①发送消息───→│ Broker │──③消费拉取───→│ Consumer │
│ │ │ (Leader) │ │ │
└─────────────┘ └─────────────┘ └─────────────┘
│②同步

┌─────────────┐
│ Follower │
│ 副本集 │
└─────────────┘

详细步骤:

阶段 操作 关键配置
① Producer 发送 序列化 → 分区路由 → 批量打包 → 发送 acks, batch.size, linger.ms
② Broker 写入 追加到 log → Follower 拉取同步 → 返回 ack min.insync.replicas
③ Consumer 消费 Fetch 请求 → 反序列化 → 业务处理 → 提交 offset enable.auto.commit

3.2 Consumer Group 与 Rebalance

消费者组内的消费者共同消费一个 Topic,每个 Partition 只能被组内一个消费者消费。

Rebalance 触发条件:

  • 消费者加入或离开消费者组
  • 订阅的 Topic Partition 数量变化
  • 消费者心跳超时(session.timeout.ms

Rebalance 的影响:

  • 数据重复消费:未提交的 offset 导致消息重新投递
  • 消费暂停:Rebalance 期间所有消费者停止消费
  • 扩散效应:一个消费者退出可能触发整个 Group 的 Rebalance

减少不必要 Rebalance 的方法:

  • 合理设置 session.timeout.msheartbeat.interval.ms
  • 增大 max.poll.interval.ms,避免消费逻辑超时
  • 使用 Static Membership(group.instance.id)减少重启引起的 Rebalance

3.3 Controller 与协调

Kafka 集群中会选举出一个 Controller Broker,负责 Partition Leader 选举、副本管理、集群元数据变更等。

  • 早期依赖 ZooKeeper 存储 controller 与部分元数据
  • KRaft 模式下,元数据以 Raft 日志形式在 controller 节点间复制,去掉外部 ZK,部署与扩缩容更简单

客户端通过 Bootstrap Server 列表首次连接后,会拉取集群元数据(Topic、Partition Leader、ISR 等),后续生产与消费都尽量直连对应 Leader Broker,避免所有读写流量经过单点代理。


四、为什么 Kafka 这么快?(面试必问)

Kafka 虽然是基于磁盘的消息队列,但吞吐量可达百万 TPS,延迟低至 ms 级别。核心原因是以下三大优化:

4.1 顺序写磁盘

原理

  • Kafka 的消息追加到 log 文件末尾,是顺序写(Sequential Write)
  • 顺序写避免了磁盘寻道时间,性能接近内存写入(磁盘顺序写 > 内存随机写)

对比

  • 顺序写:600 MB/s(SSD)
  • 随机写:100 MB/s(SSD)

面试话术

“Kafka 将消息顺序追加到磁盘,避免随机 IO,磁盘顺序写性能甚至优于内存随机写。”

4.2 Page Cache(页缓存)

原理

  • Kafka 不使用 JVM 堆内存管理缓存,而是依赖操作系统的 Page Cache
  • 操作系统会自动将热点数据缓存到内存,读写操作直接命中缓存

优势

  • 减少 GC 压力(无大对象在堆内存)
  • 操作系统级别的缓存管理更高效
  • 重启 Kafka 后 Page Cache 依然存在(操作系统管理)

面试话术

“Kafka 依赖操作系统 Page Cache,避免 JVM GC,热数据读写基本都是内存操作。”

4.3 零拷贝(Zero Copy)

传统方式(4 次拷贝)

1
磁盘 → 内核缓冲区 → 用户空间 → Socket 缓冲区 → 网卡

零拷贝方式(sendfile 系统调用)

1
磁盘 → Page Cache → 网卡(DMA 直接传输)

优势

  • 减少 2 次 CPU 拷贝(内核 → 用户空间,用户空间 → Socket 缓冲区)
  • 减少 2 次上下文切换(用户态 ↔ 内核态)

Go 代码示例(模拟零拷贝)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Go 标准库的 io.Copy 内部会使用 sendfile(Linux)或 TransmitFile(Windows)
import (
"io"
"net"
"os"
)

func SendFileWithZeroCopy(conn net.Conn, filePath string) error {
file, err := os.Open(filePath)
if err != nil {
return err
}
defer file.Close()

// io.Copy 在 Linux 下会自动使用 sendfile 系统调用
_, err = io.Copy(conn, file)
return err
}

面试话术

“Kafka 使用 sendfile 系统调用,数据从磁盘通过 DMA 直接传输到网卡,减少 CPU 拷贝和上下文切换。”

4.4 批量读写与压缩

批量发送

  • Producer 会将多条消息打包成一个 batch 发送
  • 配置 batch.size(批量大小)和 linger.ms(等待时间)

批量压缩

  • Kafka 支持 gzipsnappylz4zstd 压缩
  • 压缩后网络传输量减少,提高吞吐量

面试话术

“Kafka 通过批量发送和压缩,减少网络 IO 次数,提高吞吐量。”

4.5 分区并行

原理

  • 一个 Topic 可以有多个 Partition
  • 多个 Partition 可以并行写入/读取,充分利用多核 CPU

面试话术

“Kafka 的 Partition 机制实现了水平扩展,多个 Partition 并行处理,提高吞吐量。”


五、消息不丢失全链路保障

5.1 可靠性语义

三种语义

语义 生产端配置 消费端配置 适用场景
At-most-once acks=0 自动提交 offset 日志采集(允许丢失)
At-least-once acks=all + 重试 手动提交 offset 订单处理(不允许丢失)
Exactly-once 幂等 + 事务 事务性消费 金融场景

5.2 生产端配置

配置项 推荐值 说明
acks all (-1) 等待所有 ISR 副本确认
retries ≥3 发送失败重试次数
max.in.flight.requests.per.connection 1 配合重试保证消息顺序
enable.idempotence true 开启幂等性,防止重复发送

5.3 Broker 端配置

配置项 推荐值 说明
min.insync.replicas 2 至少 2 个副本同步才允许写入
unclean.leader.election.enable false 禁止非 ISR 副本成为 Leader
default.replication.factor 3 默认副本数

5.4 消费端配置

  • 关闭自动提交:enable.auto.commit=false
  • 消费成功后手动提交 offset
  • 消费逻辑实现幂等(唯一键/状态机/版本号)

5.5 Exactly-once 实现

Exactly-once 的两个维度

  1. Broker 内部:Producer 幂等性 + 事务
  2. 端到端:消费逻辑幂等

Producer 幂等性配置

1
2
3
4
5
6
7
8
// Go kafka-go 示例
writer := &kafka.Writer{
Addr: kafka.TCP("localhost:9092"),
Topic: "orders",
RequiredAcks: kafka.RequireAll, // acks=all
Idempotent: true, // 开启幂等性
MaxAttempts: 3, // 重试 3 次
}

事务性写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Go Sarama 示例(kafka-go 不支持事务)
import "github.com/Shopify/sarama"

config := sarama.NewConfig()
config.Producer.Idempotent = true
config.Producer.RequiredAcks = sarama.WaitForAll
config.Producer.Return.Errors = true
config.Producer.Transaction.ID = "my-transaction-id" // 事务 ID

producer, _ := sarama.NewAsyncProducer(brokers, config)

// 开始事务
producer.BeginTxn()
producer.Input() <- &sarama.ProducerMessage{Topic: "orders", Value: sarama.StringEncoder("msg1")}
producer.Input() <- &sarama.ProducerMessage{Topic: "orders", Value: sarama.StringEncoder("msg2")}
producer.CommitTxn() // 提交事务

5.6 检查清单(落地排查)

  • 写进日志才算数:Producer 未收到成功 ack 前,业务层是否错误地当作「已发送成功」并更新状态?
  • ISR 是否退化:Broker 或副本故障后 ISR 可能暂时只剩 Leader,此时 acks=all 在语义上会退化为弱一致场景,需结合副本监控与告警。
  • 提交时机:手动提交 offset 是否在业务落库、调用下游成功之后执行?先提交再处理会导致宕机丢消息。
  • 重试与顺序:提高 max.in.flight.requests.per.connection 有利于吞吐,但与 Producer 重试组合时可能影响同分区内消息顺序,需按业务是否强依赖顺序做取舍。

六、Rebalance 机制及影响

6.1 什么是 Rebalance?

Rebalance 是 Kafka 消费者组内 Partition 重新分配的过程。

触发条件:

  1. 消费者加入或离开消费者组
  2. 订阅的 Topic Partition 数量变化
  3. 消费者心跳超时(session.timeout.ms

6.2 Rebalance 的影响

面试标准答案

  1. 数据重复消费:未提交的 offset 导致消息重新投递
  2. 消费暂停:Rebalance 期间所有消费者停止消费(Stop-the-world)
  3. 扩散效应:一个消费者退出可能触发整个 Group 的 Rebalance

6.3 如何减少 Rebalance?

配置优化

配置项 推荐值 说明
session.timeout.ms 30000(30 秒) 心跳超时时间(过短易误判)
heartbeat.interval.ms 3000(3 秒) 心跳间隔(建议为 session.timeout 的 1/3)
max.poll.interval.ms 300000(5 分钟) 两次 poll 之间的最大间隔
group.instance.id 设置静态成员 ID 避免重启时触发 Rebalance

代码优化

  • 消费逻辑异步化:消费时直接返回,启动异步线程处理
  • 避免长时间阻塞:确保业务逻辑在 max.poll.interval.ms 内完成

6.4 监控 Lag 情况

查看消费积压

1
kafka-consumer-groups.sh --describe --group <group-name> --bootstrap-server <broker>

关键指标

  • CURRENT-OFFSET:当前消费位点
  • LOG-END-OFFSET:Partition 最新 offset
  • LAG:积压消息数(LOG-END-OFFSET - CURRENT-OFFSET)

告警策略

  • 核心业务 Topic(如订单):LAG > 1000 告警
  • 日志类 Topic:LAG > 10000 告警

七、文件存储机制

7.1 存储结构

逻辑上:Topic 分为多个 Partition
物理上:每个 Partition 是一个目录,包含多个 Segment 文件

1
2
3
4
5
6
7
/kafka-logs/orders-0/
├── 00000000000000000000.index # 索引文件
├── 00000000000000000000.log # 数据文件
├── 00000000000000000000.timeindex # 时间索引
├── 00000000000000368769.index
├── 00000000000000368769.log
└── 00000000000000368769.timeindex

7.2 Segment 滚动策略

触发条件

  • 文件大小达到 log.segment.bytes(默认 1GB)
  • 时间达到 log.roll.ms(默认 7 天)

文件命名:文件名为该 Segment 起始 offset(如 00000000000000368769.log

7.3 索引机制

稀疏索引

  • .index 文件不是为每条消息建索引,而是按间隔记录(默认每 4KB 建一条索引)
  • 查找时先在索引中二分查找区间,再在 .log 中顺序扫描

示例:查找 offset=368800 的消息

  1. 通过文件名定位到 00000000000000368769.log
  2. .index 文件中二分查找,找到最接近的索引项(如 offset=368790, position=1024)
  3. .log 文件的 position=1024 开始顺序扫描,找到 offset=368800

7.4 HW 与 LEO

概念 解释 面试话术
LEO Log End Offset,副本本地 log 末尾的 offset “副本最新写到哪里”
HW High Watermark,ISR 中所有副本都复制到的 offset “消费者能读到哪里”

面试追问:为什么消费者只能读到 HW 之前的数据?

  • 保证消息已被多个副本确认,避免读到未充分复制、可能丢失的数据

八、性能调优实践

8.1 消费积压排查步骤

1. 确认 lag 情况

1
kafka-consumer-groups.sh --describe --group <group-name> --bootstrap-server <broker>

2. 定位原因

  • 消费逻辑慢:查看消费端 DB/网络/外部服务耗时
  • Partition 数不够:消费者数超过 Partition 数时多余的消费者空闲
  • 消费线程阻塞:检查是否有死锁或长时间 GC

3. 应急方案

  • 临时增加消费者实例(不超过 Partition 数)
  • 消费逻辑异步化:消费时直接返回,启动异步线程处理
  • 跳过非关键消息:重置 offset 到最新位置

8.2 生产者侧调优

批量发送

1
2
3
4
5
6
writer := &kafka.Writer{
Addr: kafka.TCP("localhost:9092"),
Topic: "orders",
BatchSize: 100, // 批量大小 100 条
BatchTimeout: 10 * time.Millisecond, // 最多等待 10ms
}

压缩

1
2
3
writer := &kafka.Writer{
Compression: kafka.Lz4, // 使用 lz4 压缩
}

分区策略

1
2
3
4
5
6
7
8
9
// 无顺序要求:轮询
writer := &kafka.Writer{
Balancer: &kafka.RoundRobin{},
}

// 需要顺序:按 key Hash
writer := &kafka.Writer{
Balancer: &kafka.Hash{},
}

8.3 Broker 与系统层优化

页缓存

  • Broker 依赖 OS page cache 做热读热写,机器内存应留足给文件系统缓存
  • JVM 堆过大可能与 page cache 争用,需按官方建议调优

磁盘与 IO

  • 数据目录尽量使用高性能 SSD
  • 避免与高 IO 的其他服务混用同一盘

8.4 Go 生产级别 Producer 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
package main

import (
"context"
"log"
"time"

"github.com/segmentio/kafka-go"
)

// NewProducer 创建生产者
func NewProducer(brokers []string, topic string) *kafka.Writer {
return &kafka.Writer{
Addr: kafka.TCP(brokers...),
Topic: topic,
Balancer: &kafka.Hash{}, // 按 key Hash 分区
RequiredAcks: kafka.RequireAll, // acks=all
MaxAttempts: 3, // 重试 3 次
BatchSize: 100, // 批量 100 条
BatchTimeout: 10 * time.Millisecond,
Compression: kafka.Lz4, // lz4 压缩
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
Idempotent: true, // 幂等性
AllowAutoTopicCreation: false, // 禁止自动创建 Topic
}
}

// ProduceMessage 发送消息(带重试和错误处理)
func ProduceMessage(w *kafka.Writer, key, value string) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

err := w.WriteMessages(ctx, kafka.Message{
Key: []byte(key),
Value: []byte(value),
Time: time.Now(), // 消息时间戳
})

if err != nil {
log.Printf("Failed to send message: key=%s, error=%v", key, err)
return err
}

log.Printf("Message sent successfully: key=%s", key)
return nil
}

func main() {
producer := NewProducer([]string{"localhost:9092"}, "orders")
defer producer.Close()

// 发送消息
for i := 0; i < 100; i++ {
key := fmt.Sprintf("order-%d", i)
value := fmt.Sprintf(`{"order_id":"%s","amount":100}`, key)
if err := ProduceMessage(producer, key, value); err != nil {
log.Printf("Error: %v", err)
}
}
}

8.5 Go 生产级别 Consumer 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package main

import (
"context"
"log"
"time"

"github.com/segmentio/kafka-go"
)

// NewConsumer 创建消费者
func NewConsumer(brokers []string, topic, groupID string) *kafka.Reader {
return kafka.NewReader(kafka.ReaderConfig{
Brokers: brokers,
GroupID: groupID,
Topic: topic,
MinBytes: 10e3, // 10KB
MaxBytes: 10e6, // 10MB
MaxWait: 500 * time.Millisecond,
CommitInterval: 0, // 手动提交
StartOffset: kafka.LastOffset, // 从最新位置开始
})
}

// processMessage 业务处理逻辑(需保证幂等性)
func processMessage(msg kafka.Message) error {
log.Printf("Received: partition=%d, offset=%d, key=%s, value=%s",
msg.Partition, msg.Offset, string(msg.Key), string(msg.Value))

// TODO: 业务逻辑(写 DB、调用下游服务等)
// 注意:必须实现幂等性!

return nil
}

// ConsumeLoop 消费循环
func ConsumeLoop(r *kafka.Reader) {
ctx := context.Background()

for {
// 拉取消息
msg, err := r.FetchMessage(ctx)
if err != nil {
log.Printf("Fetch error: %v", err)
time.Sleep(1 * time.Second)
continue
}

// 业务处理
if err := processMessage(msg); err != nil {
log.Printf("Process error: %v", err)
// 注意:根据业务决定是否重试或跳过
continue
}

// 处理成功后手动提交 offset
if err := r.CommitMessages(ctx, msg); err != nil {
log.Printf("Commit error: %v", err)
}
}
}

func main() {
consumer := NewConsumer([]string{"localhost:9092"}, "orders", "order-consumer-group")
defer consumer.Close()

log.Println("Start consuming...")
ConsumeLoop(consumer)
}

8.6 常用配置参数总结

Producer 配置

1
2
3
4
5
6
7
8
c.Producer.MaxMessageBytes = 1000000  // 1MB
c.Producer.RequiredAcks = WaitForLocal // acks=1(默认)
c.Producer.Timeout = 10 * time.Second
c.Producer.Partitioner = NewHashPartitioner
c.Producer.Retry.Max = 3
c.Producer.Retry.Backoff = 100 * time.Millisecond
c.Producer.Return.Errors = true
c.Producer.CompressionLevel = CompressionLevelDefault

Consumer 配置

1
2
3
4
5
6
7
8
9
10
c.Consumer.Fetch.Min = 1
c.Consumer.Fetch.Default = 1024 * 1024 // 1MB
c.Consumer.Retry.Backoff = 2 * time.Second
c.Consumer.MaxWaitTime = 500 * time.Millisecond
c.Consumer.MaxProcessingTime = 100 * time.Millisecond
c.Consumer.Return.Errors = false
c.Consumer.Offsets.AutoCommit.Enable = true // 自动提交
c.Consumer.Offsets.AutoCommit.Interval = 1 * time.Second
c.Consumer.Offsets.Initial = OffsetNewest // 从最新位置开始
c.Consumer.Offsets.Retry.Max = 3

九、生产环境踩坑实录

9.1 案例1:消费者 Rebalance 导致大量重复消费

现象

  • 消费者频繁 Rebalance,导致同一批消息被重复消费 3-5 次
  • 数据库出现大量重复订单记录

原因分析

  • 消费逻辑耗时长(调用外部 API 3-5 秒),超过 max.poll.interval.ms(默认 5 分钟)
  • 消费者被 Coordinator 认为”假死”,触发 Rebalance

解决方案

  1. max.poll.interval.ms 调整为 10 分钟
  2. 消费逻辑异步化:消费时直接返回,启动 goroutine 处理
  3. 业务层实现幂等性:使用订单 ID 作为唯一键

9.2 案例2:Partition 数量不足导致扩容无效

现象

  • 消费积压严重(LAG > 10 万),增加消费者实例后 LAG 依然不降

原因分析

  • Topic 只有 3 个 Partition,但启动了 10 个消费者实例
  • 只有 3 个消费者在工作,其他 7 个空闲

解决方案

  1. 增加 Partition 数量到 10(注意:Partition 只能增加不能减少
  2. 重启消费者,触发 Rebalance 重新分配 Partition

9.3 案例3:acks=1 导致数据丢失

现象

  • 生产环境发现部分订单消息丢失(约 0.1%)

原因分析

  • Producer 配置 acks=1,只等待 Leader 确认
  • Leader 写入成功后,Follower 尚未同步,Leader 宕机
  • 新选举的 Leader 没有这条消息

解决方案

  1. 修改 Producer 配置:acks=all
  2. 修改 Broker 配置:min.insync.replicas=2(至少 2 个副本)
  3. 开启 Producer 幂等性:enable.idempotence=true

9.4 案例4:Page Cache 不足导致性能下降

现象

  • Broker 机器内存 32GB,JVM 堆设置为 24GB
  • Kafka 读写性能低,大量磁盘 IO

原因分析

  • JVM 堆占用过多内存,导致 OS Page Cache 不足(只剩 8GB)
  • 热数据无法完全缓存在内存,大量磁盘读

解决方案

  1. 将 JVM 堆调整为 6GB(Kafka 官方推荐 6-8GB)
  2. 留出 26GB 给 OS Page Cache
  3. 性能提升 5 倍

9.5 案例5:未设置 retention 导致磁盘爆满

现象

  • Broker 磁盘使用率达到 100%,无法写入新消息

原因分析

  • Topic 未设置 retention.ms(保留时间),消息永久保留
  • 日志类 Topic 每天产生 100GB 数据,积累半年后磁盘爆满

解决方案

  1. 设置 Topic 级别的 retention.ms=604800000(7 天)
  2. 手动删除旧数据:kafka-delete-records.sh
  3. 增加磁盘容量

十、面试高频 20 题

1. 介绍一下 Kafka?

标准答案(30 秒):
Kafka 是分布式流式消息队列,具有高吞吐(百万 TPS)、低延迟(ms 级)、持久化的特点。采用发布-订阅模式,常用于异步解耦、削峰填谷、日志采集。核心组件包括 Producer、Broker、Consumer、Topic、Partition。

追问应对

  • 为什么快? 顺序写磁盘 + Page Cache + 零拷贝
  • 如何保证高可用? 副本机制(ISR)+ Leader 选举

2. Kafka 如何保证消息不丢失?

标准答案
分三个环节保障:

  1. Produceracks=all + 重试 + 幂等性
  2. Brokermin.insync.replicas=2 + 禁止非 ISR 选举
  3. Consumer:手动提交 offset + 业务幂等

追问应对

  • acks=all 就一定不丢吗? 不一定,如果 ISR 只剩一个 Leader,依然可能丢
  • 如何实现 Exactly-once? Producer 幂等性 + 事务 + Consumer 幂等

3. Kafka 如何保证消息顺序?

标准答案

  1. 同一 Partition 内严格有序
  2. 不同 Partition 之间无序
  3. 全局有序方案:设置 Partition=1(牺牲并行度)
  4. 常见方案:按业务 key Hash 到同一分区

追问应对

  • 如何保证同一订单的消息有序? 使用订单 ID 作为 message key

4. Kafka 为什么这么快?

标准答案(3 点必答):

  1. 顺序写磁盘:避免随机 IO,性能接近内存写入
  2. Page Cache:依赖 OS 缓存,避免 JVM GC
  3. 零拷贝:sendfile 系统调用,减少 CPU 拷贝和上下文切换

追问应对

  • 零拷贝具体原理? 磁盘 → Page Cache → 网卡(DMA),避免内核态 ↔ 用户态拷贝

5. 什么是 ISR?

标准答案
ISR(In-Sync Replicas)是同步副本集合,包含与 Leader 保持同步的所有副本。

追问应对

  • 副本如何被踢出 ISR? 同步落后超过 replica.lag.time.max.ms(默认 10 秒)
  • ISR 为空怎么办? 取决于 unclean.leader.election.enable(默认 false,不允许从 OSR 选举)

6. 什么是 Rebalance?如何避免?

标准答案
Rebalance 是消费者组内 Partition 重新分配的过程。触发条件包括消费者加入/离开、Partition 数量变化、心跳超时。

如何避免

  1. 合理设置 session.timeout.msmax.poll.interval.ms
  2. 消费逻辑异步化,避免长时间阻塞
  3. 使用 Static Membership(group.instance.id

追问应对

  • Rebalance 的影响? 消费暂停 + 重复消费 + 扩散效应

7. Kafka 的存储结构是怎样的?

标准答案

  • 逻辑上:Topic → Partition → Message
  • 物理上:Partition → Segment(.log + .index + .timeindex)

追问应对

  • 如何查找某个 offset 的消息? 通过文件名定位 Segment → 在 .index 中二分查找 → 在 .log 中顺序扫描

8. HW 和 LEO 是什么?

标准答案

  • LEO:Log End Offset,副本本地 log 末尾的 offset
  • HW:High Watermark,ISR 中所有副本都复制到的 offset

追问应对

  • 为什么消费者只能读到 HW 之前的数据? 保证消息已被多个副本确认,避免读到未复制、可能丢失的数据

9. Kafka 如何实现高可用?

标准答案

  1. 副本机制:每个 Partition 有多个副本(Leader + Follower)
  2. ISR 机制:只有 ISR 中的副本才能参与选举
  3. Controller:负责 Leader 选举和元数据管理

追问应对

  • Controller 挂了怎么办? 从其他 Broker 中重新选举 Controller

10. Kafka 和 RabbitMQ 有什么区别?

对比表格

维度 Kafka RabbitMQ
吞吐量 百万级 TPS 十万级
延迟 ms 级 us 级(更低)
消息顺序 分区内有序 队列内有序
消息回溯 支持(offset) 不支持
适用场景 大数据、日志、流式计算 实时任务、RPC、微服务

11. Kafka 消费者如何实现负载均衡?

标准答案
通过 Consumer Group 实现:

  • 同一 Consumer Group 内的消费者共同消费一个 Topic
  • 每个 Partition 只能被组内一个消费者消费
  • 多个消费者并行消费不同 Partition,实现负载均衡

12. Kafka 如何处理消费积压?

标准答案

  1. 确认原因:消费逻辑慢 / Partition 数不够 / 消费者阻塞
  2. 临时方案:增加消费者实例(不超过 Partition 数)/ 消费逻辑异步化
  3. 长期方案:优化消费逻辑 / 增加 Partition 数 / 调整 max.poll.records

13. Kafka 的 offset 存储在哪里?

标准答案

  • 旧版本(0.9 之前):存储在 ZooKeeper
  • 新版本(0.9 之后):存储在 Kafka 内部 Topic(__consumer_offsets

追问应对

  • 为什么从 ZK 迁移到 Kafka? 减少 ZK 压力,提高性能

14. Kafka 如何保证幂等性?

标准答案

  1. Producer 幂等性:开启 enable.idempotence=true,Kafka 会为每条消息分配唯一 ID(PID + Sequence Number)
  2. Consumer 幂等性:业务层实现(唯一键 / 状态机 / 版本号)

15. Kafka 的分区策略有哪些?

标准答案

  1. 轮询(Round-Robin):依次分配到不同 Partition
  2. Hash:按 message key 的 Hash 值分配
  3. 自定义:实现 Partitioner 接口

16. Kafka 的压缩算法有哪些?

标准答案
支持 gzipsnappylz4zstd 四种。

推荐

  • 高吞吐lz4(压缩比中等,速度最快)
  • 高压缩比zstd(压缩比最高,速度较慢)

17. Kafka 的 acks 参数有哪些值?

标准答案

  • acks=0:不等待确认,性能最高,可能丢失
  • acks=1:等待 Leader 确认,可能丢失(Leader 挂掉)
  • acks=all(-1):等待所有 ISR 确认,最可靠

18. Kafka 的事务是如何实现的?

标准答案
Kafka 事务基于 事务协调器(Transaction Coordinator) 实现,支持:

  1. 原子性写入:多个消息要么全部成功,要么全部失败
  2. 跨分区事务:可以跨多个 Topic/Partition

追问应对

  • 如何开启事务? 设置 transactional.id + 调用 beginTransaction() / commitTransaction()

19. Kafka 如何实现消息去重?

标准答案

  1. Producer 幂等性:开启 enable.idempotence=true
  2. 事务:使用事务性发送
  3. Consumer 去重:业务层实现(Redis 缓存消息 ID / 数据库唯一键)

20. Kafka 的监控指标有哪些?

标准答案

  1. 吞吐量MessagesInPerSec / BytesInPerSec
  2. 延迟RequestLatencyAvg
  3. 消费积压ConsumerLag
  4. 副本同步UnderReplicatedPartitions(未完全同步的 Partition 数)
  5. ISR 变化IsrShrinksPerSec / IsrExpandsPerSec

常用命令

创建 Topic

1
kafka-topics.sh --create --topic orders --replication-factor 3 --partitions 10 --bootstrap-server localhost:9092

查看 Topic 详情

1
kafka-topics.sh --describe --topic orders --bootstrap-server localhost:9092

查看消费组情况

1
kafka-consumer-groups.sh --describe --group order-group --bootstrap-server localhost:9092

重置消费 offset

1
2
3
4
5
# 重置到最新位置
kafka-consumer-groups.sh --group order-group --bootstrap-server localhost:9092 --reset-offsets --all-topics --to-latest --execute

# 重置到指定时间
kafka-consumer-groups.sh --group order-group --bootstrap-server localhost:9092 --reset-offsets --all-topics --to-datetime 2024-03-10T00:00:00.000 --execute

生产消息(测试)

1
kafka-console-producer.sh --topic orders --bootstrap-server localhost:9092

消费消息(测试)

1
kafka-console-consumer.sh --topic orders --from-beginning --bootstrap-server localhost:9092

参考资料

  1. Kafka Consumer Rebalance 机制与影响(知乎)
  2. Kafka 为什么吞吐量大、速度快?(CSDN)
  3. Kafka 数据可靠性深度解读 / ISR 与副本(CSDN)
  4. Shopify Sarama 客户端配置参考(config.go)
  5. Kafka 选举机制(掘金)
  6. 简单理解 Kafka 的消息可靠性策略(腾讯云)
  7. Bootstrap server vs zookeeper in kafka?(StackOverflow)
  8. Kafka 如何保证顺序消费(CSDN)
  9. Kafka 官方文档
  10. Kafka-go GitHub

速查导航

阅读时间: 40 分钟 | 难度: ⭐⭐⭐⭐ | 面试频率: 高

核心考点速查:


基本使用

创建index,setting和mapping

1
curl -XPUT -H'Content-Type: application/json'  host/index_name?pretty=true -d@index_mapping.json 
es index example
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
 {
"settings": {
"index": {
"number_of_shards": "5",
"number_of_replicas": "2"
},
"analysis": {
"filter": {
"t2sconvert": {
"convert_type": "t2s",
"type": "stconvert"
}
},
"analyzer": {
"traditional_chinese_analyzer": {
"filter": "t2sconvert",
"type": "custom",
"tokenizer": "ik_smart"
}
},
"normalizer": {
"lowercase": {
"type": "custom",
"filter": [
"lowercase"
]
}
}
}
},
"mappings": {
"_doc": {
"properties": {
"key_type": {
"type": "integer"
},
"country": {
"type": "keyword"
},
"language_code": {
"type": "keyword"
},
"level": {
"type": "integer"
},
"code": {
"type": "keyword"
},
"is_available": {
"type": "boolean"
},
"is_popular": {
"type": "boolean"
},
"pop_rank": {
"type": "integer"
},
"name": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
}
}
}
},
"display_name": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
}
}
}
},
"address": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
}
}
}
},
"city_code": {
"type": "keyword"
},
"city_name": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
}
}
},
"updated": {
"type": "date",
"format": "strict_date_optional_time||epoch_millis"
}
}
}
}
}
}

查看index,_cat 基本信息

1
curl -XGET 'host/_cat/indices/*hotel_basic_info_v2_live*(支持正则表达式)?v=true&pretty=true'

查看索引mapping信息

1
curl -XGET 'host/index_name/_mapping?pretty=true'

查看索引的setting信息

1
curl -XGET 'host/index_name/_settings?pretty=true'

通过doc id 正向查询

1
curl -XGET  'host/index/_doc/doc_id?pretty=true'

query,search,倒排查询

1
2
curl -XPOST -H'Content-Type: application/json' 'host/index_name/_search?pretty=true' -d '{
"query":{}}'

update

1
2
3
4
5
curl -XPOST  -H'Content-Type: application/json' 'host/index/_doc/doc_id/_update' -d '{
"doc": {
"price": "6500000001"
}
}'

聚合count查询

1
2
3
4
5
6
7
curl -XPOST -H'Content-Type: application/json' 'host/index_name/_count' -d '{
"query": {
"term": {
"city_name.value_in_english.keyword": "Jakarta"
}
}
}'

增加字段

1
2
3
4
5
6
7
8
curl -XPOST -H'Content-Type: application/json' 'host/index_name/_doc/_mapping' -d '{
"properties": {
"facility_codes": {
"type":"keyword"
}
}
}'

analyzer

在 Elastic Search 中,分词器起到了非常重要的作用,在定义文档结构、录入和更新文档、查询文档的时候都会用到它。例如:

1
2
3
4
5
6
7
8
9
10
武汉市长江大桥欢迎您

默认分词器:
[武, 汉, 市, 长, 江, 大, 桥, 欢, 迎, 您]

普通分词器:
[武汉, 市, 武汉市, 长江, 大桥,长江大桥, 欢迎, 您, 欢迎您]

二哈分词器:
[武汉, 市长, 江大桥, 欢迎, 您]

normalizer

alias

1
2
3
4
5
6
7
POST /_aliases
{
"actions": [
{"remove": {"index": "l1", "alias": "a1"}},
{"add": {"index": "l1", "alias": "a2"}}
]
}
1
curl -XPUT  host/index_nane/_alias/index_alias_name

query DSL

es query dsl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
{
"query": {
"function_score": {
"functions": [
{
"filter": {
"term": {
"key_type": 1
}
},
"weight": 3
},
{
"filter": {
"term": {
"key_type": 2
}
},
"weight": 2
}
],
"min_score": 0,
"query": {
"dis_max": {
"queries": [
{
"function_score": {
"functions": [
{
"filter": {
"term": {
"search_key.filed1.keyword": "querywords"
}
},
"weight": 100
},
{
"filter": {
"term": {
"search_key.filed2.keyword": "querywords"
}
},
"weight": 100
}
],
"score_mode": "max"
}
},
{
"dis_max": {
"queries": [
{
"match_phrase": {
"search_key.filed1": {
"boost": 50,
"query": "querywords"
}
}
},
{
"match_phrase": {
"search_key.field2": {
"boost": 50,
"query": "querywords"
}
}
}
]
}
},
{
"dis_max": {
"queries": [
{
"prefix": {
"search_key.filed1.keyword": {
"boost": 30,
"value": "querywords"
}
}
},
{
"prefix": {
"search_key.field2.keyword": {
"boost": 30,
"value": "querywords"
}
}
}
]
}
},
{
"dis_max": {
"queries": [
{
"match": {
"search_key.field1": {
"boost": 10,
"fuzziness": "auto:6,20",
"minimum_should_match": "3>75%",
"query": "querywords"
}
}
},
{
"match": {
"search_key.field2": {
"boost": 10,
"fuzziness": "auto:6,20",
"minimum_should_match": "3>75%",
"query": "querywords"
}
}
}
]
}
}
]
}
}
}
},
"size": 30,
"sort": [
{
"_score": {
"order": "desc"
}
},
{
"key_type": {
"order": "asc"
}
},
{
"others": {
"order": "desc"
}
}
]
}

原理

基本概念

  • 节点:分布系统都有的master节点和普通节点。类似于kafka集群都会存在的一种节点
  • master节点:用于管理索引(创建索引、删除索引)、分配分片,维护元数据
  • 协调节点:ES的特殊性,需要由一个节点汇总多个分片的query结果。节点是否担任协调节点可通过配置文件配置。例如某个节点只想做协调节点:node.master=false,node.data=false
  • ES的读写流程主要是协调节点,主分片节点、副分片节点间的相互协调。
  • ES的读取分为GET和Search两种操作。GET根据文档id从正排索引中获取内容;Search不指定id,根据关键字从倒排索引中获取内容。

写单个文档的流程

  1. 客户端向集群中的某个节点发送写请求,该节点就作为本次请求的协调节点
  2. 协调节点使用文档ID来确定文档属于某个分片,再通过集群状态中的内容路由表信息获知该分片的主分片位置,将请求转发到主分片所在节点;
  3. 主分片节点上的主分片执行写操作。如果写入成功,则它将请求并行转发到副分片所在的节点,等待副分片写入成功。所有副分片写入成功后,主分片节点向协调节点报告成功,协调节点向客户端报告成功。

读取单个文档的流程

  1. 客户端向集群中的某个节点发送读取请求,该节点就作为本次请求的协调节点;
  2. 协调节点使用文档ID来确定文档属于某个分片,再通过集群状态中的内容路由表信息获知该分片的副本信息,此时它可以把请求转发到有副分片的任意节点读取数据。
  3. 协调节点会将客户端请求轮询发送到集群的所有副本来实现负载均衡。
  4. 收到读请求的节点将文档返回给协调节点,协调节点将文档返回给客户端

Search流程

ES的Search操作分为两个阶段:query then fetch。需要两阶段完成搜索的原因是:在查询时不知道文档位于哪个分片,因此索引的所有分片都要参与搜索,然后协调节点将结果合并,在根据文档ID获取文档内容。

Query查询阶段

  1. 客户端向集群中的某个节点发送Search请求,该节点就作为本次请求的协调节点;
  2. 协调节点将查询请求转发到索引的每个主分片或者副分片中;
  3. 每个分片在本地执行查询,并使用本地的Term/Document Frequency信息进行打分,添加结果到大小为from+size的本地有序优先队列中;
  4. 每个分片返回各自优先队列中所有文档的ID和排序值给协调节点,协调节点合并这些值到自己的优先队列中,产生一个全局排序后的列表。

Fetch拉取阶段
query节点知道了要获取哪些信息,但是没有具体的数据,fetch阶段要去拉取具体的数据。相当于执行多次上面的GET流程

  1. 协调节点向相关的节点发送GET请求;
  2. 分片所在节点向协调节点返回数据;
  3. 协调阶段等待所有的文档被取得,然后返回给客户端。

倒排索引深度解析(面试必问)

什么是倒排索引?

标准答案(30 秒)

倒排索引是搜索引擎的核心数据结构,类似书籍的”索引”。传统数据库是”文档 → 关键词”的映射(正排),倒排索引是”关键词 → 文档列表”的映射,可以快速定位包含指定关键词的所有文档。

对比

索引类型 映射关系 查询方式 示例
正排索引 文档 ID → 内容 根据 ID 查内容 MySQL 主键索引
倒排索引 关键词 → 文档 ID 列表 根据关键词查文档 Elasticsearch

倒排索引结构

示例文档

1
2
3
{"id": 1, "content": "Elasticsearch is a search engine"}
{"id": 2, "content": "Lucene is a search library"}
{"id": 3, "content": "Elasticsearch is built on Lucene"}

倒排索引表

Term(关键词) Document IDs(文档列表) Frequency(词频)
elasticsearch [1, 3] 2
search [1, 2] 2
engine [1] 1
lucene [2, 3] 2
library [2] 1
built [3] 1

**查询 “elasticsearch search”**:

  1. 查倒排索引表:elasticsearch → [1,3],search → [1,2]
  2. 求交集:[1,3] ∩ [1,2] = [1]
  3. 返回文档 ID=1

Analyzer 工作流程

Analyzer 三大组件

组件 作用 示例
Character Filter 字符预处理(去特殊字符) <html> → 空
Tokenizer 分词 Elasticsearch is → [Elasticsearch, is]
Token Filter 词项后处理(小写、停用词) [Elasticsearch, is] → [elasticsearch]

示例

1
2
3
4
5
6
7
8
9
10
11
// 输入
"Elasticsearch is a SEARCH Engine!!!"

// Character Filter: 去掉特殊字符
"Elasticsearch is a SEARCH Engine"

// Tokenizer: 分词
["Elasticsearch", "is", "a", "SEARCH", "Engine"]

// Token Filter: 小写 + 去停用词(is, a)
["elasticsearch", "search", "engine"]

常用 Analyzer

Analyzer 适用场景
standard 英文(按空格分词 + 小写)
ik_smart 中文(粗粒度分词)
ik_max_word 中文(细粒度分词)

评分算法(BM25)

面试追问:Elasticsearch 如何对搜索结果排序?

Elasticsearch 使用 BM25 算法(Best Matching 25)计算相关性得分。

核心公式

1
score(D, Q) = Σ IDF(qi) × TF(qi, D)
  • TF(Term Frequency):词频,词在文档中出现次数
  • IDF(Inverse Document Frequency):逆文档频率,词的稀有程度
    • IDF = log((总文档数 - 包含该词的文档数 + 0.5) / (包含该词的文档数 + 0.5))

示例

查询 “elasticsearch search”,文档库有 100 篇文档:

  • elasticsearch 出现在 10 篇文档中
  • search 出现在 50 篇文档中

IDF 计算

  • IDF(elasticsearch) = log((100-10+0.5)/(10+0.5)) ≈ 2.18(稀有词,权重高)
  • IDF(search) = log((100-50+0.5)/(50+0.5)) ≈ 0.69(常见词,权重低)

结论:包含 elasticsearch 的文档得分更高。

深分页问题与解决方案

深分页问题

面试标准答案

深分页是指查询”第 1000 页,每页 10 条”时,Elasticsearch 需要在每个分片上查询 10000 条数据(from=10000, size=10),然后在协调节点排序后取 10 条。数据量大时会导致内存溢出和性能急剧下降。

示例

1
2
3
4
5
GET /index/_search
{
"from": 10000, // 第 1000 页
"size": 10
}

问题

  • 5 个分片,每个分片需查询 10010 条数据
  • 协调节点需汇总 5 × 10010 = 50050 条数据
  • 内存占用:50050 × 1KB ≈ 50MB(单次查询)

解决方案1:Scroll API(快照滚动)

适用场景:导出大量数据、批量处理

原理:创建快照,保持查询上下文,依次滚动获取数据。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. 创建 scroll
POST /index/_search?scroll=5m // 保持 5 分钟
{
"size": 1000,
"query": {"match_all": {}}
}

// 2. 滚动获取下一批
POST /_search/scroll
{
"scroll": "5m",
"scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}

// 3. 清理 scroll
DELETE /_search/scroll
{
"scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}

缺点

  • 占用大量内存(保持查询上下文)
  • 不支持实时数据(快照固定)
  • 需要手动清理 scroll

解决方案2:search_after(推荐)

适用场景:实时滚动分页、无限滚动

原理:使用上一次查询的最后一条数据的排序值作为下一次查询的起点。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 1. 第一次查询
GET /index/_search
{
"size": 10,
"sort": [
{"timestamp": "desc"},
{"_id": "asc"} // 唯一字段,避免排序值相同
]
}

// 返回:
// "hits": [
// {"_id": "1", "sort": [1609459200000, "1"]},
// {"_id": "2", "sort": [1609459199000, "2"]},
// ...
// {"_id": "10", "sort": [1609459190000, "10"]}
// ]

// 2. 下一页查询(使用上一页最后一条的 sort 值)
GET /index/_search
{
"size": 10,
"search_after": [1609459190000, "10"], // 上一页最后一条的 sort
"sort": [
{"timestamp": "desc"},
{"_id": "asc"}
]
}

优势

  • 内存占用小(无需保持上下文)
  • 支持实时数据
  • 性能稳定(不受深度影响)

对比总结

方案 适用场景 优点 缺点
from+size 前 100 页 简单、支持跳页 深分页性能差
scroll 数据导出、批量处理 全量遍历 占用内存、不实时
search_after 实时分页、无限滚动 高性能、实时 不支持跳页

高可用与脑裂问题

脑裂问题

面试标准答案

脑裂(Split Brain)是指集群因网络分区,导致选举出多个 Master 节点,各自管理集群的一部分,造成数据不一致。

原因

  1. 网络分区:Master 节点与其他节点网络不通
  2. 其他节点认为 Master 挂了,重新选举新 Master
  3. 旧 Master 恢复后,集群出现两个 Master

示例

1
2
3
4
5
6
7
8
9
正常状态:
Node1 (Master) - Node2 - Node3

网络分区:
[Node1 (Master)] | [Node2 (新Master) - Node3]
↓ ↓
写入数据A 写入数据B
↓ ↓
数据不一致!

解决方案:quorum 机制

配置

1
discovery.zen.minimum_master_nodes: (N/2 + 1)  // N  Master 候选节点数

示例

  • 3 个 Master 候选节点:minimum_master_nodes = 2
  • 5 个 Master 候选节点:minimum_master_nodes = 3

原理

  • 只有获得 半数以上 节点认可,才能成为 Master
  • 网络分区后,只有”多数派”能选出 Master,”少数派”无法选举

示例

1
2
3
4
5
6
7
3 个节点,minimum_master_nodes=2

网络分区:
[Node1] | [Node2 - Node3]
↓ ↓
只有1个节点 有2个节点
无法选举 可以选举新Master

其他高可用措施

  1. Discovery.zen.ping_timeout:心跳超时时间(默认 3 秒)
  2. Discovery.zen.fd.ping_interval:心跳间隔(默认 1 秒)
  3. 分片副本:每个主分片至少 1 个副本分片

面试高频 20 题

1. Elasticsearch 是什么?

标准答案
Elasticsearch 是基于 Lucene 的分布式搜索引擎,支持全文检索、日志分析、实时监控等场景。核心特点:分布式、RESTful API、实时搜索、高可用。

2. 倒排索引是什么?

标准答案
倒排索引是”关键词 → 文档列表”的映射,与传统数据库的”文档 ID → 内容”相反。通过倒排索引可以快速定位包含指定关键词的所有文档。

3. Elasticsearch 的写入流程?

标准答案

  1. 客户端请求协调节点
  2. 协调节点根据文档 ID 路由到主分片
  3. 主分片写入,并行转发到副分片
  4. 所有副分片写入成功后,向协调节点报告
  5. 协调节点向客户端返回结果

4. Elasticsearch 的查询流程?

标准答案
分为 Query 和 Fetch 两阶段:

  • Query:协调节点向所有分片发送查询,各分片返回 top N 的文档 ID 和得分
  • Fetch:协调节点合并结果,根据文档 ID 向分片请求完整文档

5. 深分页问题如何解决?

标准答案

  1. scroll:适用于数据导出,创建快照滚动获取
  2. search_after:适用于实时分页,使用上一页最后一条的排序值作为起点
  3. 避免:使用 from+size 深分页(性能差)

6. 脑裂问题是什么?如何解决?

标准答案
脑裂是指网络分区导致选举出多个 Master,造成数据不一致。解决方案:设置 minimum_master_nodes = N/2 + 1,确保只有”多数派”能选举 Master。

7. Elasticsearch 和 Solr 的区别?

维度 Elasticsearch Solr
实时性 近实时(1 秒) 需要手动 commit
分布式 原生支持 需要 ZooKeeper
社区 更活跃 较老牌
适用场景 日志分析、实时搜索 企业搜索

8. Elasticsearch 的分片和副本是什么?

标准答案

  • 主分片(Primary Shard):数据的横向分区,创建后不可修改数量
  • 副本分片(Replica Shard):主分片的备份,提供高可用和负载均衡

9. Elasticsearch 如何实现高可用?

标准答案

  1. 分片副本:每个主分片至少 1 个副本
  2. quorum 机制:避免脑裂
  3. 自动故障转移:Master 挂了自动选举新 Master

10. Mapping 是什么?

标准答案
Mapping 是 Elasticsearch 的 schema,定义字段类型(text、keyword、date 等)和分析器。类似于数据库的表结构。

11. text 和 keyword 的区别?

字段类型 分词 适用场景
text 全文检索(如文章内容)
keyword 精确匹配(如 ID、标签)

12. Analyzer 是什么?

标准答案
Analyzer 是分词器,包含 3 个组件:

  1. Character Filter:字符预处理
  2. Tokenizer:分词
  3. Token Filter:词项后处理(小写、停用词)

13. 如何优化 Elasticsearch 写入性能?

标准答案

  1. 批量写入:使用 bulk API
  2. 调整 refresh_interval:从 1s 调整为 30s
  3. 关闭副本:写入时临时关闭副本,写入完成后再开启
  4. 增加 indexing buffer:调整 indices.memory.index_buffer_size

14. 如何优化 Elasticsearch 查询性能?

标准答案

  1. 使用 filter 而非 query:filter 可缓存
  2. 避免深分页:使用 search_after
  3. 合理设计 Mapping:减少字段数量
  4. 预热查询:使用 warmers

15. 如何监控 Elasticsearch?

标准答案

  1. 集群健康GET /_cluster/health
  2. 节点统计GET /_nodes/stats
  3. 索引统计GET /<index>/_stats
  4. 慢查询日志slowlog

16. Elasticsearch 的 routing 机制?

标准答案

  • 默认 routing:shard_num = hash(_id) % num_primary_shards
  • 自定义 routing:可以指定 routing 参数,将相关文档路由到同一分片

17. Elasticsearch 的聚合(Aggregation)是什么?

标准答案
聚合是 Elasticsearch 的分析功能,类似 SQL 的 GROUP BY,支持:

  • Bucket Aggregation:分桶(如按时间分桶)
  • Metric Aggregation:指标(如求和、平均值)

18. Elasticsearch 如何实现实时搜索?

标准答案

  • 写入数据后,默认 1 秒后 refresh,数据从内存刷到文件系统缓存,变为可搜索
  • 调整 refresh_interval 可控制实时性

19. Elasticsearch 的段(Segment)是什么?

标准答案
段是 Lucene 的存储单元,一个索引由多个段组成。写入数据时先写入内存,refresh 后生成新段。段会定期合并(merge),减少段数量,提高查询性能。

20. Elasticsearch 和 MySQL 的区别?

维度 Elasticsearch MySQL
查询类型 全文检索、模糊查询 精确查询、关联查询
事务 不支持 支持
扩展性 水平扩展(分片) 垂直扩展(分库分表)
适用场景 搜索、日志分析 事务、关系型数据

es 更新和乐观锁控制

  • “_version” : 1,
  • “_seq_no” : 426,
  • “_primary_term” : 1,

性能优化

关注哪些性能指标

  • (读)query latency 1-2ms,复杂的查询可能到几十ms
  • (读)fetch latency ,QPS,读数据量,延时
  • (写)index rate,QPS,数据量,延时
  • (写)index latency
  • 存储数据量
  • 集群读写QPS,CPU、内存、存储、网络IO的监控
  • 节点维度的监控
  • index维度的监控

集群规划

  • 业务存储量,期望的SLA指标
  • 节点数量、内存、CPU数量,是否需要SSD等
  • 预留buffer,磁盘使用率达到85%、90%、95%
  • CPU使用率
  • 内存使用率
  • 冷热数据,灾备方案

settings 索引优化实践

  • 分片数量:number_of_shards,经验值:建议每个分片大小不要超过30GB。建议根据集群节点的个数规模,分片个数建议>=集群节点的个数。5节点的集群,5个分片就比较合理。注意:除非reindex操作,分片数是不可以修改的
  • 副本数量:number_of_replicas。除非你对系统的健壮性有异常高的要求,比如:银行系统。可以考虑2个副本以上。否则,1个副本足够。注意:副本数是可以通过配置随时修改的
  • refresh_interval 是一个参数,用于配置 Elasticsearch 中的索引刷新间隔。索引刷新是将内存中的数据写入磁盘以使其可搜索的过程。刷新操作会将新的文档和更新的文档写入磁盘,并使其在搜索结果中可见。默认值表示每秒执行一次刷新操作
  • 按照日期规划索引是个很好的习惯
  • 务必使用别名,ES不像mysql方面的更改索引名称。使用别名就是一个相对灵活的选择
  • setting中定义繁体全文检索时的traditional_chinese_analyzer以及一个名为lowercase的normalizer,常用于keyword类型的匹配
  • 结合profile、explain api 分析query慢的原因。search profile api
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
{
"hotel_index_20220810": {
"settings": {
"index": {
"refresh_interval": "1s",
"number_of_shards": "5",
"provided_name": "hotel_index_20220810",
"creation_date": "1660127508475",
"analysis": {
"filter": {
"t2sconvert": {
"convert_type": "t2s",
"type": "stconvert"
}
},
"normalizer": {
"lowercase": {
"filter": [
"lowercase"
],
"type": "custom"
}
},
"analyzer": {
"traditional_chinese_analyzer": {
"filter": "t2sconvert",
"type": "custom",
"tokenizer": "ik_smart"
}
}
},
"number_of_replicas": "2",
"uuid": "afdjafkdlaf",
"version": {
"created": "6080599"
}
}
}
}
}

mapping 数据模型优化

  • 不要使用默认的mapping.默认Mapping的字段类型是系统自动识别的。其中:string类型默认分成:text和keyword两种类型。如果你的业务中不需要分词、检索,仅需要精确匹配,仅设置为keyword即可。根据业务需要选择合适的类型,有利于节省空间和提升精度,如:浮点型的选择.
  • Mapping各字段的选型流程
  • 选择合理的分词器。常见的开源中文分词器包括:ik分词器、ansj分词器、hanlp分词器、结巴分词器、海量分词器、“ElasticSearch最全分词器比较及使用方法” 搜索可查看对比效果。如果选择ik,建议使用ik_max_word。因为:粗粒度的分词结果基本包含细粒度ik_smart的结果。
  • 一个字段包含多种语言:分别设置了不同的分词器。中文:ik_max_word,英语:english等
  • analyzer:表示文档写入时的分词,search_analyzer表示检索时query的分词
  • type:text,type:keyword,不分词
  • normalizer 表示英文keyword判断时不区分大小写
  • “dynamic” : “strict”
  • https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
"properties": {
"accommodation": {
"properties": {
"value_in_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"value_in_english": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword",
"normalizer": "lowercase"
}
},
"analyzer": "english"
},
"value_in_filipino": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "standard"
},
"value_in_indonesian": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "indonesian"
},
"value_in_malay": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "standard"
},
"value_in_thai": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "thai"
},
"value_in_tw_chinese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "traditional_chinese_analyzer"
},
"value_in_vietnamese": {
"type": "text",
"fields": {
"keyword": {
"type": "keyword"
}
},
"analyzer": "standard"
}
}
}
}

数据写入优化

  1. 要不要秒级响应?Elasticsearch近实时的本质是:最快1s写入的数据可以被查询到。如果refresh_interval设置为1s,势必会产生大量的segment,检索性能会受到影响。所以,非实时的场景可以调大,设置为30s,甚至-1
  2. 能批量就不单条写入
  3. 减少副本,提升写入性能。写入前,副本数设置为0,写入后,副本数设置为原来值

读优化

  1. 分析dsl
  2. 禁用 wildcard模糊匹配,通过match_phrase和slop结合查询。
  3. 极小的概率使用match匹配
  4. 结合业务场景,大量使用filter过滤器
  5. 控制返回字段和结果,同理,ES中,_source 返回全部字段也是非必须的。要通过_source 控制字段的返回,只返回业务相关的字段。
  6. 分页深度查询和遍历.分页查询使用:from+size;遍历使用:scroll;并行遍历使用:scroll+slice

业务优化

  1. 字段抽取、倾向性分析、分类/聚类、相关性判定放在写入ES之前的ETL阶段进行
  2. 产品经理基于各种奇葩业务场景可能会提各种无理需求

SDK 使用

es migrate tools

拓展阅读

Go 和 C++ 语言对比

Go and C++ are two different programming languages with different design goals, syntax, and feature sets. Here’s a brief comparison of the two:

Syntax: Go has a simpler syntax than C++. It uses indentation for block structure and has fewer keywords and symbols. C++ has a more complex syntax with a lot of features that can make it harder to learn and use effectively.

Memory Management: C++ gives the programmer more control over memory management through its support for pointers, manual memory allocation, and deallocation. Go, on the other hand, uses a garbage collector to automatically manage memory, making it less error-prone.

Concurrency: Go has built-in support for concurrency through goroutines and channels, which make it easier to write concurrent code. C++ has a thread library that can be used to write concurrent code, but it requires more manual management of threads and locks.

Performance: C++ is often considered a high-performance language, and it can be used for system-level programming and performance-critical applications. Go is also fast but may not be as fast as C++ in some cases.

Libraries and Frameworks: C++ has a vast ecosystem of libraries and frameworks that can be used for a variety of applications, from game development to machine learning. Go’s ecosystem is smaller, but it has good support for web development and distributed systems.

Overall, the choice of programming language depends on the project requirements, the available resources, and the developer’s expertise. Both Go and C++ have their strengths and weaknesses, and the best choice depends on the specific needs of the project.

Go语法介绍

string/[]byte

  • string是golang的基本数组类型,s := “hello,world”,一旦初始化后不允许修改其内容
  • 内部实现结构,指向数据的指针data和表示长度的len
  • 字符串拼接和格式化四种方式,+=,strings.join,buffer.writestring,fmt.sprintf
  • string 与 []byte的类型转换
  • 标准库strings提供了许多字符串操作的函数,例如Split、HasPrefix,Trim。

array

  • 数组array: [3]int{1,2,3}
  • 数组是值类型,数组传参发生拷贝
  • 定长
  • 数组的创建、初始化、访问和遍历range,len(arr)求数组的长度

slice

  • 切片slice初始化: make([]int,len,cap)
  • slice是引用类型
  • 变长,用容量和长度的区别,分别使用cap和len函数获取
  • 内存结构和实现:指针、cap、size共24字节
  • 常用函数,append,cap,len
  • 切片动态扩容
  • 深拷贝copy和浅拷贝“=”的区别
  • copy(slice1,slice2)

map

sync.map

struct

  • 空结构体struct{}的用途,节省内存。
  • 不支持继承,使用结构体嵌套组合
  • struct 可以比较吗?普通struct可以比较,带引用的struc不可比较,需要使用reflect.DeepEqual
  • struct没有slice和map类型时可直接判断
  • slice和map本身不可比较,需要使用reflect.DeepEqual()。
  • struct中包含slice和map等字段时,也要使用reflect.DeepEqual().
  • https://stackoverflow.com/questions/24534072/how-to-compare-struct-slice-map-are-equal

interface

  • https://draveness.me/golang/docs/part2-foundation/ch04-basic/golang-interface/
  • 隐式接口,实现接口的所有方法就隐式地实现了接口;不需要显示申明实现某接口
  • 接口也是 Go 语言中的一种类型,它能够出现在变量的定义、函数的入参和返回值中并对它们进行约束,不过 Go 语言中有两种略微不同的接口,一种是带有一组方法的接口,另一种是不带任何方法的 interface{}:
  • interface{} 类型不是任意类型,而是将类型转换成了 interface{} 类型
  • 结构体实现接口 vs 结构体指针实现接口 区别?
  • runtime.eface 和 runtime.iface 结构?
  • 结构体类型转化为接口的类型相互变换,interface类型断言为struct类型 过程
  • 动态派发与多态。动态派发(Dynamic dispatch)是在运行期间选择具体多态操作(方法或者函数)执行的过程,它是面向对象语言中的常见特性6。Go 语言虽然不是严格意义上的面向对象语言,但是接口的引入为它带来了动态派发这一特性,调用接口类型的方法时,如果编译期间不能确认接口的类型,Go 语言会在运行期间决定具体调用该方法的哪个实现。
  • Golang没有泛型,通过interface可以实现简单泛型编程,例如的sort的实现
  • 接口实现的源码

channel

  • Go鼓励CSP模型(communicating sequential processes),Goroutin之间通过channel传递数据
  • 非缓冲的同步channel和带缓冲的异步channel
  • 内部实现结构,带锁的循环队列runtime.hchan
  • channel创建make
  • chan <- i
  • 向channel发送数据。在发送数据的逻辑执行之前会先为当前 Channel 加锁,防止多个线程并发修改数据。如果 Channel 已经关闭,那么向该 Channel 发送数据时会报 “send on closed channel” 错误并中止程序。分为的三个部分:
    当存在等待的接收者时,通过 runtime.send 直接将数据发送给阻塞的接收者;
    当缓冲区存在空余空间时,将发送的数据写入 Channel 的缓冲区;
    当不存在缓冲区或者缓冲区已满时,等待其他 Goroutine 从 Channel 接收数据;
  • i <- ch,i, ok <- ch
  • 从channel接收数据的五种情况:
    • 如果 Channel 为空,那么会直接调用 runtime.gopark 挂起当前 Goroutine;
    • 如果 Channel 已经关闭并且缓冲区没有任何数据,runtime.chanrecv 会直接返回;
    • 如果 Channel 的 sendq 队列中存在挂起的 Goroutine,会将 recvx 索引所在的数据拷贝到接收变量所在的内存空间上并将 sendq 队列中 Goroutine 的数据拷贝到缓冲区;
    • 如果 Channel 的缓冲区中包含数据,那么直接读取 recvx 索引对应的数据;
    • 在默认情况下会挂起当前的 Goroutine,将 runtime.sudog 结构加入 recvq 队列并陷入休眠等待调度器的唤醒;
  • 关闭channel
  • 如何优雅的关闭channel?https://www.jianshu.com/p/d24dfbb33781, channel关闭后读操作会发生什么?写操作会发生什么?

类型和拷贝方式

  • 值类型 :String,Array,Int,Struct,Float,Bool,pointer(深拷贝)
  • 引用类型:Slice,Map (浅拷贝)

函数和方法,匿名函数

  • init函数
  • 值接收和指针接收的区别
  • 匿名函数?闭包?闭包延时绑定问题?用闭包写fibonacci数列?

指针和unsafe.Pointer

  • 相比C/C++,为了安全性考虑,Go指针弱化。不同类型的指针不能相互转化,指针变量不支持运算,不支持c/c++中的++,需要借助unsafe包
  • 任何类型的指针都可以被转换成unsafe.Pointer类型,通过unsafe.Pointer实现不同类型指针的转化
  • uintptr值可以被转换成unsafe.Pointer类型,通过uintptr实现指针的运算
  • unsafe.Pointer是一个指针类型,指向的值不能被解析,类似于C/C++里面的(void *),只说明这是一个指针,但是指向什么的不知道。
  • uintptr 是一个整数类型,这个整数的宽度足以用来存储一个指针类型数据;那既然是整数类类型,当然就可以对其进行运算了
  • nil
  • 实践string和[]byte的高效转换
  • 在业务场景中,使用指针虽然方便,但是要注意深拷贝和浅拷贝,这种错误还是比较常见的
  • 当你对象是结构体对象的指针时,你想要获取字段属性时,可以直接使用’.’,而不需要解引用

集合set

  1. golang中本身没有提供set,但可以通过map自己实现
  2. 利用map键值不可重复的特性实现set,value为空结构体。 map[interface{}]struct{}
  3. 如何自己实现set?

defer

  • defer定义的延迟函数参数在defer语句出时就已经确定下来了
  • defer定义顺序与实际执行顺序相反
  • return不是原子操作,执行过程是: 保存返回值(若有)–>执行defer(若有)–>执行ret跳转
  • 申请资源后立即使用defer关闭资源是好习惯
  • golang中的defer用途?调用时机?调用顺序?预计算值?
  • defer 实现原理?

Go 错误处理 error、panic

  • 在Go 语言中,错误被认为是一种可以预期的结果;而异常则是一种非预期的结果,发生异常可能表示程序中存在BUG 或发生了其它不可控的问题。
  • Go 语言推荐使用 recover 函数将内部异常转为错误处理,这使得用户可以真正的关心业务相关的错误处理。
  • 在Go服务中通常需要自定义粗错误类型,最好能有效区分业务逻辑错误和系统错误,同时需要捕获panic,将panic转化为error,避免某个错误影响server重启
  • panic 时需要保留runtime stack
    1
    2
    3
    4
    5
    6
    7
    8
    9
     defer func() {
    if x := recover(); x != nil {
    panicReason := fmt.Sprintf("I'm panic because of: %v\n", x)
    logger.LogError(panicReason)
    stk := make([]byte, 10240)
    stkLen := runtime.Stack(stk, false)
    logger.LogErrorf("%s\n", string(stk[:stkLen]))
    }
    }()

Go channel通道

channel

channel是golang中的csp并发模型非常重要组成部分,使用起来非常像阻塞队列。

  • 通道channel变量本身就是指针,可用“==”操作符判断是否为同一对象
  • 未初始化的channel为nil,需要使用make初始化
  • 理解初始化的channel和nil channel的区别?读写nil channel都会阻塞,关闭nil channel会出现panic;可以读关闭的channel,写关闭的channel会发出panic,close关闭了的channel会发出panic
  • 同步模式的channel必须有配对操作的goroutine出现,否则会一直阻塞,而异步模式在缓冲区未满或者数据未读完前,不会阻塞。
  • 内置的cap和len函数返回channel缓冲区大小和当前已缓冲的数量,而对于同步通道则返回0
  • 除了使用”<-“发送和接收操作符外,还可以用ok-idom或者range模式处理chanel中的数据。
  • 重复关闭和关闭nil channel都会导致pannic
  • make可以创建单项通道,但那没有意义,通产使用类型转换来获取单向通道,并分别赋予给操作方
  • 无法将单向通道转换成双向通道

基本用法

  1. 协程之间传递数据
  2. 用作事件通知,经常使用空结构体channel作为某个事件通知
  3. select帮助同时多个通道channel,它会随机选择一个可用的通道做收发操作
  4. 使用异步channel(带有缓冲)实现信号量semaphore
  5. 标准库提供了timeout和tick的channel实现。
  6. 通道并非用来取代锁的,通道和锁有各自不同的使用场景,通道倾向于解决逻辑层次的并发处理架构,而锁则用来保护数据的安全性。
  7. channel队列本质上还是使用锁同步机制,单次获取更多的数据(批处理),减少收发的次数,可改善因为频繁加锁造成的性能问题。
  8. channel可能会导致goroutine leak问题,是指goroutine处于发送或者接收阻塞状态,但一直未被唤醒,垃圾回收器并不收集此类资源,造成资源的泄露。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    func main() {
    done := make(chan struct{})
    s := make(chan int)
    go func() {
    s <- 1
    close(done)
    }()
    fmt.Println(<-s)
    <-done
    }

    func main() {
    sem := make(chan struct{}, 2) //two groutine
    var wg sync.WaitGroup
    for i := 0; i < 10; i++ {
    wg.Add(1)
    go func(id int) {
    defer wg.Done()
    defer func() { <-sem }()
    sem <- struct{}{}
    time.Sleep(1 * time.Second)
    fmt.Println("id=", id)
    }(i)
    }
    wg.Wait()
    }


    func main() {
    go func() {
    tick := time.Tick(1 * time.Second)
    for {
    select {
    case <-time.After(5 * time.Second):
    fmt.Println("time out")
    case <-tick:
    fmt.Println("time tick 1s")
    default:
    fmt.Println("default")
    }
    }
    }()
    <-(chan struct{})(nil)
    }

Go并发模型 (Goroutine/channel/GMP)

what’s CSP?

The Communicating Sequential Processes (CSP) model is a theoretical model of concurrent programming that was first introduced by Tony Hoare in 1978. The CSP model is based on the idea of concurrent processes that communicate with each other by sending and receiving messages through channels.The Go programming language provides support for the CSP model through its built-in concurrency features, such as goroutines and channels. In Go, concurrent processes are represented by goroutines, which are lightweight threads of execution. The communication between goroutines is achieved through channels, which provide a mechanism for passing values between goroutines in a safe and synchronized manner.

Which is Goroutine ?

  • Goroutines are lightweight, user-level threads of execution that run concurrently with other goroutines within the same process.
  • Unlike traditional threads, goroutines are managed by the Go runtime, which automatically schedules and balances their execution across multiple CPUs and makes efficient use of available system resources.

比较Goroutine、thread、process

  • 比较进程、线程和Goroutine。进程是资源分配的单位,有独立的地址空间,线程是操作系统调度的单位,协程是更细力度的执行单元,需要程序自身调度。Go语言原生支持Goroutine,并提供高效的协程调度模型。
  • Goroutines, threads, and processes are all mechanisms for writing concurrent and parallel code, but they have some important differences:
  • Goroutines: A goroutine is a lightweight, user-level thread of execution that runs concurrently with other goroutines within the same process. Goroutines are managed by the Go runtime, which automatically schedules and balances their execution across multiple CPUs. Goroutines require much less memory and have much lower overhead compared to threads, allowing for many goroutines to run simultaneously within a single process.
  • Threads: A thread is a basic unit of execution within a process. Threads are independent units of execution that share the same address space as the process that created them. This allows threads to share data and communicate with each other, but also introduces the need for explicit synchronization to prevent race conditions and other synchronization issues.
  • Processes: A process is a self-contained execution environment that runs in its own address space. Processes are independent of each other, meaning that they do not share memory or other resources. Communication between processes requires inter-process communication mechanisms, such as pipes, sockets, or message queues.
  • In general, goroutines provide a more flexible and scalable approach to writing concurrent code compared to threads, as they are much lighter and more efficient, and allow for many more concurrent units of execution within a single process. Processes provide a more secure and isolated execution environment, but have higher overhead and require more explicit communication mechanisms.

Why is Goroutine lighter and more efficient than thread or process?

  • Stack size: Goroutines have a much smaller stack size compared to threads. The stack size of a goroutine is dynamically adjusted by the Go runtime, based on the needs of the goroutine. This allows for many more goroutines to exist simultaneously within a single process, as they require much less memory.
  • Scheduling: Goroutines are scheduled by the Go runtime, which automatically balances and schedules their execution across multiple CPUs. This eliminates the need for explicit thread management and synchronization, reducing overhead.
  • Context switching: Context switching is the process of saving and restoring the state of a running thread in order to switch to a different thread. Goroutines have a much lower overhead for context switching compared to threads, as they are much lighter and require less state to be saved and restored.
  • Resource sharing: Goroutines share resources with each other and with the underlying process, eliminating the need for explicit resource allocation and deallocation. This reduces overhead and allows for more efficient use of system resources.
  • Overall, the combination of a small stack size, efficient scheduling, low overhead context switching, and efficient resource sharing makes goroutines much lighter and more efficient than threads or processes, and allows for many more concurrent units of execution within a single process.
  • Goroutine 上下文切换只涉及到三个寄存器(PC / SP / DX)的值修改;而对比线程的上下文切换则需要涉及模式切换(从用户态切换到内核态)、以及 16 个寄存器、PC、SP…等寄存器的刷新;内存占用少:线程栈空间通常是 2M,Goroutine 栈空间最小 2K;Golang 程序中可以轻松支持10w 级别的 Goroutine 运行,而线程数量达到 1k 时,内存占用就已经达到 2G。
  • 理解G、P、M的含义以及调度模型

How are goroutines scheduled by runtime?

  • Cooperative (协作式). The scheduler uses a cooperative scheduling model, which means that goroutines voluntarily yield control to the runtime when they are blocked or waiting for an event.
  • Timer-based preemption. The scheduler uses a technique called timer-based preemption to interrupt the execution of a running goroutine and switch to another goroutine if it exceeds its time slice
  • Work-stealing. The scheduler uses a work-stealing algorithm, where each CPU has its own local run queue, and goroutines are dynamically moved between run queues to balance the o balance the load and improve performance.
  • no explicit prioritization. The Go runtime scheduler does not provide explicit support for prioritizing goroutines. Instead, it relies on the cooperative nature of goroutines to ensure that all goroutines make progress. In a well-designed Go program, the program should be designed such that all goroutines make progress in a fair and balanced manner.
  • https://blog.csdn.net/sinat_34715587/article/details/124990458
  • G 的数量可以远远大于 M 的数量,换句话说,Go 程序可以利用少量的内核级线程来支撑大量 Goroutine 的并发。多个 Goroutine 通过用户级别的上下文切换来共享内核线程 M 的计算资源,但对于操作系统来说并没有线程上下文切换产生的性能损耗,支持任务窃取(work-stealing)策略:为了提高 Go 并行处理能力,调高整体处理效率,当每个 P 之间的 G 任务不均衡时,调度器允许从 GRQ,或者其他 P 的 LRQ 中获取 G 执行。
  • 减少因Goroutine创建大量M:
    • 由于原子、互斥量或通道操作调用导致 Goroutine 阻塞,调度器将把当前阻塞的 Goroutine 切换出去,重新调度 LRQ 上的其他 Goroutine;
    • 由于网络请求和 IO 操作导致 Goroutine 阻塞,通过使用 NetPoller 进行网络系统调用,调度器可以防止 Goroutine 在进行这些系统调用时阻塞 M。这可以让 M 执行 P 的 LRQ 中其他的 Goroutines,而不需要创建新的 M。有助于减少操作系统上的调度负载。
    • 当调用一些系统方法的时候,如果系统方法调用的时候发生阻塞,这种情况下,网络轮询器(NetPoller)无法使用,而进行系统调用的 Goroutine 将阻塞当前 M,则创建新的M。阻塞的系统调用完成后:M1 将被放在旁边以备将来重复使用
    • 如果在 Goroutine 去执行一个 sleep 操作,导致 M 被阻塞了。Go 程序后台有一个监控线程 sysmon,它监控那些长时间运行的 G 任务然后设置可以强占的标识符,别的 Goroutine 就可以抢先进来执行。

What are the states of Goroutine and how do they flow?

  • 协程的状态流转?Grunnable、Grunning、Gwaiting
  • In Go, a Goroutine can be in one of several states during its lifetime. The states are:
  • New: The Goroutine is created but has not started executing yet.
  • Running: The Goroutine is executing on a machine-level thread.
  • Waiting: The Goroutine is waiting for some external event, such as I/O, channel communication, or a timer.
  • Sleeping: The Goroutine is sleeping, or waiting for a specified amount of time.
  • Dead: The Goroutine has completed its execution and is no longer running.

In summary, the lifetime of a Goroutine in Go starts when it is created and ends when it completes its execution or encounters a panic, and can be influenced by synchronization mechanisms such as channels and wait groups.

  • Golang context 用于在树形goroutine结构中,通过信号减少资源的消耗,包含Deadline、Done、Error、Value四个接口
  • 常用的同步原语:channel、sync.mutex、sync.RWmutex、sync.WaitGroup、sync.Once、atomic
  • 协程的状态流转?Grunnable、Grunning、Gwaiting
  • sync.Mutex 和 sync.RWMutex 互斥锁和读写锁的使用场景?
  • sync.Mutex: “锁”实现背后那些事
  • Golang 协程优雅的退出?
  • 深入理解协程gmp调度模型,以及其发展历史
  • 理解操作系统是怎么调度的,golang协程调度的优势,切换代价低,goroutine开销低,并发度高。
  • Golang IO 模型和网络轮训器

Go 内存管理和垃圾回收(memory and gc)

内存管理基本策略

为了兼顾内存分配的速度和内存利用率,大多数都采用以下策略进行内存管理:

  1. 申请:每次从操作系统申请一大块内存(比如1MB),以减少系统调用
  2. 切分:为了兼顾大小不同的对象,将申请到的内存按照一定的策略切分成小块,使用链接相连
  3. 分配:为对象分配内存时,只需从大小合适的链表中提取一块即可。
  4. 回收复用: 对象不再使用时,将该小块内存归还到原链表
  5. 释放: 如果闲置内存过多,则尝试归凡部分内存给操作系统,减少内存开销。

golang内存管理

 golang内存管理基本继承了tcmolloc成熟的架构,因此也符合内存管理的基本策略。

  1. 分三级管理,线程级的thread cache,中央center cache,和管理span的center heap。
  2. 每一级都采用链表管理不同size空闲内存,提高内存利用率
  3. 线程级的tread local cache能够减少竞争和加锁操作,提高效率。中央center cache为所有线程共享。
  4. 小对象直接从本地cache获取,大对象从center heap获取,提高内存利用率
  5. 每一级内存不足时,尝试从下一级内存获取
    内存三级管理
    线程cache
    大对象span管理
  • 多级缓存:内存分配器不仅会区别对待大小不同的对象,还会将内存分成不同的级别分别管理,TCMalloc 和 Go 运行时分配器都会引入线程缓存(Thread Cache)、中心缓存(Central Cache)和页堆(Page Heap)三个组件分级管理内存
  • 对象大小:Go 语言的内存分配器会根据申请分配的内存大小选择不同的处理逻辑,运行时根据对象的大小将对象分成微对象、小对象和大对象三种,tiny,small,large
  • mspan、mcache、mcentral、mheap

What are the memory leak scenarios in Go language?

  • Goroutine leaks: If a goroutine is created and never terminated, it can result in a memory leak. This can occur when a program creates a goroutine to perform a task but fails to provide a mechanism for the goroutine to terminate, such as a channel to receive a signal to stop.

  • Leaked closures: Closures are anonymous functions that capture variables from their surrounding scope. If a closure is created and assigned to a global variable, it can result in a memory leak, as the closure will continue to hold onto the captured variables even after they are no longer needed.

  • Incorrect use of channels: Channels are a mechanism for communicating between goroutines. If a program creates a channel but never closes it, it can result in a memory leak. Additionally, if a program receives values from a channel but never discards them, they will accumulate in memory and result in a leak.

  • Unclosed resources: In Go, it’s important to close resources, such as files and network connections, when they are no longer needed. Failure to do so can result in a memory leak, as the resources and their associated memory will continue to be held by the program.

  • Unreferenced objects: In Go, unreferenced objects are objects that are no longer being used by the program but still exist in memory. This can occur when an object is created and never explicitly deleted or when an object is assigned a new value and the old object is not properly disposed of.
    By following best practices and being mindful of these common scenarios, you can help to avoid memory leaks in your Go programs. Additionally, you can use tools such as the Go runtime profiler to detect and diagnose memory leaks in your programs.

  • Memory Leaking Scenarios

    • hanging goroutine
    • cgo
    • substring/slice
    • ticker

golang支持垃圾回收,gc能减少编程的负担,但与此同时也可能造成程序的性能问题。那么如何测量golang程序使用的内存,以及如何减少golang gc的负担呢?经历了许多版本的迭代,golang gc 沿着低延迟和高吞吐的目标在进化,相比早起版本,目前有了很大的改善,但仍然有可能是程序的瓶颈。因此要学会分析golang 程序的内存和垃圾回收问题。

如何查看程序的gc信息?

  1. 通过设置环境变量?env GODEBUG=gctrace=1
    例如: env GODEBUG=gctrace=1 godoc -http=:8080
  2. import _ “net/http/pprof”,查看/debug/pprof

tips:

  1. 减少内存分配,优先使用第二种APIs
    func (r *Reader) Read() ([]byte, error)
    func (r *Reader) Read(buf []byte) (int, error)
  2. 尽量避免string 和 []byte之间的转换
  3. 尽量减少两个字符串的合并
  4. 对slice预先分配大小
  5. 尽量不要使用cgo,因为c和go毕竟是两种语言。cgo是个high overhead的操作,调用cgo相当于阻塞IO,消耗一个线程
  6. defer is expensive?在性能要求较高的时候,考虑少用
  7. 对IO操作设置超时机制是个好习惯SetDeadline, SetReadDeadline, SetWriteDeadline
  8. 当数据量很大的时候,考虑使用流式IO(streaming IO)。io.ReaderFrom / io.WriterTo

gc 的过程

  • Marking phase: In this phase, the Go runtime identifies all objects that are accessible by the program and marks them as reachable. Objects that are not marked as reachable are considered unreachable and eligible for collection.
  • Sweeping phase: In this phase, the Go runtime scans the memory heap and frees all objects that are marked as unreachable. The memory space occupied by these objects is now available for future allocation.
  • Compacting phase: In this phase, the Go runtime rearranges the remaining objects on the heap to reduce fragmentation and minimize the impact of future allocations and deallocations.

垃圾回收算法概述

  golang是近几年出现的带有垃圾回收的现代语言,其垃圾回收算法自然也相互借鉴。因此在学习golang gc之前有必要了解目前主流的垃圾回收方法。

  1. 引用计数:熟悉C++智能指针应该了解引用计数方法。它对每一个分配的对象增加一个计数的域,当对象被创建时其值为1。每次有指针指向该对象时,其引用计数增加1,引用该对象的对象被析构时,其引用计数减1。当该对象的引用计数为0时,该对象也会被析构回收。引用对象对于C++这类没有垃圾回收器,对于便于对象管理的是不错的工具,但是维护引用计数会造成程序运行效率下降。
  2. 标记-清扫: 标记清扫是古老的垃圾回收算法,出现在70年代。通过指定每个内存阈值或者时间长度,垃圾回收器会挂起用户程序,也称为STW(stop the world)。垃圾回收器gc会对程序所涉及的所有对象进行一次遍历以确定哪些内存单元可以回收,因此分为标记(mark)和清扫(sweep),标记阶段标明哪些内存在使用不能回收,清扫阶段将不需要的内存单元释放回收。标记清扫法最大的问题是需要STW,当程序使用的内存较多时,其性能会比较差,延时较高。
  3. 三色标记法: 三色标记法是对标记清扫的改进,也是golang gc的主要算法,其最大的的优点是能够让部分gc和用户程序并发进行。它将对象分为白色、灰色和黑色:
    • 开始时所有的对象都是白色
    • 从根出发,将所有可到达对象标记为灰色,放入待处理队列
    • 从待处理队列中取出灰色对象,并将其引用的对象标记为灰色放入队列中,其自身标记为黑色。
    • 重复步骤3,直到灰色对象队列为空。最终只剩下白色对象和黑色对象,对白色对象尽心gc。
  4. 另外,还有一些在此基础上进行优化改进的gc算法,例如分代收集,节点复制等,它会考虑到对象的生命周期的长度,减少扫描标记的操作,相对来说效率会高一些。

golang垃圾回收

  golang gc是使用三色标记清理法,为了对用户对象进行标记需要将用户程序所有线程全部冻结(STW),当程序中包含很多对象时,暂停时间会很长,用户逻辑对用户的反应就会中止。那么如何缩短这个过程呢?一种自然的想法,在三色标记法扫描之后,只会存在黑色和白色两种对象,黑色是程序正在使用的对象不可回收,白色对象是此时不会被程序的对象,也是gc的要清理的对象。那么回收白色对象肯定不会和用户程序造成竞争冲突,因此回收操作和用户程序是可以并发的,这样可以缩短STW的时间。

  写屏障使得扫描操作和回收操作都可以和用户程序并发。我们试想一下,刚把一个对象标记为白色,用户程序突然又引用了它,这种扫描操作就比较麻烦,于是引入了屏障技术。内存扫描和用户逻辑也可以并发执行,用户新建的对象认为是黑色的,已经扫描过的对象有可能因为用户逻辑造成对象状态发生改变。所以**对扫描过后的对象使用操作系统写屏障功能用来监控用户逻辑这段内存,一旦这段内存发生变化写屏障会发生一个信号,gc捕获到这个信号会重新扫描改对象,查看它的引用或者被引用是否发生改变,从而判断该对象是否应该被清理。因此通过写屏障技术,是的扫描操作也可以合用户程序并发执行。

  gc控制器:gc算法并不万能的,针对不同的场景可能需要适当的设置。例如大数据密集计算可能不在乎内存使用量,甚至可以将gc关闭。golang 通过百分比来控制gc触发的时机,设置的百分比指的是程序新分配的内存与上一次gc之后剩余的内存量,例如上次gc之后程序占有2MB,那么下一次gc触发的时机是程序又新分配了2MB的内存。我们可以通过SetGCPercent函数动态设置,默认值为100,当百分比设置为负数时例如-1,表明关闭gc。
SetGCPercent

golang gc调优实例

gc 是golang程序性能优化非常重要的一部分,建议依照下面两个实例实践golang程序优化。

 

What’s Go closure?

In Go, a closure is a function that has access to variables from its outer (enclosing) function’s scope. The closure “closes over” the variables, meaning that it retains access to them even after the outer function has returned. This makes closures a powerful tool for encapsulating data and functionality and for creating reusable code.

Encapsulating State

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

import "fmt"

func counter() func() int {
i := 0
return func() int {
i++
return i
}
}

func main() {
c := counter()

fmt.Println(c()) // Output: 1
fmt.Println(c()) // Output: 2
fmt.Println(c()) // Output: 3
}

Implementing Callbacks

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package main

import "fmt"

func forEach(numbers []int, callback func(int)) {
for _, n := range numbers {
callback(n)
}
}

func main() {
numbers := []int{1, 2, 3, 4, 5}

// Define a callback function to apply to each element of the numbers slice.
callback := func(n int) {
fmt.Println(n * 2)
}

// Use the forEach function to apply the callback function to each element of the numbers slice.
forEach(numbers, callback)
}

Fibonacci

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import "fmt"

func memoize(f func(int) int) func(int) int {
cache := make(map[int]int)
return func(n int) int {
if val, ok := cache[n]; ok {
return val
}
result := f(n)
cache[n] = result
return result
}
}

func fibonacci(n int) int {
if n <= 1 {
return n
}
return fibonacci(n-1) + fibonacci(n-2)
}

func main() {
fib := memoize(fibonacci)
for i := 0; i < 10; i++ {
fmt.Println(fib(i))
}
}

Factorial

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

func main() {
factorial := func(n int) int {
if n <= 1 {
return 1
}
return n * factorial(n-1)
}

fmt.Println(factorial(5)) // Output: 120
}

Event Handling

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package main

import (
"fmt"
"time"
)

type Button struct {
onClick func()
}

func NewButton() *Button {
return &Button{}
}

func (b *Button) SetOnClick(f func()) {
b.onClick = f
}

func (b *Button) Click() {
if b.onClick != nil {
b.onClick()
}
}

func main() {
button := NewButton()
button.SetOnClick(func() {
fmt.Println("Button Clicked!")
})

go func() {
for {
button.Click()
time.Sleep(1 * time.Second)
}
}()

fmt.Scanln()
}

Go http client 实践

最近在项目开发中使用http服务与第三方服务交互,感觉golang的http封装得很好,很方便使用但是也有一些坑需要注意,一是自动复用连接,二是Response.Body的读取和关闭

http客户端自动复用连接

首先用代码直观的体验http客户端自动复用连接特点
server.go

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "hello!")
    })
    http.ListenAndServe(":8848", nil)
}

client.go

func doReq() {
    resp, err := http.Get("http://127.0.0.1:8848/test")
    if err != nil {
        fmt.Println(err)
        return
    }
    io.Copy(os.Stdout, resp.Body)
    defer resp.Body.Close()
}

func main() {
    //http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = 10
    for {
        go doReq()
        go doReq()
        //	go doReq()
        time.Sleep(300 * time.Millisecond)
    }
}

测试1:执行netstat | grep "8848" | wc -l 结果:一直都是4
测试2:增加一个go doReq(),继续测试,结果:是一直增大
测试3:在测试2的基础上设置MaxIdleConnsPerHost = 10,结果:一直都是6

测试1已经能说明golang的http会自动复用连接
测试2为什么连接数量会一直增加呢?原因是golang中默认只保持两条持久连接,http.Transport没有设置MaxIdleConnPerHost,于是便采用了默认的DefaultMaxIdleConnsPerHost,这个值是2。
测试3通过加大MaxIdleConnPerHost的值,就能高效的利用http的自动复用机制。

读取和关闭Response.Body

将Resonse.Body的读取的代码屏蔽,继续测试。

func doReq() {
    resp, err := http.Get("http://127.0.0.1:8848/test")
    if err != nil {
        fmt.Println(err)
        return
    }
    //io.Copy(os.Stdout, resp.Body)
    defer resp.Body.Close()
}  

测试结果发现,连接数一直增加。
产生的原因:body实际上是一个嵌套了多层的net.TCPConn,当body没有被完全读取,也没有被关闭是,那么这次的http事物就没有完成,除非连接因为超时终止了,否则相关资源无法被回收。
从实现上看只要body被读完,连接就能被回收,只有需要抛弃body时才需要close,似乎不关闭也可以。但那些正常情况能读完的body,即第一种情况,在出现错误时就不会被读完,即转为第二种情况。而分情况处理则增加了维护者的心智负担,所以始终close body是最佳选择。

Go sync.Pool

基本使用

https://golang.org/pkg/sync/
sync.Pool的使用非常简单,它具有以下几个特点:

  • sync.Pool设计目的是存放已经分配但暂时不用的对象,供以后使用,以减轻gc的代价,提高效率
  • 存储在Pool中的对象会随时被gc自动回收,Pool中对象的缓存期限为两次gc之间
  • 用户无法定义sync.Pool的大小,其大小仅仅受限于内存的大小
  • sync.Pool支持多协程之间共享

sync.Pool的使用非常简单,定义一个Pool对象池时,需要提供一个New函数,表示当池中没有对象时,如何生成对象。对象池Pool提供Get和Put函数从Pool中取和存放对象。

下面有一个简单的实例,直接运行是会打印两次“new an object”,注释掉runtime.GC(),发现只会调用一次New函数,表示实现了对象重用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package main

import (
"fmt"
"runtime"
"sync"
)

func main() {
p := &sync.Pool{
New: func() interface{} {
fmt.Println("new an object")
return 0
},
}

a := p.Get().(int)
a = 100
p.Put(a)
runtime.GC()
b := p.Get().(int)
fmt.Println(a, b)
}

sync.Pool 如何支持多协程共享?

sync.Pool支持多协程共享,为了尽量减少竞争和加锁的操作,golang在设计的时候为每个P(核)都分配了一个子池,每个子池包含一个私有对象和共享列表。 私有对象只有对应的和核P能够访问,而共享列表是与其它P共享的。

在golang的GMP调度模型中,我们知道协程G最终会被调度到某个固定的核P上。当一个协程在执行Pool的get或者put方法时,首先对改核P上的子池进行操作,然后对其它核的子池进行操作。因为一个P同一时间只能执行一个goroutine,所以对私有对象存取操作是不需要加锁的,而共享列表是和其他P分享的,因此需要加锁操作。

一个协程希望从某个Pool中获取对象,它包含以下几个步骤:

  1. 判断协程所在的核P中的私有对象是否为空,如果非常则返回,并将改核P的私有对象置为空
  2. 如果协程所在的核P中的私有对象为空,就去改核P的共享列表中获取对象(需要加锁)
  3. 如果协程所在的核P中的共享列表为空,就去其它核的共享列表中获取对象(需要加锁)
  4. 如果所有的核的共享列表都为空,就会通过New函数产生一个新的对象

在sync.Pool的源码中,每个核P的子池的结构如下所示:

// Local per-P Pool appendix.
type poolLocalInternal struct {
    private interface{}   // Can be used only by the respective P.
    shared  []interface{} // Can be used by any P.
    Mutex                 // Protects shared.
}

更加细致的sync.Pool源码分析,可参考http://jack-nie.github.io/go/golang-sync-pool.html

为什么不使用sync.pool实现连接池?

刚开始接触到sync.pool时,很容易让人联想到连接池的概念,但是经过仔细分析后发现sync.pool并不是适合作为连接池,主要有以下两个原因:

  • 连接池的大小通常是固定且受限制的,而sync.Pool是无法控制缓存对象的数量,只受限于内存大小,不符合连接池的目标
  • sync.Pool对象缓存的期限在两次gc之间,这点也和连接池非常不符合

golang中连接池通常利用channel的缓存特性实现。当需要连接时,从channel中获取,如果池中没有连接时,将阻塞或者新建连接,新建连接的数量不能超过某个限制。

https://github.com/goctx/generic-pool基于channel提供了一个通用连接池的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package pool

import (
"errors"
"io"
"sync"
"time"
)

var (
ErrInvalidConfig = errors.New("invalid pool config")
ErrPoolClosed = errors.New("pool closed")
)

type Poolable interface {
io.Closer
GetActiveTime() time.Time
}

type factory func() (Poolable, error)

type Pool interface {
Acquire() (Poolable, error) // 获取资源
Release(Poolable) error // 释放资源
Close(Poolable) error // 关闭资源
Shutdown() error // 关闭池
}

type GenericPool struct {
sync.Mutex
pool chan Poolable
maxOpen int // 池中最大资源数
numOpen int // 当前池中资源数
minOpen int // 池中最少资源数
closed bool // 池是否已关闭
maxLifetime time.Duration
factory factory // 创建连接的方法
}

func NewGenericPool(minOpen, maxOpen int, maxLifetime time.Duration, factory factory) (*GenericPool, error) {
if maxOpen <= 0 || minOpen > maxOpen {
return nil, ErrInvalidConfig
}
p := &GenericPool{
maxOpen: maxOpen,
minOpen: minOpen,
maxLifetime: maxLifetime,
factory: factory,
pool: make(chan Poolable, maxOpen),
}

for i := 0; i < minOpen; i++ {
closer, err := factory()
if err != nil {
continue
}
p.numOpen++
p.pool <- closer
}
return p, nil
}

func (p *GenericPool) Acquire() (Poolable, error) {
if p.closed {
return nil, ErrPoolClosed
}
for {
closer, err := p.getOrCreate()
if err != nil {
return nil, err
}
// 如果设置了超时且当前连接的活跃时间+超时时间早于现在,则当前连接已过期
if p.maxLifetime > 0 && closer.GetActiveTime().Add(time.Duration(p.maxLifetime)).Before(time.Now()) {
p.Close(closer)
continue
}
return closer, nil
}
}

func (p *GenericPool) getOrCreate() (Poolable, error) {
select {
case closer := <-p.pool:
return closer, nil
default:
}
p.Lock()
if p.numOpen >= p.maxOpen {
closer := <-p.pool
p.Unlock()
return closer, nil
}
// 新建连接
closer, err := p.factory()
if err != nil {
p.Unlock()
return nil, err
}
p.numOpen++
p.Unlock()
return closer, nil
}

// 释放单个资源到连接池
func (p *GenericPool) Release(closer Poolable) error {
if p.closed {
return ErrPoolClosed
}
p.Lock()
p.pool <- closer
p.Unlock()
return nil
}

// 关闭单个资源
func (p *GenericPool) Close(closer Poolable) error {
p.Lock()
closer.Close()
p.numOpen--
p.Unlock()
return nil
}

// 关闭连接池,释放所有资源
func (p *GenericPool) Shutdown() error {
if p.closed {
return ErrPoolClosed
}
p.Lock()
close(p.pool)
for closer := range p.pool {
closer.Close()
p.numOpen--
}
p.closed = true
p.Unlock()
return nil
}

Go指针和unsafe.pointer

  1. 不同类型的指针不能相互转化
  2. 指针变量不能进行运算,不支持c/c++中的++,–运算
  3. 任何类型的指针都可以被转换成unsafe.Pointer类型,反之也是
  4. uintptr值可以被转换成unsafe.Pointer类型,反之也是
  5. 对unsafe.Pointer和uintptr两种类型单独解释两句:
    • unsafe.Pointer是一个指针类型,指向的值不能被解析,类似于C/C++里面的(void *),只说明这是一个指针,但是指向什么的不知道。
    • uintptr 是一个整数类型,这个整数的宽度足以用来存储一个指针类型数据;那既然是整数类类型,当然就可以对其进行运算了
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      package main
      import (
      "fmt"
      "unsafe"
      )
      func main() {
      var ii [4]int = [4]int{11, 22, 33, 44}
      px := &ii[0]
      fmt.Println(&ii[0], px, *px)
      //compile error
      //pf32 := (*float32)(px)

      //compile error
      // px = px + 8
      // px++

      var pointer1 unsafe.Pointer = unsafe.Pointer(px)
      var pf32 *float32 = (*float32)(pointer1)

      var p2 uintptr = uintptr(pointer1)
      print(p2)
      p2 = p2 + 8
      var pointer2 unsafe.Pointer = unsafe.Pointer(p2)
      var pi32 *int = (*int)(pointer2)

      fmt.Println(*px, *pf32, *pi32)

      }

nil

引用类型声明而没有初始化赋值时,其值为nil。golang需要经常判断nil,防止出现panic错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
bool  -> false  
numbers -> 0
string-> ""

pointers -> nil
slices -> nil
maps -> nil
channels -> nil
functions -> nil
interfaces -> nil

package main

import (
"fmt"
)

type Person struct {
AgeYears int
Name string
Friends []Person
}

func main() {
var p Person
fmt.Printf("%v\n", p)

var slice1 []int
fmt.Println(slice1)
if slice1 == nil {
fmt.Println("slice1 is nil")
}
// fmt.Println(slice1[0]) panic

// var c chan int
// close(c) panic
}

编译器优化和逃逸分析

逃逸分析(Escape analysis)

golang在内存分配的时候没有堆(heap)和栈(stack)的区别,由编译器决定是否需要将对象逃逸到堆中。例如:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
	func Sum() int {
const count = 100
numbers := make([]int, count)
for i := range numbers {
numbers[i] = i + 1
}

var sum int
for _, i := range numbers {
sum += i
}
return sum
}

func main() {
answer := Sum()
fmt.Println(answer)
}
1
2
3
4
5
$ go build -gcflags=-m test_esc.go 
command-line-arguments
./test_esc.go:9:17: Sum make([]int, count) does not escape
./test_esc.go:23:13: answer escapes to heap
./test_esc.go:23:13: main ... argument does not escape

内敛(Inlining)

了解C/C++的应该知道内敛,golang编译器同样支持函数内敛,对于较短且重复调用的函数可以考虑使用内敛

Dead code elimination/Branch elimination

编译器会将代码中一些无用的分支进行优化,分支判断,提高效率。例如下面一段代码由于a和b是常量,编译器也可以推导出Max(a,b),因此最终F函数为空
1
2
3
4
5
6
7
8
9
10
11
12
13
func Max(a, b int) int {
if a > b {
return a
}
return b
}

func F() {
const a, b = 100, 20
if Max(a, b) == b {
panic(b)
}
}

常用的编译器选项: go build -gcflags=”-lN” xxx.go

  • “-S”,编译时查看汇编代码
  • “-l”,关闭内敛优化
  • “-m”,打印编译优化的细节
  • “-l -N”,关闭所有的优化

Go runtime 介绍

  为了避开直接通过系统调用分配内存而导致的性能开销,通常会通过预分配、内存池等操作自主管理内存。golang由运行时runtime管理内存,完成初始化、分配、回收和释放操作。目前主流的内存管理器有glibc和tcmolloc,tcmolloc由Google开发,具有更好的性能,兼顾内存分配的速度和内存利用率。golang也是使用类似tcmolloc的方法进行内存管理。建议参考下面链接学习tcmalloc的原理,其内存管理的方法也是golang内存分配的方法。另外一个原因,golang自主管理也是为了更好的配合垃圾回收。
【1】.https://zhuanlan.zhihu.com/p/29216091
【2】.http://goog-perftools.sourceforge.net/doc/tcmalloc.html

What is the Go runtime?

The Go runtime is a collection of software components that provide essential services for Go programs, including memory management, garbage collection, scheduling, and low-level system interaction. The runtime is responsible for managing the execution of Go programs and for providing a consistent, predictable environment for Go code to run in.

At a high level, the Go runtime is responsible for several core tasks:

  • Memory management: The runtime manages the allocation and deallocation of memory used by Go programs, including the stack, heap, and other data structures.
  • Garbage collection: The runtime automatically identifies and frees memory that is no longer needed by a program, preventing memory leaks and other related issues.
  • Scheduling: The runtime manages the scheduling of Goroutines, the lightweight threads used by Go programs, to ensure that they are executed efficiently and fairly.
  • Low-level system interaction: The runtime provides an interface for Go programs to interact with low-level system resources, including system calls, I/O operations, and other low-level functionality.

The Go runtime is an essential component of the Go programming language, and it is responsible for many of the language’s unique features and capabilities. By providing a consistent, efficient environment for Go code to run in, the runtime enables developers to write high-performance, scalable software that can run on a wide range of platforms and architectures.

程序启动流程

  在golang中,可执行文件的入口函数并不是我们写的main函数,编译器在编译go代码时会插入一段起引导作用的汇编代码,它引导程序进行命令行参数、运行时的初始化,例如内存分配器初始化、垃圾回收器初始化、协程调度器的初始化。golang引导初始化之后就会进入用户逻辑,因为存在特殊的init函数,main函数也不是程序最开始执行的函数。

  golang可执行程序由于运行时runtime的存在,其启动过程还是非常复杂的,这里通过gdb调试工具简单查看其启动流程:

  1. 找一个golang编译的可执行程序test,info file查看其入口地址:gdb test,info files
    (gdb) info files
    Symbols from “/home/terse/code/go/src/learn_golang/test_init/main”.
    Local exec file:
    /home/terse/code/go/src/learn_golang/test_init/main’,
    file type elf64-x86-64.
    Entry point: 0x452110
    …..
  2. 利用断点信息找到目标文件信息:
    (gdb) b *0x452110
    Breakpoint 1 at 0x452110: file /usr/local/go/src/runtime/rt0_linux_amd64.s, line 8.
  3. 依次找到对应的文件对应的行数,设置断点,调到指定的行,查看具体的内容:
    (gdb) b _rt0_amd64
    (gdb) b b runtime.rt0_go
    至此,由汇编代码针对特定平台实现的引导过程就全部完成了,后续的代码都是用Go实现的。分别实现命令行参数初始化,内存分配器初始化、垃圾回收器初始化、协程调度器的初始化等功能。
    1
    2
    3
    4
    5
    6
    7
    CALL	runtime·args(SB)
    CALL runtime·osinit(SB)
    CALL runtime·schedinit(SB)

    CALL runtime·newproc(SB)

    CALL runtime·mstart(SB)

特殊的init函数

  1. init函数先于main函数自动执行,不能被其他函数调用
  2. init函数没有输入参数、没有返回值
  3. 每个包可以含有多个同名的init函数,每个源文件也可以有多个同名的init函数
  4. 执行顺序 变量初始化 > init函数 > main函数。在复杂项目中,初始化的顺序如下:
    • 先初始化import包的变量,然后先初始化import的包中的init函数,,再初始化main包变量,最后执行main包的init函数
    • 从上到下初始化导入的包(执行init函数),遇到依赖关系,先初始化没有依赖的包
    • 从上到下初始化导入包中的变量,遇到依赖,先执行没有依赖的变量初始化
    • main包本身变量的初始化,main包本身的init函数
    • 同一个包中不同源文件的初始化是按照源文件名称的字典序

  

程序bootstrap过程

如上图所示,Go程序启动大致分为一下一个部分:

  • 参数处理,runtime·args(SB)
  • 操作系统初始化,runtime·osinit(SB)
  • 调度器初始化,runtime·schedinit(SB)
  • 运行runtime.main函数,装载用户main函数并运行,runtime.main()
    参数处理和osinit逻辑比较简单,代码也较少,这里主要记录下调度器初始化和runtime.main函数两个部分

runtime·schedinit

schedinit内容比较多,主要包含:

  • 栈初始化 stackinit()
  • 堆初始化 mallocinit()
  • gc初始化 gcinit()
  • 初始化resize allp []*p procresize()

stack

stackinit() 核心代码用于初始化全局的stackpool和stackLarge两个结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var stackpool [_NumStackOrders]struct {
item stackpoolItem
_ [cpu.CacheLinePadSize - unsafe.Sizeof(stackpoolItem{})%cpu.CacheLinePadSize]byte
}

//go:notinheap
type stackpoolItem struct {
mu mutex
span mSpanList
}

// Global pool of large stack spans.
var stackLarge struct {
lock mutex
free [heapAddrBits - pageShift]mSpanList // free lists by log_2(s.npages)
}

func stackinit() {
if _StackCacheSize&_PageMask != 0 {
throw("cache size must be a multiple of page size")
}
for i := range stackpool {
stackpool[i].item.span.init()
lockInit(&stackpool[i].item.mu, lockRankStackpool)
}
for i := range stackLarge.free {
stackLarge.free[i].init()
lockInit(&stackLarge.lock, lockRankStackLarge)
}
}

newproc 需要一个初始的stack

1
2
3
4
5
6
if gp.stack.lo == 0 {
// Stack was deallocated in gfput or just above. Allocate a new one.
systemstack(func() {
gp.stack = stackalloc(startingStackSize)
})
gp.stackguard0 = gp.stack.lo + _StackGuard

goroutine 运行时需要把stack 地址传给m

runtime.main

内存分配和管理策略mallocgc

垃圾回收garbage collector

程序并发Goroutine调度

Go 可测试编程、单元测试和性能优化

  Golang非常注重工程化,提供了非常好用单元测试、性能测试(benchmark)和调优工具(pprof),它们对提高代码的质量和服务的性能非常有帮助。参考链接中通过一段http代码非常详细的介绍了golang程序优化的步骤和方便之处。实际工作中,我们很难每次都对代码都有那么高的要求,但是能使用一些工具对程序进行优化程序性能也是golang程序员必备的技能。
dave它通过几个case非常清晰的介绍了golang性能分析与优化的技术,非常值得学习。https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html

  • testing 标准库
  • go test 测试工具
  • go tool pprof 分析 profile数据

单元测试,测试正确性

  1. 为了测试某个文件中的某个函数的性能,在相同目录下定义xxx_test.go文件,使用go build命令编译程序时会忽略测试文件

  2. 在测试文件中定义测试某函数的代码,以TestXxxx方式命名,例如TestAdd

  3. 在相同目录下运行 go test -v 即可观察代码的测试结果

     func TestAdd(t *testing.T) {
         if add(1, 3) != 4 {
             t.FailNow()
         }
     }
    

性能测试,benchmark

  1. 单元测试,测试程序的正确性。benchmark 用户测试代码的效率,执行的时间
  2. benchmark测试以BenchMark开头,例如BenchmarkAdd
  3. 运行 go test -v -bench=. 程序会运行到一定的测试,直到有比较准备的测试结果
    func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
    _ = add(1, 2)
    }
    }

    BenchmarkAdd-4 2000000000 0.26 ns/op

pprof性能分析

  1. 除了使用使用testing进行单元测试和benchanmark性能测试,golang能非常方便捕获或者监控程序运行状态数据,它包括cpu、内存、和阻塞等,并且非常的直观和易于分析。
  2. 有两种捕获方式: a、在测试时输出并保存相关数据;b、在运行阶段,在线采集,通过web接口获得实时数据。
  3. Benchamark时输出profile数据:go test -v -bench=. -memprofile=mem.out -cpuprofile=cpu.out
  4. 使用go tool pprof xxx.test mem.out 进行交互式查看,例如top5。同理,可以分析其它profile文件。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
(pprof) top5
Showing nodes accounting for 1994.93MB, 63.62% of 3135.71MB total
Dropped 28 nodes (cum <= 15.68MB)
Showing top 5 nodes out of 46
flat flat% sum% cum cum%
475.10MB 15.15% 15.15% 475.10MB 15.15% regexp/syntax.(*compiler).inst
455.58MB 14.53% 29.68% 455.58MB 14.53% regexp.progMachine
421.55MB 13.44% 43.12% 421.55MB 13.44% regexp/syntax.(*parser).newRegexp
328.61MB 10.48% 53.60% 328.61MB 10.48% regexp.onePassCopy
314.09MB 10.02% 63.62% 314.09MB 10.02% net/http/httptest.cloneHeader

- flat:仅当前函数,不包括它调用的其它函数
- cum: 当前函数调用堆栈的累计
- sum: 列表前几行所占百分比的总和

实际操作

Go实践:Goroutine同步方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
package main

import (
"context"
"fmt"
"sync"
"time"
)

//sync package
func sync1() {
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1) //设置协程等待的个数
go func(x int) {
defer func() {
wg.Done()
}()
fmt.Println("I'm", x)
}(i)
}
wg.Wait()
}

//chan
func sync2() {
chanSync := make([]chan bool, 10)
for i := 0; i < 10; i++ {
chanSync[i] = make(chan bool)
go func(x int, ch chan bool) {
fmt.Println("I'm ", x)
ch <- true
}(i, chanSync[i])
}

for _, ch := range chanSync {
<-ch
}
}

//context
func sync3() {
ctx, cancelFunc := context.WithCancel(context.Background())
defer cancelFunc()

for i := 0; i < 10; i++ {
go func(ctx context.Context, i int) {
for {
select {
case <-ctx.Done():
fmt.Println(ctx.Err(), i)
return
case <-time.After(2 * time.Second):
fmt.Println("time out", i)
return
}
}
}(ctx, i)
}
time.Sleep(5 * time.Second)
}

func main() {
sync1()
sync2()
sync3()
time.Sleep(10 * time.Second)
}

Go实践:生产者、消费者模型,并行计算累加求和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
package main

import (
"math/rand"
"sync"
"sync/atomic"

"fmt"
)

var total int32 = 100000

var producerLimit int32 = 3

var consumerLimit int32 = 4

var Q chan int32
var SumQ chan int32

var AtomicSum int32 = 0

func init() {
Q = make(chan int32, 10)
SumQ = make(chan int32)
}

func produce() {
a := total / producerLimit
b := total % producerLimit
var wg sync.WaitGroup
for i := 0; i < int(producerLimit); i++ {
batch := a
if i < int(b) {
batch += 1
}
wg.Add(1)
go func(x int32) {
defer wg.Done()
for j := 0; j < int(x); j++ {
num := rand.Intn(10)
atomic.AddInt32(&AtomicSum, int32(num))
Q <- int32(num)
}
}(batch)
}
go func() {
wg.Wait()
close(Q)
}()
}

func consumer() int32 {
var wg sync.WaitGroup
for i := 0; i < int(consumerLimit); i++ {
wg.Add(1)
go func() {
defer wg.Done()
var batchSum int32 = 0
for num := range Q {
batchSum += num
}
SumQ <- batchSum
}()
}

go func() {
wg.Wait()
close(SumQ)
}()

var ans int32 = 0
for sum := range SumQ {
ans += sum
}
return ans
}

func main() {
produce()
fmt.Printf("%d,%d\n", consumer(), atomic.LoadInt32(&AtomicSum))
}

Go 实践:interface/base/derive

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package main

import "fmt"

type service interface {
foo1()
foo2()
}

type baseService struct {
name string
}

func NewBaseService(name string) *baseService {
b := baseService{}
b.name = name
return &b
}

func (b *baseService) foo1() {
fmt.Println(b.name)
}

func (b *baseService) foo2() {
fmt.Println(b.name)
}

type AService struct {
*baseService
name string
}

func NewAService(name string, b *baseService) *AService {
s := AService{}
s.baseService = b
s.name = name
return &s
}

func (a *AService) foo1() {
fmt.Println(a.name)
}

func foo(s service) {
s.foo1()
s.foo2()
}

func main() {
b := NewBaseService("baseService")
s := NewAService("AService", b)
foo(s)
}

Go实践:设计模式的实现

https://refactoringguru.cn/design-patterns/chain-of-responsibility/go/example

Go 1.12 压测后rss内存一直无法释放问题

包和库(package)

参考

速查导航

阅读时间: 50 分钟 | 难度: ⭐⭐⭐⭐⭐ | 面试频率: 极高

核心考点速查:


本文深入剖析 Redis 的核心原理、数据结构底层实现、电商场景最佳实践。包含 Hash ziplist 字节级内存分析、渐进式 rehash 机制、缓存一致性方案等。

📋 目录导航

一、基础篇

  1. Redis 核心特性与使用场景
  2. 五种数据类型与底层实现
  3. 常用命令与时间复杂度

二、数据结构深度剖析

  1. Hash 底层实现详解(⭐ 核心重点)
    • ziplist 压缩列表(字节级分析)
    • hashtable 哈希表(渐进式 rehash)
    • 实战案例:{name: "iPhone", price: 5999}
  2. String/SDS 实现
  3. List 实现(ziplist/linkedlist/quicklist)
  4. Set 实现(intset/hashtable)
  5. ZSet 实现(ziplist/skiplist)

三、缓存设计与实践

  1. 缓存使用场景(计数器、限流、队列)
  2. 缓存与DB一致性
  3. 缓存异常处理(雪崩、穿透、击穿)
  4. 本地缓存 vs 远程缓存

四、高级特性

  1. 内存管理(淘汰策略、过期键删除)
  2. 持久化机制(RDB、AOF)
  3. 分布式方案(主从、哨兵、集群)
  4. Lua 脚本

五、性能优化

  1. 为什么 Redis 这么快
  2. 单线程模型
  3. 性能调优实践
  4. 大 Key、热 Key 问题

六、实战案例

  1. 分布式锁实现
  2. BloomFilter 应用
  3. 秒杀系统设计
  4. 排行榜实现

Redis 核心特性与使用场景

核心特性总览

特性 说明 优势
内存存储 数据全部在内存中 极快的读写速度(10w+ QPS)
单线程模型 命令串行执行 避免锁竞争,简化并发
多种数据结构 String/Hash/List/Set/ZSet 覆盖多种业务场景
持久化支持 RDB + AOF 数据不丢失
主从复制 读写分离 高可用、高并发
集群模式 分片存储 横向扩展能力

典型应用场景

缓存加速(上游数据/数据库/外部聚合数据)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 查询商品详情
func GetProduct(productID string) (*Product, error) {
// 1. 先查缓存
key := "product:" + productID
data, err := rdb.Get(ctx, key).Result()
if err == nil {
return json.Unmarshal(data) // 缓存命中
}

// 2. 缓存未命中,查询 DB
product, err := db.QueryProduct(productID)
if err != nil {
return nil, err
}

// 3. 回写缓存(设置过期时间)
rdb.Set(ctx, key, json.Marshal(product), 1*time.Hour)
return product, nil
}
// 存储 JSON 对象
rdb.Set(ctx, "user:123", `{"name":"alice","age":25}`, 0)

// 存储计数器
rdb.Set(ctx, "visit:count", 0, 0)

性能提升:DB 查询 100ms → Redis 查询 1ms(提升 100 倍)

计数器场景(incr + int)

  1. 库存扣减(最核心场景)这是计数器最广泛的应用。在秒杀或大促期间,直接操作数据库库存会瞬间拖垮磁盘 I/O。
  • 逻辑:
  1. 活动开始前,将商品库存同步到 Redis:SET stock:sku:1001 50。

  2. 用户下单时,执行:DECR stock:sku:1001。

  3. 如果返回值 >= 0,放行去写订单;如果返回值 < 0,立即返回“已售罄”。

  4. 优势: 内存级操作,单机可支撑 10w+ TPS,彻底杜绝超卖。

  5. 高频限流(防刷与风控)

  • 逻辑:
    1. Key 为 limit:user:{uid}:api:{api_name}。
    2. 每次请求执行 INCR。
    3. 如果是首次调用(返回 1),设置过期时间 EXPIRE 60(即 1 分钟限流)。
    4. 如果返回值超过阈值(如 100),直接拒绝请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const bookNormalStockScript = `
local key = KEYS[1]
local book_num = tonumber(ARGV[1])
local platform = tonumber(ARGV[2]) or 1

local normal_stock = tonumber(redis.call("HGET", key, "0")) or 0
if book_num > normal_stock then
return -1
end
redis.call("hincrby", key, "0", -book_num)
redis.call("HINCRBY", key, "booking", book_num)
end

return normal_stock - book_num
`

ZSet 使用场景

List 使用场景

  1. 在秒杀瞬间,由于流量远超数据库承受能力,先将请求“队列化”。
  • 操作: 用户的请求到达后,通过 LPUSH 压入一个排队列 seckill:queue:product_id。
  • 处理: 后台开启固定数量的 Worker 进程,通过 RPOP 或 BRPOP 获取请求进行后续减库存操作。
  • 优势: 削峰填谷,保护后端核心系统
  1. 浏览最近查看的商品(Top 10) 如果你在商城详情页下方展示“最近查看”,你可能需要先读取前 10 个显示给用户

    1
    2
    3
    4
    5
    6
    7
    8
    // 1. 先读取前 10 个给前端展示,不删除
    products, _ := rdb.LRange(ctx, "user:history:123", 0, 9).Result()

    // 2. 如果用户点开了某个商品,你再往里加,并裁剪
    pipe := rdb.Pipeline()
    pipe.LPush(ctx, "user:history:123", "new_product_id")
    pipe.LTrim(ctx, "user:history:123", 0, 9) // 只保留最新的10个
    pipe.Exec(ctx)
  2. 缓存数据(db,service) 的数据,提高访问效率

    • 缓存容量评估
    • 缓存过期机制,时间
    • 缓存miss,溯源和监控
    • 缓存雪崩,大面积key失效DB保护。
    • 缓存击穿:热key击穿保护
    • 缓存穿透:无效key击穿DB保护
    • 缓存更新和一致性问题
    • 缓存热key和大key问题
  3. 限流和计数。lua脚本。(int,incr,lua)

  4. 延时队列

    • 使用 ZSET+ 定时轮询的方式实现延时队列机制,任务集合记为 taskGroupKey
    • 生成任务以 当前时间戳 与 延时时间 相加后得到任务真正的触发时间,记为 time1,任务的 uuid 即为 taskid,当前时间戳记为 curTime
    • 使用 ZADD taskGroupKey time1 taskid 将任务写入 ZSET
    • 主逻辑不断以轮询方式 ZRANGE taskGroupKey curTime MAXTIME withscores 获取 [curTime,MAXTIME) 之间的任务,记为已经到期的延时任务(集)
    • 处理延时任务,处理完成后删除即可
    • 保存当前时间戳 curTime,作为下一次轮询时的 ZRANGE 指令的范围起点
    • https://github.com/bitleak/lmstfy
  5. 消息队列

    • redis 支持 List 数据结构,有时也会充当消息队列。使用生产者:LPUSH;消费者:RBPOP 或 RPOP 模拟队列
  6. 分布式锁:https://juejin.cn/post/6936956908007850014

  7. bloomfilter: https://juejin.cn/post/6844903862072000526

    $m = -\frac{nln(p)}{(ln2)^2}$

    $k=\frac{m}{n}ln(2)$

    1
    2
    3
    4
    n 是预期插入的元素数量(数据规模),例如 20,000,000。
    p 是预期的误判率,例如 0.001。
    m 是位数组的大小。
    k 是哈希函数的数量。

二、数据结构深度剖析

深入理解 Redis 五种数据类型的底层实现原理,掌握内存优化技巧。

参考资料


Hash 底层实现原理详解

Redis Hash 采用两种编码方式,根据数据特征自动选择:

1. 编码选择策略

graph LR
    A[创建Hash] --> B{元素数<=512 且 值长度<=64B?}
    B -->|是| C[ziplist 压缩列表]
    B -->|否| D[hashtable 哈希表]
    C -->|超过阈值| D
    
    style C fill:#e8f5e9
    style D fill:#e1f5fe

配置参数(redis.conf):

1
2
hash-max-ziplist-entries 512   # 最大元素个数
hash-max-ziplist-value 64 # 单个value最大长度(字节)

2. ziplist(压缩列表)- 内存优化

适用场景:小对象存储(如商品详情、Session、小型 List)

ziplist 是 Redis 为节省内存设计的紧凑型数据结构,所有数据存储在一块连续的内存中。

2.1 整体结构
1
2
3
4
+----------+----------+--------+---------+---------+-----+---------+--------+
| zlbytes | zltail | zllen | entry1 | entry2 | ... | entryN | zlend |
+----------+----------+--------+---------+---------+-----+---------+--------+
4字节 4字节 2字节 变长 变长 变长 1字节
字段 长度 说明
zlbytes 4 字节 整个 ziplist 占用的总字节数(包括 zlbytes 自身)
zltail 4 字节 到尾节点的偏移量(用于快速定位尾部,支持反向遍历)
zllen 2 字节 节点数量,最大 65535;超过则需遍历整个列表计数
entry 变长 实际数据节点,每个节点长度不固定
zlend 1 字节 固定为 0xFF,标记 ziplist 结束
2.2 Entry 节点详细结构(三部分)

每个 entry 由三部分组成:

1
2
3
4
+----------+----------+----------+
| prevlen | encoding | content |
+----------+----------+----------+
1或5字节 1-5字节 变长
Part 1: prevlen(前一节点长度)

记录前一个节点的长度,用于从后向前遍历

1
2
3
4
5
6
// 编码规则
if (前一节点长度 < 254 字节) {
prevlen = 1 字节 // 直接存储长度值
} else {
prevlen = 5 字节 // 第1字节=0xFE,后4字节存实际长度
}

示例

  • 前一节点 10 字节 → prevlen = 0x0A(1字节)
  • 前一节点 300 字节 → prevlen = 0xFE 0x00 0x00 0x01 0x2C(5字节)
Part 2: encoding(编码类型)

记录 content 的数据类型和长度,Redis 使用变长编码节省空间。

字符串编码(前 2 位标识):

编码格式 说明 长度范围
00pppppp 1字节,后6位存长度 0 - 63 字节
01pppppp qqqqqqqq 2字节,14位存长度 64 - 16383 字节
10______ [4字节] 5字节,后续4字节存长度 > 16383 字节

整数编码(前 2 位为 11):

编码值 说明 数据长度
11000000 int16_t 2 字节
11010000 int32_t 4 字节
11100000 int64_t 8 字节
11110000 24 位整数 3 字节
11111110 8 位整数 1 字节
1111xxxx 0-12 的整数直接编码在后4位 0 字节(无 content)
Part 3: content(实际数据)

存储实际的数据内容,根据 encoding 字段解析:

  • 字符串:原始字节数组
  • 整数:二进制整数(小端序)
2.3 实际内存布局示例

存储 Hash:{name: "iPhone", price: 5999}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
偏移 | 字段        | 值                  | 说明
-----|------------|---------------------|------------------
0-3 | zlbytes | 0x0000003F (63) | 总大小 63 字节
4-7 | zltail | 0x00000035 (53) | 尾节点偏移 53
8-9 | zllen | 0x0004 (4) | 4个节点(2 field + 2 value)

10 | prevlen | 0x00 | 第一个节点
11 | encoding | 0x04 | 字符串长度 4
12-15| content | "name" | field
| (共 6 字节)

16 | prevlen | 0x06 | 前一节点 6 字节
17 | encoding | 0x06 | 字符串长度 6
18-23| content | "iPhone" | value
| (共 8 字节)

24 | prevlen | 0x08 | 前一节点 8 字节
25 | encoding | 0x05 | 字符串长度 5
26-30| content | "price" | field
| (共 7 字节)

31 | prevlen | 0x07 | 前一节点 7 字节
32 | encoding | 0xC0 | int16_t
33-34| content | 0x176F (5999) | value(整数优化)
| (共 4 字节)

35 | zlend | 0xFF | 结束标记

内存计算:10(头)+ 6 + 8 + 7 + 4 + 1(尾)= 36 字节

2.4 遍历机制

正向遍历(从头到尾)

1
2
3
4
5
6
ptr = ziplist + 10;  // 跳过头部
while (*ptr != 0xFF) {
// 解析 encoding,计算节点长度
node_len = prevlen_size + encoding_size + content_size;
ptr += node_len; // 跳到下一个节点
}

反向遍历(从尾到头)

1
2
3
4
5
ptr = ziplist + zltail;  // 直接定位尾节点
while (ptr > ziplist + 10) {
prevlen = parse_prevlen(ptr); // 读取 prevlen
ptr -= prevlen; // 跳到前一个节点
}
2.5 连锁更新问题(Cascade Update)

问题:插入/删除节点可能导致后续节点的 prevlen 字段长度变化。

场景示例

1
2
3
4
5
6
7
8
9
初始状态:[253B] [253B] [253B]
每个节点的 prevlen 占 1 字节

插入大节点:[253B] [260B] [???] [???]

前一节点变为 260 字节 (>254)
当前节点的 prevlen 需从 1 字节扩展为 5 字节
当前节点长度从 253 → 257 字节
下一个节点的 prevlen 也需扩展...

影响

  • 最坏时间复杂度:O(n²)(所有节点连锁更新)
  • 内存重分配:连续的 realloc 操作
  • 实际概率:极低(需要大量节点刚好在 254 字节边界)

Redis 优化

  • 预先检查是否会触发连锁更新
  • 一次性分配足够内存,减少 realloc 次数
2.6 编码示例(Go 代码)
1
2
3
4
5
6
7
8
9
10
11
12
13
// 示例:字符串 "hello" 的 encoding
// 长度 5 < 63,使用 00pppppp 格式
encoding := 0b00000101 // 0x05

// 示例:整数 100 的 encoding
// 范围在 int16 内,使用 11000000
encoding := 0b11000000 // 0xC0
content := []byte{0x64, 0x00} // 小端序 100

// 示例:整数 12 的 encoding
// 0-12 直接编码在 encoding 中
encoding := 0b11111100 // 0xFC,后4位 1100 = 12
// 无需 content 字段!
2.7 优势与限制

✅ 优势

  • 内存高效:无指针开销,紧凑存储
    • 对比链表:每个节点省 16 字节(prev + next 指针)
  • CPU 缓存友好:连续内存,预读优化
  • 智能编码:整数压缩、变长存储
  • 双向遍历:通过 zltailprevlen 实现

❌ 限制

  • 查找慢:O(n) 顺序遍历,不适合大数据
  • 连锁更新:最坏 O(n²)(实际很少发生)
  • 内存重分配:插入/删除需 realloc
  • 默认阈值
    • Hash: 512 entries
    • List: 512 entries
    • ZSet: 128 entries
2.8 应用场景总结
数据类型 使用 ziplist 条件 典型场景
Hash entries ≤ 512, value ≤ 64B 商品基础信息、用户 Session
List entries ≤ 512 消息队列、浏览历史
ZSet entries ≤ 128, member ≤ 64B 小型排行榜、优先队列

监控命令

1
2
3
4
5
6
7
8
9
10
11
# 查看编码类型
redis> OBJECT ENCODING mykey
"ziplist"

# 查看内存占用
redis> MEMORY USAGE mykey
(integer) 184

# 查看 ziplist 详细信息(DEBUG 命令)
redis> DEBUG OBJECT mykey
Value at:0x7f8a9c0a0a00 refcount:1 encoding:ziplist serializedlength:48

3. hashtable(哈希表)- 性能优化

适用场景:大量字段或需要快速查找

核心结构:经典拉链法哈希表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Redis 字典结构(dict)
typedef struct dict {
dictht ht[2]; // 两个哈希表,ht[1] 用于 rehash
long rehashidx; // rehash 进度,-1 表示未进行
} dict;

// 哈希表结构(dictht)
typedef struct dictht {
dictEntry **table; // 哈希表数组(指针数组)
unsigned long size; // 大小(2的幂次方)
unsigned long used; // 已有节点数
} dictht;

// 哈希节点(dictEntry)
typedef struct dictEntry {
void *key; // 键
void *value; // 值
struct dictEntry *next; // 链表指针(解决冲突)
} dictEntry;

可视化结构:详见 redis-hashtable.mmd

1
2
3
4
5
6
7
8
dict
├── ht[0] (主哈希表)
│ ├── table[0] → NULL
│ ├── table[1] → entry(name:iPhone) → NULL
│ ├── table[2] → entry(price:5999) → entry(stock:100) → NULL (拉链法)
│ └── ...
└── ht[1] (rehash用)
└── NULL (未使用)

4. 渐进式 Rehash 机制(核心)

触发条件

1
2
3
4
5
6
// 扩容
1. 负载因子 = used/size >= 1 (无BGSAVE/BGREWRITEAOF时)
2. 负载因子 >= 5 (强制扩容)

// 缩容
负载因子 < 0.1

渐进式 Rehash 流程

  1. ht[1] 分配空间(扩容为 used * 2 的最小 2^n)
  2. rehashidx = 0 开始迁移
  3. 每次增删改查操作时,顺带迁移 ht[0].table[rehashidx] 的所有数据到 ht[1]
  4. 全部迁移完成后,释放 ht[0],将 ht[1] 设为 ht[0]

可视化流程:详见 redis-rehash-process.mmd

Rehash 期间的操作

1
2
3
查找:先查 ht[0],未找到再查 ht[1]
新增:直接写入 ht[1](新数据不进旧表)
删除/更新:在 ht[0] 或 ht[1] 中找到后操作

为什么用渐进式?

  • 避免一次性 rehash 大量数据导致 Redis 阻塞(毫秒级→微秒级)
  • 分摊到每次操作中,对单次请求影响极小

5. 哈希函数

1
2
3
// Redis 使用 MurmurHash2(速度快、分布均匀)
hash = MurmurHash2(key, len);
index = hash & dict->ht[x].sizemask; // 位运算代替取模,性能优

6. 性能对比

操作 ziplist hashtable
HSET O(n) O(1) 平均
HGET O(n) O(1) 平均
HDEL O(n) O(1) 平均
HGETALL O(n) O(n)
内存占用 (无指针) (指针+空间换时间)
CPU缓存 友好(连续) 一般(随机访问)
适用场景 < 512字段小对象 大量字段快速查找

7. 电商场景最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ✅ 推荐:商品详情缓存(字段适中,频繁部分更新)
rdb.HSet(ctx, "product:1001", map[string]interface{}{
"name": "iPhone 15",
"price": "5999",
"stock": "100",
"seller_id": "10086",
})

// 只更新库存,无需读取整个对象
rdb.HIncrBy(ctx, "product:1001", "stock", -1)

// ✅ 推荐:用户 Session 存储
rdb.HSet(ctx, "session:abc123", "uid", "88888")
rdb.HSet(ctx, "session:abc123", "role", "buyer")
rdb.Expire(ctx, "session:abc123", 30*time.Minute)

// ❌ 避免:大 Hash(> 10000 字段)
// 问题:HGETALL 阻塞、集群数据倾斜、持久化慢
// 解决:拆分为多个小 Hash,如 product:1001:base、product:1001:detail

8. Hash vs String(JSON)

维度 Hash String (JSON)
内存 ziplist 模式更省 JSON 序列化开销大
部分更新 HSET field ❌ 需整体 GET→改→SET
序列化 无需序列化 需 JSON 编解码(CPU开销)
查询灵活性 HMGET 精确取字段 GET 整体取出
适用场景 频繁部分字段更新 整体读写、复杂嵌套结构

选择建议

  • 用 Hash:对象字段 < 1000,需要单独读写某些字段(如库存)
  • 用 String:对象结构复杂嵌套(JSON),整体读写居多

9. 监控与调优

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1. 查找大 key
redis-cli --bigkeys

# 2. 查看 Hash 编码
redis-cli> OBJECT ENCODING product:1001
"ziplist" # 或 "hashtable"

# 3. 查看内存占用
redis-cli> MEMORY USAGE product:1001
(integer) 184

# 4. 调整编码阈值(根据业务调整)
CONFIG SET hash-max-ziplist-entries 1024
CONFIG SET hash-max-ziplist-value 128

String 底层实现

String/SDS 核心结构

Redis String 实际是 SDS(Simple Dynamic String),而非 C 字符串。

SDS 结构

1
2
3
4
5
struct sdshdr {
int len; // 已使用长度
int free; // 剩余可用空间
char buf[]; // 实际数据
};

优势对比

特性 C 字符串 SDS
获取长度 O(n) 遍历 O(1) 直接读 len
缓冲区溢出 不检查,易溢出 自动扩容,安全
内存重分配 每次都需要 空间预分配 + 惰性释放
二进制安全 否(遇 \0 结束) 是(记录长度)

编码类型

String 有 3 种编码:

1
2
3
4
5
6
7
8
9
10
11
# 1. int 编码(整数)
SET count 100
OBJECT ENCODING count # "int"

# 2. embstr 编码(短字符串 ≤ 44 字节)
SET short "hello"
OBJECT ENCODING short # "embstr"

# 3. raw 编码(长字符串 > 44 字节)
SET long "very long string..."
OBJECT ENCODING long # "raw"

embstr vs raw

  • embstr:SDS 和 redisObject 在连续内存(一次分配)
  • raw:SDS 和 redisObject 分开分配(两次分配)

应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 计数器
rdb.Incr(ctx, "page:views")

// 2. 分布式 ID 生成
id := rdb.Incr(ctx, "order:id").Val()

// 3. 限流(固定窗口)
count := rdb.Incr(ctx, "limit:user:123").Val()
if count == 1 {
rdb.Expire(ctx, "limit:user:123", 60*time.Second)
}
if count > 100 {
return ErrRateLimited
}

// 4. Session 存储
rdb.Set(ctx, "session:token_abc", userJSON, 30*time.Minute)

List 底层实现

List 在 Redis 3.2 之前使用 ziplistlinkedlist,3.2+ 统一使用 quicklist

quicklist 结构

1
2
3
4
5
quicklist = ziplist 链表

[ziplist1] ⇄ [ziplist2] ⇄ [ziplist3] ⇄ [ziplist4]
↓ ↓ ↓ ↓
[a,b,c] [d,e,f] [g,h,i] [j,k,l]

设计思想

  • ziplist:内存紧凑,但大量数据时性能差
  • linkedlist:插入快,但内存碎片多
  • quicklist:折中方案,每个节点是一个小 ziplist

配置参数

1
2
3
4
5
# 每个 ziplist 的最大大小(字节)
list-max-ziplist-size -2 # -2 表示 8KB

# 两端不压缩的节点数(LZF 压缩)
list-compress-depth 0 # 0 表示不压缩

应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 1. 消息队列(FIFO)
rdb.LPush(ctx, "queue:tasks", task1, task2) // 生产者
task := rdb.RPop(ctx, "queue:tasks") // 消费者

// 2. 最近浏览记录(LIFO)
rdb.LPush(ctx, "user:123:history", productID)
rdb.LTrim(ctx, "user:123:history", 0, 9) // 只保留最新 10 条

// 3. 时间线(Timeline)
rdb.LPush(ctx, "timeline:user:123", postID)
posts := rdb.LRange(ctx, "timeline:user:123", 0, 19) // 最新 20 条

// 4. 阻塞队列(BRPOP)
task := rdb.BRPop(ctx, 5*time.Second, "queue:tasks") // 阻塞等待

性能对比

操作 时间复杂度 说明
LPUSH/RPUSH O(1) 头尾插入
LPOP/RPOP O(1) 头尾弹出
LINDEX O(n) 按索引查询
LRANGE O(n) 范围查询
LINSERT O(n) 中间插入

Set 底层实现

Set 使用 intsethashtable 编码。

intset(整数集合)

适用条件

  • 所有元素都是整数
  • 元素数量 ≤ 512(set-max-intset-entries

结构

1
2
3
4
5
typedef struct intset {
uint32_t encoding; // INTSET_ENC_INT16/INT32/INT64
uint32_t length; // 元素数量
int8_t contents[]; // 有序数组
};

特点

  • ✅ 有序存储,二分查找 O(log n)
  • ✅ 内存紧凑
  • ❌ 插入/删除需移动元素 O(n)

应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 标签系统
rdb.SAdd(ctx, "user:123:tags", "VIP", "男性", "90后")
tags := rdb.SMembers(ctx, "user:123:tags")

// 2. 共同好友
rdb.SAdd(ctx, "user:123:friends", "456", "789")
rdb.SAdd(ctx, "user:456:friends", "123", "789")
common := rdb.SInter(ctx, "user:123:friends", "user:456:friends") // [789]

// 3. 去重(抽奖池)
rdb.SAdd(ctx, "lottery:pool", userIDs...)
winner := rdb.SPop(ctx, "lottery:pool") // 随机抽取

// 4. 点赞用户列表
rdb.SAdd(ctx, "post:1001:likes", "user:123")
isLiked := rdb.SIsMember(ctx, "post:1001:likes", "user:123")
likeCount := rdb.SCard(ctx, "post:1001:likes")

ZSet 底层实现

ZSet 使用 ziplistskiplist + hashtable 编码。

ziplist 编码

条件

  • 元素数量 ≤ 128(zset-max-ziplist-entries
  • 单个元素 ≤ 64 字节(zset-max-ziplist-value

存储格式

1
2
[member1, score1, member2, score2, ...]
按 score 有序存储

skiplist + hashtable 编码

为什么用两种结构?

  • skiplist:按 score 有序,范围查询 O(log n)
  • hashtable:按 member 查找,O(1) 获取 score

skiplist 结构

1
2
3
4
Level 3:  1 --------------------------------> 100
Level 2: 1 -------> 50 -------------------> 100
Level 1: 1 --> 25 > 50 --> 75 ------------> 100
Level 0: 1 > 10 > 25 > 50 > 75 > 90 > 100

平均查找复杂度:O(log n)

应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 1. 排行榜
rdb.ZAdd(ctx, "rank:score", &redis.Z{Score: 5999, Member: "user:123"})
top10 := rdb.ZRevRange(ctx, "rank:score", 0, 9) // 前 10 名

// 2. 延时队列
rdb.ZAdd(ctx, "delay:queue", &redis.Z{
Score: float64(time.Now().Add(5*time.Minute).Unix()),
Member: taskID,
})
// 定时拉取到期任务
tasks := rdb.ZRangeByScore(ctx, "delay:queue", &redis.ZRangeBy{
Min: "0",
Max: strconv.Itoa(int(time.Now().Unix())),
})

// 3. 权重推荐
rdb.ZAdd(ctx, "recommend:user:123", &redis.Z{Score: 0.95, Member: "item:1001"})
recommended := rdb.ZRevRangeByScore(ctx, "recommend:user:123", &redis.ZRangeBy{
Min: "0.8",
Max: "+inf",
Count: 10,
})

// 4. 微信步数排行榜
rdb.ZAdd(ctx, "steps:2026-01-08", &redis.Z{Score: 10000, Member: "user:123"})
myRank := rdb.ZRevRank(ctx, "steps:2026-01-08", "user:123") // 我的排名

三、缓存设计与实践

掌握缓存使用模式、一致性方案、异常处理策略,构建高可用缓存系统。


缓存容量规划与内存管理

内存淘汰策略

1
2
3
4
5
6
7
8
- noeviction(默认策略):对于写请求不再提供服务,直接返回错误(DEL请求和部分特殊请求除外)
- allkeys-lru:从所有key中使用LRU算法进行淘汰
- volatile-lru:从设置了过期时间的key中使用LRU算法进行淘汰
- allkeys-random:从所有key中随机淘汰数据
- volatile-random:从设置了过期时间的key中随机淘汰
- volatile-ttl:在设置了过期时间的key中,根据key的过期时间进行淘汰,越早过期的越优先被淘汰
LFU算法是Redis4.0里面新加的一种淘汰策略。它的全称是Least Frequently Used

redis 内存淘汰策略解析

过期键删除策略

过期策略通常有以下三种:

  • 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  • 定期过期:每隔一定的时间,会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已过期的key。该策略是前两者的一个折中方案。通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得CPU和内存资源达到最优的平衡效果。
    (expires字典会保存所有设置了过期时间的key的过期时间数据,其中,key是指向键空间中的某个键的指针,value是该键的毫秒精度的UNIX时间戳表示的过期时间。键空间是指该Redis集群中保存的所有键。)

四、高级特性

掌握持久化、分布式架构、Lua 脚本等高级特性,构建生产级 Redis 系统。


持久化机制(RDB、AOF)

Redis 提供两种持久化方案:

  • **RDB (Redis DataBase)**:快照方式,紧凑的二进制数据
  • **AOF (Append Only File)**:追加日志方式,记录所有写操作

详细对比Redis持久化原理


本地缓存 vs 远程缓存

双buffer vs LRU/LFU

本地缓存的双缓冲机制和本地LRU(Least Recently Used)算法都是常见的缓存优化技术,它们具有不同的优点和缺点。

  1. 双缓冲机制:

    • 优点:
      • 提高并发性能:双缓冲机制使用两个缓冲区,一个用于读取数据,另一个用于写入数据。这样可以避免读写冲突,提高了并发性能。
      • 提高数据访问效率:由于读取操作不会直接访问主缓存,而是读取缓冲区的数据,因此可以更快地获取数据。
    • 缺点:
      • 内存开销增加:双缓冲机制需要维护两个缓冲区,这会增加内存开销。
      • 数据延迟:数据更新定时同步,有一定延时。
  2. 本地LRU算法:

    • 优点:
      • 数据访问效率高:LRU算法根据数据的访问顺序进行缓存替换,将最近最少使用的数据淘汰出缓存。这样可以保留最常用的数据,提高数据的访问效率。
      • 简单有效:LRU算法的实现相对简单,只需要维护一个访问顺序链表和一个哈希表即可。
    • 缺点:
      • 缓存命中率下降:如果数据的访问模式不符合LRU算法的假设,即最近访问的数据在未来也是最有可能被访问的,那么LRU算法的效果可能不理想,缓存命中率会下降。
      • 对于热点数据不敏感:LRU算法只考虑了最近的访问情况,对于热点数据(频繁访问的数据)可能无法有效地保留在缓存中。

综合来看,双缓冲机制适用于需要提高并发性能、批量更新等场景,但会增加内存开销。本地LRU算法适用于需要提高数据访问效率的场景,但对于访问模式不符合LRU假设的情况下,缓存命中率可能下降。在实际应用中,可以根据具体需求和场景选择适合的缓存优化技术。

缓存与DB一致性方案

一致性问题分析

当使用 Redis 缓存 DB 数据时,DB 数据会发生 UPDATE,如何考虑 Redis 和 DB 数据的一致性问题呢?

  • 通常来说,对于流量较小的业务来说,可以设置较小的expire time,可以将redis和db的不一致的时间控制在一定的范围内部
  • 对于缓存和db一致性要求较高的场合,通常采用的是先更新db,再删除或者更新redis,考虑到并发性和两个操作的原子性(删除或者更新可能会失败),可以增加重试机制(双删除),如果考虑主从延时,可以引入mq做延时双删
  • http://kaito-kidd.com/2021/09/08/how-to-keep-cache-and-consistency-of-db/

缓存更新方式 优缺点
缓存模式+TTL 业务代码只更新DB,不更新cache,设置较短的TTL(通常分钟级),依靠cache过期无法找到key时回源DB,热key过期可能回导致请求大量请求击穿到DB,需要使用分布式锁或者singleflight等方式避免这种问题
定时刷新模式 定时任务异步获取DB数据刷新到cache,读请求可不回源,需要考虑刷新时间和批量读写
写DB,写cache 在并发条件下,DB写操作顺序和cache操作不同保证顺序一致性,需要增加分布式锁等操作
写DB,删除cache 删除cache可能失败,需要增加重试,重试也可能失败,比较复杂的加个MQ补偿重试

思考:

  • 对一致性要求有多强?
  • TTL 设置的时长
  • 并发冲突可能性
  • 热key缓存击穿保护

缓存异常处理(雪崩、穿透、击穿)

三大缓存问题

  1. 缓存雪崩:大面积 Key 同时失效或删除,导致请求全部打到 DB
  2. 缓存穿透:查询不存在的 Key(恶意攻击),绕过缓存直击 DB
  3. 缓存击穿:热点 Key 失效瞬间,高并发请求击穿到 DB

详细方案缓存异常解决方案


五、性能优化

理解 Redis 高性能原理,掌握性能调优技巧,解决大Key/热Key问题。


为什么 Redis 这么快?

核心原因

  1. 完全基于内存:绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是 O(1);
  • 2、数据结构简单,对数据操作也简单,Redis 中的数据结构是专门进行设计的;
  • 3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  • 4、使用多路 I/O 复用模型,非阻塞 IO;
  • 5、自建 VM 机制:使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis 直接自己构建了 VM 机制,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。

详细分析单线程 Redis 为什么快?


单线程模型

为什么使用单线程?

参考Redis 单线程设计

优势

  • 避免上下文切换
  • 无需考虑锁问题
  • 实现简单清晰

劣势

  • 无法利用多核 CPU
  • 长耗时命令会阻塞

大Key和热Key问题

什么是大Key?

1
2
3
# 什么是大 Key?
- String: value > 10KB
- Hash/List/Set/ZSet: 元素数量 > 10000

分布式方案(主从、哨兵、集群)

架构演进路线

阶段 方案 特点 局限性
1 单机版 简单直接 单点故障、容量有限、并发有限
2 主从复制 读写分离、高可用 主从延迟、无自动故障转移
3 哨兵模式 (Sentinel) 自动故障转移 难以扩容、主库写入瓶颈
4 集群模式 (Cluster) 横向扩展、高可用 复杂度高、跨slot操作受限
5 Codis 中心化管理、易运维 需要额外组件(Zookeeper)

主从复制

特点

  • 主库(Master)负责写操作
  • 从库(Slave)负责读操作
  • 主库数据自动同步到从库

问题

  • 主从延迟导致数据不一致
  • 无自动故障恢复,需人工介入

哨兵模式 (Sentinel)

目标:解决主从复制的自动故障恢复问题

工作机制

  • 监控主从运行状态
  • 当 Master 故障时,通过 Raft 选举
  • Leader 哨兵选择优先级最高的 Slave 作为新 Master
  • 其他 Slave 从新 Master 同步数据

局限性

  • 难以扩容
  • 单机存储、读写能力受限
  • 所有 Redis 节点都有全量数据,内存冗余

Redis Cluster 集群模式

特点

  • 无中心架构,去中心化
  • 数据分片,每个节点存储部分数据(16384 个槽)
  • 通过路由找到对应节点
  • 支持横向和纵向扩展
  • 自动故障转移

优势

  • 哨兵的所有优点
  • 可动态扩容/缩容
  • 数据分布式存储

Codis

特点

  • 豌豆荚开源方案
  • 中心化管理(Zookeeper/Etcd)
  • Proxy 层路由
  • Dashboard 可视化管理

GitHubhttps://github.com/CodisLabs/codis

参考资料


Lua 脚本

为什么使用 Lua?

Redis 执行 Lua 脚本具有以下特性:

  1. 原子性:脚本执行期间不会执行其他脚本或命令
  2. 独占性:Redis 一旦开始执行 Lua 脚本,就会一直执行完该脚本
  3. 应用广泛:分布式锁、限流、秒杀等场景

使用注意事项

  • 使用 Lua 脚本实现原子性操作的 CAS,避免不同客户端先读 Redis 数据,经过计算后再写数据造成的并发问题
  • 前后多次请求的结果有依赖关系时,最好使用 Lua 脚本将多个请求整合为一个;但请求前后无依赖时,使用 pipeline 方式,比 Lua 脚本方便
  • 为了保证安全性,在 Lua 脚本中不要定义自己的全局变量,以免污染 Redis 内嵌的 Lua 环境。因为 Lua 脚本中你会使用一些预制的全局变量,比如说 redis.call()
  • 注意 Lua 脚本的时间复杂度,Redis 的单线程同样会阻塞在 Lua 脚本的执行中,Lua 脚本不要进行高耗时操作
  • Redis 要求单个 Lua 脚本操作的 key 必须在同一个 Redis 节点上,因此 Redis Cluster 方式需要设置 HashTag(实际中不太建议这样操作)

Redis 常用命令

连接命令

1
2
3
4
5
6
7
8
# 连接 Redis
redis-cli -h host -p port -a password

# 切换数据库
SELECT 0

# 测试连接
PING # 返回 PONG

基础命令

命令 说明 时间复杂度
SET key value [NX|XX] [EX seconds] 设置键值 O(1)
GET key 获取值 O(1)
DEL key [key ...] 删除键 O(N)
EXISTS key [key ...] 检查键是否存在 O(N)
TTL key 查询过期时间(秒) O(1)
EXPIRE key seconds 设置过期时间 O(1)
SCAN cursor [MATCH pattern] 扫描键(推荐) O(1)
KEYS pattern 模式匹配键(⚠️ 生产环境禁用) O(N)

⚠️ 生产环境注意

  • **禁用 KEYS ***:遍历所有键,时间复杂度 O(N),会阻塞 Redis
  • **推荐使用 SCAN**:渐进式遍历,不阻塞服务器

性能调优与监控

大Key和热Key问题

什么是大Key?

大 Key 危害

1
2
3
4
5
6
7
8
9
10
# 什么是大 Key?
- String: value > 10KB
- Hash/List/Set/ZSet: 元素数量 > 10000

# 危害
1. 内存占用过大,可能 OOM
2. 单个操作耗时长,阻塞其他请求
3. 主从同步慢,导致从库延迟
4. 持久化慢(RDB/AOF)
5. 集群数据倾斜

发现大 Key

1
2
3
4
5
6
7
8
9
10
11
12
# 1. 扫描整个实例
redis-cli --bigkeys

# 2. 扫描指定数据库
redis-cli -n 0 --bigkeys

# 3. 分析 RDB 文件
redis-rdb-tools dump.rdb --command memory --bytes 10240

# 4. 使用 MEMORY USAGE 命令
redis> MEMORY USAGE mykey
(integer) 1048576

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 1. 拆分大 Key
// 错误:单个 Hash 存储所有商品
HSET products 1001 "{...}" 1002 "{...}" ... 10000 "{...}"

// 正确:按分片拆分
for i := 0; i < 10; i++ {
rdb.HSet(ctx, fmt.Sprintf("products:shard:%d", i), productID, data)
}

// 2. 压缩数据
data := compress(largeJSON)
rdb.Set(ctx, key, data)

// 3. 设置合理过期时间
rdb.Set(ctx, key, value, 1*time.Hour)

// 4. 异步删除大 Key
rdb.Unlink(ctx, largeKey) // 非阻塞删除

热 Key 危害

1
2
3
4
5
6
7
8
# 什么是热 Key?
QPS > 10000 的 Key(如秒杀商品)

# 危害
1. 单个 Redis 节点流量过大
2. CPU 占用过高
3. 网络带宽打满
4. 集群节点负载不均

解决方案

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 1. 本地缓存 + Redis 二级缓存
localCache := cache.New(5*time.Minute, 10*time.Minute)

func GetHotKey(key string) (string, error) {
// 先查本地缓存
if val, found := localCache.Get(key); found {
return val.(string), nil
}

// 再查 Redis
val, err := rdb.Get(ctx, key).Result()
if err == nil {
localCache.Set(key, val, cache.DefaultExpiration)
}
return val, err
}

// 2. 多副本分散请求
func GetHotKeyWithReplica(key string) (string, error) {
// 随机选择副本
replica := rand.Intn(10)
replicaKey := fmt.Sprintf("%s:replica:%d", key, replica)
return rdb.Get(ctx, replicaKey).Result()
}

// 3. 限流保护
limiter := rate.NewLimiter(10000, 20000) // 10000 QPS, burst 20000
if !limiter.Allow() {
return ErrTooManyRequests
}

监控指标

关键指标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# 1. 内存使用
redis> INFO memory
used_memory:1073741824 # 已使用内存
used_memory_peak:2147483648 # 峰值内存
used_memory_rss:1610612736 # 物理内存
mem_fragmentation_ratio:1.5 # 碎片率

# 2. 性能指标
redis> INFO stats
instantaneous_ops_per_sec:10000 # 当前 QPS
total_commands_processed:1000000 # 总命令数
rejected_connections:0 # 拒绝连接数
expired_keys:1000 # 过期键数量
evicted_keys:0 # 淘汰键数量

# 3. 持久化
redis> INFO persistence
rdb_last_save_time:1641024000 # 最后 RDB 时间
rdb_changes_since_last_save:1000 # 自上次 RDB 变更数
aof_enabled:1 # AOF 是否开启
aof_last_rewrite_time_sec:2 # 最后 AOF 重写耗时

# 4. 复制
redis> INFO replication
role:master # 角色
connected_slaves:2 # 从库数量
master_repl_offset:1000000 # 主库偏移量
repl_backlog_size:1048576 # 积压缓冲区大小

# 5. 慢查询
redis> SLOWLOG GET 10
1) 1) (integer) 1
2) (integer) 1641024000
3) (integer) 50000 # 耗时 50ms
4) 1) "KEYS"
2) "*"

监控告警

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Prometheus 监控规则
groups:
- name: redis_alerts
rules:
# 内存使用率 > 80%
- alert: RedisMemoryHigh
expr: redis_memory_used_bytes / redis_memory_max_bytes > 0.8
for: 5m

# QPS > 50000
- alert: RedisQPSHigh
expr: rate(redis_commands_processed_total[1m]) > 50000
for: 5m

# 慢查询 > 10ms
- alert: RedisSlowLog
expr: redis_slowlog_length > 100
for: 10m

# 主从延迟 > 10s
- alert: RedisReplLag
expr: redis_master_repl_offset - redis_slave_repl_offset > 10000000
for: 5m

六、实战案例

基于真实业务场景,掌握分布式锁、BloomFilter、秒杀、排行榜等高级应用。


分布式锁实现

Redis 为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对 Redis 的连接并不存在竞争关系。

基本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// SETNX + EXPIRE 实现
func Lock(key string, value string, expiration time.Duration) bool {
return rdb.SetNX(ctx, key, value, expiration).Val()
}

func Unlock(key string, value string) error {
// 使用 Lua 保证原子性
script := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
return rdb.Eval(ctx, script, []string{key}, value).Err()
}

详细实现Redis 分布式锁


BloomFilter 实践

原理

BloomFilter 用于快速判断元素是否存在,允许误判(False Positive),但不会漏判(No False Negative)

1
判断结果 = 一定不存在 or 可能存在

公式

1
2
3
4
5
m = -n*ln(p) / (ln2)²    # 位数组大小
k = m/n * ln2 # 哈希函数数量

n = 预期元素数量
p = 误判率

示例

1
2
3
4
5
n = 1,000,000(百万数据)
p = 0.01(1% 误判率)

m = 9,585,059 bits ≈ 1.15 MB
k = 7 个哈希函数

Redis 实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import "github.com/bits-and-blooms/bloom/v3"

// 1. 创建 BloomFilter
bf := bloom.NewWithEstimates(1000000, 0.01)

// 2. 添加元素
bf.Add([]byte("user:123"))
bf.Add([]byte("user:456"))

// 3. 查询元素
exists := bf.Test([]byte("user:123")) // true
exists = bf.Test([]byte("user:999")) // false 或 true(误判)

// 4. 序列化到 Redis
data, _ := bf.GobEncode()
rdb.Set(ctx, "bloomfilter:users", data, 0)

// 5. 从 Redis 加载
data, _ := rdb.Get(ctx, "bloomfilter:users").Bytes()
bf := bloom.New(1, 1)
bf.GobDecode(data)

应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 1. 防止缓存穿透
func GetUser(uid string) (*User, error) {
// 先查 BloomFilter
if !bloomFilter.Test([]byte(uid)) {
return nil, ErrUserNotFound // 一定不存在
}

// 再查缓存和 DB
return getUserFromCacheOrDB(uid)
}

// 2. 去重(爬虫 URL)
func ShouldCrawl(url string) bool {
if bloomFilter.Test([]byte(url)) {
return false // 可能已爬取
}
bloomFilter.Add([]byte(url))
return true
}

// 3. 推荐系统去重
func FilterRecommendations(userID string, items []string) []string {
var result []string
for _, item := range items {
key := userID + ":" + item
if !viewedBF.Test([]byte(key)) {
result = append(result, item) // 未看过
}
}
return result
}

秒杀系统设计

核心问题

  1. 超卖问题:库存为 10,卖出 100 单
  2. 高并发问题:瞬时 10w+ QPS
  3. 恶意刷单:机器人刷单

方案设计

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// 1. 预热库存到 Redis
func PreloadStock(productID string, stock int) error {
key := fmt.Sprintf("seckill:stock:%s", productID)
return rdb.Set(ctx, key, stock, 0).Err()
}

// 2. Lua 脚本保证原子性扣库存
var decrStockScript = redis.NewScript(`
local key = KEYS[1]
local stock = tonumber(redis.call('GET', key))
if stock <= 0 then
return -1 -- 库存不足
end
redis.call('DECR', key)
return stock - 1
`)

func DecrStock(productID string) (int, error) {
key := fmt.Sprintf("seckill:stock:%s", productID)
result, err := decrStockScript.Run(ctx, rdb, []string{key}).Int()
if err != nil {
return 0, err
}
if result < 0 {
return 0, ErrStockNotEnough
}
return result, nil
}

// 3. 限流 + 队列削峰
func HandleSeckill(userID, productID string) error {
// 3.1 用户级限流
limitKey := fmt.Sprintf("limit:user:%s", userID)
count := rdb.Incr(ctx, limitKey).Val()
if count == 1 {
rdb.Expire(ctx, limitKey, 60*time.Second)
}
if count > 5 {
return ErrTooManyRequests // 1分钟最多5次
}

// 3.2 扣库存
stock, err := DecrStock(productID)
if err != nil {
return err
}

// 3.3 异步创建订单(放入队列)
order := Order{
UserID: userID,
ProductID: productID,
CreatedAt: time.Now(),
}
rdb.LPush(ctx, "queue:orders", json.Marshal(order))

return nil
}

// 4. 消费订单队列
func ProcessOrders() {
for {
data, err := rdb.BRPop(ctx, 5*time.Second, "queue:orders").Result()
if err != nil {
continue
}

var order Order
json.Unmarshal([]byte(data[1]), &order)

// 写入 MySQL
db.CreateOrder(&order)
}
}

排行榜实现

实时排行榜

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
// 1. 更新分数
func UpdateScore(userID string, score int64) error {
return rdb.ZAdd(ctx, "rank:realtime", &redis.Z{
Score: float64(score),
Member: userID,
}).Err()
}

// 2. 获取排行榜
func GetTopN(n int) ([]User, error) {
// 获取前 N 名(分数从高到低)
result, err := rdb.ZRevRangeWithScores(ctx, "rank:realtime", 0, int64(n-1)).Result()
if err != nil {
return nil, err
}

var users []User
for _, z := range result {
users = append(users, User{
ID: z.Member.(string),
Score: int64(z.Score),
})
}
return users, nil
}

// 3. 获取用户排名
func GetUserRank(userID string) (int64, error) {
rank, err := rdb.ZRevRank(ctx, "rank:realtime", userID).Result()
if err != nil {
return 0, err
}
return rank + 1, nil // 排名从 1 开始
}

// 4. 获取用户分数
func GetUserScore(userID string) (int64, error) {
score, err := rdb.ZScore(ctx, "rank:realtime", userID).Result()
return int64(score), err
}

// 5. 获取我的附近排名(前后各5名)
func GetNearbyRank(userID string) ([]User, error) {
rank, err := rdb.ZRevRank(ctx, "rank:realtime", userID).Result()
if err != nil {
return nil, err
}

start := rank - 5
if start < 0 {
start = 0
}
end := rank + 5

return rdb.ZRevRangeWithScores(ctx, "rank:realtime", start, end).Result()
}

每日排行榜(自动过期)

1
2
3
4
5
6
7
8
9
10
func UpdateDailyScore(userID string, score int64, date time.Time) error {
key := fmt.Sprintf("rank:daily:%s", date.Format("2006-01-02"))

// 更新分数
pipe := rdb.Pipeline()
pipe.ZAdd(ctx, key, &redis.Z{Score: float64(score), Member: userID})
pipe.Expire(ctx, key, 7*24*time.Hour) // 7天后过期
_, err := pipe.Exec(ctx)
return err
}

SDK 推荐

Go Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. go-redis(推荐)
import "github.com/redis/go-redis/v9"

rdb := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
PoolSize: 100,
})

// 2. redigo
import "github.com/gomodule/redigo/redis"

pool := &redis.Pool{
MaxIdle: 10,
MaxActive: 100,
IdleTimeout: 300 * time.Second,
Dial: func() (redis.Conn, error) {
return redis.Dial("tcp", "localhost:6379")
},
}

核心知识点总结

关键数字记忆

指标 数值 说明
性能 10w+ QPS 单机 Redis 性能
延迟 1ms 内存操作平均延迟
ziplist 阈值 512 entries, 64B Hash/List 默认阈值
zset ziplist 128 entries, 64B ZSet 默认阈值
大 Key > 10KB or > 10000 需要拆分
慢查询 > 10ms 需要优化
内存碎片率 1.0 - 1.5 正常范围
主从延迟 < 1s 健康状态

数据结构选择决策树

1
2
3
4
5
6
7
8
9
10
需求:存储用户信息
├─ 只需要简单的 key-value?
│ └─ 是 → String(JSON)
├─ 需要部分字段更新?
│ ├─ 字段数 < 500 → Hash(ziplist)
│ └─ 字段数 > 500 → Hash(hashtable)
├─ 需要按顺序存储?
│ ├─ 消息队列 → List(quicklist)
│ ├─ 去重集合 → Set(intset/hashtable)
│ └─ 有序集合 → ZSet(ziplist/skiplist)

缓存设计检查清单

  • 是否设置了过期时间(TTL)?
  • 是否有缓存更新策略(写穿/写回/旁路)?
  • 是否处理了缓存穿透(BloomFilter)?
  • 是否处理了缓存击穿(分布式锁/singleflight)?
  • 是否处理了缓存雪崩(随机TTL)?
  • 是否监控了大 Key 和热 Key?
  • 是否设置了内存淘汰策略?
  • 是否配置了持久化(RDB/AOF)?
  • 是否考虑了主从/集群高可用?

最佳实践速查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ✅ 推荐做法
1. 设置过期时间:rdb.Set(ctx, key, val, 1*time.Hour)
2. 使用 Pipeline 批量操作:pipe.HSet(ctx, key, field, val)
3. 避免 KEYS 命令:使用 SCAN 替代
4. 使用 Lua 保证原子性:script.Run(ctx, rdb, keys, args)
5. 大 Key 拆分:按哈希分片或按时间分段
6. 热 Key 本地缓存:localCache + Redis 二级缓存
7. 使用连接池:PoolSize: 100
8. 设置超时:DialTimeout/ReadTimeout/WriteTimeout

// ❌ 避免做法
1. 不设置过期时间(内存泄漏)
2. 单次操作大量数据(阻塞)
3. 在循环中发送命令(网络 RTT)
4. 使用 KEYS * 命令(阻塞)
5. 单个 Key 过大(> 10MB)
6. 频繁创建连接(性能差)
7. 无监控告警(问题发现慢)
8. 不做持久化(数据丢失)

可视化图表索引

本文包含详细的数据结构可视化图表,详见:

Hash 相关

Ziplist 相关

实战案例

数据结构对比


推荐阅读

官方文档

  1. Redis 官方文档
  2. Redis 命令参考
  3. Redis 数据类型

深度文章

  1. Redis 设计与实现(黄健宏) - 深入源码级别
  2. Redis 深度历险(钱文品) - 实战进阶
  3. 美团技术团队 - 缓存那些事
  4. Kaito’s Blog - Redis 系列

实战案例

  1. 秒杀系统设计
  2. 微信步数排行榜实现
  3. 分布式锁实现
  4. BloomFilter应用

性能优化

  1. 你的 Redis 为什么变慢了?
  2. 阿里云 Redis 延迟事件处理建议
  3. Redis 常用命令时间复杂度

面试题库

  1. Redis 面试题汇总
  2. 一不小心肝出了4W字的Redis面试教程

更新日志

  • 2026-01-08: 重大更新

    • ✅ 新增 Hash 底层实现详解(ziplist 字节级分析 + hashtable 渐进式 rehash)
    • ✅ 新增 String/List/Set/ZSet 底层实现说明
    • ✅ 新增性能优化章节(大Key/热Key 处理)
    • ✅ 新增监控告警方案
    • ✅ 新增实战案例(秒杀、排行榜、BloomFilter)
    • ✅ 新增可视化图表索引
    • ✅ 优化文章结构,增加目录导航
  • 2024-03-06: 初始版本

    • 基础内容:数据类型、缓存策略、持久化、分布式方案

持续更新中,欢迎收藏! 🚀

基础语法与关键字

const 关键字

  • 定义常量、指针、引用、对象,const进行修饰的变量的值在程序的任意位置将不能再被修改
  • 修饰参数: const int x
  • 修饰成员变量: const成员变量必须通过初始化列表进行初始化
  • 修饰成员函数
  • C++ mutable变量突破const修饰成员函数的限制

static 关键字

  • 静态全局变量
  • 修饰函数内部的局部变量,作用相当于全局变量
  • 类的静态成员变量
  • 类的静态成员函数

宏定义和内联函数

  • 内联函数和宏定义减少函数调用所带来的时间和空间的开销,以空间换时间的策略
  • 宏是在预编译阶段简单文本替代,inline在编译阶段实现展开
  • 宏肯定会被替代,而复杂的inline函数不会被展开
  • 宏容易出错(运算顺序),且难以被调试,inline不会
  • 宏不是类型安全,而inline是类型安全的
  • 当函数size太大,inline虚函数,函数中存在循环或递归,内联可能失效

extern 与 static

  • extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字
  • 该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用
  • 与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块使用

extern “C”

  • extern “C”是为了实现C和C++的混合编程
  • C和C++的编译和链接是不完全相同的
  • extern “C”表明它按照类C的编译和连接规约来编译和连接,而不是C++的编译和链接
  • C++是一个面向对象语言,它为了支持函数的重载,在编译的时候会带上参数的类型来唯一标识每个函数
  • C语言中并没有重载和类这些特性,故并不像C++那样print(int i)会被编译为_print_int

struct和class的区别

  • 在C++中struct和class的区别比较小,主要类成员的默认访问权限和继承权限
  • 默认继承权限:如果不明确指定,来自class的继承按照private继承处理,来自struct的继承按照public继承处理
  • 成员的默认访问权限:class的成员默认是private权限,struct默认是public权限
  • 仅当只有数据成员时使用struct,其它一概使用class(google编码规范)

类型安全

  • 静态类型 vs 动态类型: 静态类型(C/C++,java,golang),动态类型(python)
  • 弱类型 vs 强类型: 弱类型(C、C++),强类型(python、golang)
  • 类型安全: 一般来说弱类型存在隐含的类型转换都不是类型安全的,而强类型是类型安全的

C++中的四种类型转换

  • const_cast: 字面上理解就是去const属性
  • static_cast: 命名上理解是静态类型转换。如int转换成char。类似于C风格的强制转换
  • dynamic_cast: 命名上理解是动态类型转换。如子类和父类之间的多态类型转换
  • reinterpret_cast: 仅仅重新解释类型,但没有进行二进制的转换

volatile关键字

  • volatile指出变量是随时可能发生变化的
  • 每次使用它的时候必须从内存中读取
  • 编译器生成的汇编代码会重新从内存读取数据
  • 防止编译器优化

指针和引用

  • 指针指向一块内存,它的内容是所指内存的地址
  • 引用则是某块内存的别名,引用初始化后不能改变指向
  • 使用时,引用更加安全,指针更加灵活
  • 初始化:引用必须初始化,且初始化之后不能改变;指针可以不必初始化,且指针可以改变所指的对象
  • 空值:指针可以指向空值,不存在指向空值的引用
  • 引用和指针指向一个对象时,引用的创建和销毁不会调用类的拷贝构造函数和析构函数
  • 引用和指针与const:存在常量指针和常量引用指针,表示指向的对象是常量
  • 函数参数传递时使用指针或者引用的效果是相同的,都是简洁操作主调函数中的相关变量
  • sizeof引用的时候是对象的大小,sizeof指针是指针本身的大小

面向对象与类设计

C++的空类八个默认函数

  • 缺省构造函数
  • 拷贝构造函数
  • 赋值构造函数
  • 析构函数
  • 取值操作符函数
  • const取值操作符
  • 移动构造函数C++11
  • 移动赋值构造函数C++11

C++11中delete和default的作用

  • =default显式缺省,告知编译器生成函数默认的缺省版本
  • =delete显式删除,告知编译器不生成函数默认的缺省版本
  • C++11中引进这两种新特性的目的是为了增强对”类默认函数的控制”

C++11禁用隐式类型转换explicit

  • explicit关键字用来修饰类的构造函数
  • 被修饰的构造函数的类,不能发生相应的隐式类型转换
  • 只能以显示的方式进行类型转换

new/delete和malloc/free的使用

  • new/delete是C++的运算符,malloc/free是C/C++的库函数
  • new/delete和malloc/free必须配套使用
  • mallocl/free仅仅是在堆中分配内存,需要自己指定分配内存大小以及指针类型的转换
  • new/delete会根据对象的类型调用对应的构造函数和析构函数
  • new是类型安全的,而malloc不是

new/operator new和placement new

  • new:新建对象时用,是C++操作符
  • operator new就像operator + 一样,是可以重载的
  • placement new:只是operator new重载的一个版本
  • 它并不分配内存,只是返回指向已经分配好的某段内存的一个指针

sizeof

  • 空对象的大小为1个字节
  • 编译器内存对齐
  • 继承
  • 虚函数的影响

编译器内存对齐

  • 现代计算机中内存空间都是按照byte划分的
  • 实际的计算机系统对基本类型数据在内存中存放的位置有限制
  • 它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数
  • 这就是所谓的内存对齐
  • 编译器内存对齐是为了提高数据读写的效率

浅拷贝和深拷贝

  • 对于含有堆内存的对象,浅拷贝只是对指针的拷贝
  • 拷贝后两个指针指向同一个内存空间
  • 深拷贝对指针所指向的内容进行拷贝
  • 默认拷贝构造函数为浅拷贝

friend友元函数和友元类

  • 友元的作用是提高了程序的运行效率
  • 但它破坏了类的封装性和隐藏性
  • 使得非成员函数可以访问类的私有成员
  • 实际中这一特性很少使用

类的初始化列表

  • 初始化列表是C++11中新增的类成员初始化方式
  • 没有默认构造函数的类自定义类型成员必须使用初始化列表
  • const成员、引用类型成员必须使用初始化列表
  • 初始化列表中初始化初始化的顺序与成员定义的顺序相同,与初始化列表的顺序无关
  • 初始化列表的优点:主要是对于自定义类型,初始化列表是作用在函数体之前

重载、覆盖和重写

  • 重载(overload):同类中同名函数,参数的类型、个数或者返回类型不同
  • 覆盖(override):基类函数virtual函数,派生类中重写该函数
  • 重写(overwrite):派生类的函数屏蔽了与其同名的基类函数

继承/多继承/虚继承

  • 单继承、多继承,继承时构造函数和析构函数的调用顺序
  • 继承方式,public/protected/private,默认为private继承
  • 友元函数不能被继承
  • 静态成员和静态成员函数是可以继承的
  • 虚继承的概念
  • C++对象内存模型

virtual虚

  • 虚基类成员函数,派生类override这个虚函数
  • 虚析构函数
  • 虚函数的实现,虚函数表和虚函数指针
  • 虚函数的动态绑定机制与运行期多态
  • 虚继承,在多重继承关系中,为了避免菱形继承导致的资源浪费
  • 虚继承的实现,虚基表
  • 内敛函数不能为虚函数
  • 静态函数不能为虚函数
  • 构造函数不能为虚函数
  • 纯虚函数,类似于的接口的作用

C++对象模型

  • 对象的内存布局
  • 虚函数表
  • 虚基表
  • 继承关系中的内存布局

C++11中的移动语义

  • C++中的拷贝语义和移动语义
  • 右值引用和移动语义
  • 对含堆内存类的临时对象的拷贝和赋值函数的优化
  • 使的深拷贝转化为浅拷贝

C++11智能指针

  • unique_ptr
  • shared_ptr
  • weak_ptr
  • 智能指针的使用场景

模板编程

  • 模板,函数模板,类模板
  • C++类模板碰到static
  • 每个类型一个static值
  • C++类中不能包含虚函数模板
  • 类模板可以包含虚函数
  • 模板的声明和实现为何要放在头文件中
  • 模板元编程

访函数/函数指针/lamda表达式

  • 函数对象(function object)又叫仿函数(functor)
  • 就是重载了调用运算符()的类,所生成的对象
  • 函数指针也是一个函数对象
  • lamda表达式

C++异常处理

  • 返回错误码
  • 断言
  • 异常处理Exception
  • 构造函数可以抛异常,析构函数不能抛异常

RTTI

  • RTTI是”Runtime Type Information”的缩写
  • 意思是运行时类型信息
  • 它提供了运行时确定对象类型的方法
  • 实现机制是虚函数和虚函数表
  • 谷歌禁用使用RTTI

前置声明(forward declaration)

  • 所谓「前置声明」(forward declaration)是类、函数和模板的纯粹声明
  • 没伴随着其定义
  • 尽可能地避免使用前置声明
  • 使用#include包含需要的头文件即可

#include头文件的顺序

  • C系统文件
  • C++标准库文件
  • 其它库的.h文件
  • 本项目内的的.h文件

命名空间namespace

  • 命名空间将全局作用域细分为独立的,具名的作用域
  • 可有效防止全局作用域的命名冲突
  • 不应该使用using指示引入整个命名空间的标识符号
  • 禁止用内联命名空间

接口类

  • 接口是指满足特定条件的类
  • 这些类以Interface为后缀(不强制)

函数使用引用参数

  • 在C语言中,如果函数需要修改变量的值,参数必须为指针
  • 在C++中,函数还可以声明为引用参数
  • 定义引用参数可以防止出现(*pval)++这样丑陋的代码
  • 引用参数对于拷贝构造函数这样的应用也是必需的
  • 同时也更明确地不接受空指针
  • 函数参数列表中,所有引用参数都必须是const

函数返回值后置语法

  • 只有在常规写法(返回类型前置)不便于书写或不便于阅读时使用返回类型后置语法
  • C++11引入了后置返回值的语法
  • 后置返回类型是显式地指定Lambda表达式的返回值的唯一方式

  • 流用来替代printf()和scanf()
  • 有了流,在打印时不需要关心对象的类型
  • 不用担心格式化字符串与参数列表不匹配
  • 流的构造和析构函数会自动打开和关闭对应的文件
  • 不要使用流,除非是日志接口需要
  • 使用printf之类的代替

前置自增和自减

  • 不考虑返回值的话,前置自增(++i)通常要比后置自增(i++)效率更高
  • 因为后置自增(或自减)需要对表达式的值i进行一次拷贝
  • 如果i是迭代器或其他非数值类型,拷贝的代价是比较大的
  • 既然两种自增方式实现的功能一样,为什么不总是使用前置自增呢

constexpr

  • 在C++11里,用constexpr来定义真正的常量
  • 或实现常量初始化
  • 变量可以被声明成constexpr以表示它是真正意义上的常量
  • 即在编译时和运行时都不变
  • 函数或构造函数也可以被声明成constexpr
  • 以用来定义constexpr变量

Lambda表达式

  • 适当使用lambda表达式
  • 别用默认lambda捕获
  • 所有捕获都要显式写出来
  • Lambda表达式是创建匿名函数对象的一种简易途径
  • 常用于把函数当参数传

命名

  • 文件名:http_server_logs.h,http_server_logs.cc
  • 类型性:MyExcitingClass
  • 变量名:string table_name
  • 函数名:AddTableEntry()
  • 命名空间:websearch::index

STL容器与算法

熟悉STL中17种容器及其背后对应的数据结构

  • vector
  • list
  • deque
  • stack
  • queue
  • priority_queue
  • set
  • multiset
  • map
  • multimap
  • unordered_set
  • unordered_multiset
  • unordered_map
  • unordered_multimap
  • array
  • forward_list
  • string

map和unordered_map的区别

  • map背后是红黑树,unordered_map背后是哈希表
  • map是key值有序的,unordered_map是key值无序的
  • 两者内存消耗差不多,但是插入/查找/删除效率unordered_map是map的2到3倍
  • unordered_map是通过链地址法解决冲突的
  • std::map [] operator和insert的区别

priority_queue优先队列的实现

  • priority_queue优先队列,其底层是用堆来实现的
  • 在优先队列中,队首元素一定是当前队列中优先级最高的那一个
  • 在优先队列中,没有front()函数与back()函数
  • 而只能通过top()函数来访问队首元素
  • 也就是优先级最高的元素
  • priority_queue默认为大顶堆

迭代器和迭代器失效iterator

  • 为了提高C++编程的效率,STL中提供了许多容器
  • 有些容器例如vector可以通过脚标索引的方式访问容器里面的数据
  • 但是大部分的容器不能使用这种方式
  • STL中每种容器在实现的时候设计了一个内嵌的iterator类
  • 不同的容器有自己专属的迭代器
  • 使用迭代器来访问容器中的数据
  • 通过迭代器,可以将容器和通用算法结合在一起
  • 只要给予算法不同的迭代器,就可以对不同容器执行相同的操作
  • 迭代器对指针的一些基本操作如*、->、++、==、!=、=进行了重载
  • 使其具有了遍历复杂数据结构的能力
  • 其遍历机制取决于所遍历的数据结构
  • 所有迭代的使用和指针的使用非常相似
  • 通过begin,end函数获取容器的头部和尾部迭代器
  • end迭代器不包含在容器之内
  • 当begin和end返回的迭代器相同时表示容器为空
  • 容器的插入insert和erase操作可能导致迭代器失效
  • 对于erase操作不要使用操作之前的迭代器
  • 因为erase的那个迭代器一定失效了
  • 正确的做法是返回删除操作时候的那个迭代器

容器的线程安全性Thread safety

  • STL为了效率,没有给所有操作加锁
  • 不同线程同时读同一容器对象没关系
  • 不同线程同时写不同的容器对象没关系
  • 但不能同时又读又写同一容器对象的
  • 因此,多线程要同时读写时,还是要自己加锁

STL排序

  • sort,快排加插入排序
  • stable_sort,稳定排序
  • sort_heap,堆排序
  • list.sort,链表归并排序

STL容器的内存管理方式

  • 内存分配器
  • 内存池
  • 内存释放

vector和map的内存释放

  • vector的内存释放
  • map的内存释放
  • 容器删除数据的时候注意迭代器失效
  • vector和map正确的内存释放

编译、链接与调试

排查编译问题常用工具

gcc/g++的区别和使用

  • 后缀为.c的,gcc把它当作是C程序,而g++当作是c++程序
  • 后缀为.cpp的,两者都会认为是c++程序
  • 对于C代码,编译和链接都使用gcc
  • 对于C++代码,编译时可以使用gcc/g++,gcc实际也是调用g++
  • 链接时gcc不能自动和C++使用库链接,因此要使用g++或者gcc -lstdc++

常见gcc编译链接选项

  • -c 只编译并生成目标文件
  • -g 生成调试信息,gdb可以利用该调试信息
  • -o 指定生成的输出文件,可执行程序或者动态链接库文件名
  • -I 编译时添加头文件路径
  • -L 链接时添加库文件路径
  • -D 定义宏,常用于开关控制代码
  • -shared 用于生成共享库.so
  • -Wall 显示所有警告信息,-w不生成任何警告信息
  • -O0选项不进行任何优化,debug会产出和程序预期的结果
  • O1优化会消耗少多的编译时间,它主要对代码的分支,常量以及表达式等进行优化
  • O2会尝试更多的寄存器级的优化以及指令级的优化
  • 它会在编译期间占用更多的内存和编译时间
  • 通常情况下线上代码至少加上O2优化选项
  • -fPIC 位置无关选项,生成动态库时使用
  • 实现真正意义上的多进程共享的.so库
  • -Wl选项告诉编译器将后面的参数传递给链接器
  • -Wl,-Bstatic,指明后面是链接今静态库
  • -Wl,-Bdynamic,指明后面是链接动态库

编译时添加头文件依赖路径

  • -include用来包含头文件
  • 但一般情况下包含头文件都在源码里用#include xxxxxx实现
  • -include参数很少用
  • -I参数是用来指定头文件目录
  • /usr/include目录一般是不用指定的,gcc知道去那里找
  • 但是如果头文件不在/usr/include里我们就要用-I参数指定了
  • 比如头文件放在/myinclude目录里,那编译命令行就要加上-I /myinclude参数了
  • 如果不加你会得到一个”xxxx.h: No such file or directory”的错误
  • -I参数可以用相对路径,比如头文件在当前目录,可以用-I.来指定

排查链接问题常用工具

  • 查看ld链接器的搜索顺序 ld –verbose | grep SEARCH
  • 链接时指定链接目录 -L/dir
  • -Wl,-Bstatic,指明后面是链接今静态库
  • -Wl,-Bdynamic,指明后面是链接动态库
  • 运行时找不到动态库so文件,设置LD_LIBRARY_PATH,添加依赖so文件所在路径
  • 链接完成后使用ldd查看动态库依赖关系
  • 如果依赖的某个库找不到,通过这个命令可以迅速定位问题所在
  • ldd -r,帮助检查是否存在未定义的符号undefine symbol,so库链接状态和错误信息

gdb调试基本使用

对C/C++程序的调试

  • 需要在编译前就加上-g选项
  • $gdb
  • 设置参数:set args 可指定运行时参数
  • (如:set args 10 20 30 40 50)

查看源代码

  • list:简记为l,其作用就是列出程序的源代码,默认每次显示10行
  • list 行号:将显示当前文件以”行号”为中心的前后10行代码
  • list 函数名:将显示”函数名”所在函数的源代码
  • list:不带参数,将接着上一次list命令的,输出下边的内容

设置断点和关闭断点

  • break n(简写b n):在第n行处设置断点
  • break func(简写b func):在函数func()的入口处设置断点
  • info b(info breakpoints):显示当前程序的断点设置情况
  • delete 断点号n:删除第n个断点
  • disable 断点号n:暂停第n个断点
  • clear 行号n:清除第n行的断点

程序调试运行

  • run:简记为r,其作用是运行程序
  • 当遇到断点后,程序会在断点处停止运行
  • 等待用户输入下一步的命令
  • continue(简写c):继续执行,到下一个断点处(或运行结束)
  • next:(简写n),单步跟踪程序
  • 当遇到函数调用时,也不进入此函数体
  • 此命令同step的主要区别是
  • step遇到用户自定义的函数,将步进到函数中去运行
  • 而next则直接调用函数,不会进入到函数体内
  • step(简写s):单步调试如果有函数调用,则进入函数
  • 与命令n不同,n是不进入调用的函数的
  • until:当你厌倦了在一个循环体内单步跟踪时
  • 这个命令可以运行程序直到退出循环体
  • until+行号:运行至某行,不仅仅用来跳出循环
  • finish:运行程序,直到当前函数完成返回
  • 并打印函数返回时的堆栈地址和返回值及参数值等信息
  • call 函数(参数):调用程序中可见的函数,并传递”参数”
  • quit:简记为q,退出gdb

打印程序运行的调试信息

  • print 表达式:简记为p
  • 其中”表达式”可以是任何当前正在被测试程序的有效表达式
  • 比如当前正在调试C语言的程序
  • 那么”表达式”可以是任何C语言的有效表达式
  • 包括数字,变量甚至是函数调用
  • print a:将显示整数a的值
  • print name:将显示字符串name的值
  • print gdb_test(22):将以整数22作为参数调用gdb_test()函数
  • print gdb_test(a):将以变量a作为参数调用gdb_test()函数
  • 扩展info locals:显示当前堆栈页的所有变量

查询运行信息

  • where/bt:当前运行的堆栈列表
  • bt backtrace 显示当前调用堆栈
  • up/down 改变堆栈显示的深度
  • set args 参数:指定运行时的参数
  • show args:查看设置好的参数
  • info program:来查看程序的是否在运行,进程号,被暂停的原因

gdb调试coredump问题

  • Coredump叫做核心转储
  • 它是进程运行时在突然崩溃的那一刻的一个内存快照
  • 操作系统在程序发生异常而异常在进程内部又没有被捕获的情况下
  • 会把进程此刻内存、寄存器状态、运行堆栈等信息转储保存在一个文件里
  • 该文件也是二进制文件,可以使用gdb调试
  • 虽然我们知道进程在coredump的时候会产生core文件
  • 但是有时候却发现进程虽然core了,但是我们却找不到core文件
  • 在ubuntu系统中需要进行设置
  • ulimit -c 可以设置core文件的大小
  • 如果这个值为0.则不会产生core文件
  • 这个值太小,则core文件也不会产生
  • 因为core文件一般都比较大
  • 使用ulimit -c unlimited来设置无限大
  • 则任意情况下都会产生core文件
  • gdb打开core文件时,有显示没有调试信息
  • 因为之前编译的时候没有带上-g选项
  • 没有调试信息是正常的
  • 实际上它也不影响调试core文件
  • 因为调试core文件时,符号信息都来自符号表
  • 用不到调试信息
  • 如下为加上调试信息的效果
  • 调试步骤:
  • $gdb program core_file 进入
  • $ bt或者where # 查看coredump位置
  • 当程序带有调试信息的情况下
  • 我们实际上是可以看到core的地方和代码行的匹配位置
  • 但往往正常发布环境是不会带上调试信息的
  • 因为调试信息通常会占用比较大的存储空间
  • 一般都会在编译的时候把-g选项去掉
  • 这种情况啊也是可以通过core_dump文件找到错误位置的
  • 但这个过程比较复杂

gdb调试线上死锁问题

  • 如果你的程序是一个服务程序
  • 那么你可以指定这个服务程序运行时的进程ID
  • gdb会自动attach上去,并调试
  • 对于服务进程,我们除了使用gdb调试之外
  • 还可以使用pstack跟踪进程栈
  • 这个命令在排查进程问题时非常有用
  • 比如我们发现一个服务一直处于work状态
  • (如假死状态,好似死循环)
  • 使用这个命令就能轻松定位问题所在
  • 可以在一段时间内,多执行几次pstack
  • 若发现代码栈总是停在同一个位置
  • 那个位置就需要重点关注
  • 很可能就是出问题的地方
  • gdb比pstack更加强大
  • gdb可以随意进入进程、线程中改变程序的运行状态和查看程序的运行信息
  • 思考:如何调试死锁?
  • $gdb
  • $pstack pid

undefined symbol问题解决步骤

  • file 检查so或者可执行文件的架构

    1
    2
    $ file _visp.so 
    _visp.so: ELF 64-bit LSB pie executable, x86-64, version 1 (GNU/Linux), dynamically linked, BuildID[sha1]=6503ba6b7545e38e669ab9ed31f86449d8a5f78b, stripped
  • ldd -r _visp.so 命令查看so库链接状态和错误信息

    1
    2
    undefined symbol: __itt_api_version_ptr__3_0	(./_visp.so)
    undefined symbol: __itt_id_create_ptr__3_0 (./_visp.so)
  • c++filt symbol 定位错误在那个C++文件中

    1
    2
    base) terse@ubuntu:~/code/terse-visp$ c++filt __itt_domain_create_ptr__3_0
    __itt_domain_create_ptr__3_0
  • 还可以使用grep -R __itt_domain_create_ptr__3_0 ./
    最终发现这个符号来自XXX/opencv-3.4.6/build/share/OpenCV/3rdparty/libittnotify.a

  • 通过nm命令也能看出该符号确实未定义

    1
    2
    $ nm _visp.so | grep __itt_domain_create_ptr__3_0
    U __itt_domain_create_ptr__3_0

pkg-config找第三方库的头文件和库文件

  • pkg-config能方便使用第三方库和头文件和库文件
  • 其运行原理
  • 它首先根据PKG_CONFIG_PATH环境变量下寻找库对应的pc文件
  • 然后从pc文件中获取该库对应的头文件和库文件的位置信息
  • 例如在项目中需要使用opencv库
  • 该库包含的头文件和库文件比较多
  • 首先查看是否有对应的opencv.pc find /usr -name opencv.pc
  • 查看该路径是否包含在PKG_CONFIG_PATH
  • 使用pkg-config –cflags –libs opencv 查看库对应的头文件和库文件信息
  • pkg-config –modversion opencv 查看版本信息
    参考链接:https://blog.csdn.net/luotuo44/article/details/24836901

cmake中的find_package

  • find_package原理
  • 首先明确一点,cmake本身不提供任何搜索库的便捷方法
  • 所有搜索库并给变量赋值的操作必须由cmake代码完成
  • 比如下面将要提到的FindXXX.cmake和XXXConfig.cmake
  • 只不过,库的作者通常会提供这两个文件
  • 以方便使用者调用
  • find_package采用两种模式搜索库:

Module模式:搜索CMAKE_MODULE_PATH指定路径下的FindXXX.cmake文件,执行该文件从而找到XXX库。其中,具体查找库并给XXX_INCLUDE_DIRS和XXX_LIBRARIES两个变量赋值的操作由FindXXX.cmake模块完成。

Config模式:搜索XXX_DIR指定路径下的XXXConfig.cmake文件,执行该文件从而找到XXX库。其中具体查找库并给XXX_INCLUDE_DIRS和XXX_LIBRARIES两个变量赋值的操作由XXXConfig.cmake模块完成。

两种模式看起来似乎差不多,不过cmake默认采取Module模式,如果Module模式未找到库,才会采取Config模式。如果XXX_DIR路径下找不到XXXConfig.cmake文件,则会找/usr/local/lib/cmake/XXX/中的XXXConfig.cmake文件。总之,Config模式是一个备选策略。通常,库安装时会拷贝一份XXXConfig.cmake到系统目录中,因此在没有显式指定搜索路径时也可以顺利找到。

ldd解决运行时问题

  • 现象:

  • error while loading shared libraries: libopencv_cudabgsegm.so.3.4: cannot open shared object file: No such file or directory

  • ldd ./xxx,发现库文件not found

    libopencv_cudaobjdetect.so.3.4 => not found  
    libopencv_cudalegacy.so.3.4 => not found
    
  • ld.so 动态共享库搜索顺序:

  • ELF可执行文件中动态段DT_RPATH指定;gcc加入链接参数”-Wl,-rpath”指定动态库搜索路径;

  • 环境变量LD_LIBRARY_PATH指定路径;

  • /etc/ld.so.cache中缓存的动态库路径。可以通过修改配置文件/etc/ld.so.conf 增删路径(修改后需要运行ldconfig命令);

  • 默认的 /lib/;

  • 默认的 /usr/lib/

  • 解决办法:

  • 确认系统中是包含这个库文件的

  • pkg-config –libs opencv 查看opencv库的路径

  • export LD_LIBRARY_PATH=/usr/local/lib64,增加运行时加载路径

参考链接:https://www.cnblogs.com/amyzhu/p/8871475.html

makefile和cmake的使用

  • 跟我学些makefile
  • CMake入门实战

性能分析与优化

time

shell time

  • time非常方便获取程序运行的时间
  • 包括用户态时间user、内核态时间sys和实际运行的时间real
  • 我们可以通过(user+sys)/real计算程序CPU占用率
  • 判断程序时CPU密集型还是IO密集型程序

/usr/bin/time

  • Linux中除了shell time,还有/usr/bin/time
  • 它能获取程序运行更多的信息
  • 通常带有-v参数

top

  • top是linux系统的任务管理器
  • 它既能看系统所有任务信息
  • 也能帮助查看单个进程资源使用情况
  • 主要有以下几个功能:
  • 查看系统任务信息
  • 查看CPU使用情况
  • 查看内存使用情况
  • 查看单个进程资源使用情况
  • 除此之外top还提供了一些交互命令

perf

perf stat

  • 做任何事都最好有条有理
  • 老手往往能够做到不慌不忙,循序渐进
  • 而新手则往往东一下,西一下,不知所措
  • 面对一个问题程序,最好采用自顶向下的策略
  • 先整体看看该程序运行时各种统计事件的大概
  • 再针对某些方向深入细节
  • 而不要一下子扎进琐碎细节,会一叶障目的
  • 有些程序慢是因为计算量太大
  • 其多数时间都应该在使用CPU进行计算
  • 这叫做CPU bound型
  • 有些程序慢是因为过多的IO
  • 这种时候其CPU利用率应该不高
  • 这叫做IO bound型
  • 对于CPU bound程序的调优和IO bound的调优是不同的
  • 如果您认同这些说法的话
  • Perf stat应该是您最先使用的一个工具
  • 它通过概括精简的方式提供被调试程序运行的整体情况和汇总数据

perf top

  • Perf top用于实时显示当前系统的性能统计信息
  • 该命令主要用来观察整个系统当前的状态
  • 比如可以通过查看该命令的输出来查看当前系统最耗时的内核函数或某个用户进程

perf record/perf report

  • 使用top和stat之后
  • 这时对程序基本性能有了一个大致的了解
  • 为了优化程序,便需要一些粒度更细的信息
  • 比如说您已经断定目标程序计算量较大
  • 也许是因为有些代码写的不够精简
  • 那么面对长长的代码文件
  • 究竟哪几行代码需要进一步修改呢
  • 这便需要使用perf record记录单个函数级别的统计信息
  • 并使用perf report来显示统计结果
  • 您的调优应该将注意力集中到百分比高的热点代码片段上
  • 假如一段代码只占用整个程序运行时间的0.1%
  • 即使您将其优化到仅剩一条机器指令
  • 恐怕也只能将整体的程序性能提高0.1%
  • 俗话说,好钢用在刀刃上
  • 要优化热点函数

gprof

  • gprof是GNU profiler工具
  • 可以显示程序运行的”flat profile”
  • 包括每个函数的调用次数
  • 每个函数消耗的处理器时间
  • 也可以显示”调用图”
  • 包括函数的调用关系
  • 每个函数调用花费了多少时间
  • 还可以显示”注释的源代码”
  • 是程序源代码的一个复本
  • 标记有程序中每行代码的执行次数

内存问题与valgrind

常见的内存问题

  • 使用未初始化的变量
  • 内存访问越界
  • 内存覆盖
  • 动态内存管理错误
  • 内存泄露

valgrind内存检测

  • valgrind是一个工具集
  • 其中最有名的是Memcheck
  • 它可以帮助我们检查程序中的内存问题
  • 如内存泄漏、越界访问、重复释放等

自定义timer计时器

  • 自己写一个计时器
  • 计算局部函数的时间

常见问题与面试题

如何让类对象只在栈(堆)上分配空间?

  • 只能在栈上建立对象
  • 只能在堆上建立对象

C++不可继承类的实现?

  • 使用final关键字
  • 使用私有构造函数
  • 使用虚析构函数

如何定义和实现一个类的成员函数为回调函数?

  • 友元函数/静态成员函数消除this指针的影响

C++复制构造函数的参数为什么是引用类型?

  • 编译时报错
  • 需要首先调用该类的拷贝构造函数来初始化形参(局部对象)
  • 造成无线循环递归

C++全局对象如何在main函数之前构造和析构?

  • 全局对象的构造和析构顺序
  • 如何控制全局对象的构造和析构顺序

其它常见的问题

  • vector和map的内存释放问题
  • 容器删除数据的时候注意迭代器失效
  • vector和map正确的内存释放
  • C++的iostream的局限
  • STL::list::sort链表归并排序

参考资料

Python程序为什么慢?

  不同的场景下,代码是有不同的要求,大体有三个等级,“管用、更好、更快”。相比C/C++,Python具有较好的开发系效率,但是程序的性能运行速度会差一些。究其原因是Python为了灵活性,牺牲了效率。

  1. 动态类型。对于C/C++等静态类型语言,由于变量的类型固定,变量之间的运算很容易指定特定的函数。而动态类型在运行的时间需要大量if else判断处理,直到找到符合条件的函数。动态类型增加语言的易用性,但是牺牲了程序的运行效率

  2. GIL(Global Interpreter Lock)全局解释锁,CPython在解释执行任何Python代码的时候,首先都需要they acquire GIL when running,release GIL when blocking for I/O。如果没有涉及I/O操作,只有CPU密集型操作时,解释器每隔100一段时间(100ticks)就会释放GIL。GIL是实现Python解释器的(Cython)时所引入的一个概念,不是Python的特性。
    由于GIL的存在,使得Python对于计算密集型任务,多线程threading模块形同虚设,因为线程在实际运行的时候必须获得GIL,而GIL只有一个,因此无法发挥多核的优势。为了绕过GIL的限制,只能使用multiprocessing等多进程模块,每个进程各有一个CPython解释器,也就各有一个GIL。

  3. CPython不支持JIT(Just-In-Time Compiler),JIT 能充分利用程序运行时信息,进行类型推导等优化,对于重复执行的代码段来说加速效果明显。对于CPython如果想使用JIT对计算密集型任务进行优化,可以尝试使用JIT包numba,它能使得相应的函数变成JIT编译。

Python程序优化的思路?

  最近在做一些算法优化方面的工作,简单总结一下思路:

  1. 熟悉算法的整体流程,对于算法代码,最开始尽可能不要使用多线程和多进程方法,
  2. 在1的基础上跑出算法的CPU profile,整体了解算法耗时分布和瓶颈。Python提供的cProfile模块灵活的针对特定函数或者文件产生profile文件,根据profile数据进行代码性能优化。
  1. 程序(算法)本身的剪枝。比如视频追踪中,考虑是否让每个像素点都参与计算?优化后选择梯度变化最大的1w个像素点参与计算,能提高分辨率大的视频追踪效率。
  2. 使用矩阵操作代替循环操作。(get_values())
  3. 任务分解,在理解算法的基础上寻找并行机会,利用多线程或者多进程充分利用机器资源。生产者消费者模型,专门的线程负责图像获取和图形变换,专门的线程负责特征提取和追踪。
  4. 使用C/C++重写效率低的瓶颈部分
  5. 使用GPU计算

  最近项目中需要用到visp库中的模板追踪算法,visp库用C++编写的,代码多,功能丰富。但是,对于项目来说直接调visp库并不方便,因此我们摘取visp库中的所需代码,提供python调用的接口,并根据项目需求进行优化和扩展。开源项目越来越多,以后工作也可能会遇到提取复杂库中部分功能,然后提供python调用的接口,因此这里总结一下,过程并不复杂,但是也遇到一些坑。主要注意以下几点:

  1. 依赖库采用静态方式编译。最开始的时候采用默认的动态编译,导致项目依赖复杂,部署起来非常不方便。要注意的是,visp库依赖opencv库,两个库都要采用静态方式编译。
  2. 提取所需代码,封装成类。提取visp库中的模板追踪算法,封装成类。为了便于后续的优化工作,接口扩展性尽可能好。
  3. **swig实现python调用C++**。有很多方法实现python调用C++,我这里采用swig,适合懒人。

静态编译opencv库

  opencv采用cmake项目管理,通过ccmake可以很方便的设置静态编译选项。BUILD_SHARED_LIBS设置为OFF即为静态编译。另外,为了保持系统整洁,避免安装到系统路径,设置了安装路径,CMAKE_INSTALL_PREFIX=/home/terse/code/terse-visp/opencv-3.4.6/build

  1. git clone https://github.com/opencv/opencv.git
  2. cd opencv-3.4.6
  3. mkdir build && cd build
  4. ccmake ..(关闭动态编译选项,设置安装路径),cmake ..
  5. make -j4
  6. make install

静态编译visp库

  visp库https://github.com/lagadic/visp.git 和opencv库一样都采用cmake管理,编译过程和opencv一样,这里只需要设置静态编译和设置安装路径:
关闭动态编译选项:BUILD_SHARED_LIBS=OFF
设置安装路径: CMAKE_INSTALL_PREFIX=/home/terse/code/terse-visp/visp/build

  1. git clone https://github.com/lagadic/visp.git
  2. cd visp
  3. mkdir build && cd build
  4. ccmake ..(关闭动态编译选项,设置安装路径),cmake ..
  5. make -j4
  6. make install

提取模板追踪算法,封装成C++类

  visp库中提供了模板追踪算法,但是它不能解决遮挡的情况,参考区域很大的时候,追踪速度也很慢,因此在项目中针对这些问题做了一些优化,这个不是本文的重点就不赘述了。下面从visp中摘取的代码,封装成C++类,say_hello成员函数,没有实际用途,只是为了后续的验证python代码的正确性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#ifndef VISP_H_
#define VISP_H_

#include <visp3/io/vpImageIo.h>
#include <visp3/tt/vpTemplateTrackerSSDInverseCompositional.h>
#include <visp3/tt/vpTemplateTrackerWarpHomography.h>
#include <visp3/core/vpException.h>
#include <opencv2/opencv.hpp>
#include <fstream>

class TemplateTracker
{
public:
TemplateTracker();

void SetSampling(unsigned int sample_i, unsigned int sample_j);
void SetLambda(double lamda);
void SetIterationMax(unsigned int n);
void SetPyramidal(unsigned int nlevels, unsigned int level_to_stop);
void SetUseTemplateSelect(bool bselect);
void SetThresholdGradient(float threshold);

int Init(unsigned char* imgData, unsigned int h, unsigned int w, int* ref, unsigned int points_num, bool bshow);
int InitWithMask(unsigned char* imgData, unsigned int h, unsigned int w, int* ref, unsigned int points_num, bool bshow, unsigned char* mask_data,int h2,int w2);

int ComputeH(unsigned char* imgData, unsigned int h,unsigned int w,float* H_matrix,int num);
int ComputeHWithMask(unsigned char* imgData, unsigned int h,unsigned int w,float* H_matrix,int num,unsigned char* mask_data,int h2,int w2);

void Reset();
~TemplateTracker();
void say_hello();


private:
vpTemplateTrackerWarpHomography warp_;
vpTemplateTrackerSSDInverseCompositional tracker_;
int height_, width_;
vpImage<unsigned char> I_;
bool bshow_;
};

#endif /* VISP_H_ */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
#include "visp.h"  


TemplateTracker::TemplateTracker() :
tracker_(&warp_),
bshow_(false)
{};

void TemplateTracker::SetSampling(unsigned int sample_i, unsigned int sample_j)
{
tracker_.setSampling(sample_i, sample_j);
}

void TemplateTracker::SetLambda(double lamda)
{
tracker_.setLambda(lamda);
}

void TemplateTracker::SetIterationMax(unsigned int n)
{
tracker_.setIterationMax(n);
}

void TemplateTracker::SetPyramidal(unsigned int nlevels, unsigned int level_to_stop)
{
tracker_.setPyramidal(nlevels, level_to_stop);
}

void TemplateTracker::SetUseTemplateSelect(bool bselect)
{
tracker_.setUseTemplateSelect(bselect);
}

void TemplateTracker::SetThresholdGradient(float threshold)
{
tracker_.setThresholdGradient(threshold);
}


int TemplateTracker::Init(unsigned char* imgData, unsigned int h, unsigned int w, int* ref, unsigned int points_num, bool bshow)
{
cv::Mat img_gray(h, w, CV_8UC1, (unsigned char*)(imgData)); //浅拷贝
I_.init(imgData, h, w, true);
height_ = h;
width_ = w;
bshow_ = bshow;
std::vector<vpImagePoint> v_ip;
for (int i = 0; i < points_num/2; i++)
{
vpImagePoint ip(ref[i * 2], ref[i * 2 + 1]);
v_ip.push_back(ip);
}

try{
tracker_.initFromPoints(I_, v_ip);
}catch(vpException &e){
return e.getCode();
}

return 0;
}


int TemplateTracker::InitWithMask(unsigned char* imgData, unsigned int h, unsigned int w, int* ref, unsigned int points_num, bool bshow, unsigned char* mask_data,int h2,int w2)
{
cv::Mat img_gray(h, w, CV_8UC1, (unsigned char*)(imgData)); //浅拷贝
I_.init(imgData, h, w, true);
height_ = h;
width_ = w;
bshow_ = bshow;
if (NULL != mask_data)
{
cv::Mat mask_gray(h, w, CV_8UC1, (unsigned char*)(mask_data)); //浅拷贝
I_.SetMask(mask_gray);
}

std::vector<vpImagePoint> v_ip;
for (int i = 0; i < points_num/2; i++)
{
vpImagePoint ip(ref[i * 2], ref[i * 2 + 1]);
v_ip.push_back(ip);
}

try{
tracker_.initFromPoints(I_, v_ip);
}catch(vpException &e){
return e.getCode();
}

return 0;
}



int TemplateTracker::ComputeH(unsigned char* imgData, unsigned int h,unsigned int w,float* H_matrix,int num)
{
I_.init(imgData, height_, width_, true);
try{
tracker_.track(I_);
}catch(vpTrackingException &e){
std::cout << e.getMessage() << std::endl;
return e.getCode();
}
vpColVector p = tracker_.getp();
vpHomography H = warp_.getHomography(p);
for (int m = 0; m < 3; m++)
{
for (int n = 0; n < 3; n++)
{
H_matrix[m * 3 + n] = H[m][n];
}
}
return 0;
}


int TemplateTracker::ComputeHWithMask(unsigned char* imgData, unsigned int h,unsigned int w,float* H_matrix,int num,unsigned char* mask_data,int h2,int w2)
{
I_.init(imgData, height_, width_, true);
if (NULL != mask_data)
{
cv::Mat mask_gray(height_, width_, CV_8UC1, (unsigned char*)(mask_data)); //浅拷贝
I_.SetMask(mask_gray);
}

try{
tracker_.track(I_);
}catch(vpTrackingException &e){
std::cout << e.getMessage() << std::endl;
return e.getCode();
}
vpColVector p = tracker_.getp();
vpHomography H = warp_.getHomography(p);
for (int m = 0; m < 3; m++)
{
for (int n = 0; n < 3; n++)
{
H_matrix[m * 3 + n] = H[m][n];
}
}
return 0;
}

void TemplateTracker::Reset()
{
tracker_.resetTracker();
}

TemplateTracker::~TemplateTracker(){};

void TemplateTracker::say_hello(){
std::cout << "hello" << std::endl;
}

采用swig实现python调用C++

  python调用C++的方法有很多,例如ctypes、PyObject、Boost.python,采用了swig方法,使用之后感觉确挺方便的。为了给追踪功能提供numpy参数的输入和输出,这里需要引入numpy.i文件。
参考:http://www.swig.org/Doc1.3/Python.html#Python

1. 定义接口文件:visp.i

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* File: visp.i */
%module visp

%{
#define SWIG_FILE_WITH_INIT
#include "visp.h"
%}

%include "numpy.i"

%init %{
import_array();
%}

%apply (unsigned char* IN_ARRAY2, int DIM1, int DIM2) {(unsigned char* imgData, unsigned int h, unsigned int w)}
%apply (unsigned char* IN_ARRAY2, int DIM1, int DIM2) {(unsigned char* mask_data, int h2, int w2)}
%apply (int* IN_ARRAY1, int DIM1) {(int* ref, unsigned int points_num)}
%apply (float* INPLACE_ARRAY1, int DIM1) {(float* H_matrix,int num)}

%include "visp.h"

2. swig 编译visp.i 文件生成C++和py代码,生成visp_wrap.cxx,visp.py

1
swig -c++ -python -py3 visp.i //python3

3. 分别编译visp.cc和visp_wrap.cxx代码

1
2
g++  -O2 -fPIC  -c visp.cc -I/home/terse/code/terse-visp/VispSource/build/include
g++ -O2 -fPIC -c visp_wrap.cxx -I/home/terse/anaconda3/include/python3.6m -I/home/terse/code/terse-visp/VispSource/build/include -I//home/terse/anaconda3/lib/python3.6/site-packages/numpy/core/include/

4. 链接生成_visp.so文件

1
2
3
g++ -shared visp_wrap.o visp.o -L/home/terse/code/terse-visp/VispSource/build/lib -lvisp_ar -lvisp_blob -lvisp_core -lvisp_detection -lvisp_core -lvisp_gui -lvisp_imgproc -lvisp_io -lvisp_klt -lvisp_mbt -lvisp_me -lvisp_robot -lvisp_sensor -lvisp_tt -lvisp_tt_mi -lvisp_vision -lvisp_visual_features -lvisp_vs -lvisp_tt  -lvisp_ar -lvisp_blob -lvisp_core -lvisp_detection -lvisp_core -lvisp_gui -lvisp_imgproc -lvisp_io -lvisp_klt -lvisp_mbt -lvisp_me -lvisp_robot -lvisp_sensor -lvisp_tt -lvisp_tt_mi -lvisp_vision -lvisp_visual_features -lvisp_vs -Wl,-Bstatic -L/home/terse/code/terse-visp/opencv-3.4.6/build/lib -lopencv_dnn -lopencv_ml -lopencv_objdetect -lopencv_shape -lopencv_stitching -lopencv_superres -lopencv_videostab -lopencv_calib3d -lopencv_features2d -lopencv_highgui -lopencv_videoio -lopencv_imgcodecs -lopencv_video -lopencv_photo -lopencv_imgproc -lopencv_flann -lopencv_core -Wl,-Bstatic -L/home/terse/code/terse-visp/opencv-3.4.6/build/share/OpenCV/3rdparty/lib -littnotify -llibprotobuf -llibjasper -lquirc -lippiw -lippicv -Wl,-Bdynamic -lpython3.7m -Wl,-Bdynamic  -llapack  -fopenmp -ldl  -lz -lrt -ltiff -o _visp.so


  这里有个坑纠结了挺久了,最开始生成的_visp.so文件中通过ldd -r 查看一直有几个未定义的符号。排查后发现来自opencv_core库中,通过nm查看,发现libopencv_core.a中是未定义的,而libopencv_sore.so是正常的。最后发现那几个未定义的符号在/home/terse/code/terse-visp/opencv-3.4.6/build/3rdparty/lib/libittnotify.a库中,链接时加入这个库就将未定义的符号解决了。这个链接文件中有许多依赖的库是不需要的,这里就没有仔细排查了,只要没少就能链接成功。

简单测试

  通过ldd -r 检查_visp.so文件没有问题,理论上就没什么问题里,这里通过代码中故意遗留的函数测试一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import visp
import cv2
import numpy as np

def get_frame(cap, frame_index):
pos = cap.get(cv2.CAP_PROP_POS_FRAMES)
if pos != frame_index:
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_index)
ret, frame = cap.read()
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
return gray

if __name__ == '__main__':
video_name = "./test_data/1166/input.mp4"

ref_area = [105, 73, 479, 62, 126, 309, 120, 297, 471, 57, 457, 291]
key_frame = 5520

cap = cv2.VideoCapture(video_name) #video name
tracker = visp.TemplateTracker()
tracker.SetLambda(0.001)
tracker.SetPyramidal(3,0)
tracker.SetIterationMax(200)
tracker.SetSampling(1,1) #x和y方向的降采样率
tracker.say_hello()
img = get_frame(cap, key_frame)
ret_code = tracker.Init(img,ref_area,True)

H_array = np.empty(9,dtype=np.float32)
img = get_frame(cap, key_frame+1)
ret_code = tracker.ComputeH(img,H_array)
print(ret_code,H_array)

  最近在做一些算法优化的工作,由于对Python认识不够,开始的入坑使用了多线程。发现在一个四核机器,即使使用多线程,CPU使用率始终在100%左右(一个核)。后来发现Python中并行计算要使用多进程,改成多进程模式后,CPU使用率达到340%,也提升了算法的效率。另外multiprocessing对多线程和多进程做了很好的封装,需要掌握。这里总结下面两个问题:

  1. Python中的并行计算为什么要使用多进程?
  2. Python多线程和多进程简单测试
  3. multiprocessing库的使用

Python中的并行计算为什么要使用多进程?

  Python在并行计算中必须使用多进程的原因是GIL(Global Interpreter Lock,全局解释器锁)。GIL使得在解释执行Python代码时,会产生互斥锁来限制线程对共享资源的访问,直到解释器遇到I/O操作或者操作次数达到一定数目时才会释放GIL。这使得Python一个进程内同一时间只能允许一个线程进行运算”,也就是说多线程无法利用多核CPU。因此:

  1. 对于CPU密集型任务(循环、计算等),由于多线程触发GIL的释放与在竞争,多个线程来回切换损耗资源,因此多线程不但不会提高效率,反而会降低效率。所以计算密集型程序,要使用多进程
  2. 对于I/O密集型代码(文件处理、网络爬虫、sleep等待),开启多线程实际上是**并发(不是并行)**,线程A在IO等待时,会切换到线程B,从而提升效率。
  3. 大多数程序包含CPU和IO操作,但不考虑进程的资源开销,多进程通常都是优于多线程的
  4. 由于Python多线程的问题,因此通常情况下都使用多进程,使用多进程需要注意进程间变量的共享。

Python多线程和多进程简单测试

  • job1是一个完成CPU没有任务IO的死循环,观察CPU使用率,无论使用多少线程数量num,CPU使用率始终在100%左右,也就是说只能利用核的资源。而多进程则可以使用多核资源,num为1时CPU使用率为100%,num为2时CPU使用率接近200%。
  • job2是一个IO密集型的程序,主要的耗时在print系统调用。num=4时,多线程跑了10.81s,cpu使用率93%;多进程只用了3.23s,CPU使用率130%。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import multiprocessing
import threading

def job1():
'''
full cpu
'''
while True:
continue

NUMS = 100000
def job2():
'''
cpu and io
'''
for i in range(NUMS):
print("hello,world")

def multi_threads(num,job):
threads = []
for i in range(num):
t = threading.Thread(target=job,args=())
threads.append(t)
for t in threads:
t.start()
for t in threads:
t.join()

def multi_process(num,job):
process = []
for i in range(num):
p = multiprocessing.Process(target=job,args=())
process.append(p)
for p in process:
p.start()
for p in process:
p.join()

if __name__ == '__main__':
# multi_threads(4,job1)
# multi_process(4,job1)
# multi_threads(4,job2)
multi_process(4,job2)

multiprocessing的使用

参考:https://docs.python.org/3/library/multiprocessing.html#module-multiprocessing

  1. 单个进程multiprocessing.Process对象,和threading.Thread的API完全一样,start(),join(),参考上文中的测试代码。
  2. 进程池
  3. 进程间对象共享队列multiprocessing.Queue()
  4. 进程同步multiprocessing.Lock()
  5. 进程间状态共享multiprocessing.Value,multiprocessing.Array
  6. multiprocessing.Manager()
  7. 进程池:multiprocessing.Pool()
  • pool.map
  • pool.imap_unordered
  • pool.apply_async

  Python多线程和多进程的使用非常方面,因为multiprocessing提供了非常好的封装。为了方便设置线程和进程的数量,通常都会使用池pool技术。

1
2
from multiprocessing.dummy import Pool as DummyPool   # thread pool
from multiprocessing import Pool # process pool

multilprocessing包的使用可参考:

速查导航

阅读时间: 60 分钟 | 难度: ⭐⭐⭐⭐⭐ | 面试频率: 极高

核心考点速查:


多查看文档
MySQL 5.7 Reference Manual

数据建模

https://vertabelo.com/blog/types-data-models/
https://blog.csdn.net/zhulangfly/article/details/130432124
https://aws.amazon.com/cn/what-is/data-modeling
https://www.qlik.com/us/data-modeling

基本使用

如何建表?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE TABLE `hotel_info_tab` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`hotel_id` bigint(20) NOT NULL DEFAULT '0',
`hotel_name` varchar(64) NOT NULL DEFAULT '',
`area_code` varchar(64) NOT NULL DEFAULT '',
`phone_no` varchar(24) NOT NULL DEFAULT '',
`address` text,
`star_rating` varchar(16) NOT NULL DEFAULT '',
`popularity_score` int(11) NOT NULL DEFAULT '0',
`longitude` varchar(64) NOT NULL DEFAULT '',
`latitude` varchar(64) NOT NULL DEFAULT '',
`policies` text,
`ext_info` text,
`update_time` bigint(20) NOT NULL DEFAULT '0',
`create_time` bigint(20) NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `uidx_hotel_id` (`hotel_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=COMPRESSED

类型选择?

  • 数值类型:int,tinyint,int(10),bigint
  • 定点数(exact-value),decimal,使用字符串存储,精度
  • 浮点数(approximate-value (floating-point)):float,double,精度缺失
  • 字符串: varchar(256),char(10)(定长,根据需要使用空格填充)
  • 文本: text,json
    1
    2
    JSON 数据类型提供了数据格式验证和以及一些内置函数帮助查询和检索。
    JSON数据类型更适合存储和处理结构化的JSON数据,而TEXT数据类型更适合存储纯文本字符串。如果你需要在数据库中存储和操作JSON数据,并且使用MySQL 5.7及更高版本,那么JSON数据类型是更好的选择。如果你只需要存储普通的文本字符串,而不需要对JSON数据进行特殊处理,那么TEXT数据类型就足够了
  • 时间time:建表时通常会带上create_time,update_time,datetime,timestamp类型,有时也会用int32和int64的时间戳类型
    1
    2
    `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
    `update_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
    通常存储的都是时间戳,需要考虑使用mysql服务器的时间还是业务的时间戳,考虑使用mysql时间戳是否会有不利的影响

primary key

  • 主键PRIMARY KEY。数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。主键是数据库确保数据行在整张表唯一 性的保障,即使业务上本张表没有主键,也建议添加一个自增长的ID列作为主键。设定了主键之后,在后续的删改查的时候可能更加快速以及确保操作数据范围安全。
  • 自增主键还是UUID?优缺点?怎么生成UUID?,比如item表使用自增ID,order表使用订单id,订单id可以认为是uuid。

unique key

  • 唯一性约束UNIQUE KEY:唯一性约束是很重要的特性,防止重复插入数据

关于FOREIGN KEY约束,不建议使用

int(10),bigint(20)

  • 整数类型的括号中的数字仅用于指定显示宽度,并不会影响存储范围或存储空
  • 显示宽度:括号中的数字用于指定在查询结果中显示整数类型字段时的字符个数。它可以控制字段在查询结果中的对齐和显示格式。例如,如果将一个整数字段定义为 int(3),并插入值 100,在查询时该字段将以 ‘100’ 的形式显示,左侧用空格填充以达到指定的宽度
  • 零填充:括号中的数字还可以与 ZEROFILL 属性一起使用,以实现零填充的效果。当整数类型字段定义为 int(3) ZEROFILL 时,如果插入的值不足指定的宽度,MySQL 将在左侧用零进行填充

编码方式

  • 编码方式utf8mb4:通过 show variables like ‘character_set_%’; 可以查看系统默认字符集。mysql中有utf8和utf8mb4两种编码,在mysql中请大家忘记utf8,永远使用utf8mb4。这是mysql的一个遗留问题,mysql中的utf8最多只能支持3bytes长度的字符编码,对于一些需要占据4bytes的文字,mysql的utf8就不支持了,要使用utf8mb4才行
  • COLLATE=utf8mb4_unicode_ci,所谓utf8_unicode_ci,其实是用来排序的规则。对于mysql中那些字符类型的列,如VARCHAR,CHAR,TEXT类型的列,都需要有一个COLLATE类型来告知mysql如何对该列进行排序和比较。简而言之,COLLATE会影响到ORDER BY语句的顺序,会影响到WHERE条件中大于小于号筛选出来的结果,会影响DISTINCTGROUP BYHAVING语句的查询结果。另外,mysql建索引的时候,如果索引列是字符类型,也会影响索引创建,只不过这种影响我们感知不到。总之,凡是涉及到字符类型比较或排序的地方,都会和COLLATE有关。
  • 行格式,row_format,(https://dev.mysql.com/doc/refman/5.7/en/innodb-row-format.html)
  • 10.9.1 The utf8mb4 Character Set (4-Byte UTF-8 Unicode Encoding)

关于 null 的使用

  • 除text类型外其它类型一般不使用null,都应该指定默认值
    在MySQL和许多其他数据库系统中,NULL是一个特殊的值,表示缺少值或未知值。虽然NULL在某些情况下是有用的,但由于它的特殊性,使用NULL可能会带来一些问题,因此在某些情况下不建议过度使用NULL。一般只有text类型回用到,其它都应该制定默认值
  1. 逻辑判断和比较的复杂性:由于NULL表示未知或缺少值,它的比较结果不是true也不是false,而是NULL。这意味着使用NULL进行逻辑判断和比较时需要额外的注意,可能需要使用IS NULL或IS NOT NULL等特殊的操作符。
  2. 聚合函数的结果处理:在使用聚合函数(如SUM、AVG、COUNT等)进行计算时,NULL的处理可能会产生意外的结果。通常情况下,聚合函数会忽略NULL值,因此如果某列中有NULL值,可能会导致计算结果不准确。
  3. 索引的使用限制:某些类型的索引在处理NULL值时可能会受到限制。例如,对于普通索引(B-tree索引)来说,NULL值并不会被索引,因此在查询时可能无法充分利用索引的性能优势。
  4. 查询语句的复杂性增加:当使用NULL值进行查询时,可能需要编写更复杂的查询语句来处理NULL的情况,这会增加查询的复杂性和维护成本。

虽然NULL有其合理的用途,例如表示缺失的数据或未知的值,但过度使用NULL可能会导致代码的复杂性增加、查询的不准确性和性能问题。在设计数据库模式和数据模型时,需要根据实际需求和业务逻辑合理使用NULL,并考虑到其带来的潜在问题。

存储引擎(Storage Engine) 选择

Setting the Storage Engine
MySQL支持多种存储引擎,每种存储引擎都有其特点和适用场景。以下是几种常见的MySQL存储引擎对比:

  • InnoDB:

    • 事务支持:InnoDB是MySQL默认的事务性存储引擎,支持ACID事务特性,适用于需要强一致性和事务支持的应用。
    • 行级锁定:InnoDB支持行级锁定,提供更好的并发性能。
    • 外键约束:InnoDB支持外键约束,可以保持数据完整性。
    • Crash Recovery:InnoDB具有崩溃恢复机制,能够在故障恢复时保证数据的一致性。
    • 适用场景:适用于高并发、需要事务支持和数据完整性的应用,如电子商务、在线交易等。
  • MyISAM:

    • 速度和性能:MyISAM对于读取操作有很好的性能表现,适用于读取频繁的应用。
    • 表级锁定:MyISAM使用表级锁定,对并发性能有一定影响。
    • 不支持事务:MyISAM不支持事务和崩溃恢复机制,不保证数据的完整性和一致性。
    • 全文索引:MyISAM支持全文索引,适用于对文本内容进行高效搜索的应用。
    • 适用场景:适用于读取频繁、对事务和数据完整性要求不高的应用,如博客、新闻等。
  • mysql存储引擎是插件式的,支持多种存储引擎,比较常用的是innodb和myisam

  • 存储结构上的不同:innodb数据和索引时集中存储的,myism数据和索引是分开存储的

  • 数据插入顺序不同:innodb插入记录时是按照主键大小有序插入,myism插入数据时是按照插入顺序保存的

  • 事务的支持:Innodb提供了对数据库ACID事务的支持,并且还提供了行级锁和外键的约束。MyIASM引擎不提供事务的支持,支持表级锁,不支持行级锁和外键。

  • 索引的不同:innodb主键索引是聚簇索引,非主键索引是非聚簇索引,myisam是非聚簇索引。聚簇索引的叶子节点就是数据节点,而myism索引的叶子节点仍然是索引节点,只不过是指向对应数据块的指针,InnoDB的非聚簇索引叶子节点存储的是主键,需要再寻址一次才能得到数据
    总结:

  • 是否需要支持事务?innodb

  • 并发写是不是很多?innoda

  • 读多,写少,追求读速度?myisam

索引选择

mysql隐式类型变换(有一次面试题:存储类型和查询类型不一致会发生什么?)

在MySQL中,隐式类型转换是指在表达式或操作中自动将一个数据类型转换为另一个数据类型。MySQL会根据一组规则来执行隐式类型转换,以便执行操作或比较不同类型的数据。以下是MySQL中的一些常见的隐式类型转换规则:

  • 数值类型之间的转换:MySQL会自动将不同数值类型之间进行隐式转换,例如将整数转换为浮点数,或将较小的数值类型转换为较大的数值类型。
  • 字符串和数值类型之间的转换:MySQL会尝试将字符串转换为数值类型,或将数值类型转换为字符串。如果字符串可以解析为有效的数值,那么它将被转换为相应的数值类型。
  • 日期和时间类型之间的转换:MySQL会自动将日期和时间类型转换为其他日期和时间类型。例如,可以将日期类型转换为字符串,或将字符串转换为日期类型。
  • NULL的处理:在与其他数据类型进行操作时,MySQL会将NULL隐式转换为适当的数据类型。例如,NULL与数值类型相加时会被转换为0

mysql 线上DDL表结构变更注意事项

在MySQL中进行字段类型修改、增加字段、增加索引和删除索引时,需要注意以下事项:

  • 数据备份:在进行任何结构变更之前,务必备份数据库的数据。这样可以在出现意外情况或错误时恢复数据。
  • 考虑数据类型转换:如果要修改字段的数据类型,需要考虑可能的数据类型转换问题。确保目标数据类型能够容纳原有数据,并且进行数据类型转换时不会导致数据丢失或截断。
  • 处理依赖关系:在修改字段类型、增加字段或删除字段时,需要考虑是否存在其他对象(如视图、存储过程或触发器)依赖于该字段。如果存在依赖关系,需要先处理这些依赖关系,以免操作失败或导致不一致性。
  • 使用ALTER TABLE语句:对于字段类型修改、增加字段和删除字段操作,可以使用ALTER TABLE语句来执行。确保在执行ALTER TABLE语句之前,先检查表的当前状态和结构,以避免不必要的错误。
  • 考虑数据量和性能:在进行结构变更操作时,特别是增加字段或增加索引时,需要考虑表中的数据量和性能影响。某些操作可能需要较长时间来完成,或者会对数据库的性能产生影响。在进行这些操作时,要谨慎评估和测试,以确保不会对正常运行产生负面影响。
  • 索引的选择和删除:在增加索引时,需要根据查询需求和数据访问模式选择合适的索引类型(如B-tree索引、哈希索引等)。而在删除索引时,需要确保不会影响到相关查询的性能。在进行索引的修改和删除操作时,最好事先进行性能测试和评估。
  • 注意并发操作和锁定:某些结构变更操作可能需要锁定表或行,以确保数据的一致性。在进行这些操作时,要注意可能的并发访问冲突,并在必要时进行合理的调度和通知,以避免对系统的影响。
  • 测试和验证:在进行结构变更之后,务必进行充分的测试和验证,以确保数据库的功能和性能没有受到不良影响。验证包括执行常见的查询、操作和业务逻辑,以确保一切正常。
  • 一般要求先变更DB,再发布代码
    总之,在进行MySQL的字段类型修改、增加字段、增加索引和删除索引时,需要谨慎行事,提前做好充分的准备、备份和测试,以确保操作的成功和数据的安全性
  • 表锁定和影响:某些DDL操作可能需要锁定整个表,这可能会对其他用户的操作产生影响。请在合适的时机执行DDL操作,避免对关键业务时间或频繁访问的表造成过多的阻塞。
  • 大型表操作:对于大型表的DDL操作(如ALTER TABLE),可能会涉及大量的数据移动和重建,可能会导致长时间的操作和额外的存储空间使用。在执行这些操作之前,请确保对表的大小和操作的影响进行评估
  • 错误处理和回滚:在执行DDL操作时,要注意捕获和处理可能的错误。如果DDL操作失败,确保有适当的错误处理机制和回滚策略,以保持数据的一致性
  • 数据库备份:在执行重要的DDL操作之前,请确保对数据库进行备份,以防操作出现问题导致数据丢失或不可恢复。这可以帮助你在需要时还原到先前的状态

mysql架构扩展

关系型数据库扩展包括许多技术:主从复制主主复制联合分片非规范化SQL调优

主从复制


资料来源:可扩展性、可用性、稳定性、模式

主库同时负责读取和写入操作,并复制写入到一个或多个从库中,从库只负责读操作。树状形式的从库再将写入复制到更多的从库中去。如果主库离线,系统可以以只读模式运行,直到某个从库被提升为主库或有新的主库出现。主要的优缺点: - 读写分离提供集群的性能 - 主、从多节点,宕机容灾 - 将从库提升为主库需要额外的逻辑 - 主从延时问题,需要监控

主主复制,多主复制


资料来源:可扩展性、可用性、稳定性、模式

两个主库都负责读操作和写操作,写入操作时互相协调。如果其中一个主库挂机,系统可以继续读取和写入。

  • 多主复制
    优缺点:
  • 你需要添加负载均衡器或者在应用逻辑中做改动,来确定写入哪一个数据库。
  • 多数主-主系统要么不能保证一致性(违反 ACID),要么因为同步产生了写入延迟。
  • 随着更多写入节点的加入和延迟的提高,如何解决冲突显得越发重要
  • 多活架构

联合(垂直分实例,比如商品实例、订单实例等分开)


资料来源:扩展你的用户数到第一个一千万

优缺点:
联合(或按功能划分)将数据库按对应功能分割。例如,你可以有三个数据库:论坛用户产品,而不仅是一个单体数据库,从而减少每个数据库的读取和写入流量,减少复制延迟。较小的数据库意味着更多适合放入内存的数据,进而意味着更高的缓存命中几率。没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。

  • 如果你的数据库模式需要大量的功能和数据表,联合的效率并不好。
  • 你需要更新应用程序的逻辑来确定要读取和写入哪个数据库。
  • 从两个库联结数据更复杂。
  • 联合需要更多的硬件和额外的复杂度。

分片 (水平分实例,比如订单按照用户shard)


资料来源:可扩展性、可用性、稳定性、模式

https://www.digitalocean.com/community/tutorials/understanding-database-sharding

分片将数据分配在不同的数据库上,使得每个数据库仅管理整个数据集的一个子集。以用户数据库为例,随着用户数量的增加,越来越多的分片会被添加到集群中。
类似联合的优点,分片可以减少读取和写入流量,减少复制并提高缓存命中率。也减少了索引,通常意味着查询更快,性能更好。如果一个分片出问题,其他的仍能运行,你可以使用某种形式的冗余来防止数据丢失。类似联合,没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。
常见的做法是用户姓氏的首字母或者用户的地理位置来分隔用户表。

  • 你需要修改应用程序的逻辑来实现分片,这会带来复杂的 SQL 查询。
  • 分片不合理可能导致数据负载不均衡。例如,被频繁访问的用户数据会导致其所在分片的负载相对其他分片高。
  • 再平衡会引入额外的复杂度。基于一致性哈希的分片算法可以减少这种情况。
  • 联结多个分片的数据操作更复杂。
  • 分片需要更多的硬件和额外的复杂度。
  • 分片时代来临
  • 数据库分片架构
  • 一致性哈希

分表/分库/历史数据归档和路由

原文链接:https://juejin.cn/post/6844903872134135816

  • 今天,探讨一个有趣的话题:MySQL 单表数据达到多少时才需要考虑分库分表?有人说 2000 万行,也有人说 500 万行。那么,你觉得这个数值多少才合适呢?
    曾经在中国互联网技术圈广为流传着这么一个说法:MySQL 单表数据量大于 2000 万行,性能会明显下降。事实上,这个传闻据说最早起源于百度。具体情况大概是这样的,当年的 DBA 测试 MySQL性能时发现,当单表的量在 2000 万行量级的时候,SQL 操作的性能急剧下降,因此,结论由此而来。然后又据说百度的工程师流动到业界的其它公司,也带去了这个信息,所以,就在业界流传开这么一个说法。
    再后来,阿里巴巴《Java 开发手册》提出单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。对此,有阿里的黄金铁律支撑,所以,很多人设计大数据存储时,多会以此为标准,进行分表操作。那么,你觉得这个数值多少才合适呢?为什么不是 300 万行,或者是 800 万行,而是 500 万行?也许你会说这个可能就是阿里的最佳实战的数值吧?那么,问题又来了,这个数值是如何评估出来的呢?稍等片刻,请你小小思考一会儿。事实上,这个数值和实际记录的条数无关,而与 MySQL 的配置以及机器的硬件有关。因为,MySQL 为了提高性能,会将表的索引装载到内存中。InnoDB buffer size 足够的情况下,其能完成全加载进内存,查询不会有问题。但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降。当然,这个还有具体的表结构的设计有关,最终导致的问题都是内存限制。这里,增加硬件配置,可能会带来立竿见影的性能提升哈。
    那么,我对于分库分表的观点是,需要结合实际需求,不宜过度设计,在项目一开始不采用分库与分表设计,而是随着业务的增长,在无法继续优化的情况下,再考虑分库与分表提高系统的性能。对此,阿里巴巴《Java 开发手册》补充到:如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表。那么,回到一开始的问题,你觉得这个数值多少才合适呢?我的建议是,根据自身的机器的情况综合评估,如果心里没有标准,那么暂时以 500 万行作为一个统一的标准,相对而言算是一个比较折中的数值。

案例1. 酒店分表:

  • 酒店数量100w, 支持8中语言,2000kw种房型,1亿的图片。支持未来3年可能扩展成:酒店数量500w, 支持8钟语言,房型1亿,图片5亿
  • 分表方式:hotel 1张表,多语言表10张表,房型表20张,图片表:100张表
  • 酒店和多语言文本垂直分表
  • 根据酒店id水平分表。
  • 如果还要继续扩展,可以重新搞一个库,酒店id从500w开始,不断扩展。增加一个数据路由的模块。

案例2. 订单分表和历史订单归档(3个月或者更长时间)

案例3. 数据历史版本记录、快照表

  • 在有些场景中,数据变更不回特别频繁,特别是人工变更时,记录数据版本和快照是非常好的习惯,方便追溯历史行为记录
  • 数据变更时通常会先写入快照表或者历史记录表,通常在业务代码中实现
  • 有时也会采用mysql 存储过程实现:https://blog.csdn.net/wcdunf/article/details/129792810

案例4. 商品库存扣减方案

索引

基础内容

  • 关于MySQL索引那些事
  • 什么是索引,对索引的理解,索引时一种数据结构,通过增加索引通常可以提高数据库查询的效率,但是为了维护索引结构也会降低数据更新的效率和增加一些存储代价。
  • 索引类型
    1
    2
    3
    4
    5
    普通索引(INDEX):最基本的索引,没有任何限制
    唯一索引(UNIQUE):与"普通索引"类似,不同的就是:索引列的值必须唯一,但允许有空值。
    主键索引(PRIMARY):它 是一种特殊的唯一索引,不允许有空值。
    全文索引(FULLTEXT ):仅可用于 MyISAM 表, 用于在一篇文章中,检索文本信息的, 针对较大的数据,生成全文索引很耗时好空间。
    组合索引:为了更多的提高mysql效率可建立组合索引,遵循”最左前缀“原则。
  • 理解主键索引和普通索引、聚簇索引和非聚簇索引、单列索引和联合索引、覆盖索引和回表
    1
    2
    3
    4
    5
    - 主键索引和普通索引。数据和主键索引用B+Tree来组织的,没有主键innodb会生成唯一列,类似于rowid。InnoDB非主键索引的叶子节点存储的是主键
    - 单列索引和联合索引,联合索引的存储结构,联合索引的左前缀原则
    - 聚簇索引和非聚簇索引,聚簇索引数据和索引一起存储,非聚簇索引在无法做到索引覆盖的情况下需要回表
    - 覆盖索引。覆盖索引(covering index)指一个查询语句的执行只用从索引中就能够取得,不必从数据表中读取。也可以称之为实现了索引覆盖。
    如果一个索引包含了(或覆盖了)满足查询语句中字段与条件的数据就叫做覆盖索引
  • 索引的数据结构,红黑树、B树、B+树的比较
  • 面试题:InnoDB中一棵B+树能存多少行数据?计算innob的高度

面试必问:B+ 树 vs 红黑树 vs B 树

维度 B+ 树 红黑树 B 树
高度 3-4 层(千万级数据) 20+ 层 3-4 层
磁盘 IO 3-4 次 20+ 次 4-5 次
范围查询 高效(叶子节点链表) 低效(中序遍历) 低效(需回溯)
数据存储 仅叶子节点存数据 每个节点存数据 每个节点存数据
稳定性 高(所有查询深度一致) 中(查询深度不一致)

面试标准答案(30 秒)

MySQL 使用 B+ 树而非红黑树的原因:1)B+ 树高度低(3-4 层),磁盘 IO 少;2)叶子节点有序链表,支持高效范围查询;3)所有数据在叶子节点,非叶子节点只存 key,可以存更多索引,进一步降低高度。

详细对比

1. 为什么不用红黑树?

  • 高度问题:红黑树是二叉树,100 万条数据约需 20 层,意味着 20 次磁盘 IO
  • 范围查询慢:需要中序遍历,无法利用顺序结构
  • 数据分散:数据分散在各个节点,无法批量读取

示例

1
2
3
4
5
6
7
红黑树(100 万数据):
50
/ \
25 75
/ \ / \
... ... ...
↓ 需要遍历 20 层才能找到叶子节点

2. 为什么不用 B 树?

  • 数据分散:B 树的数据分布在所有节点,而 B+ 树只在叶子节点存数据
  • 范围查询慢:B 树需要”中序遍历”整棵树,B+ 树只需遍历叶子节点链表
  • 缓存效率低:非叶子节点存数据,导致索引占用空间大,缓存命中率低

B 树 vs B+ 树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
B 树(每个节点存数据):
┌───────────┐
│ 10 30 │ ← 非叶子节点也存数据
└─┬───┬───┬─┘
│ │ │
... │ ...

┌─────┐
│ 20 │ ← 叶子节点
└─────┘

B+ 树(只有叶子节点存数据):
┌───────────┐
│ 10 30 │ ← 非叶子节点只存 key
└─┬───┬───┬─┘
│ │ │
... │ ...

┌─────┐ → ┌─────┐ → ┌─────┐ ← 叶子节点有序链表
│ 10 │ │ 20 │ │ 30 │
└─────┘ └─────┘ └─────┘

3. B+ 树的优势

  • 更矮的树:非叶子节点不存数据,只存 key,一个页可以存更多索引,树更矮
  • 高效范围查询:叶子节点有序链表,扫描连续数据只需顺序读取
  • 稳定查询性能:所有查询都到达叶子节点,深度一致

示例:B+ 树能存多少数据?

假设:

  • InnoDB 页大小:16KB
  • 主键 bigint:8 字节
  • 指针:6 字节
  • 每行数据:1KB

非叶子节点(只存 key + 指针)

  • 每个索引项:8 + 6 = 14 字节
  • 每页可存:16KB / 14B ≈ 1170 个索引

叶子节点(存完整数据)

  • 每页可存:16KB / 1KB = 16 行数据

3 层 B+ 树可存数据

  • 第 1 层(根):1 页,1170 个索引
  • 第 2 层:1170 页,1170 × 1170 个索引
  • 第 3 层(叶子):1170 × 1170 页,每页 16 行
  • 总计:1170 × 1170 × 16 ≈ 2000 万行

面试追问:为什么 MySQL 查询最多 3-4 次磁盘 IO?

  • 根节点常驻内存(热数据),只需 1 次内存访问
  • 第 2 层可能在内存,也可能 1 次磁盘 IO
  • 第 3 层(叶子节点)必定 1 次磁盘 IO
  • 总计:1-2 次磁盘 IO(根节点在内存)+ 1 次叶子节点读取
  • 列出索引失效的几种场景?
    • 条件中包含or
    • 条件中包含%like
    • 联合索引,违背最左匹配原则
    • 在索引列上有一些额外的计算操作
  • 联合索引和最左匹配原则

单值主键索引(无需回表)

1
2
3
4
5
6
7
8
9
10
11
12
13
1. 在主键索引 B+树中查找 id=25
2. 找到叶子节点,直接读取完整行数据
3. 返回结果

┌─────────────┐
│ 主键索引树 │
│ 查找id=25 │
│ ↓ │
│ 叶子节点 │
│ (完整数据) │
└─────────────┘

磁盘IO: 2-3次 (树高度决定)

单值非索引(需要回表)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
  CREATE TABLE user (
id BIGINT PRIMARY KEY,
name VARCHAR(50),
age INT,
city VARCHAR(50),
INDEX idx_age (age) -- 单值索引
);

【单值索引 B+树】

Root (非叶子节点)
┌──────[30]──────┐
| |
[20] [40]
/ \ / \
/ \ / \
┌────┐ ┌────┐ ┌────┐ ┌────┐
│ 18 │←→│ 25 │←→ │ 35 │←→│ 45 │ ← 叶子节点(双向链表)
└────┘ └────┘ └────┘ └────┘

叶子节点详细结构:
┌────────────────────────────────┐
│ age=18 → [主键id: 1, 5, 8] │ ← 只存储 索引值+主键
├────────────────────────────────┤
│ age=20 → [主键id: 2, 10] │
├────────────────────────────────┤
│ age=25 → [主键id: 7, 9, 15] │
├────────────────────────────────┤
│ age=28 → [主键id: 3, 11] │

联合索引结构(需要回表,也可以不回表)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
                        非叶子节点(索引页)
Root
┌─────────────────────┐
│ [李四,25,上海] │
└──────┬──────────┬───┘
/ \
/ \
┌──────────┐ ┌──────────┐
│ [李四,20] │ │ [王五,30] │
└─────┬────┘ └─────┬────┘
/ \ / \
/ \ / \
┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐
│张三,18,北京│ │李四,22,上海│ │李四,28,广州│ │王五,35,杭州│
│ → [1,5] │←→│ → [2,7] │←→│ → [3,10] │←→│ → [4,6] │
└───────────┘ └───────────┘ └───────────┘ └───────────┘
↑ ↑
└────────────── 双向链表 ────────────────────────┘

叶子节点(数据页,存储完整索引值 + 主键)


INDEX idx_name_age_city (name, age, city)

-- 索引列大小计算
name: VARCHAR(50) ≈ 50 bytes (假设平均长度)
age: INT = 4 bytes
city: VARCHAR(50) ≈ 50 bytes
主键: BIGINT = 8 bytes
-----------------------------------------
每个索引项总大小 ≈ 112 bytes


每个索引页能存储的索引项数 = 页大小 / (索引列大小 + 指针大小)
= 16384 / (104 + 6)
= 16384 / 110
≈ 149 条


每个数据页能存储的索引项数 = 页大小 / (索引列大小 + 主键大小)
= 16384 / (104 + 8)
= 16384 / 112
≈ 146 条

两层: 149 * 146 = 2w
三层: 149 * 2w = 300w

事务和并发控制

事务以及事务之间的隔离属性

  • 精读innodb事务的ACID特性,以及其对应的实现原理?
    • 原子性:在很多场景中,一个操作需要执行多条 update/insert SQL。原子性保证了SQL语句要么全执行,要么全不执行,是事务最核心的特性,事务本身就是以原子性来定义的;实现主要基于undolog/redolog
    • 持久性:保证事务提交后不会因为宕机等原因导致数据丢失;实现主要基于redo log
    • 隔离性:保证事务执行尽可能不受其他事务影响;InnoDB默认的隔离级别是RR,RR的实现主要基于锁机制(包含next-key lock)、MVCC(包括数据的隐藏列、基于undo log的版本链、ReadView)
    • 一致性:事务追求的最终目标,一致性的实现既需要数据库层面的保障,也需要应用层面的保障
  • innodb四种隔离属性以及分别会产生什么问题?分别举例说明
    • 读未提交(READ UNCOMMITTED),会产生脏读问题
    • 读提交,READ-COMMITTED,会产生不可重复读问题
    • 可重复读 (REPEATABLE READ),幻读问题(insert),mysql 默认的事务隔离级别
    • SERIALIZABLE(可串行化)
  • 事务的隔离属性底层实现原理,关于锁和mvcc
    • 可以先阐述四种隔离级别,再阐述它们的实现原理。隔离级别就是依赖锁和MVCC实现的

MVCC(多版本并发控制)深度解析

面试高频追问:MVCC 是如何实现的?

MVCC(Multi-Version Concurrency Control)是 MySQL InnoDB 实现读已提交(RC)可重复读(RR)隔离级别的核心机制,通过维护数据的多个版本,实现读不加锁,大幅提升并发性能。

MVCC 三大核心组件

组件 作用 面试话术
隐藏列 每行记录包含 trx_id(事务 ID)和 roll_pointer(回滚指针) “记录谁修改的、怎么回滚”
undo log 存储旧版本数据,形成版本链 “历史快照链”
ReadView 判断哪些版本对当前事务可见 “可见性规则”

1. 隐藏列

每行记录实际包含 3 个隐藏列:

1
2
| DB_TRX_ID(6字节) | DB_ROLL_PTR(7字节) | DB_ROW_ID(6字节) |
| 最后修改的事务ID | 回滚指针(指向undo log) | 隐藏主键(无主键时) |

示例

1
2
3
4
5
6
7
8
9
10
11
-- 逻辑表结构
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(50),
age INT
);

-- 实际物理存储(包含隐藏列)
| id | name | age | DB_TRX_ID | DB_ROLL_PTR | DB_ROW_ID |
|----|-------|-----|-----------|-------------|-----------|
| 1 | Alice | 25 | 100 | 0x7f3a... | NULL |

2. undo log 版本链

每次 UPDATE 操作都会生成一条 undo log,通过 DB_ROLL_PTR 形成版本链

示例

1
2
3
4
5
6
7
8
-- 初始数据
INSERT INTO user (id, name, age) VALUES (1, 'Alice', 25); -- trx_id=100

-- 事务 101 修改
UPDATE user SET age = 26 WHERE id = 1; -- trx_id=101

-- 事务 102 修改
UPDATE user SET age = 27 WHERE id = 1; -- trx_id=102

版本链

1
2
3
4
5
6
7
当前版本(id=1, age=27, trx_id=102)
↓ DB_ROLL_PTR
undo log(id=1, age=26, trx_id=101)
↓ DB_ROLL_PTR
undo log(id=1, age=25, trx_id=100)

NULL

3. ReadView(可见性判断)

**ReadView 是事务开启快照读时创建的”可见性规则”**,包含 4 个关键字段:

字段 含义
m_ids 当前活跃事务 ID 列表
min_trx_id 活跃事务中最小的事务 ID
max_trx_id 系统下一个将要分配的事务 ID
creator_trx_id 当前事务 ID

可见性判断规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func IsVisible(trx_id int64, readView *ReadView) bool {
// 规则1:如果记录的 trx_id == 当前事务ID,说明是自己修改的,可见
if trx_id == readView.creator_trx_id {
return true
}

// 规则2:如果 trx_id < min_trx_id,说明事务已提交,可见
if trx_id < readView.min_trx_id {
return true
}

// 规则3:如果 trx_id >= max_trx_id,说明事务在ReadView创建后才开启,不可见
if trx_id >= readView.max_trx_id {
return false
}

// 规则4:如果 trx_id 在 m_ids 中,说明事务未提交,不可见
if contains(readView.m_ids, trx_id) {
return false
}

// 规则5:否则可见
return true
}

RC vs RR 的 ReadView 差异

隔离级别 ReadView 创建时机 现象
RC 每次 SELECT 都创建新 ReadView 能读到其他事务已提交的修改(不可重复读)
RR 事务第一次 SELECT 创建 ReadView 只能读到事务开始时的快照(可重复读)

示例(RR 隔离级别)

1
2
3
4
5
6
7
-- 时间线
T1: BEGIN; -- trx_id=100
T2: BEGIN; -- trx_id=101
T3:
T4: T1: SELECT * FROM user WHERE id = 1; -- 读到 age=25, 创建ReadView(min=100, max=102, m_ids=[100,101])
T5: T2: UPDATE user SET age = 26 WHERE id = 1; COMMIT; -- trx_id=101 提交
T6: T1: SELECT * FROM user WHERE id = 1; -- 仍然读到 age=25(因为ReadView不变)

面试追问:为什么 T6 还是读到 age=25?

  • T1 的 ReadView 在 T4 时创建,m_ids=[100,101]
  • T6 读取时,记录的 trx_id=101 仍在 m_ids
  • 根据规则 4,trx_id=101 不可见,沿着 undo log 版本链找到 trx_id=100 的版本(age=25)

Next-Key Lock 与幻读

幻读:同一事务中,前后两次查询到的行数不一致

示例

1
2
3
4
5
6
-- 事务 A
BEGIN;
SELECT * FROM user WHERE age > 20; -- 查到 3 条记录
-- 此时事务 B 插入一条 age=22 的记录并提交
SELECT * FROM user WHERE age > 20; -- 查到 4 条记录(幻读!)
COMMIT;

Next-Key Lock = Record Lock + Gap Lock

  • Record Lock:锁定记录本身
  • Gap Lock:锁定记录之间的”间隙”,防止插入

示例

1
2
3
4
5
6
7
-- 假设索引值:10, 20, 30
SELECT * FROM user WHERE age = 20 FOR UPDATE;

-- 锁定范围:
-- Record Lock:age=20
-- Gap Lock:(10, 20) 和 (20, 30)
-- 即 Next-Key Lock:(10, 30]

面试话术

RR 隔离级别通过 MVCC 解决不可重复读,通过 Next-Key Lock 解决幻读

悲观锁

行级锁 (SELECT … FOR UPDATE)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 基础用法
START TRANSACTION;
SELECT * FROM inventory WHERE product_id = 1001 FOR UPDATE;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 1001;
COMMIT;

-- 锁定多行
SELECT * FROM inventory WHERE product_id IN (1001, 1002) FOR UPDATE;

-- 排他锁(写锁)
SELECT * FROM orders WHERE order_id = 'ORD123' FOR UPDATE;

-- 共享锁(读锁) - 允许其他事务读取,但不允许修改
SELECT * FROM orders WHERE order_id = 'ORD123' LOCK IN SHARE MODE;
-- 或 MySQL 8.0+ 新语法
SELECT * FROM orders WHERE order_id = 'ORD123' FOR SHARE;

注意悲观锁的范围

1
2
3
4
5
6
7
8
-- 精确匹配:只锁定符合条件的行
SELECT * FROM orders WHERE id = 123 FOR UPDATE;

-- 范围查询:锁定范围内的行 + Gap Lock
SELECT * FROM orders WHERE id > 100 AND id < 200 FOR UPDATE;

-- 无索引:全表锁
SELECT * FROM orders WHERE remark = 'test' FOR UPDATE; -- 如果remark无索引

注意加锁顺序,避免死锁风险

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// ❌ 容易死锁的写法
// 事务1: 锁定商品A -> 锁定商品B
// 事务2: 锁定商品B -> 锁定商品A

// ✅ 正确:统一加锁顺序
func LockMultipleProducts(ctx context.Context, productIDs []int64) error {
// 排序后按顺序加锁
sort.Slice(productIDs, func(i, j int) bool {
return productIDs[i] < productIDs[j]
})

for _, pid := range productIDs {
_, err := tx.ExecContext(ctx,
"SELECT * FROM inventory WHERE product_id = ? FOR UPDATE", pid)
if err != nil {
return err
}
}
return nil
}

注意索引实效导致锁表

1
2
3
4
5
-- ❌ 危险:如果product_sku没有索引,会锁整张表
SELECT * FROM inventory WHERE product_sku = 'SKU123' FOR UPDATE;

-- ✅ 正确:确保WHERE条件使用索引
SELECT * FROM inventory WHERE product_id = 1001 FOR UPDATE; -- product_id有索引

账户扣款悲观锁案例(user1->user2)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

func TransferMoney(ctx context.Context, fromUserID, toUserID int64, amount decimal.Decimal) error {
tx, err := db.BeginTx(ctx, nil)
if err != nil {
return err
}
defer tx.Rollback()

// 锁定两个账户(注意:按照ID顺序加锁避免死锁)
accounts := []int64{fromUserID, toUserID}
sort.Slice(accounts, func(i, j int) bool { return accounts[i] < accounts[j] })

var balances = make(map[int64]decimal.Decimal)
for _, uid := range accounts {
var balance decimal.Decimal
err = tx.QueryRowContext(ctx,
"SELECT balance FROM wallet WHERE user_id = ? FOR UPDATE", uid).Scan(&balance)
if err != nil {
return err
}
balances[uid] = balance
}

// 检查余额
if balances[fromUserID].LessThan(amount) {
return errors.New("insufficient balance")
}

// 扣款和入账
_, err = tx.ExecContext(ctx,
"UPDATE wallet SET balance = balance - ? WHERE user_id = ?",
amount, fromUserID)
if err != nil {
return err
}

_, err = tx.ExecContext(ctx,
"UPDATE wallet SET balance = balance + ? WHERE user_id = ?",
amount, toUserID)
if err != nil {
return err
}

return tx.Commit()
}

锁等待监控

1
2
3
4
5
6
7
8
-- 查看当前锁等待情况
SELECT * FROM information_schema.innodb_locks;
SELECT * FROM information_schema.innodb_lock_waits;
SELECT * FROM performance_schema.data_locks; -- MySQL 8.0+

-- 设置锁等待超时
SET innodb_lock_wait_timeout = 5; -- 默认50秒

其它

  • Select for update使用详解 及在库存和金钱系统上的应用
  • 悲观锁:悲观锁是一种保守的并发控制机制,它假设在并发访问中会发生冲突,因此在访问数据之前会锁定资源,阻止其他事务对资源进行修改。在MySQL中,悲观锁主要通过以下方式实现:
    • 使用SELECT … FOR UPDATE语句:在读取数据时对所选行进行锁定,确保其他事务不能对这些行进行修改。
    • 使用LOCK TABLES语句:锁定整个表,防止其他事务对该表进行读取和修改。

乐观锁 (低并发,冲突少)

基于版本号机制

检查 affected_rows
使用场景:

  1. 低并发场景
  2. 商品信息和价格变更
  3. 订单状态更新
  4. 个人信息变更
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 表结构
CREATE TABLE product_info (
id BIGINT PRIMARY KEY,
name VARCHAR(100),
price DECIMAL(10,2),
version INT NOT NULL DEFAULT 0, -- 版本号
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

-- 更新操作
UPDATE product_info
SET price = 299.00, version = version + 1
WHERE id = 1001 AND version = 5; -- 当前版本号是5

-- 检查 affected_rows
-- affected_rows = 0 表示更新失败(版本号已变化)
-- affected_rows = 1 表示更新成功

必须检查 affected_rows

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ❌ 错误:没有检查影响行数
_, err := db.Exec("UPDATE ... WHERE id = ? AND version = ?", id, version)
if err != nil {
return err
}
// 即使version不匹配,err也是nil,但实际没更新成功!

// ✅ 正确:检查affected_rows
result, err := db.Exec("UPDATE ... WHERE id = ? AND version = ?", id, version)
if err != nil {
return err
}
affected, _ := result.RowsAffected()
if affected == 0 {
return errors.New("version conflict or record not found")
}

避免ABA 问题,版本号不能回退

1
2
3
4
// 场景: version从5变到6再变回5
// 解决方案1: 不允许版本号回退
// 解决方案2: 使用时间戳 + 版本号
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
  • 乐观锁是一种乐观的并发控制机制,它假设在并发访问中不会发生冲突,允许多个事务同时访问资源。当提交事务时,系统会检查资源是否被其他事务修改,如果检测到冲突,则回滚事务。在MySQL中,乐观锁通常通过以下方式实现:
  • 使用版本号或时间戳:在数据表中增加一个版本号或时间戳字段,每次修改数据时更新该字段。在提交事务时,检查版本号或时间戳是否与开始事务时的值相同,如果不同则表示发生了冲突。
  • 使用CAS(Compare and Swap)操作:在编程语言层面,通过CAS操作来比较内存中的值与预期值是否相等,如果相等则修改,否则放弃修改。
  • 乐观锁 cas_version + affected_rows。 如果affected_rows=0表示版本号已经被其它修改过,更新失败。(避免忘记检查affected_rows)

使用乐观锁和悲观锁的选择取决于应用场景和需求:悲观锁适合在并发冲突频繁的情况下,通过独占资源避免并发问题,但会对系统性能产生一定的影响。乐观锁适合在并发冲突较少的情况下,通过乐观的并发控制机制提高系统性能,但需要处理冲突的情况。在实际使用时,需要根据具体业务场景和需求选择适当的并发控制机制,并注意处理冲突和回滚事务的策略,以确保数据的一致性和完整性。

死锁问题

  • 死锁问题,如何避免死锁

    • 死锁的条件:
      • 事务并发执行:多个事务同时操作相同的数据,请求相同或不同的锁资源。
      • 锁竞争:事务之间竞争相同的资源而产生死锁。
      • 不同的锁顺序:不同的事务以不同的顺序请求锁资源,导致死锁。
    • 避免死锁的方法:
      • 统一锁资源访问顺序:对于需要操作多个锁资源的事务,保持统一的访问顺序,避免不同事务之间出现交叉的锁请求顺序
      • 减少事务持有时间:尽量将事务的持有时间缩短,减少锁资源的占用时间,降低死锁的概率。
      • 使用合理的索引:合理的索引设计可以减少查询中的锁竞争,提高并发性能,减少死锁的可能性。
      • 限制事务并发度:通过调整事务的并发度,限制同时执行的事务数量,减少锁竞争的机会。
  • 分布式事务

数据库调优

mysql 架构全景图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
  ┌─────────────────────────────────────────────────────────────────┐
│ 客户端/应用层 │
│ (MySQL Client, JDBC, PHP, Python, Go等) │
└────────────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 第一层:连接层 (Connection Layer) │
├─────────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ 连接/线程处理 │ │ 认证授权 │ │ 连接池管理 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ 功能: │
│ • TCP/IP连接管理 │
│ • 用户认证(用户名/密码/IP验证) │
│ • 权限验证(数据库、表、列级别权限) │
│ • 连接线程分配 │
│ • 连接池维护 │
└────────────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 第二层:服务层/SQL层 (SQL Layer) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ SQL Interface (SQL接口) │ │
│ │ • 接收SQL语句 │ │
│ │ • 返回查询结果 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Parser (解析器) │ │
│ │ • 词法分析:将SQL拆分成token │ │
│ │ • 语法分析:检查SQL语法是否正确 │ │
│ │ • 生成解析树(Parse Tree) │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Optimizer (查询优化器) │ │
│ │ • 逻辑优化:改写SQL、子查询优化 │ │
│ │ • 物理优化:索引选择、join方式选择 │ │
│ │ • 成本计算:基于统计信息计算执行成本 │ │
│ │ • 生成执行计划 │ │
│ └──────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Caches & Buffers (缓存) │ │
│ │ • Query Cache (查询缓存,8.0已移除) │ │
│ │ • Table Cache (表缓存) │ │
│ │ • Key Cache (索引缓存) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 第三层:存储引擎层 (Storage Engine Layer) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ InnoDB │ │ MyISAM │ │ Memory │ │
│ │ (默认引擎) │ │ (已弃用) │ │ (内存表) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ InnoDB核心组件: │
│ ┌────────────────────────────────────────────────┐ │
│ │ 内存结构 (In-Memory Structures) │ │
│ │ • Buffer Pool: 缓存数据页和索引页 │ │
│ │ • Change Buffer: 缓存二级索引的修改 │ │
│ │ • Adaptive Hash Index: 自适应哈希索引 │ │
│ │ • Log Buffer: 缓存redo log │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ 磁盘结构 (On-Disk Structures) │ │
│ │ • 表空间 (Tablespaces): 存储表和索引数据 │ │
│ │ • Redo Log: 重做日志,保证持久性 │ │
│ │ • Undo Log: 回滚日志,保证原子性和MVCC │ │
│ │ • Doublewrite Buffer: 双写缓冲,防止页断裂 │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ 后台线程 (Background Threads) │ │
│ │ • Master Thread: 主线程,调度其他线程 │ │
│ │ • IO Thread: 处理IO请求 │ │
│ │ • Purge Thread: 清理undo log │ │
│ │ • Page Cleaner Thread: 刷新脏页 │ │
│ └────────────────────────────────────────────────┘ │
│ │
└────────────────────────────┬────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────────┐
│ 第四层:文件系统层 (File System) │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 数据文件 │ │
│ │ • .ibd 文件: InnoDB表空间文件 │ │
│ │ • .frm 文件: 表结构定义文件(MySQL 8.0前) │ │
│ │ • ibdata1: 系统表空间 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 日志文件 │ │
│ │ • ib_logfile0, ib_logfile1: Redo Log物理文件 │ │
│ │ • undo logs: Undo日志文件 │ │
│ │ • binlog: 二进制日志(Server层) │ │
│ │ • error log: 错误日志 │ │
│ │ • slow query log: 慢查询日志 │ │
│ │ • general log: 通用查询日志 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

大表的分页、排序,where 过滤 深翻页问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌──────────────┬─────────────────┬──────────────────┬────────────────┐
│ 功能 │ 执行层 │ 性能影响 │ 优化建议 │
├──────────────┼─────────────────┼──────────────────┼────────────────┤
│ │ 存储引擎层 │ │ │
│ WHERE过滤 │ (索引扫描) │ 高 │ • 创建合适索引 │
│ │ + │ │ • 利用ICP优化 │
│ │ Server层 │ │ • 避免函数操作 │
│ │ (条件判断) │ │ │
├──────────────┼─────────────────┼──────────────────┼────────────────┤
│ │ │ 内存排序: 中 │ • 利用索引避免 │
│ ORDER BY排序 │ Server层 │ 磁盘排序: 低 │ • 增大buffer │
│ │ │ 索引排序: 高 │ • 减少SELECT列 │
├──────────────┼─────────────────┼──────────────────┼────────────────┤
│ │ │ 浅分页: 高 │ • 游标分页 │
│ LIMIT分页 │ Server层 │ 深分页: 低 │ • 延迟关联 │
│ │ │ (OFFSET越大越慢) │ • 避免深度分页 │
└──────────────┴─────────────────┴──────────────────┴────────────────┘
  • where:当where条件有index时,innodb会根据index过滤,否则返回全表数据由server过滤

  • 排序:server。order by

  • 分页:server层。

无索引,全表扫描,排序成本分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
SELECT * FROM employees 
ORDER BY salary DESC
LIMIT 10 OFFSET 100000;

-- 假设:employees表有1,000,000行,无索引

┌────────────────────────────────────────────────────────────────┐
│ 无索引情况下的性能成本分解 │
├────────────────────────────────────────────────────────────────┤
│ │
│ 1. 全表扫描成本 (Storage Engine Layer) │
│ ┌──────────────────────────────────────────────┐ │
│ │ • 读取1,000,000行数据 │ │
│ │ • 磁盘IO: ~10,000次页读取 (假设每页100行) │ │
│ │ • 数据传输: 存储引擎 → Server层 │ │
│ │ │ │
│ │ 耗时: ~2-3秒 │ │
│ │ 成本占比: ~5-10% │ │
│ │ 是否和OFFSET有关: ❌ 无关 (总是全表扫描) │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 2. 排序成本 (Server Layer) ⚠️ 主要瓶颈 │
│ ┌──────────────────────────────────────────────┐ │
│ │ • 对1,000,000行数据按salary排序 │ │
│ │ │ │
│ │ 情况A: 内存排序 (sort_buffer足够) │ │
│ │ 算法: 快速排序 │ │
│ │ 复杂度: O(n log n) = 1M * 20 ≈ 20M操作 │ │
│ │ 耗时: ~5-10秒 │ │
│ │ │ │
│ │ 情况B: 外部排序 (sort_buffer不够) ❌ 常见 │ │
│ │ 算法: 归并排序 │ │
│ │ 需要: 磁盘临时文件 │ │
│ │ 磁盘IO: 多次读写临时文件 │ │
│ │ 耗时: ~30-60秒 ❌❌❌ │ │
│ │ │ │
│ │ 成本占比: ~85-95% (最大瓶颈!) │ │
│ │ 是否和OFFSET有关: ❌ 无关 (总是排序全表) │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 3. OFFSET跳过成本 (Server Layer) │
│ ┌──────────────────────────────────────────────┐ │
│ │ • 遍历排序后的结果集 │ │
│ │ • 跳过前OFFSET行 │ │
│ │ │ │
│ │ OFFSET = 0: 跳过0行 → 0秒 │ │
│ │ OFFSET = 1,000: 跳过1K行 → 0.001秒 │ │
│ │ OFFSET = 10,000: 跳过10K行 → 0.01秒 │ │
│ │ OFFSET = 100,000: 跳过100K行 → 0.1秒 │ │
│ │ OFFSET = 1,000,000: 跳过1M行 → 1秒 │ │
│ │ │ │
│ │ 成本占比: ~0.1-3% │ │
│ │ 是否和OFFSET有关: ✅ 有关 (线性增长) │ │
│ └──────────────────────────────────────────────┘ │
│ │
│ 总耗时估算: │
│ OFFSET = 0: 2 + 30 + 0 = ~32秒 │
│ OFFSET = 100,000: 2 + 30 + 0.1 = ~32.1秒 │
│ OFFSET = 1,000,000: 2 + 30 + 1 = ~33秒 │
│ │
│ 结论: │
│ ✅ OFFSET会影响性能 (0.1秒 vs 1秒) │
│ ⚠️ 但影响很小,只占总成本的 0.3%-3% │
│ ❌ 主要瓶颈是排序,占总成本的 85-95% │
│ │
└────────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
方案1:
-- SQL语句
SELECT * FROM employees
ORDER BY salary DESC
LIMIT 10 OFFSET 100000;



方案2:
CREATE INDEX idx_salary ON employees(salary);

-- SQL语句
SELECT * FROM employees
ORDER BY salary DESC
LIMIT 10 OFFSET 100000;



方案3:
CREATE INDEX idx_salary ON employees(salary);

-- SQL语句
SELECT e.*
FROM employees e
INNER JOIN (
SELECT id
FROM employees
ORDER BY salary DESC
LIMIT 10 OFFSET 100000
) t ON e.id = t.id
ORDER BY e.salary DESC;


方案4:
CREATE INDEX idx_salary_name_age ON employees(salary, name, age);

-- SQL语句(只查询索引中的列)
SELECT id, salary, name, age
FROM employees
ORDER BY salary DESC
LIMIT 10 OFFSET 100000;


方案5:

-- 索引
CREATE INDEX idx_salary ON employees(salary);

-- SQL语句
SELECT * FROM employees
WHERE id >= (
SELECT id
FROM employees
ORDER BY salary DESC
LIMIT 1 OFFSET 100000
)
ORDER BY salary DESC
LIMIT 10;


方案6:

-- 索引
CREATE INDEX idx_salary_id ON employees(salary DESC, id DESC);

-- 第一次查询(第1页)
SELECT * FROM employees
ORDER BY salary DESC, id DESC
LIMIT 10;
-- 返回: 最后一行 salary=99990, id=12345

-- 第N次查询(基于上次的最后一行)
SELECT * FROM employees
WHERE salary < 99990
OR (salary = 99990 AND id < 12345)
ORDER BY salary DESC, id DESC
LIMIT 10;



┌──────────────────────────────────────────────────────────────────────────────┐
│ 性能对比总表 │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ 指标 │方案1 │方案2 │方案3 │方案4 │方案5 │方案6 │
│ │无索引 │有索引 │延迟关联│覆盖索引│子查询 │游标分页 │
│ ────────────────────────────────────────────────────────────────────────────│
│ 索引扫描 │ 0 │100010 │100010 │100010 │100001 │ 10 ✅ │
│ 回表次数 │ 0 │ 10 │ 10 │ 0 ✅ │ 10 │ 10 │
│ 排序成本 │ 高❌ │ 无✅ │ 低 │ 无✅ │ 中 │ 无✅ │
│ 磁盘IO (次) │10000❌│ 140 │ 100 │ 100 │ 110 │ 14 ✅ │
│ 内存消耗 │ 高❌ │ 低✅ │ 低✅ │ 低✅ │ 低✅ │ 低✅ │
│ 总耗时 │ 32秒❌ │ 0.5秒 │ 0.3秒 │ 0.3秒 │ 0.4秒 │ 0.01秒 🏆 │
│ vs基准(方案1) │ 1x │ 64x │106x │106x │ 80x │3200x 🏆 │
│ OFFSET影响 │ 小 │ 大❌ │ 大❌ │ 大❌ │ 大❌ │ 无✅ 🏆 │
│ 可跳页 │ ✅ │ ✅ │ ✅ │ ✅ │ ✅ │ ❌ │
│ 实现复杂度 │ 低✅ │ 低✅ │ 中 │ 低✅ │ 中 │ 高⚠️ │
│ 存储成本 │ 低✅ │ 低✅ │ 低✅ │ 高⚠️ │ 低✅ │ 中 │
│ 写入性能 │ 高✅ │ 中 │ 中 │ 低⚠️ │ 中 │ 中 │
│ │
│ 推荐度 │ ⭐ │ ⭐⭐ │ ⭐⭐⭐ │ ⭐⭐⭐ │ ⭐⭐⭐ │ ⭐⭐⭐⭐⭐ 🏆 │
│ │
└──────────────────────────────────────────────────────────────────────────────┘

innodb 数据读取策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
┌─────────────────────────────────────────────────────────────────┐
│ InnoDB 读取流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 接收读请求 │
│ SELECT * FROM employees WHERE id = 12345; │
│ ↓ │
│ │
│ 2. 解析执行计划 │
│ • 优化器决定使用主键索引 │
│ • 定位到需要读取的页面号: Page #1234 │
│ ↓ │
│ │
│ 3. 检查Buffer Pool │
│ ┌────────────────────────────────────┐ │
│ │ 在Buffer Pool中? │ │
│ ├────────────────────────────────────┤ │
│ │ YES → 缓存命中 (Cache Hit) │ │
│ │ ↓ │ │
│ │ 3a. 从Buffer Pool读取 │ │
│ │ • 读取内存中的页面 │ │
│ │ • 更新LRU链表(如果需要) │ │
│ │ • 返回数据 ✅ 极快! │ │
│ │ • IO次数: 0 │ │
│ │ • 时间: ~0.001ms │ │
│ └────────────────────────────────────┘ │
│ OR │
│ ┌────────────────────────────────────┐ │
│ │ NO → 缓存未命中 (Cache Miss) │ │
│ │ ↓ │ │
│ │ 3b. 从磁盘读取 │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ Step 1: 发起磁盘IO │ │ │
│ │ │ • 读取Page #1234 │ │ │
│ │ │ • 可能触发预读 │ │ │
│ │ │ • 时间: ~5-10ms (HDD) │ │ │
│ │ │ ~0.1ms (SSD) │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ ↓ │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ Step 2: 加载到Buffer Pool │ │ │
│ │ │ • 检查Free List │ │ │
│ │ │ - 有空闲 → 使用空闲页 │ │ │
│ │ │ - 无空闲 → 淘汰Old区页面 │ │ │
│ │ │ • 插入到Old区头部 │ │ │
│ │ └──────────────────────────────┘ │ │
│ │ ↓ │ │
│ │ ┌──────────────────────────────┐ │ │
│ │ │ Step 3: 返回数据 │ │ │
│ │ │ • 从Buffer Pool读取 │ │ │
│ │ │ • 后续访问会命中缓存 │ │ │
│ │ └──────────────────────────────┘ │ │
│ └────────────────────────────────────┘ │
│ │
│ 4. 统计信息更新 │
│ • Innodb_buffer_pool_read_requests++ │
│ • 如果是Cache Miss: Innodb_buffer_pool_reads++ │
│ │
└─────────────────────────────────────────────────────────────────┘

性能对比:
Cache Hit: 0.001ms 🏆
Cache Miss: 5-10ms (HDD), 0.1ms (SSD) ❌

命中率 >99% 的重要性:
99%命中率: 每100次读取,1次磁盘IO
90%命中率: 每100次读取,10次磁盘IO ← 慢10倍!

innodb 写入策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101

┌─────────────────────────────────────────────────────────────────┐
│ InnoDB 写入流程 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. 接收写请求 │
│ UPDATE employees SET salary = 10000 WHERE id = 12345; │
│ ↓ │
│ │
│ 2. 开始事务 │
│ BEGIN; │
│ ↓ │
│ │
│ 3. WAL (Write-Ahead Logging) 先写日志 🔑 关键! │
│ ┌────────────────────────────────────────┐ │
│ │ 3a. 写入Undo Log │ │
│ │ • 记录修改前的值 │ │
│ │ • 用于事务回滚 │ │
│ │ • 用于MVCC │ │
│ │ │ │
│ │ Undo Log: │ │
│ │ [TRX_ID] [id=12345] [old_salary=8000]│ │
│ └────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────┐ │
│ │ 3b. 修改Buffer Pool中的页面 │ │
│ │ • 将页面标记为脏页 (Dirty Page) │ │
│ │ • 修改数据: salary = 8000 → 10000 │ │
│ │ • 加入Flush List │ │
│ └────────────────────────────────────────┘ │
│ ↓ │
│ ┌────────────────────────────────────────┐ │
│ │ 3c. 写入Redo Log Buffer │ │
│ │ • 记录修改后的值 │ │
│ │ • 用于崩溃恢复 │ │
│ │ │ │
│ │ Redo Log: │ │
│ │ [TRX_ID] [Page#1234] [Offset] [new_data]│ │
│ └────────────────────────────────────────┘ │
│ ↓ │
│ │
│ 4. 提交事务 │
│ COMMIT; │
│ ↓ │
│ ┌────────────────────────────────────────┐ │
│ │ Redo Log Buffer → Redo Log File │ │
│ │ │ │
│ │ innodb_flush_log_at_trx_commit: │ │
│ │ │ │
│ │ = 0: 每秒刷新 │ │
│ │ • 性能最好 │ │
│ │ • 可能丢失1秒数据 ❌ │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ Redo Log Buffer │ │ │
│ │ │ ↓ 每秒 │ │ │
│ │ │ OS Buffer │ │ │
│ │ │ ↓ fsync │ │ │
│ │ │ Redo Log File (磁盘) │ │ │
│ │ └──────────────────────┘ │ │
│ │ │ │
│ │ = 1: 每次提交刷新 🏆 默认 │ │
│ │ • 性能中等 │ │
│ │ • 数据最安全 ✅ │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ Redo Log Buffer │ │ │
│ │ │ ↓ 每次提交 │ │ │
│ │ │ Redo Log File (磁盘) │ │ │
│ │ └──────────────────────┘ │ │
│ │ │ │
│ │ = 2: 每次提交写入OS,每秒fsync │ │
│ │ • 性能较好 │ │
│ │ • 可能丢失1秒数据 ⚠️ │ │
│ │ ┌──────────────────────┐ │ │
│ │ │ Redo Log Buffer │ │ │
│ │ │ ↓ 每次提交 │ │ │
│ │ │ OS Buffer │ │ │
│ │ │ ↓ 每秒fsync │ │ │
│ │ │ Redo Log File (磁盘) │ │ │
│ │ └──────────────────────┘ │ │
│ └────────────────────────────────────────┘ │
│ ↓ │
│ │
│ 5. 脏页异步刷新 (后台进程) │
│ ┌────────────────────────────────────────┐ │
│ │ Page Cleaner Thread │ │
│ │ • 定期扫描Flush List │ │
│ │ • 选择脏页刷新到磁盘 │ │
│ │ • 刷新策略: │ │
│ │ - LRU刷新 (从LRU尾部) │ │
│ │ - Flush List刷新 (按LSN顺序) │ │
│ │ • 不阻塞事务提交 ✅ │ │
│ └────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘

关键点:
1. ✅ 写入内存(Buffer Pool)是同步的,很快
2. ✅ 写入Redo Log是同步的,保证持久性
3. ✅ 脏页刷盘是异步的,不阻塞事务
4. 🔑 WAL机制:先写日志,后刷数据页

undo log 和 Redo log

快速比对

┌─────────────────────────────────────────────────────────────────┐
│ Undo Log vs Redo Log │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 维度 │ Undo Log │ Redo Log │
│ ─────────────────────────────────────────────────────────────│
│ 作用 │ 回滚 + MVCC │ 崩溃恢复 │
│ 记录内容 │ 逻辑日志(旧值) │ 物理日志(新值) │
│ 保证特性 │ 原子性(A) + 隔离性(I) │ 持久性(D) │
│ 写入时机 │ 事务开始时 │ 数据修改时 │
│ 存储位置 │ 系统表空间/独立表空间 │ 独立日志文件 │
│ 生命周期 │ 事务提交后可清理 │ 覆盖式循环使用 │
│ 大小限制 │ 动态增长 │ 固定大小循环 │
│ 用途 │ 事务回滚、一致性读 │ 数据恢复、持久化 │
│ │
└─────────────────────────────────────────────────────────────────┘

undo log 有两个作用
  • 优化的步骤

    • 考虑数据量大导致的性能问题,访问量大导致的性能问题?
    • sql语句优化。分析执行计划,减少load的数据量
    • 考虑能否通过增加索引优化查询效率,检查索引是否生效
    • 是否有缓存
    • 垂直分表、水平分表、分库
    • 根据场景来看,写操作多的情况下,考虑读写分离
    • 数据归档:数据是否有冷热的区别,例如订单数据有比较明显的时间冷热的区别,可以考虑冷数据归档。比如半年前的订单数据可以写入hbase
    • 池化
  • 架构优化

    • 分库,分表。垂直分,水平分。依据QPS和耗时,服务端最大并非连接数量
    • 读写分离
    • 批量读写,批量更新
    • 异步写,写平滑
    • 缓存优化
    • 历史数据归档
  • 连接池的配置和使用

    • 连接池能减少连接创建和释放带来的开销,大多数SDK也支持是支持连接池的,通常实际生产环境中也都会使用到连接池,需要关注一下几个参数
    • max_idle_connections: 最大空闲连接数
    • max_open_connections: 最大连接数
    • connection_max_lifetime: 连接最大可重用时间
    • 要使用好连接池,除了关注客户端的配置还需要关注mysql服务端的配置
    • 服务端最大连接数量:show variables like ‘%connection%’; max_connections
    • 服务端连接最大生命周期:show variables like ‘%wait_timeout%’
      1
      2
      3
      最大空闲连接数 =(QPS*请求平均耗时)/ 应用节点个数
      最大连接数 =(QPS*请求最大耗时)/ 应用节点个数
      客户端连接maxlifetime < 数据库服务端设置的connection_max_lifttime
  • 慢sql优化

    • 慢查询问题,查看慢查询设置的阈值。show variables like ‘%long_query%’;
    • 打开慢查询日志
    • 分析数据sql的结构是否加载了不必要的字段和数据
    • 深度分页查询优化
    • 子查询和连接查询
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
            	explain select * from test_xxxx_tab txt order by id limit 10000,10;
      explain SELECT * from test_xxxx_tab txt where id >= (select id from test_xxxx_tab txt order by id limit 10,1) limit 10;
      id列:在复杂的查询语句中包含多个查询使用id标示
      select_type:select/subquery/derived/union
      table: 显示对应行正在访问哪个表
      type:访问类型,关联类型。非常重要,All,index,range,ref,const,
      possible_keys: 显示可以使用哪些索引列
      key列:显示mysql决定使用哪个索引来优化对该表的访问
      key_len:显示在索引里使用的字节数
      rows:为了找到所需要的行而需要读取的行数
    • 慢查询日志样例子
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      # Time: 2022-05-10T10:15:32.123456Z
      # User@Host: myuser[192.168.0.1] @ localhost [] Id: 12345
      # Query_time: 3.456789 Lock_time: 0.123456 Rows_sent: 10 Rows_examined: 100000
      SET timestamp=1657475732;
      SELECT * FROM orders WHERE customer_id = 1001 ORDER BY order_date DESC LIMIT 10;
      这个慢查询日志示例包含以下重要的信息:

      时间戳(Time): 日志记录的时间,以 UTC 时间表示。
      用户和主机(User@Host): 执行查询的用户和主机地址。
      连接 ID(Id): 表示执行查询的连接 ID。
      查询时间(Query_time): 查询执行所花费的时间,以秒为单位。
      锁定时间(Lock_time): 在执行查询期间等待锁定资源所花费的时间,以秒为单位。
      返回行数(Rows_sent): 查询返回的结果集中的行数。
      扫描行数(Rows_examined): 在执行查询过程中扫描的行数。
      时间戳(SET timestamp): 查询开始执行的时间戳。
      查询语句(SELECT * FROM orders WHERE customer_id = 1001 ORDER BY order_date DESC LIMIT 10): 实际执行的查询语句
  • index优化

    • 会查看sql执行计划explain
    • 关注:type、const、ref
    • 关注:extra等字段
  • 使用缓存优化DB需要考虑的问题

    • 缓存更新、过期、淘汰的策略
    • 缓存可能遇到的三大问题,雪崩、穿透、击穿
    • 缓存和db的一致性问题,缓存更新策略及其分析?,业界比较通用的先更新DB,再删除cache
  • 库表优化/分表/分库

    • 垂直分表
    • 水平分表
    • 分库
    • 业界成熟的方案
  • 架构优化读写分离优化

    • 在写操作的较多的情况可以考虑数据库读写分离的方案
    • 业界的方案,代理实现和业务实现
  • 核心监控告警指标

    • read write qps 监控/select/update/insert
    • connections
    • thread
    • InnoDB buffer pool
    • 慢查询监控
    • 网络流量IO
    • 读写分离架构时需要监控主从延时
  • 关键配置查看

    1
    2
    3
    4
    5
    show global variables;
    show variables like '%max_connection%'; 查看最大连接数
    show status like 'Threads%';
    show processlist;
    show variables like '%connection%';
  • 存储空间information_schema

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    -- desc information_schema.tables;
    -- 查看 MySQL「所有库」的容量大小
    SELECT table_schema AS '数据库', SUM(table_rows) AS '记录数',
    SUM(truncate(data_length / 1024 / 1024, 2)) AS '数据容量(MB)',
    SUM(truncate(index_length / 1024 / 1024, 2)) AS '索引容量(MB)',
    SUM(truncate(DATA_FREE / 1024 / 1024, 2)) AS '碎片占用(MB)'
    FROM information_schema.tables
    GROUP BY table_schema
    ORDER BY SUM(data_length) DESC, SUM(index_length) DESC;
    -- 指定书库查看表的数据量
    SELECT
    table_schema as '数据库',
    table_name as '表名',
    table_rows as '记录数',
    truncate(data_length/1024/1024, 2) as '数据容量(MB)',
    truncate(index_length/1024/1024, 2) as '索引容量(MB)',
    truncate(DATA_FREE/1024/1024, 2) as '碎片占用(MB)'
    from
    information_schema.tables
    where
    table_schema='<数据库名>'
    order by
    data_length desc, index_length desc;

MySQL多表关联查询 vs 多次单表查询service组装

  • 多次单表查询+Service组装:
    • 灵活性:多次单表查询+Service组装方式更加灵活,可以根据具体需求灵活组装和调整查询逻辑,适应各种复杂的查询需求。
    • 可扩展性:通过多次单表查询和Service组装,可以将查询逻辑分解为多个简单的查询,有助于代码的模块化和可扩展性,方便后续的维护和修改。
    • 缓存利用:多次单表查询+Service组装方式可以更好地利用缓存,针对每个单表查询的结果进行缓存,提高查询性能
      https://www.zhihu.com/question/68258877

mysql binlog

show processlist;

常用命令

  • mysql登陆:
    mysql -h主机 -P端口 -u用户 -p密码
    SET PASSWORD FOR ‘root‘@’localhost’ = PASSWORD(‘root’);
    create database wxquare_test;
    show databases;
    use wxquare_test;
  • 查看见表sql:show create table table_name;
  • show variables like ‘%timeout%’;
  • update json 文本需要转义
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
      update table set extinfo='{
    \"urls\": [
    {
    \"url\": \"/path1\",
    \"type\": \"type1\"
    },
    {
    \"url\": \"/path2\",
    \"type\": \"type2\"
    },
    ]
    }' where id = 2;
  • truncate table 属于ddl语句,需要ddl的权限
  • mysqldump 库表结构
    1
    mysqldump --column-statistics=0 -hhost -PPort -uuser_name -ppassword --databases -d db_name --skip-lock-tables --skip-add-drop-table --set-gtid-purged=OFF | sed 's/ AUTO_INCREMENT=	[0-9]*//g' > db.sql

    - 批量更新
    1
    2
    3
    4
    5
    6
    7
    8
    UPDATE employees
    SET salary = CASE
    WHEN grade = 'A' THEN salary * 1.1
    WHEN grade = 'B' THEN salary * 1.05
    WHEN grade = 'C' THEN salary * 1.03
    ELSE salary
    END
    WHERE department = 'IT';

推荐阅读:

1、linux基础命令

  • 帮助命令:man、info
  • 查找命令路径:which、whereis
  • 查看文件文件个数:find ./ | wc -l
  • 以时间顺序显示目录项:ls -lrt
  • 查看文件时同时显示行数:cat -n xxx
  • 查看两个文件的差别:diff file1 file2
  • 动态显示文本最新信息,常用于查看日志: tail -f xxx.log
  • 软连接/硬链接: ln cc ccAgain 和 ln -s cc ccAgain
  • command1 && command2
  • command1 || command2
  • 查找txt和pdf文件:find . ( -name “.txt” -o -name “.pdf” ) -print
  • find查找文件时指定深度:find . -maxdepth 1 -type f
  • find只查找目录:find . -type d -print
  • 文本处理
  • 打包:tar -cvf xxx.tar . 解包: tar -xvf xxx.tar
  • 压缩与解压:-z 解压gz文件;-j解压bz2;-J解压xz文件
  • grep 查找文件中指定字符出现的次数
1
cat Temp\ Query\ 1_20230914-171937.csv | grep  "\"sop_v3_user" | grep -v "xxxx" | awk -F ',' '{print $2,$5,$6}' | sort | uniq -c | sort -rk 2

2、系统信息查看工具

  • 查看操作系统发行版:lsb_release -a
  • 查看内核版本信息:uname -a
  • 查看cpu信息:cat /proc/cpuinfo
  • 查看cpu核数:cat /proc/cpuinfo | grep processor | wc -l
  • 查看内存信息:cat /proc/meminfo
  • 显示架构:arch
  • 查看进程间ipc资源情况:ipcs
  • 显示当前所有的系统资源limit信息: ulimit -a
  • 对生成的core文件的大小不进行限制:ulimit -c unlimited

3、系统资源管理和监控

  • 查询正在运行的进程信息:ps -ef 或者 ps -ajx
  • 查询某用户的进程: ps -ef | grep username 或者 ps -lu username
  • 实时显示进程信息: top linux下的任务管理器,内存VIRT和RES
  • 查看用户打开的文件: lsof -u username
  • 查看某进程打开的文件: lsof -p pid
  • 杀死某进程:kill -9 pid
  • pmap输出进程内存你的状况,用来分析线程堆栈
  • 查看内存使用量:free -m 或者 vmstat n m
  • 查看磁盘使用情况:df -h
  • du -ha –max-depth=1
  • iostat 监视I/O子系统,ubuntu安装systat。通过iostat方便查看CPU、网卡、tty设备、磁盘、CD-ROM 等等设备的活动情况, 负载信息
  • sar 找出系统瓶颈的利器
    *ubuntu系统下,默认可能没有安装这个包,使用apt-get install sysstat 来安装;
    安装完毕,将性能收集工具的开关打开: vi /etc/default/sysstat
    设置 ENABLED=”true”
    启动这个工具来收集系统性能数据: /etc/init.d/sysstat start.

4、网络工具

  • 查看网络流量信息iftop
  • netstat命令用于显示各种网络相关信息
  • 查询某端口port被某个进程占用:netstat -antp | grep port,然后使用ps pid查询进程名称
  • 也可以使用lsof -i:port 直接查询该端口的进程
  • ping 测试网络连通情况
  • traceroute IP 探测前往ip的路由信息
  • 直接下载文件或者网页:wget
  • 网络远程复制:scp -r localpath ID@host:path
  • 使用ssh协议下载: scp -r ID@host:path localpath
  • nc服务器编程常用,既可以作为客户端又可以指定端口作为服务端。
  • 查看网络端口使用情况:https://www.runoob.com/w3cnote/linux-check-port-usage.html

5、环境变量

  • 全局/etc/profile->/etc/profile.d;
  • 读取当前用户下面的:/.bash_profile->/.bash_login->~/.profile
  • 读取当前用户目录下面的:~/.bashrc
  • export环境变量,退出失效

6、查看GPU信息

  • 查看gpu信息 nvidia-smi
  • 查看gpu驱动版本信息 cat /proc/driver/nvidia/version
  • pkgconfig? PKG_CONFIG_PATH环境变量

7、测试系统磁盘的性能

dd是Linux/UNIX 下的一个非常有用的命令,作用是用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换。另外在linux中,有两个特殊的设备:/dev/null:回收站、无底洞,经常作为写端,不会产生IO,/dev/zero产生字符,经常作为读端,也不会产生IO。
(1)测试磁盘写能力
dd if=/dev/zero of=/test1.img bs=4k count=10000
因为/dev//zero是一个伪设备,它只产生空字符流,对它不会产生IO,所以,IO都会集中在of文件中,of文件只用于写,所以这个命令相当于测试磁盘的写能力。命令结尾添加oflag=direct将跳过内存缓存,添加oflag=sync将跳过hdd缓存。
(2)测试磁盘读能力
dd if=/dev/sda of=/dev/null bs=4k count=10000
因为/dev/sdb是一个物理分区,对它的读取会产生IO,/dev/null是伪设备,相当于黑洞,of到该设备不会产生IO,所以,这个命令的IO只发生在/dev/sdb上,也相当于测试磁盘的读能力。
(3)测试同时读写能力
time dd if=/dev/sda of=/test1.img bs=4k count=10000
在这个命令下,一个是物理分区,一个是实际的文件,对它们的读写都会产生IO(对/dev/sda是读,对/test.img是写),假设它们都在一个磁盘中,这个命令就相当于测试磁盘的同时读写能力。

8、使用dd和nc命令测试网络性能

nc是netcat的简写,有着网络界的瑞士军刀美誉。因为它短小精悍、功能实用,被设计为一个简单、可靠的网络工具
(1)实现任意TCP/UDP端口的侦听,nc可以作为server以TCP或UDP方式侦听指定端口
(2)端口的扫描,nc可以作为client发起TCP或UDP连接
(3)机器之间传输文件
(4)机器之间网络测速
nc命令有个-l参数可以用来监听指定端口,因此我们要完成上面的功能,就只需要简单的从/dev/zero或者其他虚拟设备读入数据:

time nc -l -p 5001 < /test.img

然后另外一台电脑使用nc来连接到这个端口并读入数据:
time nc 192.168.0.11 5001 > /dev/null
上面的测试的结果中,是从磁盘读数据通过网络获取,通过time命令或缺时间参数,可以计算出网络的性能。更准备的测试应该从/dev/zero中多数据会更好一些

参考:https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/index.html

TCP和UDP协议

  1. tcp头格式,其20个字节包含哪些内容? udp头部格式,其8个字节分别包含哪些内容?

  2. 为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢?原因是 TCP 有可变长的「选项」字段,而 UDP 头部长度则是不会变化的,无需多一个字段去记录 UDP 的首部长度

  3. tcp和udp的区别以及应用场景

    • TCP是面向连接的,而UDP是不需要建立连接的
    • TCP 是一对一的两点服务,UDP 支持一对一、一对多、多对多的交互通信
    • 可靠性,TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。UDP 是尽最大努力交付,不保证可靠交付数据。
    • TCP有拥塞控制、流量控制
    • 首部开销,TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
    • 传输方式,TCP 是流式传输,没有边界,但保证顺序和可靠。UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序

    TCP 和 UDP 应用场景:由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于,FTP 文件传输HTTP / HTTPS,由于 UDP 面向无连接,它可以随时发送数据,再加上UDP本身的处理既简单又高效,因此经常用于:包总量较少的通信,如 DNS 、SNMP 等视频、音频等多媒体通信广播通信

  4. TCP协议如何保证可靠传输?

    • 三次握手四次挥手确保连接的建立和释放
    • 超时重发:数据切块发送,等待确认,超时未确认会重发
    • 数据完整性校验:TCP首部中数据有端到端的校验和,接收方会校验,一旦出错将丢弃且不确认收到此报文
    • 根据序列码进行数据的排序和去重
    • 根据接收端缓冲区大小做流量控制
    • 根据网络环境做拥塞控制。当网络拥塞时,会减少数据的发送
  5. TCP怎么通过三次握手和四次挥手建立可靠连接以及需要注意的问题

    • 分别准确画出三次握手和四次挥手状态转换图 从上面的过程可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的,这也是面试常问的题
    • 为什么需要三次握手? 通过三次握手实现了同步序列号和避免了旧的重复连接初始化造成混乱,浪费服务器资源,两个作用
    • 为什么需要四次挥手?全双工通信
    • time_wait状态什么作用? 防止之前的报文造成新连接数据混乱,通过2msl使前一连接数据失效;确保ack报文发送给服务端。
  6. 超时重传和快速重传

    • 客户端通过定时器在指定时间内未发现会收到ack信息就认为进行超时重传
    • 客户端收到连续三个重复ack信息就会发起快速重传而不用等待超时重传
  7. 如何解决可能出现的乱序和重复数据问题

  8. TCP流量控制和滑动窗口

    • 为了提高数据传输的效率,tcp避免了一问一答式的消息传输策略
    • 通过累积确认ACK的方式提高效率
    • 在累积确认时通过接收窗口进行流量控制

  9. tcp拥塞控制和拥塞窗口?
    TCP拥塞控制

    • tcp在数据发送时会结合整个网络环境调整数据发送的速率
    • 发送者如何判断拥塞已经发生的?发送超时,或者说TCP重传定时器溢出;接收到重复的确认报文段
    • 快重传算法(接收端到失序的报文段立即重传、发送端一旦接收三个重复的确认报文段,立即重传,不用等定时器)
  10. TCP 的连接状态查看,在 Linux 可以通过 netstat -napt 命令查看

  11. 什么是SYN攻击,怎么避免SYN攻击?

  • SYN攻击属于DOS攻击的一种,它利用TCP协议缺陷,通过发送大量的半连接请求,耗费CPU和内存资源。SYN攻击除了能影响主机外,还可以危害路由器、防火墙等网络系统,事实上SYN攻击并不管目标是什么系统,只要这些系统打开TCP服务就可以实施。从上图可看到,服务器接收到连接请求(syn=j),将此信息加入未连接队列,并发送请求包给客户(syn=k,ack=j+1),此时进入SYN_RECV状态。当服务器未收到客户端的确认包时,重发请求包,一直到超时,才将此条目从未连接队列删除。配合IP欺骗,SYN攻击能达到很好的效果,通常,客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送syn包,服务器回复确认包,并等待客户的确认,由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。
  1. 如何解决close_wait和time_wait过多的问题?

    • CLOSE_WAIT,只会发生在客户端先关闭连接的时候,但已经收到客户端的fin包,但服务器还没有关闭的时候会产生这个状态,如果服务器产生大量的这种连接一般是程序问题导致的,如部分情况下不会执行socket的close方法,解决方法是查程序
    • TIME_WAIT,time_wait是一个需要特别注意的状态,他本身是一个正常的状态,只在主动断开那方出现,每次tcp主动断开都会有这个状态的,维持这个状态的时间是2个msl周期(2分钟),设计这个状态的目的是为了防止我发了ack包对方没有收到可以重发。那如何解决出现大量的time_wait连接呢?千万不要把tcp_tw_recycle改成1,这个我再后面介绍,正确的姿势应该是降低msl周期,也就是tcp_fin_timeout值,同时增加time_wait的队列(tcp_max_tw_buckets),防止满了。
  2. 什么是TCP粘包,应用层怎么解决,http是怎么解决的。tcp是字节流,需要根据特殊字符和长度信息将消息分开

  3. udp协议怎么做可靠传输?
    由于在传输层UDP已经是不可靠的连接,那就要在应用层自己实现一些保障可靠传输的机制,简单来讲,要使用UDP来构建可靠的面向连接的数据传输,就要实现类似于TCP协议的,超时重传(定时器),有序接受 (添加包序号),应答确认 (Seq/Ack应答机制),滑动窗口流量控制等机制 (滑动窗口协议),等于说要在传输层的上一层(或者直接在应用层)实现TCP协议的可靠数据传输机制,比如使用UDP数据包+序列号,UDP数据包+时间戳等方法。目前已经有一些实现UDP可靠传输的机制,比如UDT(UDP-based Data Transfer Protocol)基于UDP的数据传输协议(UDP-based Data Transfer Protocol,简称UDT)是一种互联网数据传输协议。UDT的主要目的是支持高速广域网上的海量数据传输,而互联网上的标准数据传输协议TCP在高带宽长距离网络上性能很差。 顾名思义,UDT建于UDP之上,并引入新的拥塞控制和数据可靠性控制机制。UDT是面向连接的双向的应用层协议。它同时支持可靠的数据流传输和部分可靠的数据报传输。 由于UDT完全在UDP上实现,它也可以应用在除了高速数据传输之外的其它应用领域,例如点到点技术(P2P),防火墙穿透,多媒体数据传输等等

  4. TCP 保活机制KeepAlive?其局限性?Http的keep-alive?为什么应用层也经常做心跳检查?

    • TCP KeepAlive 的基本原理是,隔一段时间给连接对端发送一个探测包,如果收到对方回应的 ACK,则认为连接还是存活的,在超过一定重试次数之后还是没有收到对方的回应,则丢弃该 TCP 连接。TCP-Keepalive-HOWTO 有对 TCP KeepAlive 特性的详细介绍,有兴趣的同学可以参考。
    • TCP KeepAlive 的局限。首先 TCP KeepAlive 监测的方式是发送一个 probe 包,会给网络带来额外的流量,另外 TCP KeepAlive 只能在内核层级监测连接的存活与否,而连接的存活不一定代表服务的可用。例如当一个服务器 CPU 进程服务器占用达到 100%,已经卡死不能响应请求了,此时 TCP KeepAlive 依然会认为连接是存活的。因此 TCP KeepAlive 对于应用层程序的价值是相对较小的。需要做连接保活的应用层程序,例如 QQ,往往会在应用层实现自己的心跳功能。
      除了TCP自带的Keeplive机制,实现业务中经常在业务层面定制“心跳”功能,主要有以下几点考虑:
    • TCP自带的keepalive使用简单,仅提供连接是否存活的功能
    • 应用层心跳包不依赖于传输协议,支持tcp和udp
    • 应用层心跳包可以定制,可以应对更加复杂的情况或者传输一些额外的消息
    • Keepalive仅仅代表连接保持着,而心跳往往还表示服务正常工作
      在 HTTP 1.0 时期,每个 TCP 连接只会被一个 HTTP Transaction(请求加响应)使用,请求时建立,请求完成释放连接。当网页内容越来越复杂,包含大量图片、CSS 等资源之后,这种模式效率就显得太低了。所以,在 HTTP 1.1 中,引入了 HTTP persistent connection 的概念,也称为 HTTP keep-alive,目的是复用TCP连接,在一个TCP连接上进行多次的HTTP请求从而提高性能。HTTP1.0中默认是关闭的,需要在HTTP头加入”Connection: Keep-Alive”,才能启用Keep-Alive;HTTP1.1中默认启用Keep-Alive,加入”Connection: close “,才关闭。两者在写法上不同,http keep-alive 中间有个”-“符号。 HTTP协议的keep-alive 意图在于连接复用,同一个连接上串行方式传递请求-响应数据。TCP的keepalive机制意图在于保活、心跳,检测连接错误。
  5. TCP 协议性能问题分析?

    • TCP 的拥塞控制在发生丢包时会进行退让,减少能够发送的数据段数量,但是丢包并不一定意味着网络拥塞,更多的可能是网络状况较差;
    • TCP 的三次握手带来了额外开销,这些开销不只包括需要传输更多的数据,还增加了首次传输数据的网络延迟;
    • TCP 的重传机制在数据包丢失时可能会重新传输已经成功接收的数据段,造成带宽的浪费;
  6. QUIC 是如何解决TCP 性能瓶颈的?

  7. 科普:QUIC协议原理分析

http和https

  1. HTTP协议协议格式详解

    • 请求行(request line)。请求方法、域名、协议版本。
    • 请求头部(header)从第二行起为请求头部,Host指出请求的目的地(主机域名);User-Agent是客户端的信息,它是检测浏览器类型的重要信息,由浏览器定义,并且在每个请求中自动发送
    • 空行
    • 请求数据
  2. http 常见的状态码有哪些?

    • 200 成功
    • 3xx重定向相关,301 永久重定向,302临时重定向
    • 4xx客户端错误,400请求报文有问题,403服务器禁止访问资源,404资源不存在
    • 5xx服务器内部错误,501 请求的功能暂不支持,502 服务器逻辑有问题,503 服务器繁忙
  3. get 和 post 区别

    • GET参数通过URL传递,POST放在Request body中
    • GET请求只能进行url编码,而POST支持多种编码方式
    • GET请求在URL中传送的参数是有长度限制的,而POST没有
    • GET比POST更不安全,因为参数直接暴露在URL上,所以不能用来传递敏感信息。
    • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  4. https的工作原理和流程

  5. http和https的区别

    • http采用明文传输,http+ssl的加密传输
    • http是80端口,https是443端口
    • HTTP的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比HTTP协议安全
  6. 浏览器输入http://www.baidu.com
    事件顺序
    (1) 浏览器获取输入的域名www.baidu.com
    (2) 浏览器向DNS请求解析www.baidu.com的IP地址
    (3) 域名系统DNS解析出百度服务器的IP地址
    (4) 浏览器与该服务器建立TCP连接(默认端口号80)
    (5) 浏览器发出HTTP请求,请求百度首页
    (6) 服务器通过HTTP响应把首页文件发送给浏览器
    (7) TCP连接释放
    (8) 浏览器将首页文件进行解析,并将Web页显示给用户。

  7. http长连接和短连接?http长连接和短连接以及keep-Alive的含义,HTTP 长连接不可能一直保持,例如 Keep-Alive: timeout=5, max=100,表示这个TCP通道可以保持5秒,max=100,表示这个长连接最多接收100次请求就断开。

  8. http cookie和session

    • Cookie和Session都是客户端与服务器之间保持状态的解决方案,具体来说,cookie机制采用的是在客户端保持状态的方案,而session机制采用的是在服务器端保持状态的方案
    • Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie,而客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器,服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容
  9. http1.0,tttp1.1,http2.0,http 3.0各有什么变化

    • http 1.0
    • http 1.1, 长连接
    • http 2.0,二进制压缩+连接复用
    • http QUIC,udp+ssl
  10. HTTP/3 竟然基于 UDP,HTTP 协议这些年都经历了啥?

  11. 使用curl

  12. https中间人攻击原理以及防御措施

  13. 如何理解http的无连接和无状态的特点?

  14. 半链接和Sync 攻击原理及防范技术


资料来源:OSI 7层模型

超文本传输协议(HTTPS/HTTP1.1/HTTP2/HTTP3)

https://aws.amazon.com/cn/compare/the-difference-between-https-and-http/

HTTP 是一种在客户端和服务器之间编码和传输数据的方法。它是一个请求/响应协议:客户端和服务端针对相关内容和完成状态信息的请求和响应。HTTP 是独立的,允许请求和响应流经许多执行负载均衡,缓存,加密和压缩的中间路由器和服务器。

一个基本的 HTTP 请求由一个动词(方法)和一个资源(端点)组成。 以下是常见的 HTTP 动词:

动词 描述 *幂等 安全性 可缓存
GET 读取资源 Yes Yes Yes
POST 创建资源或触发处理数据的进程 No No Yes,如果回应包含刷新信息
PUT 创建或替换资源 Yes No No
DELETE 删除资源 Yes No No

  • HTTPS 是基于 HTTP 的安全版本,通过使用 SSL 或 TLS 加密和身份验证通信。
  • HTTP/1.1 是 HTTP 的第一个主要版本,引入了持久连接、管道化请求等特性。
  • HTTP/2 是 HTTP 的第二个主要版本,使用二进制协议,引入了多路复用、头部压缩、服务器推送等特性。
  • HTTP/3 是 HTTP 的第三个主要版本,基于 QUIC 协议,使用 UDP,提供更快的传输速度和更好的性能

多次执行不会产生不同的结果

HTTP 是依赖于较低级协议(如 TCPUDP)的应用层协议。

来源及延伸阅读:HTTP

传输控制协议(TCP)


资料来源:如何制作多人游戏

TCP 是通过 IP 网络的面向连接的协议。 使用握手建立和断开连接。 发送的所有数据包保证以原始顺序到达目的地,用以下措施保证数据包不被损坏:

如果发送者没有收到正确的响应,它将重新发送数据包。如果多次超时,连接就会断开。TCP 实行流量控制拥塞控制。这些确保措施会导致延迟,而且通常导致传输效率比 UDP 低。

为了确保高吞吐量,Web 服务器可以保持大量的 TCP 连接,从而导致高内存使用。在 Web 服务器线程间拥有大量开放连接可能开销巨大,消耗资源过多,也就是说,一个 memcached 服务器。连接池 可以帮助除了在适用的情况下切换到 UDP。

TCP 对于需要高可靠性但时间紧迫的应用程序很有用。比如包括 Web 服务器,数据库信息,SMTP,FTP 和 SSH。

以下情况使用 TCP 代替 UDP:

  • 你需要数据完好无损。
  • 你想对网络吞吐量自动进行最佳评估。

用户数据报协议(UDP)


资料来源:如何制作多人游戏

UDP 是无连接的。数据报(类似于数据包)只在数据报级别有保证。数据报可能会无序的到达目的地,也有可能会遗失。UDP 不支持拥塞控制。虽然不如 TCP 那样有保证,但 UDP 通常效率更高。

UDP 可以通过广播将数据报发送至子网内的所有设备。这对 DHCP 很有用,因为子网内的设备还没有分配 IP 地址,而 IP 对于 TCP 是必须的。

UDP 可靠性更低但适合用在网络电话、视频聊天,流媒体和实时多人游戏上。

以下情况使用 UDP 代替 TCP:

  • 你需要低延迟
  • 相对于数据丢失更糟的是数据延迟
  • 你想实现自己的错误校正方法

来源及延伸阅读:TCP 与 UDP

远程过程调用协议(RPC)


Source: Crack the system design interview

在 RPC 中,客户端会去调用另一个地址空间(通常是一个远程服务器)里的方法。调用代码看起来就像是调用的是一个本地方法,客户端和服务器交互的具体过程被抽象。远程调用相对于本地调用一般较慢而且可靠性更差,因此区分两者是有帮助的。热门的 RPC 框架包括 ProtobufThriftAvro

RPC 是一个“请求-响应”协议:

  • 客户端程序 ── 调用客户端存根程序。就像调用本地方法一样,参数会被压入栈中。
  • 客户端 stub 程序 ── 将请求过程的 id 和参数打包进请求信息中。
  • 客户端通信模块 ── 将信息从客户端发送至服务端。
  • 服务端通信模块 ── 将接受的包传给服务端存根程序。
  • 服务端 stub 程序 ── 将结果解包,依据过程 id 调用服务端方法并将参数传递过去。

RPC 调用示例:

1
2
3
4
5
6
7
GET /someoperation?data=anId

POST /anotheroperation
{
"data":"anId";
"anotherdata": "another value"
}

RPC 专注于暴露方法。RPC 通常用于处理内部通讯的性能问题,这样你可以手动处理本地调用以更好的适应你的情况。

当以下情况时选择本地库(也就是 SDK):

  • 你知道你的目标平台。
  • 你想控制如何访问你的“逻辑”。
  • 你想对发生在你的库中的错误进行控制。
  • 性能和终端用户体验是你最关心的事。

遵循 REST 的 HTTP API 往往更适用于公共 API。

缺点:RPC

  • RPC 客户端与服务实现捆绑地很紧密。
  • 一个新的 API 必须在每一个操作或者用例中定义。
  • RPC 很难调试。
  • 你可能没办法很方便的去修改现有的技术。举个例子,如果你希望在 Squid 这样的缓存服务器上确保 RPC 被正确缓存的话可能需要一些额外的努力了。

表述性状态转移(REST)

REST 是一种强制的客户端/服务端架构设计模型,客户端基于服务端管理的一系列资源操作。服务端提供修改或获取资源的接口。所有的通信必须是无状态和可缓存的。

RESTful 接口有四条规则:

  • 标志资源(HTTP 里的 URI) ── 无论什么操作都使用同一个 URI。
  • 表示的改变(HTTP 的动作) ── 使用动作, headers 和 body。
  • 可自我描述的错误信息(HTTP 中的 status code) ── 使用状态码,不要重新造轮子。
  • HATEOAS(HTTP 中的HTML 接口) ── 你的 web 服务器应该能够通过浏览器访问。

REST 请求的例子:

1
2
3
4
GET /someresources/anId

PUT /someresources/anId
{"anotherdata": "another value"}

REST 关注于暴露数据。它减少了客户端/服务端的耦合程度,经常用于公共 HTTP API 接口设计。REST 使用更通常与规范化的方法来通过 URI 暴露资源,通过 header 来表述并通过 GET、POST、PUT、DELETE 和 PATCH 这些动作来进行操作。因为无状态的特性,REST 易于横向扩展和隔离。

缺点:REST

  • 由于 REST 将重点放在暴露数据,所以当资源不是自然组织的或者结构复杂的时候它可能无法很好的适应。举个例子,返回过去一小时中与特定事件集匹配的更新记录这种操作就很难表示为路径。使用 REST,可能会使用 URI 路径,查询参数和可能的请求体来实现。
  • REST 一般依赖几个动作(GET、POST、PUT、DELETE 和 PATCH),但有时候仅仅这些没法满足你的需要。举个例子,将过期的文档移动到归档文件夹里去,这样的操作可能没法简单的用上面这几个 verbs 表达。
  • 为了渲染单个页面,获取被嵌套在层级结构中的复杂资源需要客户端,服务器之间多次往返通信。例如,获取博客内容及其关联评论。对于使用不确定网络环境的移动应用来说,这些多次往返通信是非常麻烦的。
  • 随着时间的推移,更多的字段可能会被添加到 API 响应中,较旧的客户端将会接收到所有新的数据字段,即使是那些它们不需要的字段,结果它会增加负载大小并引起更大的延迟。

RPC 与 REST 比较

操作 RPC REST
注册 POST /signup POST /persons
注销 POST /resign
{
“personid”: “1234”
}
DELETE /persons/1234
读取用户信息 GET /readPerson?personid=1234 GET /persons/1234
读取用户物品列表 GET /readUsersItemsList?personid=1234 GET /persons/1234/items
向用户物品列表添加一项 POST /addItemToUsersItemsList
{
“personid”: “1234”;
“itemid”: “456”
}
POST /persons/1234/items
{
“itemid”: “456”
}
更新一个物品 POST /modifyItem
{
“itemid”: “456”;
“key”: “value”
}
PUT /items/456
{
“key”: “value”
}
删除一个物品 POST /removeItem
{
“itemid”: “456”
}
DELETE /items/456

资料来源:你真的知道你为什么更喜欢 REST 而不是 RPC 吗

网络通讯协议

OSI 七层网络模型


资料来源:OSI 7层模型

常用的应用层协议

HTTP (Hypertext Transfer Protocol)

用途:主要用于Web浏览器和服务器之间的通信,是万维网的数据传输基础。
特点:无状态、请求-响应模式。
版本:HTTP/1.1, HTTP/2, HTTP/3

FTP (File Transfer Protocol)

用途:用于在客户端和服务器之间传输文件。
特点:支持文件上传和下载,支持匿名访问和身份验证。

邮件协议

  • SMTP (Simple Mail Transfer Protocol)
    用途:用于发送电子邮件。
    特点:主要用于邮件服务器之间的邮件传输。
  • POP3 (Post Office Protocol 3)
    用途:用于从邮件服务器下载邮件到本地客户端。
    特点:下载后邮件通常会从服务器删除。
  • IMAP (Internet Message Access Protocol)
    用途:用于从邮件服务器读取邮件。
    特点:支持在服务器上管理和存储邮件,客户端和服务器邮件同步

WebSocket

用途:提供全双工通信的协议,允许在客户端和服务器之间建立持久连接。
特点:低延迟、实时通信、减少HTTP请求开销。
为什么需要websocket

WebRTC (Web Real-Time Communication)

用途:用于实现浏览器和移动应用之间的实时音视频通信和数据共享。
特点:P2P通信、低延迟、高质量音视频传输。
webRTC

MQTT (Message Queuing Telemetry Transport)

用途:轻量级的发布/订阅消息传输协议,常用于物联网(IoT)设备之间的通信。
特点:低带宽、低能耗、可靠性高

超文本传输协议

  • aws http 选择介绍
  • HTTPS 是基于 HTTP 的安全版本,通过使用 SSL 或 TLS 加密和身份验证通信。
  • HTTP/1.1 是 HTTP 的第一个主要版本,引入了持久连接、管道化请求等特性。
  • HTTP/2 是 HTTP 的第二个主要版本,使用二进制协议,引入了多路复用、头部压缩、服务器推送等特性。
  • HTTP/3 是 HTTP 的第三个主要版本,基于 QUIC 协议,使用 UDP,提供更快的传输速度和更好的性能

其它

  1. https://blog.csdn.net/justloveyou_/article/details/78303617
  2. 图解https的过程:https://segmentfault.com/a/1190000021494676
  3. 35 张图解:被问千百遍的 TCP 三次握手和四次挥手面试题
  4. 30张图解: TCP 重传、滑动窗口、流量控制、拥塞控制
  5. 硬核!30 张图解 HTTP 常见的面试题

CPU任务调度,进程/线程/协程

  1. 进程和线程的区别,了解协程吗?CPU调度,数据共享。
  2. 复杂系统中通常融合了多进程编程,多线程编程,协程编程
  3. 进程之间怎么通信,线程通信通信,协程怎么通信
  4. 进程之间怎么同步(信号量,自旋锁,屏障),线程之间怎么同步(锁),协程怎么同步。进程之间通过共享内存、管道、消息队列消息队列等方式通信,通过信号和信号量进行同步。线程在进程内部,全部变量时共享的,通过锁机制来同步。
  5. 死锁:产生的四个条件、四个解决方法,死锁检测
  6. 守护进程,linux系统编程实现守护进程
  7. 在Linux上,对于多进程,子进程继承了父进程的下列哪些?堆栈、文件描述符、进程组、会话、环境变量、共享内存
  8. 僵尸进程和孤儿进程。孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么那些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并由init进程对它们完成状态收集工作。僵尸进程:一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。
  9. 进程的状态。
    • TASK_RUNNING(运行态):进程是可执行du的;或者正在执行,zhi或者在运行队列中等待执行。
    • TASK_INTERRUPTIBLE(可中断睡眠态):进程被阻塞,等待某些条件的完成。一旦完成这些条件,内核就会将该进程的状态设置为运行态。
    • TASK_UNINTERRUPTIBLE(不可中断睡眠态):进程被阻塞,等待某些条件的完成。与可中断睡眠态不同的是,该状态进程不可被信号唤醒。
    • TASK_ZOMBIE(僵死态):该进程已经结束,但是其父进程还没有将其回收。
    • TASK_STOP(终止态):进程停止执行。通常进程在收到SIGSTOP、SIGTTIN、SIGTTOU等信号的时候会进入该状态。
  10. linux的CFS调度机制是什么?时间片/policy(进程类别)/priority(优先级)/counter。linux的任务调度机制是什么?在每个进程的task_struct结构中有以下四项:policy、priority、counter、rt_priority。这四项是选择进程的依据。其中,policy是进程的调度策略,用来区分实时进程和普通进程,实时进程优先于普通进程运行;priority是进程(包括实时和普通)的静态优先级;counter是进程剩余的时间片,它的起始值就是priority的值;由于counter在后面计算一个处于可运行状态的进程值得运行的程度goodness时起重要作用,因此,counter 也可以看作是进程的动态优先级。rt_priority是实时进程特有的,用于实时进程间的选择。 Linux用函数goodness()来衡量一个处于可运行状态的进程值得运行的程度。该函数综合了以上提到的四项,还结合了一些其他的因素,给每个处于可运行状态的进程赋予一个权值(weight),调度程序以这个权值作为选择进程的唯一依据。
  11. goroutine的GPM,没有时间片和优先级的概念,但也支持“抢占式调度”。 goroutine的主要状态grunnable、grunning、gwaiting
  12. 线程的状态
    • runnable
    • running
    • blocked
    • dead
  13. 进程、线程与协程的区别
  14. 操作系统写时复制:https://juejin.cn/post/6844903702373859335
  15. 操作系统为什么设计用户态和内核态,用户态和内核态的权限不同?怎么解决IO频繁发生内核和用户态的态的切换(缓存)?
  16. select、epoll的监听回调机制,红黑树?
  17. 从一道面试题谈linux下fork的运行机制
  18. malloc分配多少内存:http://fallincode.com/blog/2020/01/malloc%e6%9c%80%e5%a4%9a%e8%83%bd%e5%88%86%e9%85%8d%e5%a4%9a%e5%b0%91%e5%86%85%e5%ad%98/

存储系统,内存和存储

  1. 寄存器、缓存cache、内存和磁盘
  2. 可执行文件的空间结构,进程的空间结构(虚拟地址空间,栈,堆,未初始化变量,初始化区,代码)
  3. 查看进程使用的资源,top,ps,cat /proc/pid/status
  4. 进程的虚拟内存机制(虚拟地址-页表-物理地址)。Linux虚拟内存的实现需要6种机制的支持:地址映射机制、内存分配回收机制、缓存和刷新机制、请求页机制、交换机制和内存共享机制,内存管理程序通过映射机制把用户程序的逻辑地址映射到物理地址。当用户程序运行时,如果发现程序中要用的虚地址没有对应的物理内存,就发出了请求页要求。如果有空闲的内存可供分配,就请求分配内存(于是用到了内存的分配和回收),并把正在使用的物理页记录在缓存中(使用了缓存机制)。如果没有足够的内存可供分配,那么就调用交换机制;腾出一部分内存。另外,在地址映射中要通过TLB(翻译后援存储器)来寻找物理页;交换机制中也要用到交换缓存,并且把物理页内容交换到交换文件中,也要修改页表来映射文件地址。
  5. 操作系统内存分配算法常用缓存置换算法(FIFO,LRU,LFU),LRU算法的实现和优化?
  6. Linux系统原理之文件系统(磁盘、分区、文件系统、inode表、data block)
  7. 在linux执行ls上实际发生了什么
  8. CPU寻址过程,tlb,cache miss.
  9. 栈和堆的区别

系统编程以及其它注意事项

  1. 使用过哪些进程间通讯机制,并详细说明,linux进程之间的通信7种方式
  2. 内核函数、系统调用、库函数/API,strace系统调用追踪调试
  3. coredump文件产生?内存访问越界、野指针、堆栈溢出等等
  4. fork 和 vfork,exec,system(进程的用户空间是在执行系统调用的fork时创建的,基于写时复制的原理,子进程创建的时候继承了父进程的用户空间,仅仅是mm_struc结构的建立、vm_area_struct结构的建立以及页目录和页表的建立,并没有真正地复制一个物理页面,这也是为什么Linux内核能迅速地创建进程的原因之一。)写时复制(Copy-on-write)是一种可以推迟甚至免除拷贝数据的技术。内核此时并不复制整个进程空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也就是说,资源的复制只有在需要写入的时候才进行,在此之前,以只读方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候。有时共享页根本不会被写入,例如,fork()后立即调用exec(),就无需复制父进程的页了。fork()的实际开销就是复制父进程的页表以及给子进程创建唯一的PCB。这种优化可以避免拷贝大量根本就不会使用的数据
  5. 锁?互斥锁的属性设置、多进程共享内存的使用、多线程的使用互斥锁、pshaed和type设置。使用互斥量和条件变脸实现互斥锁
  6. 共享内存的同步机制,使用信号量,无锁数据结构
  7. 多线程里一个线程sleep,实质上是在干嘛,忙等还是闲等。?
  8. exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是”清理I/O缓冲”。
  9. select/epoll https://www.cnblogs.com/anker/p/3265058.html
  • select 内核态和用户态重复拷贝
  • select 需要遍历遍历查找就绪的socket
  • select 有数量限制1024
  • epoll 注册时写进内核
  • epoll_wait 返回就绪的事件

网络编程

  1. 简单了解C语言的socket编程api。socket,bind,listen,accept,connect,read/write.

  2. Linux下socket的五种I/O 模式,同步阻塞、同步非阻塞、同步I/O复用、异步I/O、信号驱动I/O

  3. Linux套接字和I/O模型

  4. select和epoll的区别

  5. 什么是I/O 复用?关于I/O多路复用(又被称为“事件驱动”),首先要理解的是,操作系统为你提供了一个功能,当你的某个socket可读或者可写的时候,它可以给你一个通知。这样当配合非阻塞的socket使用时,只有当系统通知我哪个描述符可读了,我才去执行read操作,可以保证每次read都能读到有效数据而不做纯返回-1和EAGAIN的无用功。写操作类似。操作系统的这个功能通过select/poll/epoll/kqueue之类的系统调用函数来使用,这些函数都可以同时监视多个描述符的读写就绪状况,这样,多个描述符的I/O操作都能在一个线程内并发交替地顺序完成,这就叫I/O多路复用,这里的“复用”指的是复用同一个线程。

  6. 网络分析工具。ping/tcpdump/netstat/lsof

其它问题

  1. 计算机中浮点数表示方法,以及浮点数转换中精度缺失的问题
0%