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

🏗️ 快取策略模式:Cache-Aside、Write-Through、Write-Behind

📌 四種常見的快取策略

策略 讀取 寫入 適用場景
Cache-Aside App 先查快取,Miss 再查 DB App 寫 DB,再清快取 最通用
Read-Through 快取自動從 DB 載入 - 讀多寫少
Write-Through - 同時寫快取和 DB 資料一致性高
Write-Behind - 先寫快取,非同步寫 DB 寫入量大

📌 Cache-Aside(旁路快取)— 最常用!

工作流程

讀取:
1. App 先查 Redis
2. 命中 → 直接回傳
3. 未命中 → 查 DB → 寫入 Redis → 回傳

寫入:
1. App 更新 DB
2. 刪除 Redis 中的快取(而非更新)

為什麼是「刪除」而非「更新」快取?

更新快取可能導致 並發問題: 兩個請求同時更新,A 先寫 DB 但後寫快取,快取就會是舊資料。 刪除快取讓下一次讀取自動載入最新資料,更安全。

.NET 完整實作

public class OrderService
{
    private readonly AppDbContext _db;
    private readonly IDistributedCache _cache;
    private readonly TimeSpan _cacheExpiry = TimeSpan.FromMinutes(10);

    public OrderService(AppDbContext db, IDistributedCache cache)
    {
        _db = db;
        _cache = cache;
    }

    // 讀取:Cache-Aside 模式
    public async Task<Order?> GetOrderAsync(int orderId)
    {
        var cacheKey = $"order:{orderId}";

        // Step 1: 查快取
        var cached = await _cache.GetStringAsync(cacheKey);
        if (cached != null)
        {
            Console.WriteLine($"[Cache HIT] order:{orderId}");
            return JsonSerializer.Deserialize<Order>(cached);
        }

        // Step 2: 快取 Miss,查 DB
        Console.WriteLine($"[Cache MISS] order:{orderId}");
        var order = await _db.Orders
            .Include(o => o.Items)
            .FirstOrDefaultAsync(o => o.Id == orderId);

        if (order == null) return null;

        // Step 3: 寫入快取
        await _cache.SetStringAsync(cacheKey,
            JsonSerializer.Serialize(order),
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = _cacheExpiry
            });

        return order;
    }

    // 寫入:更新 DB 後刪除快取
    public async Task UpdateOrderStatusAsync(int orderId, string status)
    {
        var order = await _db.Orders.FindAsync(orderId);
        if (order == null) throw new Exception("Order not found");

        order.Status = status;
        await _db.SaveChangesAsync();

        // 刪除快取(不是更新!)
        await _cache.RemoveAsync($"order:{orderId}");
        Console.WriteLine($"[Cache INVALIDATED] order:{orderId}");
    }
}

📌 Read-Through(穿透讀取)

快取層自動處理 DB 查詢,應用程式只跟快取互動。

// 概念實作:快取自動載入
public class ReadThroughCache<T>
{
    private readonly IDatabase _redis;
    private readonly Func<string, Task<T?>> _dataLoader;
    private readonly TimeSpan _ttl;

    public ReadThroughCache(
        IDatabase redis,
        Func<string, Task<T?>> dataLoader,
        TimeSpan ttl)
    {
        _redis = redis;
        _dataLoader = dataLoader;
        _ttl = ttl;
    }

    public async Task<T?> GetAsync(string key)
    {
        // 自動查快取
        var cached = await _redis.StringGetAsync(key);
        if (!cached.IsNullOrEmpty)
            return JsonSerializer.Deserialize<T>(cached!);

        // 自動從資料源載入
        var data = await _dataLoader(key);
        if (data != null)
        {
            await _redis.StringSetAsync(key,
                JsonSerializer.Serialize(data), _ttl);
        }
        return data;
    }
}

// 使用
var productCache = new ReadThroughCache<Product>(
    db, key => dbContext.Products.FindAsync(int.Parse(key.Split(':')[1])).AsTask(),
    TimeSpan.FromMinutes(10));

var product = await productCache.GetAsync("product:42");

📌 Write-Through(穿透寫入)

寫入時同時更新快取和 DB,確保兩者一致。

public class WriteThroughService
{
    private readonly AppDbContext _db;
    private readonly IDatabase _redis;

    public async Task SaveProductAsync(Product product)
    {
        // 同時寫 DB 和快取
        _db.Products.Update(product);
        await _db.SaveChangesAsync();

        var cacheKey = $"product:{product.Id}";
        await _redis.StringSetAsync(cacheKey,
            JsonSerializer.Serialize(product),
            TimeSpan.FromMinutes(30));

        // 兩者都成功才算完成
        Console.WriteLine($"[Write-Through] product:{product.Id} synced");
    }
}

優點: 快取永遠是最新的。 缺點: 寫入延遲增加(要寫兩個地方)。


📌 Write-Behind / Write-Back(回寫)

先寫入快取,非同步批量寫入 DB。

public class WriteBehindService
{
    private readonly IDatabase _redis;
    private readonly Channel<WriteTask> _writeQueue;

    // 寫入:只寫快取,任務放入佇列
    public async Task SaveAsync(string key, object data)
    {
        await _redis.StringSetAsync(key,
            JsonSerializer.Serialize(data));

        // 非同步寫入佇列
        await _writeQueue.Writer.WriteAsync(
            new WriteTask { Key = key, Data = data });
    }

    // 背景工作:批量寫入 DB
    public async Task ProcessWriteQueueAsync(CancellationToken ct)
    {
        var batch = new List<WriteTask>();

        await foreach (var task in _writeQueue.Reader.ReadAllAsync(ct))
        {
            batch.Add(task);

            // 累積 100 筆或每 5 秒批量寫入
            if (batch.Count >= 100)
            {
                await FlushToDatabase(batch);
                batch.Clear();
            }
        }
    }
}

優點: 寫入極快,DB 壓力小。 缺點: 快取掛掉可能丟失資料。


📌 策略比較表

特性 Cache-Aside Read-Through Write-Through Write-Behind
實作複雜度
讀取效能
寫入效能 - 極高
資料一致性 最終一致 最終一致 強一致 弱一致
資料遺失風險
適用場景 通用 讀多寫少 一致性要求高 寫入量極大

🔑 重點整理

  1. Cache-Aside 是最常用的策略,先查快取再查 DB
  2. 寫入後應 刪除快取,而非更新快取,避免並發問題
  3. Write-Through 適合需要高一致性的場景
  4. Write-Behind 效能最好但有資料遺失風險
  5. 大多數 .NET 專案用 Cache-Aside 就夠了

💡 大家的想法 · 0

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