近日 Redis 6.0.0 GA 版本发布,这是 Redis 历史上最大的一次版本更新,包括了客户端缓存 (Client side caching)、ACL、Threaded I/O 和 Redis Cluster Proxy 等诸多更新。html
咱们今天就依次聊一下客户端缓存的必要性、具体使用、原理分析和实现。redis
咱们都知道,使用 Redis 进行数据的缓存的主要目的是减小对 MySQL 等数据库的访问,提供更快的访问速度,毕竟 《Redis in Action》中提到的, Redis 的性能大体是普通关系型数据库的 10 ~ 100 倍。数据库
因此,以下图所示,Redis 用来存储热点数据,Redis 未命中,再去访问数据库,这样能够应付大多数状况下的性能要求。centos
可是,Redis 也有其性能上限,而且访问 Redis 必然有必定的网络 I/O 以及序列化反序列化损耗。因此,每每会引入进程缓存,将最热的数据存储在本地,进一步加快访问速度。缓存
如上图所示,Guava Cache 等进程缓存做为一级缓存,Redis 做为二级缓存:bash
只使用 Redis 分布式缓存时,遇到数据更新时,应用程序更新完 MySQL 中的数据,能够直接将 Redis 中对应缓存失效掉,保持数据的一致性。服务器
而进程内缓存的数据一致性比分布式的缓存面临更大的挑战。数据更新的时候,如何通知其余进程也更新本身的缓存呢?网络
若是按照分布式缓存的思路,咱们能够设置极短的缓存失效时间,这样没必要实现复杂的通知机制。分布式
可是不一样进程内的数据依然会面临不一致的问题,而且不一样进程缓存失效时间不统一,同一个请求到了不一样的进程,可能出现反复幻读的状况。
ide
Ben 在 RedisConf18 给出了一个方案(视频和 PPT 连接在文末),经过 Redis 的 Pub/Sub,能够通知其余进程缓存对此缓存进行删除。若是 Redis 挂了或者订阅机制不靠谱,依靠超时设定,依然能够作兜底处理。
Antirez(Redis 的做者) 也正是听取 Ben 这个方案后,才决定在 Redis Server 支持客户端缓存的,由于在有服务端参与的状况下能够更好的处理上述这些问题。
下面使用 Docker 安装 Redis 6.0.1,而后使用 telnet 来简单演示一下 Redis 6.0 的客户端缓存功能。全部相关的功能以下图所示,分别是使用RESP3 协议版本的普通模式和广播模式,以及使用 RESP2 协议版本的转发模式。咱们先来看普通模式。
先使用 redis-cli 设置缓存值 test=111,使用 telnet 链接上 Redis,而后发送 hello 3 开启 RESP3 协议。
[root@VM_0_3_centos ~]# telnet 127.0.0.1 6379 Trying 127.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
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 资源,因此注意别使用过长的前缀。
上述操做时客户端都须要先开启 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 服务端代替业务系统发送消息通知。
使用 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 的失效消息(前缀匹配的)。
默认状况下,失效消息会发送给全部须要的 Redis 客户端,可是有些状况下触发失效消息也就是更新 key 的客户端不须要收到该消息。
设置 NOLOOP,能够避免这种状况,更新 Key 的客户端将再也不收到消息,该选项在普通模式和广播模式下都适用。
由上文能够知道,普通模式下须要存储大量的被 tracking 的 key 和客户端信息(具体存储的数据下文中会讲解),因此当 10k 客户端使用该模式处理百万个键时,会消耗大量的内存空间,因此 Redis 引入了 tracking_table_max_keys 配置,默认为无,不限制。
当有一个新的键被 tracking 时,若是当前 tracking 的 key 的数量大于 tracking_table_max_keys,则会随机删除以前 tracking 的 key,而且向对应的客户端发送失效消息。
咱们也先讲解普通模式的原理,Redis 服务端使用 TrackingTable 存储普通模式的客户端数据,它的数据类型是基数树(radix tree)。
基数树是针对稀疏的长整型数据查找的多叉搜索树,能快速且节省空间的完映射,通常用于解决 Hash冲突和 Hash表大小的设计问题,Linux 的内存管理就使用了它。
Redis 用它存储键的指针和客户端 ID 的映射关系。由于键对象的指针就是内存地址,也就是长整型数据。客户端缓存的相关操做就是对该数据的增删改查:
enableTracking
方法使用基数树记录下该 key 和 clientId 的映射关系。trackingInvalidateKey
方法根据 key 从 TrackingTable 中查找全部对应的客户端ID,而后调用 sendTrackingMessage
方法发送失效消息给这些客户端(会检查 CLIENT_TRACKING 相关标志位是否开启和是否开启了 NOLOOP)。广播模式与普通模式相似,Redis 一样使用 PrefixTable
存储广播模式下的客户端数据,它存储前缀字符串指针和(须要通知的key和客户端ID)的映射关系。它和广播模式最大的区别就是真正发送失效消息的时机不一样:
PrefixTable
的前缀对应的客户端列表中加入该客户端ID。trackingInvalidateKey
方法,trackingInvalidateKey
方法中若是发现 PrefixTable
不为空,则调用 trackingRememberKeyToBroadcast
依次遍历全部前缀,若是key 符合前缀规则,则记录到 PrefixTable
对应的位置。trackingBroadcastInvalidationMessages
函数来真正发送消息。Redis 会在每次执行过命令后(processCommand方法)调用 trackingLimitUsedSlots
来判断是否须要进行清理:
关于源码,在 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,(unsigned char*)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,(unsigned char*)sdskey,sdslen(sdskey),NULL); }
源码如上所示,trackingInvalidateKey 方法主要作了 7 件事情:
下面来看真正发送消息的 sendTrackingMessage
函数,它主要作了6件事:
_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); } else if (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 的其余亮点功能,请你们继续关注。