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

一、设计思路

 

1. 架构分层

 

  1. 一级缓存:IMemoryCache(进程内内存缓存,读写纳秒级,无网络开销)
  2. 二级缓存:IDistributedCache(Redis 分布式缓存,跨服务共享,毫秒级)
  3. 数据源:数据库 / 接口(兜底,避免缓存穿透)

 

2. 读写流程

 

读取数据(Get)

 

  1. 先查本地缓存,命中直接返回
  2. 本地未命中,查分布式缓存,命中则回写本地缓存
  3. 分布式未命中,查数据源,查到后同时写入本地 + 分布式缓存
  4. 未查到:返回空 / 处理缓存穿透

 

写入 / 更新数据(Set/Remove)

 

  1. 先更新数据源(保证数据可靠)
  2. 同时删除 / 更新 本地缓存 + 分布式缓存(保证双缓存一致性)

 

3. 关键配置

 

  • 本地缓存过期时间 < 分布式缓存过期时间(避免本地脏数据)
  • 支持缓存键前缀、序列化方式、过期时间单独配置
  • 支持缓存穿透 (不存在)/ 击穿(单个过期) / 雪崩(大量过期)防护

 


 

二、完整代码实现

 

1. 依赖安装

 

# 内存缓存(框架自带)
# 分布式缓存(Redis)
Install-Package Microsoft.Extensions.Caching.StackExchangeRedis
# 序列化
Install-Package System.Text.Json

 

2. 双缓存核心接口

  定义统一操作规范,解耦实现:  

/// <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);
}

 

3. 双缓存实现类(核心代码)

 

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);
    }
}

 

4. 注册服务(Program.cs)

 

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();

 

5. 业务使用示例(Controller)

 

[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
    private readonly IDoubleCache _doubleCache;
    // 模拟数据库仓储
    private readonly IProductRepository _productRepository;

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

    /// <summary>
    /// 获取商品(双缓存读取)
    /// </summary>
    [HttpGet("{id}")]
    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>
    [HttpPut("{id}")]
    public async Task<IActionResult> UpdateProduct(int id, Product product)
    {
        // 1. 更新数据库
        await _productRepository.UpdateAsync(product);
        
        // 2. 删除双缓存(核心:保证缓存一致性)
        await _doubleCache.RemoveAsync($"product:{id}");

        return Ok();
    }
}

   


 

三、优化

 

1. 缓存穿透防护

  缓存空对象,避免大量无效请求打到数据库:  

// 在 GetAsync 方法中补充
if (distValue == null)
{
    // 缓存空对象,过期时间更短
    _memoryCache.Set(key, null, TimeSpan.FromSeconds(30));
    return default;
}

 

2. 缓存雪崩防护

 

  • 给过期时间添加随机偏移量,避免大量缓存同时过期

 

// 随机偏移 10~30 秒
var localExp = localExpire ?? _defaultLocalExpire.Add(TimeSpan.FromSeconds(new Random().Next(10, 30)));

 

3. 缓存击穿防护

  使用互斥锁,防止高并发下缓存失效时大量请求打数据库:  

// 读取时加锁
lock (key)
{
    if (_memoryCache.TryGetValue(key, out value))
    {
        return (T)value;
    }
}

 

4. 支持泛型 + 灵活过期时间

  已在核心代码中实现,可单独为每个缓存配置:  

// 本地缓存1分钟,分布式缓存10分钟
await _doubleCache.SetAsync(key, data, 
    TimeSpan.FromMinutes(1), 
    TimeSpan.FromMinutes(10));

 

5. 跨实例缓存同步(可选)

  多服务实例下,本地缓存更新可使用Redis 发布订阅通知所有实例删除本地缓存:  

  1. 实例 A 更新数据 → 删除本地 + 分布式缓存
  2. 发布 Redis 消息
  3. 其他实例订阅消息 → 删除本地缓存

 


 

四、双缓存策略优缺点

 

优点

 

  1. 性能提高:热点数据走内存缓存,无网络开销
  2. 一致性强:分布式缓存保证跨实例数据一致
  3. 高可用:Redis 宕机可降级为纯本地缓存
  4. 抗流量冲击:秒杀 / 热点场景保护数据库

 

缺点

 

  1. 内存占用:本地缓存会占用应用进程内存
  2. 一致性成本:多实例需要额外机制同步本地缓存

 


 

总结

 

  1. 双缓存 = 本地缓存(快)+ 分布式缓存(一致)
  2. 核心流程:读先本地→再分布式→最后数据库;写先数据库→再删双缓存
  3. 生产环境:穿透 / 雪崩 / 击穿防护 + 过期时间随机化
  4. 代码支持灵活配置、泛型、自定义过期时间

文章摘自:https://www.cnblogs.com/chuansheng/p/19916122