Skip to content
DEE
Database Engineering Essentials

[DEE-402] 鍵值存儲模式

INFO

為你的存取模式設計鍵;值的結構取決於存儲引擎的能力。使用一致的鍵命名慣例,對暫時性資料設定 TTL,並為每個使用情境選擇正確的資料結構。

背景

鍵值存儲是最簡單的 NoSQL 資料庫類別:每筆記錄都是一個鍵(唯一字串)對應一個值(二進位大物件、字串或結構化資料型別)。這種簡潔性帶來了極致效能 —— Redis 通常能提供亞毫秒級的讀寫,DynamoDB 則在任何規模下都能提供個位數毫秒的延遲。

然而,此類別中的存儲引擎在功能上差異顯著。Redis 提供豐富的資料結構(字串、雜湊、列表、集合、有序集合、串流等),適用於快取、工作階段管理、排行榜、速率限制和即時分析。DynamoDB 透過排序鍵和次要索引擴展鍵值模型,支援在單一表格中處理複雜的存取模式。較簡單的存儲如 Memcached 或 etcd 僅提供不透明的字串值。

鍵的設計是最關鍵的決策。不同於關聯式資料庫可以事後添加索引,鍵值存儲幾乎完全透過鍵來存取。設計不良的鍵結構意味著你無法有效率地檢索所需的資料。

原則

  • MUST在撰寫任何應用程式碼之前建立並執行鍵命名慣例。該慣例SHOULD使用分層命名空間搭配一致的分隔符(例如 service:entity:id:attribute)。
  • MUST對暫時性資料(如工作階段、快取和速率限制計數器)設定 TTL(存活時間)。缺少 TTL 會導致記憶體無限增長,直到存儲耗盡記憶體或發生不可預測的驅逐。
  • SHOULD為每個使用情境選擇適當的 Redis 資料結構,而非將所有東西序列化為字串。
  • MUST NOT儲存超過存儲引擎設計容量的值(例如 Redis 值超過 512 MB、DynamoDB 項目超過 400 KB)。過大的值會增加延遲、記憶體碎片化和複製延遲。
  • SHOULD設計鍵以避免熱點 —— 單一鍵承受不成比例的大量流量。

視覺化

範例

Redis 工作階段存儲

將使用者工作階段儲存為帶有 TTL 的 Redis 雜湊。雜湊中的每個欄位都可以獨立讀取或更新,無需反序列化整個工作階段:

redis
-- 建立帶有 30 分鐘 TTL 的工作階段
HSET auth:session:abc123 user_id 42 role "admin" login_at "2025-06-01T08:30:00Z"
EXPIRE auth:session:abc123 1800

-- 讀取單一欄位(無需反序列化)
HGET auth:session:abc123 user_id
-- "42"

-- 在活動時延長工作階段(滑動 TTL)
EXPIRE auth:session:abc123 1800

-- 登出時刪除工作階段
DEL auth:session:abc123

Redis 有序集合排行榜

有序集合依分數維持元素排序,支援 O(log N) 插入和 O(log N + M) 範圍查詢(其中 M 是回傳元素數量):

redis
-- 記錄玩家分數
ZADD leaderboard:weekly:2025-w22 1500 "player:alice"
ZADD leaderboard:weekly:2025-w22 2300 "player:bob"
ZADD leaderboard:weekly:2025-w22 1800 "player:carol"

-- 前 3 名玩家(最高分優先)
ZREVRANGE leaderboard:weekly:2025-w22 0 2 WITHSCORES
-- 1) "player:bob"    2) "2300"
-- 3) "player:carol"  4) "1800"
-- 5) "player:alice"  6) "1500"

-- 玩家排名(從 0 開始,最高分優先)
ZREVRANK leaderboard:weekly:2025-w22 "player:carol"
-- 1

-- 原子性增加分數
ZINCRBY leaderboard:weekly:2025-w22 200 "player:alice"

-- 設定 TTL 自動過期舊排行榜
EXPIRE leaderboard:weekly:2025-w22 604800

DynamoDB 單表設計基礎

DynamoDB 使用分區鍵(PK)和可選的排序鍵(SK)來組織資料。單表設計在一個表格中儲存多種實體類型,使用鍵前綴來區分:

PK                  SK                      Attributes
─────────────────   ─────────────────────   ──────────────────────────
USER#42             PROFILE                 { name: "Alice", email: "alice@ex.com" }
USER#42             ORDER#2025-06-01#001    { total: 44.97, status: "shipped" }
USER#42             ORDER#2025-06-15#002    { total: 19.99, status: "pending" }
PRODUCT#W-01        METADATA                { name: "Widget", price: 10.99 }
PRODUCT#W-01        REVIEW#2025-06-10#u42   { rating: 5, comment: "Great" }

啟用的存取模式:

  • 取得使用者檔案:PK = USER#42, SK = PROFILE
  • 列出使用者訂單:PK = USER#42, SK begins_with ORDER#
  • 取得日期範圍內的訂單:PK = USER#42, SK between ORDER#2025-06-01 and ORDER#2025-06-30

Redis 資料結構選擇指南

資料結構適用時機範例使用情境關鍵操作
String簡單值、計數器、序列化物件頁面瀏覽計數器、快取的 JSON 回應GET, SET, INCR, SETNX
Hash具有命名欄位且需獨立讀取/更新的物件使用者工作階段、產品詳情HGET, HSET, HMGET, HINCRBY
List有序序列、佇列、最近項目任務佇列、動態消息(最近 N 筆項目)LPUSH, RPOP, LRANGE, LTRIM
Set唯一成員資格、標籤、交集/聯集使用者角色、線上使用者、標籤篩選SADD, SISMEMBER, SINTER, SUNION
Sorted Set排名資料、時間序列視窗、優先佇列排行榜、速率限制器(滑動視窗)ZADD, ZRANGE, ZREVRANK, ZINCRBY
Stream僅附加事件日誌搭配消費者群組事件溯源、訊息代理XADD, XREAD, XREADGROUP

常見錯誤

錯誤為何有害修正方式
熱鍵 —— 單一鍵(例如全域計數器、熱門快取條目)承受大部分流量在 Redis 中,對一個鍵的所有操作都是序列化的。在 DynamoDB 中,熱分區鍵會節流該分區中的所有項目。分片熱鍵(例如 counter:{0..N} 搭配隨機分片的 INCRBY,讀取時加總)。在 DynamoDB 中,使用計算後綴的寫入分片。
暫時性資料缺少 TTL —— 工作階段、快取和暫時資料沒有過期設定記憶體無限增長。Redis 驅逐策略(allkeys-lruvolatile-lru)是安全網,不是設計策略。存儲最終會 OOM 或驅逐重要資料。對每個暫時性鍵設定明確的 TTL:EXPIRE key secondsSET key value EX seconds
過大的值 —— 在 Redis 或 DynamoDB 中儲存多 MB 的二進位大物件(圖片、PDF、完整 HTML 頁面)過大的值會增加網路延遲、記憶體碎片化和複製延遲。在 DynamoDB 中,超過 400 KB 的項目會被拒絕。將大型二進位物件存放在物件存儲(S3);在鍵值存儲中僅存放中繼資料或參考 URL。
無鍵命名慣例 —— 臨時鍵名如 u42bob_sessiondata鍵變得無法管理、除錯或監控。你無法使用 SCAN 模式來找到相關鍵。服務之間會發生命名空間衝突。在第一天就定義慣例(例如 service:entity:id)並在程式碼審查中執行。
在 Redis 中將所有東西序列化為 JSON 字串失去原子性地獨立更新個別欄位的能力。每次更新都需要完整的讀取-修改-寫入循環。使用 Hash 處理物件、Sorted Set 處理排名資料、Set 處理成員資格 —— 善用 Redis 原生結構。

相關 DEE

參考資料