.Net 开源项目 FreeRedis 实现思路之 - Redis 6.0 客户端缓存技术

1|0写在开头

FreeRedis 是一款继 CSRedisCore 之后重写的 .NET redis 客户端开源组件,以 MIT 协议开源托管于 github,目前支持 .NET 5、.NETCore 2.1+、.NETFramework 4.0+、Xamarin,有可能已经支持 AOT 编译(目前未测试,但会往这个方向走)。

  • 🌈 所有方法名与 redis-cli 保持一致
  • 🌌 支持 Redis 集群(服务端要求 3.2 及以上版本)
  • ⛳ 支持 Redis 哨兵模式
  • 🎣 支持主从分离(Master-Slave)
  • 📡 支持发布订阅(Pub-Sub)
  • 📃 支持 Redis Lua 脚本
  • 💻 支持管道(Pipeline)
  • 📰 支持事务
  • 🌴 支持 GEO 命令(服务端要求 3.2 及以上版本)
  • 🌲 支持 STREAM 类型命令(服务端要求 5.0 及以上版本)
  • ⚡ 支持本地缓存(Client-side-cahing,服务端要求 6.0 及以上版本)
  • 🌳 支持 Redis 6 的 RESP3 协议

github: https://github.com/2881099/FreeRedis

2|0了解 Redis

Redis是一个开源的使用C语言编写、开源、支持网络、可基于内存亦可持久化的日志型、高性能的Key-Value数据库,并提供多种语言的API。它通常被称为 数据结构服务器 ,因为值(value)可以是 字符串(string)、哈希(map)、 列表(list)、集合(sets)、有序集合(sorted sets)、地理位置(Geo)、消息列队(Streams)等类型。

与其他 key - value 缓存产品有以下三个特点:

  • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。
  • Redis不仅仅支持简单的key-value类型的数据,同时还提供list,set,zset,hash等数据结构的存储。
  • Redis支持数据的备份,即master-slave模式的数据备份。

优势:

  • 性能极高 – Redis能读的速度是110000次/s,写的速度是81000次/s 。
  • 丰富的数据类型 – Redis支持二进制案例的 Strings, Lists, Hashes, Sets, Ordered Sets, Geo, Streams 数据类型操作。
  • 原子 – Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。
  • 丰富的特性 – Redis还支持 publish/subscribe, 通知, key 过期等等特性。

3|0本文看点

Redis 6.0 是一个可期的版本,增加了 RESP3.0 协议,ACL 权限控制,从原有的单线程改为多线程(性能提升2-3倍)等诸多更新。今天向大家介绍他的另一个重要特性:客户端缓存技术,讲解如何落设计在 .NET 中。

为什么需要客户端缓存?

我们都知道,使用 Redis 进行数据的缓存的主要目的是减少对 MySQL 等数据库的访问,提供更快的访问速度,毕竟 《Redis in Action》中提到的, Redis 的性能大致是普通关系型数据库的 10 ~ 100 倍。

所以,如下图所示,Redis 用来存储热点数据,Redis 未命中,再去访问数据库,这样可以应付大多数情况下的性能要求。

但是,Redis 也有其性能上限,并且访问 Redis 必然有一定的网络 I/O 以及序列化反序列化损耗。所以,往往会引入进程缓存,将最热的数据存储在本地,进一步加快访问速度。

如上图所示,Guava Cache 等进程缓存作为一级缓存,Redis 作为二级缓存:

  • 先去 Guava Cache 中查询数据,如果命中则直接返回。
  • Guava Cache 中未命中,则再去 Redis 中查询,如果命中则返回数据,并在 Guava Cache 中设置此数据。
  • Redis 也未命中的话,只有去 MySQL 中查询,然后依次将数据设置到 Redis 和 Guava Cache 中。

只使用 Redis 分布式缓存时,遇到数据更新时,应用程序更新完 MySQL 中的数据,可以直接将 Redis 中对应缓存失效掉,保持数据的一致性。

而进程内缓存的数据一致性比分布式的缓存面临更大的挑战。数据更新的时候,如何通知其他进程也更新自己的缓存呢?

如果按照分布式缓存的思路,我们可以设置极短的缓存失效时间,这样不必实现复杂的通知机制。

但是不同进程内的数据依然会面临不一致的问题,并且不同进程缓存失效时间不统一,同一个请求到了不同的进程,可能出现反复幻读的情况。

4|0落地分析

如上当 key 失效的时候,Redis 6.0 提供了三种模式通知客户端,普通模式、广播模式、转发模式。

1、普通模式

普通模式依赖 RESP3.0 协议,需要在连接成功时使用 hello 命令开启 RESP3.0 模式。

hello 3

client tracking on

落地在 .NET 之中时,我们必然是使用连接池技术,那么每个连接都必须使用以上的两个命令,此时每个连接是一个循环读的操作,如下:

while (true) { var msg = await redisSocket.ReceiveAsync(); //等待 key 失效的通知 }

本来我们可以比较简单的这样执行命令:

await redisSocket.SendAsync("GET key1"); await redisSocket.ReceiveAsync(); //读取响应的结果

可以看出来,两段代码同时读,会导致读取的结果错乱。如何解决还需要三思,而我们 PASS 了这种模式。


2、广播模式

广播模式和普通模式差不多,都需要依赖 RESP3.0 协议。这种方式下 Redis 服务端不再消耗过多内存存储信息,而是发送更多的失效消息给客户端。

与普通模式必须获取一次键的规则不同,广播模式下,只要键被修改或删除,符合规则的客户端都会收到失效消息,而且是可以多次获取的

与普通模式相比,虽然少存储了一些数据,但是由于需要对前缀规则进行匹配,会消耗一定的 CPU 资源,所以注意别使用过长的前缀。

广播模式和普通模式一样,需要解决命令同时读取的问题(请见上面的两段代码)。


3、转发模式

Redis 为了兼容 RESP2 协议提供了转发(Redirect)模式,不再使用 RESP3 原生支持 PUSH 消息,而是将消息通过 Pub/Sub 通知给另外一个客户端,具体流程如下图所示。

public void Start() { //订阅 __redis__:invalidate _sub = _cli.Subscribe("__redis__:invalidate", InValidate) as IPubSubSubscriber; //拦截缓存 _cli.Interceptors.Add(() => new MemoryCacheAop(this)); //当网络断开的时候,清空本地缓存 _cli.Unavailable += (_, e) => { lock (_dictLock) _dictSort.Clear(); _dict.Clear(); }; _cli.Connected += (_, e) => { //最关键的一个命令,否则 __redis__:invalidate 无法收到订阅消息 e.Client.ClientTracking(true, _sub.RedisSocket.ClientId, null, false, false, false, false); }; } void InValidate(string chan, object msg) { var keys = msg as object[]; foreach (var key in keys) //移除本地缓存 RemoveCache(string.Concat(key)); }

MemoryCacheAop 是 FreeRedis 已经实现好的拦截器,主要实现拦截命令执行,获取本地内存。完整代码:https://github.com/2881099/FreeRedis/blob/master/src/FreeRedis/ClientSideCaching.cs

5|0测试功能

static Lazy<RedisClient> _cliLazy = new Lazy<RedisClient>(() => { var r = new RedisClient("192.168.164.10:6379,database=1"); //redis 6.0 r.Serialize = obj => JsonConvert.SerializeObject(obj); r.Deserialize = (json, type) => JsonConvert.DeserializeObject(json, type); r.Notice += (s, e) => Console.WriteLine(e.Log); return r; }); static RedisClient cli => _cliLazy.Value; static void Main(string[] args) { cli.UseClientSideCaching(new ClientSideCachingOptions { //本地缓存的容量 Capacity = 3, //过滤哪些键能被本地缓存 KeyFilter = key => key.StartsWith("Interceptor"), //检查长期未使用的缓存 CheckExpired = (key, dt) => DateTime.Now.Subtract(dt) > TimeSpan.FromSeconds(2) }); cli.Set("Interceptor01", "123123"); //redis-server var val1 = cli.Get("Interceptor01"); //redis-server var val2 = cli.Get("Interceptor01"); //本地 var val3 = cli.Get("Interceptor01"); //断点等3秒,redis-server cli.Set("Interceptor01", "234567"); //redis-server var val4 = cli.Get("Interceptor01"); //redis-server var val5 = cli.Get("Interceptor01"); //本地 var val6 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //redis-server var val7 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //本地 var val8 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //本地 cli.MSet("Interceptor01", "Interceptor01Value", "Interceptor02", "Interceptor02Value", "Interceptor03", "Interceptor03Value"); //redis-server var val9 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //redis-server var val10 = cli.MGet("Interceptor01", "Interceptor02", "Interceptor03"); //本地 //以下 KeyFilter 返回 false,从而不使用本地缓存 cli.Set("123Interceptor01", "123123"); //redis-server var val11 = cli.Get("123Interceptor01"); //redis-server var val12 = cli.Get("123Interceptor01"); //redis-server var val23 = cli.Get("123Interceptor01"); //redis-server Console.ReadKey(); }

cli.Notice 事件在控制台输出内容:

Not connected 代表没有经过 redis-server

192.168.164.10:6379 > CLIENT TRACKING ON REDIRECT 46 FreeRedis.RedisResult (0ms) 192.168.164.10:6379 > SET Interceptor01 123123 OK (24ms) 192.168.164.10:6379 > GET Interceptor01 123123 (2ms) Not connected > GET Interceptor01 123123 (0ms) 192.168.164.10:6379 > GET Interceptor01 123123 (0ms) 192.168.164.10:6379 > SET Interceptor01 234567 OK (0ms) 192.168.164.10:6379 > GET Interceptor01 234567 (0ms) Not connected > GET Interceptor01 234567 (0ms) 192.168.164.10:6379 > MGET Interceptor01 Interceptor02 Interceptor03 [234567, Interceptor02Value, Interceptor03Value] (0ms) Not connected > MGET Interceptor01 Interceptor02 Interceptor03 [234567, Interceptor02Value, Interceptor03Value] (0ms) Not connected > MGET Interceptor01 Interceptor02 Interceptor03 [234567, Interceptor02Value, Interceptor03Value] (0ms) 192.168.164.10:6379 > MSET Interceptor01 Interceptor01Value Interceptor02 Interceptor02Value Interceptor03 Interceptor03Value False (3ms) 192.168.164.10:6379 > MGET Interceptor01 Interceptor02 Interceptor03 [Interceptor01Value, Interceptor02Value, Interceptor03Value] (1ms) Not connected > MGET Interceptor01 Interceptor02 Interceptor03 [Interceptor01Value, Interceptor02Value, Interceptor03Value] (0ms) 192.168.164.10:6379 > SET 123Interceptor01 123123 OK (0ms) 192.168.164.10:6379 > GET 123Interceptor01 123123 (0ms) 192.168.164.10:6379 > GET 123Interceptor01 123123 (0ms) 192.168.164.10:6379 > GET 123Interceptor01 123123 (0ms)

6|0写在最后

FreeRedis 是一款继 CSRedisCore 之后重写的 .NET redis 客户端开源组件,以 MIT 协议开源托管于 github,目前支持 .NET 5、.NETCore 2.1+、.NETFramework 4.0+、Xamarin,有可能已经支持 AOT 编译(目前未测试,但会往这个方向走)。

github: https://github.com/2881099/FreeRedis

谢谢支持!!


__EOF__

本文作者FreeSql & CSRedis
本文链接https://www.cnblogs.com/kellynic/p/14009158.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   nicye  阅读(9418)  评论(42编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示