商品系統的儲存分層#

商品資料看似一張表,實際是多模態資料的組合:

資料類型例子儲存適配
結構化基本價格、庫存、SKU、類目MySQL / PostgreSQL
半結構化參數規格屬性(顏色、容量、面料、能耗)MongoDB / PG JSONB / MySQL JSON
非結構化媒體圖片、影片物件儲存(S3、OSS、COS)
長文本商品介紹、富文本物件儲存 + CDN 靜態化
搜尋索引全文檢索、分類過濾Elasticsearch
熱門資料首頁、分類頁的熱門商品Redis

「全部塞 MySQL」── 行得通但代價慘:每筆商品幾 MB、查詢慢、索引膨脹、備份痛苦。對的東西放對的地方是商品儲存的第一原則。

SKU vs SPU#

電商兩個高頻名詞,混淆會出大事:

概念全名例子
SPUStandard Product UnitiPhone 15
SKUStock Keeping UnitiPhone 15 / 256GB / 黑色

一個 SPU 對應多個 SKU。庫存與訂單明細按 SKU;商品聚合頁、評論按 SPU。

CREATE TABLE spus (
    id BIGINT PK,
    name VARCHAR(255),
    category_id BIGINT,
    brand_id BIGINT,
    description TEXT
);

CREATE TABLE skus (
    id BIGINT PK,
    spu_id BIGINT,
    price DECIMAL(15,2),
    stock INT,
    attributes JSON,           -- 規格屬性
    image_url VARCHAR(512)     -- 圖片 URL(指向物件儲存)
);

為什麼參數用 MongoDB / JSON#

商品規格參數的麻煩:不同類目欄位完全不同

  • 手機:螢幕大小、解析度、晶片、電池容量、相機
  • 服裝:尺寸、顏色、面料、產地、洗滌方式
  • 家電:能耗、容量、噪音、保固

如果用關係型 schema,要嘛:

  • 一張大表 200 個欄位(多數欄位永遠 NULL)── 浪費空間
  • 每個類目一張表 ── schema 爆炸
  • EAV 模型(entity-attribute-value)── 查詢成噩夢

JSON / Document 模型自然適配:

{
  "spu_id": 12345,
  "params": {
    "screen_size": "6.1 inch",
    "resolution": "2556x1179",
    "chipset": "A17 Pro",
    "ram": "8GB",
    "storage_options": ["128GB", "256GB", "512GB", "1TB"],
    "colors": ["natural", "blue", "white", "black"]
  }
}

選 MongoDB 還是 PG JSONB / MySQL JSON:

選項優勢劣勢
MongoDB原生 document、aggregation 強兩個資料庫運維
PG JSONB與主資料庫同源、強型別、可建 GIN 索引要學 PG 語法
MySQL JSON只用 MySQL 一家索引較弱(functional index 但索引 doc 內部欄位仍卡)

近年趨勢是用 PG JSONB 取代 MongoDB ── 一個資料庫搞定關係 + 文件,運維簡化。除非你已經 MongoDB 社群很熟,否則 PG 是更穩的選擇。

圖片與影片:物件儲存#

商品圖至少 3~5 張、每張 100KB ~ 數 MB。一個有 100 萬商品的電商,圖片總量輕鬆 TB 等級。

物件儲存(S3、阿里雲 OSS、騰訊 COS)的合適:

  • 為大檔案設計,無 schema
  • 內建多副本、跨區複製
  • 配合 CDN 邊緣分發
  • 按用量付費,不用容量規劃

關鍵:MySQL 只存 URL,不存圖片二進位

image_url VARCHAR(512)
-- 例:https://cdn.example.com/products/12345/main.jpg

檔案命名要有規律 ── <bucket>/products/<spu_id>/<seq>.<ext> 之類,方便管理與失敗 recover。

CDN 與圖片優化#

物件儲存 + CDN 是電商標配:

用戶 → CDN 邊緣節點 ─cache miss─→ 物件儲存源站
              ↓ cache hit
              直接回應(< 50ms)

關鍵優化:

技巧效果
多解析度版本thumbnail(列表頁)vs full(詳情頁)vs zoom
WebP / AVIF比 JPEG 小 30~50%
Lazy load滾動到視窗才載入
dimensions 標註防止 layout shift(CLS)
Cache-Control商品圖一年不變 → 設極長 max-age
URL 帶版本號?v=2025-04 強制更新

CDN 設定應與業務團隊一致 ── 不能讓「促銷活動換主圖、結果用戶看到舊圖一週」。

商品介紹靜態化#

商品介紹(detail HTML)特性:

  • 寫入:商家後台改、頻率低
  • 讀取:每個 PDP(product detail page)載一次,極高頻

如果每次 PDP 都從 DB 拉介紹 HTML → DB 壓力巨大、可能 1MB 以上。

解決:靜態化 ── 商家更新時生成靜態 HTML 上傳到物件儲存,PDP 透過 CDN 載入:

商家後台改介紹
   ↓
生成 detail.html
   ↓
上傳 OSS:products/12345/detail.html
   ↓
CDN 自動同步
   ↓
PDP 用 <iframe> 或 fetch 載入

同樣思路用於:

  • 評論列表(增量更新 + 分頁靜態化)
  • 排行榜
  • 公開規則頁

購物車的儲存特性#

購物車跟訂單天差地別:

特性訂單購物車
寫頻率一筆一次用戶反覆改
必存性絕對必存可丟(暫存)/ 必存(已登入)
一致性最終一致即可
查詢結構化查詢簡單 user_id → items
容量訂單越來越多用戶量 × 平均購物車大小(可控)

兩種購物車:暫存 vs 用戶#

暫存購物車(未登入):用戶沒帳號,但已加幾項商品。

  • 存哪?瀏覽器 LocalStorage 或 cookie
  • 為什麼不上服務端?
    • 跑了就丟,沒必要佔資源
    • 隱私:未登入用戶不該有伺服器記錄
  • 何時遷移?登入時,前端把 LocalStorage 內容上傳,與服務端 user cart 合併

用戶購物車(已登入):必須伺服端持久化,跨裝置同步。

# 登入合併
local_cart = read_local_storage()
server_cart = fetch_user_cart(user_id)
merged = merge(local_cart, server_cart)  # 同 SKU 數量加總或取較新
upload_user_cart(user_id, merged)
clear_local_storage()

服務端購物車的儲存選擇#

存 MySQL?沒問題但不夠快。每次取整個購物車:

SELECT * FROM user_cart_items WHERE user_id = ?

OK,但更新某項時:

UPDATE user_cart_items SET quantity = ?
WHERE user_id = ? AND sku_id = ?

每次操作一次寫。1000 萬日活、平均 5 次操作 → 5000 萬次寫,MySQL 壓力大。

Redis hash 是更好的契合:

HSET cart:user:12345 sku:67890 '{"qty":2,"price":99,"selected":true}'
HGETALL cart:user:12345
HDEL cart:user:12345 sku:67890

每次操作 O(1),QPS 容易上幾十萬。

但 Redis 是揮發的(即使持久化也有風險)── 真實電商常用:

write:  寫 Redis(fast path)+ 異步寫 MySQL(持久化)
read:   讀 Redis;miss 時從 MySQL 載入

或更激進:純 Redis + 定期 dump 到 MySQL 備份。權衡點是「資料丟了用戶痛不痛」── 購物車丟了用戶會抱怨但不會告,所以多數電商接受最終一致性,純 Redis + 異步落 DB。

購物車 schema#

Redis hash 形式:

key: cart:{user_id}
field: sku:{sku_id}
value: {"qty": 2, "added_at": 1700000000, "selected": true}

附帶設定:

  • TTL:未登入半年,已登入無限或 1 年
  • maxmemory-policy:購物車 instance 用 volatile-lru 自然淘汰

如果用 MySQL:

CREATE TABLE user_cart_items (
    user_id BIGINT,
    sku_id BIGINT,
    quantity INT,
    selected BOOLEAN,
    added_at TIMESTAMP,
    PRIMARY KEY (user_id, sku_id)
);

(user_id, sku_id) 複合主鍵,按 user_id 自然 sharding。

購物車的「暫存物」問題#

購物車裡的商品可能:

  • 下架了 → 結算時要 alert
  • 漲價了 → 結算時要重新計價
  • 限時搶購結束了
  • 庫存不足了

這些只在結算時校驗,不要在 add-to-cart 時做。原因:

  • 用戶可能加了 100 個商品要等促銷時下單
  • 即時校驗每個 sku 的價格、庫存 → 拖慢購物車載入
  • 結算時校驗一次就夠了

購物車與庫存的關係#

普遍誤解:「加入購物車就佔庫存」── 大型電商不這麼做

原因:

  • 加購頻率 » 下單頻率,鎖庫存浪費
  • 購物車可長存幾個月,鎖庫存實質凍結資源
  • 競品惡意刷加購可耗盡庫存

實務:庫存只在下單那一刻扣減(pre-occupy),支付完成轉為實扣,超時未支付歸還。

例外:限時秒殺、預售、限量 ── 確實需要加購階段就鎖。但這是特例,不是預設。

小結#

商品系統:

  • 多模態儲存,各取所長:MySQL(結構化)+ MongoDB/JSONB(規格)+ 物件儲存(媒體)+ ES(搜尋)+ Redis(熱門)
  • 圖片必走 CDN,HTML 介紹靜態化
  • SKU vs SPU 的劃分

購物車系統:

  • 未登入瀏覽器、登入合併
  • 服務端 Redis 為主、MySQL 異步備份
  • 庫存不在加購扣減、結算才校驗

下章看交易性最強的賬戶系統 ── 必須與資料庫事務正確使用。