沫欢 發表於 2026-5-2 14:25:00

.NET 双缓存策略:本地缓存、分布式缓存

<h2>一、设计思路</h2>
<div>&nbsp;</div>
<h3>1. 架构分层</h3>
<div>&nbsp;</div>
<ol>
<li>一级缓存:<code>IMemoryCache</code>(进程内内存缓存,读写纳秒级,无网络开销)</li>
<li>二级缓存:<code>IDistributedCache</code>(Redis 分布式缓存,跨服务共享,毫秒级)</li>
<li>数据源:数据库 / 接口(兜底,避免缓存穿透)</li>
</ol>
<div>&nbsp;</div>
<h3>2. 读写流程</h3>
<div>&nbsp;</div>
<h4>读取数据(Get)</h4>
<div>&nbsp;</div>
<ol>
<li>先查本地缓存,命中直接返回</li>
<li>本地未命中,查分布式缓存,命中则回写本地缓存</li>
<li>分布式未命中,查数据源,查到后同时写入本地 + 分布式缓存</li>
<li>未查到:返回空 / 处理缓存穿透</li>
</ol>
<div>&nbsp;</div>
<h4>写入 / 更新数据(Set/Remove)</h4>
<div>&nbsp;</div>
<ol>
<li>先更新数据源(保证数据可靠)</li>
<li>同时删除 / 更新 本地缓存 + 分布式缓存(保证双缓存一致性)</li>
</ol>
<div>&nbsp;</div>
<h3>3. 关键配置</h3>
<div>&nbsp;</div>
<ul>
<li>本地缓存过期时间 &lt; 分布式缓存过期时间(避免本地脏数据)</li>
<li>支持缓存键前缀、序列化方式、过期时间单独配置</li>
<li>支持缓存穿透 (不存在)/ 击穿(单个过期) / 雪崩(大量过期)防护</li>
</ul>
<div>&nbsp;</div>
<hr>
<div>&nbsp;</div>
<h2>二、完整代码实现</h2>
<div>&nbsp;</div>
<h3>1. 依赖安装</h3>
<div>&nbsp;</div>
<div>
<div dir="ltr">
<div>
<pre><code># 内存缓存(框架自带)
# 分布式缓存(Redis)
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
# 序列化
Install-Package System.Text.Json
</code></pre>
</div>
<div>&nbsp;</div>
</div>
</div>
<h3>2. 双缓存核心接口</h3>
<div>&nbsp;</div>
<div>定义统一操作规范,解耦实现:</div>
<div>&nbsp;</div>
<div>
<div dir="ltr">
<div>
<pre><code>/// &lt;summary&gt;
/// 双缓存服务接口
/// &lt;/summary&gt;
public interface IDoubleCache
{
    // 获取缓存
    Task&lt;T&gt; GetAsync&lt;T&gt;(string key);
   
    // 设置缓存
    Task SetAsync&lt;T&gt;(string key, T value,
      TimeSpan? localExpire = null,
      TimeSpan? distExpire = null);
   
    // 删除缓存
    Task RemoveAsync(string key);
}
</code></pre>
</div>
<div>&nbsp;</div>
</div>
</div>
<h3>3. 双缓存实现类(核心代码)</h3>
<div>&nbsp;</div>
<div>
<div dir="ltr">
<div>
<pre><code>using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Caching.Memory;
using System.Text.Json;

public class DoubleCache : IDoubleCache
{
    private readonly IMemoryCache _memoryCache;
    private readonly IDistributedCache _distributedCache;
   
    // 默认配置:本地缓存2分钟,分布式缓存5分钟
    private readonly TimeSpan _defaultLocalExpire = TimeSpan.FromMinutes(2);
    private readonly TimeSpan _defaultDistExpire = TimeSpan.FromMinutes(5);

    public DoubleCache(IMemoryCache memoryCache, IDistributedCache distributedCache)
    {
      _memoryCache = memoryCache;
      _distributedCache = distributedCache;
    }

    /// &lt;summary&gt;
    /// 双缓存读取
    /// &lt;/summary&gt;
    public async Task&lt;T&gt; GetAsync&lt;T&gt;(string key)
    {
      // 1. 优先读本地缓存
      if (_memoryCache.TryGetValue(key, out var value) &amp;&amp; value != null)
      {
            return (T)value;
      }

      // 2. 本地未命中,读分布式缓存
      var distValue = await _distributedCache.GetStringAsync(key);
      if (!string.IsNullOrEmpty(distValue))
      {
            var obj = JsonSerializer.Deserialize&lt;T&gt;(distValue);
            // 回写本地缓存
            _memoryCache.Set(key, obj, _defaultLocalExpire);
            return obj;
      }

      // 3. 都未命中,返回默认值
      return default;
    }

    /// &lt;summary&gt;
    /// 双缓存写入
    /// &lt;/summary&gt;
    public async Task SetAsync&lt;T&gt;(string key, T value,
      TimeSpan? localExpire = null,
      TimeSpan? distExpire = null)
    {
      if (value == null) return;

      // 过期时间配置
      var localExp = localExpire ?? _defaultLocalExpire;
      var distExp = distExpire ?? _defaultDistExpire;

      // 1. 写入本地缓存
      _memoryCache.Set(key, value, localExp);

      // 2. 写入分布式缓存
      var jsonValue = JsonSerializer.Serialize(value);
      await _distributedCache.SetStringAsync(key, jsonValue, new DistributedCacheEntryOptions
      {
            AbsoluteExpirationRelativeToNow = distExp
      });
    }

    /// &lt;summary&gt;
    /// 双缓存删除
    /// &lt;/summary&gt;
    public async Task RemoveAsync(string key)
    {
      // 删除本地
      _memoryCache.Remove(key);
      // 删除分布式
      await _distributedCache.RemoveAsync(key);
    }
}
</code></pre>
</div>
<div>&nbsp;</div>
</div>
</div>
<h3>4. 注册服务(Program.cs)</h3>
<div>&nbsp;</div>
<div>
<div dir="ltr">
<div>
<pre><code>var builder = WebApplication.CreateBuilder(args);

// 1. 注册内存缓存
builder.Services.AddMemoryCache();

// 2. 注册Redis分布式缓存
builder.Services.AddStackExchangeRedisCache(options =&gt;
{
    options.Configuration = "localhost:6379,password=123456"; // Redis连接串
    options.InstanceName = "DoubleCache:"; // 缓存键前缀
});

// 3. 注册双缓存服务
builder.Services.AddScoped&lt;IDoubleCache, DoubleCache&gt;();

// 业务服务注册
builder.Services.AddControllers();
</code></pre>
</div>
<div>&nbsp;</div>
</div>
</div>
<h3>5. 业务使用示例(Controller)</h3>
<div>&nbsp;</div>
<div>
<div dir="ltr">
<div>
<pre><code>
")]
public class ProductController : ControllerBase
{
    private readonly IDoubleCache _doubleCache;
    // 模拟数据库仓储
    private readonly IProductRepository _productRepository;

    public ProductController(IDoubleCache doubleCache, IProductRepository productRepository)
    {
      _doubleCache = doubleCache;
      _productRepository = productRepository;
    }

    /// &lt;summary&gt;
    /// 获取商品(双缓存读取)
    /// &lt;/summary&gt;
   
    public async Task&lt;IActionResult&gt; GetProduct(int id)
    {
      var key = $"product:{id}";
      
      // 1. 查双缓存
      var product = await _doubleCache.GetAsync&lt;Product&gt;(key);
      if (product != null) return Ok(product);

      // 2. 缓存未命中,查数据库
      product = await _productRepository.GetByIdAsync(id);
      if (product == null) return NotFound();

      // 3. 写入双缓存
      await _doubleCache.SetAsync(key, product);

      return Ok(product);
    }

    /// &lt;summary&gt;
    /// 更新商品(双缓存更新)
    /// &lt;/summary&gt;
   
    public async Task&lt;IActionResult&gt; UpdateProduct(int id, Product product)
    {
      // 1. 更新数据库
      await _productRepository.UpdateAsync(product);
      
      // 2. 删除双缓存(核心:保证缓存一致性)
      await _doubleCache.RemoveAsync($"product:{id}");

      return Ok();
    }
}
</code></pre>
</div>
<div>&nbsp;</div>
</div>
</div>
<div>&nbsp;</div>
<hr>
<div>&nbsp;</div>
<h2>三、优化</h2>
<div>&nbsp;</div>
<h3>1. 缓存穿透防护</h3>
<div>&nbsp;</div>
<div>缓存空对象,避免大量无效请求打到数据库:</div>
<div>&nbsp;</div>
<div>
<div dir="ltr">
<div>
<pre><code>// 在 GetAsync 方法中补充
if (distValue == null)
{
    // 缓存空对象,过期时间更短
    _memoryCache.Set(key, null, TimeSpan.FromSeconds(30));
    return default;
}
</code></pre>
</div>
<div>&nbsp;</div>
</div>
</div>
<h3>2. 缓存雪崩防护</h3>
<div>&nbsp;</div>
<ul>
<li>给过期时间添加随机偏移量,避免大量缓存同时过期</li>
</ul>
<div>&nbsp;</div>
<div>
<div dir="ltr">
<div>
<pre><code>// 随机偏移 10~30 秒
var localExp = localExpire ?? _defaultLocalExpire.Add(TimeSpan.FromSeconds(new Random().Next(10, 30)));
</code></pre>
</div>
<div>&nbsp;</div>
</div>
</div>
<h3>3. 缓存击穿防护</h3>
<div>&nbsp;</div>
<div>使用互斥锁,防止高并发下缓存失效时大量请求打数据库:</div>
<div>&nbsp;</div>
<div>
<div dir="ltr">
<div>
<pre><code>// 读取时加锁
lock (key)
{
    if (_memoryCache.TryGetValue(key, out value))
    {
      return (T)value;
    }
}
</code></pre>
</div>
<div>&nbsp;</div>
</div>
</div>
<h3>4. 支持泛型 + 灵活过期时间</h3>
<div>&nbsp;</div>
<div>已在核心代码中实现,可单独为每个缓存配置:</div>
<div>&nbsp;</div>
<div>
<div dir="ltr">
<div>
<pre><code>// 本地缓存1分钟,分布式缓存10分钟
await _doubleCache.SetAsync(key, data,
    TimeSpan.FromMinutes(1),
    TimeSpan.FromMinutes(10));
</code></pre>
</div>
<div>&nbsp;</div>
</div>
</div>
<h3>5. 跨实例缓存同步(可选)</h3>
<div>&nbsp;</div>
<div>多服务实例下,本地缓存更新可使用Redis 发布订阅通知所有实例删除本地缓存:</div>
<div>&nbsp;</div>
<ol>
<li>实例 A 更新数据 → 删除本地 + 分布式缓存</li>
<li>发布 Redis 消息</li>
<li>其他实例订阅消息 → 删除本地缓存</li>
</ol>
<div>&nbsp;</div>
<hr>
<div>&nbsp;</div>
<h2>四、双缓存策略优缺点</h2>
<div>&nbsp;</div>
<h3>优点</h3>
<div>&nbsp;</div>
<ol>
<li>性能提高:热点数据走内存缓存,无网络开销</li>
<li>一致性强:分布式缓存保证跨实例数据一致</li>
<li>高可用:Redis 宕机可降级为纯本地缓存</li>
<li>抗流量冲击:秒杀 / 热点场景保护数据库</li>
</ol>
<div>&nbsp;</div>
<h3>缺点</h3>
<div>&nbsp;</div>
<ol>
<li>内存占用:本地缓存会占用应用进程内存</li>
<li>一致性成本:多实例需要额外机制同步本地缓存</li>
</ol>
<div>&nbsp;</div>
<hr>
<div>&nbsp;</div>
<h2>总结</h2>
<div>&nbsp;</div>
<ol>
<li>双缓存 = 本地缓存(快)+ 分布式缓存(一致)</li>
<li>核心流程:读先本地→再分布式→最后数据库;写先数据库→再删双缓存</li>
<li>生产环境:穿透 / 雪崩 / 击穿防护 + 过期时间随机化</li>
<li>代码支持灵活配置、泛型、自定义过期时间</li>
</ol><br><br>
来源:https://www.cnblogs.com/chuansheng/p/19916122
頁: [1]
查看完整版本: .NET 双缓存策略:本地缓存、分布式缓存