深入了解 Redis:資料結構與 CRUD 操作完整指南

🌏 Read the English version


為什麼需要了解 Redis 資料結構?

Redis 在現代應用架構中的核心地位

Redis(Remote Dictionary Server)不僅是一個快取系統,更是一個多功能的記憶體資料庫。深入理解 Redis 資料結構的重要性體現在三個方面:

1. 效能優化的關鍵

選擇正確的資料結構能帶來數量級的效能提升:

  • 時間複雜度差異:使用 Hash 而非多個 String 可將操作從 O(n) 降至 O(1)
  • 記憶體使用:Sorted Set 比儲存 JSON 字串節省 40-60% 記憶體
  • 網路往返次數:Pipeline 與原子操作減少 RTT,提升 10-50 倍吞吐量

實際案例:某電商平台將購物車從多次 GET 操作改為 Hash 結構,響應時間從 50ms 降至 5ms,同時記憶體使用減少 35%。

2. 業務場景的最佳匹配

不同業務需求對應不同的資料結構:

  • 社交應用:Set 實現共同好友、Sorted Set 實現排行榜
  • 即時系統:List 實現訊息佇列、Pub/Sub 實現即時通知
  • 計數統計:HyperLogLog 實現億級去重計數,記憶體僅 12KB
  • 地理位置:Geo 實現附近的人、外送範圍計算

3. 避免常見陷阱

  • ❌ 將所有資料塞進 String(JSON 序列化)→ 無法原子更新部分欄位
  • ❌ 用 List 實現排序 → 每次排序 O(n log n),應用 Sorted Set O(log n)
  • ❌ 用 Set 儲存時間序列 → 無法按時間範圍查詢,應用 Sorted Set

Redis 五大基礎資料結構詳解

1. String(字串)

特性與應用場景

基本特性

  • 最大 512MB
  • 二進位安全(可儲存任何資料)
  • 支援整數與浮點數運算
  • 自動過期(TTL)

典型應用

  • Session 儲存
  • 分散式鎖
  • 計數器(瀏覽量、點讚數)
  • 快取 JSON/HTML 片段

CRUD 操作完整指南

Create & Update(設定值)

# 基本設定
SET user:1000:name "Alice"

# 設定並指定過期時間(秒)
SETEX session:abc123 3600 "user_data"

# 設定過期時間(毫秒)
PSETEX cache:product:999 60000 '{"name":"iPhone","price":999}'

# 僅當 key 不存在時設定(分散式鎖)
SET lock:resource:1 "locked" NX EX 30

# 僅當 key 存在時設定(更新現有值)
SET user:1000:email "alice@example.com" XX

# 批次設定(原子操作)
MSET user:1:name "Alice" user:1:age "30" user:1:city "Taipei"

# 僅當所有 key 都不存在時批次設定
MSETNX user:2:name "Bob" user:2:age "25"

Read(讀取值)

# 基本讀取
GET user:1000:name

# 批次讀取(減少網路往返)
MGET user:1:name user:1:age user:1:city

# 取得舊值並設定新值(原子操作)
GETSET counter:visits 0

# 取得部分字串(0-based index)
GETRANGE message:welcome 0 9

# 取得值的長度
STRLEN user:1000:name

Update(更新值)

# 整數增加
INCR page:home:views                    # 增加 1
INCRBY page:home:views 10               # 增加 10
INCRBYFLOAT product:price 0.5           # 增加 0.5

# 整數減少
DECR stock:product:123                  # 減少 1
DECRBY stock:product:123 5              # 減少 5

# 附加字串到結尾
APPEND log:error:2024 "New error messagen"

# 更新部分字串(從 offset 開始覆蓋)
SETRANGE message:welcome 0 "Hi"

Delete(刪除)

# 刪除單個 key
DEL user:1000:name

# 刪除多個 key(原子操作)
DEL user:1:name user:1:age user:1:city

# 檢查 key 是否存在
EXISTS user:1000:name                   # 返回 1(存在)或 0(不存在)

# 設定過期時間(秒)
EXPIRE session:abc123 300

# 設定過期時間(毫秒)
PEXPIRE cache:data 60000

# 設定絕對過期時間(Unix timestamp)
EXPIREAT session:abc123 1735689600

# 查看剩餘過期時間(秒,-1 表示永久,-2 表示不存在)
TTL session:abc123

# 移除過期時間
PERSIST session:abc123

進階技巧與最佳實踐

1. 分散式鎖實作

# 正確的分散式鎖(原子操作 + 自動過期)
SET lock:order:123 "server-1-uuid" NX EX 10

# 釋放鎖(使用 Lua 確保原子性)
redis-cli --eval release_lock.lua lock:order:123 , server-1-uuid

Lua 腳本 release_lock.lua

if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

2. 限流器實作(滑動視窗)

# 每分鐘最多 10 次請求
SET rate:user:1000:20241018:1430 1 NX EX 60
INCR rate:user:1000:20241018:1430

3. Session 儲存最佳實踐

# 使用 Hash 而非 String 儲存 session(更靈活)
# 但若需整個 session 原子操作,用 String + JSON
SET session:abc123 '{"user_id":1000,"role":"admin","login_at":1735689600}' EX 3600

2. Hash(雜湊表)

特性與應用場景

基本特性

  • 類似 Map,field-value 對應
  • 適合儲存物件(避免序列化)
  • 每個 Hash 最多 2^32 – 1 個 field
  • 記憶體優化:小型 Hash 使用 ziplist 編碼

典型應用

  • 使用者資料(profile)
  • 商品詳情
  • 配置資訊
  • 購物車

CRUD 操作完整指南

Create & Update(設定欄位)

# 設定單個欄位
HSET user:1000 name "Alice"

# 設定多個欄位(Redis 4.0+)
HSET user:1000 name "Alice" age "30" city "Taipei" email "alice@example.com"

# 批次設定(舊版 Redis)
HMSET user:1000 name "Alice" age "30" city "Taipei"

# 僅當欄位不存在時設定
HSETNX user:1000 created_at "2024-10-18"

# 設定並返回舊值(Redis 不原生支援,需 Lua)

Read(讀取欄位)

# 讀取單個欄位
HGET user:1000 name

# 讀取多個欄位
HMGET user:1000 name age city

# 讀取所有欄位與值
HGETALL user:1000

# 僅讀取所有欄位名稱
HKEYS user:1000

# 僅讀取所有值
HVALS user:1000

# 取得欄位數量
HLEN user:1000

# 檢查欄位是否存在
HEXISTS user:1000 email

Update(更新欄位)

# 整數增加
HINCRBY user:1000 login_count 1
HINCRBY user:1000 points 100

# 浮點數增加
HINCRBYFLOAT product:999 rating 0.5

Delete(刪除欄位)

# 刪除單個欄位
HDEL user:1000 temp_token

# 刪除多個欄位
HDEL user:1000 old_field1 old_field2

# 刪除整個 Hash
DEL user:1000

進階技巧與最佳實踐

1. Hash vs String(JSON)選擇

場景 推薦 原因
需要更新單一欄位 Hash 避免反序列化整個物件
需要原子更新多個欄位 String (JSON) + Lua Hash 無法原子更新多欄位
欄位數量 < 10 Hash ziplist 編碼省記憶體
需要設定整個物件過期 String (JSON) Hash 無法對單一欄位過期

2. 購物車實作

# 加入商品到購物車
HSET cart:user:1000 product:123 "2"     # 商品 ID: 數量

# 更新商品數量
HINCRBY cart:user:1000 product:123 1    # 數量 +1

# 移除商品
HDEL cart:user:1000 product:123

# 取得購物車商品數
HLEN cart:user:1000

# 取得所有商品
HGETALL cart:user:1000

3. 記憶體優化(ziplist 編碼條件)

# 查看編碼方式
DEBUG OBJECT user:1000

# ziplist 編碼條件(可在 redis.conf 調整)
# hash-max-ziplist-entries 512  (欄位數 <= 512)
# hash-max-ziplist-value 64      (值長度 <= 64 bytes)

3. List(列表)

特性與應用場景

基本特性

  • 有序、可重複
  • 底層為雙向連結串列(quicklist)
  • 頭尾操作 O(1),中間操作 O(n)
  • 最多 2^32 – 1 個元素

典型應用

  • 訊息佇列(消息隊列)
  • 最新動態列表
  • 任務佇列
  • Undo/Redo 功能

CRUD 操作完整指南

Create & Update(插入元素)

# 從左側(頭部)插入
LPUSH timeline:user:1000 "post:999"
LPUSH timeline:user:1000 "post:998" "post:997"

# 從右側(尾部)插入
RPUSH queue:email "email:1" "email:2"

# 僅當 List 存在時插入
LPUSHX timeline:user:1000 "post:996"
RPUSHX queue:email "email:3"

# 在指定元素前/後插入
LINSERT timeline:user:1000 BEFORE "post:999" "post:1000"
LINSERT timeline:user:1000 AFTER "post:999" "post:998"

# 設定指定索引的值(0-based,負數從尾部算)
LSET timeline:user:1000 0 "updated_post:999"

Read(讀取元素)

# 取得指定範圍的元素(0 為第一個,-1 為最後一個)
LRANGE timeline:user:1000 0 9          # 最新 10 篇貼文
LRANGE timeline:user:1000 0 -1         # 所有元素

# 取得指定索引的元素
LINDEX timeline:user:1000 0            # 第一個元素
LINDEX timeline:user:1000 -1           # 最後一個元素

# 取得 List 長度
LLEN timeline:user:1000

Update(更新元素)

# 修剪 List(保留指定範圍,刪除其他)
LTRIM timeline:user:1000 0 99          # 僅保留最新 100 篇

# 更新指定索引的值
LSET timeline:user:1000 5 "new_value"

Delete(刪除元素)

# 從左側(頭部)彈出
LPOP queue:email                       # 彈出一個
LPOP queue:email 3                     # 彈出三個(Redis 6.2+)

# 從右側(尾部)彈出
RPOP queue:email

# 阻塞式彈出(用於消息隊列,超時單位:秒)
BLPOP queue:email 30                   # 等待 30 秒
BRPOP queue:email 0                    # 永久等待

# 刪除指定值(count > 0 從頭刪,< 0 從尾刪,= 0 刪全部)
LREM timeline:user:1000 1 "post:999"   # 從頭刪第一個 "post:999"
LREM timeline:user:1000 -2 "spam"      # 從尾刪兩個 "spam"
LREM timeline:user:1000 0 "ad"         # 刪除所有 "ad"

# 刪除整個 List
DEL timeline:user:1000

進階技巧與最佳實踐

1. 可靠訊息佇列實作

# Producer:發送訊息
LPUSH queue:tasks "task:process_order:123"

# Consumer:取出並處理(原子操作,避免遺失)
BRPOPLPUSH queue:tasks queue:tasks:processing 30

# 處理完成後刪除
LREM queue:tasks:processing 1 "task:process_order:123"

# 若處理失敗,可從 processing 佇列重試

2. 最新動態時間線(固定長度)

# 發佈新貼文
LPUSH timeline:user:1000 "post:1001"

# 自動修剪(僅保留最新 100 篇)
LTRIM timeline:user:1000 0 99

# 取得最新 20 篇
LRANGE timeline:user:1000 0 19

3. 分頁查詢

# 第 1 頁(每頁 10 筆)
LRANGE messages:chat:room1 0 9

# 第 2 頁
LRANGE messages:chat:room1 10 19

# 第 n 頁(從 0 開始)
# start = n * page_size
# end = start + page_size - 1

4. Set(集合)

特性與應用場景

基本特性

  • 無序、不重複
  • 底層為 hashtable 或 intset
  • 新增/刪除/查找 O(1)
  • 支援集合運算(交集、聯集、差集)

典型應用

  • 標籤系統
  • 共同好友
  • 去重(UV 統計)
  • 抽獎系統

CRUD 操作完整指南

Create & Update(新增元素)

# 新增單個元素
SADD tags:post:1000 "Redis"

# 新增多個元素
SADD tags:post:1000 "Database" "NoSQL" "Cache"

# 新增並返回成功新增的數量
SADD followers:user:1000 "user:2" "user:3" "user:3"  # 返回 2(user:3 重複)

Read(讀取元素)

# 取得所有元素(無序)
SMEMBERS tags:post:1000

# 取得元素數量
SCARD tags:post:1000

# 檢查元素是否存在
SISMEMBER tags:post:1000 "Redis"       # 返回 1(存在)或 0(不存在)

# 隨機取得 n 個元素(不刪除)
SRANDMEMBER tags:post:1000 2

# 隨機彈出 n 個元素(刪除)
SPOP tags:post:1000 1

Set 運算(交集、聯集、差集)

# 交集(共同元素)
SINTER followers:user:A followers:user:B   # A 和 B 的共同好友

# 聯集(所有元素)
SUNION tags:post:1 tags:post:2             # 所有標籤

# 差集(A 有但 B 沒有)
SDIFF followers:user:A followers:user:B    # A 的好友但不是 B 的

# 交集並儲存結果
SINTERSTORE result:common followers:user:A followers:user:B

# 聯集並儲存結果
SUNIONSTORE result:all tags:post:1 tags:post:2

# 差集並儲存結果
SDIFFSTORE result:diff followers:user:A followers:user:B

Delete(刪除元素)

# 刪除指定元素
SREM tags:post:1000 "OldTag"

# 刪除多個元素
SREM tags:post:1000 "Tag1" "Tag2" "Tag3"

# 隨機彈出元素(刪除並返回)
SPOP lottery:users 5                   # 抽獎:隨機抽 5 個中獎者

# 刪除整個 Set
DEL tags:post:1000

進階技巧與最佳實踐

1. 共同好友功能

# 用戶 A 的好友
SADD friends:userA "user1" "user2" "user3" "user4"

# 用戶 B 的好友
SADD friends:userB "user2" "user3" "user5" "user6"

# 計算共同好友
SINTER friends:userA friends:userB
# 結果:user2, user3

# 推薦好友(B 的好友但不是 A 的)
SDIFF friends:userB friends:userA
# 結果:user5, user6

2. 標籤系統

# 為文章打標籤
SADD tags:post:100 "Redis" "Database" "NoSQL"

# 為標籤建立反向索引(找出有此標籤的文章)
SADD tag:Redis:posts "post:100" "post:101" "post:102"
SADD tag:Database:posts "post:100" "post:103"

# 找出同時有 Redis 和 Database 標籤的文章
SINTER tag:Redis:posts tag:Database:posts

3. UV(獨立訪客)統計

# 記錄訪客
SADD uv:page:home:20241018 "user:1" "user:2" "user:1"

# 取得 UV 數量
SCARD uv:page:home:20241018

# 注意:大量 UV 會佔用記憶體,考慮使用 HyperLogLog

4. 抽獎系統

# 參加抽獎
SADD lottery:event:2024 "user:1" "user:2" "user:3" ... "user:10000"

# 抽出 10 個中獎者(刪除)
SPOP lottery:event:2024 10

# 若要保留參與名單,用 SRANDMEMBER
SRANDMEMBER lottery:event:2024 10

5. Sorted Set(有序集合)

特性與應用場景

基本特性

  • 有序、不重複
  • 每個元素關聯一個 score(排序依據)
  • 底層為 skiplist + hashtable
  • 新增/刪除/查找 O(log n)
  • 範圍查詢 O(log n + m)

典型應用

  • 排行榜(遊戲分數、熱門文章)
  • 延遲佇列(score = 執行時間戳)
  • 時間序列資料
  • 範圍查詢(地理位置、價格區間)

CRUD 操作完整指南

Create & Update(新增/更新元素)

# 新增單個元素(score member)
ZADD leaderboard:game1 1000 "player:Alice"

# 新增多個元素
ZADD leaderboard:game1 950 "player:Bob" 1050 "player:Charlie" 800 "player:David"

# 更新選項(Redis 3.0.2+)
ZADD leaderboard:game1 NX 1100 "player:Eve"     # 僅當 member 不存在
ZADD leaderboard:game1 XX 1200 "player:Alice"   # 僅當 member 存在
ZADD leaderboard:game1 GT 1150 "player:Alice"   # 僅當新 score > 舊 score
ZADD leaderboard:game1 LT 900 "player:Bob"      # 僅當新 score < 舊 score

# 返回受影響的元素數量
ZADD leaderboard:game1 CH 1300 "player:Alice"   # 返回變更數(新增或更新)

# 整數增加 score
ZINCRBY leaderboard:game1 50 "player:Bob"        # Bob 分數 +50

Read(讀取元素)

# 按排名範圍取得元素(0-based,從低到高)
ZRANGE leaderboard:game1 0 9                    # 分數最低的 10 名
ZRANGE leaderboard:game1 0 -1                   # 所有元素

# 按排名範圍取得(從高到低)
ZREVRANGE leaderboard:game1 0 9                 # 分數最高的 10 名(排行榜)

# 按排名範圍取得(帶分數)
ZRANGE leaderboard:game1 0 9 WITHSCORES

# 按 score 範圍取得(-inf 表示負無窮,+inf 表示正無窮)
ZRANGEBYSCORE leaderboard:game1 900 1100        # score 在 900-1100 之間
ZRANGEBYSCORE leaderboard:game1 (900 1100       # score 在 (900, 1100](開區間)
ZRANGEBYSCORE leaderboard:game1 -inf +inf WITHSCORES LIMIT 0 10  # 分頁

# 按 score 範圍取得(從高到低)
ZREVRANGEBYSCORE leaderboard:game1 1100 900

# 取得元素數量
ZCARD leaderboard:game1

# 取得 score 範圍內的元素數量
ZCOUNT leaderboard:game1 900 1100

# 取得元素的 score
ZSCORE leaderboard:game1 "player:Alice"

# 取得元素的排名(從 0 開始,從低到高)
ZRANK leaderboard:game1 "player:Alice"

# 取得元素的排名(從高到低)
ZREVRANK leaderboard:game1 "player:Alice"       # 用於排行榜

Delete(刪除元素)

# 刪除指定元素
ZREM leaderboard:game1 "player:David"

# 刪除多個元素
ZREM leaderboard:game1 "player:A" "player:B"

# 按排名範圍刪除
ZREMRANGEBYRANK leaderboard:game1 0 4           # 刪除排名 0-4(最低的 5 名)

# 按 score 範圍刪除
ZREMRANGEBYSCORE leaderboard:game1 0 500        # 刪除 score 0-500 的元素

# 刪除整個 Sorted Set
DEL leaderboard:game1

進階技巧與最佳實踐

1. 遊戲排行榜

# 更新玩家分數
ZADD leaderboard:weekly 1580 "player:Alice"

# 取得 Top 10
ZREVRANGE leaderboard:weekly 0 9 WITHSCORES

# 取得玩家排名(從 1 開始需要 +1)
rank=$(redis-cli ZREVRANK leaderboard:weekly "player:Alice")
echo $((rank + 1))

# 取得玩家周圍的排名(前後各 5 名)
rank=$(redis-cli ZREVRANK leaderboard:weekly "player:Alice")
redis-cli ZREVRANGE leaderboard:weekly $((rank - 5)) $((rank + 5)) WITHSCORES

2. 延遲佇列(定時任務)

# 新增延遲任務(score = 執行時間的 Unix timestamp)
ZADD delayed_queue $(date -d "+5 minutes" +%s) "task:send_email:123"
ZADD delayed_queue $(date -d "+1 hour" +%s) "task:cleanup:old_data"

# 消費者:取出已到期的任務
current_time=$(date +%s)
redis-cli ZRANGEBYSCORE delayed_queue -inf $current_time LIMIT 0 100

# 取出並刪除(原子操作,使用 Lua)
redis-cli --eval pop_delayed_tasks.lua delayed_queue , $current_time

Lua 腳本 pop_delayed_tasks.lua

local tasks = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', ARGV[1], 'LIMIT', 0, 100)
if #tasks > 0 then
    redis.call('ZREM', KEYS[1], unpack(tasks))
end
return tasks

3. 時間序列資料(價格歷史)

# 記錄價格(score = timestamp)
ZADD price:BTC:USD 1697635200 "45000"
ZADD price:BTC:USD 1697638800 "45200"
ZADD price:BTC:USD 1697642400 "44800"

# 查詢特定時間範圍的價格
ZRANGEBYSCORE price:BTC:USD 1697635200 1697642400 WITHSCORES

# 取得最新價格
ZREVRANGE price:BTC:USD 0 0 WITHSCORES

4. 熱門文章(按瀏覽量排序)

# 文章被瀏覽,增加分數
ZINCRBY trending:posts 1 "post:1234"

# 取得 24 小時熱門文章(Top 10)
ZREVRANGE trending:posts:20241018 0 9 WITHSCORES

# 自動過期(每天清零)
EXPIRE trending:posts:20241018 86400

5. 集合運算

# 計算交集(取最小 score)
ZINTERSTORE result:inter 2 set1 set2 WEIGHTS 1 1 AGGREGATE MIN

# 計算聯集(取最大 score)
ZUNIONSTORE result:union 2 set1 set2 WEIGHTS 1 1 AGGREGATE MAX

# 計算加權聯集(score 相加)
ZUNIONSTORE result:weighted 2 set1 set2 WEIGHTS 0.7 0.3 AGGREGATE SUM

常見問題 FAQ

Q1: 何時使用 Hash,何時使用 String(JSON)?

答案:根據操作模式與過期需求決定

場景 推薦 原因
頻繁更新單一欄位 Hash 避免反序列化整個 JSON
需要欄位級別的增減操作 Hash HINCRBY 原子操作
需要整個物件過期 String (JSON) Hash 無法對單一 field 設定 TTL
需要原子更新多個欄位 String (JSON) + Lua Hash 的 HSET 無法保證多欄位原子性
欄位數量 < 100 且值較小 Hash ziplist 編碼省記憶體
需要在應用層做複雜查詢 String (JSON) 反序列化後使用程式語言能力

實際範例

# 使用 Hash(適合頻繁更新單一欄位)
HSET user:1000 login_count "0"
HINCRBY user:1000 login_count 1         # 每次登入 +1

# 使用 String(適合需要整體過期的 session)
SET session:abc123 '{"user_id":1000,"role":"admin"}' EX 3600

Q2: List vs Sorted Set,何時用哪個?

答案:視是否需要排序與範圍查詢

特性 List Sorted Set
有序性 插入順序 按 score 排序
重複元素 允許 不允許
頭尾操作 O(1) O(log n)
範圍查詢 按索引 O(n) 按 score O(log n + m)
排名查詢 不支援 O(log n)
適用場景 訊息佇列、最新動態 排行榜、延遲佇列

選擇建議

  • List:訊息佇列(FIFO)、最新 N 筆記錄、Undo/Redo
  • Sorted Set:排行榜、定時任務、時間序列、範圍查詢

Q3: 如何實現分頁查詢?

答案:根據資料結構選擇方法

List 分頁

# 第 1 頁(每頁 20 筆)
LRANGE messages:chat 0 19

# 第 2 頁
LRANGE messages:chat 20 39

# 第 n 頁(n 從 1 開始)
start=$((($n - 1) * $page_size))
end=$(($start + $page_size - 1))
LRANGE messages:chat $start $end

Sorted Set 分頁

# 按排名分頁(第 1 頁)
ZREVRANGE leaderboard 0 19 WITHSCORES

# 按 score 範圍分頁
ZRANGEBYSCORE timeline -inf +inf WITHSCORES LIMIT 0 20    # 第 1 頁
ZRANGEBYSCORE timeline -inf +inf WITHSCORES LIMIT 20 20   # 第 2 頁

Set/Hash 分頁

# Set 使用 SSCAN(游標分頁,避免阻塞)
SSCAN tags:post:1000 0 COUNT 20

# Hash 使用 HSCAN
HSCAN user:1000 0 COUNT 20

Q4: Redis 如何實現分散式鎖?

答案:使用 SET NX EX + Lua 釋放

正確實作

# 1. 加鎖(原子操作,帶過期時間,避免死鎖)
SET lock:resource:123 "server-1-uuid-12345" NX EX 10

# 2. 執行業務邏輯
# ...

# 3. 釋放鎖(使用 Lua 確保原子性,避免誤刪其他程序的鎖)
redis-cli --eval release_lock.lua lock:resource:123 , "server-1-uuid-12345"

release_lock.lua

if redis.call("GET", KEYS[1]) == ARGV[1] then
    return redis.call("DEL", KEYS[1])
else
    return 0
end

常見錯誤

  • ❌ 未設定過期時間 → 死鎖
  • ❌ 先 SETNX 再 EXPIRE → 非原子,可能死鎖
  • ❌ 釋放時未檢查 value → 可能誤刪其他程序的鎖

進階方案

  • Redlock 演算法(多節點分散式鎖)
  • RedissonLock(Java 客戶端,支援可重入鎖)

Q5: 如何避免 big key 問題?

答案:拆分大 key 或使用合適的資料結構

Big Key 的危害

  • 阻塞主執行緒(Redis 單執行緒)
  • 網路傳輸延遲
  • 記憶體碎片
  • 主從同步延遲

檢測 Big Key

# 使用 redis-cli 掃描
redis-cli --bigkeys

# 使用 MEMORY USAGE(Redis 4.0+)
MEMORY USAGE user:1000

# 使用 DEBUG OBJECT
DEBUG OBJECT large_hash

解決方案

1. Hash 拆分

# 不好:單一大 Hash
HSET user:1000 field1 value1
HSET user:1000 field2 value2
... (10萬個欄位)

# 好:拆分成多個小 Hash
HSET user:1000:0 field1 value1
HSET user:1000:1 field2 value2
... (每個 Hash 最多 1000 個欄位)

# 使用 hash 函數分片
shard=$(echo -n "field_name" | md5sum | cut -c1-2)  # 00-FF
HSET user:1000:$shard field_name value

2. List 拆分

# 按時間分片
LPUSH timeline:user:1000:202410 "post:1"
LPUSH timeline:user:1000:202411 "post:2"

3. String 壓縮

# 應用層壓縮(gzip, snappy)
compressed_data=$(gzip -c data.json)
SET cache:large_data "$compressed_data"

4. 使用 HyperLogLog(大量去重計數)

# 不好:使用 Set(記憶體 O(n))
SADD uv:20241018 "user:1" "user:2" ... "user:1000000"

# 好:使用 HyperLogLog(記憶體固定 12KB,誤差 0.81%)
PFADD uv:20241018 "user:1" "user:2" ... "user:1000000"
PFCOUNT uv:20241018

Q6: Redis 的記憶體淘汰策略如何選擇?

答案:根據業務需求選擇合適的 maxmemory-policy

8 種淘汰策略

策略 說明 適用場景
noeviction 記憶體滿時拒絕寫入(預設) 不允許資料遺失
allkeys-lru 所有 key 中淘汰最少使用 通用快取
allkeys-lfu 所有 key 中淘汰最不常用 熱點資料快取
allkeys-random 所有 key 中隨機淘汰 測試環境
volatile-lru 有過期時間的 key 中淘汰 LRU 混合場景(部分永久 key)
volatile-lfu 有過期時間的 key 中淘汰 LFU 熱點資料 + 永久 key
volatile-random 有過期時間的 key 中隨機淘汰 少用
volatile-ttl 淘汰最早過期的 key 時間敏感資料

配置方式

# redis.conf
maxmemory 2gb
maxmemory-policy allkeys-lru

# 或使用 CONFIG SET
CONFIG SET maxmemory-policy allkeys-lfu

選擇建議

  • 純快取allkeys-lruallkeys-lfu
  • 快取 + 持久化資料volatile-lruvolatile-lfu
  • 時效性資料volatile-ttl

Q7: 如何監控 Redis 效能?

答案:使用內建指令與外部監控工具

內建監控指令

# 1. 即時監控命令執行
redis-cli MONITOR

# 2. 查看伺服器資訊
redis-cli INFO
redis-cli INFO stats          # 統計資訊
redis-cli INFO memory         # 記憶體資訊
redis-cli INFO replication    # 主從複製資訊

# 3. 查看慢查詢
redis-cli SLOWLOG GET 10

# 4. 查看客戶端連線
redis-cli CLIENT LIST

# 5. 查看記憶體使用
redis-cli MEMORY STATS

# 6. 查看指令統計
redis-cli INFO commandstats

關鍵指標

指標 說明 正常範圍
used_memory 已使用記憶體 < maxmemory 的 80%
mem_fragmentation_ratio 記憶體碎片率 1.0 – 1.5
instantaneous_ops_per_sec 每秒操作數(QPS) 視硬體而定
keyspace_hits / keyspace_misses 快取命中率 > 80%
connected_clients 連線數 < maxclients
evicted_keys 淘汰的 key 數量 視策略而定

外部監控工具

  • Redis Exporter + Prometheus + Grafana(開源)
  • RedisInsight(官方 GUI 工具)
  • AWS CloudWatch(ElastiCache)
  • Datadog / New Relic(商業 APM)

最佳實踐總結

效能優化

  1. 使用 Pipeline 批次操作
    # 不好:多次網路往返
    for i in {1..100}; do
      redis-cli SET key:$i value$i
    done
    
    # 好:使用 Pipeline
    redis-cli --pipe < commands.txt
    
  2. 使用 Lua 腳本實現原子操作
    # 確保多個命令的原子性
    redis-cli --eval complex_operation.lua keys , args
    
  3. 合理設定過期時間
    • 使用隨機過期時間避免快取雪崩
    • 使用 SCAN 而非 KEYS 掃描(避免阻塞)

資料建模

  1. Key 命名規範
    業務:物件類型:ID:欄位
    user:profile:1000:name
    order:detail:20241018:123456
    cache:product:999
    
  2. 選擇合適的資料結構
    • 需要排序 → Sorted Set
    • 需要去重 → Set
    • 需要頻繁更新單一欄位 → Hash
    • 需要整體過期 → String
  3. 避免 Big Key
    • Hash/Set/Sorted Set 單個 key 不超過 10000 個元素
    • String 不超過 10MB

安全與可靠性

  1. 啟用持久化
    • RDB:定期快照(適合備份)
    • AOF:記錄每個寫操作(適合災難復原)
    • 混合持久化:RDB + AOF(Redis 4.0+)
  2. 設定密碼與權限
    # redis.conf
    requirepass your_strong_password
    
    # ACL(Redis 6.0+)
    ACL SETUSER alice on >password ~cache:* +get +set
    
  3. 使用主從複製與 Sentinel/Cluster
    • 主從複製:讀寫分離
    • Sentinel:自動故障轉移
    • Cluster:水平擴展

總結

深入理解 Redis 的五大資料結構與 CRUD 操作,是高效使用 Redis 的基礎。關鍵要點:

  • 📌 String:最簡單但最靈活,適合快取、計數、分散式鎖
  • 📌 Hash:適合物件儲存,但無法對單一 field 設定過期
  • 📌 List:適合訊息佇列、最新動態,但不支援排序查詢
  • 📌 Set:適合標籤、去重、集合運算
  • 📌 Sorted Set:適合排行榜、延遲佇列、時間序列

選擇正確的資料結構,能帶來數量級的效能提升與記憶體節省。

實踐建議:

  1. ✅ 根據業務場景選擇資料結構(而非全用 String)
  2. ✅ 使用 Pipeline 與 Lua 減少網路往返
  3. ✅ 避免 Big Key,合理拆分資料
  4. ✅ 設定合理的過期時間與淘汰策略
  5. ✅ 監控效能指標,及時優化

透過本指南的學習,您應能夠靈活運用 Redis 各種資料結構,打造高效能、可擴展的系統架構。

相關文章

Leave a Comment