首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

無縫快取讀取簡化實作:單一儲存槽搭配 Lambda 委派

     編輯:關於.NET

先前筆者發表《無縫快取讀取:雙儲存策略》,採用兩個儲存位址輪替以達無鎖讀取。經網友 Cat Chen 指點,發現雙指標切換其實屬過度設計 —— 單一儲存位址配合寫入時的執行緒同步,即可達成同樣的無縫讀取效果。本文將展示簡化後的架構與程式碼。

核心設計:Medium 快取介質類別

拋棄雙緩衝區指標,改用單一儲存槽加上狀態旗標:

namespace CHCache
{
    /// 快取介質
    public class Medium
    {
        /// 儲存區:存放實際快取實體
        public object Store { get; set; }

        /// 是否正在更新中
        public bool IsUpdating { get; set; }
    }
}

Store 保存快取資料本體;IsUpdating 標記該槽位是否正在進行寫入操作,讀取端據此判斷是否命中有效資料。

DictionaryCache:快取容器與讀寫邏輯

using System;
using System.Collections;
using System.Collections.Generic;
using System.Threading;

namespace CHCache
{
    public class DictionaryCache : IEnumerable
    {
        public DictionaryCache()
        {
            Store = new Dictionary<string, Medium>();
        }

        /// 新增/更新快取項目
        /// 快取鍵值
        /// 資料載入委派 (Lambda 表達式)
        public void Add(string key, Func<object> func)
        {
            if (Store.ContainsKey(key))
            {
                var elem = Store[key];
                if (elem.IsUpdating) return; // 正在更新,略過本次請求

                // 啟動背景執行緒重新載入
                var th = new ThreadHelper(elem, func);
                var td = new Thread(th.Doit);
                td.Start();
            }
            else
            {
                // 首次寫入:主執行緒同步執行,確保立即可用
                Console.WriteLine("Begin first write");
                Store.Add(key, new Medium { Store = func() });
                Console.WriteLine("End first write");
            }
        }

        /// 讀取快取資料
        public object this[string key]
        {
            get
            {
                if (!Store.ContainsKey(key)) return null;
                return Store[key].Store; // 直接回傳,無鎖讀取
            }
        }

        public Dictionary<string, Medium> Store { get; set; }

        public IEnumerator GetEnumerator()
        {
            return ((IEnumerable)Store).GetEnumerator();
        }
    }
}

關鍵點解析

  • 首次寫入同步執行:避免首次存取時快取為空,主執行緒直接呼叫 func() 取得資料。
  • 後續更新非同步化:偵測到 IsUpdating == false 時,啟動新執行緒執行 ThreadHelper.Doit,不阻塞呼叫端。
  • 讀取零同步開銷:索引器直接回傳 Store 參考,完全無鎖,極適合高併發讀取場景。

ThreadHelper:背景執行緒包裝類

using System;

namespace CHCache
{
    public class ThreadHelper
    {
        public Func<object> Fun { get; set; }
        public Medium Medium { get; set; }

        public ThreadHelper(Medium m, Func<object> fun)
        {
            Medium = m;
            Fun = fun;
        }

        public void Doit()
        {
            Medium.IsUpdating = true;
            Console.WriteLine("Begin write.");
            var ret = Fun.Invoke(); // 執行使用者提供的資料載入邏輯
            Medium.Store = ret;     // 原子性替換參考
            Console.WriteLine("End write.");
            Medium.IsUpdating = false;
        }
    }
}

Doit 方法流程清晰:設定旗標 → 執行載入 → 替換儲存體參考 → 清除旗標。參考替換在 .NET 中屬原子操作,讀取端要么看到舊資料,要么看到新資料,絕不會讀到半殘損狀態。

設計哲學反思

這是一個典型的「由簡入繁、由繁返簡」過程。初期受雙緩衝區思想影響,刻意引入指標切換機制;經同好提點後才發現,單一儲存槽配合 IsUpdating 旗標即能滿足「寫入不阻塞讀取、讀取不阻塞寫入」的核心需求。實務開發中,這類過度工程現象屢見不鮮,保持對本質問題的敏銳度至關重要。

適用場景與限制

  • ✅ 讀多寫少、資料載入耗時較長 (DB 查詢、外部 API 呼叫)
  • ✅ 允許短暫讀到舊資料 (最終一致性)
  • ❌ 強一致性要求場景 (需引入讀寫鎖或版本號機制)
  • ❌ 快取項目極大量 (Dictionary 本身鎖競爭會成為瓶頸,可改用 ConcurrentDictionary)

完整原始碼可參考筆者部落格附件下載。這個精簡版本不僅程式碼行數減少 40%,更消除了雙指標切換帶來的潛在競態風險,是實務專案中極佳的輕量級快取基石。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved