☕ NEW! 完成新手任務即可參加抽獎!LINE 星巴克禮券等你拿,名額有限!        🎉 推廣活動:邀請好友註冊 DevLearn,累積推薦抽 LINE 星巴克禮券! 活動詳情 →        🔥 活動期間 2026/4/1 - 5/31 |已有 0 人參加       
redis 進階

🚀 Redis 效能優化與監控

📌 Pipeline 批量操作

每個 Redis 命令都是一次網路往返(RTT),大量命令時網路延遲會成為瓶頸。

沒有 Pipeline:
Client → SET key1 → Server → OK → Client
Client → SET key2 → Server → OK → Client
Client → SET key3 → Server → OK → Client
= 3 次 RTT

有 Pipeline:
Client → SET key1, SET key2, SET key3 → Server
Server → OK, OK, OK → Client
= 1 次 RTT

.NET Pipeline 實作

var db = redis.GetDatabase();

// ❌ 逐一操作(慢)
for (int i = 0; i < 1000; i++)
    await db.StringSetAsync($"key:{i}", $"value:{i}");

// ✅ Pipeline 批量操作(快 10 倍以上)
var batch = db.CreateBatch();
var tasks = new List<Task>();

for (int i = 0; i < 1000; i++)
{
    tasks.Add(batch.StringSetAsync($"key:{i}", $"value:{i}"));
}

batch.Execute();
await Task.WhenAll(tasks);

// ✅ 或使用 FireAndForget(不需等待結果)
for (int i = 0; i < 1000; i++)
{
    db.StringSet($"key:{i}", $"value:{i}",
        flags: CommandFlags.FireAndForget);
}

📌 Lua Script 原子操作

Redis 執行 Lua 腳本時是 原子性 的,不會被其他命令打斷。

// 範例:限流器(每分鐘最多 100 次請求)
public class RateLimiter
{
    private readonly IDatabase _db;

    private const string LuaScript = @"
        local key = KEYS[1]
        local limit = tonumber(ARGV[1])
        local window = tonumber(ARGV[2])

        local current = tonumber(redis.call('GET', key) or '0')

        if current < limit then
            redis.call('INCR', key)
            if current == 0 then
                redis.call('EXPIRE', key, window)
            end
            return 1  -- 允許
        else
            return 0  -- 拒絕
        end";

    public async Task<bool> IsAllowed(string clientId)
    {
        var result = await _db.ScriptEvaluateAsync(
            LuaScript,
            new RedisKey[] { $"ratelimit:{clientId}" },
            new RedisValue[] { 100, 60 });  // 每 60 秒 100 次

        return (int)result == 1;
    }
}

// 在 Middleware 中使用
app.Use(async (context, next) =>
{
    var clientIp = context.Connection.RemoteIpAddress?.ToString() ?? "unknown";
    var limiter = context.RequestServices.GetRequiredService<RateLimiter>();

    if (!await limiter.IsAllowed(clientIp))
    {
        context.Response.StatusCode = 429;
        await context.Response.WriteAsync("Too Many Requests");
        return;
    }

    await next();
});

📌 大 Key 問題與解法

大 Key 會導致:記憶體不均、網路阻塞、刪除時阻塞其他操作。

# 找出大 Key
redis-cli --bigkeys

# 查看特定 Key 的記憶體用量
MEMORY USAGE user:sessions

解法

// ❌ 一個 key 存大量資料
await db.StringSetAsync("all_products",
    JsonSerializer.Serialize(allProducts));  // 可能 50MB!

// ✅ 拆分成多個小 key
foreach (var product in allProducts)
{
    await db.StringSetAsync(
        $"product:{product.Id}",
        JsonSerializer.Serialize(product));
}

// ❌ 一個 Hash 存百萬欄位
await db.HashSetAsync("user_scores",
    scores.Select(s => new HashEntry(s.UserId, s.Score)).ToArray());

// ✅ 分桶(Bucket)
foreach (var score in scores)
{
    var bucket = score.UserId % 100;  // 分成 100 個桶
    await db.HashSetAsync(
        $"user_scores:{bucket}",
        score.UserId.ToString(), score.Score);
}

📌 慢查詢日誌 SLOWLOG

# 設定慢查詢閾值(微秒,10000 = 10ms)
CONFIG SET slowlog-log-slower-than 10000

# 設定保留的慢查詢數量
CONFIG SET slowlog-max-len 128

# 查看慢查詢
SLOWLOG GET 10

# 查看慢查詢數量
SLOWLOG LEN

# 清除
SLOWLOG RESET

常見慢操作

操作 時間複雜度 建議
KEYS * O(N) 用 SCAN 替代
HGETALL (大 Hash) O(N) 只取需要的欄位
SMEMBERS (大 Set) O(N) 用 SSCAN 替代
DEL (大 key) O(N) 用 UNLINK 非同步刪除
FLUSHDB O(N) 用 FLUSHDB ASYNC
// ✅ 用 SCAN 替代 KEYS(不阻塞)
var server = redis.GetServer("localhost:6379");
await foreach (var key in server.KeysAsync(pattern: "product:*"))
{
    Console.WriteLine(key);
}

// ✅ 用 UNLINK 替代 DEL(非同步刪除大 key)
await db.KeyDeleteAsync("big_key", CommandFlags.FireAndForget);

📌 Redis INFO 監控指標

# 查看所有資訊
INFO

# 查看特定區段
INFO memory
INFO stats
INFO clients
INFO replication

重要監控指標

指標 說明 警戒值
used_memory 已用記憶體 接近 maxmemory
connected_clients 連線數 > 1000
instantaneous_ops_per_sec 每秒操作數 接近瓶頸
hit_rate 命中率 < 90%
evicted_keys 被淘汰的 key 數 > 0
blocked_clients 阻塞的客戶端 > 0
// .NET 中取得 Redis 資訊
var server = redis.GetServer("localhost:6379");
var info = await server.InfoAsync();

foreach (var group in info)
{
    Console.WriteLine($"=== {group.Key} ===");
    foreach (var pair in group)
        Console.WriteLine($"  {pair.Key}: {pair.Value}");
}

// 取得特定指標
var memoryInfo = (await server.InfoAsync("memory"))
    .SelectMany(g => g)
    .ToDictionary(p => p.Key, p => p.Value);
Console.WriteLine($"已用記憶體: {memoryInfo["used_memory_human"]}");

📌 持久化策略:RDB vs AOF

特性 RDB AOF
方式 定時快照 追加寫入日誌
檔案大小 較小 較大
恢復速度
資料安全 可能丟失最近快照後的資料 最多丟 1 秒
效能影響 fork 時短暫阻塞 持續寫入
# RDB 設定(redis.conf)
save 900 1      # 900 秒內至少 1 次修改則快照
save 300 10     # 300 秒內至少 10 次修改則快照
save 60 10000   # 60 秒內至少 10000 次修改則快照

# AOF 設定
appendonly yes
appendfsync everysec   # 每秒同步(推薦)
# appendfsync always   # 每次寫入都同步(最安全但慢)
# appendfsync no       # 由 OS 決定(最快但不安全)

推薦: 同時開啟 RDB + AOF,兼顧效能和安全。


📌 Azure Cache for Redis

// appsettings.json
{
  "ConnectionStrings": {
    "Redis": "your-cache.redis.cache.windows.net:6380,password=xxx,ssl=True,abortConnect=False"
  }
}

Azure 方案比較

方案 記憶體 價格/月 適用
Basic C0 250MB ~$16 開發測試
Standard C1 1GB ~$60 小型生產
Premium P1 6GB ~$200 企業級
Enterprise E10 12GB ~$400 大規模

📌 .NET 效能最佳實踐

// 1. ConnectionMultiplexer 必須是 Singleton
builder.Services.AddSingleton<IConnectionMultiplexer>(
    ConnectionMultiplexer.Connect(config));

// 2. 避免使用 KEYS,改用 SCAN
// ❌ db.Execute("KEYS", "*");
// ✅ server.Keys(pattern: "prefix:*");

// 3. 序列化用 System.Text.Json(比 Newtonsoft.Json 快)
var json = JsonSerializer.Serialize(obj);

// 4. 大量讀取用 Pipeline
var batch = db.CreateBatch();
var tasks = keys.Select(k => batch.StringGetAsync(k)).ToArray();
batch.Execute();
var results = await Task.WhenAll(tasks);

// 5. 不需要結果時用 FireAndForget
await db.StringSetAsync(key, value, flags: CommandFlags.FireAndForget);

// 6. 合理設定 TTL,避免記憶體膨脹
await db.StringSetAsync(key, value, TimeSpan.FromMinutes(30));

// 7. 使用 Hash 存物件(比整個 JSON 更省記憶體)
await db.HashSetAsync("user:1001", entries);

// 8. 監控連線池狀態
var status = redis.GetStatus();
Console.WriteLine(status);

🔑 重點整理

  1. Pipeline 把多個命令合併成一次網路往返,大幅提升效能
  2. Lua Script 保證原子性,適合限流器、分散式鎖等場景
  3. 避免 大 Key,用拆分或分桶策略
  4. SCAN 替代 KEYS,用 UNLINK 替代 DEL
  5. 同時開啟 RDB + AOF 持久化
  6. Azure Cache for Redis 是雲端最方便的方案

💡 大家的想法 · 0

載入中...
💬 即時聊天室 🟢 0 人在線
😀 😎 🤓 💻 🎮 🎸 🔥
➕ 新問題
📋 我的工單
💬 LINE 社群
🔒
需要註冊才能使用此功能
註冊帳號即可解鎖測驗、遊戲、簽到、筆記下載等所有功能,完全免費!
免費註冊