来源 | 程序员历小冰责编 | Carol封图 | CSDN 付费下载于视觉中国近日 Redis 6.0.0 GA 版本发布,这是 Redis 历史上最大的一次版本更新,包括了客户端缓存 (Client side caching)、ACL、Threaded I/O 和 Redis Cluster Proxy 等诸多更新。我们今天就依次聊一下客户端缓存的必要性、具体使用、原理分析和实现。

为什么需要客户端缓存?

我们都知道,使用 Redis 进行数据的缓存的主要目的是减少对 MySQL 等数据库的访问,提供更快的访问速度,毕竟 《Redis in Action》中提到的, Redis 的性能大致是普通关系型数据库的 10 ~ 100 倍。所以,如下图所示,Redis 用来存储热点数据,Redis 未命中,再去访问数据库,这样可以应付大多数情况下的性能要求。但是,Redis 也有其性能上限,并且访问 Redis 必然有一定的网络 I/O 以及序列化反序列化损耗。所以,往往会引入进程缓存,将最热的数据存储在本地,进一步加快访问速度。如上图所示(示意图,细节不必过度在意,下同),Guava Cache 等进程缓存作为一级缓存,Redis 作为二级缓存:

  1. 先去 Guava Cache 中查询数据,如果命中则直接返回。

  2. Guava Cache 中未命中,则再去 Redis 中查询,如果命中则返回数据,并在 Guava Cache 中设置此数据。

  3. Redis 也未命中的话,只有去 MySQL 中查询,然后依次将数据设置到 Redis 和 Guava Cache 中。

只使用 Redis 分布式缓存时,遇到数据更新时,应用程序更新完 MySQL 中的数据,可以直接将 Redis 中对应缓存失效掉,保持数据的一致性。而进程内缓存的数据一致性比分布式的缓存面临更大的挑战。数据更新的时候,如何通知其他进程也更新自己的缓存呢?如果按照分布式缓存的思路,我们可以设置极短的缓存失效时间,这样不必实现复杂的通知机制。但是不同进程内的数据依然会面临不一致的问题,并且不同进程缓存失效时间不统一,同一个请求到了不同的进程,可能出现反复幻读的情况。Ben 在 RedisConf18 给出了一个方案(视频和 PPT 链接在文末),通过 Redis 的 Pub/Sub,可以通知其他进程缓存对此缓存进行删除。如果 Redis 挂了或者订阅机制不靠谱,依靠超时设定,依然可以做兜底处理。Antirez(Redis 的作者) 也正是听取 Ben 这个方案后,才决定在 Redis Server 支持客户端缓存的,因为在有服务端参与的情况下可以更好的处理上述这些问题。

功能介绍和演示

下面使用 Docker 安装 Redis 6.0.1,然后使用 telnet 来简单演示一下 Redis 6.0 的客户端缓存功能。所有相关的功能如下图所示,分别是使用RESP3 协议版本的普通模式和广播模式,以及使用 RESP2 协议版本的转发模式。我们先来看普通模式。

1、普通模式

先使用 redis-cli 设置缓存值 test=111,使用 telnet 连接上 Redis,然后发送 hello 3 开启 RESP3 协议。

[root@VM_0_3_centos ~]# telnet 127.0.0.16379

Trying127.0.0.1...

Connected to 127.0.0.1.

Escape character is'^]'.

hello 3

// telnet 输出结果格式化标准化后如下,否则换行太多并且是 RESP3 格式,不需要了解格式。

> HELLO 3

1# "server" => "redis"

2# "version" => "6.0.1"

3# "proto" => (integer) 3

4# "id" => (integer) 10

5# "mode" => "standalone"

6# "role" => "master"

7# "modules" => (empty array)

这里需要注意,Redis 服务端只会 track 客户端在一个连接生命周期内的获取的只读命令的 key值Redis 客户端默认不开启 track 模式,需要使用命令开启,然后必须要先获取一次 test 的值,这样 Redis 服务器才会记录它。

client tracking on

+OK

get test

$3

111

当键被修改,或者因为失效时间(expire time)和内存上限 maxmemory 策略被驱除时,Redis 服务端会通知这些客户端。我们这里简单地更新 test 的值,telnet 则会收到如下通知

>2// RESP3 中的 PUSH 类型,标志为 > 符号

$10

invalidate

*1

$4

test

如果你再一次更新 test 值,这次 telnet 就不会再收到失效(invalidate)消息。除非 telnet 再进行一次 get 操作,重新 tracking 对应的键值。也就是说 Redis 服务端记录的客户端 track 信息只生效一次,发送过失效消息后就会删除,只有下次客户端再次执行只读命令被 track,才会进行下一次消息通知 。取消 tracking 的命令如下所示。

client tracking off

+OK

2、广播模式

Redis 还提供了一种广播模式(BCAST),它是另外一种客户端缓存的实现方式。这种方式下 Redis 服务端不再消耗过多内存存储信息,而是发送更多的失效消息给客户端。这是服务端存储过多数据,消耗内存和客户端收到过多消息,消耗网络带宽之间的权衡(tradeoff)。

// 已经 hello 3 开启 RESP3 协议,不然无法收到失效消息,下同

client tracking on bcast

+OK

// 此时设置 key 为 a 的键值,收到如下消息。

>2

$10

invalidate

*1

$1

a

如果你不想所有的键值的失效消息都收到,则可以限制 key 的前缀,如下命令则表示只关注前缀为 test 的键值的消息。一般来说,业务的缓存 key 都是根据业务拥有统一的前缀,所以这一特性十分方便。

client tracking on bcast prefix test

与普通模式必须获取一次键的规则不同,广播模式下,只要键被修改或删除,符合规则的客户端都会收到失效消息,而且是可以多次获取的与普通模式相比,虽然少存储了一些数据,但是由于需要对前缀规则进行匹配,会消耗一定的 CPU 资源,所以注意别使用过长的前缀

3、转发模式

上述操作时客户端都需要先开启 RESP3,Redis 为了兼容 RESP2 协议提供了转发(Redirect)模式,不再使用 RESP3 原生支持 PUSH 消息,而是将消息通过 Pub/Sub 通知给另外一个客户端,具体流程如下图所示。这里需要两个 telnet,其中一个 telnet 需要订阅 _redis_:invalidate信道。然后另一个 telnet 开启 Redirect 模式,并制定将失效消息通过订阅信道发送给第一个 telnet。

# telent B

client id

:368

subscribe _redis_:invalidate

# telnet A,开启 track 并指定转发给 B

client tracking on bcast redirect 368

# telent B 此时有键值被修改,收到 __redis__:invalidate 信道的消息

message

$20

__redis__:invalidate

*1

$1

a

你会发现,转发模式和文章开始提到的多级缓存中的更新机制很类似了,只不过那个方案中是业务系统修改完 key 后发送消息通知,而这里是 Redis 服务端代替业务系统发送消息通知。

4、OPTIN 和 OPTOUT 选项

使用 OPTIN 可以选择性的开启 tracking。只有你发送 client caching yes (Redis 文档中是 CACHING 命令,但是实验时发现无效)之后的下一条的只读命令的 key 才会 tracking,否则其他的只读命令的 key 不会被 tracking。

client tracking on optin

client caching yes

get a

get b

// 此时修改 a 和 b 的值,发现只收到 a 的失效消息

>2

$10

invalidate

*1

$1

a

而 OPTOUT 参数与之相反,你可以有选择的退出 tracking。发送 client caching off 之后的下一条只读命令的 key 不会被 tracking,其他只读命令都会被 tracking。OPTIN 和 OPTOUT 是针对非 BCAST 模式,也就是只有发送了某个 key 的只读命令后,才会追踪相应的 key。而 BCAST 模式是无论你是否发送某个 key 的只读命令,只有 Redis 修改了 key,都会发送相应的 key 的失效消息(前缀匹配的)。

5、NOLOOP 选项

默认情况下,失效消息会发送给所有需要的 Redis 客户端,但是有些情况下触发失效消息也就是更新 key 的客户端不需要收到该消息。设置 NOLOOP,可以避免这种情况,更新 Key 的客户端将不再收到消息,该选项在普通模式和广播模式下都适用。

6、最大 tracking 上限 trackingtablemax_keys

由上文可以知道,普通模式下需要存储大量的被 tracking 的 key 和客户端信息(具体存储的数据下文中会讲解),所以当 10k 客户端使用该模式处理百万个键时,会消耗大量的内存空间,所以 Redis 引入了 trackingtablemax_keys 配置,默认为无,不限制。当有一个新的键被 tracking 时,如果当前 tracking 的 key 的数量大于 trackingtablemax_keys,则会随机删除之前 tracking 的 key,并且向对应的客户端发送失效消息。

原理和源码实现

1、普通模式原理

我们也先讲解普通模式的原理,Redis 服务端使用 TrackingTable 存储普通模式的客户端数据,它的数据类型是基数树(radix tree)。基数树是针对稀疏的长整型数据查找的多叉搜索树,能快速且节省空间的完映射,一般用于解决 Hash冲突和 Hash表大小的设计问题,Linux 的内存管理就使用了它。Redis 用它存储键的指针客户端 ID 的映射关系。因为键对象的指针就是内存地址,也就是长整型数据。客户端缓存的相关操作就是对该数据的增删改查:

  • 当开启 track 功能的客户端获取某一个键值时,Redis 会调用 enableTracking 方法使用基数树记录下该 key 和 clientId 的映射关系。

  • 当某一个 key 被修改或删除时,Redis 会调用 trackingInvalidateKey 方法根据 key 从 TrackingTable 中查找所有对应的客户端ID,然后调用 sendTrackingMessage 方法发送失效消息给这些客户端(会检查 CLIENT_TRACKING 相关标志位是否开启和是否开启了 NOLOOP)。

  • 发送完失效消息后,根据键的指针值将映射关系从 TrackingTable中删除。

  • 客户端关闭 track 功能后,因为删除需要进行大量操作,所以 Redis 使用懒删除方式,只是将该客户端的 CLIENT_TRACKING 相关标志位删除掉。

2、广播模式原理

广播模式与普通模式类似,Redis 同样使用 PrefixTable存储广播模式下的客户端数据,它存储前缀字符串指针和(需要通知的key和客户端ID)的映射关系。它和广播模式最大的区别就是真正发送失效消息的时机不同:

  • 当客户端开启广播模式时,会在 PrefixTable的前缀对应的客户端列表中加入该客户端ID。

  • 当某一个 key 被修改或删除时,Redis 会调用 trackingInvalidateKey 方法, trackingInvalidateKey 方法中如果发现 PrefixTable 不为空,则调用 trackingRememberKeyToBroadcast 依次遍历所有前缀,如果key 符合前缀规则,则记录到 PrefixTable 对应的位置。

  • 在 Redis 的事件处理周期函数 beforeSleep 函数里会调用 trackingBroadcastInvalidationMessages 函数来真正发送消息。

3、处理最大 tracking 上限

Redis 会在每次执行过命令后(processCommand方法)调用 trackingLimitUsedSlots来判断是否需要进行清理:

  • 判断 TrackingTable 中键的数量是否大于 trackingtablemax_keys;

  • 在一定时间段内(不能太长,阻塞主流程),随机从 TrackingTable 中选出一个键删除,直到数量小于或者时间用完为止。

4、具体源码

关于源码,在 tracking.c 文件下,我们这里只看一下最为关键的 trackingInvalidateKey函数和 sendTrackingMessage函数,理解了这两个函数,广播模式和处理最大 tracking 上限等相关函数都与之类似。

void trackingInvalidateKey(client *c, robj *keyobj) {

if(TrackingTable== NULL) return;

sds sdskey = keyobj->ptr;

// 省略,如果广播模式的记录基数树不为空,则先处理广播模式

// 1 根据键的指针去 TrackingTable 查找

rax *ids = raxFind(TrackingTable,(unsignedchar*)sdskey,sdslen(sdskey));

if(ids == raxNotFound) return;

// 2 使用迭代器遍历

raxIterator ri;raxStart(&ri,ids);raxSeek(&ri,"^",NULL,0);

while(raxNext(&ri)) {

// 3 根据 clientId 查找 client 实例

client *target = lookupClientByID(id);

// 4 如果未开启 track 或者是广播模式则跳过。

if(target == NULL ||

!(target->flags & CLIENT_TRACKING)||

target->flags & CLIENT_TRACKING_BCAST)

{ continue; }

// 5 如果开启了 NOLOOP 并且是导致key发生变化的client则跳过。

if(target->flags & CLIENT_TRACKING_NOLOOP &&

target == c)

{ continue; }

// 6 发送失效消息

sendTrackingMessage(target,sdskey,sdslen(sdskey),0);

}

// 7 减少数据统计,根据sdskey删除对应的记录

TrackingTableTotalItems-= raxSize(ids);

raxFree(ids);

raxRemove(TrackingTable,(unsignedchar*)sdskey,sdslen(sdskey),NULL);

}

源码如上所示,trackingInvalidateKey 方法主要做了 7 件事情:

  • 根据键的指针去 TrackingTable 查找客户端ID列表;

  • 使用迭代器遍历列表;

  • 根据 clientId 查找 client 实例;

  • 如果 client 实例未开启 track 或者是广播模式则跳过;

  • 如果 client 实例开启了 NOLOOP 并且是导致key发生变化的client则跳过;

  • 调用 sendTrackingMessage 方法发送失效消息;

  • 减少数据统计,根据sdskey删除对应的记录

下面来看真正发送消息的 sendTrackingMessage函数,它主要做了6件事:

  • 如果 clienttrackingredirection 不为空,则开启了转发模式;

  • 找到转发的客户端实例;

  • 如果转发客户端关闭了,则必须通知原客户端;

  • 如果是客户端使用 RESP3 则发 PUSH 消息;

  • 如果是转发模式,往 TrackingChannelName 也就是 _redis_:invalidate 信道中发送失效消息的头部信息;

  • 发送键等信息。

void sendTrackingMessage(client *c, char*keyname, size_t keylen, int proto) {

int using_redirection = 0;

// 1 如果 client_tracking_redirection 不为空,则开启了转发模式

if(c->client_tracking_redirection) {

// 2 找到转发的客户端实例

client *redir = lookupClientByID(c->client_tracking_redirection);

if(!redir) {

// 3 如果转发客户端关闭了,则必须通知原客户端

....

return;

}

c = redir;

using_redirection = 1;

}

if(c->resp > 2) {

// 4 如果是 RESP3 则发PUSH

addReplyPushLen(c,2);

addReplyBulkCBuffer(c,"invalidate",10);

} elseif(using_redirection && c->flags & CLIENT_PUBSUB) {

// 5 转发模式,往 TrackingChannelName 信道中发送消息

addReplyPubsubMessage(c,TrackingChannelName,NULL);

} else{

return;

}

// 6 发送键等信息,和上边4,5操作连在一起的。

addReplyProto(c,keyname,keylen);

}

欢迎点赞评论,后续还会学习其他 Redis 6.0.0 的其他亮点功能,欢迎继续关注~

添加小助手:vipcsdn

回复关键词:遇见OFFER快速入群!

推荐阅读

  • 手把手教你配置VS Code 远程开发工具,工作效率提升N倍

  • 用大白话彻底搞懂HBase RowKey详细设计

  • 后端程序员必备:书写高质量SQL的30条建议

  • Go 远超 Python,机器学习人才极度稀缺,全球 16,655 位程序员告诉你这些真相!

  • 任正非谈“狼文化”:华为没有 996,更没有 007

  • 区块链必读“上链”哲学:“胖链下”与“瘦链上”

  • 在商业中,如何与人工智能建立共生关系?

真香,朕在看了!

linux安装telnet客户端_Redis 6.0 的客户端缓存是怎么肥事?一文带你了解!相关推荐

  1. Linux安装redis最新版5.0.8

    详解Linux安装redis最新版5.0.8 如果是在校大学生,可以到阿里云网站去免费领取一个半年的阿里云服务器,不知道现在还可以免费领取不. 也可以安装虚拟机,在虚拟机上安装redis. 在wind ...

  2. linux安装telnet服务

    文章目录 前言 一.telnet是什么? 二.使用步骤 1.安装telent 2.重新启动守护进程 3.测试 总结 前言 最新公司需要迁移项目需要用到telnet命令,趁此机会做个总结归纳 一.tel ...

  3. openssh离线升级到最新版本【openssh8.0和8.6说明】方法-详细说明、linux安装telnet服务和报错Login incorrect,登陆慢说明

    文章目录 说明 配置本地源 配置telnet[可以不做] 说明 允许root登陆 终端登陆配置 Login incorrect报错说明 登陆慢原因 登陆很慢 服务安装 telnet登录虚拟机 安装依赖 ...

  4. CentOS Linux安装telnet服务

    Telnet服务的配置步骤如下: 一.安装telnet-server软件包 检测系统是否已经安装telnet服务:[root@root]#rpm -qa telnet-server 若无输入内容,则表 ...

  5. Linux安装telnet软件包,Linux下安装telnet

    Linux 环境下未安装telnet,会出现以下错误:[root@RedHat71 ~]# telnet 129.223.248.159 bash: telnet: command not found ...

  6. Linux 安装telnet

    一.安装telnet 1.首先我们检测telnet-server的rpm包是否安装 [root@localhost ~]# rpm -qa telnet-server 若无输入内容,则表示没有安装. ...

  7. Linux安装telnet

    一.安装telnet 1.检测telnet-server的rpm包是否安装 [root@localhost ~]# rpm -qa telnet-server 若输出如下信息则表示已安装telnet- ...

  8. linux安装telnet组件,LINUX下如何安装telnet

    1.插入安装盘 2.进如/Package目录 3.安装telnet 客户端 查看客户端有没安装 rpm -q telnet 安装客户端 rpm -ivh telnet-0.17-39.el6.x86_ ...

  9. linux找不到telnet命令,Linux安装telnet

    一.安装telnet 1.检测telnet-server的rpm包是否安装 [root@localhost ~]# rpm -qa telnet-server 若无输入内容,则表示没有安装.出于安全考 ...

最新文章

  1. JSP的三六九四七(三大指令、六大标签、九大内置对象、四大作用域、七个动作指令)
  2. python selenium基本用法
  3. eclipse-注释
  4. 横屏模式(landscape)下的UIDatePicker
  5. 8. JavaScript 全局对象
  6. java变量练习_Java变量与运算符练习
  7. 全网首发:以字型为例,以bit表示的二维数组矩阵,旋转90、-90
  8. 用友t3 服务器通讯协议,远程登陆用友t3服务器
  9. 【手册】Linux User's Manual
  10. 樊登读书搞定读后感_樊登读书法的读后感。
  11. 如何在远程桌面无响应的情况下完成远程电脑重启
  12. primordials is not defined错误,正确的解决方案 亲测 完美 好用
  13. 什么是无监督学习?概念、使用场景及常用算法详解
  14. 真杜比全景声家庭影院级投影设备,当贝做到了五千元内也支持
  15. 语音识别-特征提取 (一)
  16. DE2-115 SDRAM地址问题
  17. 计算机网络(6)体系结构:计算机网络协议、接口、服务的概念
  18. Libgdx介绍及环境安装
  19. ESP32 直接使用WIFI进行UDP通信, 将光敏电阻传感器数值, 显示在OLED上 - 米思齐/Arduino
  20. 电脑和手机实现网页互相切换

热门文章

  1. ios端 ajax url的一个小问题
  2. 企业签名打包错误+[MICodeSigningVerifier _validateSignatureAndCopyInfoForURL:withOptions:error:]:...
  3. HDU2833 最短路 floyd
  4. 《Android开发艺术探索》读书笔记 (7) 第7章 Android动画深入分析
  5. 合理设置域名TTL值给网站加速
  6. 微软Windows2003的正版安装序列号
  7. 【个人笔记】OpenCV4 C++ 快速入门 10课
  8. java serlet清空cookie_如何删除Java Servlet中的Cookie
  9. 跨平台音频编辑器ocenaudio(十七)
  10. Prolog 语言入门(一)