商品系統的儲存分層#
商品資料看似一張表,實際是多模態資料的組合:
| 資料類型 | 例子 | 儲存適配 |
|---|---|---|
| 結構化基本 | 價格、庫存、SKU、類目 | MySQL / PostgreSQL |
| 半結構化參數 | 規格屬性(顏色、容量、面料、能耗) | MongoDB / PG JSONB / MySQL JSON |
| 非結構化媒體 | 圖片、影片 | 物件儲存(S3、OSS、COS) |
| 長文本 | 商品介紹、富文本 | 物件儲存 + CDN 靜態化 |
| 搜尋索引 | 全文檢索、分類過濾 | Elasticsearch |
| 熱門資料 | 首頁、分類頁的熱門商品 | Redis |
「全部塞 MySQL」── 行得通但代價慘:每筆商品幾 MB、查詢慢、索引膨脹、備份痛苦。對的東西放對的地方是商品儲存的第一原則。
SKU vs SPU#
電商兩個高頻名詞,混淆會出大事:
| 概念 | 全名 | 例子 |
|---|---|---|
| SPU | Standard Product Unit | iPhone 15 |
| SKU | Stock Keeping Unit | iPhone 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 異步備份
- 庫存不在加購扣減、結算才校驗
下章看交易性最強的賬戶系統 ── 必須與資料庫事務正確使用。