.NET 双缓存策略:本地缓存、分布式缓存
<h2>一、设计思路</h2><div> </div>
<h3>1. 架构分层</h3>
<div> </div>
<ol>
<li>一级缓存:<code>IMemoryCache</code>(进程内内存缓存,读写纳秒级,无网络开销)</li>
<li>二级缓存:<code>IDistributedCache</code>(Redis 分布式缓存,跨服务共享,毫秒级)</li>
<li>数据源:数据库 / 接口(兜底,避免缓存穿透)</li>
</ol>
<div> </div>
<h3>2. 读写流程</h3>
<div> </div>
<h4>读取数据(Get)</h4>
<div> </div>
<ol>
<li>先查本地缓存,命中直接返回</li>
<li>本地未命中,查分布式缓存,命中则回写本地缓存</li>
<li>分布式未命中,查数据源,查到后同时写入本地 + 分布式缓存</li>
<li>未查到:返回空 / 处理缓存穿透</li>
</ol>
<div> </div>
<h4>写入 / 更新数据(Set/Remove)</h4>
<div> </div>
<ol>
<li>先更新数据源(保证数据可靠)</li>
<li>同时删除 / 更新 本地缓存 + 分布式缓存(保证双缓存一致性)</li>
</ol>
<div> </div>
<h3>3. 关键配置</h3>
<div> </div>
<ul>
<li>本地缓存过期时间 < 分布式缓存过期时间(避免本地脏数据)</li>
<li>支持缓存键前缀、序列化方式、过期时间单独配置</li>
<li>支持缓存穿透 (不存在)/ 击穿(单个过期) / 雪崩(大量过期)防护</li>
</ul>
<div> </div>
<hr>
<div> </div>
<h2>二、完整代码实现</h2>
<div> </div>
<h3>1. 依赖安装</h3>
<div> </div>
<div>
<div dir="ltr">
<div>
<pre><code># 内存缓存(框架自带)
# 分布式缓存(Redis)
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
# 序列化
Install-Package System.Text.Json
</code></pre>
</div>
<div> </div>
</div>
</div>
<h3>2. 双缓存核心接口</h3>
<div> </div>
<div>定义统一操作规范,解耦实现:</div>
<div> </div>
<div>
<div dir="ltr">
<div>
<pre><code>/// <summary>
/// 双缓存服务接口
/// </summary>
public interface IDoubleCache
{
// 获取缓存
Task<T> GetAsync<T>(string key);
// 设置缓存
Task SetAsync<T>(string key, T value,
TimeSpan? localExpire = null,
TimeSpan? distExpire = null);
// 删除缓存
Task RemoveAsync(string key);
}
</code></pre>
</div>
<div> </div>
</div>
</div>
<h3>3. 双缓存实现类(核心代码)</h3>
<div> </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;
}
/// <summary>
/// 双缓存读取
/// </summary>
public async Task<T> GetAsync<T>(string key)
{
// 1. 优先读本地缓存
if (_memoryCache.TryGetValue(key, out var value) && value != null)
{
return (T)value;
}
// 2. 本地未命中,读分布式缓存
var distValue = await _distributedCache.GetStringAsync(key);
if (!string.IsNullOrEmpty(distValue))
{
var obj = JsonSerializer.Deserialize<T>(distValue);
// 回写本地缓存
_memoryCache.Set(key, obj, _defaultLocalExpire);
return obj;
}
// 3. 都未命中,返回默认值
return default;
}
/// <summary>
/// 双缓存写入
/// </summary>
public async Task SetAsync<T>(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
});
}
/// <summary>
/// 双缓存删除
/// </summary>
public async Task RemoveAsync(string key)
{
// 删除本地
_memoryCache.Remove(key);
// 删除分布式
await _distributedCache.RemoveAsync(key);
}
}
</code></pre>
</div>
<div> </div>
</div>
</div>
<h3>4. 注册服务(Program.cs)</h3>
<div> </div>
<div>
<div dir="ltr">
<div>
<pre><code>var builder = WebApplication.CreateBuilder(args);
// 1. 注册内存缓存
builder.Services.AddMemoryCache();
// 2. 注册Redis分布式缓存
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = "localhost:6379,password=123456"; // Redis连接串
options.InstanceName = "DoubleCache:"; // 缓存键前缀
});
// 3. 注册双缓存服务
builder.Services.AddScoped<IDoubleCache, DoubleCache>();
// 业务服务注册
builder.Services.AddControllers();
</code></pre>
</div>
<div> </div>
</div>
</div>
<h3>5. 业务使用示例(Controller)</h3>
<div> </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;
}
/// <summary>
/// 获取商品(双缓存读取)
/// </summary>
public async Task<IActionResult> GetProduct(int id)
{
var key = $"product:{id}";
// 1. 查双缓存
var product = await _doubleCache.GetAsync<Product>(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);
}
/// <summary>
/// 更新商品(双缓存更新)
/// </summary>
public async Task<IActionResult> UpdateProduct(int id, Product product)
{
// 1. 更新数据库
await _productRepository.UpdateAsync(product);
// 2. 删除双缓存(核心:保证缓存一致性)
await _doubleCache.RemoveAsync($"product:{id}");
return Ok();
}
}
</code></pre>
</div>
<div> </div>
</div>
</div>
<div> </div>
<hr>
<div> </div>
<h2>三、优化</h2>
<div> </div>
<h3>1. 缓存穿透防护</h3>
<div> </div>
<div>缓存空对象,避免大量无效请求打到数据库:</div>
<div> </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> </div>
</div>
</div>
<h3>2. 缓存雪崩防护</h3>
<div> </div>
<ul>
<li>给过期时间添加随机偏移量,避免大量缓存同时过期</li>
</ul>
<div> </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> </div>
</div>
</div>
<h3>3. 缓存击穿防护</h3>
<div> </div>
<div>使用互斥锁,防止高并发下缓存失效时大量请求打数据库:</div>
<div> </div>
<div>
<div dir="ltr">
<div>
<pre><code>// 读取时加锁
lock (key)
{
if (_memoryCache.TryGetValue(key, out value))
{
return (T)value;
}
}
</code></pre>
</div>
<div> </div>
</div>
</div>
<h3>4. 支持泛型 + 灵活过期时间</h3>
<div> </div>
<div>已在核心代码中实现,可单独为每个缓存配置:</div>
<div> </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> </div>
</div>
</div>
<h3>5. 跨实例缓存同步(可选)</h3>
<div> </div>
<div>多服务实例下,本地缓存更新可使用Redis 发布订阅通知所有实例删除本地缓存:</div>
<div> </div>
<ol>
<li>实例 A 更新数据 → 删除本地 + 分布式缓存</li>
<li>发布 Redis 消息</li>
<li>其他实例订阅消息 → 删除本地缓存</li>
</ol>
<div> </div>
<hr>
<div> </div>
<h2>四、双缓存策略优缺点</h2>
<div> </div>
<h3>优点</h3>
<div> </div>
<ol>
<li>性能提高:热点数据走内存缓存,无网络开销</li>
<li>一致性强:分布式缓存保证跨实例数据一致</li>
<li>高可用:Redis 宕机可降级为纯本地缓存</li>
<li>抗流量冲击:秒杀 / 热点场景保护数据库</li>
</ol>
<div> </div>
<h3>缺点</h3>
<div> </div>
<ol>
<li>内存占用:本地缓存会占用应用进程内存</li>
<li>一致性成本:多实例需要额外机制同步本地缓存</li>
</ol>
<div> </div>
<hr>
<div> </div>
<h2>总结</h2>
<div> </div>
<ol>
<li>双缓存 = 本地缓存(快)+ 分布式缓存(一致)</li>
<li>核心流程:读先本地→再分布式→最后数据库;写先数据库→再删双缓存</li>
<li>生产环境:穿透 / 雪崩 / 击穿防护 + 过期时间随机化</li>
<li>代码支持灵活配置、泛型、自定义过期时间</li>
</ol><br><br>
来源:https://www.cnblogs.com/chuansheng/p/19916122
頁:
[1]