Login
网站首页 > 文章中心 > 其它

.NetCore下基于FreeRedis实现的Redis6.0客户端缓存之缓存键条件优雅过滤

作者:小编 更新时间:2023-09-02 07:22:56 浏览量:416人看过

前言

众所周知内存缓存(MemoryCache)数据是从内存中获取,性能表现上是最优的,但是内存缓存有一个缺点就是不支持分布式,数据在各个部署节点上各存一份,每份缓存的过期时间不一致,会导致幻读等各种问题,所以我们实现分布式缓存通常会用上Redis

但如果在高并发的情况下读取Redis的缓存,会进行频繁的网络I/O,假如有一些不经常变动的热点缓存,这不就会白白浪费了带宽,并且读到数据以后可能还需要进行反序列化,还影响了CPU性能,造成资源的浪费

我们当时为了实现某个对性能有较高要求的产品需求,但不想额外增加硬件上的资源,急需使用上这一特性,在调研后发现了这个组件,经过测试后发现没什么问题就直接用上了

目前FreeRedis在我司项目中也已经稳定运行了一年多,这里分享一下我们在项目中的实际用法

扩展前

为什么要改造?因为当看过官方的Demo以后,其中让我比较难受的是本地缓存键的过滤条件设置

.NetCore下基于FreeRedis实现的Redis6.0客户端缓存之缓存键条件优雅过滤-图1

.NetCore下基于FreeRedis实现的Redis⑥0客户端缓存之缓存键条件优雅过滤

我想到的有三种方式配置这个条件

第一种:在具体实现某个缓存的地方,才设置过滤条件

缺点:

每次都得写一遍有点冗余,而且查看源码可以发现UseClientSideCaching这个方法每次都会实例一个叫ClientSideCachingContext的类,并在里面添加订阅、添加拦截器等一系列操作

.NetCore下基于FreeRedis实现的Redis6.0客户端缓存之缓存键条件优雅过滤-图2

所以意味具体业务实现代码中每次还实现一下不重复调用UseClientSideCaching的特殊逻辑,即使实现了,但每个不重复的Key都会往RedisClient新增一个拦截器,极力不推荐这种方式!

.NetCore下基于FreeRedis实现的Redis⑥0客户端缓存之缓存键条件优雅过滤

第二种:在同一个地方把所有需要进行本地缓存的键一口气设置好过滤条件

时间长了以后,这里会写得非常的长,非常的丑陋,而且你并不知道哪些键已经废弃以及对应的业务

当然项目是从头到尾是你一个人负责开发的或需要本地缓存的Key并不多的时候,这种方式其实也够了

.NetCore下基于FreeRedis实现的Redis⑥0客户端缓存之缓存键条件优雅过滤

需要给团队提前培训下这个注意项,但是时间长了以后,大伙完全不知道后面匹配的那么多键对应是什么业务

在Key不多且项目参与人数不多的情况下,用这个方式是最简单方便的

.NetCore下基于FreeRedis实现的Redis6.0客户端缓存之缓存键条件优雅过滤-图3

.NetCore下基于FreeRedis实现的Redis⑥0客户端缓存之缓存键条件优雅过滤

第一种

扩展后

基类中已经实现好了对应数据结构通用的方法,例如CacheBaseString中已经实现了Get Set Del Expire这样的通用方法,在派生的缓存类中只要重写基类的抽象方法,设置下Key的命名和缓存过期时间,一个缓存实现就结束了,这样便于管理和使用,团队的小伙伴几年来也都习惯了这种用法

可以发现UseClientSideCaching中的KeyFilter是个Lambda Func委托,返回一个布尔值

.NetCore下基于FreeRedis实现的Redis⑥0客户端缓存之缓存键条件优雅过滤

那么我马上想到的就是表达式树,我们在各种高度封装的ORM中经常能看到使用表达式树去组装SQL的Where条件

同样的原理,我们也可以通过在项目启动时通过反射拿到所有派生类,并调用基类中的一个抽象方法,最后合并表达树,返回一个Func给这个KeyFilter

其中核心的两个方法就是 Key的抽象 和 过滤条件的抽象,其中的 FreeRedisService 是已经实现好的一个FreeRedisClient,需要在IOC容器中注入为单例,所以在这基类的构造函数中,必须传入IServiceProvider,从容器拿到FreeRedisService实例才能实现下面那些通用方法

    /// 
/// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    public abstract Expression> SetCacheKeyFilter();

    /// 
    /// 
    /// 
    /// 
    /// 
        _redisService = serviceProvider.GetService();
    /// 
    /// 
    /// 
    /// 
    public T Get()
        return _redisService.Instance.Get(GetRedisKey());
    /// 
    /// 
    /// 
    /// 
    /// 
    public bool Set(T data)
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    public bool Set(T data,int seconds)
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    public bool Set(T data,TimeSpan expired)
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    public bool Set(T data,DateTime expiredAt)
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
}

具体继承用法如下:

    /// 
/// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    public override Expression> SetCacheKeyFilter()
    /// 
    /// 
    /// 
/// 
/// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    public override Expression> SetCacheKeyFilter()
    /// 
    /// 
    /// 
}

其中关键代码就是一次性设置好项目中所有本地缓存的过滤条件,FreeRedisService最终会注册为一个单例

    public class FreeRedisService
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    private readonly static Lazy redisClientLazy = new Lazy(() => {
    /// 
    /// 
    /// 
    /// 
    /// 
}

我们写一个反射的方法,去遍历所有的缓存派生类,并调用其中重写过的过滤条件抽象方法,最后合并为一个表达式树,Or这个方法是一个自定义扩展方法,具体看Github完整项目

    /// 
/// 
    /// 
    /// 
    /// 
    /// 
    /// serviceProvider
    /// 当前类所在的项目dll名
    /// 
    public static Func Build(IServiceProvider serviceProvider,string dllName = DefaultDllName)
        Expression> expression = o => false; //默认false
}

我们在项目启动时,调用上面的Build方法,将返回的Func委托传入到FreeRedisService中即可,这里我是写了一个IServiceCollection的扩展方法

    public static class ServiceCollectionExtensions
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    static FreeRedisOption GetRedisOption(IConfiguration configuration,Func clientSideCacheKeyFilter = null)
}
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddHealthChecks();

//注入Redis服务
builder.Services.AddRedisService(builder.Configuration);

//可选:注入客户端缓存具体实现类. 如果实现有很多,这里会有一大堆注入代码.在代码中直接实例化类并传入IServiceProvider也一样的
builder.Services.AddSingleton();
builder.Services.AddSingleton();

//构建WebApplication
var app = builder.Build();

app.UseAuthorization();

app.MapControllers();

app.UseHealthChecks("/health");

app.Run();

其中的ClientSideDemoOneCache这个实例,我们可以通过直接实例化并传入IServiceProvider的方式使用,也可以通过构造函数注入,前提是在上面IOC容器中注入过了

    [ApiController]
    private readonly ILogger _logger;
    public HomeController(ILogger logger,IServiceProvider serviceProvider,ClientSideDemoOneCache clientSideDemoOneCache)
    /// 
    /// 
    /// 
        var value = cacheOne.Get();
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
    /// 
}

① 启动项目看一下,先设置一个值,可以看到在Redis中已经添加成功

.NetCore下基于FreeRedis实现的Redis⑥0客户端缓存之缓存键条件优雅过滤

.NetCore下基于FreeRedis实现的Redis⑥0客户端缓存之缓存键条件优雅过滤

.NetCore下基于FreeRedis实现的Redis⑥0客户端缓存之缓存键条件优雅过滤

.NetCore下基于FreeRedis实现的Redis⑥0客户端缓存之缓存键条件优雅过滤

.NetCore下基于FreeRedis实现的Redis⑥0客户端缓存之缓存键条件优雅过滤

.NetCore下基于FreeRedis实现的Redis⑥0客户端缓存之缓存键条件优雅过滤

.NetCore下基于FreeRedis实现的Redis⑥0客户端缓存之缓存键条件优雅过滤

以上的完整代码已经放到Github上:查看完整代码

版权声明:倡导尊重与保护知识产权。未经许可,任何人不得复制、转载、或以其他方式使用本站《原创》内容,违者将追究其法律责任。本站文章内容,部分图片来源于网络,如有侵权,请联系我们修改或者删除处理。

编辑推荐

热门文章