【Redis 原理】通信协议 && 内存回收

打印 上一主题 下一主题

主题 830|帖子 830|积分 2490

通信协议–RESP

Redis是一个CS架构的软件,通信一般分两步(不包罗pipeline和PubSub):

  • 客户端(client)向服务端(server)发送一条下令
  • 服务端解析并执行下令,返反响应结果给客户端
因此客户端发送下令的格式、服务端响应结果的格式必须有一个规范,这个规范就是通信协议。
而在Redis中接纳的是RESP(Redis Serialization Protocol)协议:


  • Redis 1.2版本引入了RESP协议
  • Redis 2.0版本中成为与Redis服务端通信的标准,称为RESP2
  • Redis 6.0版本中,从RESP2升级到了RESP3协议,增加了更多数据类型而且支持6.0的新特性–客户端缓存
但如今,默认使用的依然是RESP2协议,也是我们要学习的协议版本(以下简称RESP)
在RESP中,通过首字节的字符来区分差异数据类型,常用的数据类型包罗5种:

  • 单行字符串:首字节是 + ,后面跟上单行字符串,以CRLF( "\r\n" )结尾。比方返回OK: +OK\r\n
    由此可见:单行字符串中不能包含一些特殊字符(如\r\n等),是非二进制安全的
  • 错误(Errors):首字节是 -,与单行字符串格式一样,只是字符串是异常信息,比方:-Error message\r\n
  • 数值:首字节是 :,后面跟上数字格式的字符串,以CRLF结尾。比方::10\r\n
  • 多行字符串:首字节是 $,表示二进制安全的字符串,最大支持512MB:
    如果大小为0,则代表空字符串:$0\r\n\r\n
    如果大小为-1,则代表不存在:$-1\r\n
  • 数组:首字节是 *,后面跟上数组元素个数,再跟上元素,元素数据类型不限
内存回收

Redis之以是性能强,最紧张的原因就是基于内存存储
然而单节点的Redis其内存大小不宜过大,会影响持久化或主从同步性能。
我们可以通过修改配置文件来设置Redis的最大内存:
  1. # 格式:
  2. # maxmemory <bytes>
  3. # 例如:
  4. maxmemory 1gb
复制代码
当内存使用到达上限时,就无法存储更多数据了。
为了解决这个问题,Redis提供了一些策略实现内存回收:
内存逾期策略 && 内存淘汰策略
内存逾期策略

Redis可以通过expire下令给Redis的key设置TTL(存活时间):

可以发现,当key的TTL到期以后,再次访问name返回的是nil,说明这个key已经不存在了,对应的内存也得到释放。从而起到内存回收的目标。
但是这背后是怎么实现的?让我们从以下两个问题入手:
Q1:Redis是如何知道一个key是否逾期呢?
Redis自己是一个典范的key-value内存存储数据库,因此全部的key、value都生存在之前学习过的Dict结构中。
而且在其database结构体中,有两个关键Dict:一个用来记载key-value;另一个用来记载key-TTL,界说如下:
  1. typedef struct redisDb {
  2.     dict *dict;                 /* 存放所有key及value的地方,也被称为keyspace*/
  3.     dict *expires;              /* 存放每一个key及其对应的TTL存活时间,只包含设置了TTL的key*/
  4.     dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/
  5.     dict *ready_keys;           /* Blocked keys that received a PUSH */
  6.     dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
  7.     int id;                     /* Database ID,0~15 */
  8.     long long avg_ttl;          /* 记录平均TTL时长 */
  9.     unsigned long expires_cursor; /* expire检查时在dict中抽样的索引位置. */
  10.     list *defrag_later;         /* 等待碎片整理的key列表. */
  11. } redisDb;
复制代码
因此到这里我们可以解答Q1:使用两个Dict分别记载key-value对及key-ttl键值对,如许就可以查询一个key是否逾期
Q2:是不是TTL到期就立即删除了呢?
NO,TTL到期的key并不是立即删除,而是接纳如下策略:
惰性删除 && 周期删除
惰性删除

顾明思议并不是在TTL到期后就立刻删除,而是在访问一个key的时间,查抄该key的存活时间,如果已经逾期才执行删除
核心代码片段如下:
  1. // 查找一个key执行写操作
  2. robj *lookupKeyWriteWithFlags(redisDb *db, robj *key, int flags) {
  3.     // 检查key是否过期
  4.     expireIfNeeded(db,key);
  5.     return lookupKey(db,key,flags);
  6. }
  7. // 查找一个key执行读操作
  8. robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
  9.     robj *val;
  10.     // 检查key是否过期   
  11.     if (expireIfNeeded(db,key) == 1) {
  12.         // ...略
  13.     }
  14.     return NULL;
  15. }
  16. int expireIfNeeded(redisDb *db, robj *key) {
  17.     // 判断是否过期,如果未过期直接结束并返回0
  18.     if (!keyIsExpired(db,key)) return 0;
  19.     // ... 略
  20.     // 删除过期key
  21.     deleteExpiredKeyAndPropagate(db,key);
  22.     return 1;
  23. }
复制代码
可以看到岂论是查找一个key做读还是写操作,都会调用expireIfNeeded函数来判断当前key是否逾期,如果逾期就会删除该key
那么这就有一个问题:如果一个key我们设置了TTL,而且它已经逾期了,但是一直没有人去访问这个key,如果redis仅仅靠惰性删除的话,显然这个本该删除的key就永久不可能删除了。这显然是不公道的。因此有了下面的周期删除。
周期删除

通过一个定时任务,周期性的抽样部门逾期的key,然后执行删除。执行周期有两种:

  • Redis服务初始化函数initServer()中设置定时任务,按照server.hz的频率来执行逾期key整理,模式为SLOW,核心代码如下:
  1. // server.c
  2. void initServer(void){
  3.     // ...
  4.     // 创建定时器,关联回调函数serverCron,处理周期取决于server.hz,默认10
  5.     aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL)
  6. }
  7. // server.c
  8. int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
  9.     // 更新lruclock到当前时间,为后期的LRU和LFU做准备
  10.     unsigned int lruclock = getLRUClock();
  11.     atomicSet(server.lruclock,lruclock);
  12.     // 执行database的数据清理,例如过期key处理
  13.     databasesCron();
  14. }
  15. void databasesCron(void) {
  16.     // 尝试清理部分过期key,清理模式默认为SLOW
  17.     activeExpireCycle(
  18.           ACTIVE_EXPIRE_CYCLE_SLOW);
  19. }
复制代码

  • Redis的每个事件循环前会调用beforeSleep()函数,执行逾期key整理,模式为FAST
  1. void beforeSleep(struct aeEventLoop *eventLoop){
  2.     // ...
  3.     // 尝试清理部分过期key,清理模式默认为FAST
  4.     activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST);
  5. }
复制代码
SLOW模式规则:

  • 执行频率受server.hz影响,默认为10,即每秒执行10次,每个执行周期100ms。
  • 执行整理耗时不凌驾一次执行周期的25%.默认slow模式耗时不凌驾25ms
  • 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否逾期
  • 如果没到达时间上限(25ms)而且逾期key比例大于10%,再进行一次抽样,否则竣事
FAST模式规则(逾期key比例小于10%不执行 ):

  • 执行频率受beforeSleep()调用频率影响,但两次FAST模式间隔不低于2ms
  • 执行整理耗时不凌驾1ms
  • 逐个遍历db,逐个遍历db中的bucket,抽取20个key判断是否逾期
  • 如果没到达时间上限(1ms)而且逾期key比例大于10%,再进行一次抽样,否则竣事
内存淘汰策略

当Redis内存使用到达设置的上限时,自动挑选部门key删除以释放更多内存的流程
Redis会在处理客户端下令的方法processCommand()中实验做内存淘汰:
  1. int processCommand(client *c) {
  2.     // 如果服务器设置了server.maxmemory属性,并且并未有执行lua脚本
  3.     if (server.maxmemory && !server.lua_timedout) {
  4.         // 尝试进行内存淘汰performEvictions
  5.         int out_of_memory = (performEvictions() == EVICT_FAIL);
  6.         // ...
  7.         if (out_of_memory && reject_cmd_on_oom) {
  8.             rejectCommand(c, shared.oomerr);
  9.             return C_OK;
  10.         }
  11.         // ....
  12.     }
  13. }
复制代码
Redis支持8种差异策略来选择要删除的key:

  • noeviction: 不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略
  • volatile-ttl: 对设置了TTL的key,比力key的剩余TTL值,TTL越小越先被淘汰
  • allkeys-random:对全体key随机进行淘汰。也就是直接从db->dict中随机挑选
  • volatile-random:对设置了TTL的key随机进行淘汰。也就是从db->expires中随机挑选
  • allkeys-lru: 对全体key,基于LRU算法进行淘汰
  • volatile-lru: 对设置了TTL的key,基于LRU算法进行淘汰
  • allkeys-lfu: 对全体key,基于LFU算法进行淘汰
  • volatile-lfu: 对设置了TTL的key,基于LFU算法进行淘汰
比力容易混淆的有两个:
LRU(Least Recently Used),最少最近使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高
LFU(Least Frequently Used),最少频率使用。会统计每个key的访问频率值越小淘汰优先级越高
对于上面的8种策略,可以通过如下字段设置详细的策略:

我们偏重了解一下LRU和LFU的实现方式
我们都知道,Redis的数据都会被封装为RedisObject结构:
  1. typedef struct redisObject {
  2.     unsigned type:4;        // 对象类型
  3.     unsigned encoding:4;    // 编码方式
  4.     unsigned lru:LRU_BITS;  // LRU:以秒为单位记录最近一次访问时间,长度24bit
  5.                           // LFU:高16位以分钟为单位记录最近一次访问时间,低8位记录逻辑访问次数
  6.     int refcount;           // 引用计数,计数为0则可以回收
  7.     void *ptr;              // 数据指针,指向真实数据
  8. } robj;
复制代码
此中 unsigned lruRU_BITS; 根据maxmemory-policy可以设置详细的策略:

  • maxmemory-policy allkeys-lru 或maxmemory-policy volatile-lru
    此时obj种的lru以秒为单位记载最近一次访问时间,长度24bit
  • maxmemory-policy allkeys-lfu 或 maxmemory-policy volatile-lfu
    此时obj种的lru的高16位以分钟为单位记载最近一次访问时间,低8位记载逻辑访问次数
    问:作甚逻辑访问次数?
    答:LFU的访问次数之以是叫做逻辑访问次数,是因为并不是每次key被访问都计数,而是通过以下运算方式:
    ①生成0~1之间的随机数R
    ②计算 (旧次数 * lfu_log_factor + 1),记载为P
    ③如果 R < P ,则计数器 + 1,且最大不凌驾255
    ④访问次数会随时间衰减,距离上一次访问时间每隔 lfu_decay_time 分钟(默认是1),计数器 -1
详细的淘汰策略如下图所示:


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

雁过留声

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表