如何搭配使用分布式内存缓存与分布式SQL Server缓存?
如何搭配使用分布式内存缓存与分布式SQL Server缓存?
嘿,你这个场景太接地气了——想用内存缓存当快速访问的L1,SQL Server缓存当持久化的L2,但因为俩都实现了IDistributedCache,直接注册会互相覆盖对吧?别慌,我给你捋个清晰的路子,不用改太多现有代码就能搞定!
核心思路
问题的关键在于:默认情况下DI容器里IDistributedCache只能有一个默认实现,所以咱得给两个缓存实例“起名字”,让DI能区分它们,然后再封装一个自定义服务,把“先查L1、再查L2,写入同步双端”的分层逻辑给包起来,消费端只用和这个自定义服务打交道就行。
具体步骤
1. 给两个缓存做命名注册
在你的Program.cs里,先分别注册分布式内存缓存和SQL Server缓存,并且给它们指定唯一的键名,让DI能精准找到对应的实例:
// 注册分布式内存缓存作为L1,命名为"L1Cache" services.AddDistributedMemoryCache(); services.AddKeyedSingleton<IDistributedCache, DistributedMemoryCache>("L1Cache"); // 注册SQL Server缓存作为L2,命名为"L2Cache" services.AddDistributedSqlServerCache(options => { options.ConnectionString = Configuration.GetConnectionString("App"); options.SchemaName = "dbo"; options.TableName = "Cache"; }); services.AddKeyedSingleton<IDistributedCache, SqlServerCache>("L2Cache");
2. 封装分层缓存服务
咱自己写一个接口和实现类,把分层缓存的逻辑封装好,这样消费端不用关心底层是两个缓存:
// 先定义一个分层缓存的接口,按需加你需要的方法 public interface ILayeredCache { Task<byte[]?> GetAsync(string key, CancellationToken token = default); Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default); Task RemoveAsync(string key, CancellationToken token = default); } // 实现类,把L1和L2的逻辑串起来 public class LayeredCache : ILayeredCache { private readonly IDistributedCache _l1MemoryCache; private readonly IDistributedCache _l2SqlCache; // 通过[FromKeyedServices]特性,从DI里取出对应名字的缓存实例 public LayeredCache( [FromKeyedServices("L1Cache")] IDistributedCache l1Cache, [FromKeyedServices("L2Cache")] IDistributedCache l2Cache) { _l1MemoryCache = l1Cache; _l2SqlCache = l2Cache; } public async Task<byte[]?> GetAsync(string key, CancellationToken token = default) { // 第一步:先查L1内存缓存,速度最快 var value = await _l1MemoryCache.GetAsync(key, token); if (value != null) { return value; } // 第二步:L1没有,再查L2 SQL Server缓存 value = await _l2SqlCache.GetAsync(key, token); if (value != null) { // 把L2的结果同步到L1,下次访问就直接走L1了 await _l1MemoryCache.SetAsync(key, value, token); } return value; } public async Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default) { // 写入的时候同时更新L1和L2,保证数据一致 await Task.WhenAll( _l1MemoryCache.SetAsync(key, value, options, token), _l2SqlCache.SetAsync(key, value, options, token) ); } public async Task RemoveAsync(string key, CancellationToken token = default) { // 删除的时候也要同时删两个缓存的键 await Task.WhenAll( _l1MemoryCache.RemoveAsync(key, token), _l2SqlCache.RemoveAsync(key, token) ); } }
3. 注册自定义服务并在消费端使用
最后把咱的分层缓存服务注册到DI里:
services.AddScoped<ILayeredCache, LayeredCache>();
然后在你需要用缓存的地方,直接注入ILayeredCache就行,完全不用管底层的两个IDistributedCache实例:
public class ProductService { private readonly ILayeredCache _layeredCache; public ProductService(ILayeredCache layeredCache) { _layeredCache = layeredCache; } public async Task<Product?> GetProductByIdAsync(int id) { var cacheKey = $"Product_{id}"; var cachedValue = await _layeredCache.GetAsync(cacheKey); if (cachedValue != null) { return JsonSerializer.Deserialize<Product>(cachedValue); } // 从数据库查数据的逻辑... var product = await _dbContext.Products.FindAsync(id); if (product != null) { var serializedValue = JsonSerializer.SerializeToUtf8Bytes(product); await _layeredCache.SetAsync(cacheKey, serializedValue, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(1) }); } return product; } }
几个要注意的小细节
- 内存缓存是进程内的,如果你的应用是多实例部署(比如集群、容器化),每个实例的L1缓存是独立的,可能会出现短暂的数据不一致。如果对一致性要求高,可以给L1缓存设置较短的过期时间,缩小不一致的窗口。
- 写入的时候用
Task.WhenAll并行操作,能提升性能,但如果其中一个缓存写入失败,可能会出现数据不一致。如果你的业务对一致性要求极高,可以改成串行写入(比如先写L2成功,再写L1),或者加简单的重试逻辑。 - 要是你不想写自定义服务,也可以在消费端直接注入
IKeyedServiceProvider,然后手动解析对应的IDistributedCache实例,但这样消费端会和底层缓存实现耦合,不如自定义服务优雅。
内容来源于stack exchange
相关产品推荐
相关产品推荐

