前言

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

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

从Redis 6.0开始有一个重要特性就是支持客户端缓存(仅支持String类型),效果跟内存缓存是一样的,数据都是从内存中获取,如果服务端缓存数据发送变动,会在极短的时间内通知到所有客户端进行数据同步

在 .NetCore 环境中,我们常用的Redis组件是 StackExchangeRedis 和 CSRedisCore,但是都不支持6.0的客户端缓存这一特性,CSRedisCore 的作者在前两年又重新开发了一个叫 FreeRedis 的组件,并支持了客户端缓存

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

不过我们的主力组件还是CSRedisCore,FreeRedis基本只是用到了客户端缓存,因为当时的版本还不支持异步方法,我记得是今年才加上的

FreeRedis组件介绍原文,有关客户端缓存具体实现原理看看这篇就够了:FreeRedis

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

扩展前

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

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

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

缺点:

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

这种方式我测试过,虽然每次都调用一下不影响最后客户端缓存效果,但RedisClient中的拦截器是一直在新增的,这上线后不得崩了?

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

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

缺点:

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

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

第三种:所有用到客户端缓存的键约定好一个统一命名前缀,那么过滤条件这里只需要写一个 StartWith(命名前缀) 的条件就行了

缺点:

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

某些业务可能一口气需要用到了好几个缓存Key组合进行实现,但其中只有一个Key需要本地缓存,那么这个Key的前缀和其他Key的业务命名前缀就不统一了,虽然没什么问题,但是在客户端工具中查看键值时没放在一起,不利于查找

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

 三种方式在实现好用程度上排个序: 第三种 > 第二种 > 第一种

扩展后

三种方式在我司项目中其实都不好用,我们项目中之前的所有缓存都是一个缓存实现对应一个缓存类,每个缓存类会继承一个对应该缓存用的Redis数据结构基类,例如CacheBaseStringCacheBaseSetCacheBaseSortedSetCacheBaseList...等

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

所以基于这个要求,我们对FreeRedis的客户端缓存实现进行一下扩展,首先客户端缓存只支持String类型,所以就是再写一个String结构的ClienSideCacheBase就好了,最麻烦的就是如何优雅的统一实现Key的过滤条件

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

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

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

1. 首先我们先设计一下基类

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

    /// <summary>
/// Redis6.0客户端缓存实现基类
/// </summary>
public abstract class ClienSideCacheBase
{
/// <summary>
/// RedisService
/// </summary>
private static FreeRedisService _redisService; /// <summary>
/// 获取RedisKey
/// </summary>
/// <returns></returns>
protected abstract string GetRedisKey(); /// <summary>
/// 设置客户端缓存Key过滤条件
/// </summary>
/// <returns></returns>
public abstract Expression<Func<string,bool>> SetCacheKeyFilter(); /// <summary>
/// 私有构造函数
/// </summary>
private ClienSideCacheBase() { } /// <summary>
/// 构造函数
/// </summary>
/// <param name="serviceProvider"></param>
public ClienSideCacheBase(IServiceProvider serviceProvider)
{
_redisService = serviceProvider.GetService<FreeRedisService>();
} /// <summary>
/// 获取值
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public T Get<T>()
{
return _redisService.Instance.Get<T>(GetRedisKey());
} /// <summary>
/// 设置值
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <returns></returns>
public bool Set<T>(T data)
{
_redisService.Instance.Set(GetRedisKey(),data);
return true;
} /// <summary>
/// 设置值
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <param name="seconds"></param>
/// <returns></returns>
public bool Set<T>(T data,int seconds)
{
_redisService.Instance.Set(GetRedisKey(),data,TimeSpan.FromSeconds(seconds));
return true;
} /// <summary>
/// 设置值
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <param name="expired"></param>
/// <returns></returns>
public bool Set<T>(T data,TimeSpan expired)
{
_redisService.Instance.Set(GetRedisKey(),data,expired);
return true;
} /// <summary>
/// 设置值
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <param name="expiredAt"></param>
/// <returns></returns>
public bool Set<T>(T data,DateTime expiredAt)
{
_redisService.Instance.Set(GetRedisKey(),data,TimeSpan.FromSeconds(expiredAt.Subtract(DateTime.Now).TotalSeconds));
return true;
} /// <summary>
/// 设置过期时间
/// </summary>
/// <returns></returns>
public bool SetExpire(int seconds)
{
return _redisService.Instance.Expire(GetRedisKey(),TimeSpan.FromSeconds(seconds));
} /// <summary>
/// 设置过期时间
/// </summary>
/// <returns></returns>
public bool SetExpire(TimeSpan expired)
{
return _redisService.Instance.Expire(GetRedisKey(),expired);
} /// <summary>
/// 设置过期时间
/// </summary>
/// <returns></returns>
public bool SetExpireAt(DateTime expiredTime)
{
return _redisService.Instance.ExpireAt(GetRedisKey(),expiredTime);
} /// <summary>
/// 移除缓存
/// </summary>
/// <returns></returns>
public long Remove()
{
return _redisService.Instance.Del(GetRedisKey());
} /// <summary>
/// 缓存是否存在
/// </summary>
/// <returns></returns>
public bool Exists()
{
return _redisService.Instance.Exists(GetRedisKey());
}
}

具体继承用法如下:

    /// <summary>
/// 实现客户端缓存Demo1
/// </summary>
public class ClientSideDemoOneCache : ClienSideCacheBase
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="serviceProvider"></param>
public ClientSideDemoOneCache(IServiceProvider serviceProvider) : base(serviceProvider) { } /// <summary>
/// 设置Key过滤规则
/// </summary>
/// <returns></returns>
public override Expression<Func<string,bool>> SetCacheKeyFilter()
{
return o => o == GetRedisKey();
} /// <summary>
/// 获取缓存的Key
/// </summary>
/// <returns></returns>
protected override string GetRedisKey()
{
return "DemoOneRedisKey";
}
} /// <summary>
/// 实现客户端缓存Demo2
/// </summary>
public class ClientSideDemoTwoCache : ClienSideCacheBase
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="serviceProvider"></param>
public ClientSideDemoTwoCache(IServiceProvider serviceProvider) : base(serviceProvider) { } /// <summary>
/// 设置Key过滤规则
/// </summary>
/// <returns></returns>
public override Expression<Func<string,bool>> SetCacheKeyFilter()
{
return o => o.StartsWith(GetRedisKey());
} /// <summary>
/// 获取缓存的Key
/// </summary>
/// <returns></returns>
protected override string GetRedisKey()
{
return "DemoTwoRedisKey";
}
}

2. FreeRedisService的实现

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

    public class FreeRedisService
{
/// <summary>
/// RedisClient
/// </summary>
private static RedisClient _redisClient; /// <summary>
/// 初始化配置
/// </summary>
private FreeRedisOption _redisOption; /// <summary>
/// 构造函数
/// </summary>
public FreeRedisService(FreeRedisOption redisOption)
{
if (redisOption == null) {
throw new NullReferenceException("初始化配置为空");
}
_redisOption = redisOption;
InitRedisClient();
} /// <summary>
/// 懒加载Redis客户端
/// </summary>
private readonly static Lazy<RedisClient> redisClientLazy = new Lazy<RedisClient>(() => {
var r = _redisClient;
r.Serialize = obj => JsonConvert.SerializeObject(obj);
r.Deserialize = (json,type) => JsonConvert.DeserializeObject(json,type);
r.Notice += (s,e) => Console.WriteLine(e.Log);
return r;
}); private static readonly object obj = new object(); /// <summary>
/// 初始化Redis
/// </summary>
/// <returns></returns>
bool InitRedisClient()
{
if (_redisClient == null) {
lock (obj) {
if (_redisClient == null) {
_redisClient = new RedisClient($"{_redisOption.RedisHost}:{_redisOption.RedisPort},password={_redisOption.RedisPassword},defaultDatabase={_redisOption.DefaultIndex},poolsize={_redisOption.Poolsize},ssl=false,writeBuffer=10240,prefix={_redisOption.Prefix},asyncPipeline={_redisOption.asyncPipeline},connectTimeout={_redisOption.ConnectTimeout},abortConnect=false");
//设置客户端缓存
if (_redisOption.UseClientSideCache) {
if (_redisOption.ClientSideCacheKeyFilter == null) {
throw new NullReferenceException("如果开启客户端缓存,必须设置客户端缓存Key过滤条件");
}
_redisClient.UseClientSideCaching(new ClientSideCachingOptions() {
Capacity = 0, //本地缓存的容量,0不限制
KeyFilter = _redisOption.ClientSideCacheKeyFilter, //过滤哪些键能被本地缓存
CheckExpired = (key,dt) => DateTime.Now.Subtract(dt) > TimeSpan.FromSeconds(3) //检查长期未使用的缓存
});
}
return true;
}
}
}
return _redisClient != null;
} /// <summary>
/// 获取Client实例
/// </summary>
public RedisClient Instance {
get {
if (InitRedisClient()) {
return redisClientLazy.Value;
}
throw new NullReferenceException("Redis不可用");
}
}
}

3. 反射遍历获取所有过滤条件

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

    /// <summary>
/// 构建Redis客户端缓存Key条件
/// </summary>
public class ClientSideCacheKeyBuilder
{
/// <summary>
/// 具体缓存业务实现所在项目程序集
/// </summary>
const string DefaultDllName = "Hy.Components.Api"; /// <summary>
/// 构建表达式树
/// </summary>
/// <param name="serviceProvider">serviceProvider</param>
/// <param name="dllName">当前类所在的项目dll名</param>
/// <returns></returns>
public static Func<string,bool> Build(IServiceProvider serviceProvider,string dllName = DefaultDllName)
{
Expression<Func<string,bool>> expression = o => false; //默认false
var baseClass = typeof(ClienSideCacheBase);
Assembly ass = Assembly.LoadFrom($"{AppDomain.CurrentDomain.BaseDirectory}{dllName}.dll");
Type[] types = ass.GetTypes();
foreach (Type item in types) {
if (item.IsInterface || item.IsEnum || item.GetCustomAttribute(typeof(ObsoleteAttribute)) != null) {
continue;
}
//判读基类
if (item != null && item.BaseType == baseClass) {
var instance = (ClienSideCacheBase)Activator.CreateInstance(item,serviceProvider); //这里参数带入IServiceProvider纯粹为了创建实例不报错
var expr = instance.SetCacheKeyFilter();
expression = expression.Or(expr); //合并树
}
}
return expression.Compile();
}
}

4. 将FreeRedis服务在IOC容器中注入

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

    public static class ServiceCollectionExtensions
{
/// <summary>
/// ServiceInject
/// </summary>
/// <param name="services"></param>
public static void AddRedisService(this IServiceCollection services,IConfiguration configuration)
{
var clientCacheKeyFilter = ClientSideCacheKeyBuilder.Build(services.BuildServiceProvider()); //构造过滤条件
var option = GetRedisOption(configuration,clientCacheKeyFilter); //组装Redis初始配置
services.AddSingleton(c => new FreeRedisService(option)); //FreeRedis注入为单例
} /// <summary>
/// 获取配置
/// </summary>
/// <param name="configuration"></param>
/// <param name="clientSideCacheKeyFilter"></param>
/// <returns></returns>
static FreeRedisOption GetRedisOption(IConfiguration configuration,Func<string,bool> clientSideCacheKeyFilter = null)
{
return new FreeRedisOption() {
RedisHost = configuration.GetSection("Redis:RedisHost").Value,
RedisPassword = configuration.GetSection("Redis:RedisPassword").Value,
RedisPort = Convert.ToInt32(configuration.GetSection("Redis:RedisPort").Value),
SyncTimeout = 5000,
ConnectTimeout = 15000,
DefaultIndex = 0,
Poolsize = 5,
UseClientSideCache = clientSideCacheKeyFilter != null,
ClientSideCacheKeyFilter = clientSideCacheKeyFilter
};
}
}

在项目IOC容器中注入,以下为.Net6的Program模板

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllers();

builder.Services.AddHealthChecks();

//注入Redis服务
builder.Services.AddRedisService(builder.Configuration); //可选:注入客户端缓存具体实现类。 如果实现有很多,这里会有一大堆注入代码。在代码中直接实例化类并传入IServiceProvider也一样的
builder.Services.AddSingleton<ClientSideDemoOneCache>();
builder.Services.AddSingleton<ClientSideDemoTwoCache>(); //构建WebApplication
var app = builder.Build(); app.UseAuthorization(); app.MapControllers(); app.UseHealthChecks("/health"); app.Run();

5. 最后看下我们在业务代码中的具体用法

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

    [ApiController]
[Route("[controller]")]
public class HomeController : ControllerBase
{
private readonly ILogger<HomeController> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly ClientSideDemoOneCache _clientSideDemoOneCache; public HomeController(ILogger<HomeController> logger,IServiceProvider serviceProvider,ClientSideDemoOneCache clientSideDemoOneCache)
{
_logger = logger;
_serviceProvider = serviceProvider;
_clientSideDemoOneCache = clientSideDemoOneCache;
} #region 可通过启动不同端口的Api,分别调用以下接口对同一个Key进行操作,测试客户端缓存是否生效以及是否及时同步 /// <summary>
/// 测试get
/// </summary>
/// <returns></returns>
[HttpGet, Route("getvalue")]
public string TestGetValue()
{
ClientSideDemoOneCache cacheOne = new ClientSideDemoOneCache(_serviceProvider);
//cacheOne = _clientSideDemoOneCache; //通过容器拿到实例
var value = cacheOne.Get<string>();
return value ?? "缓存空了";
} /// <summary>
/// 测试set
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
[HttpGet, Route("setvalue")]
public string TestSetValue([FromQuery] string value)
{
ClientSideDemoOneCache cacheOne = new ClientSideDemoOneCache(_serviceProvider);
cacheOne.Set(value);
return "OK";
} /// <summary>
/// 测试del
/// </summary>
/// <returns></returns>
[HttpGet, Route("delvalue")]
public string TestDelValue()
{
ClientSideDemoOneCache cacheOne = new ClientSideDemoOneCache(_serviceProvider);
cacheOne.Remove();
return "OK";
} #endregion
}

6. 单机测试

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

Redis客户端:

2. 再获取一下值,成功拿到

3. 再次刷新一下,我们看下打印出来的日志,可以发现第一次是从服务端取值,第二次显示从本地取值,说明过滤条件已经生效了

 7. 在本机开启两个Api服务,模拟分布式测试

1. 通过2个不同的端口启动两个Api服务,可以看到目前拿到都是同一个值

2. 我们通过其中一个服务修改一下值,发现另外一台马上就变化了

3. 再次刷新一下getvalue接口,看下日志,发现第一次的值222222是从服务端获取,第二次又是从本地获取了

4. 接着我们再通过其中一个服务,删掉这个Key,发现另一个服务马上就获取不到值了

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

原创作者:Harry

原文出处:https://www.cnblogs.com/simendancer/articles/17052784.html

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

  1. 实战派 | Java项目中玩转Redis6.0客户端缓存!

    原创:微信公众号 码农参上,欢迎分享,转载请保留出处. 哈喽大家好啊,我是Hydra. 在前面的文章中,我们介绍了Redis6.0中的新特性客户端缓存client-side caching,通过tel ...

  2. c++下基于windows socket的单线程服务器客户端程序(基于TCP协议)

    今天自己编写了一个简单的c++服务器客户端程序,注释较详细,在此做个笔记. windows下socket编程的主要流程可概括如下:初始化ws2_32.dll动态库-->创建套接字-->绑定 ...

  3. linux下安装redis-6.0.6、配置redis远程连接

    官网下载安装包redis-6.0.6.tar.gz https://redis.io/ 上传到服务器之后使用tar -zxvf进行解压,解压后如下: 进入解压的文件之后我们可以看到他的配置文件(配置文 ...

  4. .netcore下的微服务、容器、运维、自动化发布

    原文:.netcore下的微服务.容器.运维.自动化发布 微服务 1.1     基本概念 1.1.1       什么是微服务? 微服务架构是SOA思想某一种具体实现.是一种将单应用程序作为一套小型 ...

  5. SpringMVC快速使用——基于XML配置和Servlet3.0

    SpringMVC快速使用--基于XML配置和Servlet3.0 1.官方文档 https://docs.spring.io/spring-framework/docs/5.2.8.RELEASE/ ...

  6. 追求性能极致:Redis6.0的多线程模型

    Redis系列1:深刻理解高性能Redis的本质 Redis系列2:数据持久化提高可用性 Redis系列3:高可用之主从架构 Redis系列4:高可用之Sentinel(哨兵模式) Redis系列5: ...

  7. .NET环境下基于RBAC的访问控制

    .NET环境下基于RBAC的访问控制 Access Control of Application Based on RBAC model in .NET Environment 摘 要:本文从目前信息 ...

  8. 【应用笔记】【AN004】VB环境下基于RS-485的4-20mA电流采集

    版本:第一版作者:周新稳 杨帅 日期:20160226 =========================== 本资料高清PDF 下载: http://pan.baidu.com/s/1c1uuhLQ ...

  9. windows下基于sublime text3的nodejs环境搭建

    第一步:先安装sublime text3.详细教程可自行百度,这边不具体介绍了. 第二步.安装nodejs插件,有两种方式 第一种方式:直接下载https://github.com/tanepiper ...

  10. 在XP下基于VHD版XP 2003 win7制作的RAMOS心得

    在XP下基于VHD版win7制作的RAMOS心得1.用DiskGenius创建1.85G的VHD固定磁盘文件,以win7prosen.vhd为例,然后进行分区格式化,格式化时启用NTFS压缩.2.为了 ...

随机推荐

  1. java 入土--集合详解

    java 集合 集合是对象的容器,实现了对对象的常用的操作,类似数组功能. 和数组的区别: 数组长度固定,集合长度不固定 数组可以存储基本类型和引用类型,集合只能存储引用类型 使用时需要导入类 Col ...

  2. 齐博x1关于小程序个性源代码的说明

    系统默认推荐商家小程序使用通用型的源码,即框架套壳iframe形式的.这个灵活性更高.但如果有特殊需求的话,也可以设置个性源码,比如配合uni-app使用,针对不同的小程序就使用不同的uni-app风 ...

  3. 动词时态=>3.现在时态和过去时态构成详解

    现在时态构成详解 一般现在时态 最容易构成的时态,直接加动词原形(字典当中显示的词条)就可以 第三人称"单数"的话需要加s 这是最容易出错的时态:容易将 现在的时间,和一般的状态: ...

  4. (数据科学学习手札145)在Python中利用yarl轻松操作url

    本文示例代码已上传至我的Github仓库https://github.com/CNFeffery/DataScienceStudyNotes 1 简介 大家好我是费老师,在诸如网络爬虫.web应用开发 ...

  5. 一篇文章带你了解NoSql数据库——Redis简单入门

    一篇文章带你了解NoSql数据库--Redis简单入门 Redis是一个基于内存的key-value结构数据库 我们会利用其内存存储速度快,读写性能高的特点去完成企业中的一些热门数据的储存信息 在本篇 ...

  6. .NET中的拦截器filter的使用

    拦截器的使用 使用场景分析 我们先想像一个场景,就是程序员开发软件,他是怎么工作的呢?我们都知道,普通的程序员只需要根据需求文档开发相应的功能即可,他不用和客户谈论软件需求,不用理会软件卖多少钱,他要 ...

  7. Oracle性能优化之内存管理

    Oracle实例中的内存使用分为两类:程序全局区(program global area, PGA)和系统全局区(system global area, SGA).前者专门供每个会话使用,后者由所有O ...

  8. java学习之SpringMVC

    0x00前言 Spring MVC 是 Spring 提供的一个基于 MVC 设计模式的轻量级 Web 开发框架,本质上相当于 Servlet. Spring MVC 是结构最清晰的 Servlet+ ...

  9. 2022ICPC区域赛参后感悟

    第一次参加正式的大类赛事,在某种程度上挺激动的.我呢,可以说是刚步入竞赛一年,在此期间遇见了一些志同道合的朋友,最重要的是遇见了我的队友. 开始前,我幻想过我们小队可以超常发挥,拿取学校中第一个区域赛 ...

  10. vue阻止向上和向下冒泡

    阻止向下冒泡 <div class="content" @click.self="cancelFunc"></div> 阻止向上冒泡 & ...