OpenClaw深度调研:27万星标的个人AI助手革命
经过一天深度使用OpenClaw,我为你带来这份全面调研报告。从项目背景、核心特性到实战体验,深入分析这个27万星标的现象级开源项目如何重新定义个人AI助手。
经过一天深度使用OpenClaw,我为你带来这份全面调研报告。从项目背景、核心特性到实战体验,深入分析这个27万星标的现象级开源项目如何重新定义个人AI助手。
本文是你的系统设计学习路线图,涵盖核心概念、4步设计方法论、实战练习和完整资源推荐。无论你是准备面试还是提升架构能力,这里都有你需要的一切。
在数字电商/本地生活平台中,不同品类的定价逻辑差异极大:
| 品类 | 价格特点 | 定价维度 | 费用组成 | 典型示例 |
|---|---|---|---|---|
| 酒店 (Hotel) | 时间维度定价,每日价格独立 | 日期 × 房型 × 早餐 | 房价 + DP Fee + Hub Fee | 曼谷万豪豪华房 |
| 电影票 (Movie) | 场次 × 座位 × 票种定价 | 场次 × 座位区 × 票种 | 票价 + DP Fee + 选座费 | 阿凡达3 IMAX 成人票 |
| 话费充值 (TopUp) | 面额定价,无 SKU 变体 | 运营商 × 面额 | 面额 + 手续费 - 补贴 | AIS 100฿ 充值 |
| 电子券 (E-voucher) | 面值 vs 售价差异 | 品牌 × 面值 | 面值 - 平台折扣 + DP Fee | 星巴克 500฿ 电子券 |
| 礼品卡 (Giftcard) | 面值定价 + 平台折扣 | 品牌 × 面值 | 面值 - 折扣 + DP Fee | Google Play 充值卡 |
| 本地生活套餐 | 组合定价,子项加总 | 套餐 × 子项 × 份数 | 套餐价 + 服务费 | 海底捞双人套餐 |
| 目标 | 说明 | 优先级 |
|---|---|---|
| 统一价格中心 | 所有价格计算通过统一的 Pricing Engine 进行 | P0 |
| 分层价格模型 | 基础价 → 营销价 → 费用 → 优惠券 → 最终价,层次清晰 | P0 |
| 规则引擎化 | 营销活动、费用规则可配置,支持动态调整 | P0 |
| 价格快照 | 每笔订单保留完整价格计算明细,支持审计和追溯 | P0 |
| 高性能 | P99 < 100ms,支持万级 QPS 并发价格计算 | P1 |
| 多级降级 | 促销/优惠券服务不可用时,仍能返回基础价格 | P1 |
| 可扩展 | 新增营销活动类型、费用类型、优惠券类型无需改动核心架构 | P1 |
1 | ┌─────────────────────────────────────────────────────────────────────┐ |
1 | ┌──────────────────────────────────────────────────────────────────┐ |
| 组件 | 说明 | 计算方式 | 示例 |
|---|---|---|---|
| Base Price | SKU 基础售价 | 固定 / 时间动态 | 480฿(电影票) |
| Promotion Discount | 营销活动折扣 | 百分比 / 固定金额 / 满减 | -50฿(新用户立减) |
| DP Fee | DP 平台手续费 | 固定 / 百分比 | +10฿ |
| Hub Fee | Hub 商户服务费 | 固定 / 百分比 / 阶梯 | +5฿ |
| Service Fee | 附加服务费 | 固定 | +20฿(选座费) |
| Tax | 税费 | 百分比 | +7%(VAT) |
| Voucher Discount | 优惠券抵扣 | 固定 / 百分比 / 封顶 | -30฿ |
| Final Price | 最终支付价格 | Base - Promo + Fee - Voucher | 435฿ |
sku_tab(SKU 基础价格,已在商品模型中定义):
1 | -- 核心价格字段: |
dynamic_pricing_rule_tab(动态定价规则):
1 | CREATE TABLE `dynamic_pricing_rule_tab` ( |
promotion_activity_tab(营销活动主表):
1 | CREATE TABLE `promotion_activity_tab` ( |
discount_value JSON 配置说明:
| 折扣类型 | JSON 配置示例 | 说明 |
|---|---|---|
| percentage | {"percentage": 20} |
打8折(减20%) |
| fixed_amount | {"amount": 50} |
立减50฿ |
| full_reduction | {"threshold": 3000, "discount": 200} |
满3000减200 |
| buy_n_get_m | {"buy": 3, "free": 1} |
买3送1 |
| tiered_discount | {"tiers": [{"threshold": 500, "percentage": 5}, {"threshold": 200, "percentage": 3}]} |
阶梯折扣 |
promotion_usage_log_tab(活动使用记录):
1 | CREATE TABLE `promotion_usage_log_tab` ( |
fee_config_tab(费用配置表):
1 | CREATE TABLE `fee_config_tab` ( |
calculation_config JSON 配置说明:
| 计算方式 | JSON 配置示例 | 说明 |
|---|---|---|
| fixed | {"amount": 10} |
固定10฿ |
| percentage | {"percentage": 2.5} |
按基础价的2.5% |
| tiered | {"tiers": [{"threshold": 5000, "fee": 150}, {"threshold": 3000, "fee": 100}, {"threshold": 0, "fee": 50}]} |
金额阶梯 |
voucher_tab(优惠券主表):
1 | CREATE TABLE `voucher_tab` ( |
user_voucher_tab(用户优惠券表):
1 | CREATE TABLE `user_voucher_tab` ( |
price_snapshot_tab(价格快照表):
1 | CREATE TABLE `price_snapshot_tab` ( |
price_change_log_tab:
1 | CREATE TABLE `price_change_log_tab` ( |
1 | // PricingEngine 价格计算引擎 |
1 | // Calculate 计算价格(核心入口) |
不同东南亚国家的币种精度差异很大,这是必须处理的核心问题:
| 币种 | 代码 | 小数位 | 最小单位 | 示例 |
|---|---|---|---|---|
| 泰铢 | THB | 2 | 0.01 | 480.50฿ |
| 越南盾 | VND | 0 | 1 | 120000₫ |
| 印尼盾 | IDR | 0 | 1 | 85000Rp |
| 马来西亚林吉特 | MYR | 2 | 0.01 | RM 25.90 |
| 新加坡元 | SGD | 2 | 0.01 | S$12.50 |
| 菲律宾比索 | PHP | 2 | 0.01 | ₱350.00 |
1 | // roundByCurrency 按币种精度四舍五入 |
1 | // PromotionMatcher 营销活动匹配器 |
1 | // FeeCalculator 费用计算器 |
1 | // buildPriceFormula 构建人类可读的价格公式 |
1 | 用户请求价格计算 |
问题:用户在商品列表、详情页、购物车、订单确认页看到的价格不一致。
1 | ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ |
方案:
1 | // 加入购物车 → 生成价格快照 |
规则矩阵:
| 活动 | Priority | Exclusivity | Voucher Compatible | 说明 |
|---|---|---|---|---|
| 秒杀特价 | 15 | 1(互斥) | 0(与券互斥) | 独占,与券互斥 |
| 新用户立减50฿ | 10 | 0(可叠加) | 1(可与券叠加) | 可叠加,可与券叠加 |
| 满3000减200 | 5 | 0(可叠加) | 1(可与券叠加) | 可叠加,可与券叠加 |
| 周末特惠 | 3 | 0(可叠加) | 1(可与券叠加) | 可叠加,可与券叠加 |
匹配流程:
1 | 活动列表(按 Priority DESC 排序) |
计算示例:
问题:哪些费用可以被优惠券抵扣?
| 费用类型 | can_be_discounted | 原因 |
|---|---|---|
| DP Fee | 0(不可抵扣) | 平台收入,不参与优惠 |
| Hub Fee | 按协议配置 | 商户协议决定 |
| Service Fee(选座费) | 1(可抵扣) | 增值服务,可参与优惠 |
| Tax | 0(不可抵扣) | 税费不可被优惠 |
抵扣逻辑:
1 | // calculateDiscountableAmount 计算优惠券可抵扣金额 |
示例:
1 | 商品小计: 1000฿ |
秒杀/限量活动场景下,配额扣减必须保证原子性。使用 Redis Lua 脚本实现:
1 | -- promotion_quota_consume.lua |
1 | // 优惠券核销:Redis + MySQL 双写 |
1 | // 订单取消/退款 → 回退优惠券和配额 |
基础数据:
1 | -- SKU: 阿凡达3 IMAX 成人票 |
计算过程:
1 | 请求: user_id=100001, sku_id=2000001, quantity=2, voucher=VOUCHER_MOVIE_30 |
基础数据:
1 | -- 动态定价: 库存紧张加价15% |
计算过程:
1 | 请求: user_id=100002, sku_id=1000002(豪华房), quantity=1, context.nights=2, context.available_rooms=3 |
1 | 请求: user_id=100003, item=AIS 500฿充值, sku_id=3000001, quantity=1 |
价格服务是核心链路,任何组件故障不能导致整个计算失败。
1 | ┌─────────────────────────────────────────────────────────────┐ |
1 | // DegradeConfig 降级配置 |
通过配置中心(Apollo / Nacos)动态控制降级开关,无需重启服务:
1 | { |
1 | ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ |
| 数据类型 | Redis Key | TTL | 说明 |
|---|---|---|---|
| SKU 基础价格 | price:base:{sku_id} |
5min | 基础价格变化不频繁 |
| 品类活动列表 | promo:active:cat:{category_id} |
5min | 缓存生效活动 |
| 费用配置 | fee:config:cat:{category_id} |
10min | 费用配置变化少 |
| 优惠券信息 | voucher:info:{voucher_code} |
5min | 券信息 |
| 活动配额 | promo:quota:{activity_id} |
无过期 | 配额计数器 |
| 价格计算结果 | price:calc:{sku_id}:{user_hash} |
60s | 短缓存,防穿透 |
1 | // 价格变更时主动失效缓存(通过 Kafka 消息驱动) |
| 事件 | Topic | 生产者 | 消费者 | 说明 |
|---|---|---|---|---|
| 价格变更 | dp.pricing.price_changed |
价格管理后台 | Pricing Engine、搜索服务 | SKU 基础价格变更 |
| 活动创建/更新 | dp.pricing.promotion_changed |
营销管理后台 | Pricing Engine | 营销活动变更 |
| 费用配置变更 | dp.pricing.fee_changed |
费用管理后台 | Pricing Engine | 费用配置变更 |
| 价格快照生成 | dp.pricing.snapshot_created |
Pricing Engine | 数据仓库 | 快照异步落库 |
| 优惠券核销 | dp.pricing.voucher_consumed |
Pricing Engine | 优惠券服务 | 优惠券使用 |
| 退款回退 | dp.pricing.refund_rollback |
订单服务 | Pricing Engine | 退款时回退配额/券 |
1 | // PriceChangeEvent 价格变更事件 |
1 | // 幂等处理:避免重复消费导致配额多次扣减 |
基础价格、促销匹配、费用计算三者互相独立,可并行执行:
1 | func (e *PricingEngine) parallelCalculate(ctx context.Context, req *PriceRequest) ( |
列表页需要同时计算多个商品的价格,使用批量接口减少 RPC 和 DB 调用:
1 | // BatchCalculate 批量价格计算(列表页场景) |
| 指标 | 目标值 | 监控方式 |
|---|---|---|
| 单价计算 P50 | < 20ms | Prometheus |
| 单价计算 P99 | < 100ms | Prometheus |
| 批量计算 P99(20个) | < 200ms | Prometheus |
| 缓存命中率 | > 85% | Redis Monitor |
| QPS | > 10,000 | Load Test |
| CPU 使用率 | < 60% | Grafana |
1 | # 价格计算 QPS 和延迟 |
| 告警名称 | 条件 | 级别 | 处理 |
|---|---|---|---|
| 价格计算超时 | P99 > 200ms 持续5分钟 | P1 | 检查缓存、DB、下游服务 |
| 降级率过高 | 降级率 > 5% 持续3分钟 | P1 | 检查促销/优惠券服务可用性 |
| 价格计算失败率 | 失败率 > 1% 持续3分钟 | P0 | 紧急排查,可能影响下单 |
| 缓存命中率下降 | 命中率 < 70% 持续5分钟 | P2 | 检查 Redis 集群状态 |
| 配额即将耗尽 | 剩余配额 < 10% | P3 | 通知运营补充配额 |
| 价格异常波动 | 同 SKU 价格波动 > 30% | P2 | 检查动态定价规则 |
1 | // ReconstructPriceCalculation 根据订单还原价格计算(客服工具) |
四步接入:
fee_config_tab。promotion_activity_tab 中 category_ids 加入新品类 ID。BasePriceCalculator 接口。1 | // 示例:接入新品类 "演唱会门票" |
品类接入检查清单:
| 检查项 | 说明 | 必须 |
|---|---|---|
| 基础价格来源 | SKU 固定价 / 日历价 / 动态定价 | ✅ |
| 费用配置 | 确定涉及哪些费用类型 | ✅ |
| 币种精度 | 确认该品类对应的币种和精度 | ✅ |
| 营销活动兼容 | 验证现有活动对新品类是否生效 | ✅ |
| 价格快照字段 | 确认 context 中是否需要额外字段 | ⬜ |
| 自定义计算器 | 是否需要特殊基础价格计算逻辑 | ⬜ |
1 | ┌─────────────────────────────────────────────────────────────────┐ |
| 维度 | Phase 1 | Phase 2 | Phase 3 | Phase 4 | Phase 5 |
|---|---|---|---|---|---|
| 计算方式 | 硬编码 | 配置化 | 规则引擎 | ML 模型 | 在线学习 |
| 响应时间 | <10ms | <50ms | <100ms | <200ms | <100ms |
| 扩展性 | 差 | 中 | 好 | 好 | 优秀 |
| 维护成本 | 高 | 中 | 中 | 中 | 低 |
| 智能程度 | 无 | 低 | 中 | 高 | 极高 |
| 代表企业 | 早期电商 | 淘宝/京东 | 天猫/Amazon | Uber/Airbnb | Amazon/阿里 |
1 | 价格计算链路: 商品价格 → 营销活动 → 优惠券 → 积分 → 红包 |
1 | 价格计算链路: 基础价 → 会员价(Plus) → 活动 → 优惠券 → 京豆 → 运费 |
1 | 价格计算链路: 基础价 → ML 动态定价 → Prime 折扣 → Lightning Deal → Subscribe & Save |
1 | 定价逻辑: base_price = distance × rate + duration × rate → surge_multiplier |
| 特性 | 我们的设计 | 淘宝天猫 | 京东 | Amazon |
|---|---|---|---|---|
| 分层架构 | 4层(Base/Promo/Fee/Voucher) | 5层(含积分/红包) | 5层(含京豆) | 3层(简化) |
| 规则引擎 | DB 配置 + JSON | Drools + 自研 | 自研 | 自研 |
| 动态定价 | 规则驱动 | ML 驱动 | 规则驱动 | ML 驱动 |
| 价格快照 | ✅ 完整支持 | ✅ 支持 | ✅ 支持 | ✅ 支持 |
| 费用拆分 | ✅ 详细(DP/Hub/Service/Tax) | ✅ 详细 | ✅ 详细 | ✅ 详细 |
| 多币种 | ✅ 6种东南亚货币 | 单一(CNY) | 单一(CNY) | 全球多币种 |
| 降级策略 | ✅ 5级降级 | ✅ 完善 | ✅ 完善 | ✅ 完善 |
| ML 定价 | ❌ 待规划 | ✅ 深度应用 | ⚠️ 部分 | ✅ 核心能力 |
我们的差异化优势:
| 决策 | 选择 | 原因 |
|---|---|---|
| 统一 vs 独立 | 统一 Pricing Engine + 策略模式 | 复用计算逻辑,新品类零代码接入 |
| 同步 vs 异步 | 价格计算同步,快照异步写入 | 热路径极速,冷路径可靠 |
| 缓存策略 | L1 本地 + L2 Redis + L3 MySQL | 多级缓存,高性能 + 可靠 |
| 精度处理 | decimal.Decimal + 币种精度表 | 避免浮点误差 |
| 降级策略 | 5级降级,促销/券可降级,基础价不可降级 | 保证核心链路可用 |
| 互斥规则 | Priority + Exclusivity 字段 | 灵活配置,无需改代码 |
| 配额管理 | Redis Lua 原子操作 | 高并发场景不超卖 |
| 审计追溯 | 价格快照 + 变更日志 | 完整记录,客诉可还原 |
| 组件 | 技术选型 | 说明 |
|---|---|---|
| Pricing Engine | Go 微服务 | 价格计算核心引擎 |
| 缓存层 | Redis Cluster + sync.Map | 多级缓存 |
| 规则引擎 | MySQL + JSON 配置 | 灵活配置营销规则 |
| 价格快照 | MySQL | 持久化价格明细 |
| 消息队列 | Kafka | 价格变动事件、快照异步写入 |
| 配额管理 | Redis Lua | 原子扣减 |
| 监控 | Prometheus + Grafana | 性能监控与告警 |
| 配置中心 | Apollo / Nacos | 降级开关、超时配置 |
| 阶段 | 时间 | 内容 | 交付物 |
|---|---|---|---|
| P1: 基础架构 | 2周 | 数据库表、Pricing Engine 框架、基础价格层 | 可运行的计算引擎 |
| P2: 营销活动 | 3周 | 促销规则引擎、匹配器、配额管理、管理后台 | 支持折扣/满减/秒杀 |
| P3: 费用与优惠券 | 2周 | Fee 计算器、Voucher 系统、叠加规则 | 完整4层计算 |
| P4: 性能与审计 | 2周 | 多级缓存、并行计算、价格快照、审计工具 | P99 < 100ms |
| P5: 全量上线 | 1周 | 全品类覆盖、监控告警、文档培训 | 生产环境稳定运行 |
1 | ┌─────────────────────────────────────────────────────────────────┐ |
| 风险 | 影响 | 缓解措施 |
|---|---|---|
| 规则配置错误 | 价格异常 | 配置审批流程 + 灰度发布 + 回滚机制 |
| 性能瓶颈 | 响应超时 | 缓存预热 + 限流降级 + 水平扩展 |
| 价格不一致 | 用户投诉 | 统一 API + 快照机制 + 实时校验 |
| 促销超卖 | 成本损失 | Redis Lua 原子扣减 + 异步对账 |
| 多币种精度 | 金额误差 | decimal.Decimal + 币种精度表 + 分摊尾差处理 |
| 缓存穿透 | DB 压力 | 布隆过滤器 + 空值缓存 + 限流 |
在数字电商/本地生活平台中,商品上架的数据来源和审核策略差异极大:
| 数据来源 | 触发方式 | 数据可信度 | 审核策略 | 典型场景 |
|---|---|---|---|---|
| 供应商 Push | 供应商实时推送 MQ 消息 | 高(合作方) | 自动审核(快速通道) | 电影票场次变更 |
| 供应商 Pull | 定时任务主动拉取 API | 高(合作方) | 自动审核(快速通道) | 酒店房型价格同步 |
| 运营上传 | 运营后台单品/批量 | 高(内部) | 免审核或自动审核 | 话费充值面额配置 |
| 商家上传 | Merchant App/Portal | 低(需审核) | 人工审核 | 商家自营电子券 |
| API 接口 | 第三方系统调用 | 中(看调用方) | 根据来源配置 | 批量导入工具 |
| 品类 | 主要数据来源 | 对接方式 | 审核策略 | 特殊处理 |
|---|---|---|---|---|
| 酒店 (Hotel) | 供应商 Pull / 运营批量 | 定时拉取 API (Cron) | 自动审核 | 价格日历校验 |
| 电影票 (Movie) | 供应商 Push | 实时推送 (MQ) | 自动审核(快速通道) | 场次时间校验 |
| 话费充值 (TopUp) | 运营上传 | 单品表单 / Excel 批量 | 免审核 | 面额范围校验 |
| 电子券 (E-voucher) | 商家上传 / 供应商 Pull | Portal + 券码池 / API | 人工审核 | 券码池异步导入 |
| 礼品卡 (Giftcard) | 运营上传 / 商家上传 | 单品表单 / Merchant App | 商家需审核,运营免审 | 库存校验 |
核心痛点:
| 目标 | 说明 | 优先级 |
|---|---|---|
| 统一上架流程 | 所有品类共享统一状态机和流程 | P0 |
| 异步化处理 | 上传、审核、发布异步化,提升响应速度 | P0 |
| 批量上传 | 支持 Excel/CSV 批量上传 | P0 |
| 状态可追溯 | 完整的状态变更历史记录 | P0 |
| 并发安全 | 乐观锁 + 唯一索引保证一致性 | P1 |
| 故障自愈 | 看门狗机制监控超时任务,自动重试 | P1 |
1 | ┌─────────────────────────────────────────────────────────────┐ |
1 | ┌──────────┐ |
1 | const ( |
每次上架操作对应一条任务记录,是整个流程的核心载体:
1 | CREATE TABLE listing_task_tab ( |
Excel 批量导入时,一个文件对应一条批量任务,下挂多条 listing_task:
1 | CREATE TABLE listing_batch_task_tab ( |
1 | CREATE TABLE listing_batch_item_tab ( |
1 | -- 审核日志 |
根据数据来源自动选择审核策略:
1 | CREATE TABLE listing_audit_config_tab ( |
根据数据来源自动选择审核策略:
1 | 创建上架任务 |
1 | 用户提交表单 |
1 | 用户上传 Excel |
1 | 供应商发送影片/场次变更消息 (MQ) |
1 | 定时任务触发(每小时 / 每 30 分钟) |
| 对比项 | 推送模式 (Push) | 拉取模式 (Pull) |
|---|---|---|
| 代表品类 | Movie(电影票) | Hotel(酒店)、E-voucher |
| 触发方式 | 供应商主动推送 MQ 消息 | 定时任务周期性拉取 |
| 实时性 | 高(毫秒级) | 中(分钟级) |
| 数据完整性 | 依赖 MQ 可靠性 | 主动拉取保证完整 |
| 系统耦合度 | 供应商需感知平台 | 平台主动拉取,供应商无感知 |
| 适用场景 | 高频变更、实时性要求高、单次数据量小 | 低频变更、可接受延迟、单次数据量大 |
1 | CREATE TABLE supplier_sync_state_tab ( |
所有状态变更使用乐观锁,防止并发冲突:
1 | func UpdateStatus(taskID int64, fromStatus, toStatus int, action string) error { |
task_code 唯一索引保证同一上架操作不会重复创建:
1 | func CreateTask(req *CreateTaskRequest) (*ListingTask, error) { |
监控超时和卡住的任务,自动重试或告警:
1 | func (w *WatchdogService) Start() { |
不同品类注册不同校验规则,通过规则引擎统一执行:
1 | type ValidationEngine struct { |
批量上架使用 Worker Pool 控制并发度:
1 | func PublishBatch(batchID int64) error { |
| Topic | 触发时机 | 消费者 |
|---|---|---|
listing.batch.created |
Excel 上传完成 | ExcelParseWorker |
listing.audit.pending |
提交审核 | AuditWorker |
listing.publish.ready |
审核通过 | PublishWorker |
listing.published |
发布成功 | ES 同步、缓存刷新、通知 |
listing.batch.parsed |
Excel 解析完成 | BatchAuditWorker |
listing.batch.audited |
批量审核完成 | BatchPublishWorker |
1 | message ListingEvent { |
| 借鉴来源 | 应用方式 |
|---|---|
| 淘宝:强模板 + 定时上架 | 品类校验规则引擎 + 定时发布 |
| 京东:三级审核 + 商品池 | 自动/人工审核 + 状态机管理 |
| Amazon:质量评分 + API 接入 | 数据完整度校验 + 供应商/API 双模式 |
| Shopee:本地化 + 快速上架 | 多国家模板 + 供应商快速通道 |
| 指标 | 目标值 | 告警阈值 |
|---|---|---|
| 上架成功率 | > 95% | < 90% |
| 平均上架时长 | < 5 分钟 | > 10 分钟 |
| 批量处理速度 | > 100 条/分钟 | < 50 条/分钟 |
| 审核通过率 | > 90% | < 80% |
| Worker 处理延迟 | < 1 分钟 | > 5 分钟 |
| Kafka 消息积压 | < 1000 条 | > 5000 条 |
1 | listing_task_total{type="single|batch|supplier", status="success|fail"} |
四步接入:
ValidationRule 接口,注册到校验引擎。1 | // 示例:接入新品类"演唱会门票" |
| 决策 | 选择 | 原因 |
|---|---|---|
| 统一 vs 独立流程 | 统一状态机 + 策略模式 | 复用流程,新品类零代码接入 |
| 同步 vs 异步 | API 层同步创建任务,审核/发布异步 Worker | 快速响应 + 后台可靠处理 |
| 供应商对接 | Push + Pull 双模式 | 适配不同供应商实时性需求 |
| 审核策略 | 数据来源驱动(供应商/运营/商家) | 灵活控制审核流程 |
| 并发控制 | 乐观锁 + 唯一索引 | 轻量级,无分布式锁开销 |
| 故障恢复 | 看门狗 + 自动重试 | 超时/卡住任务自动恢复 |
| 批量处理 | Worker Pool + 分批事务 | 控制并发 + 保证一致性 |
在数字电商/本地生活平台中,不同品类的库存特性差异极大:
| 品类 | 库存特点 | 扣减时机 | 典型示例 |
|---|---|---|---|
| 电子券 (Deal) | 券码制,每个券码唯一 | 下单预订 | 星巴克电子券 |
| 虚拟服务券 (OPV) | 数量制,分平台统计 | 下单预订 | 美甲/按摩服务券 |
| 酒店 | 时间维度,按日期管理 | 支付成功 | Agoda 酒店房间 |
| 机票/票务 | 座位/场次制 | 支付成功 | 航班座位、电影票 |
| 礼品卡 (Giftcard) | 实时生成或预采购卡密 | 支付成功 | Google Play 充值卡 |
| 话费充值 (TopUp) | 无限库存 | 无需扣减 | 手机话费 |
| 本地生活套餐 | 组合型,多子项联动 | 下单预订 | 火锅双人套餐 |
| 目标 | 说明 | 优先级 |
|---|---|---|
| 统一模型 | 多品类共用一套库存模型 | P0 |
| 高性能 | 支持万级 QPS 秒杀场景 | P0 |
| 灵活扩展 | 新品类接入无需修改核心代码 | P0 |
| 最终一致 | Redis 与 MySQL 数据最终一致 | P0 |
| 供应商集成 | 支持实时/定时/推送多种同步策略 | P1 |
设计统一库存模型的关键是将所有品类抽象为 两个正交维度:
维度一:谁管库存?(Management Type)
1 | const ( |
维度二:库存长什么样?(Unit Type)
1 | const ( |
| 品类 | 管理类型 | 单元类型 | 扣减时机 |
|---|---|---|---|
| 电子券 (Deal) | Self | Code | 下单 |
| 虚拟服务券 (OPV) | Self | Quantity | 下单 |
| 本地服务 | Self | Quantity | 下单 |
| 酒店 | Supplier | Time | 支付 |
| 机票 | Supplier | Quantity | 支付 |
| 话费充值 | Unlimited | - | 无 |
| 礼品卡(预采购) | Self | Code | 下单 |
| 礼品卡(实时生成) | Supplier | Code | 支付 |
| 套餐组合 | Self | Bundle | 下单 |
核心洞察:任何新品类接入时,只需确定它属于哪个
(ManagementType, UnitType)组合,即可复用对应的库存策略,无需修改核心代码。
每个 SKU 一条配置,决定该商品使用哪种库存策略:
1 | CREATE TABLE inventory_config ( |
所有品类共用一张库存表,通过不同字段组合适配不同场景:
1 | CREATE TABLE inventory ( |
库存恒等式:
1 | total_stock = available_stock + booking_stock + locked_stock + sold_stock |
可售库存计算(不同管理类型计算方式不同):
1 | func CalcAvailable(inv *Inventory, cfg *Config) int32 { |
仅用于券码制商品(Deal、Giftcard 预采购模式):
1 | CREATE TABLE inventory_code_pool_00 ( |
所有库存变更留痕,用于对账和审计:
1 | CREATE TABLE inventory_operation_log ( |
1 | ┌─────────────────────────────────────────────────┐ |
1 | // InventoryStrategy 库存管理策略接口 |
1 | Key: inventory:code:pool:{itemID}:{skuID}:{batchID} |
1 | 用户下单 |
出货 Lua 脚本(原子性保证):
1 | -- 原子取出 N 个券码 |
补货流程(加分布式锁防并发):
1 | func (s *SelfManagedStrategy) replenish(ctx context.Context, itemID, skuID, batchID uint64) error { |
1 | Key: inventory:qty:stock:{itemID}:{skuID} |
1 | local key = KEYS[1] |
1 | -- 支付成功:booking → issued |
| 策略 | 适用场景 | 实时性 | 实现方式 |
|---|---|---|---|
| 实时查询 | 库存变化快(机票) | 高 | 每次请求调供应商 API(30s 缓存) |
| 定时同步 | 库存变化中等(酒店) | 中 | 定时任务每 5 分钟拉取 |
| Webhook | 供应商主动推送 | 高 | 接收推送更新本地缓存 |
1 | func (s *SupplierManagedStrategy) CheckStock(ctx context.Context, req *CheckStockReq) (*CheckStockResp, error) { |
供应商 API 质量好,预订接口同步返回结果:
1 | func (s *SupplierManagedStrategy) BookStock(ctx context.Context, req *BookStockReq) (*BookStockResp, error) { |
场景:部分供应商系统不稳定,预订流程为:
booking_id(状态 PENDING)CONFIRMED / FAILEDCONFIRMED 后才能继续下单挑战:
状态机设计:
1 | 用户下单 |
数据库表设计:
1 | CREATE TABLE supplier_booking ( |
实现流程:
1 | // 1. 用户下单时:创建 booking 单,立即返回"处理中" |
问题:用户下单后看到”预订处理中”,体验不佳。
优化方案:
1 | // 用户下单后,前端每 2 秒轮询订单状态 |
1 | // 服务端:booking 确认后推送消息 |
1 | // 预订成功后 1 分钟内发送通知 |
场景 1:轮询期间用户取消订单
1 | func PollBookingStatus(task *BookingPollTask) { |
场景 2:供应商 API 持续超时
1 | // 连续 3 次查询超时 → 降级到人工处理 |
场景 3:供应商确认后用户未支付
1 | // booking 成功后设置 15 分钟支付超时 |
| 指标 | 阈值 | 说明 |
|---|---|---|
| booking 成功率 | > 95% | 供应商库存准确性 |
| 平均确认时长 | < 10s | P99 < 30s |
| 超时率 | < 1% | 需要人工介入的比例 |
| 取消率 | < 5% | 用户等待期间取消订单 |
1 | // Prometheus Metrics |
最简单的策略,只记录操作日志:
1 | type UnlimitedStrategy struct{} |
1 | 用户下单 |
1 | 支付回调 |
1 | 订单取消 / 超时未支付 |
| 操作 | Redis | MySQL | 一致性保障 |
|---|---|---|---|
| 预订 (Book) | 同步扣减(Lua 原子) | Kafka 异步更新 | 最终一致 |
| 支付 (Sell) | 同步更新 | Kafka 异步更新 | 最终一致 |
| 营销锁定 (Lock) | 同步 | 同步(DB 事务) | 强一致 |
| 补货 (Replenish) | 同步写入 | 不变 | - |
核心原则:
1 | func Reconcile() { |
1 | Redis 可用 → 正常读写 Redis |
1 | message InventoryEvent { |
Topic 设计:
inventory.book — 预订inventory.unbook — 释放inventory.sell — 售出inventory.refund — 退款inventory.sync — 供应商同步Giftcard 横跨三种库存模式,是统一模型的最佳验证:
| 模式 | 管理类型 | 流程 | 适用场景 |
|---|---|---|---|
| 预采购卡密 | Self + Code | 批量导入 → Redis 出货 | 高频热销卡 |
| 实时生成 | Supplier + Code | 支付成功 → 调 API 生成 → 存入 code_pool | 长尾低频卡 |
| 无限库存 | Unlimited | 直接成功 | 供应商保证库存 |
卡密安全:
XXXX-XXXX-XXXX-1234)。供应商 API 超时处理:
| 指标 | 阈值 | 告警级别 |
|---|---|---|
| 超卖次数 | > 0 | P0 |
| Redis vs MySQL 差异 | > 100 | P1 |
| 库存服务错误率 | > 1% | P1 |
| 库存扣减 P99 | > 200ms | P2 |
| 补货失败率 | > 5% | P2 |
| 供应商同步延迟 | > 10min | P2 |
| 低库存商品数 | > 100 | P3 |
1 | # 操作计数 |
三步接入:
(ManagementType, UnitType, DeductTiming)。inventory_config 表插入一条记录。InventoryManager.BookStock() 即可。1 | // 示例:接入新品类"演唱会门票" |
| 指标 | 数值 | 说明 |
|---|---|---|
| 秒杀峰值 QPS | 20,000 | 单个爆款商品,持续 5-10 分钟 |
| 日均 QPS | 50 | 常态流量 |
| 日均订单量 | 2,000,000 | 支付成功订单 |
| 日均库存扣减 | 6,700,000 | 含预订、支付、取消等操作 |
| 峰值/日均比 | 870:1 | 流量极度不均匀 |
容量规划推算:
1 | 日均订单 2M / 86400s ≈ 23 TPS |
1 | 拓扑: Redis Cluster (3 主 3 从) |
容量规划:
1 | 实例数: 10 台 (Kubernetes Pod) |
为什么 10 台能抗 2w QPS?
1 | 架构: 1 主 2 从(半同步复制) |
容量规划:
1 | Broker: 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 | 非秒杀场景 |
秒杀场景优化后:
问题:
解决方案:
**本地缓存 (Caffeine)**:
Key 分散(适用于读多写少):
stock:item_123:0 ~ stock:item_123:9。限流前置:
item_id 限流,单商品最大 2.5w QPS(留 20% 余量)。问题:
根因:
inventory_code_pool_xx 表数据量大(千万级),status=1 索引选择性差。解决方案:
优化 SQL:
1 | -- 增加复合索引 |
耗时从 12s 降至 50ms。
锁续期:
异步补货:
问题:
瓶颈分析:
UPDATE inventory SET booking_stock = booking_stock + 1解决方案:
批量写入:
1 | // 攒批 100 条,批量 INSERT |
TPS 从 5k 提升至 8 万(提升 16 倍)。
降低一致性要求:
inventory_operation_log 日志表改为异步从库写入。inventory 核心表。削峰:
linger.ms=100ms,Producer 端攒批发送。统计数据(3 个月):
主要根因:
| 原因 | 占比 | 说明 |
|---|---|---|
| Kafka 消费延迟 | 60% | 秒杀后消费积压,MySQL 未及时更新 |
| Redis 补货未同步 MySQL | 25% | 券码补货只更新 Redis,DB 未记录 |
| 人工后台操作 | 10% | 运营手动修改 DB 库存 |
| Redis 重启丢数据 | 5% | AOF 未及时刷盘(appendfsync everysec) |
优化措施:
total_stock。appendfsync always(性能下降 30%,换取强一致)。| 资源 | 配置 | 数量 | 月成本(美元) |
|---|---|---|---|
| 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 美分)
| 决策 | 选择 | 原因 |
|---|---|---|
| 统一 vs 独立 | 统一模型 + 策略模式 | 复用逻辑,新品类零代码接入 |
| Redis vs MySQL | Redis 优先,MySQL 持久化 | 高并发性能 + 数据可靠 |
| 同步 vs 异步 | 扣减同步,落库异步 | 热路径极速,冷路径可靠 |
| 券码出货方式 | Lazy Loading(按需补货) | 节省内存,避免一次性加载全量 |
| 对账策略 | 每小时自动对账,MySQL 为准 | 兜底一致性 |
| 降级策略 | Redis 宕机切 MySQL | 性能下降 10 倍,但业务不中断 |
| 维度 | 淘宝/京东 | Amazon | 本设计 |
|---|---|---|---|
| 库存单元 | SKU 数量 | ASIN + FBA | SKU + 批次/日期 |
| 扣减时机 | 下单预订 | 支付成功 | 可配置 |
| 虚拟商品 | 部分支持 | 完善 | 核心场景 |
| 时间维度 | 不支持 | 不支持 | 支持 |
| 券码管理 | 部分 | 完善 | 核心能力 |
| 供应商集成 | 少量 | FBA 模式 | 多策略 |
| 峰值 QPS | 100 万+ | 50 万+ | 2 万(中型平台) |
核心挑战:瞬时流量巨大、库存超卖、恶意脚本。
架构分层:
| 层级 | 策略 |
|---|---|
| 客户端/CDN | 静态资源缓存;按钮置灰+答题验证(削峰防刷) |
| 网关层 | 令牌桶/漏桶限流;黑名单拦截;设备指纹识别 |
| 服务层 | 库存预热到 Redis;MQ 异步扣减 DB 库存;非核心服务降级 |
防超卖(核心):
if redis.call('get', key) > 0 then redis.call('decr', key) ...UPDATE stock SET num = num - 1 WHERE id = ? AND num > 0防黄牛/脚本:
算法对比:
| 算法 | 优点 | 缺点 |
|---|---|---|
| 固定窗口计数器 | 实现简单 | 临界突发:窗口交界处可能 2 倍流量 |
| 滑动窗口 | 解决临界突发 | 内存开销大(需存每个请求时间戳) |
| 漏桶 | 平滑输出 | 无法应对合理突发 |
| 令牌桶 | 允许突发 | 实现稍复杂 |
分布式实现:Redis + Lua(ZSet 滑动窗口 / Token Bucket)。
动态限流:基于 CPU、RT、错误率自适应调整阈值(Sentinel / Hystrix)。
场景:秒杀商品、热搜词、突发事件导致单个 Key 流量爆炸。
方案:
key_1 ~ key_N),分散到多个 Redis 分片。| 手段 | 目标 | 触发条件 |
|---|---|---|
| 限流 | 控制入口流量 | QPS 超阈值 |
| 熔断 | 切断对下游的调用 | 下游错误率/超时率过高 |
| 降级 | 关闭非核心功能 | 系统负载高、人工/自动触发 |
| 兜底 | 给用户默认响应 | 降级后的补偿策略 |
口诀:限流防激增,熔断防雪崩,降级保核心,兜底提体验。
挑战:LLM 推理慢(秒级)、显存/线程池易耗尽、Token 成本高。
优化策略:
方案对比:
| 方案 | 空间 | 精确度 | 支持删除 |
|---|---|---|---|
| Bitmap | 40亿 ≈ 500MB | 精确 | 否 |
| Bloom Filter | 极小(几十 MB) | 有误判 | 否(Counting BF 可以,但空间 ×4) |
| HyperLogLog | 12KB | 误差 0.81% | 否 |
最佳回答:
Redis ZSet 方案:
1 | ZADD rank 5000 "player_1" |
陷阱:ZSet 元素超过千万级 → 大 Key 阻塞主线程。
解决方案(分桶 + 聚合):
rank_0, rank_1 ... rank_N。分页优化:
ZRANGE 深分页性能差(O(logN + M))。(score, member_id),下一页从该位置继续查。Bitmap:1 bit 表示 1 个用户的在线/离线。1亿用户仅 12MB,10 亿用户约 120MB。
1 | SETBIT online 123456 1 -- 用户123456上线 |
场景:下单 30 分钟未支付自动关闭。
| 方案 | 优点 | 缺点 |
|---|---|---|
| 定时任务扫表 | 实现简单 | 数据量大时效率低,延迟高 |
| Redis 过期监听 | 简单 | 不可靠(不保证触发),不推荐 |
| Redis ZSet 轮询 | 精度高 | 需维护消费者 |
| RocketMQ 延迟消息 | 可靠、可扩展 | 延迟级别有限 |
| RabbitMQ TTL + DLX | 灵活 | 架构复杂 |
| 时间轮 (Time Wheel) | 高吞吐、内存高效 | 适合固定延迟场景 |
最佳回答:
| 方案 | 有序性 | 性能 | 问题 |
|---|---|---|---|
| UUID | 无序 | 高 | 太长(128bit),B+ 树索引性能差 |
| 数据库号段 | 趋势递增 | 高 | 批量取号,DB 宕机有号段浪费 |
| Snowflake | 趋势递增 | 高 | 依赖时钟,回拨会重复 |
| Redis INCR | 递增 | 高 | 持久化风险,单点问题 |
Snowflake 结构:1 位符号 + 41 位时间戳(69 年)+ 10 位机器 ID + 12 位序列号(4096/ms)。
容器化环境机器 ID 唯一:
生成策略:
重定向选择:
点击统计:302 重定向时解析 UA/IP/渠道 → 异步写入日志 → Flink 聚合 → ClickHouse 存储。
| 模式 | 读性能 | 写性能 | 适用场景 |
|---|---|---|---|
| 推 (Write-fanout) | 快 | 慢(写 N 个粉丝收件箱) | 普通用户 |
| 拉 (Read-fanout) | 慢(聚合 N 个关注人) | 快 | 大 V |
| 推拉结合 | 均衡 | 均衡 | 业界主流 |
推拉结合策略:
已读去重:用户维度维护 RoaringBitmap,推送前 if (!bitmap.contains(postId)) push()。
存储模型对比:
| 模型 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| 邻接表 | id, parent_id |
简单 | 查子树需递归,性能差 |
| 路径枚举 | id, path="1/2/5" |
前缀查询方便 | 路径长度受限 |
| 闭包表 | 单独表存所有祖先-后代 | 查询极快 | 写入量大 |
业界主流(两层结构):
parent_id 指向一级评论,reply_to_id 指向被回复的人。不做无限嵌套。防灌水:发言频率限制 → 敏感词过滤(AC 自动机)→ 举报+审核队列 → 新用户评论需审核(信任分体系)。
二倍均值法:amount = random(1, remain / remain_count * 2),数学上保证期望恒定。
高并发实现:
LPOP 原子弹出,天然串行化。核心链路:下单 → 锁库存 → 创建支付单 → 调第三方支付 → 异步回调 → 扣库存 → 发货。
关键设计点:
transaction_id 唯一索引,重复回调不重复处理。核心洞察:不同品类库存差异巨大,需要抽象出通用模型。
两个正交维度分类:
1 | 维度一:谁管库存? |
品类分类矩阵示例:
| 品类 | 管理类型 | 单元类型 | 扣减时机 |
|---|---|---|---|
| 电子券 | Self | Code | 下单 |
| 虚拟服务券 | Self | Quantity | 下单 |
| 酒店 | Supplier | Time | 支付 |
| 礼品卡(实时生成) | Supplier | Code | 支付 |
架构设计(策略模式):
1 | 业务层 (Order Service) |
核心优势:
Redis 存储结构:
1 | Key: inventory:code:pool:{itemID}:{skuID}:{batchID} |
出货流程(核心):
1 | 1. 检查库存空标志 → 命中则直接返回缺货 |
Lua 脚本(原子性保证):
1 | local result = redis.call('LRANGE', KEYS[1], 0, ARGV[1] - 1) |
关键设计:
Redis HASH 设计:
1 | Key: inventory:qty:stock:{itemID}:{skuID} |
预订 Lua 脚本(支持营销库存):
1 | -- 1. 获取普通库存和营销库存 |
亮点:动态字段设计,无需提前建表,营销活动 ID 直接作为 HASH field。
三种同步策略:
| 策略 | 适用场景 | 实时性 | 实现 |
|---|---|---|---|
| 实时查询 | 库存变化快(机票) | 高 | 每次请求调 API(30s 缓存) |
| 定时同步 | 变化中等(酒店) | 中 | 定时任务每 5 分钟拉取 |
| Webhook 推送 | 供应商主动推送 | 高 | 接收推送更新本地缓存 |
实时查询流程:
1 | func CheckStock() { |
预订时:调供应商预订接口 → 保存供应商订单号映射 → 更新本地 booking_stock。
双写策略:
| 操作 | Redis | MySQL | 一致性 |
|---|---|---|---|
| 预订 (Book) | 同步扣减(Lua) | Kafka 异步更新 | 最终一致 |
| 支付 (Sell) | 同步更新 | Kafka 异步更新 | 最终一致 |
| 营销锁定 (Lock) | 同步 | 同步(DB 事务) | 强一致 |
核心原则:
定时对账(每小时):
1 | redisStock := getRedisAvailable(itemID) |
降级方案:
1 | Redis 可用 |
注意:
问题:支付成功后调供应商 API 生成卡密,超时会导致用户等待。
解决方案(异步生成 + 重试补偿):
1 | 支付成功 |
卡密安全:
XXXX-XXXX-XXXX-1234)。差异:
| 维度 | 普通库存 | 时间维度库存 |
|---|---|---|
| 库存粒度 | SKU 级别 | SKU + 日期 |
| 存储 | 单条记录 | 每个日期一条记录 |
| 查询 | 按 item_id + sku_id | 按 item_id + sku_id + date |
| TTL | 永久 | Redis 缓存 7 天 |
Redis 设计:
1 | Key: inventory:time:stock:{itemID}:{skuID}:{date} |
挑战:
MGET + 并行查询。场景:运营配置秒杀活动,需从总库存中锁定 1000 件,活动结束释放。
Lua 脚本(营销锁定):
1 | local available = tonumber(redis.call('HGET', key, 'available') or 0) |
数据库同步:
1 | UPDATE inventory |
活动结束解锁:反向操作,营销库存 → 普通库存。
三步接入:
1 | // 1. 评估分类 |
亮点:配置驱动,零代码接入。
Q:为什么券码制库存不一次性加载全量到 Redis,而是按需补货?
Q:库存对账发现 Redis 比 MySQL 多 500 个,怎么办?
Q:多平台(Shopee、ShopeePay)如何独立统计库存?
booking_shopee、booking_shopeepay 字段。platform 参数路由到不同字段。booking_stock 和 spp_booking_stock。Q:库存扣减后支付失败,如何归还库存?
HINCRBY booking -1, HINCRBY available +1。| 方案 | 一致性 | 性能 | 侵入性 | 适用场景 |
|---|---|---|---|---|
| 2PC (XA) | 强一致 | 差(阻塞) | 低 | 单体拆分初期 |
| TCC | 最终一致 | 中 | 高(需写 Try/Confirm/Cancel) | 金融转账 |
| 本地消息表 | 最终一致 | 高 | 中 | 通用场景 |
| 事务消息 (RocketMQ) | 最终一致 | 高 | 低 | 电商下单 |
| Saga | 最终一致 | 高 | 中 | 长事务(跨多个服务) |
TCC 追问:Confirm/Cancel 失败怎么办?
| 方案 | 流程 | 优缺点 |
|---|---|---|
| Cache Aside(推荐) | 先更新 DB → 再删 Cache | 简单,极端并发下有短暂不一致 |
| 延迟双删 | 删 Cache → 更 DB → sleep → 再删 Cache | 减少脏读窗口,sleep 时间难定 |
| Canal 订阅 Binlog | 更 DB → Canal 监听 → 异步删/更新 Cache | 最终一致性好,架构复杂 |
追问:先删缓存再更新 DB 有什么问题?
场景:网络抖动重复提交、支付回调重复通知。
| 方案 | 实现 | 适用 |
|---|---|---|
| 数据库唯一索引 | INSERT IGNORE 或 UNIQUE KEY |
写操作去重 |
| Token 机制 | 请求前获取 Token,提交时 Redis Lua 原子校验+删除 | 表单防重复提交 |
| 状态机 | UPDATE SET status='PAID' WHERE id=? AND status='UNPAID' |
状态流转类 |
支付回调幂等:支付平台传唯一 transaction_id → INSERT IGNORE INTO payment_records (txn_id),已存在则跳过。
线程数设置:
N + 1(N = CPU 核数)。N × (1 + Wait/Compute) 或简化为 2N。量化估算:
核心接口 RT = 500ms,目标 1 万 QPS。
单线程 QPS = 1000/500 = 2。
单机需线程数 = 10000 / 2 = 5000 → 不现实。
→ 需 多台机器:如 10 台,每台承担 1000 QPS,每台 500 线程。
共享 vs 独享:
监控:暴露 activeCount, queueSize, completedTaskCount,队列 >80% 告警。
场景:接口串行调用 A(用户信息)、B(积分)、C(优惠券),总耗时 T = Ta + Tb + Tc。
优化:CompletableFuture (Java) / errgroup (Go) 并行调用,T = max(Ta, Tb, Tc)。
风险与应对:
orTimeout(500ms) 强制超时。| 维度 | Kafka | RocketMQ | RabbitMQ |
|---|---|---|---|
| 吞吐量 | 极高(百万级 TPS) | 高(十万级) | 中(万级) |
| 延迟 | ms 级 | ms 级 | us 级 |
| 事务消息 | 不支持 | 支持 | 不支持 |
| 延迟队列 | 不原生 | 支持 | TTL + DLX |
| 适用场景 | 日志、大数据 | 金融、电商 | 中小规模、复杂路由 |
为什么用 MQ?
消息不丢失(三环节保障):
SYNC_FLUSH) + 主从同步。消息重复:消费端做幂等(唯一索引/状态机)。
消息积压:先扩容消费者 → 排查消费阻塞原因 → 必要时跳过非关键消息。
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 缓存穿透 | 查不存在的数据 | Bloom Filter / 缓存空值 |
| 缓存击穿 | 热点 Key 过期 | 互斥锁(Mutex) / 逻辑过期 |
| 缓存雪崩 | 大量 Key 同时过期 | 随机过期时间 / 多级缓存 |
| Big Key | 阻塞主线程 | 拆分 / UNLINK 异步删除 |
Key 过期内存释放:
拆分策略:
Hash(UserID) 或 Range(Time) 分散数据行。核心难题:
索引高频考点:最左前缀、回表与覆盖索引、索引失效(函数/隐式转换/!=/LIKE '%xx')、深分页优化(WHERE id > last_id LIMIT 10)。
日增 1TB 场景设计:
查询优化:
wildcard,改用 ngram 分词器。keyword 类型。search_after 替代 from + size。问题:为什么只能重置密码,不能找回原密码?
回答:密码存储的是 bcrypt(password + salt) 的不可逆哈希值。即使数据库泄露,攻击者也无法还原明文。
| 攻击 | 防御 |
|---|---|
| XSS | 输出转义、CSP 头、HttpOnly Cookie |
| CSRF | CSRF Token、SameSite Cookie |
| SQL 注入 | 预编译(#{} 而非 ${}) |
| 重放攻击 | 签名 + 时间戳 + nonce + 设备指纹 |
一句话:非对称加密传密钥,对称加密传数据。
| 支柱 | 工具 | 核心 |
|---|---|---|
| Logging | Filebeat → Kafka → ES → Kibana | 结构化 JSON 日志,含 trace_id |
| Metrics | Prometheus + Grafana | 黄金信号:延迟、流量、错误率、饱和度 |
| Tracing | Jaeger / Zipkin | TraceID 串联全链路 |
止血第一:先回滚或切流量,再定位根因。
Decimal 类型(定点数)或转为整数(分)计算。Q:系统瓶颈在哪?怎么优化?
先定位(DB?Redis?MQ?外部接口?)→ 再给方案(索引/分库/缓存/异步/并行/批量化)。
Q:流量突增 10 倍怎么扛?
限流(挡住超量)→ 扩容(水平加机器)→ 缓存(减少穿透)→ 异步(削峰填谷)→ 降级(保核心)。
Q:线上故障排查流程?
止血(回滚/切流)→ 看监控 → 看日志 → 看调用链 → 定位根因 → 修复 → 复盘。
Q:分布式系统最难的是什么?
网络不可靠、时钟不一致、节点随时会挂。核心矛盾是 CAP 取舍:金融选 CP(强一致),互联网选 AP(最终一致)。
Q:方案有什么副作用?
面试加分项——主动说出 trade-off。例如:”虽然异步解耦了,但增加了链路追踪的复杂度和排查成本。”
Pipeline Pattern + Service Layer 是一种将复杂业务流程分解为可组合、可重用组件的设计模式组合。它将传统的面向过程代码转换为面向对象的、高度模块化的架构。
1 | // 职责:处理HTTP请求,参数验证,响应格式化 |
特点:
1 | type FlashSaleService interface { |
特点:
1 | type Pipeline interface { |
特点:
1 | type Processor interface { |
特点:
1 | type FlashSaleContext struct { |
数据流转过程:
1 | // 单元测试示例 |
1 | // 添加新功能只需要新增Processor |
1 | // Processor可以在不同的Pipeline中重用 |
1 | // 不同的排序策略 |
1 | // 每个Processor形成一个责任链 |
1 | // 基础Processor提供模板 |
1 | // 为Processor添加额外功能 |
1 | type ParallelPipeline struct { |
1 | type CacheProcessor struct { |
1 | type CircuitBreakerProcessor struct { |
1 | type MetricsCollector interface { |
1 | type TracingProcessor struct { |
Pipeline Pattern + Service Layer 组合架构通过将复杂的业务流程分解为独立的、可组合的组件,实现了:
这种架构特别适合处理电商、金融等复杂业务场景,能够显著提升代码质量和开发效率。
dp[i] = max(nums[i], dp[i-1] + nums[i])】max(最大子数组和, 总和 - 最小子数组和)】count(PrefixSum - K)】sum >= target 时收缩左边界】Left 跳跃式收缩】cnt 记录有效字符数,满足条件后尝试收缩 Left】L,多起点 0 ~ L-1 遍历】next 数组实现 $O(m+n)$ 匹配】dp[i] += dp[i-1] + dp[i-2]】tails[]】len[i] 和 cnt[i],注意相等长度时的累加】dp[i][j] 表示 text1[0:i] 和 text2[0:j] 的 LCS】dp[i][j] = min(插入, 删除, 替换) + 1】dp[i][j] 表示 s1[0..i] 和 s2[0..j] 能否交错组成 s3[0..i+j]】s[i]==t[j] 时可选匹配或不匹配,dp[i][j] = dp[i-1][j-1] + dp[i-1][j]】nums[i] 归位到 nums[i]-1】(r/3)*3 + c/3 映射九宫格】reverse 实现 $O(1)$ 空间位移】nums[j+1] != nums[j]+1 识别连续区间断点】count(PrefixSum - K)】[L, R],利用 l <= cur_right 动态扩展右边界】unordered_set 实现 $O(1)$ 查找,仅从序列起点 (x-1 不存在) 开始计数,确保 $O(n)$ 复杂度】unordered_set 实现 $O(n)$ 频率检测,最基础的去重思想】unordered_set】std::set::lower_bound 寻找满足范围条件的元素】unordered_set 记录历史值或使用“快慢指针”在 $O(1)$ 空间内检测无限循环】int[26] 数组实现 $O(n)$ 时间 $O(1)$ 空间的高性能频率校验】int[26] 计数,通过“先加后减”配合“负数早期退出”实现 $O(n)$ 校验】mapS[s[i]] == mapT[t[i]] 校验字符映射的一致性】stringstream 处理单词拆分】0~n 的索引与 nums 元素一起异或,缺失的索引会被剩下】val,将 nums[abs(val)-1] 变为负数;最后仍为正数的索引即为缺失值】x 放到下标 x-1 的位置;遍历找第一个 nums[i] != i+1 的位置】val,若 nums[abs(val)-1] 已经是负数,说明 val 重复出现】i -> nums[i],转化为“环形链表找入口”问题】x & -x (lowbit) 将数组分为两组,每组转化回 136 题】cnt 和已拼出的 string】.. 执行出栈,配合 stringstream 拆分单词】max == index 则可分块】P[i] - P[dq.front()] >= K 则更新结果并出队】pre, cur, next 三指针完成原地调向】a+c+b = b+c+a,消除长度差实现首遇】fast 走两步 slow 走一步,fast 到头时 slow 在中点】A->A'->B->B' 插入法,实现 $O(1)$ 空间拷贝随机指针】size 控制当前层边界】long long 防止累加溢出】L->left vs R->right 且 L->right vs R->left】max(left, right) + 1】x * 10 + node->val 传递路径状态,推荐“结果上传”的纯函数写法】max(0, gain) 过滤负贡献】isSameTree】next 指针作为“下一层”的驱动,实现 $O(1)$ 空间复杂度】root->val 与 p, q 的大小关系快速剪枝】'O' 开始标记,未被标记的内部 'O' 均需填充】[原节点 -> 新节点] 防止死循环】h*t)优化状态转移搜索】核心思想:高效处理区间操作的数据结构家族。从静态的前缀和到动态的线段树,根据问题的查询/更新需求选择合适的工具。
技术演进路径:前缀和(静态查询)→ 差分(批量更新)→ 树状数组(动态单点)→ 线段树(动态区间)
场景:静态数组的快速区间查询;预处理 $O(n)$,查询 $O(1)$
核心:sum[l, r] = prefix[r+1] - prefix[l],支持一维/二维/前缀积等变体
场景:多次区间更新 + 一次性查询;时间 $O(n + q)$,空间 $O(n)$
核心:diff[l] += val, diff[r+1] -= val,最后前缀和还原;是前缀和的”逆运算”
[l, r] 加值转化为差分数组两个端点操作】场景:单点更新 + 区间查询(前缀和);时间 $O(\log n)$,空间 $O(n)$
核心:利用lowbit(x) = x & -x实现树状结构,是动态版本的前缀和
场景:区间更新 + 区间查询(最大/最小/和);时间 $O(\log n)$,空间 $O(n)$ 或动态开点
核心:完全二叉树结构,支持懒标记(Lazy Propagation)批量更新,是最通用的区间数据结构
. 通配符的模糊匹配】. 通配符的模糊匹配】核心:关注两个“点”的博弈,利用有序性或特征缩减搜索空间。
场景:有序数组求和、反转、容积问题
sum > target 右移,sum < target 左移】i,对 j, k 进行双指针搜索,注意去重】left_max 和 right_max,短板决定当前格蓄水量】k 向两边贪心扩展,维护当前最小值】场景:原地修改、子序列匹配、区间归并
fast 找非零,slow 维护非零边界】s 走一步,t 一直走】核心:关注两个指针中间的“区域/区间”的性质维护。
逻辑:
Right扩张 -> 满足条件? -> 更新结果
Left 跳跃式收缩】逻辑:
Right扩张 -> 满足条件? ->Left收缩 (Update Min) -> 循环直到不满足
cnt 记录有效字符数,满足条件后尝试收缩 Left】sum >= target 时收缩左边界】逻辑:窗口大小固定为 K,
Right进Left出
L,多起点 0 ~ L-1 遍历】一维二分 (1D Binary Search)
[l, r] 或左闭右开 [l, r)】>= target 的位置】l++, r-- 恢复单调性,最坏 $O(n)$】mid 与 right 确定最小值所在半区】mid 与 mid+1 确定爬坡方向,在无序数组中实现 $O(\log n)$ 查找】二维矩阵二分 (2D Matrix Search)
二分答案 (Binary Search on Answer)
[0, x] 范围内寻找 k^2 <= x 的最大整数】进阶划分与技巧 (Advanced Partitioning & Tactics)
核心思想:从海量数据中高效选取第 K 个元素或维护动态极值,是算法面试的高频考点。
场景:无序数组中找第 K 大/小元素
场景:动态数据流中维护中位数或极值
场景:有序矩阵、多数组归并中的第 K 小问题
场景:从多个有序源中生成第 K 个元素
p3, p5, p7 三个指针,每次选择 min(dp[p3]*3, dp[p5]*5, dp[p7]*7) 作为下一个魔法数,相等时多指针同时前移去重;最优 $O(k)$ 时间复杂度,类似 264 题丑数 II】核心思想:排序是算法的基础工具,通过消除无序性简化问题,常与双指针、贪心、二分等技巧结合。
场景:手撕排序算法、理解排序原理
场景:需要特殊排序规则的问题
a+b > b+a 确定全局最优序】场景:多个有序序列的合并
场景:排序后利用有序性进行双指针搜索
i,对 j, k 进行双指针搜索,注意去重】sum > target 右移,sum < target 左移】场景:排序后贪心选择、区间处理
场景:排序后枚举关键点或分界线
场景:排序后利用单调性进行二分或 LIS
tails[]】场景:判断数组能否分块排序
max(arr[0...i]) == i 时可分块】场景:利用有序性质的特殊问题
场景:有向无环图的线性排序
a+b > b+a 确定全局最优序】max(arr[0...i]) == i 时可分块】d[i]】核心逻辑:
- **分解 (Divide)**:将原问题拆分为规模较小、相互独立的子问题(如左右子树、数组半区)。
- **解决 (Conquer)**:递归解决子问题,直到触及边界。
- **合并 (Combine)**:将子问题的解合并为原问题的解(如归并排序的
merge或 LCA 的状态上传)。
L->left vs R->right 且 L->right vs R->left】核心思想:不枚举所有可能的子集/子数组,而是枚举每个元素,计算该元素对最终答案的“贡献”次数或值。
__gcd(a, b)】(a / b) % c = (a * b^(c-2)) % c (当 c 为质数时,费马小定理)】(a * 10 + 1) % k 的迭代处理】n & (n-1) 消除最低位 1;n & -n 获取最低位 1 (lowbit)】__builtin_popcount 或 n & (n-1) 迭代】ans = (ans << 1) | (n & 1) 或 分治法】n > 0 && (n & (n-1)) == 0】left 和 right 的二进制公共前缀】ones 和 twos 记录位状态,或统计位 1 个数模 3】核心区别:
- DFS(深度优先搜索):遍历所有状态,通常用于连通性判断、路径查找、拓扑排序
- 回溯(Backtracking):在 DFS 基础上增加”撤销选择”,用于求解组合优化问题(排列、组合、子集)
场景:图/树的遍历、连通分量统计、环检测
[原节点 -> 新节点] 防止死循环】场景:路径和、路径记录、子树信息聚合
path 数组,到达叶子节点且路径和等于目标值时保存当前路径的副本,回溯时弹出节点】prefixSum - targetSum 统计满足条件的路径数,类似 560 题】⌈人数/seats⌉ 辆车的油耗;也可用拓扑排序 BFS 从叶子向根逐层合并(见 BFS 章节)】场景:求所有方案、排列组合、子集生成
- 组合靠
start:不回头看,一路向右。- 排列靠
used:全员参与,位掩码标记。- 重复靠排序:前人未用,后人莫入(
!used[i-1])。
start:不回头看,一路向右】i 而非 i+1 实现元素可重复选取】used:全员参与,位掩码标记】!used[i-1])】场景:棋盘问题、数独、复杂约束条件
- 棋盘靠标记:列号、和、差,三位一体定乾坤。
- 括号看余额:左括号不超标,右括号不透支。
- 矩阵靠沉岛:先占位再递归,事后记得还原。
cols[c]、diag1[r+c]、diag2[r-c+n] 三个数组实现 O(1) 冲突检测】场景:重叠子问题、状态空间搜索、自顶向下 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)$ | 时空权衡:有时为了降低时间复杂度(如利用前缀和优化转移),可能会增加空间复杂度。 |
核心逻辑:识别“依赖范围”是掌握 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)$ |
| 掌握重点 | 空间优化 (滚动数组/变量替代) | 时间优化 (数据结构/二分/单调性) |
| 一眼辨别 | “我只能从前一个或前几个状态跳过来” | “我可以从前面任意一个符合条件的点跳过来” |
核心特点:只需要知道“最近”的几步,就能决定下一步。如果发现 dp[i] 只依赖 dp[i-1],通常可以压缩至 $O(1)$ 空间。
核心特点:需要纵观所有历史状态才能决定下一步。面试常考“如何将 $O(n^2)$ 降到 $O(n \log n)$ 或 $O(n)$”。
最基础的递推,dp[i] 只依赖于前面几个状态
dp[i] += dp[i-1] + dp[i-2]】dp[i][j] = dp[i-1][j] + dp[i][j-1] 或组合数】grid[i][j] == 1 则 dp[i][j] = 0】grid[i][j] += min(左, 上)】dp[j] = min(dp[j], dp[j+1]) + val,空间优化至 O(N)】dp[i][j] = min(左, 上, 左上) + 1】dp[i][j] 既是最大边长,也是以该点为右下角的正方形个数】min_val】dp[i] = max(nums[i], dp[i-1] + nums[i])】max(最大子数组和, 总和 - 最小子数组和)】max_prod 和 min_prod 以应对负数】dp[i] += dp[i-1] + dp[i-2]】dp[i] = max(dp[i-1], dp[i-2] + nums[i])】[0, n-2] 和 [1, n-1] 两次线性 DP】核心在于定义“持有”、“冷冻”、“卖出”等有限状态,画状态转移图
min_price,计算 price - min_price】max(0, p[i]-p[i-1])】buy1, sell1, buy2, sell2 四个状态】buy[k] 和 sell[k] 数组,若 k > n/2 退化为无限次】sell 转移时减去 fee】dp[i] = max(dp[i-1], dp[i-2] + nums[i])】[0, n-2] 和 [1, n-1] 两次线性 DP】{偷, 不偷} 两个状态】处理字符串或数组子序列问题,核心是 LCS/LIS 模型
tails 数组实现 $O(n \log n)$】len[i] 和 cnt[i],注意相等长度时的累加】dp[i][j] = s1[i]==s2[j] ? dp[i-1][j-1]+1 : max(左, 上);注意:DP 数组大小为 (M+1)*(N+1) 处理空串】dp[i][j] = min(插入, 删除, 替换) + 1;注意:DP 数组大小为 (M+1)*(N+1) 处理空串】dp[i][j] 表示 s1[0..i] 和 s2[0..j] 能否交错组成 s3[0..i+j];注意:DP 数组大小为 (M+1)*(N+1) 处理空串】m + n - 2 * LCS;注意:DP 数组大小为 (M+1)*(N+1) 处理空串】s[i]==t[j] 时可选匹配或不匹配,dp[i][j] = dp[i-1][j-1] + dp[i-1][j];注意:DP 数组大小为 (M+1)*(N+1) 处理空串】dp[i][j] 匹配 s[0..i] 和 p[0..j];遇到 * 时可匹配 0 次或多次前字符;*注意:DP 数组大小为 (M+1)(N+1)**】* 可匹配任意序列;dp[i][j] = dp[i][j-1] || dp[i-1][j]】dp[i][j] 表示 s[i...j] 的最长回文子序列长度】将数组/字符串切分为 k 段,求最优解
组合优化问题,关注容量与价值
从小区间合并到大区间,枚举分割点 k
自底向上汇总信息,或换根 DP
数据范围 n < 20,用二进制表示集合
mask 记录数字使用状态,博弈论必胜态判断】dp[mask] 表示选取状态为 mask 时的方案数,或 DFS 剪枝】(node, mask),求覆盖所有节点的最短路】dp[mask] 记录当前累加和,或 DFS 每次凑齐一个桶】dp[mask][last_val] 记录状态与上一个元素,满足整除关系转移】按位填数,通常配合记忆化搜索
| 函数名 | 检查内容 | 说明 |
|---|---|---|
isdigit(c) |
是否为数字 (0-9) | 数字字符 |
isalpha(c) |
是否为字母 (a-z, A-Z) | 纯字符/字母 |
isalnum(c) |
是否为字母或数字 | 字母数字混合 |
tolower(c) |
转换为小写 | 字符转换 |
toupper(c) |
转换为大写 | 字符转换 |
首先回答另一个问题,怎样的系统算是稳定的?
Google SRE中(SRE三部曲[1])有一个层级模型来描述系统可靠性基础和高层次需求(Dickerson’s Hierarchy of Service Reliability),如下图:
该模型由Google SRE工程师Mikey Dickerson在2013年提出,将系统稳定性需求按照基础程度进行了不同层次的体系化区分,形成稳定性标准金字塔模型:
从金字塔模型我们可以看到构建维护一个高可用服务所需要做到的几方面工作:
站在监控的角度看,我们的系统从上到下一般可以分为三层:业务(Biz)、应用(Application)、系统(System)。系统层为最下层基础,表示操作系统相关状态;应用层为JVM层,涵盖主应用进程与中间件运行状态;业务层为最上层,为业务视角下服务对外运行状态。因此进行大促稳定性监控梳理时,可以先脱离现有监控,先从核心、资损链路开始,按照业务、应用(中间件、JVM、DB)、系统三个层次梳理需要哪些监控,再从根据这些索引找到对应的监控告警,如果不存在,则相应补上;如果存在则检查阈值、时间、告警人是否合理。
监控系统一般有四项黄金指标:延时(Latency), 错误(Error),流量(Traffic), 饱和度(Situation),各层的关键性监控同样也可以按照这四项指标来进行归类,具体如下:
是不是每项监控都需要告警?答案当然是否定的。建议优先设置Biz层告警,因为Biz层我们对外服务最直观业务表现,最贴切用户感受。Application&System层指标主要用于监控,部分关键&高风险指标可设置告警,用于问题排查定位以及故障提前发现。对于一项告警,我们一般需要关注级别、阈值、通知人等几个点。
完成该项梳理工作后,我们应该产出以下数据:
高可用架构是系统稳定性的基石。一个好的架构设计能够从根本上减少故障发生的概率,并在故障发生时将影响范围降到最低。
冗余是实现高可用的基础手段,通过部署多个相同的组件来消除单点故障。
| 冗余层级 | 实现方式 | 典型场景 |
|---|---|---|
| 应用层冗余 | 多实例部署 + 负载均衡 | Web 服务、API 网关 |
| 数据层冗余 | 主从复制、多副本 | MySQL 主从、Redis Cluster |
| 机房冗余 | 同城双活、异地多活 | 核心交易系统 |
| 网络冗余 | 多运营商、多链路 | CDN、DNS |
冗余度计算:
假设单节点可用性为 99%,双节点冗余后可用性 = 1 - (1-0.99)² = 99.99%
1 | 紧耦合架构 解耦架构 |
解耦手段:
| 隔离维度 | 实现方式 | 目的 |
|---|---|---|
| 线程池隔离 | 核心业务独立线程池 | 防止非核心业务拖垮核心链路 |
| 进程隔离 | 独立部署、容器化 | 资源隔离,故障不传导 |
| 集群隔离 | 按业务/用户分集群 | 大客户/VIP 用户独立资源 |
| 机房隔离 | 单元化部署 | 机房级故障隔离 |
无状态服务是水平扩展和快速故障恢复的前提。
1 | // 有状态设计(不推荐) |
状态外置方案:
1 | MySQL 主从架构: |
1 | Redis Cluster 架构(16384 槽位): |
1 | ┌─────────────┐ |
关键设计点:
异地多活是最复杂也是可用性最高的架构模式,核心挑战是数据一致性。
1 | 北京 上海 广州 |
单元化设计要点:
1 | // 令牌桶限流器实现 |
熔断器三状态:关闭(正常)→ 打开(熔断)→ 半开(探测)
1 | 错误率 > 阈值 |
| 降级类型 | 触发条件 | 降级策略 |
|---|---|---|
| 自动降级 | 错误率/RT 超阈值 | 返回缓存数据或默认值 |
| 手动降级 | 大促前/故障时 | 关闭非核心功能 |
| 读降级 | 主库压力大 | 读走从库或缓存 |
| 写降级 | 下游不可用 | 写入 MQ 异步重试 |
1 | // 超时控制最佳实践 |
超时设置原则:
系统链路梳理是所有保障工作的基础,如同对整体应用系统进行一次全面体检,从流量入口开始,按照链路轨迹,逐级分层节点,得到系统全局画像与核心保障点。
一个系统往往存在十几个甚至更多流量入口,包含HTTP、RPC、消息等都多种来源。如果无法覆盖所有所有链路,可以从以下三类入口开始进行梳理:
对于复杂场景可以做节点分层判断
流量入口就如同线团中的线头,挑出线头后就可按照流量轨迹对链路上的节点(HSF\DB\Tair\HBase等一切外部依赖)按照依赖程度、可用性、可靠性进行初级分层区分。
完成该项梳理工作后,我们应该产出以下数据:对应业务域所有核心链路分析,技术&业务强依赖、核心上游、下游系统、资损风险应明确标注。
不同于高可用系统建设体系,大促稳定性保障体系与面向特定业务活动的针对性保障建设,因此,业务策略与数据是我们进行保障前不可或缺的数据。
一般大促业务数据可分为两类,全局业务形态评估以及应急策略&玩法。
该类数据从可以帮助我们进行精准流量评估、峰值预测、大促人力排班等等,一般包含下面几类:
容量规划的本质是追求计算风险最小化和计算成本最小化之间的平衡,只追求任意其一都不是合理的。为了达到这两者的最佳平衡点,需尽量精准计算系统峰值负载流量,再将流量根据单点资源负载上限换算成相应容量,得到最终容量规划模型。
对于一次大促,系统峰值入口流量一般由常规业务流量与非常规增量(比如容灾预案&业务营销策略变化带来的流量模型配比变化)叠加拟合而成。
节点容量是指一个节点在运行过程中,能够同时处理的最大请求数。它反映了系统的瞬时负载能力。
1)Little Law衍生法则
不同类型资源节点(应用容器、Tair、DB、HBASE等)流量-容量转化比各不相同,但都服从Little Law衍生法则,即:
节点容量=节点吞吐率×平均响应时间
2)N + X 冗余原则
在满足目标流量所需要的最小容量基础上,冗余保留X单位冗余能力
X与目标成本与资源节点故障概率成正相关,不可用概率越高,X越高
对于一般应用容器集群,可考虑X = 0.2N
要想在大促高并发流量场景下快速对线上紧急事故进行响应处理,仅仅依赖值班同学临场发挥是远远不够的。争分夺秒的情况下,无法给处理人员留有充足的策略思考空间,而错误的处理决策,往往会导致更为失控严重的业务&系统影响。因此,要想在大促现场快速而正确的响应问题,值班同学需要做的是选择题(Which),而不是陈述题(What)。而选项的构成,便是我们的业务&系统预案。从执行时机与解决问题属性来划分,预案可分为技术应急预案、技术前置预案、业务应急预案、业务前置预案等四大类。结合之前的链路梳理和业务评估结果,我们可以快速分析出链路中需要的预案,遵循以下原则:
完成该项梳理工作后,我们应该产出以下数据:
阶段性产出-全链路作战地图
进行完上述几项保障工作,我们基本可得到全局链路作战地图,包含链路分支流量模型、强弱依赖节点、资损评估、对应预案&处理策略等信息。大促期间可凭借该地图快速从全局视角查看应急事件相关影响,同时也可根据地图反向评估预案、容量等梳理是否完善合理。
作战手册是整个大促保障的行动依据,贯穿于整个大促生命周期,可从事前、事中、事后三个阶段展开考虑。整体梳理应本着精准化、精细化的原则,理想状态下,即便是对业务、系统不熟悉的轮班同学,凭借手册也能快速响应处理线上问题。
事前
1)前置检查事项清单
事中
实战沙盘演练是应急响应方面的最后一项保障工作,以历史真实故障CASE作为应急场景输入,模拟大促期间紧急状况,旨在考验值班同学们对应急问题处理的响应情况。
一般来说,一个线上问题从发现到解决,中间需要经历定位&排查&诊断&修复等过程,总体遵循以下几点原则:
应对单库或某单元依赖因为自身原因(宿主机或网络),造成局部流量成功率下跌下跌。
Google SRE中,对于紧急事故管理有以下几点要素:
资损(资金损失)是互联网业务中最严重的故障类型之一,尤其在金融、电商、支付等领域。资损防控体系需要覆盖事前预防、事中发现、事后止血的全生命周期。
| 资损类型 | 场景举例 | 影响 |
|---|---|---|
| 多扣 | 重复扣款、金额计算错误 | 用户资金损失,客诉赔付 |
| 少扣 | 优惠券超发、折扣计算错误 | 公司资金损失 |
| 错付 | 付款到错误账户 | 资金追回困难 |
| 漏扣 | 订单状态异常导致未收款 | 应收账款损失 |
| 数据不一致 | 账务与实际资金不符 | 财务风险 |
通过链路梳理识别所有资损风险点:
1 | 订单链路资损风险点: |
| 检查项 | 防控措施 | 责任人 |
|---|---|---|
| 接口幂等 | 唯一索引 + 状态机 | 开发 |
| 金额计算 | Decimal 类型,禁用浮点 | 开发 |
| 参数校验 | 金额上下限、业务规则校验 | 开发 |
| 权限控制 | 敏感操作二次确认 | 产品 |
| 配置变更 | 双人复核、灰度发布 | 运维 |
| 代码审查 | 资损相关代码强制 Review | Tech Lead |
1 | // 幂等性设计 - 数据库唯一索引 |
1 | 实时对账架构: |
1 | // 对账规则定义 |
| 告警级别 | 触发条件 | 处理方式 |
|---|---|---|
| P0 紧急 | 金额差异 > 10万 或 差异笔数 > 100 | 立即电话 + 自动熔断 |
| P1 严重 | 金额差异 > 1万 或 差异笔数 > 10 | 钉钉 + 短信 |
| P2 一般 | 金额差异 > 1000 或 差异笔数 > 1 | 钉钉通知 |
| P3 提醒 | 存在差异记录 | 日报汇总 |
1 | 资损事件处理流程: |
| 场景 | 修复策略 | 注意事项 |
|---|---|---|
| 多扣用户 | 原路退款 + 补偿 | 需财务审批 |
| 少扣用户 | 评估是否追缴 | 金额较小可放弃 |
| 优惠券超发 | 标记失效 + 用户通知 | 已使用的认亏 |
| 数据不一致 | 以权威数据源为准修复 | 保留修复日志 |
| 指标 | 计算方式 | 目标值 |
|---|---|---|
| 资损发现率 | 发现资损笔数 / 实际资损笔数 | > 99% |
| 发现时效 | 从发生到发现的平均时间 | < 5 分钟 |
| 止血时效 | 从发现到止血的平均时间 | < 10 分钟 |
| 修复率 | 已修复金额 / 资损总金额 | > 95% |
| 资损率 | 资损金额 / 交易总金额 | < 0.001% |
风控体系是保障业务安全的重要防线,主要应对欺诈、薅羊毛、恶意攻击等风险场景。一个完善的风控体系需要覆盖事前、事中、事后全链路。
| 风险类型 | 典型场景 | 影响 |
|---|---|---|
| 账户风险 | 盗号、撞库、虚假注册 | 用户资产损失、平台信誉受损 |
| 交易风险 | 刷单、套现、恶意退款 | 资金损失、数据失真 |
| 营销风险 | 薅羊毛、优惠券滥用、黄牛 | 营销成本浪费 |
| 内容风险 | 垃圾信息、违规内容 | 合规风险、用户体验差 |
| 信用风险 | 恶意欠款、信用违约 | 坏账损失 |
1 | ┌─────────────────┐ |
规则引擎是风控系统的核心组件,支持灵活配置和动态更新。
1 | // 规则定义示例 |
| 特征类型 | 示例 | 计算方式 |
|---|---|---|
| 实时特征 | 用户当前IP、设备指纹 | 请求参数直接获取 |
| 滑动窗口特征 | 5分钟内登录次数 | Redis ZSET 滑动窗口 |
| 累计特征 | 历史消费总额、注册天数 | 用户画像服务 |
| 关联特征 | 同设备关联账号数 | 图数据库查询 |
| 模型特征 | 欺诈概率分数 | ML 模型实时推理 |
1 | // 滑动窗口特征计算 |
| 模型类型 | 适用场景 | 特点 |
|---|---|---|
| 规则模型 | 明确特征的欺诈行为 | 可解释性强,快速上线 |
| 有监督模型 | 有标签数据的场景 | XGBoost、LightGBM |
| 无监督模型 | 异常检测、新型欺诈 | Isolation Forest |
| 图模型 | 团伙欺诈、关联分析 | GNN、社区发现 |
1 | 用户请求 → 数据采集 → 特征计算 → 规则引擎 → 模型推理 → 决策聚合 → 返回结果 |
决策聚合策略:
| 处置动作 | 适用场景 | 用户体验 |
|---|---|---|
| 直接通过 | 低风险用户 | 无感知 |
| 人工审核 | 中风险,需人工判断 | 延迟处理 |
| 验证码挑战 | 疑似机器行为 | 轻度打扰 |
| 短信验证 | 账户安全验证 | 中度打扰 |
| 直接拒绝 | 高风险/黑名单 | 阻断 |
| 静默观察 | 采集数据不阻断 | 无感知 |
| 指标 | 定义 | 目标 |
|---|---|---|
| 拦截率 | 拦截的风险事件 / 实际风险事件 | > 95% |
| 误杀率 | 误拦正常用户 / 正常用户总数 | < 0.1% |
| 响应时间 | 风控请求 RT P99 | < 50ms |
| 覆盖率 | 接入风控的业务场景比例 | 100% |
1 | 数据驱动的风控策略迭代闭环: |
性能优化是稳定性建设的重要组成部分。高性能不仅带来更好的用户体验,也能在流量高峰时提供更大的容量 Buffer,降低系统压力。
1 | 性能问题定位流程: |
| 优化点 | 方案 | 效果 |
|---|---|---|
| 减少 RTT | CDN 加速、边缘计算 | 降低用户感知延迟 |
| 连接复用 | HTTP/2、连接池 | 减少建连开销 |
| 数据压缩 | Gzip、Brotli | 减少传输数据量 |
| 协议优化 | HTTP/3(QUIC) | 减少队头阻塞 |
a) 缓存优化
1 | 缓存层级: |
1 | // 多级缓存读取示例 |
b) 并发优化
1 | // 串行调用 → 并行调用 |
c) 批量处理
1 | // 单条查询 → 批量查询 |
a) SQL 优化
| 问题 | 优化方案 |
|---|---|
| 无索引/索引失效 | 添加合适索引,避免索引失效场景 |
| **SELECT *** | 只查需要的字段 |
| 大事务 | 拆分小事务,减少锁持有时间 |
| 深分页 | 游标分页(WHERE id > last_id LIMIT 10) |
| N+1 查询 | JOIN 或批量查询 |
b) 索引优化原则
1 | -- 联合索引最左前缀 |
c) 读写分离
1 | ┌─────────────┐ |
| 优化点 | 方案 |
|---|---|
| 堆内存 | 根据业务特点设置合理大小 |
| GC 算法 | G1/ZGC 替代 CMS |
| 对象池 | 复用大对象,减少 GC 压力 |
| 逃逸分析 | 栈上分配,减少堆分配 |
1 | // 1. sync.Pool 复用对象 |
| 类型 | 目的 | 场景 |
|---|---|---|
| 基准测试 | 获取单接口性能基线 | 新接口上线前 |
| 负载测试 | 验证目标 QPS 下表现 | 常规压测 |
| 压力测试 | 找到系统极限 | 容量评估 |
| 稳定性测试 | 长时间运行是否稳定 | 内存泄漏排查 |
| 全链路压测 | 真实场景模拟 | 大促前 |
| 指标 | 含义 | 目标参考 |
|---|---|---|
| QPS/TPS | 每秒请求/事务数 | 根据业务目标 |
| RT(P50/P95/P99) | 响应时间分位值 | P99 < 500ms |
| 错误率 | 失败请求占比 | < 0.1% |
| CPU 使用率 | 计算资源占用 | < 70%(留 Buffer) |
| 内存使用率 | 内存资源占用 | < 80% |
| GC 频率/耗时 | 垃圾回收影响 | Full GC < 1次/天 |
1 | 问题:商品详情接口 P99 RT = 500ms |
| 概念 | 全称 | 定义 |
|---|---|---|
| SLI | Service Level Indicator | 服务质量指标,可量化的度量值 |
| SLO | Service Level Objective | 服务质量目标,SLI 的目标值 |
| SLA | Service Level Agreement | 服务等级协议,对外承诺 |
1 | 关系:SLI(指标)→ SLO(目标)→ SLA(协议) |
| 指标类型 | SLI 定义 | SLO 示例 |
|---|---|---|
| 可用性 | 成功请求数 / 总请求数 | > 99.9% |
| 延迟 | 请求耗时分位值 | P99 < 200ms |
| 吞吐量 | 单位时间处理请求数 | > 10000 QPS |
| 正确性 | 返回正确结果的比例 | > 99.99% |
| 新鲜度 | 数据更新延迟 | < 5 分钟 |
Error Budget 是 SRE 的核心概念,用于平衡可靠性与迭代速度。
1 | Error Budget = 1 - SLO |
Error Budget 策略:
混沌工程是一种通过主动注入故障来验证系统韧性的实践方法。
| 故障类型 | 注入方式 | 验证目标 |
|---|---|---|
| 进程故障 | Kill 进程、OOM | 服务自愈、故障切换 |
| 网络故障 | 延迟、丢包、断网 | 超时处理、重试机制 |
| 依赖故障 | Mock 下游返回错误 | 降级策略、熔断机制 |
| 资源耗尽 | CPU 打满、磁盘填满 | 限流、告警 |
| 时钟偏移 | NTP 故障模拟 | 时间敏感逻辑 |
1 | ┌─────────────────────────────────────────────────────┐ |
| 工具 | 适用场景 | 特点 |
|---|---|---|
| ChaosBlade | 多平台通用 | 阿里开源,支持丰富故障场景 |
| Chaos Monkey | AWS/云环境 | Netflix 开源,随机终止实例 |
| LitmusChaos | Kubernetes | CNCF 项目,云原生 |
| Gremlin | 企业级 | 商业产品,功能全面 |
变更是系统故障的主要来源。Google SRE 统计显示,70% 的故障与变更相关。
| 变更类型 | 风险等级 | 审批要求 |
|---|---|---|
| 代码发布 | 高 | CR + 灰度 |
| 配置变更 | 中-高 | 双人审批 |
| DB Schema 变更 | 高 | DBA 审批 |
| 基础设施变更 | 高 | 架构组审批 |
| 依赖升级 | 中 | 兼容性测试 |
1 | 灰度发布流程: |
On-Call 是 SRE/运维团队的核心职责,负责生产环境的故障响应。
| 维度 | 最佳实践 |
|---|---|
| 轮值周期 | 一周为单位,避免过长导致疲劳 |
| 人员配置 | 主备双人制,避免单点 |
| 交接 | 详细的 Handoff 文档 |
| 补偿 | 调休或补贴 |
1 | 告警触发 |
书籍:
文章:
工具:
E-commerce whole process of system
| 阶段 | 主要特征/能力 | 技术架构/数据模型 | 适用场景/目标 | 实现方式简单说明 |
|---|---|---|---|---|
| 初始阶段 | - 商品信息简单,字段少 - SKU/SPU未严格区分 - 价格库存直接在商品表 - 仅支持基本的增删改查 |
单表/简单表结构 | 小型电商、业务初期,SKU数量少 | 单体应用,单表存储,简单业务逻辑,直接数据库操作 |
| 成长阶段 | - 引入SPU/SKU模型 - 属性、类目、品牌等实体独立 - 支持多规格商品 - 价格库存可拆分为独立表 |
关系型数据库,ER模型优化 | SKU多样化,品类扩展,业务快速增长 | 关系型数据库,ER模型优化,多表存储,业务逻辑复杂 |
| 成熟阶段 | - 商品中台化,支持多业务线/多渠道 - 属性体系灵活可扩展 - 多级类目、标签、图片、描述等丰富 - 商品快照、操作日志、版本控制 |
中台架构,微服务/多表/NoSQL | 大型平台,业务复杂,需支撑多业务场景 | 分布式服务,插件化/配置化流程,状态机驱动,异步消息,灵活数据模型 |
| 未来演进 | - 多语言多币种支持 - 商品内容多媒体化(视频、3D等) - AI智能标签/推荐 - 商品数据实时分析与洞察 |
分布式/云原生/大数据平台 | 国际化、智能化、数据驱动的电商生态 | 云原生架构,AI/大数据分析,自动化运维,弹性伸缩,智能路由与风控 |
方案一:同时创建多个SKU,并同步生成关联的SPU。整体方案是直接创建SKU,并维护多个不同的属性;该方案适用于大多数C2C综合电商平台(例如,阿里巴巴就是采用这种方式创建商品)。
方案二:先创建SPU,再根据SPU创建SKU。整体方案是由平台的主数据团队负责维护SPU,商家(包括自营和POP)根据SPU维护SKU。在创建SKU时,首先选择SPU(SPU中的基本属性由数据团队维护),然后基于SPU维护销售属性和物流属性,最后生成SKU;该方案适用于高度专业化的垂直B2B行业,如汽车、医药等。
这两种方案的原因是:垂直B2B平台上的业务(传统行业、年长的商家)操作能力有限,维护产品属性的错误率远高于C2C平台,同时平台对产品结构控制的要求较高。为了避免同一产品被不同商家维护成多个不同的属性(例如,汽车轮胎的胎面宽度、尺寸等属性),平台通常选择专门的数据团队来维护产品的基本属性,即维护SPU。
此外,B2B垂直电商的品类较少,SKU数量相对较小,品类标准化程度高,平台统一维护的可行性较高。
对于拥有成千上万品类的综合电商平台,依靠平台数据团队的统一维护是不现实的,或者像服装这样非标准化的品类对商品结构化管理的要求较低。因此,综合平台(阿里巴巴和亚马逊)的设计方向与垂直平台有所不同。
实际上,即使对于综合平台,不同的品类也会有不同的设计方法。一些品类具有垂直深度,因此也采用平台维护SPU和商家创建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
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"
}
]
}
]
}
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": "全国通用"
}
]
}
]
}
在这种方案中,SKU(Stock Keeping Unit) 表包含商品的所有信息,包括价格和库存数量。每个 SKU 记录一个独立的商品实例,它有唯一的标识符,直接关联价格和库存。
1 | CREATE TABLE sku_tab ( |
优点:
缺点:
适用场景:
1 |
|
优点:
缺点:
适用场景:
1 | CREATE TABLE `snapshot_tab` ( |
1 | CREATE TABLE user_operation_logs ( |
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"
}
实物订单
典型场景:电商平台购物(如买衣服、家电)
核心特征:
需要物流配送,涉及收货地址、运费、物流跟踪
需要库存校验与扣减
售后流程(退货、换货、退款)复杂
订单状态多(待发货、已发货、已收货等)
虚拟订单
典型场景:会员卡、电子券、游戏点卡、电影票等
核心特征:
无物流配送,不需要收货地址和运费
通常无需库存(或库存为虚拟库存)
订单完成后直接发放虚拟物品或凭证
售后流程简单或无售后
预售订单
典型场景:新品预售、定金膨胀、众筹等
核心特征:
订单分为定金和尾款两阶段
需校验定金支付、尾款支付的时效
可能涉及定金不退、尾款未付订单自动关闭等规则
发货时间通常在尾款支付后
O2O订单,外卖订单
典型场景:酒店预订
核心特征:
需选择入住/离店日期、房型、入住人信息
需对接第三方酒店系统实时查房、锁房
取消、变更政策复杂,可能涉及违约金
无物流,但有电子凭证或入住确认
| 阶段 | 主要特征/能力 | 技术架构/数据模型 | 适用场景/目标 | 实现方式简单说明 |
|---|---|---|---|---|
| 初始阶段 | - 实现订单基本流转(下单、支付、发货、收货、取消) - 单一订单类型(实物订单) - 订单与商品、用户简单关联 |
单体应用/单表或少量表结构 | 业务初期,订单量小,流程简单,SKU/商家数量有限 | 单体应用,单表存储,简单业务逻辑,直接数据库操作 |
| 成长阶段(订单中心) | - 支持订单拆单、合单(如多仓发货、合并支付) - 支持多品类订单(如实物+虚拟) - 订单中心化,订单与支付、配送、售后等子系统解耦 - 订单与商品快照、操作日志关联 |
微服务/多表/订单中心架构 | 平台型电商,业务扩展,需支持多商家、多类型订单,订单量大幅增长 | 订单中心服务,微服务拆分,多表关联,服务间接口调用,快照与日志表设计 |
| 成熟期(平台化) | - 支持多样化订单类型(预售、虚拟、O2O、定制、JIT等) - 订单流程可配置/插件化/工作流引擎/状态机框架/规则引擎等 - 订单状态机、履约、支付、退款等子流程解耦 - 支持复杂的促销、分账、履约模式 |
分布式/服务化/灵活数据模型 | 大型/综合电商,业务复杂,需快速适应新业务模式和高并发场景 | 分布式服务,插件化/配置化流程,状态机驱动,异步消息,灵活数据模型 |
| 未来智能化 | - 订单智能路由与分配(如智能分仓、智能客服) - 实时风控与反欺诈 - 订单数据实时分析与洞察 - 高可用、弹性伸缩、自动化运维 |
云原生/大数据/AI驱动架构 | 超大规模平台,国际化、智能化、数据驱动,需极致稳定与创新能力 | 云原生架构,AI/大数据分析,自动化运维,弹性伸缩,智能路由与风控 |
1 | # 时间戳 + 机器id + uid % 1000 + 自增序号 |
1 | -- 商品系统维护的快照表 |
缺点
1 | CREATE TABLE order_item ( |
设计思路:
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
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 | P0: PAYMENT_NOT_STARTED - 未开始 |
常见的异常:
营销部分:
支付部分:
1 | F0: FULFILLMENT_NOT_STARTED - 未开始 |
1 | UPDATE orders |
1 | // 使用支付单号作为幂等键 |
1 | @Transactional |
| 策略 | 优点 | 缺点 |
|---|---|---|
| 1. 直接读取主库 | - 一致性: 始终获取最新的数据。 | - 性能: 增加主库的负载,可能导致性能瓶颈。 |
| - 简单性: 实现简单直接,因为它直接查询可信的源。 | - 可扩展性: 主库可能成为瓶颈,限制系统在高读流量下有效扩展的能力。 | |
| 2. 使用VersionCache与从库 | - 性能: 分散读取负载到从库,减少主库的压力。 | - 复杂性: 实现更加复杂,需要进行缓存管理并处理潜在的不一致性问题。 |
| - 可扩展性: 通过将大部分读取操作卸载到从库,实现更好的扩展性。 | - 缓存管理: 需要进行适当的缓存失效处理和同步,以确保数据的一致性。 | |
| - 一致性: 通过比较版本并在必要时回退到主库,提供确保最新数据的机制。 | - 潜在延迟: 从库的数据可能仍然存在不同步的可能性,导致数据更新前有轻微延迟。 |
场景:
解决方案:
前端方案
前端通过js脚本控制,无法解决用户刷新提交的请求。另外也无法解决恶意提交。
不建议采用该方案,如果想用,也只是作为一个补充方案。
中间环节去重。根据请求参数中间去重
当用户点击购买按钮时,渲染下单页面,展示商品、收货地址、运费、价格等信息,同时页面会埋上 Token 信息,用户提交订单时,后端业务逻辑会校验token,有且匹配才认为是合理请求。
利用数据库自身特性 “主键唯一约束”,在插入订单记录时,带上主键值,如果订单重复,记录插入会失败。
操作过程如下:
引入一个服务,用于生成一个”全局唯一的订单号”;
进入创建订单页面时,前端请求该服务,预生成订单ID;
提交订单时,请求参数除了业务参数外,还要带上这个预生成订单ID
为了保证数据的 完整性、可追溯性,写操作需要关注的问题
场景:
商品信息是可以修改的,当用户下单后,为了更好解决后面可能存在的买卖纠纷,创建订单时会同步保存一份商品详情信息,称之为订单快照
解决方案:
同一件商品,会有很多用户会购买,如果热销商品,短时间就会有上万的订单。如果每个订单都创建一份快照,存储成本太高。另外商品信息虽然支持修改,但毕竟是一个低频动作。我们可以理解成,大部分订单的商品快照信息都是一样的,除非下单时用户修改过。
如何实时识别修改动作是解决快照成本的关键所在。我们采用摘要比对的方法。创建订单时,先检查商品信息摘要是否已经存在,如果不存在,会创建快照记录。订单明细会关联商品的快照主键。
账户余额更新,保证事务
用户支付,我们要从买家账户减掉一定金额,再往卖家增加一定金额,为了保证数据的 完整性、可追溯性, 变更余额时,我们通常会同时插入一条 记录流水。
账户流水核心字段: 流水ID、金额、交易双方账户、交易时间戳、订单号。
账户流水只能新增,不能修改和删除。流水号必须是自增的。
后续,系统对账时,我们只需要对交易流水明细数据做累计即可,如果出现和余额不一致情况,一般以交易流水为准来修复余额数据。
更新余额、记录流水 虽属于两个操作,但是要保证要么都成功,要么都失败。要做到事务。
当然,如果涉及多个微服务调用,会用到 分布式事务。
分布式事务,细想下也很容易理解,就是 将一个大事务拆分为多个本地事务, 本地事务依然借助于数据库自身事务来解决,难点在于解决这个分布式一致性问题,借助重试机制,保证最终一致是我们常用的方案。
场景:
商家发货,填写运单号,开始填了 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, 这部分数据毕竟不是共享的,但是不太好的增加了设计的复杂度。
客户端需要借助本地数据索引,远程请求查完整信息;
如果是登录态,还要增加数据合并逻辑;
考虑到这两部分数据只是用户标识的差异性,所以作者还是建议统一存到服务端,日后即使业务逻辑变更,只需要改一处就可以了,毕竟自运营系统,良好的可维护性也需要我们非常关注的。
购物车是电商系统的标配功能,暂存用户想要购买的商品。
item ID 自增。(100w级别)
order id. 时间戳 + 机器ID + uid % 100 + sequence
DP唯一ID生成调研说明
request 生成方法:时间戳 + 机器mac地址 + sequence
Google SRE中(SRE三部曲[1])有一个层级模型来描述系统可靠性基础和高层次需求(Dickerson’s Hierarchy of Service Reliability),如下图:
该模型由Google SRE工程师Mikey Dickerson在2013年提出,将系统稳定性需求按照基础程度进行了不同层次的体系化区分,形成稳定性标准金字塔模型:
金字塔的底座是监控(Monitoring),这是一个系统对于稳定性最基础的要求,缺少监控的系统,如同蒙上眼睛狂奔的野马,无从谈及可控性,更遑论稳定性。
更上层是应急响应(Incident Response),从一个问题被监控发现到最终解决,这期间的耗时直接取决于应急响应机制的成熟度。合理的应急策略能保证当故障发生时,所有问题能得到有序且妥善的处理,而不是慌乱成一锅粥。
研发流程规范
容量规划(Capacity Planning)则是针对于这方面变化进行的保障策略。现有系统体量是否足够支撑新的流量需求,整体链路上是否存在不对等的薄弱节点,都是容量规划需要考虑的问题。
高可用性产品设计(Product)与软件研发(Development),即通过优秀的产品设计与软件设计使系统具备更高的可靠性,构建高可用产品架构体系,从而提升用户体验
除此之外,还需要系统资损防控等
下面也会从六个个方面分别介绍
| 监控类型 | 监控指标 |
|---|---|
| 业务监控 | - 订单量、GMV、转化率等业务KPI - 用户活跃度、留存率等用户指标 - 支付成功率、退款率等交易指标 - 下单失败率 - 支付超时率 - 库存不足率 |
| 应用监控 | - 接口响应时间(RT) - 接口调用量(QPS) - 接口成功率 - 应用JVM指标 - 线程池使用情况 - 应用日志监控 |
| 系统监控 | - 服务器CPU/内存/磁盘使用率 - 网络带宽使用率 - 系统负载 - 容器资源使用情况 - 网关流量/延迟/错误率 |
| 外部依赖 | - RPC调用成功率/延迟 - 数据库连接池状态/慢查询 - 缓存命中率/延迟 - 消息队列积压量/消费延迟 - 第三方服务调用成功率/超时率 - 分布式锁获取成功率 - 分布式事务状态 - CDN服务质量 - 对象存储可用性 |
| 其它 | - 安全相关指标(登录失败/异常IP等) - 业务告警统计/处理率 - 系统变更记录/回滚率 - 运维操作审计 - 数据备份状态 - 证书过期监控 - 配置变更记录 - 资源成本监控 - 服务SLA达标率 |
是不是每项监控都需要告警?答案当然是否定的。建议优先设置Biz层告警,因为Biz层我们对外服务最直观业务表现,最贴切用户感受。Application&System层指标主要用于监控,部分关键&高风险指标可设置告警,用于问题排查定位以及故障提前发现。对于一项告警,我们一般需要关注级别、阈值、通知人等几个点。
常用的应急策略
| 策略类型 | 具体场景 | 实现方式 | 应用案例 | 配置建议 |
|---|---|---|---|---|
| 降级策略 | 服务降级 | 降级开关 | 推荐服务返回本地缓存 | 开关粒度: 接口级 降级时长: 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)Little Law衍生法则
不同类型资源节点(应用容器、Tair、DB、HBASE等)流量-容量转化比各不相同,但都服从Little Law衍生法则,即:
节点容量=节点吞吐率×平均响应时间
2)N + X 冗余原则
在满足目标流量所需要的最小容量基础上,冗余保留X单位冗余能力
X与目标成本与资源节点故障概率成正相关,不可用概率越高,X越高
对于一般应用容器集群,可考虑X = 0.2N
| 架构层面 | 设计类型 | 具体措施 | 说明 |
|---|---|---|---|
| 整体架构和部署 | 冗余设计 | 多机房部署 | 在不同地理位置部署多个机房,避免单点故障 |
| 服务多副本 | 每个服务部署多个实例,提供故障转移能力 | ||
| 数据多副本 | 数据多副本存储,保证数据可靠性 | ||
| 无状态服务设计 | 服务无状态化,便于水平扩展 | ||
| 容灾设计 | 同城双活 | 同一城市两个机房同时对外服务 | |
| 异地多活 | 多地机房同时对外服务,就近访问 | ||
| 机房级容灾 | 单机房故障时可切换到其他机房 | ||
| 数据中心容灾 | 数据中心级别的容灾能力 | ||
| 灾备演练 | 演练计划 | 定期制定灾备演练计划 | |
| 演练流程 | 标准化的演练流程和checklist | ||
| 效果评估 | 对演练结果进行评估和复盘 | ||
| 持续改进 | 根据演练反馈持续优化容灾方案 | ||
| 安全架构 | 网络安全 | 防火墙/WAF | 防御网络攻击,过滤恶意流量 |
| VPN/专线 | 安全的网络连接方式 | ||
| 数据安全 | 加密存储 | 敏感数据加密存储 | |
| 访问控制 | 严格的数据访问权限控制 | ||
| 应用安全 | 身份认证 | 多因素认证,防止账号盗用 | |
| 操作审计 | 记录关键操作审计日志 |
| 设计类型 | 具体措施 | 说明 |
|---|---|---|
| 隔离设计 | 服务隔离 | 核心服务独立部署,避免互相影响 |
| 数据隔离 | 按业务领域划分数据存储 | |
| 故障隔离 | 故障隔离机制,防止故障扩散 | |
| 资源隔离 | CPU/内存等资源隔离管理 | |
| 水平扩展设计 | 服务无状态化 | 服务设计无状态,便于扩容 |
| 数据分片策略 | 数据分片存储,支持横向扩展 | |
| 弹性伸缩 | 根据负载自动扩缩容 | |
| 负载均衡 | 多实例间的负载分配策略 |
系统链路梳理是所有保障工作的基础,它就像对整个应用系统进行一次全面体检。从流量入口开始,按照链路轨迹,逐级分层节点,最终得到系统全局画像与核心保障点。主要包含以下几个方面:
强弱依赖判定
可用性风险判定
高风险节点识别
系统调用拓扑图(可借助分布式链路追踪系统),包含QPS、强弱依赖、高风险节点标注
接口时序图(以创建订单为例)
应用层优化
数据层优化
缓存优化
什么是SPU和SKU?它们之间的关系是什么?
电商系统中的商品分类体系是如何设计的? category 父子类目
什么是商品属性?如何区分规格属性(Sales Attributes))和非规格属性?属性,会影响商品SKU的属性直接关系到库存和价格,用户购买时需要选择的属性,例如:颜色、尺码、内存容量等。非规格属性(Basic Attributes):用于描述商品特征,产地、材质、生产日期
商品的生命周期包含哪些状态?
1 | 创建阶段 |
什么是商品快照?为什么需要商品快照?
商品的 SKU 和 SPU 概念在后台系统中如何体现?两者的关系是怎样的?
电商后台系统中商品的基础信息包括哪些?如何设计商品表的数据库模型?
商品的库存管理和价格管理在后台系统中是如何关联的?
如何处理商品的多规格(如颜色、尺寸、型号等)信息?数据库表结构如何设计?
商品详情页的信息(如描述、图片、参数)在后台系统中如何存储和管理?
商品上下架的逻辑在后台系统中是如何实现的?需要考虑哪些因素(如库存、审核状态等)?
商品的搜索和筛选功能在后台系统中是如何实现的?涉及哪些技术(如全文搜索、数据库索引等)?
新品发布和商品淘汰在后台系统中的处理流程是怎样的?
如何保证商品信息的唯一性和完整性,避免重复录入和数据错误?
背景:
外部依赖调研
业界方案调研和对比:
整体设计:
功能设计:
非功能设计:
资源清单:
任务拆分和排期:
评审记录:
** 附录:设计文档模板 **
设计文档没有定式。即使如此,笔者参考谷歌设计文档的结构和格式,并结合实际工作经验加以完善。在此提供一个可供新手参考的设计文档模版,您可以使用此文档模板作为思考的基础。通常,无须事无巨细地填写每一部分,不相关的内容直接略过即可。
计决策的合理性,同时也有助于日后迭代设计时,检查最初的假设是否仍然成立。
为设计文档的目标读者提供理解详细设计所需的背景信息。按读者范围来提供背景。见上文关于目标读者的圈定。设计文档应该是“自足的”(self-contained),即应该为读者提供足够的背景知识,使其无需进一步的查阅资料即可理解后文的设计。保持简洁,通常以几段为宜,每段简要介绍即可。如果需要向读者提供进一步的信息,最好只提供链接。警惕知识的诅咒(知识的诅咒(Curse of knowledge)是一种认知偏差,指人在与他人交流的时候,下意识地假设对方拥有理解交流主题所需要的背景知识)
背景通常可以包括:
需求动机以及可能的例子。 如,“(tRPC) 微服务模式正在公司内变得流行,但是缺少一个通用的、封装了常用内部工具及服务接口的微服务框架”。 - 这是放置需求文档的链接的好地方。
此前的版本以及它们的问题。 如,“(tRPC) Taf 是之前的应用框架, 有以下特点,…………, 但是有以下局限性及历史遗留问题”。
其它已有方案, 如公司内其它方案或开源方案, “tRPC v.s. gRPC v.s. Arvo”
相关的项目,如 “tRPC 框架中可能会对接的其它 PCG 系统”
不要在背景中写你的设计,或对问题的解决思路。
“解决这个问题的难点和挑战”
用几句话说明该设计文档的关键目的,让读者能够一眼得知自己是否对该设计文档感兴趣。 如:“本文描述 Spanner 的顶层设计”
继而,使用 Bullet Points 描述该设计试图达到的重要目标,如:
“我们如何解决这个问题?”
用一页描述高层设计。说明系统的主要组成部分,以及一些关键设计决策。应该说明该系统的模块和决策如何满足前文所列出的目标。
本设计文档的评审人应该能够根据该总体设计理解你的设计思路并做出评价。描述应该对一个新加入的、不在该项目工作的腾讯工程师而言是可以理解的。
推荐使用系统关系图描述设计。它可以使读者清晰地了解文中的新系统和已经熟悉的系统间的关系。它也可以包含新系统内部概要的组成模块。
注意:不要只放一个图而不做任何说明,请根据上面小节的要求用文字描述设计思想。
一个示例体统关系图
自举的文档结构图
可能不太好的顶层设计
不要在这里描述细节,放在下一章节中; 不要在这里描述背景,放在上一章节中。
在这一节中,除了介绍设计方案的细节,还应该包括在产生最终方案过程中,主要的设计思想及权衡(tradeoff)。这一节的结构和内容因设计对象(系统,API,流程等)的不同可以自由决定,可以划分一些小节来更好地组织内容,尽可能以简洁明了的结构阐明整个设计。
不要过多写实现细节。就像我们不推荐添加只是说明”代码做了什么”的注释,我们也不推荐在设计文档中只说明你具体要怎么实现该系统。否则,为什么不直接实现呢? 以下内容可能是实现细节例子,不适合在设计文档中讨论:
** 各子模块的设计 **
阐明一些复杂模块内部的细节,可以包含一些模块图、流程图来帮助读者理解。可以借助时序图进行展现,如一次调用在各子模块中的运行过程。每个子模块需要说明自己存在的意义。如无必要,勿添模块。如果没有特殊情况(例如该设计文档是为了描述并实现一个核心算法),不要在系统设计加入代码或者伪代码。
** 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 服务、时间协议服务、运行集群等。
描述时间及人力安排(如里程碑)。 这利于相关人员了解预期,调整工作计划。
未来可能的计划会方便读者更好地理解该设计以及其定位。
性能与可扩展性的权衡:提高性能可能需要牺牲一部分可扩展性,因为某些优化可能会引入复杂性或限制系统的扩展性。
可维护性与性能的权衡:某些优化措施可能会降低代码的可读性和可维护性,因此需要在维护性和性能之间进行权衡。
时间与成本的权衡:系统设计需要考虑开发时间和成本,以确保在给定资源限制下实现最佳的设计方案
安全性与用户体验的权衡:强大的安全措施可能会增加用户的身份验证和授权过程,从而影响用户体验。
架构权衡评估方法(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):如何评估一个系统的质量
系统架构
单一职责原则(SRP):每个组件或模块应该具有单一的责任,降低耦合度,提高可维护性。
开闭原则(OCP):系统应对扩展开放,对修改关闭,通过接口和抽象来实现。
替换原则(LSP):子类应该能够替换其基类,而不会影响系统的正确性。
接口隔离原则(ISP):客户端不应该依赖于它不需要的接口,接口应该精简而专注。
依赖倒置原则(DIP):高层模块不应该依赖于低层模块,两者都应该依赖于抽象
SOLID 原则是一套比较经典且流行的架构原则(主要还是名字起得好):
单一职责:与 Unix 哲学所倡导的“Do one thing and do it well”不谋而合;
开闭原则:用新增(扩展)来取代修改(破坏现有封装),这与函数式的 immutable 思想也有异曲同工之妙;
里式替换:父类能够出现的地方子类一定能够出现,这样它们之间才算是具备继承的“Is-A”关系;
接口隔离:不要让一个类依赖另一个类中用不到的接口,简单说就是最小化组件之间的接口依赖和耦合;
依赖反转:依赖抽象类与接口,而不是具体实现;让低层次模块依赖高层次模块的稳定抽象,实现解耦
此外,我们做架构设计时也会尽量遵循如下一些原则(与上述 SOLID 原则在本质上也是相通的):
正交性:架构同一层次拆分出的各组件之间,应该尽量保持正交,即彼此职责独立,边界清晰,没有重叠;
高内聚:同一组件内部应该是高度内聚的(cohesive),像是一个不可分割的整体(否则就应该拆开);
低耦合:不同组件之间应该尽量减少耦合(coupling),既降低相互的变化影响,也能增强组件可复用性;
隔离变化:许多架构原则与模式的本质都是在隔离变化 —— 将预期可能变化的部分都隔离到一块,减少发生变化时受影响(需要修改代码、重新测试或产生故障隐患)的其他稳定部分
https://github.com/leewaiho/Clean-Architecture-zh/tree/master?tab=readme-ov-file
1 | Latency Comparison Numbers |
基于上述数字的指标:
📌 𝐒𝐲𝐬𝐭𝐞𝐦 𝐃𝐞𝐬𝐢𝐠𝐧 𝐊𝐞𝐲 𝐂𝐨𝐧𝐜𝐞𝐩𝐭𝐬
🛠️ 𝐒𝐲𝐬𝐭𝐞𝐦 𝐃𝐞𝐬𝐢𝐠𝐧 𝐁𝐮𝐢𝐥𝐝𝐢𝐧𝐠 𝐁𝐥𝐨𝐜𝐤𝐬
🖇️ 𝐒𝐲𝐬𝐭𝐞𝐦 𝐃𝐞𝐬𝐢𝐠𝐧 𝐀𝐫𝐜𝐡𝐢𝐭𝐞𝐜𝐭𝐮𝐫𝐚𝐥 𝐏𝐚𝐭𝐭𝐞𝐫𝐧𝐬
单体服务(Monolithic Services):单体服务是指将整个应用程序作为一个单一的、紧密耦合的单元进行开发、部署和运行的架构模式。在单体服务中,应用程序的各个功能模块通常运行在同一个进程中,并共享相同的数据库和资源。单体服务的优点是开发简单、部署方便,但随着业务规模的增长,单体服务可能变得庞大且难以维护。
微服务(Microservices):微服务是一种将应用程序拆分为一组小型、独立部署的服务的架构模式。每个微服务都专注于单个业务功能,并通过轻量级的通信机制(如RESTful API或消息队列)进行相互通信。微服务的优点是灵活性高、可扩展性好,每个微服务可以独立开发、测试、部署和扩展。然而,微服务架构也带来了分布式系统的复杂性和管理的挑战。
Service Mesh:Service Mesh是一种用于解决微服务架构中服务间通信和治理问题的基础设施层。它通过在服务之间插入一个专用的代理(称为Sidecar)来提供服务间的通信、安全性、可观察性和弹性的功能。Service Mesh可以提供流量管理、负载均衡、故障恢复、安全认证、监控和追踪等功能,而不需要在每个微服务中显式实现这些功能。常见的Service Mesh实现包括Istio、Linkerd和Consul Connect等。
与此讨论相关的话题是 微服务,可以被描述为一系列可以独立部署的小型的,模块化服务。每个服务运行在一个独立的线程中,通过明确定义的轻量级机制通讯,共同实现业务目标。1例如,Pinterest 可能有这些微服务: 用户资料、关注者、Feed 流、搜索、照片上传等。
ZooKeeper
etcd
etcd是一个开源的分布式键值存储系统,由CoreOS开发并后来成为Cloud Native Computing Foundation(CNCF)的项目之一。
etcd被设计为一个高可用、可靠的分布式存储系统,用于存储和管理关键的配置数据和元数据。
etcd使用Raft一致性算法来保证数据的一致性和可靠性,Raft是一种强一致性的分布式共识算法。
etcd提供了一个简单的键值存储接口,可以存储和检索键值对数据,并支持对数据的原子更新操作。
etcd还提供了一些高级特性,如目录结构、事务操作和观察者机制,用于构建复杂的分布式系统和应用
Source: Crack the system design interview
在 RPC 中,客户端会去调用另一个地址空间(通常是一个远程服务器)里的方法。调用代码看起来就像是调用的是一个本地方法,客户端和服务器交互的具体过程被抽象。远程调用相对于本地调用一般较慢而且可靠性更差,因此区分两者是有帮助的。热门的 RPC 框架包括 Protobuf、Thrift 和 Avro。
RPC 是一个“请求-响应”协议:
域名系统是把 www.example.com 等域名转换成 IP 地址。域名系统是分层次的,一些 DNS 服务器位于顶层。当查询(域名) IP 时,路由或 ISP 提供连接 DNS 服务器的信息。较底层的 DNS 服务器缓存映射,它可能会因为 DNS 传播延时而失效。DNS 结果可以缓存在浏览器或操作系统中一段时间,时间长短取决于存活时间 TTL。
CNAME 记录( example.com 指向 www.example.com )或映射到一个 A 记录。
负载均衡器将传入的请求分发到应用服务器和数据库等计算资源。无论哪种情况,负载均衡器将从计算资源来的响应返回给恰当的客户端。负载均衡器的效用在于:
负载均衡器能基于多种方式来路由流量:
四层负载均衡根据监看传输层的信息来决定如何分发请求。通常,这会涉及来源,目标 IP 地址和请求头中的端口,但不包括数据包(报文)内容。四层负载均衡执行网络地址转换(NAT)来向上游服务器转发网络数据包。
七层负载均衡器根据监控应用层来决定怎样分发请求。这会涉及请求头的内容,消息和 cookie。七层负载均衡器终结网络流量,读取消息,做出负载均衡判定,然后传送给特定服务器。比如,一个七层负载均衡器能直接将视频流量连接到托管视频的服务器,同时将更敏感的用户账单流量引导到安全性更强的服务器。
以损失灵活性为代价,四层负载均衡比七层负载均衡花费更少时间和计算资源,虽然这对现代商用硬件的性能影响甚微。
负载均衡器还能帮助水平扩展,提高性能和可用性。使用商业硬件的性价比更高,并且比在单台硬件上垂直扩展更贵的硬件具有更高的可用性。相比招聘特定企业系统人才,招聘商业硬件方面的人才更加容易。
反向代理是一种可以集中地调用内部服务,并提供统一接口给公共客户的 web 服务器。来自客户端的请求先被反向代理服务器转发到可响应请求的服务器,然后代理再把服务器的响应结果返回给客户端。
带来的好处包括:
将 Web 服务层与应用层(也被称作平台层)分离,可以独立缩放和配置这两层。添加新的 API 只需要添加应用服务器,而不必添加额外的 web 服务器。用于完成基础的:
路径名称避免动词
1 | 路径名称避免动词 |
GET 获取指定 URI 的资源信息
1 | # 代表获取当前系统的所有订单信息 |
POST 通过指定的 URI 创建资源
1 | curl -X POST /orders \ |
PUT 创建或全量替换指定 URI 上的资源
1 | curl -X PUT http://httpbin.org/orders/1 \ |
PATCH 执行一个资源的部分更新
1 | # 代表将 id 为 1 的 order 中的 region 字段进行更改,其他数据保持不变 |
DELETE 通过指定的 URI 移除资源
1 | # 代表将id的 order 删除 |
其它规则:
规则1:应使用连字符( - )来提高URI的可读性
规则2:不得在URI中使用下划线(_)
规则3:URI路径中全都使用小写字母
1 | 如: Facebook API 的错误 Code 设计,始终返回 200 http status code: |
1 | 如: Twitter API 的错误设计 |
1 | 如: 微软 Bing API 的错误设计,会根据错误类型,返回合适的 HTTP Code,并在 Body 中返回详尽的错误信息 |
1 | 如: 错误代码说明:100101 |
内容分发网络(CDN)是一个全球性的代理服务器分布式网络,它从靠近用户的位置提供内容。通常,HTML/CSS/JS,图片和视频等静态内容由 CDN 提供,虽然亚马逊 CloudFront 等也支持动态内容。CDN 的 DNS 解析会告知客户端连接哪台服务器。
将内容存储在 CDN 上可以从两个方面来提供性能:
当你服务器上内容发生变动时,推送 CDN 接受新内容。直接推送给 CDN 并重写 URL 地址以指向你的内容的 CDN 地址。你可以配置内容到期时间及何时更新。内容只有在更改或新增是才推送,流量最小化,但储存最大化。
CDN 拉取是当第一个用户请求该资源时,从服务器上拉取资源。你将内容留在自己的服务器上并重写 URL 指向 CDN 地址。直到内容被缓存在 CDN 上为止,这样请求只会更慢,
存活时间(TTL)决定缓存多久时间。CDN 拉取方式最小化 CDN 上的储存空间,但如果过期文件并在实际更改之前被拉取,则会导致冗余的流量。
高流量站点使用 CDN 拉取效果不错,因为只有最近请求的内容保存在 CDN 中,流量才能更平衡地分散。
抽象模型:嵌套的
ColumnFamily<RowKey, Columns<ColKey, Value, Timestamp>>映射
类型存储的基本数据单元是列(名/值对)。列可以在列族(类似于 SQL 的数据表)中被分组。超级列族再分组普通列族。你可以使用行键独立访问每一列,具有相同行键值的列组成一行。每个值都包含版本的时间戳用于解决版本冲突。
Google 发布了第一个列型存储数据库 Bigtable,它影响了 Hadoop 生态系统中活跃的开源数据库 HBase 和 Facebook 的 Cassandra。像 BigTable,HBase 和 Cassandra 这样的存储系统将键以字母顺序存储,可以高效地读取键列。
列型存储具备高可用性和高可扩展性。通常被用于大数据相关存储。
抽象模型: 图
在图数据库中,一个节点对应一条记录,一个弧对应两个节点之间的关系。图数据库被优化用于表示外键繁多的复杂关系或多对多关系。
图数据库为存储复杂关系的数据模型,如社交网络,提供了很高的性能。它们相对较新,尚未广泛应用,查找开发工具或者资源相对较难。许多图只能通过 REST API 访问。
选取 SQL 的原因:
选取 NoSQL 的原因:
适合 NoSQL 的示例数据:
缓存可以提高页面加载速度,并可以减少服务器和数据库的负载。在这个模型中,分发器先查看请求之前是否被响应过,如果有则将之前的结果直接返回,来省掉真正的处理。
数据库分片均匀分布的读取是最好的。但是热门数据会让读取分布不均匀,这样就会造成瓶颈,如果在数据库前加个缓存,就会抹平不均匀的负载和突发流量对数据库的影响。
异步工作流有助于减少那些原本顺序执行的请求时间。它们可以通过提前进行一些耗时的工作来帮助减少请求时间,比如定期汇总数据。
消息队列接收,保留和传递消息。如果按顺序执行操作太慢的话,你可以使用有以下工作流的消息队列:
参考:
如果队列开始明显增长,那么队列大小可能会超过内存大小,导致高速缓存未命中,磁盘读取,甚至性能更慢。背压可以通过限制队列大小来帮助我们,从而为队列中的作业保持高吞吐率和良好的响应时间。一旦队列填满,客户端将得到服务器忙或者 HTTP 503 状态码,以便稍后重试。客户端可以在稍后时间重试该请求,也许是指数退避
参考:tengo GitHub
参考:anko GitHub
这一部分需要更多内容。一起来吧!
安全是一个宽泛的话题。除非你有相当的经验、安全方面背景或者正在申请的职位要求安全知识,你不需要了解安全基础知识以外的内容:
参考:
1 | sudo ip netns add ns1 |
1 | # 查看网桥 |
1 | $ docker ps | grep etcd |
1 | $ ls -l /opt/cni/bin/ |
支持的路由方式
1 | # minikube |
参考资料
业务上需要考关注失败、丢失、重复三个问题:
三种语义:
实践
Kafka消息发送有两种方式:同步(sync)和异步(async),默认是同步方式,可通过producer.type属性进行配置。Kafka通过配置request.required.acks属性来确认消息的生产:
综上所述,有6种消息生产的情况,下面分情况来分析消息丢失的场景:
通常来说,producer 采用at least once方式
在实际业务场景中,由于consumer消费速度慢于producer的速度,会造成消息堆积,最终会导致消息过期删除丢失。业务需要监控这种lag情况,并及时告警出来。
另外需要注意的是,kafka只允许单个分区的数据被一个消费者线程消费,如果消费者越多意味着partition也要越多。
然而在分区数量有限的情况下,消费者数量也就会被限制。在这种约束下,如果消息堆积了该如何处理?
消费消息的时候直接返回,然后启动异步线程去处理消息,消息如果再处理的过程中失败的话,再重新发送到kafka中。
Rebalance本身是Kafka集群的一个保护设定,用于剔除掉无法消费或者过慢的消费者,然后由于我们的数据量较大,同时后续消费后的数据写入需要走网络IO,很有可能存在依赖的第三方服务存在慢的情况而导致我们超时。Rebalance对我们数据的影响主要有以下几点:
Kafka虽然除了具有上述优点之外,还具有高性能、高吞吐、低延时的特点,其吞吐量动辄几十万、上百万。
1 | c.Producer.MaxMessageBytes = 1000000 |
创建topic
1 | bin/kafka-topics.sh --create --topic topic-name --replication-factor 2 --partitions 3 --bootstrap-server ip:port |
查看topic情况
1 | bin/kafka-topics.sh --topic topic_name --describe --bootstrap-server broker |
查看消费组情况
1 | ./bin/kafka-consumer-groups.sh --describe --group group_name --bootstrap-server brokers |
重置消费offsets
1 |
|
1 | curl -XPUT -H'Content-Type: application/json' host/index_name?pretty=true -d@index_mapping.json |
1 | { |
1 | curl -XGET 'host/_cat/indices/*hotel_basic_info_v2_live*(支持正则表达式)?v=true&pretty=true' |
1 | curl -XGET 'host/index_name/_mapping?pretty=true' |
1 | curl -XGET 'host/index_name/_settings?pretty=true' |
1 | curl -XGET 'host/index/_doc/doc_id?pretty=true' |
1 | curl -XPOST -H'Content-Type: application/json' 'host/index_name/_search?pretty=true' -d '{ |
1 | curl -XPOST -H'Content-Type: application/json' 'host/index/_doc/doc_id/_update' -d '{ |
1 | curl -XPOST -H'Content-Type: application/json' 'host/index_name/_count' -d '{ |
1 | curl -XPOST -H'Content-Type: application/json' 'host/index_name/_doc/_mapping' -d '{ |
参考:https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-index-search-time.html
Elastic Search 在处理 Text 类型数据的时候,会把数据交给分词器处理。然后根据分词器给的词,建立倒排索引,通常一句话都由若干词语组成,分词结果会极大的影响到查询结果的质量
在 Elastic Search 中,分词器起到了非常重要的作用,在定义文档结构、录入和更新文档、查询文档的时候都会用到它。例如:
1 | 武汉市长江大桥欢迎您 |
1 | POST /_aliases |
1 | curl -XPUT host/index_nane/_alias/index_alias_name |
term level queries
full text queries
compound queries
https://www.elastic.co/guide/en/elasticsearch/reference/6.7/query-dsl.html
1 | { |
ES的Search操作分为两个阶段:query then fetch。需要两阶段完成搜索的原因是:在查询时不知道文档位于哪个分片,因此索引的所有分片都要参与搜索,然后协调节点将结果合并,在根据文档ID获取文档内容。
Query查询阶段
Fetch拉取阶段
query节点知道了要获取哪些信息,但是没有具体的数据,fetch阶段要去拉取具体的数据。相当于执行多次上面的GET流程
1 | { |
1 | "properties": { |
本文深入剖析 Redis 的核心原理、数据结构底层实现、电商场景最佳实践。包含 Hash ziplist 字节级内存分析、渐进式 rehash 机制、缓存一致性方案等。
{name: "iPhone", price: 5999}| 特性 | 说明 | 优势 |
|---|---|---|
| 内存存储 | 数据全部在内存中 | 极快的读写速度(10w+ QPS) |
| 单线程模型 | 命令串行执行 | 避免锁竞争,简化并发 |
| 多种数据结构 | String/Hash/List/Set/ZSet | 覆盖多种业务场景 |
| 持久化支持 | RDB + AOF | 数据不丢失 |
| 主从复制 | 读写分离 | 高可用、高并发 |
| 集群模式 | 分片存储 | 横向扩展能力 |
1 | // 查询商品详情 |
性能提升:DB 查询 100ms → Redis 查询 1ms(提升 100 倍)
活动开始前,将商品库存同步到 Redis:SET stock:sku:1001 50。
用户下单时,执行:DECR stock:sku:1001。
如果返回值 >= 0,放行去写订单;如果返回值 < 0,立即返回“已售罄”。
优势: 内存级操作,单机可支撑 10w+ TPS,彻底杜绝超卖。
高频限流(防刷与风控)
1 | const bookNormalStockScript = ` |
浏览最近查看的商品(Top 10) 如果你在商城详情页下方展示“最近查看”,你可能需要先读取前 10 个显示给用户
1 | // 1. 先读取前 10 个给前端展示,不删除 |
缓存数据(db,service) 的数据,提高访问效率
限流和计数。lua脚本。(int,incr,lua)
延时队列
消息队列
bloomfilter: https://juejin.cn/post/6844903862072000526
$m = -\frac{nln(p)}{(ln2)^2}$
$k=\frac{m}{n}ln(2)$
1 | n 是预期插入的元素数量(数据规模),例如 20,000,000。 |
深入理解 Redis 五种数据类型的底层实现原理,掌握内存优化技巧。
参考资料:
Redis Hash 采用两种编码方式,根据数据特征自动选择:
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 | hash-max-ziplist-entries 512 # 最大元素个数 |
适用场景:小对象存储(如商品详情、Session、小型 List)
ziplist 是 Redis 为节省内存设计的紧凑型数据结构,所有数据存储在一块连续的内存中。
1 | +----------+----------+--------+---------+---------+-----+---------+--------+ |
| 字段 | 长度 | 说明 |
|---|---|---|
| zlbytes | 4 字节 | 整个 ziplist 占用的总字节数(包括 zlbytes 自身) |
| zltail | 4 字节 | 到尾节点的偏移量(用于快速定位尾部,支持反向遍历) |
| zllen | 2 字节 | 节点数量,最大 65535;超过则需遍历整个列表计数 |
| entry | 变长 | 实际数据节点,每个节点长度不固定 |
| zlend | 1 字节 | 固定为 0xFF,标记 ziplist 结束 |
每个 entry 由三部分组成:
1 | +----------+----------+----------+ |
记录前一个节点的长度,用于从后向前遍历。
1 | // 编码规则 |
示例:
prevlen = 0x0A(1字节)prevlen = 0xFE 0x00 0x00 0x01 0x2C(5字节)记录 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) |
存储实际的数据内容,根据 encoding 字段解析:
存储 Hash:{name: "iPhone", price: 5999}
1 | 偏移 | 字段 | 值 | 说明 |
内存计算:10(头)+ 6 + 8 + 7 + 4 + 1(尾)= 36 字节
正向遍历(从头到尾):
1 | ptr = ziplist + 10; // 跳过头部 |
反向遍历(从尾到头):
1 | ptr = ziplist + zltail; // 直接定位尾节点 |
问题:插入/删除节点可能导致后续节点的 prevlen 字段长度变化。
场景示例:
1 | 初始状态:[253B] [253B] [253B] |
影响:
realloc 操作Redis 优化:
realloc 次数1 | // 示例:字符串 "hello" 的 encoding |
✅ 优势:
zltail 和 prevlen 实现❌ 限制:
realloc| 数据类型 | 使用 ziplist 条件 | 典型场景 |
|---|---|---|
| Hash | entries ≤ 512, value ≤ 64B | 商品基础信息、用户 Session |
| List | entries ≤ 512 | 消息队列、浏览历史 |
| ZSet | entries ≤ 128, member ≤ 64B | 小型排行榜、优先队列 |
监控命令:
1 | # 查看编码类型 |
适用场景:大量字段或需要快速查找
核心结构:经典拉链法哈希表
1 | // Redis 字典结构(dict) |
可视化结构:详见 redis-hashtable.mmd
1 | dict |
触发条件:
1 | // 扩容 |
渐进式 Rehash 流程:
ht[1] 分配空间(扩容为 used * 2 的最小 2^n)rehashidx = 0 开始迁移ht[0].table[rehashidx] 的所有数据到 ht[1]ht[0],将 ht[1] 设为 ht[0]可视化流程:详见 redis-rehash-process.mmd
Rehash 期间的操作:
1 | 查找:先查 ht[0],未找到再查 ht[1] |
为什么用渐进式?
1 | // Redis 使用 MurmurHash2(速度快、分布均匀) |
| 操作 | ziplist | hashtable |
|---|---|---|
| HSET | O(n) | O(1) 平均 |
| HGET | O(n) | O(1) 平均 |
| HDEL | O(n) | O(1) 平均 |
| HGETALL | O(n) | O(n) |
| 内存占用 | 低(无指针) | 高(指针+空间换时间) |
| CPU缓存 | 友好(连续) | 一般(随机访问) |
| 适用场景 | < 512字段小对象 | 大量字段快速查找 |
1 | // ✅ 推荐:商品详情缓存(字段适中,频繁部分更新) |
| 维度 | Hash | String (JSON) |
|---|---|---|
| 内存 | ziplist 模式更省 | JSON 序列化开销大 |
| 部分更新 | ✅ HSET field |
❌ 需整体 GET→改→SET |
| 序列化 | 无需序列化 | 需 JSON 编解码(CPU开销) |
| 查询灵活性 | HMGET 精确取字段 |
GET 整体取出 |
| 适用场景 | 频繁部分字段更新 | 整体读写、复杂嵌套结构 |
选择建议:
1 | # 1. 查找大 key |
Redis String 实际是 SDS(Simple Dynamic String),而非 C 字符串。
1 | struct sdshdr { |
| 特性 | C 字符串 | SDS |
|---|---|---|
| 获取长度 | O(n) 遍历 | O(1) 直接读 len |
| 缓冲区溢出 | 不检查,易溢出 | 自动扩容,安全 |
| 内存重分配 | 每次都需要 | 空间预分配 + 惰性释放 |
| 二进制安全 | 否(遇 \0 结束) |
是(记录长度) |
String 有 3 种编码:
1 | # 1. int 编码(整数) |
embstr vs raw:
1 | // 1. 计数器 |
List 在 Redis 3.2 之前使用 ziplist 或 linkedlist,3.2+ 统一使用 quicklist。
1 | quicklist = ziplist 链表 |
设计思想:
1 | # 每个 ziplist 的最大大小(字节) |
1 | // 1. 消息队列(FIFO) |
| 操作 | 时间复杂度 | 说明 |
|---|---|---|
| LPUSH/RPUSH | O(1) | 头尾插入 |
| LPOP/RPOP | O(1) | 头尾弹出 |
| LINDEX | O(n) | 按索引查询 |
| LRANGE | O(n) | 范围查询 |
| LINSERT | O(n) | 中间插入 |
Set 使用 intset 或 hashtable 编码。
适用条件:
set-max-intset-entries)结构:
1 | typedef struct intset { |
特点:
1 | // 1. 标签系统 |
ZSet 使用 ziplist 或 skiplist + hashtable 编码。
条件:
zset-max-ziplist-entries)zset-max-ziplist-value)存储格式:
1 | [member1, score1, member2, score2, ...] |
为什么用两种结构?
skiplist 结构:
1 | Level 3: 1 --------------------------------> 100 |
平均查找复杂度:O(log n)
1 | // 1. 排行榜 |
掌握缓存使用模式、一致性方案、异常处理策略,构建高可用缓存系统。
1 | - noeviction(默认策略):对于写请求不再提供服务,直接返回错误(DEL请求和部分特殊请求除外) |
过期策略通常有以下三种:
掌握持久化、分布式架构、Lua 脚本等高级特性,构建生产级 Redis 系统。
Redis 提供两种持久化方案:
详细对比:Redis持久化原理
本地缓存的双缓冲机制和本地LRU(Least Recently Used)算法都是常见的缓存优化技术,它们具有不同的优点和缺点。
双缓冲机制:
本地LRU算法:
综合来看,双缓冲机制适用于需要提高并发性能、批量更新等场景,但会增加内存开销。本地LRU算法适用于需要提高数据访问效率的场景,但对于访问模式不符合LRU假设的情况下,缓存命中率可能下降。在实际应用中,可以根据具体需求和场景选择适合的缓存优化技术。
当使用 Redis 缓存 DB 数据时,DB 数据会发生 UPDATE,如何考虑 Redis 和 DB 数据的一致性问题呢?
| 缓存更新方式 | 优缺点 |
|---|---|
| 缓存模式+TTL | 业务代码只更新DB,不更新cache,设置较短的TTL(通常分钟级),依靠cache过期无法找到key时回源DB,热key过期可能回导致请求大量请求击穿到DB,需要使用分布式锁或者singleflight等方式避免这种问题 |
| 定时刷新模式 | 定时任务异步获取DB数据刷新到cache,读请求可不回源,需要考虑刷新时间和批量读写 |
| 写DB,写cache | 在并发条件下,DB写操作顺序和cache操作不同保证顺序一致性,需要增加分布式锁等操作 |
| 写DB,删除cache | 删除cache可能失败,需要增加重试,重试也可能失败,比较复杂的加个MQ补偿重试 |
详细方案:缓存异常解决方案
理解 Redis 高性能原理,掌握性能调优技巧,解决大Key/热Key问题。
详细分析:单线程 Redis 为什么快?
参考:Redis 单线程设计
优势:
劣势:
1 | # 什么是大 Key? |
| 阶段 | 方案 | 特点 | 局限性 |
|---|---|---|---|
| 1 | 单机版 | 简单直接 | 单点故障、容量有限、并发有限 |
| 2 | 主从复制 | 读写分离、高可用 | 主从延迟、无自动故障转移 |
| 3 | 哨兵模式 (Sentinel) | 自动故障转移 | 难以扩容、主库写入瓶颈 |
| 4 | 集群模式 (Cluster) | 横向扩展、高可用 | 复杂度高、跨slot操作受限 |
| 5 | Codis | 中心化管理、易运维 | 需要额外组件(Zookeeper) |
特点:
问题:
目标:解决主从复制的自动故障恢复问题
工作机制:
局限性:
特点:
优势:
特点:
GitHub:https://github.com/CodisLabs/codis
参考资料:
Redis 执行 Lua 脚本具有以下特性:
1 | # 连接 Redis |
| 命令 | 说明 | 时间复杂度 |
|---|---|---|
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),会阻塞 RedisSCAN**:渐进式遍历,不阻塞服务器1 | # 什么是大 Key? |
1 | # 1. 扫描整个实例 |
1 | // 1. 拆分大 Key |
1 | # 什么是热 Key? |
1 | // 1. 本地缓存 + Redis 二级缓存 |
1 | # 1. 内存使用 |
1 | # Prometheus 监控规则 |
基于真实业务场景,掌握分布式锁、BloomFilter、秒杀、排行榜等高级应用。
Redis 为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户端对 Redis 的连接并不存在竞争关系。
1 | // SETNX + EXPIRE 实现 |
详细实现:Redis 分布式锁
BloomFilter 用于快速判断元素是否存在,允许误判(False Positive),但不会漏判(No False Negative)。
1 | 判断结果 = 一定不存在 or 可能存在 |
1 | m = -n*ln(p) / (ln2)² # 位数组大小 |
示例:
1 | n = 1,000,000(百万数据) |
1 | import "github.com/bits-and-blooms/bloom/v3" |
1 | // 1. 防止缓存穿透 |
1 | // 1. 预热库存到 Redis |
1 | // 1. 更新分数 |
1 | func UpdateDailyScore(userID string, score int64, date time.Time) error { |
1 | // 1. go-redis(推荐) |
| 指标 | 数值 | 说明 |
|---|---|---|
| 性能 | 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 | 需求:存储用户信息 |
1 | // ✅ 推荐做法 |
本文包含详细的数据结构可视化图表,详见:
{name:"iPhone", price:5999} 完整分析(字节级)2026-01-08: 重大更新
2024-03-06: 初始版本
持续更新中,欢迎收藏! 🚀
多查看文档
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 | CREATE TABLE `hotel_info_tab` ( |
1 | JSON 数据类型提供了数据格式验证和以及一些内置函数帮助查询和检索。 |
1 | `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP, |
虽然NULL有其合理的用途,例如表示缺失的数据或未知的值,但过度使用NULL可能会导致代码的复杂性增加、查询的不准确性和性能问题。在设计数据库模式和数据模型时,需要根据实际需求和业务逻辑合理使用NULL,并考虑到其带来的潜在问题。
Setting the Storage Engine
MySQL支持多种存储引擎,每种存储引擎都有其特点和适用场景。以下是几种常见的MySQL存储引擎对比:
InnoDB:
MyISAM:
mysql存储引擎是插件式的,支持多种存储引擎,比较常用的是innodb和myisam
存储结构上的不同:innodb数据和索引时集中存储的,myism数据和索引是分开存储的
数据插入顺序不同:innodb插入记录时是按照主键大小有序插入,myism插入数据时是按照插入顺序保存的
事务的支持:Innodb提供了对数据库ACID事务的支持,并且还提供了行级锁和外键的约束。MyIASM引擎不提供事务的支持,支持表级锁,不支持行级锁和外键。
索引的不同:innodb主键索引是聚簇索引,非主键索引是非聚簇索引,myisam是非聚簇索引。聚簇索引的叶子节点就是数据节点,而myism索引的叶子节点仍然是索引节点,只不过是指向对应数据块的指针,InnoDB的非聚簇索引叶子节点存储的是主键,需要再寻址一次才能得到数据
总结:
是否需要支持事务?innodb
并发写是不是很多?innoda
读多,写少,追求读速度?myisam
在MySQL中,隐式类型转换是指在表达式或操作中自动将一个数据类型转换为另一个数据类型。MySQL会根据一组规则来执行隐式类型转换,以便执行操作或比较不同类型的数据。以下是MySQL中的一些常见的隐式类型转换规则:
在MySQL中进行字段类型修改、增加字段、增加索引和删除索引时,需要注意以下事项:
关系型数据库扩展包括许多技术:主从复制、主主复制、联合、分片、非规范化和 SQL调优。
两个主库都负责读操作和写操作,写入操作时互相协调。如果其中一个主库挂机,系统可以继续读取和写入。
优缺点:
联合(或按功能划分)将数据库按对应功能分割。例如,你可以有三个数据库:论坛、用户和产品,而不仅是一个单体数据库,从而减少每个数据库的读取和写入流量,减少复制延迟。较小的数据库意味着更多适合放入内存的数据,进而意味着更高的缓存命中几率。没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。
分片将数据分配在不同的数据库上,使得每个数据库仅管理整个数据集的一个子集。以用户数据库为例,随着用户数量的增加,越来越多的分片会被添加到集群中。
类似联合的优点,分片可以减少读取和写入流量,减少复制并提高缓存命中率。也减少了索引,通常意味着查询更快,性能更好。如果一个分片出问题,其他的仍能运行,你可以使用某种形式的冗余来防止数据丢失。类似联合,没有只能串行写入的中心化主库,你可以并行写入,提高负载能力。
常见的做法是用户姓氏的首字母或者用户的地理位置来分隔用户表。
原文链接:https://juejin.cn/post/6844903872134135816
案例1. 酒店分表:
案例2. 订单分表和历史订单归档(3个月或者更长时间)
案例3. 数据历史版本记录、快照表
案例4. 商品库存扣减方案
1 | 普通索引(INDEX):最基本的索引,没有任何限制 |
1 | - 主键索引和普通索引。数据和主键索引用B+Tree来组织的,没有主键innodb会生成唯一列,类似于rowid。InnoDB非主键索引的叶子节点存储的是主键 |
1 | 1. 在主键索引 B+树中查找 id=25 |
1 | CREATE TABLE user ( |
1 | 非叶子节点(索引页) |
1 | -- 基础用法 |
1 | -- 精确匹配:只锁定符合条件的行 |
1 | // ❌ 容易死锁的写法 |
1 | -- ❌ 危险:如果product_sku没有索引,会锁整张表 |
1 |
|
1 | -- 查看当前锁等待情况 |
检查 affected_rows
使用场景:
1 | -- 表结构 |
1 | // ❌ 错误:没有检查影响行数 |
1 | // 场景: version从5变到6再变回5 |
使用乐观锁和悲观锁的选择取决于应用场景和需求:悲观锁适合在并发冲突频繁的情况下,通过独占资源避免并发问题,但会对系统性能产生一定的影响。乐观锁适合在并发冲突较少的情况下,通过乐观的并发控制机制提高系统性能,但需要处理冲突的情况。在实际使用时,需要根据具体业务场景和需求选择适当的并发控制机制,并注意处理冲突和回滚事务的策略,以确保数据的一致性和完整性。
死锁问题,如何避免死锁
分布式事务
1 | ┌─────────────────────────────────────────────────────────────────┐ |
1 | ┌──────────────┬─────────────────┬──────────────────┬────────────────┐ |
where:当where条件有index时,innodb会根据index过滤,否则返回全表数据由server过滤
排序:server。order by
分页:server层。
1 | SELECT * FROM employees |
1 | 方案1: |
1 | ┌─────────────────────────────────────────────────────────────────┐ |
1 |
|
┌─────────────────────────────────────────────────────────────────┐
│ Undo Log vs Redo Log │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 维度 │ Undo Log │ Redo Log │
│ ─────────────────────────────────────────────────────────────│
│ 作用 │ 回滚 + MVCC │ 崩溃恢复 │
│ 记录内容 │ 逻辑日志(旧值) │ 物理日志(新值) │
│ 保证特性 │ 原子性(A) + 隔离性(I) │ 持久性(D) │
│ 写入时机 │ 事务开始时 │ 数据修改时 │
│ 存储位置 │ 系统表空间/独立表空间 │ 独立日志文件 │
│ 生命周期 │ 事务提交后可清理 │ 覆盖式循环使用 │
│ 大小限制 │ 动态增长 │ 固定大小循环 │
│ 用途 │ 事务回滚、一致性读 │ 数据恢复、持久化 │
│ │
└─────────────────────────────────────────────────────────────────┘
优化的步骤
架构优化
连接池的配置和使用
1 | 最大空闲连接数 =(QPS*请求平均耗时)/ 应用节点个数 |
慢sql优化
1 | explain select * from test_xxxx_tab txt order by id limit 10000,10; |
1 | # Time: 2022-05-10T10:15:32.123456Z |
index优化
使用缓存优化DB需要考虑的问题
库表优化/分表/分库
架构优化读写分离优化
核心监控告警指标
关键配置查看
1 | show global variables; |
存储空间information_schema
1 | -- desc information_schema.tables; |
1 | update table set extinfo='{ |
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 | UPDATE employees |
在 oCPM(Optimized Cost Per Mille)智能出价场景中,广告系统需要根据实时转化效果动态调整出价。核心公式为:
1 | eCPM = Target_CPA × pCTR × pCVR × 校准因子 × 成本控制因子 × 1000 |
其中:
calibration = 实际CVR / 预估CVR| 挑战 | 说明 |
|---|---|
| 时效性 | 数据越新,调价越精准;但实时处理成本高 |
| 准确性 | 需要归因服务确定转化归属,反作弊过滤虚假转化 |
| 稳定性 | 高 QPS 场景下,数据系统必须高可用 |
| 多窗口 | 需要同时维护 2h/24h/7d 等多个时间窗口的指标 |
将数据上线时间从 10 分钟级提升到 1 分钟级,提升调价引擎的响应速度和准确性。
1 | ┌─────────────────────────────────────────────────────────────────────────────┐ |
核心问题:一个转化应该归功于哪次广告曝光/点击?
常见归因模型:
核心问题:过滤虚假转化,防止脏数据污染调价
检测维度:
| 指标 | 说明 | 来源 |
|---|---|---|
| Impressions | 有效曝光数 | 曝光日志 |
| Clicks | 有效点击数 | 点击日志 |
| Conversions | 有效转化数 | 转化日志(归因后) |
| Cost | 实际消耗 | 计费系统 |
| CTR | 点击率 = Click/Impression | 计算 |
| CVR | 转化率 = Conversion/Click | 计算 |
| CPA | 转化成本 = Cost/Conversion | 计算 |
| 指标 | 说明 | 采集点 |
|---|---|---|
| Σ pCTR | 每次曝光时的 pCTR 累加 | 曝光时记录 |
| Σ pCVR | 每次曝光时的 pCVR 累加 | 曝光时记录 |
| Σ pCVR|click | 每次点击时的 pCVR 累加 | 点击时记录 |
1 | CTR 校准因子 = 实际点击数 / Σ pCTR |
| 窗口 | 样本量 | 时效性 | 适用场景 |
|---|---|---|---|
| 2h | 少 | 高 | 捕捉突发变化(素材衰退、竞争加剧) |
| 24h | 中 | 中 | 日常调价主力,平衡稳定性和响应速度 |
| 7d/10d | 多 | 低 | 提供基准线,防止短期波动误判 |
1 | func GetCalibration(adID string) *CalibrationData { |
方案1:样本量加权
1 | weightedCVR = (cvr_2h × clicks_2h + cvr_24h × clicks_24h × 0.8 + cvr_7d × clicks_7d × 0.5) |
方案2:置信度阈值切换
不同时间窗口对时效性要求不同,可以采用分层并行计算,平衡计算成本和数据新鲜度:
1 | ┌─────────────────────────────────────────────────────────────────────────────┐ |
| 优势 | 说明 |
|---|---|
| 资源高效 | 只有短窗口需要实时计算,长窗口用离线资源 |
| 成本优化 | 实时计算资源贵,离线资源便宜 |
| 并行处理 | 三层独立运行,互不阻塞 |
| 容错隔离 | 实时层故障不影响离线层,反之亦然 |
1 | // 分层数据源 |
1 | # 实时层 - Flink Job (常驻) |
1 | 实时数据流 |
| 对比项 | V2 全内存方案 | 分层计算方案 |
|---|---|---|
| 2h 窗口 | 内存计算 | 内存计算 |
| 24h 窗口 | 内存计算 | 离线小时级 |
| 7d 窗口 | 内存计算 | 离线天级 |
| 内存压力 | 高 (存全量) | 低 (只存短窗口) |
| 计算成本 | 高 | 低 |
| 延迟 | 全部分钟级 | 2h分钟级/24h小时级/7d天级 |
| 适用场景 | 全窗口高时效性要求 | 短窗口高时效,长窗口可接受延迟 |
选择建议:如果业务对 24h/7d 窗口的时效性要求不高(小时/天级延迟可接受),分层计算方案更经济;如果全部窗口都需要分钟级时效,则选择 V2 全内存方案。
1 | ┌─────────────────────────────────────────────────────────────────────────────┐ |
| 组件 | 职责 | 延迟 |
|---|---|---|
| SparkStreaming | 处理 Tdbank 3路数据流,聚合到 aid 维度,增量写入 TPG | 1 min |
| TPG | 存储 oCPM 广告全量明细数据 | - |
| AdProfile | 每分钟执行多个 SQL,生成多时间窗口指标 | 8 min |
| SnsMixer | 缓存 AdProfile 数据,响应出价引擎查询 | 5 min TTL |
1 | // 串行执行多个 SQL |
| 阶段 | 延迟 | 原因 |
|---|---|---|
| SparkStreaming | 1 min | 微批处理 batch interval |
| AdProfile SQL | 8 min | 串行执行,全量扫描 TPG |
| SnsMixer 缓存 | 0-5 min | TTL 导致数据滞后 |
| 总计 | 10-14 min | - |
把”查询时聚合”变成”写入时聚合”,用空间换时间
1 | ┌─────────────────────────────────────────────────────────────────────────────┐ |
1 | // 读取配置 |
1 | val map // 存储增量数据(内存) |
1 | val map // 存储全量数据(内存) |
| 优化点 | V1 | V2 | 效果 |
|---|---|---|---|
| 数据存储 | TPG (磁盘) | 内存 map | 查询:秒级 → 毫秒级 |
| 计算方式 | 每次 SQL 全量扫描 | 增量 merge + 后台计算 | 8分钟 → 20秒 |
| 指标更新 | 串行阻塞 | 后台线程异步 | 非阻塞 |
| 缓存层 | 5min TTL | 无需额外缓存 | 减少 0-5min |
| 公司 | 技术栈 | 延迟 |
|---|---|---|
| Meta | Scribe → Spark/Flink → TAO | 秒级 |
| Pub/Sub → Dataflow → Bigtable | 秒级 | |
| 字节 | BMQ → Flink → Abase | 秒级 |
| 腾讯广告 V1 | Tdbank → Spark → TPG → AdProfile | 10分钟级 |
| 腾讯广告 V2 | Tdbank → Spark + 内存计算 | 1分钟级 |
| 架构 | 特点 | 适用场景 |
|---|---|---|
| Lambda | 批流分离,离线修正实时 | 数据一致性要求高 |
| Kappa | 纯流处理,Kafka 作为 source of truth | 追求低延迟 |
| V2 方案 | 类 Lambda,实时计算 + 离线对账 | 平衡延迟和一致性 |
| 指标 | V1 | V2 | 提升 |
|---|---|---|---|
| 数据上线延迟 | 10-14 分钟 | < 1 分钟 | 10x |
| 指标计算频率 | 1 分钟/次 | 20 秒/次 | 3x |
| 查询响应时间 | 秒级 | 毫秒级 | 1000x |
通过将”查询时聚合”变成”写入时聚合”,在内存中维护全量数据并增量更新,将数据上线延迟从 10 分钟优化到 1 分钟。
Q: 内存不够怎么办?
A: 按广告 ID 分片,每个节点只存部分数据;冷数据落盘或淘汰。
Q: 如何保证数据一致性?
A: checkpoint 持久化 + 离线对账修复。
Q: 时间窗口怎么选择?
A: 优先短窗口(时效性高),样本不足时回退到长窗口;或样本量加权融合。
Q: 校准因子的作用?
A: 修正模型预估偏差。模型预估 CVR=3%,实际 CVR=2.4%,校准因子=0.8,压低出价。
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.
1 | defer func() { |
channel是golang中的csp并发模型非常重要组成部分,使用起来非常像阻塞队列。
1 | func main() { |
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.
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内存管理基本继承了tcmolloc成熟的架构,因此也符合内存管理的基本策略。



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.
golang支持垃圾回收,gc能减少编程的负担,但与此同时也可能造成程序的性能问题。那么如何测量golang程序使用的内存,以及如何减少golang gc的负担呢?经历了许多版本的迭代,golang gc 沿着低延迟和高吞吐的目标在进化,相比早起版本,目前有了很大的改善,但仍然有可能是程序的瓶颈。因此要学会分析golang 程序的内存和垃圾回收问题。
如何查看程序的gc信息?
tips:
golang是近几年出现的带有垃圾回收的现代语言,其垃圾回收算法自然也相互借鉴。因此在学习golang gc之前有必要了解目前主流的垃圾回收方法。
golang gc是使用三色标记清理法,为了对用户对象进行标记需要将用户程序所有线程全部冻结(STW),当程序中包含很多对象时,暂停时间会很长,用户逻辑对用户的反应就会中止。那么如何缩短这个过程呢?一种自然的想法,在三色标记法扫描之后,只会存在黑色和白色两种对象,黑色是程序正在使用的对象不可回收,白色对象是此时不会被程序的对象,也是gc的要清理的对象。那么回收白色对象肯定不会和用户程序造成竞争冲突,因此回收操作和用户程序是可以并发的,这样可以缩短STW的时间。
写屏障使得扫描操作和回收操作都可以和用户程序并发。我们试想一下,刚把一个对象标记为白色,用户程序突然又引用了它,这种扫描操作就比较麻烦,于是引入了屏障技术。内存扫描和用户逻辑也可以并发执行,用户新建的对象认为是黑色的,已经扫描过的对象有可能因为用户逻辑造成对象状态发生改变。所以**对扫描过后的对象使用操作系统写屏障功能用来监控用户逻辑这段内存,一旦这段内存发生变化写屏障会发生一个信号,gc捕获到这个信号会重新扫描改对象,查看它的引用或者被引用是否发生改变,从而判断该对象是否应该被清理。因此通过写屏障技术,是的扫描操作也可以合用户程序并发执行。
gc控制器:gc算法并不万能的,针对不同的场景可能需要适当的设置。例如大数据密集计算可能不在乎内存使用量,甚至可以将gc关闭。golang 通过百分比来控制gc触发的时机,设置的百分比指的是程序新分配的内存与上一次gc之后剩余的内存量,例如上次gc之后程序占有2MB,那么下一次gc触发的时机是程序又新分配了2MB的内存。我们可以通过SetGCPercent函数动态设置,默认值为100,当百分比设置为负数时例如-1,表明关闭gc。
gc 是golang程序性能优化非常重要的一部分,建议依照下面两个实例实践golang程序优化。
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.
1 | package main |
1 | package main |
1 | package main |
1 | package main |
1 | package main |
最近在项目开发中使用http服务与第三方服务交互,感觉golang的http封装得很好,很方便使用但是也有一些坑需要注意,一是自动复用连接,二是Response.Body的读取和关闭
首先用代码直观的体验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的自动复用机制。
将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是最佳选择。
https://golang.org/pkg/sync/
sync.Pool的使用非常简单,它具有以下几个特点:
sync.Pool的使用非常简单,定义一个Pool对象池时,需要提供一个New函数,表示当池中没有对象时,如何生成对象。对象池Pool提供Get和Put函数从Pool中取和存放对象。
下面有一个简单的实例,直接运行是会打印两次“new an object”,注释掉runtime.GC(),发现只会调用一次New函数,表示实现了对象重用。
1 | package main |
sync.Pool支持多协程共享,为了尽量减少竞争和加锁的操作,golang在设计的时候为每个P(核)都分配了一个子池,每个子池包含一个私有对象和共享列表。 私有对象只有对应的和核P能够访问,而共享列表是与其它P共享的。
在golang的GMP调度模型中,我们知道协程G最终会被调度到某个固定的核P上。当一个协程在执行Pool的get或者put方法时,首先对改核P上的子池进行操作,然后对其它核的子池进行操作。因为一个P同一时间只能执行一个goroutine,所以对私有对象存取操作是不需要加锁的,而共享列表是和其他P分享的,因此需要加锁操作。
一个协程希望从某个Pool中获取对象,它包含以下几个步骤:
在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并不是适合作为连接池,主要有以下两个原因:
golang中连接池通常利用channel的缓存特性实现。当需要连接时,从channel中获取,如果池中没有连接时,将阻塞或者新建连接,新建连接的数量不能超过某个限制。
https://github.com/goctx/generic-pool基于channel提供了一个通用连接池的实现
1 | package pool |
1 | package main |
引用类型声明而没有初始化赋值时,其值为nil。golang需要经常判断nil,防止出现panic错误。
1 | bool -> false |
golang在内存分配的时候没有堆(heap)和栈(stack)的区别,由编译器决定是否需要将对象逃逸到堆中。例如:
1 | func Sum() int { |
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
了解C/C++的应该知道内敛,golang编译器同样支持函数内敛,对于较短且重复调用的函数可以考虑使用内敛
编译器会将代码中一些无用的分支进行优化,分支判断,提高效率。例如下面一段代码由于a和b是常量,编译器也可以推导出Max(a,b),因此最终F函数为空
1 | func Max(a, b int) int { |
常用的编译器选项: go build -gcflags=”-lN” xxx.go
为了避开直接通过系统调用分配内存而导致的性能开销,通常会通过预分配、内存池等操作自主管理内存。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
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:
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 | CALL runtime·args(SB) |
如上图所示,Go程序启动大致分为一下一个部分:
schedinit内容比较多,主要包含:
stackinit() 核心代码用于初始化全局的stackpool和stackLarge两个结构
1 | var stackpool [_NumStackOrders]struct { |
1 | if gp.stack.lo == 0 { |
goroutine 运行时需要把stack 地址传给m
Golang非常注重工程化,提供了非常好用单元测试、性能测试(benchmark)和调优工具(pprof),它们对提高代码的质量和服务的性能非常有帮助。参考链接中通过一段http代码非常详细的介绍了golang程序优化的步骤和方便之处。实际工作中,我们很难每次都对代码都有那么高的要求,但是能使用一些工具对程序进行优化程序性能也是golang程序员必备的技能。
dave它通过几个case非常清晰的介绍了golang性能分析与优化的技术,非常值得学习。https://dave.cheney.net/high-performance-go-workshop/dotgo-paris.html。
为了测试某个文件中的某个函数的性能,在相同目录下定义xxx_test.go文件,使用go build命令编译程序时会忽略测试文件
在测试文件中定义测试某函数的代码,以TestXxxx方式命名,例如TestAdd
在相同目录下运行 go test -v 即可观察代码的测试结果
func TestAdd(t *testing.T) {
if add(1, 3) != 4 {
t.FailNow()
}
}
1 | (pprof) top5 |
1 | package main |
1 | package main |
1 | package main |
https://refactoringguru.cn/design-patterns/chain-of-responsibility/go/example
const int xfile 检查so或者可执行文件的架构
1 | $ file _visp.so |
ldd -r _visp.so 命令查看so库链接状态和错误信息
1 | undefined symbol: __itt_api_version_ptr__3_0 (./_visp.so) |
c++filt symbol 定位错误在那个C++文件中
1 | base) terse@ubuntu:~/code/terse-visp$ c++filt __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 | $ nm _visp.so | grep __itt_domain_create_ptr__3_0 |
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到系统目录中,因此在没有显式指定搜索路径时也可以顺利找到。
现象:
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