电商系统设计系列 (篇次与(一)推荐阅读顺序 一致)
电商系统设计:商品中心系统 商品中心是电商平台的「商品库」,负责商品全生命周期管理。本文将深入探讨商品系统的设计与实现,重点讲解 SPU/SKU 模型、异构商品治理、多级缓存三大核心技术,并通过标准实物商品、虚拟商品、服务商品、组合商品四个黄金案例,展示如何设计可扩展的商品系统。
本文既适合系统设计面试准备,也适合工程实践参考。
目录
1. 系统概览 1.1 业务场景 商品中心是电商平台的「商品库」,负责商品全生命周期管理。
核心职责:
商品信息管理(PIM) :SPU/SKU、属性、类目、图片、描述
商品上架流程 :商家上传、供应商同步、运营管理
商品导购服务 :搜索、详情、列表、筛选
商品快照生成 :为订单提供不可变的商品信息
库存协同 :与库存系统实时交互
价格协同 :为计价中心提供基础价格
业务模式:
B2B2C 模式 (约 70%~80%):供应商商品,平台运营(机票、酒店、充值等)
B2C 模式 (约 20%~30%):平台自营商品(礼品卡、券类等)
商品系统的职责边界:
负责 :商品数据管理、上架审核、搜索与缓存、快照生成
不负责 :具体库存扣减逻辑(由库存系统负责)、最终售价计算(由计价中心负责)
与其他系统的交互:
订单系统 :获取商品详情、库存校验、创建订单快照
库存系统 :实时库存查询、库存扣减与回补
计价中心 :提供基础价格、类目信息
营销系统 :提供商品标签、圈品规则
搜索系统 :同步商品索引
1.2 核心挑战 1. 异构商品
实物商品:多规格 SKU 组合(服装、3C)
虚拟商品:无 SKU 或简单 SKU(充值卡、会员)
服务商品:时间维度库存(酒店、机票)
组合商品:多 SKU 组合(套餐)
2. 多角色上架
商家上传:Portal/App,人工审核,限流防刷
供应商同步:Push/Pull,自动审核,幂等设计
运营管理:后台上传,免审核或轻审核,批量处理
3. 高并发读
商品详情页:QPS 可达万级
商品列表页:QPS 可达千级
多级缓存:L1 本地缓存 + L2 Redis + L3 数据库,配合 CDN
4. 数据一致性
商品变更后:缓存失效、搜索索引更新、下游感知版本
最终一致性:Kafka 事件、CDC
补偿机制:定时对账、修复任务
5. 扩展性
新品类快速接入:适配器模式、配置化平台
尽量少改核心链路:开闭原则、策略模式
1.3 系统架构 商品系统在平台中承接上架写入与导购读取,经网关统一接入,核心能力按领域拆分为多个服务,并通过消息队列与订单、库存等系统解耦。
核心模块:
商品信息服务 :SPU/SKU CRUD、版本管理、属性管理
类目属性服务 :类目树、动态属性、品牌管理
上架审核服务 :多角色上架、状态机、审核流
搜索服务 :Elasticsearch 索引、多维筛选、排序
缓存服务 :多级缓存(L1/L2)、智能刷新、缓存预热
快照服务 :商品快照生成、Hash 复用、订单引用
同步服务 :供应商数据同步、全量/增量、失败重试
技术栈:
数据库:MySQL(分库分表,例如按 SPU 哈希 16 张表)、MongoDB(ExtInfo)
缓存:Redis、本地缓存(Caffeine 等)
搜索:Elasticsearch 7.x
消息队列:Kafka(变更事件、CDC)
对象存储:OSS(图片/视频)
监控:Prometheus + Grafana
系统架构图 graph TB
subgraph 客户端层
A1[商家 Portal]
A2[运营后台]
A3[前端商城]
end
subgraph 接入层
B[商品 API Gateway]
end
subgraph 服务层
C1[商品信息服务]
C2[上架审核服务]
C3[搜索服务]
C4[缓存服务]
C5[快照服务]
C6[同步服务]
end
subgraph 数据层
D1[MySQL 分库分表]
D2[Redis 多级缓存]
D3[Elasticsearch]
D4[MongoDB ExtInfo]
D5[OSS 图片存储]
end
subgraph 消息队列
E[Kafka]
end
subgraph 外部系统
F1[供应商系统]
F2[订单系统]
F3[库存系统]
end
A1 --> B
A2 --> B
A3 --> B
B --> C1
B --> C2
B --> C3
C1 --> C4
C1 --> C5
C3 --> D3
C4 --> D2
C1 --> D1
C1 --> D4
C1 --> D5
C6 --> F1
C1 --> E
E --> F2
E --> F3
style C1 fill:#e1f5ff
style D1 fill:#ffe1e1
style D2 fill:#ffe1e1
style E fill:#e1ffe1
1.4 数据模型概览 核心表(逻辑名):
spu_tab:商品主信息(SPU)
sku_tab:SKU 信息
category_tab:类目
attribute_tab:属性定义
product_attribute_tab:商品属性值(EAV)
product_ext_tab 或 MongoDB 集合:扩展信息
product_snapshot_tab:商品快照
product_audit_tab:审核记录
product_log_tab:变更日志
ER 图 erDiagram
SPU ||--o{ SKU : contains
SPU ||--o| CATEGORY : belongs_to
SPU ||--o{ PRODUCT_ATTRIBUTE : has
SPU ||--|| PRODUCT_EXT : extends
SPU ||--o{ PRODUCT_SNAPSHOT : generates
SPU ||--o{ PRODUCT_LOG : tracks
ATTRIBUTE ||--o{ PRODUCT_ATTRIBUTE : defines
SPU {
string spu_id PK
string title
int64 category_id FK
int64 brand_id
int status
int64 version
timestamp created_at
}
SKU {
string sku_id PK
string spu_id FK
string spec_values
int64 price
int stock
int status
}
CATEGORY {
int64 category_id PK
int64 parent_id
string name
int level
}
ATTRIBUTE {
int64 attribute_id PK
string attribute_name
string attribute_type
}
PRODUCT_ATTRIBUTE {
int64 id PK
string spu_id FK
int64 attribute_id FK
string value
}
PRODUCT_SNAPSHOT {
string snapshot_id PK
string spu_id FK
string sku_id FK
string content
}
2. 商品创建和上架流程 商品上架需要区分三种角色:商家(Merchant)、供应商(Partner)、运营(Ops)。不同角色的入口、审核策略与幂等要求不同,但底层都落在统一的 SPU/SKU 模型与状态机上。
2.1 商家上传(Merchant) 商家通过 Portal 或 App 上传商品,通常需要人工审核,并配合限流与风控,降低虚假商品与刷单风险。
业务流程:
商家在 Portal/App 填写商品信息
提交后进入「待审核」状态
审核通过后才能上架
需要人工审核(防止虚假商品)
技术要点:
表单验证(前后端双重校验)
限流(防止恶意刷单)
审核队列(异步处理)
审核历史可追溯
流程图:
sequenceDiagram
participant M as 商家
participant API as 商品 API
participant DB as 数据库
participant MQ as Kafka
participant Audit as 审核服务
M->>API: 创建商品
API->>API: 身份验证
API->>API: 限流检查
API->>DB: 保存商品(草稿)
API->>MQ: 发布审核事件
API-->>M: 返回商品 ID
MQ->>Audit: 消费审核事件
Audit->>Audit: 人工审核
Audit->>DB: 更新状态
Audit->>MQ: 发布审核结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 func MerchantCreateProduct (ctx context.Context, req *MerchantProductRequest) (*Product, error ) { merchant, err := ValidateMerchant(ctx, req.MerchantID) if err != nil { return nil , ErrUnauthorized } limiterKey := fmt.Sprintf("merchant_create:%d" , req.MerchantID) if !rateLimiter.Allow(limiterKey, 10 , time.Minute) { return nil , ErrRateLimitExceeded } if err := ValidateProductRequest(req); err != nil { return nil , err } product := &Product{ SPUID: GenerateSPUID(), Title: req.Title, CategoryID: req.CategoryID, Status: ProductStatusDraft, Source: SourceMerchant, MerchantID: req.MerchantID, Version: 1 , CreatedAt: time.Now(), } if err := db.InsertProduct(ctx, product); err != nil { return nil , err } audit := &AuditTask{ TaskID: GenerateAuditID(), ProductID: product.SPUID, Type: AuditTypeMerchant, Priority: AuditPriorityNormal, Status: AuditStatusPending, } if err := db.InsertAuditTask(ctx, audit); err != nil { return nil , err } event := &AuditEvent{ TaskID: audit.TaskID, ProductID: product.SPUID, EventType: "audit.created" , } PublishAuditEvent(ctx, event) RecordProductLog(ctx, product.SPUID, "商家创建商品" , merchant.Name) return product, nil } func HandleAudit (ctx context.Context, event *AuditEvent) error { task, err := db.GetAuditTask(ctx, event.TaskID) if err != nil { return err } product, _ := db.GetProduct(ctx, task.ProductID) var approved bool if ContainsSensitiveWords(product.Title) { approved = false } else { approved = true } if approved { task.Status = AuditStatusApproved product.Status = ProductStatusApproved } else { task.Status = AuditStatusRejected product.Status = ProductStatusRejected } db.UpdateAuditTask(ctx, task) db.UpdateProduct(ctx, product) resultEvent := &AuditResultEvent{ ProductID: product.SPUID, Approved: approved, } PublishAuditResultEvent(ctx, resultEvent) return nil }
2.2 供应商同步(Partner) 供应商侧数据可通过 Push (供应商推送到平台 MQ)或 Pull (平台定时拉取)进入商品中心。自动审核可走快速通道,同时必须用幂等与版本控制避免重复写入与乱序覆盖。
技术要点:
幂等性(重复推送去重)
字段映射(供应商模型 → 平台模型)
同步监控与告警
热门商品可配合更积极的缓存刷新策略(见第 5 章)
Push 模式流程图:
sequenceDiagram
participant P as 供应商
participant MQ as Kafka
participant Sync as 同步服务
participant DB as 数据库
P->>MQ: 推送商品数据
MQ->>Sync: 消费消息
Sync->>Sync: 幂等性检查
Sync->>Sync: 数据映射
Sync->>Sync: 自动审核
Sync->>DB: Upsert 商品
Sync->>MQ: 发布变更事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 func PartnerPushProduct (ctx context.Context, msg *PartnerProductMessage) error { idempotentKey := fmt.Sprintf("partner_push:%s:%s" , msg.PartnerID, msg.ProductID) record := &IdempotentRecord{ Key: idempotentKey, Status: IdempotentProcessing, ExpireAt: time.Now().Add(10 * time.Minute), } if err := db.InsertIdempotentRecord(ctx, record); err != nil { return nil } product := MapPartnerProduct(msg) product.Source = SourcePartner product.PartnerID = msg.PartnerID if AutoAudit(product) { product.Status = ProductStatusOnline } else { product.Status = ProductStatusPendingAudit } existing, _ := db.GetProductByPartnerID(ctx, msg.PartnerID, msg.ProductID) if existing != nil { product.SPUID = existing.SPUID product.Version = existing.Version + 1 if err := db.UpdateProductWithVersion(ctx, product, existing.Version); err != nil { return err } } else { product.SPUID = GenerateSPUID() product.Version = 1 if err := db.InsertProduct(ctx, product); err != nil { return err } } changed := &ProductChangedEvent{ SPUID: product.SPUID, ChangeType: "partner_sync" , } PublishProductChangedEvent(ctx, changed) db.UpdateIdempotentStatus(ctx, idempotentKey, IdempotentSuccess) return nil } func PartnerPullProducts (ctx context.Context, partnerID string ) error { lastSyncTime := GetLastSyncTime(partnerID) products, err := partnerClient.GetProducts(partnerID, lastSyncTime) if err != nil { return err } for _, p := range products { msg := ConvertToMessage(p) if err := PartnerPushProduct(ctx, msg); err != nil { log.Error("failed to sync product" , "partnerID" , partnerID, "productID" , p.ID, "error" , err) continue } } UpdateLastSyncTime(partnerID, time.Now()) return nil } func AutoAudit (product *Product) bool { if ContainsSensitiveWords(product.Title) { return false } if product.Price < 0 || product.Price > 1000000 { return false } if !CategoryExists(product.CategoryID) { return false } if product.Title == "" || product.CategoryID == 0 { return false } return true }
2.3 运营上传(Ops) 运营在后台可单品录入或批量导入(如 Excel)。通常免人工审核或仅做抽检,要求批量任务可观测、单行失败可定位。
技术要点:
Excel 流式解析,控制内存
行级校验与错误汇总
写库与发事件的一致策略(必要时按批次事务)
操作审计日志
批量上传流程图:
sequenceDiagram
participant Ops as 运营
participant API as 商品 API
participant Parser as Excel 解析器
participant DB as 数据库
participant MQ as Kafka
Ops->>API: 上传 Excel 文件
API->>Parser: 流式解析
loop 每行数据
Parser->>Parser: 验证数据
Parser->>DB: 保存商品
Parser->>MQ: 发布事件
end
API-->>Ops: 返回结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 func OpsBatchUpload (ctx context.Context, file *ExcelFile) (*UploadResult, error ) { result := &UploadResult{ Success: []string {}, Failed: []UploadError{}, } parser := NewExcelParser(file) rowNum := 0 for row := range parser.Parse() { rowNum++ product, err := ValidateRow(row) if err != nil { result.Failed = append (result.Failed, UploadError{Row: rowNum, Error: err.Error()}) continue } product.SPUID = GenerateSPUID() product.Status = ProductStatusOnline product.Source = SourceOps product.Version = 1 if err := db.InsertProduct(ctx, product); err != nil { result.Failed = append (result.Failed, UploadError{Row: rowNum, Error: err.Error()}) continue } evt := &ProductCreatedEvent{SPUID: product.SPUID} PublishProductCreatedEvent(ctx, evt) result.Success = append (result.Success, product.SPUID) } RecordBatchUploadLog(ctx, result) return result, nil } type ExcelParser struct { file *ExcelFile } func (p *ExcelParser) Parse() <-chan *ExcelRow { ch := make (chan *ExcelRow, 100 ) go func () { defer close (ch) f, err := excelize.OpenFile(p.file.Path) if err != nil { return } defer f.Close() rows, _ := f.GetRows("Sheet1" ) for i, row := range rows { if i == 0 { continue } ch <- &ExcelRow{RowNum: i + 1 , Data: row} } }() return ch }
2.4 上架状态机与审核策略 商品状态驱动上架生命周期:草稿 → 待审核 → 通过/拒绝 → 上线/下线。转换规则应集中配置,写库时使用乐观锁或状态条件更新,避免并发覆盖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 const ( ProductStatusDraft = 0 ProductStatusPendingAudit = 1 ProductStatusApproved = 2 ProductStatusRejected = 3 ProductStatusOnline = 4 ProductStatusOffline = 5 ) var allowedTransitions = map [int ]map [int ]bool { ProductStatusDraft: {ProductStatusPendingAudit: true }, ProductStatusPendingAudit: { ProductStatusApproved: true , ProductStatusRejected: true , }, ProductStatusApproved: {ProductStatusOnline: true }, ProductStatusOnline: {ProductStatusOffline: true }, ProductStatusOffline: {ProductStatusOnline: true }, } func TransitionProductStatus (ctx context.Context, spuID string , from, to int ) error { if !allowedTransitions[from][to] { return ErrIllegalTransition } product, err := db.GetProduct(ctx, spuID) if err != nil { return err } if product.Status != from { return ErrStatusMismatch } if err := db.UpdateProductStatus(ctx, spuID, to, product.Version); err != nil { return err } RecordProductLog(ctx, spuID, fmt.Sprintf("状态变更: %d -> %d" , from, to), "system" ) return nil }
stateDiagram-v2
[*] --> 草稿
草稿 --> 待审核: 提交审核
待审核 --> 已拒绝: 审核不通过
待审核 --> 已通过: 审核通过
已通过 --> 已上线: 上架
已上线 --> 已下线: 下架
已下线 --> 已上线: 重新上架
已拒绝 --> [*]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 type AuditStrategy interface { ShouldAudit(product *Product) bool GetPriority() int } type MerchantAuditStrategy struct {}func (s *MerchantAuditStrategy) ShouldAudit(product *Product) bool { return product.Source == SourceMerchant } func (s *MerchantAuditStrategy) GetPriority() int { return AuditPriorityNormal } type PartnerAuditStrategy struct {}func (s *PartnerAuditStrategy) ShouldAudit(product *Product) bool { return product.Source == SourcePartner && !AutoAudit(product) } func (s *PartnerAuditStrategy) GetPriority() int { return AuditPriorityHigh } type OpsAuditStrategy struct {}func (s *OpsAuditStrategy) ShouldAudit(product *Product) bool { return false } var strategies = []AuditStrategy{ &MerchantAuditStrategy{}, &PartnerAuditStrategy{}, &OpsAuditStrategy{}, } func RouteAuditStrategy (product *Product) AuditStrategy { for _, strategy := range strategies { if strategy.ShouldAudit(product) { return strategy } } return nil }
3. 商品数据模型设计专题 本章从 SPU/SKU、类目属性、动态字段存储到订单快照,串起商品中心最核心的数据面设计。
3.1 SPU/SKU 模型设计 SPU(Standard Product Unit) 描述「卖的是什么」:标题、类目、图文、共用属性。SKU(Stock Keeping Unit) 描述「可售卖的最小单元」:规格组合、价格、可售状态。一笔下单通常指向 SKU,搜索与列表常聚合在 SPU 维度展示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 type SPU struct { SPUID string Title string CategoryID int64 BrandID int64 MainImages []string Description string SpecDefs []SpecDef ProductType string Status int Version int64 } type SpecDef struct { Name string Values []string } type SKU struct { SKUID string SPUID string SpecValues map [string ]string Price int64 Status int }
规格组合(笛卡尔积) :给定多个规格维度及其取值,生成所有合法 SKU。实务中常加规则表剔除无效组合(例如某颜色不提供某尺码)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 func CartesianSKUs (spuID string , defs []SpecDef) []*SKU { if len (defs) == 0 { return []*SKU{{SPUID: spuID, SKUID: GenerateSKUID(spuID, nil ), SpecValues: map [string ]string {}}} } var out []*SKU var dfs func (i int , cur map [string ]string ) dfs = func (i int , cur map [string ]string ) { if i == len (defs) { m := make (map [string ]string , len (cur)) for k, v := range cur { m[k] = v } out = append (out, &SKU{ SPUID: spuID, SKUID: GenerateSKUID(spuID, m), SpecValues: m, }) return } d := defs[i] for _, val := range d.Values { cur[d.Name] = val dfs(i+1 , cur) delete (cur, d.Name) } } dfs(0 , map [string ]string {}) return out }
示例(手机) :颜色 {黑, 白} × 存储 {128G, 256G} → 4 个 SKU;若再 × 版本 {标准, Pro} → 8 个 SKU。服装类目常见「颜色 × 尺码」,SKU 数量更易膨胀,需要规格模板与批量编辑工具。
flowchart LR
SPU[SPU 商品主体] --> SK1[SKU 规格组合 A]
SPU --> SK2[SKU 规格组合 B]
SPU --> SK3[SKU 规格组合 C]
SK1 --> INV[库存单元]
SK2 --> INV
SK3 --> INV
3.2 类目与属性系统 类目树支持前台导航、属性继承与搜索筛选维度配置。叶子类目绑定「销售属性」与「关键属性」,非叶子类目可定义通用属性并由子类目继承或覆盖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 type Category struct { CategoryID int64 ParentID int64 Name string Level int Path string IsLeaf bool } type Attribute struct { AttributeID int64 Name string InputType string Required bool CategoryIDs []int64 } type ProductAttribute struct { SPUID string AttributeID int64 Value string } func BuildCategoryTree (nodes []*Category) map [int64 ][]*Category { children := make (map [int64 ][]*Category) for _, n := range nodes { children[n.ParentID] = append (children[n.ParentID], n) } return children } func IndexCategoriesByID (nodes []*Category) map [int64 ]*Category { m := make (map [int64 ]*Category, len (nodes)) for _, n := range nodes { m[n.CategoryID] = n } return m } func GetCategoryPath (byID map [int64 ]*Category, leafID int64 ) []*Category { var path []*Category cur, ok := byID[leafID] for ok && cur != nil { path = append ([]*Category{cur}, path...) if cur.ParentID == 0 { break } cur, ok = byID[cur.ParentID] } return path }
graph TD
ROOT[根类目] --> A[服饰]
ROOT --> B[数码]
A --> A1[男装]
A --> A2[女装]
B --> B1[手机]
B1 --> B1L[智能手机 叶子]
1 2 3 func ExampleCategoryPath (nodes []*Category, leafID int64 ) []*Category { return GetCategoryPath(IndexCategoriesByID(nodes), leafID) }
3.3 动态属性与 EAV 模型 不同类目字段差异大:服装要「材质、版型」,手机要「CPU、屏幕」。直接用超宽表会导致大量稀疏列;全 EAV 查询与索引压力大。工程上常见混合方案 :高频筛选字段进主表或 JSON 索引,长尾属性走 EAV 或文档库。
方案
优点
缺点
适用
宽表
查询简单、性能好
schema 僵化、稀疏列浪费
属性稳定的标品类
EAV
扩展灵活
多表 Join、索引复杂
长尾属性、运营可配字段
混合
平衡性能与扩展
需要治理与同步策略
大型平台主流选择
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type ProductBase struct { SPUID string Title string CategoryID int64 BrandID int64 PriceMin int64 PriceMax int64 } type ProductExt struct { SPUID string Payload map [string ]interface {} } func LoadProductFull (ctx context.Context, spuID string ) (*SPU, map [int64 ]string , *ProductExt, error ) { spu, err := db.GetSPU(ctx, spuID) if err != nil { return nil , nil , nil , err } eav, _ := db.ListProductAttributes(ctx, spuID) ext, _ := extStore.Get(ctx, spuID) return spu, eav, ext, nil }
3.4 商品快照生成与复用 订单需要固化「下单瞬间」的商品展示信息与价格依据,避免后续改价改图引发纠纷。快照内容建议与计价结果、税费规则版本等一并由订单域引用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import ( "context" "crypto/md5" "encoding/hex" "encoding/json" "fmt" "time" ) type ProductSnapshot struct { SnapshotID string SPUID string SKUID string Title string Price int64 Image string SpecsJSON string AttrsJSON string CreatedAt time.Time } func SnapshotContentKey (spu *SPU, sku *SKU) string { b, _ := json.Marshal(sku.SpecValues) return fmt.Sprintf("%s|%s|%s|%d|%s" , spu.SPUID, sku.SKUID, spu.Title, sku.Price, string (b)) } func FindOrCreateSnapshot (ctx context.Context, spu *SPU, sku *SKU) (string , error ) { key := SnapshotContentKey(spu, sku) sum := md5.Sum([]byte (key)) snapshotID := hex.EncodeToString(sum[:]) if snap, _ := db.GetSnapshot(ctx, snapshotID); snap != nil { return snapshotID, nil } specsJSON, _ := json.Marshal(sku.SpecValues) snap := &ProductSnapshot{ SnapshotID: snapshotID, SPUID: spu.SPUID, SKUID: sku.SKUID, Title: spu.Title, Price: sku.Price, Image: spu.MainImages[0 ], SpecsJSON: string (specsJSON), CreatedAt: time.Now(), } if err := db.InsertSnapshot(ctx, snap); err != nil { return "" , err } return snapshotID, nil }
4. 异构商品治理 实物、虚拟、服务、组合在 SKU 形态、库存维度、价格与履约链路上差异显著。治理目标是:统一生命周期与检索体验 ,又允许品类在扩展点上替换实现。
4.1 异构商品的挑战
维度
标准实物
虚拟商品
服务商品
组合商品
SKU 模型
多规格 SKU
常单 SKU 或无 SKU
房型/时段颗粒度
子 SKU 组合
库存
数量库存
卡密池/核销码
日历房态/座位图
子品库存联合约束
价格
标价 + 促销
面值/折扣规则
日历价、动态溢价
打包价、分摊规则
履约
物流发货
直充/发码
预约/入住/出行
分履约合并展示
核心矛盾:一套核心表与流程 vs 品类特有字段与规则 。解法是把「共性」沉到内核,把「差异」关进扩展点(适配器/策略/配置)。
4.2 统一抽象与适配器模式 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 import "errors" type ProductSearchDoc struct { SPUID string Title string CategoryID int64 ExtraTags []string } type ProductType string const ( ProductTypeStandard ProductType = "standard" ProductTypeVirtual ProductType = "virtual" ProductTypeService ProductType = "service" ProductTypeBundle ProductType = "bundle" ) type ProductAdapter interface { Type() ProductType Validate(spu *SPU, skus []*SKU) error NormalizeForSearch(doc *ProductSearchDoc) error StockDimensions() []string } type StandardProductAdapter struct {}func (a *StandardProductAdapter) Type() ProductType { return ProductTypeStandard }func (a *StandardProductAdapter) Validate(spu *SPU, skus []*SKU) error { if len (skus) == 0 { return errors.New("standard product requires skus" ) } return nil } func (a *StandardProductAdapter) NormalizeForSearch(doc *ProductSearchDoc) error { return nil } func (a *StandardProductAdapter) StockDimensions() []string { return []string {"warehouse_sku" } } type ServiceProductAdapter struct {}func (a *ServiceProductAdapter) Type() ProductType { return ProductTypeService }func (a *ServiceProductAdapter) Validate(spu *SPU, skus []*SKU) error { return nil } func (a *ServiceProductAdapter) NormalizeForSearch(doc *ProductSearchDoc) error { doc.ExtraTags = append (doc.ExtraTags, "service" ) return nil } func (a *ServiceProductAdapter) StockDimensions() []string { return []string {"date" , "room_type" } } var adapterRegistry = map [ProductType]ProductAdapter{ ProductTypeStandard: &StandardProductAdapter{}, ProductTypeService: &ServiceProductAdapter{}, } func RouteAdapter (spu *SPU) ProductAdapter { t := ProductType(spu.ProductType) if a, ok := adapterRegistry[t]; ok { return a } return adapterRegistry[ProductTypeStandard] }
classDiagram
class ProductAdapter {
<>
+Type() ProductType
+Validate()
+NormalizeForSearch()
+StockDimensions()
}
class StandardProductAdapter
class ServiceProductAdapter
ProductAdapter <|.. StandardProductAdapter
ProductAdapter <|.. ServiceProductAdapter
4.3 配置化与低代码平台 将「表单字段、校验规则、上架步骤、审核模板」配置化,新品类主要工作是配置 + 少量插件,而不是复制一套后台。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 type FormField struct { Key string Label string Widget string Required bool Validator string DataSource string } type FormConfig struct { ProductType ProductType Fields []FormField Steps []string } var HotelSPUForm = FormConfig{ ProductType: ProductTypeService, Fields: []FormField{ {Key: "hotel.star" , Label: "星级" , Widget: "select" , Required: true , DataSource: "/meta/stars" }, {Key: "hotel.city" , Label: "城市" , Widget: "city" , Required: true }, {Key: "hotel.address" , Label: "地址" , Widget: "input" , Required: true }, }, Steps: []string {"basic" , "room" , "policy" , "media" }, }
4.4 多维度库存管理 库存可分层:平台库存网关 统一对外,GetStock 根据 ProductType 与维度路由到仓库库存、卡密池、房态服务等。
graph LR
API[下单/详情] --> GW[库存网关]
GW --> W[仓配库存]
GW --> C[卡密池]
GW --> R[房态/日历库存]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 type StockQuery struct { SKUID string Date string Warehouse string } type InventoryGateway struct { warehouse StockProvider cardPool StockProvider roomState StockProvider } type StockProvider interface { Name() string Available(ctx context.Context, q StockQuery) (int64 , error ) } func (g *InventoryGateway) GetStock(ctx context.Context, adapter ProductAdapter, q StockQuery) (int64 , error ) { dims := adapter.StockDimensions() switch { case contains(dims, "warehouse_sku" ): return g.warehouse.Available(ctx, q) case contains(dims, "date" ): return g.roomState.Available(ctx, q) default : return g.cardPool.Available(ctx, q) } } func contains (arr []string , v string ) bool { for _, x := range arr { if x == v { return true } } return false }
5. 商品搜索与多级缓存 导购链路读多写少,典型优化是 Elasticsearch 承担检索与排序 ,Redis + 本地缓存承担热点详情 。写入路径发布变更事件,异步刷新索引与失效缓存,接受短暂最终一致。
5.1 Elasticsearch 索引设计 索引文档应同时满足:关键词检索、类目/品牌筛选、价格区间、标签过滤、排序 (销量、上架时间、相关性得分)。sku_list 可用 nested 或扁平化子文档,视查询复杂度权衡。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 type ProductSearchDoc struct { SPUID string `json:"spu_id"` Title string `json:"title"` CategoryID int64 `json:"category_id"` BrandID int64 `json:"brand_id"` Tags []string `json:"tags"` PriceMin int64 `json:"price_min"` PriceMax int64 `json:"price_max"` Status int `json:"status"` Sales30d int64 `json:"sales_30d"` OnShelfAt int64 `json:"on_shelf_at"` ExtraTags []string `json:"extra_tags"` SKUs []SearchSKUInline `json:"skus"` } type SearchSKUInline struct { SKUID string `json:"sku_id"` Specs map [string ]string `json:"specs"` Price int64 `json:"price"` InStock bool `json:"in_stock"` }
Mapping 要点 :title 使用 text + keyword 子字段;筛选字段 keyword;价格 long;skus 使用 nested 以便按规格价查询。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 { "mappings" : { "properties" : { "spu_id" : { "type" : "keyword" } , "title" : { "type" : "text" , "fields" : { "kw" : { "type" : "keyword" , "ignore_above" : 256 } } } , "category_id" : { "type" : "long" } , "brand_id" : { "type" : "long" } , "tags" : { "type" : "keyword" } , "price_min" : { "type" : "long" } , "price_max" : { "type" : "long" } , "status" : { "type" : "integer" } , "sales_30d" : { "type" : "long" } , "on_shelf_at" : { "type" : "long" } , "extra_tags" : { "type" : "keyword" } , "skus" : { "type" : "nested" , "properties" : { "sku_id" : { "type" : "keyword" } , "specs" : { "type" : "flattened" } , "price" : { "type" : "long" } , "in_stock" : { "type" : "boolean" } } } } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 import ( "context" "encoding/json" ) type SearchRequest struct { Keyword string CategoryID int64 BrandID int64 PriceMin int64 PriceMax int64 Page int PageSize int Sort string } func SearchProducts (ctx context.Context, es *ESClient, req *SearchRequest) ([]ProductSearchDoc, int , error ) { must := []map [string ]interface {}{ {"term" : map [string ]interface {}{"status" : 4 }}, } if req.Keyword != "" { must = append (must, map [string ]interface {}{ "multi_match" : map [string ]interface {}{ "query" : req.Keyword, "fields" : []string {"title^2" , "title.kw" }, }, }) } filters := []map [string ]interface {}{} if req.CategoryID > 0 { filters = append (filters, map [string ]interface {}{ "term" : map [string ]interface {}{"category_id" : req.CategoryID}, }) } if req.BrandID > 0 { filters = append (filters, map [string ]interface {}{ "term" : map [string ]interface {}{"brand_id" : req.BrandID}, }) } if req.PriceMin > 0 || req.PriceMax > 0 { rng := map [string ]interface {}{} if req.PriceMin > 0 { rng["gte" ] = req.PriceMin } if req.PriceMax > 0 { rng["lte" ] = req.PriceMax } filters = append (filters, map [string ]interface {}{ "range" : map [string ]interface {}{"price_min" : rng}, }) } boolQ := map [string ]interface {}{"must" : must} if len (filters) > 0 { boolQ["filter" ] = filters } body := map [string ]interface {}{ "query" : map [string ]interface {}{"bool" : boolQ}, "from" : (req.Page - 1 ) * req.PageSize, "size" : req.PageSize, } payload, err := json.Marshal(body) if err != nil { return nil , 0 , err } return es.Search(ctx, "product_index" , payload) }
5.2 多级缓存策略 flowchart TB
Client[详情/列表请求] --> L1[L1 本地缓存 Caffeine]
L1 -->|miss| L2[L2 Redis 集群]
L2 -->|miss| L3[L3 MySQL 主从]
L3 --> L2
L2 --> L1
MQ[Kafka 变更事件] -.->|失效/异步回填| L2
MQ -.-> L1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 import ( "context" "encoding/json" "fmt" "time" ) type ProductDetailDTO struct { SPU *SPU SKUs []*SKU } func cacheKeyDetail (spuID string ) string { return fmt.Sprintf("pd:%s" , spuID) } func GetProductDetail (ctx context.Context, spuID string ) (*ProductDetailDTO, error ) { if v, ok := localCache.Get(spuID); ok { return v.(*ProductDetailDTO), nil } raw, err := redis.Get(ctx, cacheKeyDetail(spuID)) if err == nil && raw != "" { var dto ProductDetailDTO if json.Unmarshal([]byte (raw), &dto) == nil { localCache.Set(spuID, &dto, 5 *time.Second) return &dto, nil } } spu, err := db.GetSPU(ctx, spuID) if err != nil { return nil , err } skus, err := db.ListSKUBySPU(ctx, spuID) if err != nil { return nil , err } dto := &ProductDetailDTO{SPU: spu, SKUs: skus} b, _ := json.Marshal(dto) _ = redis.Set(ctx, cacheKeyDetail(spuID), string (b), 10 *time.Minute) localCache.Set(spuID, dto, 5 *time.Second) return dto, nil } func InvalidateProductCaches (ctx context.Context, spuID string ) { _ = redis.Del(ctx, cacheKeyDetail(spuID)) localCache.Invalidate(spuID) }
5.3 智能刷新规则 热门商品变更后应更快可见;长尾商品可降低刷新频率以节省 ES 与 Redis 成本。可结合 近实时销量、搜索曝光、运营打标 计算刷新间隔。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 func CalculateRefreshInterval (spuID string , score float64 ) time.Duration { if score > 1000 { return 5 * time.Second } if score > 100 { return 30 * time.Second } if score > 10 { return 2 * time.Minute } return 10 * time.Minute } func HotnessScore (expose1h, cart1h, sales30d int64 ) float64 { return float64 (expose1h)*0.1 + float64 (cart1h)*2 + float64 (sales30d)*0.01 }
6. 特殊商品类型(黄金案例) 以下四个案例覆盖多数面试与工程追问:规格矩阵、虚拟履约、日历库存、组合约束 。
6.1 标准实物商品 场景 :T 恤,颜色 {红, 蓝, 黑},尺码 {S, M, L, XL},共 12 个 SKU。列表价可一致,也可按颜色区分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 func ExampleTShirtSPU () *SPU { return &SPU{ SPUID: "SPU_TSHIRT_DEMO" , Title: "纯棉圆领 T 恤" , CategoryID: 5001 , ProductType: "standard" , SpecDefs: []SpecDef{ {Name: "颜色" , Values: []string {"红" , "蓝" , "黑" }}, {Name: "尺码" , Values: []string {"S" , "M" , "L" , "XL" }}, }, } }
graph LR
subgraph SKUs["12 x SKU"]
R1[红-S] --- R2[红-M]
B1[蓝-S] --- B2[黑-XL]
end
SPU[T 恤 SPU] --> SKUs
6.2 虚拟商品 场景 :话费充值,SKU 对应面额;库存来自 卡密池 或 直充渠道额度 。下单后即时履约,无需物流。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 type TopUpSKU struct { SKUID string FaceValue int64 Channel string } type CardPool struct { PoolID string SKUID string } type CardSecret struct { CardID string SKUID string Secret string Status int } func IssueCard (ctx context.Context, skuID string ) (*CardSecret, error ) { card, err := db.AcquireOneCard(ctx, skuID) if err != nil { return nil , err } if err := db.MarkCardIssued(ctx, card.CardID); err != nil { return nil , err } return card, nil }
sequenceDiagram
participant U as 用户
participant O as 订单
participant P as 商品/卡密
U->>O: 下单支付成功
O->>P: 申请卡密
P-->>O: 返回卡密/直充结果
O-->>U: 展示履约结果
6.3 服务类商品 场景 :酒店房型,价格随 日期 波动,库存为 每日可售间夜 。搜索与下单需传入住离店日期。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 type HotelRoomSKU struct { SKUID string RoomType string HotelID string } type PriceCalendar struct { SKUID string Date string Price int64 } type StockCalendar struct { SKUID string Date string Stock int64 } func CalculateHotelPrice (ctx context.Context, skuID, checkIn, checkOut string ) (int64 , error ) { nights := NightsBetween(checkIn, checkOut) var total int64 for d := range EachNight(checkIn, nights) { p, err := db.GetDailyPrice(ctx, skuID, d) if err != nil { return 0 , err } total += p } return total, nil } func NightsBetween (checkIn, checkOut string ) int { return 1 }func EachNight (checkIn string , n int ) []string { return []string {checkIn} }
6.4 组合商品 场景 :电影票 + 小食套餐,子商品各自有 SKU 与库存,下单需 联合校验 与 打包售价分摊 (可固定价或按比例)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 import "fmt" type BundleItem struct { ChildSPUID string ChildSKUID string Quantity int } type BundleProduct struct { SPUID string Items []BundleItem Price int64 } func CheckBundleStock (ctx context.Context, b *BundleProduct) error { for _, it := range b.Items { n, err := inventory.GetStock(ctx, it.ChildSKUID, "" ) if err != nil { return err } if n < int64 (it.Quantity) { return fmt.Errorf("insufficient stock for %s" , it.ChildSKUID) } } return nil } func AllocateBundlePrice (items []BundleItem, total int64 ) map [string ]int64 { out := make (map [string ]int64 ) var units int64 for _, it := range items { units += int64 (it.Quantity) } if units == 0 { return out } var allocated int64 for i, it := range items { var share int64 if i == len (items)-1 { share = total - allocated } else { share = total * int64 (it.Quantity) / units } out[it.ChildSKUID] = share allocated += share } return out }
flowchart TB
B[组合 SPU] --> M1[子 SKU 电影票]
B --> M2[子 SKU 爆米花]
M1 --> INV1[影厅座位库存]
M2 --> INV2[卖品库存]
7. 商品版本管理与快照 商品变更频繁,需要 可追溯的版本历史 与 面向订单的不可变快照 。版本表支撑审计与回滚;快照表支撑下单展示与纠纷处理;Kafka 将变更广播给搜索与缓存等消费者。
7.1 版本控制 目标 :记录每次变更、支持回滚、满足合规审计。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 import ( "context" "encoding/json" "time" ) type ProductVersion struct { VersionID string SPUID string Version int64 Content string ChangeType string Operator string CreatedAt time.Time } func CreateProductVersion (ctx context.Context, product *Product, operator string ) error { content, _ := json.Marshal(product) version := &ProductVersion{ VersionID: GenerateVersionID(), SPUID: product.SPUID, Version: product.Version, Content: string (content), ChangeType: "update" , Operator: operator, CreatedAt: time.Now(), } return db.InsertProductVersion(ctx, version) } func RollbackToVersion (ctx context.Context, spuID string , targetVersion int64 ) error { ver, err := db.GetProductVersion(ctx, spuID, targetVersion) if err != nil { return err } product := &Product{} if err := json.Unmarshal([]byte (ver.Content), product); err != nil { return err } current, _ := db.GetProduct(ctx, spuID) product.Version = current.Version + 1 if err := db.UpdateProduct(ctx, product); err != nil { return err } return CreateProductVersion(ctx, product, "system_rollback" ) }
7.2 快照机制 订单创建时引用快照 ID,即使商品改价改图,订单详情仍展示下单时内容。内容 Hash 相同则复用一条快照记录,节省存储。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 import ( "context" "crypto/md5" "encoding/hex" "encoding/json" "fmt" "time" ) type ProductSnapshotOrder struct { SnapshotID string SPUID string SKUID string Title string Price int64 Image string Specs string Attributes string CreatedAt time.Time } func CreateSnapshot (ctx context.Context, sku *SKU) (string , error ) { spu, err := db.GetSPU(ctx, sku.SPUID) if err != nil { return "" , err } content := fmt.Sprintf("%s_%s_%s_%d_%s" , spu.SPUID, sku.SKUID, spu.Title, sku.Price, encodeSpecs(sku.SpecValues)) sum := md5.Sum([]byte (content)) snapshotID := hex.EncodeToString(sum[:]) if existing, _ := db.GetSnapshot(ctx, snapshotID); existing != nil { return snapshotID, nil } snap := &ProductSnapshotOrder{ SnapshotID: snapshotID, SPUID: spu.SPUID, SKUID: sku.SKUID, Title: spu.Title, Price: sku.Price, Image: spu.MainImages[0 ], Specs: encodeSpecs(sku.SpecValues), CreatedAt: time.Now(), } if err := db.InsertSnapshot(ctx, snap); err != nil { return "" , err } return snapshotID, nil } func encodeSpecs (m map [string ]string ) string { b, _ := json.Marshal(m) return string (b) }
7.3 变更事件与最终一致性 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 import ( "encoding/json" "time" ) type ProductChangedEvent struct { SPUID string ChangeType string Version int64 Timestamp time.Time } func PublishProductChangedEvent (ctx context.Context, event *ProductChangedEvent) error { data, _ := json.Marshal(event) msg := &KafkaMessage{ Topic: "product.changed" , Key: event.SPUID, Value: data, } return kafkaProducer.Send(msg) } func ConsumeProductChangedEvent () { consumer := kafka.NewConsumer("product.changed" , "search-sync-group" ) for msg := range consumer.Messages() { event := &ProductChangedEvent{} if err := json.Unmarshal(msg.Value, event); err != nil { continue } if err := UpdateSearchIndex(event.SPUID); err != nil { log.Error("failed to update search index" , "error" , err) continue } InvalidateCache(event.SPUID) consumer.CommitMessage(msg) } }
flowchart LR
W[商品写库] --> E[发布 product.changed]
E --> S[搜索同步 Worker]
E --> C[缓存失效]
E --> O[下游对账/监控]
8. 商品类型扩展设计 8.1 扩展点识别
商品模型扩展 :品类特有属性与扩展存储(MongoDB / JSON / EAV)
上架流程扩展 :审核模板、必填项、校验插件
库存扩展 :库存维度与网关路由(见第 4 章)
价格扩展 :计价参数、日历价、动态溢价
搜索与展示扩展 :索引字段、列表卡片模板、筛选器组件
8.2 策略模式应用 1 2 3 4 5 6 7 8 9 type ProductTypeStrategy interface { Validate(product *Product) error GenerateSKUs(spu *SPU) []*SKU GetStock(skuID string ) (int , error ) CalculatePrice(sku *SKU, params map [string ]interface {}) (int64 , error ) GetExtAttributes(spu *SPU) map [string ]interface {} }
8.3 新品类接入指南 flowchart LR
A[1. 分析差异] --> B[2. 定义模型]
B --> C[3. 实现策略]
C --> D[4. 注册路由]
D --> E[5. 测试验证]
分析差异 :相对标准实物,列出 SKU、库存、价格、履约差异
定义模型 :SPU/SKU 扩展字段、Ext 文档 schema
实现策略 :实现 ProductTypeStrategy(或 ProductAdapter)并补单测
注册路由 :在注册表挂载 ProductType → 实现,并配置表单/索引
测试验证 :集成测试覆盖上架、搜索、下单快照全链路
8.4 扩展性设计原则
开闭原则 :新增品类以注册策略为主,避免修改核心状态机主干
单一职责 :每个策略只处理一个品类或一族相似品类
依赖倒置 :上层依赖 ProductTypeStrategy 接口,而非具体类
9. 工程实践要点 9.1 商品 ID 生成
方案
优点
缺点
适用
Snowflake
趋势递增、高性能、全局唯一
时钟回拨需治理
大规模推荐
UUID
实现简单
无序、索引碎片化
中小流量
DB 自增
简单
分库分表扩展难
单库早期
flowchart LR
ID[64 bit ID] --> T[41 bit 时间戳]
ID --> M[10 bit 机器号]
ID --> S[12 bit 序列]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 import ( "fmt" "sync" "time" ) type SnowflakeGenerator struct { machineID int64 sequence int64 lastTime int64 mu sync.Mutex } func (g *SnowflakeGenerator) NextID() string { g.mu.Lock() defer g.mu.Unlock() now := time.Now().UnixMilli() if now < g.lastTime { time.Sleep(time.Duration(g.lastTime-now) * time.Millisecond) now = time.Now().UnixMilli() } if now == g.lastTime { g.sequence = (g.sequence + 1 ) & 0xFFF if g.sequence == 0 { for now <= g.lastTime { now = time.Now().UnixMilli() } } } else { g.sequence = 0 } g.lastTime = now id := ((now - 1609459200000 ) << 22 ) | (g.machineID << 12 ) | g.sequence return fmt.Sprintf("SP%d" , id) }
9.2 商品同步任务治理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import "time" func FullSync (ctx context.Context, partnerID string ) error { products, err := partnerClient.GetAllProducts(partnerID) if err != nil { return err } for _, p := range products { msg := ConvertToMessage(p) if err := PartnerPushProduct(ctx, msg); err != nil { log.Error("full sync row failed" , "err" , err) } } return nil } func IncrementalSync (ctx context.Context, partnerID string ) error { last := GetLastSyncTime(partnerID) products, err := partnerClient.GetChangedProducts(partnerID, last) if err != nil { return err } for _, p := range products { msg := ConvertToMessage(p) if err := PartnerPushProduct(ctx, msg); err != nil { log.Error("incr sync row failed" , "err" , err) } } UpdateLastSyncTime(partnerID, time.Now()) return nil }
热门商品刷新间隔见第 5.3 节 CalculateRefreshInterval。
9.3 监控告警体系
业务 :上架量、审核通过率、搜索 QPS、缓存命中率
应用 :核心接口 P99、错误率、同步成功率
依赖 :MySQL 慢查询、Redis 连接、ES 查询延迟
系统 :CPU、内存、磁盘、网络
1 2 3 4 5 6 7 8 9 import "time" func RecordMetrics (spuID string , operation string , latency time.Duration) { metrics.IncrCounter("product_operation_total" , "operation" , operation) metrics.ObserveHistogram("product_operation_latency" , latency.Milliseconds(), "operation" , operation) if latency > 100 *time.Millisecond { metrics.IncrCounter("product_operation_slow" , "operation" , operation) } }
9.4 性能优化
数据库 :按 SPU ID 哈希分表;组合索引如 (category_id, status);读写分离
缓存 :多级缓存、预热 TOP N、布隆过滤器防穿透、空值短 TTL
搜索 :合理分片与副本、避免深分页、聚合用近似算法
1 2 3 4 5 6 7 8 9 10 11 12 func ShardIndex (spuID string , shards int ) int { return int (fnv32(spuID) % uint32 (shards)) } func fnv32 (s string ) uint32 { var h uint32 = 2166136261 for i := 0 ; i < len (s); i++ { h ^= uint32 (s[i]) h *= 16777619 } return h }
1 2 3 4 func MaybeProductExists (bloom *BloomFilter, spuID string ) bool { return bloom.MightContain(spuID) }
9.5 故障处理
故障
处理思路
缓存雪崩
TTL 加随机抖动、热点 key 独立策略
缓存穿透
布隆过滤器、空值缓存
数据不一致
定时对账、修复任务、人工兜底
同步失败
重试队列、死信告警
ES 慢查询
优化 mapping、查询裁剪、冷热索引
总结 核心要点回顾:
商品中心是电商平台的「商品库」,核心技术要点包括:
SPU/SKU 模型 :标准产品单元 + 库存单位,规格组合与笛卡尔积生成 SKU
多角色上架 :商家、供应商、运营三条链路,配合状态机与审核策略
异构商品治理 :适配器 + 配置化 + 库存网关,隔离品类差异
多级缓存与搜索 :Elasticsearch 负责检索,L1/L2 缓存扛热点读
版本与快照 :版本审计与回滚,快照 Hash 复用服务订单域
事件驱动 :product.changed 串联搜索、缓存与下游
面试要点:
画出 SPU/SKU 关系,说明规格组合如何生成与如何剔除无效组合
说明异构商品的挑战与适配器、策略模式如何落地
描述多级缓存与热点刷新策略,以及接受怎样的最终一致窗口
对比宽表、EAV、混合存储的取舍
说明订单为何引用商品快照,以及 Hash 复用如何做
扩展阅读:
DDD 在商品域的建模与限界上下文划分
商品中台演进与多租户隔离
搜索排序:相关性、商业化、个性化权重
参考资料 业界文章与分享
淘宝商品中心技术演进相关分享
京东商品系统架构实践
亚马逊商品目录(Catalog)设计公开资料
开源项目
Elasticsearch — 搜索引擎
Caffeine — 本地缓存
Excelize — Excel 处理
系列文章(本仓库)
20-ecommerce-overview.md — 电商总览
21-ecommerce-listing.md — 商品上架系统
22-ecommerce-inventory.md — 库存系统
26-ecommerce-order-system.md — 订单系统
设计过程与章节拆解见仓库内 docs/superpowers/specs/2026-04-07-product-center-design.md。