Redis 作為高效能的記憶體資料庫,在現代應用架構中扮演著關鍵角色。然而,記憶體資源始終是有限的,如何在 Ubuntu 22.04 環境下有效優化 Redis 的記憶體使用,是每位系統管理員和開發人員必須面對的課題。本文將深入探討各種記憶體優化策略,幫助您充分發揮 Redis 的效能潛力。
1. Redis 記憶體使用分析
在進行優化之前,我們需要先了解 Redis 目前的記憶體使用狀況。Redis 提供了豐富的命令來分析記憶體使用情況。
使用 INFO memory 命令
輸出範例:
1
2
3
4
5
6
7
8
9
10
11
12
13
| # Memory
used_memory:1073741824
used_memory_human:1.00G
used_memory_rss:1181116416
used_memory_rss_human:1.10G
used_memory_peak:1610612736
used_memory_peak_human:1.50G
used_memory_overhead:52428800
used_memory_startup:803424
used_memory_dataset:1021313024
used_memory_dataset_perc:95.12%
mem_fragmentation_ratio:1.10
mem_allocator:jemalloc-5.2.1
|
關鍵指標解讀
| 指標 | 說明 |
|---|
used_memory | Redis 分配的記憶體總量 |
used_memory_rss | 作業系統實際分配給 Redis 的記憶體 |
used_memory_peak | 記憶體使用峰值 |
mem_fragmentation_ratio | 記憶體碎片率(rss/used_memory) |
used_memory_dataset | 實際存儲資料使用的記憶體 |
使用 MEMORY DOCTOR 診斷
1
| redis-cli MEMORY DOCTOR
|
此命令會自動分析記憶體使用狀況並提供優化建議。
分析特定鍵的記憶體使用
1
2
3
4
5
| # 查看單一鍵的記憶體使用量
redis-cli MEMORY USAGE mykey
# 查看鍵的詳細資訊
redis-cli DEBUG OBJECT mykey
|
2. 資料結構選擇優化
Redis 提供多種資料結構,選擇合適的資料結構對記憶體效率至關重要。
資料結構記憶體效率比較
| 資料結構 | 適用場景 | 記憶體效率 |
|---|
| String | 簡單鍵值對、計數器 | 中等 |
| Hash | 物件屬性存儲 | 高(小 Hash 使用 ziplist) |
| List | 佇列、時間序列 | 高(使用 quicklist) |
| Set | 唯一值集合、標籤 | 中等(小 Set 使用 intset) |
| Sorted Set | 排行榜、優先佇列 | 中等(小 ZSet 使用 ziplist) |
使用 Hash 替代多個 String
不推薦的做法:
1
2
3
| SET user:1000:name "John"
SET user:1000:email "john@example.com"
SET user:1000:age "30"
|
推薦的做法:
1
| HSET user:1000 name "John" email "john@example.com" age "30"
|
Hash 分桶策略
對於大量小物件,可以使用 Hash 分桶來節省記憶體:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| import redis
import hashlib
def get_bucket_key(user_id, bucket_size=100):
bucket = int(user_id) // bucket_size
return f"users:{bucket}"
def get_field_key(user_id):
return str(user_id)
r = redis.Redis()
# 存儲使用者資料
user_id = 12345
bucket_key = get_bucket_key(user_id)
field_key = get_field_key(user_id)
r.hset(bucket_key, field_key, '{"name": "John", "email": "john@example.com"}')
# 讀取使用者資料
data = r.hget(bucket_key, field_key)
|
3. 記憶體淘汰策略(maxmemory-policy)
當 Redis 記憶體達到上限時,淘汰策略決定了如何處理新的寫入請求。
設定最大記憶體
編輯 Redis 設定檔 /etc/redis/redis.conf:
1
2
3
4
5
| # 設定最大記憶體為 2GB
maxmemory 2gb
# 或使用命令動態設定
redis-cli CONFIG SET maxmemory 2gb
|
淘汰策略選項
1
2
| # 設定淘汰策略
redis-cli CONFIG SET maxmemory-policy allkeys-lru
|
| 策略 | 說明 | 適用場景 |
|---|
noeviction | 不淘汰,寫入時返回錯誤 | 資料不可丟失的場景 |
allkeys-lru | 從所有鍵中淘汰最近最少使用的 | 通用快取 |
allkeys-lfu | 從所有鍵中淘汰最不常使用的 | 熱點資料明顯的場景 |
volatile-lru | 從設有過期時間的鍵中淘汰 LRU | 混合持久化與快取 |
volatile-lfu | 從設有過期時間的鍵中淘汰 LFU | 混合場景,熱點明顯 |
allkeys-random | 隨機淘汰 | 訪問模式隨機 |
volatile-random | 從過期鍵中隨機淘汰 | 測試環境 |
volatile-ttl | 淘汰 TTL 最短的鍵 | 需要控制過期順序 |
LRU 取樣數設定
1
2
| # 增加取樣數可提高 LRU 準確度,但會消耗更多 CPU
redis-cli CONFIG SET maxmemory-samples 10
|
設定範例
1
2
3
4
| # /etc/redis/redis.conf
maxmemory 4gb
maxmemory-policy allkeys-lfu
maxmemory-samples 10
|
4. 編碼優化(ziplist、intset)
Redis 針對小型資料結構使用特殊的緊湊編碼格式來節省記憶體。
Ziplist 編碼
Ziplist 是一種緊湊的雙向鏈表,適用於小型 Hash、List 和 Sorted Set。
1
2
3
4
5
6
7
8
9
10
| # Hash 使用 ziplist 的條件
hash-max-ziplist-entries 512 # 元素數量閾值
hash-max-ziplist-value 64 # 單一值大小閾值(位元組)
# List 使用 quicklist(基於 ziplist)
list-max-ziplist-size -2 # -2 表示每個 ziplist 最大 8KB
# Sorted Set 使用 ziplist 的條件
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
|
查看鍵的編碼方式
1
| redis-cli OBJECT ENCODING mykey
|
可能的編碼類型:
embstr:短字串(<=44 位元組)int:整數raw:長字串ziplist:壓縮列表quicklist:快速列表(List 專用)hashtable:雜湊表intset:整數集合skiplist:跳躍表
Intset 編碼
當 Set 只包含整數且數量較小時,使用 intset 編碼:
1
2
| # Set 使用 intset 的條件
set-max-intset-entries 512
|
優化設定範例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # /etc/redis/redis.conf
# Hash 優化
hash-max-ziplist-entries 512
hash-max-ziplist-value 64
# List 優化
list-max-ziplist-size -2
list-compress-depth 1
# Set 優化
set-max-intset-entries 512
# Sorted Set 優化
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
|
驗證編碼效果
1
2
3
4
5
6
7
8
9
| # 建立測試資料
redis-cli HSET test:hash field1 value1 field2 value2
# 檢查編碼
redis-cli OBJECT ENCODING test:hash
# 輸出: "ziplist" 或 "listpack"
# 檢查記憶體使用
redis-cli MEMORY USAGE test:hash
|
5. 過期鍵清理機制
Redis 使用兩種機制清理過期鍵:惰性刪除和定期刪除。
惰性刪除
當客戶端訪問一個鍵時,Redis 會檢查該鍵是否過期。如果過期,則刪除。
定期刪除
Redis 定期掃描帶有過期時間的鍵,並刪除已過期的鍵。
1
2
3
| # 調整定期刪除的頻率(預設 10,範圍 1-500)
# 數值越高,CPU 使用越高,但過期鍵清理越及時
hz 10
|
設定過期時間的最佳實踐
1
2
3
4
5
6
7
8
9
10
11
| # 設定鍵和過期時間(原子操作)
redis-cli SET session:abc123 "user_data" EX 3600
# 為現有鍵設定過期時間
redis-cli EXPIRE mykey 3600
# 設定精確到毫秒的過期時間
redis-cli PEXPIRE mykey 3600000
# 設定絕對過期時間
redis-cli EXPIREAT mykey 1735689600
|
過期鍵統計
1
2
| # 查看過期鍵相關統計
redis-cli INFO stats | grep expired
|
輸出範例:
1
2
3
| expired_keys:123456
expired_stale_perc:0.50
expired_time_cap_reached_count:0
|
批量設定過期時間
1
2
3
4
5
6
7
8
9
10
11
| import redis
r = redis.Redis()
pipe = r.pipeline()
# 為多個鍵設定過期時間
keys_to_expire = ['key1', 'key2', 'key3']
for key in keys_to_expire:
pipe.expire(key, 3600)
pipe.execute()
|
6. 記憶體碎片處理
記憶體碎片會導致 Redis 實際使用的記憶體超過存儲資料所需的記憶體。
識別記憶體碎片
1
| redis-cli INFO memory | grep fragmentation
|
輸出:
1
2
| mem_fragmentation_ratio:1.52
mem_fragmentation_bytes:536870912
|
mem_fragmentation_ratio > 1.5:存在嚴重碎片問題mem_fragmentation_ratio < 1:表示使用了 swap,效能會大幅下降
啟用自動碎片整理
Redis 4.0+ 支援自動記憶體碎片整理:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| # /etc/redis/redis.conf
# 啟用自動碎片整理
activedefrag yes
# 碎片率超過此值時開始整理
active-defrag-ignore-bytes 100mb
# 碎片率閾值
active-defrag-threshold-lower 10
active-defrag-threshold-upper 100
# CPU 使用限制
active-defrag-cycle-min 1
active-defrag-cycle-max 25
# 每次掃描的最大鍵數
active-defrag-max-scan-fields 1000
|
手動觸發碎片整理
1
2
| # 手動觸發碎片整理(Redis 4.0+)
redis-cli MEMORY PURGE
|
監控碎片整理狀態
1
| redis-cli INFO memory | grep defrag
|
輸出:
1
2
3
4
5
| active_defrag_running:0
active_defrag_hits:1234
active_defrag_misses:5678
active_defrag_key_hits:100
active_defrag_key_misses:50
|
7. 大鍵識別與處理
大鍵(Big Key)是指佔用大量記憶體的單一鍵,它們可能導致效能問題和記憶體浪費。
識別大鍵
使用 redis-cli 掃描
1
2
3
4
5
| # 掃描大鍵(--bigkeys 選項)
redis-cli --bigkeys
# 使用取樣方式掃描
redis-cli --bigkeys -i 0.1
|
輸出範例:
1
2
3
4
5
6
7
8
| # Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.
[00.00%] Biggest string found so far 'large_string' with 10485760 bytes
[00.00%] Biggest hash found so far 'user:sessions' with 50000 fields
[00.00%] Biggest list found so far 'queue:tasks' with 100000 items
[00.00%] Biggest set found so far 'followers:123' with 80000 members
[00.00%] Biggest zset found so far 'leaderboard' with 200000 members
|
使用 MEMORY USAGE 分析
1
2
| # 查看特定鍵的記憶體使用
redis-cli MEMORY USAGE large_key SAMPLES 0
|
使用 Python 腳本掃描
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
| import redis
def find_big_keys(r, threshold_bytes=1048576):
"""找出超過閾值的大鍵"""
big_keys = []
cursor = 0
while True:
cursor, keys = r.scan(cursor=cursor, count=1000)
for key in keys:
try:
memory = r.memory_usage(key)
if memory and memory > threshold_bytes:
key_type = r.type(key).decode()
big_keys.append({
'key': key.decode(),
'type': key_type,
'memory': memory,
'memory_human': f"{memory / 1048576:.2f} MB"
})
except Exception as e:
continue
if cursor == 0:
break
return sorted(big_keys, key=lambda x: x['memory'], reverse=True)
r = redis.Redis()
big_keys = find_big_keys(r, threshold_bytes=10*1024*1024) # 10MB
for key in big_keys:
print(f"{key['key']}: {key['memory_human']} ({key['type']})")
|
處理大鍵
分割大 Hash
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| import redis
import json
def split_large_hash(r, source_key, chunk_size=1000):
"""將大 Hash 分割成多個小 Hash"""
cursor = 0
chunk_index = 0
while True:
cursor, data = r.hscan(source_key, cursor=cursor, count=chunk_size)
if data:
new_key = f"{source_key}:chunk:{chunk_index}"
r.hset(new_key, mapping=data)
chunk_index += 1
if cursor == 0:
break
return chunk_index
r = redis.Redis()
chunks = split_large_hash(r, 'large_hash')
print(f"Split into {chunks} chunks")
|
漸進式刪除大鍵
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
| import redis
import time
def delete_large_key(r, key, batch_size=1000, delay=0.01):
"""漸進式刪除大鍵,避免阻塞"""
key_type = r.type(key).decode()
if key_type == 'hash':
cursor = 0
while True:
cursor, fields = r.hscan(key, cursor=cursor, count=batch_size)
if fields:
r.hdel(key, *fields.keys())
if cursor == 0:
break
time.sleep(delay)
elif key_type == 'set':
while r.scard(key) > 0:
members = r.srandmember(key, batch_size)
if members:
r.srem(key, *members)
time.sleep(delay)
elif key_type == 'zset':
while r.zcard(key) > 0:
r.zremrangebyrank(key, 0, batch_size - 1)
time.sleep(delay)
elif key_type == 'list':
while r.llen(key) > 0:
r.ltrim(key, batch_size, -1)
time.sleep(delay)
else:
r.delete(key)
r = redis.Redis()
delete_large_key(r, 'very_large_hash')
|
8. 監控與調校工具
Redis 內建監控命令
INFO 命令
1
2
3
4
5
6
7
8
| # 查看所有資訊
redis-cli INFO
# 查看特定部分
redis-cli INFO memory
redis-cli INFO stats
redis-cli INFO clients
redis-cli INFO replication
|
MONITOR 命令(謹慎使用)
1
2
| # 即時監控所有命令(會影響效能)
redis-cli MONITOR
|
SLOWLOG 慢查詢日誌
1
2
3
4
5
6
7
8
9
10
11
| # 設定慢查詢閾值(微秒)
redis-cli CONFIG SET slowlog-log-slower-than 10000
# 設定慢查詢日誌最大長度
redis-cli CONFIG SET slowlog-max-len 128
# 查看慢查詢日誌
redis-cli SLOWLOG GET 10
# 重置慢查詢日誌
redis-cli SLOWLOG RESET
|
使用 redis-stat 監控
1
2
3
4
5
6
| # 安裝 redis-stat
sudo apt-get install ruby ruby-dev
sudo gem install redis-stat
# 啟動監控(每秒更新)
redis-stat --server=8080 1
|
使用 Prometheus 和 Grafana
安裝 Redis Exporter
1
2
3
4
5
6
7
| # 下載 Redis Exporter
wget https://github.com/oliver006/redis_exporter/releases/download/v1.54.0/redis_exporter-v1.54.0.linux-amd64.tar.gz
tar xvfz redis_exporter-v1.54.0.linux-amd64.tar.gz
cd redis_exporter-v1.54.0.linux-amd64
# 啟動 Exporter
./redis_exporter --redis.addr=redis://localhost:6379
|
Prometheus 設定
1
2
3
4
5
| # /etc/prometheus/prometheus.yml
scrape_configs:
- job_name: 'redis'
static_configs:
- targets: ['localhost:9121']
|
自動化監控腳本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| #!/bin/bash
# /usr/local/bin/redis-memory-check.sh
REDIS_CLI="redis-cli"
THRESHOLD_MB=3000
ALERT_EMAIL="admin@example.com"
# 取得記憶體使用量(MB)
USED_MEMORY=$($REDIS_CLI INFO memory | grep used_memory: | cut -d: -f2 | tr -d '\r')
USED_MEMORY_MB=$((USED_MEMORY / 1024 / 1024))
# 取得碎片率
FRAG_RATIO=$($REDIS_CLI INFO memory | grep mem_fragmentation_ratio | cut -d: -f2 | tr -d '\r')
# 記錄到日誌
echo "$(date '+%Y-%m-%d %H:%M:%S') - Memory: ${USED_MEMORY_MB}MB, Fragmentation: ${FRAG_RATIO}" >> /var/log/redis-memory.log
# 發送警告
if [ $USED_MEMORY_MB -gt $THRESHOLD_MB ]; then
echo "Redis memory usage is high: ${USED_MEMORY_MB}MB" | mail -s "Redis Memory Alert" $ALERT_EMAIL
fi
|
設定 cron 定時執行:
1
2
| # 每 5 分鐘執行一次
*/5 * * * * /usr/local/bin/redis-memory-check.sh
|
完整的監控 Dashboard 指標
建議監控以下關鍵指標:
| 指標 | 說明 | 警告閾值 |
|---|
used_memory | 記憶體使用量 | > 80% maxmemory |
mem_fragmentation_ratio | 碎片率 | > 1.5 |
connected_clients | 連線數 | > 1000 |
blocked_clients | 阻塞客戶端數 | > 0 |
evicted_keys | 被淘汰的鍵數 | 持續增加 |
expired_keys | 過期的鍵數 | 觀察趨勢 |
keyspace_hits/misses | 命中率 | < 90% |
instantaneous_ops_per_sec | 每秒操作數 | 觀察趨勢 |
總結
Redis 記憶體優化是一個持續的過程,需要根據實際使用情況不斷調整。以下是關鍵建議:
- 定期監控:使用 INFO memory 和監控工具追蹤記憶體使用趨勢
- 選擇合適的資料結構:優先使用 Hash 存儲物件,利用編碼優化
- 設定合理的淘汰策略:根據業務場景選擇 LRU 或 LFU
- 控制鍵的大小:避免大鍵,使用分桶策略
- 設定過期時間:為快取資料設定合理的 TTL
- 處理記憶體碎片:啟用自動碎片整理
- 建立告警機制:在記憶體使用超標時及時通知
透過以上策略的綜合運用,您可以在 Ubuntu 22.04 環境下充分優化 Redis 的記憶體使用效率,確保系統穩定高效運行。