.NET缓存键问题及其解决方案

前言

IDistributedCache 接口是一个抽象接口,用于通过各种后端实现缓存数据,如内存、Redis或SQL Server。

但是,在使用 IDistributedCache 时需要注意一个问题,即缓存键的命名。

缓存键是标识缓存数据的唯一字符串,它们决定缓存数据的存储位置和检索方法。如果不同的缓存数据使用相同的缓存键,可能会导致数据被覆盖或混淆。.

为了避免这些问题,一种常见的做法是使用前缀来区分不同来源的缓存数据。使用前缀,我们可以将缓存键组合成以下格式:

{Prefix}:{Key}

例如:

  • app1:user123 表示 App1 中 ID 为 123 的用户信息。
  • app2:user123 表示 App2 中 ID 为 123 的用户信息。

但是,使用前缀来命名缓存键并不是一件容易的事情。因为 IDistributedCache 接口本身并不提供任何支持前缀的方法或属性。我们只能手动拼接前缀和键值,然后传递给 IDistributedCache 接口。

这样做有以下几个问题:

  • 代码冗余:我们需要在每次使用 IDistributedCache 接口时都手动拼接前缀和键值,这会增加代码量和出错概率。
  • 代码分散:我们需要在应用程序中多处定义和使用前缀和键值,这会导致代码分散和难以维护。
  • 代码耦合:我们需要直接依赖于 IDistributedCache 接口和具体的前缀和键值,这会导致代码耦合和难以测试。

那么,有没有更好的解决方案呢?

解决方案

为了解决这些问题,我们可以使用一种叫做装饰器模式(Decorator Pattern)的设计模式来扩展IDistributedCache接口。

装饰器模式是一种结构型设计模式,它可以在不修改原有对象的情况下,动态地给对象添加新的功能或行为。装饰器模式通常由以下几个角色组成:

  • 抽象组件(Component):定义一个对象的接口,用于给对象动态地添加功能或行为。
  • 具体组件(Concrete Component):实现抽象组件的接口,表示被装饰的原始对象。
  • 抽象装饰器(Decorator):继承或实现抽象组件的接口,表示装饰器的基类,包含一个抽象组件的引用,用于调用原始对象的方法。
  • 具体装饰器(Concrete Decorator):继承或实现抽象装饰器的接口,表示具体的装饰器,可以在调用原始对象的方法之前或之后添加新的功能或行为。

抽象组件

IDistributedCache 接口提供以下方法来处理分布式缓存实现中的项:

  • Get、GetAsync:如果在缓存中找到,则接受字符串键并以 byte[] 数组的形式检索缓存项。
  • Set、SetAsync:使用字符串键将项(作为 byte[] 数组)添加到缓存。
  • Refresh、RefreshAsync:根据键刷新缓存中的项,重置其可调到期超时(如果有)。
  • Remove、RemoveAsync:根据字符串键删除缓存项。

具体组件

这里使用的是分布式 Redis 缓存(RedisCache) 作为示例。

分布式 Redis 缓存是框架提供的 IDistributedCache 实现,位于 Microsoft.Extensions.Caching.StackExchangeRedis NuGet 包中。

抽象装饰器

实现代码如下:

public abstract class DistributedCacheDecorator : IDistributedCache
{
    protected readonly IDistributedCache _innerCache;

    protected DistributedCacheDecorator(IDistributedCache innerCache)
    {
        _innerCache = innerCache;
    }

    public virtual byte[] Get(string key)
    {
        return _innerCache.Get(key);
    }

    public virtual Task<byte[]> GetAsync(string key, CancellationToken token = default)
    {
        return _innerCache.GetAsync(key, token);
    }

    public virtual void Set(string key, byte[] value, DistributedCacheEntryOptions options)
    {
        _innerCache.Set(key, value, options);
    }

    public virtual Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
    {
        return _innerCache.SetAsync(key, value, options, token);
    }

    public virtual void Refresh(string key)
    {
        _innerCache.Refresh(key);
    }

    public virtual Task RefreshAsync(string key, CancellationToken token = default)
    {
        return _innerCache.RefreshAsync(key, token);
    }

    public virtual void Remove(string key)
    {
        _innerCache.Remove(key);
    }

    public virtual Task RemoveAsync(string key, CancellationToken token = default)
    {
        return _innerCache.RemoveAsync(key, token);
    }
}

具体装饰器

实现代码如下:

public class PrefixDistributedCache : DistributedCacheDecorator
{
    private readonly string _prefix;

    public PrefixDistributedCache(IDistributedCache innerCache, string prefix) : base(innerCache)
    {
        _prefix = prefix;
    }

    private string AddPrefix(string key)
    {
        return $"{_prefix}:{key}";
    }

    public override byte[] Get(string key)
    {
        return base.Get(AddPrefix(key));
    }

    public override Task<byte[]> GetAsync(string key, CancellationToken token = default)
    {
        return base.GetAsync(AddPrefix(key), token);
    }

    public override void Set(string key, byte[] value, DistributedCacheEntryOptions options)
    {
        base.Set(AddPrefix(key), value, options);
    }

    public override Task SetAsync(string key, byte[] value, DistributedCacheEntryOptions options, CancellationToken token = default)
    {
        return base.SetAsync(AddPrefix(key), value, options, token);
    }

    public override void Refresh(string key)
    {
        base.Refresh(AddPrefix(key));
    }

    public override Task RefreshAsync(string key, CancellationToken token = default)
    {
        return base.RefreshAsync(AddPrefix(key), token);
    }

    public override void Remove(string key)
    {
        base.Remove(AddPrefix(key));
    }

    public override Task RemoveAsync(string key, CancellationToken token = default)
    {
        return base.RemoveAsync(AddPrefix(key), token);
    }
}

使用

首先,创建一个新的扩展方法来注册我们的实现。代码如下:

public static class PrefixDistributedCacheExtentions
{
    public static IServiceCollection AddPrefixDistributedCache(this IServiceCollection services, string prefix)
    {
        return services.AddSingleton<IDistributedCache>(x =>
        {
            var prefixDistributedCache = new PrefixDistributedCache(new MemoryDistributedCache(null), prefix);
            return prefixDistributedCache;
        });
    }
}

然后,就可以在我们的项目中使用了:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddPrefixDistributedCache("myio-demo");
var app = builder.Build();

app.MapGet("/cache/{key}/{value}", (IDistributedCache cache, string key,string value) => cache.SetString(key,value));

app.Run();

访问 API 后,让我们看一下redis数据库,它应该包含带有"myio-demo"前缀的缓存键:

.NET缓存键问题及其解决方案

总结

在本文中,我们学习了如何使用装饰器模式来扩展IDistributedCache接口,以便在不修改原有对象的情况下,动态地给对象添加新的功能或行为。