当前位置: 首页 > 科技观察

Redis6.0更新放大:如何用好客户端缓存

时间:2023-03-12 07:40:25 科技观察

Redis6.0.0GA版于近日发布,这是Redis史上最大的一次版本更新,包括客户端缓存(Clientsidecaching)、ACL、ThreadedI/O和RedisClusterProxy以及许多其他更新。今天我们就来依次谈谈客户端缓存的必要性、具体使用、原理分析和实现。我们都知道为什么需要客户端缓存。使用Redis做数据缓存的主要目的是减少对MySQL等数据库的访问,提供更快的访问速度。毕竟在《Redis in Action》中提到,Redis的性能大约是普通关系型数据库的10~100倍。因此,如下图所示,使用Redis存储热点数据,Redis未命中,再访问数据库,可以满足大部分情况下的性能需求。但是Redis也有它的性能限制,访问Redis必然有一定的网络I/O和序列化反序列化损失。因此,往往会引入进程缓存,将最热的数据存储在本地,进一步加快访问速度。如上图(示意图,不用太在意细节,下同),GuavaCache等进程缓存作为一级缓存,Redis作为二级缓存:先去GuavaCache查询数据,命中直接返回。如果GuavaCache中有未命中,则去Redis中查询,如果命中则返回数据,并将此数据设置到GuavaCache中。如果Redis也miss,只能在MySQL中查询,然后依次将数据设置到Redis和GuavaCache中。当只使用Redis分布式缓存时,当数据更新时,应用程序可以在更新MySQL中的数据后,直接将Redis中对应的缓存作废,以保持数据的一致性。进程内缓存的数据一致性比分布式缓存面临更大的挑战。当数据更新时,如何通知其他进程更新自己的缓存?如果按照分布式缓存的思路,我们可以设置一个很短的缓存过期时间,这样就不需要实现复杂的通知机制了。但是,不同进程中的数据仍然面临不一致的问题,不同进程的缓存过期时间也不统一。如果同一个请求在不同的进程中到达,可能会出现重复的幻读。Ben在RedisConf18上给出了一个解决方案(视频和PPT链接在文末),通过Redis的Pub/Sub,可以通知其他进程缓存删除这个缓存。如果Redis挂了或者订阅机制不可靠,根据超时设置,还是可以做底线的。Antirez(Redis的作者)在听了Ben的提议后决定在RedisServer中支持客户端缓存,因为有服务端的参与可以更好地处理上述问题。功能介绍及演示下面我们将使用Docker安装Redis6.0.1,然后使用telnet简单演示一下Redis6.0的客户端缓存功能。所有相关功能如下图所示,分别是使用RESP3协议版本的普通模式和广播模式,以及使用RESP2协议版本的转发模式。我们先来看普通模式。普通模式下,先使用redis-cli设置缓存值test=111,使用telnet连接Redis,然后发送hello3启动RESP3协议。[root@VM_0_3_centos~]#telnet127.0.0.16379Trying127.0.0.1...Connectedto127.0.0.1.Escapecharacteris'^]'.hello3//telnet输出结果格式化规范如下,否则换行太多并且它是RESP3格式,不需要格式知识。>HELLO31#"server"=>"redis"2#"version"=>"6.0.1"3#"proto"=>(整数)34#"id"=>(整数)105#"mode"=>"standalone"6#"role"=>"master"7#"modules"=>(emptyarray)这里需要注意的是,Redis服务端只会跟踪客户端获取的只读命令的键值连接生命周期。Redis客户端默认不开启track模式。需要使用命令开启,然后必须先获取test的值,这样Redis服务器才会记录下来。clienttrackingon+OKgettest$3111Redis服务器会在密钥因过期时间和最大内存策略而被修改或逐出时通知这些客户端。我们这里简单更新test的值,telnet会收到如下通知:>2//RESP3中PUSH类型,标记为>symbol$10invalidate*1$4test如果再次更新test的值,telnet就不会了再次收到无效消息。除非telnet再执行一次get操作,否则相应的key值会再次被跟踪。也就是说,Redis服务器记录的clienttrack信息只生效一次,发送失效消息后会被删除。只有当客户端再次执行只读命令并再次被跟踪时,才会发送下一条消息通知。取消跟踪的命令如下:clienttrackingoff+OK广播模式Redis还提供了一种广播模式(BCAST),这是客户端缓存的另一种实现方式。这样,Redis服务器不再消耗过多的内存来存储信息,而是向客户端发送更多的失效消息。这是服务器存储过多数据、消耗内存与客户端接收过多消息、消耗网络带宽之间的权衡。//hello3上已经启用了RESP3协议,否则收不到失效消息,同clienttrackingonbcast+OK//此时设置key为a的key值,收到如下消息。>2$10invalidate*1$1a如果不想收到所有键值的失效消息,可以限制键的前缀。以下命令表示只关注前缀为test的键值消息。一般来说,业务的缓存key根据业务有统一的前缀,所以这个特性很方便。clienttrackingonbcastprefixtest不同于普通模式下必须获取一次key的规则。在广播模式下,只要修改或删除key,符合规则的客户端就会收到一条失效消息,可以多次获取。与普通模式相比,虽然存储的数据较少,但由于需要匹配前缀规则,会消耗一定的CPU资源,所以注意不要使用太长的前缀。转发方式对于以上操作,客户端需要先开启RESP3。Redis为了兼容RESP2协议,提供了转发(Redirect)模式。不是使用RESP3原生支持PUSH消息,而是通过Pub/Sub将消息通知给另一个客户端。具体过程如下图所示:这里需要两个telnet,其中一个telnet需要订阅_redis_:invalidate频道。然后另一个telnet启动Redirect模式,通过订阅通道约定将失效报文发送给第一个telnet。#telentBclientid:368subscribe_redis_:invalidate#telnetA,打开track指定转发到Bclienttrackingonbcastredirect368#telentB,此时key值被修改,收到来自__redis__:invalidate的消息message$20__redis__:invalidate*1$1achannel,你会发现转发的方式和文章开头提到的多级缓存中的更新机制很相似,只不过那个方案中,业务系统修改key后发送消息通知,但是这里Redis服务器代替业务系统发送消息通知。OPTIN和OPTOUT选项使用OPTIN有选择地启用跟踪。只有你发送clientcachingyes后的下一个只读命令的key(Redis文档中的CACHING命令,实验中发现无效)才会被追踪,否则其他只读命令的key会被追踪不被追踪。clienttrackingonoptinclientcachingyesgetagetb//此时修改a和b的值,发现只收到a>2$10invalidate*1$1a的失效报文,而OPTOUT参数则相反,可以选择退出跟踪.关闭clientcachingoff后的下一个只读命令的key不会被跟踪,其他的只读命令会被跟踪。OPTIN和OPTOUT是针对非BCAST模式的,即只有发送了一个key的只读命令后,才会跟踪对应的key。在BCAST模式下,无论是否对某个key发送只读命令,只有Redis修改了key,才会发送对应key(前缀匹配)的失效消息。NOLOOP选项默认情况下,失效消息会发送给所有需要的Redis客户端,但在某些情况下会触发失效消息,即更新key的客户端不需要接收消息。设置NOLOOP可以避免这种情况,更新Key的客户端将不再接收消息。此选项适用于普通模式和广播模式。trackingtablemax_keys最大跟踪上限trackingtablemax_keys。从上面可以看出,在普通模式下需要存储大量的被跟踪的key和client信息(具体存储的数据下面会说明),所以当10k的client使用这种模式处理百万级的key时,会消耗大量的内存空间,所以Redis引入了trackingtablemax_keys配置,默认是none,没有限制。当一个新的key被跟踪时,如果当前被跟踪的key的个数大于trackingtablemax_keys,则之前被跟踪的key将被随机删除,并向相应的客户端发送失效消息。实现普通模式原理的原理和源码我们先来解释一下普通模式的原理。Redis服务器使用TrackingTable在普通模式下存储客户端数据。它的数据类型是基数树。基数树是一种用于稀疏长整数数据搜索的多叉搜索树。它可以快速完成映射并节省空间。一般用于解决Hash冲突和Hash表大小的设计问题。Linux内存管理使用它。Redis使用它来存储键指针和客户端ID之间的映射。因为key对象的指针是内存地址,也就是长整型数据。客户端缓存的相关操作是对数据进行增删改查:当开启track功能的客户端获取到某个key值时,Redis会调用enableTracking方法记录key与clientId的映射关系,使用基数树。当一个key被修改或删除时,Redis会调用trackingInvalidateKey方法根据key从TrackingTable中找到所有对应的clientID,然后调用sendTrackingMessage方法向这些client发送失效消息(检查CLIENT_TRACKING的相关flags是否为启用以及NOLOOP是否打开)。发送失效消息后,根据key的指针值从TrackingTable中删除映射关系。客户端关闭track功能后,由于删除需要大量操作,Redis采用惰性删除的方式,只删除客户端的CLIENT_TRACKING相关标志。广播模式的原理广播模式与普通模式类似。Redis也使用PrefixTable以广播方式存储客户端数据,它存储了前缀字符串指针与(要通知的key和客户端ID)的映射关系。它与广播模式最大的不同在于实际发送失效消息的时机不同:当客户端开启广播模式时,客户端ID会被添加到PrefixTable的前缀对应的客户端列表中。当一个key被修改或删除时,Redis会调用trackingInvalidateKey方法。如果在trackingInvalidateKey方法中发现PrefixTable不为空,就会调用trackingRememberKeyToBroadcast依次遍历所有前缀。如果key满足前缀规则,则记录到PrefixTable的相应位置。在Redis的事件处理循环函数beforeSleep函数中,会调用trackingBroadcastInvalidationMessages函数来真正发送消息。处理最大跟踪上限,Redis会在每条命令执行后调用trackingLimitUsedSlots(processCommand方法)判断是否需要清理:判断TrackingTable中的key个数是否大于trackingtablemax_keys;在一定时间内(不要太长,阻塞主进程),从TrackingTable中随机选择一个key删除,直到数量小于或时间用完。具体源码关于源码,在tracking.c文件下,我们这里只看最关键的trackingInvalidateKey函数和sendTrackingMessage函数。了解这两个函数后,广播模式、处理最大跟踪限制等相关函数与它们类似。voidtrackingInvalidateKey(client*c,robj*keyobj){if(TrackingTable==NULL)return;sdssdskey=keyobj->ptr;//省略,如果广播模式的记录基数树不为空,则处理广播模式首先//1根据key指针去TrackingTable中查找rax*ids=raxFind(TrackingTable,(unsignedchar*)sdskey,sdslen(sdskey));if(ids==raxNotFound)return;//2使用迭代器遍历raxIteratorri;raxStart(&ri,ids);raxSeek(&ri,"^",NULL,0);while(raxNext(&ri)){//3根据clientId查找client实例client*target=lookupClientByID(id);//4如果没有启用track或broadcast,则跳过模式。if(target==NULL||!(target->flags&CLIENT_TRACKING)||target->flags&CLIENT_TRACKING_BCAST){continue;}//5如果启用了NOLOOP并且是导致key改变的客户端则跳过。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件事:根据到关键指针找到客户端ID列表;用迭代器遍历列表;根据clientId查找客户端实例;如果客户端实例没有开启track或broadcast模式,则跳过;如果客户端实例启用了NOLOOP并且是导致密钥更改的客户端,则跳过它;调用sendTrackingMessage方法发送无效消息;reduce数据统计,根据sdskey删除对应的记录我们来看真正发送消息的sendTrackingMessage函数。主要做了六件事:如果clienttrackingredirection不为空,则开启转发模式;客户端实例;如果转发客户端关闭,则必须通知原客户端;如果客户端使用RESP3,会发送PUSH消息;如果是转发模式,将失效消息的头发送给TrackingChannelName,即_redis_:invalidatechannelInformation;发送密钥和其他信息。