Stories

Detail Return Return

C#.NET MemoryCache 深入解析:本地緩存機制與最佳實踐 - Stories Detail

簡介

在許多應用程序中,緩存是提升性能的常見方法,尤其是在訪問頻繁且不經常變化的數據時。MemoryCache.NET 提供的一個內存緩存實現,它允許在內存中存儲數據,以減少對數據庫、文件系統或其他遠程服務的訪問,進而提升系統響應速度。

MemoryCache 的核心優勢是:

  • 高效:內存操作非常快速,適合用於緩存短期有效的數據。
  • 輕量:它是一個線程安全的緩存系統,且易於在 .NET 應用中配置和使用。
  • 靈活:支持過期時間、優先級設置等多種功能,能夠滿足大多數緩存需求。

核心功能

  • 線程安全

    • MemoryCache 是線程安全的,允許多個線程同時訪問緩存中的數據。
  • 過期策略

    • 絕對過期:指定一個具體的時間點,緩存項在該時間點後過期。
    • 滑動過期:緩存項最後訪問後的指定時間段內過期。
  • 緩存項優先級

    • 可以為緩存項設置優先級,允許緩存管理器在內存不足時根據優先級回收緩存項。
  • 回調:

    • PostEvictionCallback:緩存項移除時觸發回調。
    • CacheEntryOptions:支持自定義過期和移除邏輯。
  • 依賴關係:

    • 使用 ChangeToken 支持基於外部信號的緩存失效(如文件更改)。
  • 線程安全:內置併發控制,支持多線程訪問。
  • DI 集成:通過 IMemoryCache 接口與 ASP.NET Core DI 無縫集成。
  • 數據大小限制

    • 可以設置緩存的最大容量,以防止佔用過多內存。
  • 支持絕對過期和滑動過期組合使用

    • 能靈活配置緩存失效的時間策略。

核心 API

MemoryCache 主要通過 IMemoryCache 接口操作,位於 Microsoft.Extensions.Caching.Memory 命名空間。核心 API 如下:

  • 接口方法:

    • ICacheEntry CreateEntry(object key):創建緩存項。
    • bool TryGetValue(object key, out object value):嘗試獲取緩存值。
    • void Remove(object key):移除緩存項。
    • TItem Get<TItem>(object key):獲取指定類型的緩存值。
    • TItem GetOrCreate<TItem>(object key, Func<ICacheEntry, TItem> factory):獲取或創建緩存值。
    • Task<TItem> GetOrCreateAsync<TItem>(object key, Func<ICacheEntry, Task<TItem>> factory):異步獲取或創建。
    • TItem Set<TItem>(object key, TItem value, MemoryCacheEntryOptions options = null):設置緩存值。
  • MemoryCacheEntryOptions

    • DateTimeOffset? AbsoluteExpiration:絕對過期時間。
    • TimeSpan? AbsoluteExpirationRelativeToNow:相對當前時間的絕對過期。
    • TimeSpan? SlidingExpiration:滑動過期時間。
    • CacheItemPriority Priority:緩存項優先級。
    • IChangeToken ExpirationTokens:依賴的變更令牌。
    • Action<ICacheEntry, EvictionReason, object> PostEvictionCallbacks:移除回調。
    • long? Size:緩存項大小(用於大小限制,.NET 6+)。
  • MemoryCacheOptions

    • TimeSpan ExpirationScanFrequency:過期掃描間隔(默認 1 分鐘)。
    • long? SizeLimit:緩存總大小限制(.NET 6+)。
    • double CompactionPercentage:內存壓力下壓縮比例(.NET 6+)。
注意:
舊版本.net 使用 System.Runtime.Caching
新版本.net 使用 Microsoft.Extensions.Caching.Memory

API 用法

創建與初始化 MemoryCache

using System.Runtime.Caching;

// 創建默認的 MemoryCache 實例
MemoryCache cache = MemoryCache.Default;

// 或者創建帶名稱的實例
MemoryCache customCache = new MemoryCache("MyCache");

添加緩存項

CacheItemPolicy policy = new CacheItemPolicy
{
    AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(10), // 設置絕對過期時間
    SlidingExpiration = TimeSpan.FromMinutes(5),           // 設置滑動過期時間
    Priority = CacheItemPriority.Default,                  // 設置優先級
    RemovedCallback = args => { Console.WriteLine("緩存項已移除"); } // 過期時執行回調
};

cache.Add("key", "value", policy); // 向緩存中添加項

獲取緩存項

var value = cache.Get("key"); // 獲取緩存項
Console.WriteLine(value); // 輸出:value

更新緩存項

cache.Set("key", "newValue", DateTimeOffset.Now.AddMinutes(5)); // 更新緩存項

移除緩存項

cache.Remove("key"); // 移除緩存項

使用 TryGetValue 方法檢查緩存項

object value;
if (cache.TryGetValue("key", out value))
{
    Console.WriteLine(value); // 如果存在,打印緩存值
}
else
{
    Console.WriteLine("緩存項不存在"); // 如果不存在,打印提示
}

獲取所有緩存項(遍歷)

foreach (var item in cache)
{
    Console.WriteLine($"Key: {item.Key}, Value: {item.Value}");
}

自定義緩存實例

// 創建帶自定義配置的緩存
var cacheConfig = new NameValueCollection
{
    {"cacheMemoryLimitMegabytes", "100"}, // 100MB 內存限制
    {"physicalMemoryLimitPercentage", "50"}, // 物理內存50%
    {"pollingInterval", "00:05:00"} // 5分鐘檢查一次
};

var customCache = new MemoryCache("MyCustomCache", cacheConfig);

// 使用自定義緩存
customCache.Set("userData", userProfile, new CacheItemPolicy());

優先級策略

var highPriorityPolicy = new CacheItemPolicy
{
    Priority = CacheItemPriority.NotRemovable // 內存不足時不會被移除
};

var lowPriorityPolicy = new CacheItemPolicy
{
    Priority = CacheItemPriority.Default // 默認優先級
};

ASP.NET Core 中通過 DI 使用 IMemoryCache

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using System;

[ApiController]
[Route("api/cache")]
public class CacheController : ControllerBase
{
    private readonly IMemoryCache _cache;

    public CacheController(IMemoryCache cache)
    {
        _cache = cache;
    }

    [HttpGet("set")]
    public IActionResult SetCache()
    {
        var key = "myKey";
        var value = $"Data at {DateTime.Now:HH:mm:ss}";
        var options = new MemoryCacheEntryOptions
        {
            AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5),
            SlidingExpiration = TimeSpan.FromSeconds(30)
        };

        _cache.Set(key, value, options);
        return Ok($"Cached: {value}");
    }

    [HttpGet("get")]
    public IActionResult GetCache()
    {
        if (_cache.TryGetValue("myKey", out string value))
            return Ok($"Cached value: {value}");
        return NotFound("Cache miss");
    }
}

註冊服務:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddMemoryCache(); // 註冊 MemoryCache
builder.Services.AddControllers();

緩存依賴(ChangeToken)

基於外部信號失效(如文件更改):

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.FileProviders;
using System;
using System.IO;
using System.Threading.Tasks;

[ApiController]
[Route("api/config")]
public class ConfigController : ControllerBase
{
    private readonly IMemoryCache _cache;
    private readonly PhysicalFileProvider _fileProvider;

    public ConfigController(IMemoryCache cache)
    {
        _cache = cache;
        _fileProvider = new PhysicalFileProvider(Directory.GetCurrentDirectory());
    }

    [HttpGet]
    public async Task<IActionResult> GetConfig()
    {
        var cacheKey = "configKey";
        var config = await _cache.GetOrCreateAsync(cacheKey, async entry =>
        {
            var file = _fileProvider.GetFileInfo("config.txt");
            entry.AddExpirationToken(_fileProvider.Watch("config.txt"));
            entry.SlidingExpiration = TimeSpan.FromMinutes(5);
            // 讀取文件
            string content = await File.ReadAllTextAsync(file.PhysicalPath);
            return content;
        });

        return Ok(config);
    }
}

説明:

  • 使用 PhysicalFileProvider.Watch 創建 IChangeToken
  • 文件更改時緩存失效,重新加載。

性能優化

避免過多緩存數據

設置合理的緩存大小和最大容量,避免 MemoryCache 佔用過多內存。可以通過設置緩存的最大項數或緩存最大內存大小來控制。

var policy = new CacheItemPolicy
{
    AbsoluteExpiration = DateTimeOffset.Now.AddMinutes(10)
};
var cache = new MemoryCache("MyCache", new NameValueCollection
{
    { "CacheMemoryLimitMegabytes", "100" },  // 限制最大內存為 100 MB
    { "PhysicalMemoryLimitPercentage", "80" }, // 限制物理內存佔用為 80%
    { "PollingInterval", "00:01:00" } // 每 1 分鐘檢查一次緩存
});

合理使用過期策略

  • 對於一些短時間有效的數據,建議使用滑動過期(SlidingExpiration),它可以保證數據的最新性。

    • 對於長期不變的數據,可以使用絕對過期(AbsoluteExpiration),避免內存被長時間佔用。

避免緩存穿透

  • 在向緩存寫入數據之前,確保數據已經在某些存儲中存在,避免因緩存項缺失導致每次訪問都去請求外部數據庫。
  • 如果緩存為空,可以緩存空值,以防止緩存穿透。
public T GetOrCreate<T>(string key, Func<T> factory, TimeSpan expiration)
{
    if (cache.Get(key) is T result) 
        return result;
    
    // 特殊值標記緩存穿透
    if ("__NULL__".Equals(cache.Get(key)))
        return default;
    
    try
    {
        result = factory();
        if (result == null)
        {
            // 緩存空值避免反覆查詢
            cache.Set(key, "__NULL__", TimeSpan.FromMinutes(5));
            return default;
        }
        
        cache.Set(key, result, expiration);
        return result;
    }
    catch
    {
        // 異常時設置短期空緩存
        cache.Set(key, "__NULL__", TimeSpan.FromMinutes(1));
        throw;
    }
}

合理設置回調函數

  • 使用 RemovedCallback 來處理過期的緩存項,執行資源清理或日誌記錄等操作。
CacheItemPolicy policy = new CacheItemPolicy
{
    RemovedCallback = args =>
    {
        Console.WriteLine($"緩存項 {args.CacheItem.Key} 被移除");
    }
};

定期清理

  • 可以通過 PollingInterval 來定期檢查緩存並進行清理,避免緩存中存放過期數據。

緩存統計監控

// 獲取緩存統計信息
var stats = ((MemoryCache)cache).GetCacheStatistics();

Console.WriteLine($"緩存命中率: {stats.CacheHitRatio:P}");
Console.WriteLine($"總緩存項: {stats.TotalCount}");
Console.WriteLine($"緩存大小: {stats.CacheSizeKB} KB");
Console.WriteLine($"內存限制: {stats.MemoryLimitMB} MB");

優缺點

優點

  • 高性能:進程內緩存,訪問速度快。
  • 輕量簡單:無外部依賴,易於集成。
  • 靈活過期:支持絕對、滑動和依賴失效。
  • 異步支持:GetOrCreateAsync 適合異步加載。
  • DI 集成:與 ASP.NET Core 無縫結合。
  • 內存管理:.NET 6+ 支持大小限制和壓縮。

缺點

  • 進程內限制:數據不跨進程或實例共享,重啓丟失。
  • 內存佔用:大數據量可能導致內存壓力。
  • 無分佈式支持:不適合多實例部署(需用 Redis)。
  • 手動管理:需顯式設置鍵和過期策略。
  • 有限功能:無高級功能(如標籤、區域緩存)。

常見使用場景

適用場景

  • 數據緩存

    • Web 應用中緩存數據庫查詢結果、API 請求結果等,減少數據庫查詢的壓力,提高響應速度。
    • 示例:緩存用户信息、商品列表等。
  • 會話存儲

    • 使用緩存來存儲用户會話信息,避免每次請求都查詢數據庫。
    • 示例:存儲用户的登錄狀態或臨時數據。
  • 頻繁訪問的數據

    • 在頻繁讀取但不常更新的數據上使用緩存來提高性能。
    • 示例:熱點新聞、熱銷商品、排行榜等。
  • API 限流

    • 用於控制 API 調用頻率,存儲用户請求的時間戳,避免過多的請求對後台服務產生壓力。
    • 示例:API 請求次數的緩存,防止暴力請求。
  • 耗時操作結果緩存

    • 對於計算成本較高的操作,可以將結果緩存起來,減少重複計算。
    • 示例:圖片處理後的結果、文件讀取操作的緩存等。

不適用場景

  • 分佈式系統共享數據
  • 大型數據集(大於內存限制)
  • 需要持久化的數據
  • 嚴格的實時數據一致性要求

與相關技術對比

特性 MemoryCache HttpRuntime.Cache IDistributedCache Redis
存儲位置 內存 內存 分佈式存儲 分佈式
應用範圍 通用 通用 分佈式系統 分佈式
性能 最高 中高
持久化
過期策略 豐富 豐富 基礎 豐富
集羣支持
.NET Core

總結

MemoryCache.NET 中輕量高效的進程內緩存組件,適合高頻讀取、低更新頻率的場景,如熱點數據、配置或計算結果。它提供靈活的過期策略、回調和依賴失效,支持異步操作和 ASP.NET Core DI。相比 IDistributedCache(如 Redis),它性能更高但限於單實例。

資源和文檔

  • MSDN 文檔: https://learn.microsoft.com/zh-cn/dotnet/api/system.runtime.c...
  • 緩存模式指南: https://learn.microsoft.com/zh-cn/azure/architecture/patterns...
  • MemoryCache 源碼: https://github.com/dotnet/runtime/blob/main/src/libraries/System.Runtime.Caching/src/System/Runtime/Caching/MemoryCache.cs
user avatar wodekouwei Avatar yujiaao Avatar solvep Avatar guanguans Avatar
Favorites 4 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.