為什麼需要了解 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-lru或allkeys-lfu - 快取 + 持久化資料:
volatile-lru或volatile-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)
最佳實踐總結
效能優化
- 使用 Pipeline 批次操作
# 不好:多次網路往返 for i in {1..100}; do redis-cli SET key:$i value$i done # 好:使用 Pipeline redis-cli --pipe < commands.txt - 使用 Lua 腳本實現原子操作
# 確保多個命令的原子性 redis-cli --eval complex_operation.lua keys , args - 合理設定過期時間
- 使用隨機過期時間避免快取雪崩
- 使用 SCAN 而非 KEYS 掃描(避免阻塞)
資料建模
- Key 命名規範
業務:物件類型:ID:欄位 user:profile:1000:name order:detail:20241018:123456 cache:product:999 - 選擇合適的資料結構
- 需要排序 → Sorted Set
- 需要去重 → Set
- 需要頻繁更新單一欄位 → Hash
- 需要整體過期 → String
- 避免 Big Key
- Hash/Set/Sorted Set 單個 key 不超過 10000 個元素
- String 不超過 10MB
安全與可靠性
- 啟用持久化
- RDB:定期快照(適合備份)
- AOF:記錄每個寫操作(適合災難復原)
- 混合持久化:RDB + AOF(Redis 4.0+)
- 設定密碼與權限
# redis.conf requirepass your_strong_password # ACL(Redis 6.0+) ACL SETUSER alice on >password ~cache:* +get +set - 使用主從複製與 Sentinel/Cluster
- 主從複製:讀寫分離
- Sentinel:自動故障轉移
- Cluster:水平擴展
總結
深入理解 Redis 的五大資料結構與 CRUD 操作,是高效使用 Redis 的基礎。關鍵要點:
- 📌 String:最簡單但最靈活,適合快取、計數、分散式鎖
- 📌 Hash:適合物件儲存,但無法對單一 field 設定過期
- 📌 List:適合訊息佇列、最新動態,但不支援排序查詢
- 📌 Set:適合標籤、去重、集合運算
- 📌 Sorted Set:適合排行榜、延遲佇列、時間序列
選擇正確的資料結構,能帶來數量級的效能提升與記憶體節省。
實踐建議:
- ✅ 根據業務場景選擇資料結構(而非全用 String)
- ✅ 使用 Pipeline 與 Lua 減少網路往返
- ✅ 避免 Big Key,合理拆分資料
- ✅ 設定合理的過期時間與淘汰策略
- ✅ 監控效能指標,及時優化
透過本指南的學習,您應能夠靈活運用 Redis 各種資料結構,打造高效能、可擴展的系統架構。