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

🔐 Redis Session 管理與分散式鎖

📌 ASP.NET Core 分散式 Session

為什麼需要分散式 Session?

單機 Session(In-Memory):
User → Server A (Session 在這) ✅
User → Server B (沒有 Session) ❌  ← 負載平衡切換後 Session 不見了

分散式 Session(Redis):
User → Server A → Redis (Session) ✅
User → Server B → Redis (Session) ✅  ← 所有 Server 共用 Session

📌 設定 Redis Session Provider

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
// Program.cs
var builder = WebApplication.CreateBuilder(args);

// 1. 註冊 Redis 分散式快取
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration
        .GetConnectionString("Redis");
    options.InstanceName = "DevLearn:Session:";
});

// 2. 註冊 Session 服務
builder.Services.AddSession(options =>
{
    options.IdleTimeout = TimeSpan.FromMinutes(30);
    options.Cookie.HttpOnly = true;
    options.Cookie.IsEssential = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
});

var app = builder.Build();

// 3. 啟用 Session 中介軟體
app.UseSession();

使用 Session

// 設定 Session
public IActionResult Login(string username)
{
    HttpContext.Session.SetString("Username", username);
    HttpContext.Session.SetInt32("LoginCount",
        (HttpContext.Session.GetInt32("LoginCount") ?? 0) + 1);

    return Ok("登入成功");
}

// 讀取 Session
public IActionResult Profile()
{
    var username = HttpContext.Session.GetString("Username");
    if (username == null)
        return Unauthorized("請先登入");

    return Ok(new { Username = username });
}

// 存物件(需要 Extension Method)
public static class SessionExtensions
{
    public static void SetObject<T>(
        this ISession session, string key, T value)
    {
        session.SetString(key, JsonSerializer.Serialize(value));
    }

    public static T? GetObject<T>(
        this ISession session, string key)
    {
        var value = session.GetString(key);
        return value == null ? default : JsonSerializer.Deserialize<T>(value);
    }
}

// 使用
HttpContext.Session.SetObject("Cart", new ShoppingCart { Items = items });
var cart = HttpContext.Session.GetObject<ShoppingCart>("Cart");

📌 Session vs JWT 的取捨

特性 Session (Redis) JWT
狀態 有狀態(存在 Server) 無狀態(存在 Client)
儲存 Redis Cookie / LocalStorage
撤銷 容易(刪除 Session) 困難(需黑名單)
效能 每次要查 Redis 不需查 Server
大小 Cookie 只有 Session ID Token 可能很大
適用 傳統 Web API / 微服務

建議: 傳統 MVC 用 Session,SPA/API 用 JWT,也可以混合使用。


📌 分散式鎖(Distributed Lock)

為什麼需要分散式鎖?

沒有鎖的情況:
User A: 讀取庫存(10) → 扣減 → 寫入庫存(9)
User B: 讀取庫存(10) → 扣減 → 寫入庫存(9)  ← 應該是 8!
→ 超賣問題!

有分散式鎖:
User A: 取得鎖 → 讀取(10) → 扣減 → 寫入(9) → 釋放鎖
User B: 等待鎖... → 取得鎖 → 讀取(9) → 扣減 → 寫入(8) → 釋放鎖
→ 正確!

基本實作:SETNX

public class RedisDistributedLock
{
    private readonly IDatabase _db;

    public RedisDistributedLock(IConnectionMultiplexer redis)
    {
        _db = redis.GetDatabase();
    }

    // 取得鎖
    public async Task<bool> AcquireLockAsync(
        string lockKey, string lockValue, TimeSpan expiry)
    {
        // SETNX:只在 key 不存在時設定
        return await _db.StringSetAsync(
            lockKey, lockValue, expiry, When.NotExists);
    }

    // 釋放鎖(用 Lua 確保原子性)
    public async Task<bool> ReleaseLockAsync(
        string lockKey, string lockValue)
    {
        // 只有持有者才能釋放(防止誤刪別人的鎖)
        var script = @"
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end";

        var result = await _db.ScriptEvaluateAsync(
            script,
            new RedisKey[] { lockKey },
            new RedisValue[] { lockValue });

        return (int)result == 1;
    }
}

使用範例:庫存扣減

public class InventoryService
{
    private readonly RedisDistributedLock _lock;
    private readonly AppDbContext _db;

    public async Task<bool> DeductStock(int productId, int quantity)
    {
        var lockKey = $"lock:inventory:{productId}";
        var lockValue = Guid.NewGuid().ToString();
        var lockExpiry = TimeSpan.FromSeconds(10);

        // 嘗試取得鎖
        if (!await _lock.AcquireLockAsync(lockKey, lockValue, lockExpiry))
        {
            // 取不到鎖,可以重試或回傳失敗
            return false;
        }

        try
        {
            var product = await _db.Products.FindAsync(productId);
            if (product == null || product.Stock < quantity)
                return false;

            product.Stock -= quantity;
            await _db.SaveChangesAsync();
            return true;
        }
        finally
        {
            // 一定要釋放鎖!
            await _lock.ReleaseLockAsync(lockKey, lockValue);
        }
    }
}

📌 RedLock 演算法

在 Redis Cluster 環境中,單個 Redis 節點的鎖不夠可靠。 RedLock 要在多數節點上取得鎖才算成功。

RedLock 流程:
1. 取得當前時間 T1
2. 在 N 個 Redis 節點上嘗試取得鎖(短超時)
3. 在多數節點(N/2 + 1)成功取得 → 鎖定成功
4. 有效時間 = 初始 TTL - (T2 - T1)
5. 如果失敗 → 在所有節點釋放鎖
// 使用 RedLock.net 套件
// dotnet add package RedLock.net

using RedLockNet.SERedis;
using RedLockNet.SERedis.Configuration;

var endPoints = new List<RedLockMultiplexer>
{
    ConnectionMultiplexer.Connect("redis1:6379"),
    ConnectionMultiplexer.Connect("redis2:6379"),
    ConnectionMultiplexer.Connect("redis3:6379")
};

var redlockFactory = RedLockFactory.Create(endPoints);

// 取得分散式鎖
await using var redLock = await redlockFactory.CreateLockAsync(
    resource: "inventory:product:1001",
    expiryTime: TimeSpan.FromSeconds(30));

if (redLock.IsAcquired)
{
    // 安全地執行庫存扣減
    await DeductStock(1001, 1);
}
else
{
    Console.WriteLine("無法取得鎖,請稍後重試");
}

📌 避免死鎖的技巧

技巧 說明
設定 TTL 鎖一定要有過期時間,避免持有者掛掉永遠不釋放
唯一 lockValue 用 GUID 標識持有者,避免誤刪別人的鎖
Lua 原子釋放 用 Lua 腳本確保「檢查 + 刪除」是原子操作
重試機制 取不到鎖時,用指數退避重試
看門狗 長任務自動延長鎖的 TTL
// 指數退避重試
public async Task<bool> AcquireWithRetry(
    string lockKey, string lockValue,
    TimeSpan expiry, int maxRetries = 3)
{
    for (int i = 0; i < maxRetries; i++)
    {
        if (await _lock.AcquireLockAsync(lockKey, lockValue, expiry))
            return true;

        // 指數退避:100ms, 200ms, 400ms...
        await Task.Delay(TimeSpan.FromMilliseconds(100 * Math.Pow(2, i)));
    }
    return false;
}

🔑 重點整理

  1. Redis Session 讓多台 Server 共享 Session 資料
  2. Session vs JWT 各有優缺,按場景選擇
  3. 分散式鎖 用 SETNX + TTL + Lua 實作
  4. RedLock 適用於 Redis Cluster 的可靠鎖定
  5. 鎖一定要設 TTL唯一標識,避免死鎖

💡 大家的想法 · 0

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