Redis 2.8-4.0过期键优化过程全纪录

前言

之前 白馨(陌陌-技术保障部存储工程师 )在Redis技术交流群里,总结了一下Redis从2.8~4.0关于过期键相关的fix记录,非常有帮助,但有些东西未尽详细,本文将进行详细说明。

先从一个问题来看,运行环境如下:

Redis: 2.8.19
db0:keys=10000000,expires=10000000
主从结构

从下图中可以看到,在从节点get hello非空,在主节点get hello为空,之后从节点get hello为空,经排查主从同步offset基本正常,但出现了主从不一致。

原因先不说,本文来探讨下Redis2.8-4.0版本迭代中,针对过期键的fix,看看能不能找到答案。

一、过期功能回顾

当你执行了一条setex命令后,Redis会向内部的dict和expires哈希结构中分别插入数据:

dict------dict[key]:value
expires---expires[key]:timeout

例如:

127.0.0.1:6379> setex hello 120 world
OK
127.0.0.1:6379> info
# 该数据库中设置为过期键并且未被删除的总量(如果曾设置为过期键且删除则不计入)
db0:keys=1,expires=1,avg_ttl=41989
# 历史上每一次删除过期键就做一次加操作,记录删除过期键的总数。
expired_keys:0

二、Redis过期键的删除策略:

当键值过期后,Redis是如何处理呢?综合考虑Redis的单线程特性,有两种策略:惰性删除和定时删除。

1.惰性删除策略:

在每次执行key相关的命令时,都会先从expires中查找key是否过期,下面是3.0.7的源码(db.c):

下面是读写key相关的入口:

robj *lookupKeyRead(redisDb *db, robj *key) {
 robj *val;

 expireIfNeeded(db,key);
 val = lookupKey(db,key);
 ......
 return val;
}

robj *lookupKeyWrite(redisDb *db, robj *key) {
 expireIfNeeded(db,key);
 return lookupKey(db,key);
}

可以看到每次读写key前,所有的Redis命令在执行之前都会调用expireIfNeeded函数:

int expireIfNeeded(redisDb *db, robj *key) {
 mstime_t when = getExpire(db,key);
 mstime_t now;
 if (when < 0) return 0; /* No expire for this key */
 now = server.lua_caller ? server.lua_time_start : mstime();
 if (server.masterhost != NULL) return now > when;
 /* Return when this key has not expired */
 if (now <= when) return 0;
 /* Delete the key */
 server.stat_expiredkeys++;
 propagateExpire(db,key);
 notifyKeyspaceEvent(NOTIFY_EXPIRED,
  "expired",key,db->id);
 return dbDelete(db,key);
}

从代码可以看出,主从逻辑略有不同:

(1) 主库:过期则expireIfNeeded会删除过期键,删除成功返回1,否则返回0。

(2) 从库:expireIfNeeded不会删除key,而会返回一个逻辑删除的结果,过期返回1,不过期返回0 。

但是从库过期键删除由主库的synthesized DEL operations控制。

2.定时删除策略:

单单靠惰性删除,肯定不能删除所有的过期key,考虑到Redis的单线程特性,Redis使用了定期删除策略,采用策略是从一定数量的数据库的过期库中取出一定数量的随机键进行检查,不为空则删除。不保证实时删除。有兴趣的同学可以看看activeExpireCycle中具体实现,还是挺有意思的,下图是个示意图

if (server->masterhost == NULL) activeExpireCycle();

(1)主库: 会定时删除过期键。

(2)从库: 不执行定期删除。

综上所述:

主库:

(1) 在执行所有操作之前调用expireIfNeeded惰性删除。

(2) 定期执行调用一次activeExpireCycle,每次随机删除部分键(定时删除)。

从库:

过期键删除由主库的synthesized DEL operations控制。

三、过期读写问题

Redis过期删除策略带来的问题。我们只从用户操作的角度来讨论。

1、过期键读操作

下面是Redis 2.8~4.0过期键读操作的fix记录

(1) Redis2.8主从不一致

2.8中的读操作中都先调用lookupKeyRead函数:

robj *lookupKeyRead(redisDb *db, robs *key) {
  robj *val;
  expireIfNeeded(db,key);
  val = lookupKey(db,key);
  if (val == NULL)
    server.stat_keyspace_misses++;
  else
    server.stat_keyspace_hits++;
  return val;
}

•对于主库,执行expireIfNeeded时,过期会删除key。lookupKey返回 NULL。

•对于从库,执行expireIfNeeded时,过期不会删除key。lookupKey返回value。

所以对于过期键的读操作,主从返回就会存在不一致的情况,也就是开篇提到的问题。

(2) Redis 3.2主从除exists之外都一致

https://github.com/antirez/redis/commit/06e76bc3e22dd72a30a8a614d367246b03ff1312

3.2-rc1读操作中同样先调用了lookupKeyRead,实际上调用的是lookupKeyReadWithFlags函数:

robj *lookupKeyReadWithFlags(redisDb *db, robj *key) {
  robj *val;
  if (expireIfNeeded(db,key) == 1) {
    if (server.masterhost == NULL) return NULL;
    if (server.current_client && //当前客户端存在
      server.current_client != server.master && //当前客户端不是master请求建立的(用户请求的客户端)
      server.current_client->cmd &&
      server.current_client->cmd->flags & REDIS_CMD_READONLY) { //读命令
        return NULL;
       }
  val = lookupKey(db,key,flags);
  if (val == NULL)
    server.stat_keyspace_misses++;
  else
    server.stat_keyspace_hits++;
  return val;
  }

可以看到,相对于2.8,增加了对expireIfNeeded返回结果的判断:

•对于主库,执行expireIfNeeded时,过期会删除key,返回1。masterhost为空返回NULL。

•对于从库,执行expireIfNeeded时,过期不会删除key,返回1。满足当前客户端不为 master且为读命令时返回NULL。

除非程序异常。正常情况下对于过期键的读操作,主从返回一致。

(2) Redis 4.0.11解决exists不一致的情况

https://github.com/antirez/redis/commit/32a7a2c88a8b8cca8119b849eee7976b8ada8936

3.2并未解决exists这个命令的问题,虽然它也是个读操作。之后的4.0.11中问题才得以解决.

2、过期键写操作

在具体说这个问题之前,我们先说一下可写从库的使用场景。

(1).主从分离场景中,利用从库可写执行耗时操作提升性能。

作者在https://redis.io/topics/replication 中提到过:

For example computing slow Set or Sorted set operations and storing them into local keys is an use case for writable slaves that was observed multiple times.

在 https://github.com/antirez/redis/commit/c65dfb436e9a5a28573ec9e763901b2684eadfc4 举了一个更具体的例子:

For instance imagine having slaves replicating certain Sets keys from the master. When accessing the data on the slave, we want to peform intersections between
such Sets values. However we don't want to intersect each time: to cache the intersection for some time often is a good idea.

也就是说在读写分离的场景中,可以使用过期键的机制将从库作为一个缓存,去缓存从库上耗时操作的结果,提升整体性能。

(2). 迁移数据时,需要先将从库设置为可写。

比如下列场景:线上Redis服务正常,但可能遇到一些硬件的情况,需要对该机器上的Redis主从集群迁移。迁数据的方式就是搭建一个新的主从集群,让新主成为旧主的从。

进行如下操作:

•(1)主(旧主)从(新主)同步,rdb传输完毕90s之后,设置从库(新主)可写。

•(2)在主库(旧主)完全没有业务连接后,从库(新主)执行slaveof no one。

这种场景下,为了保证数据完全同步,并且尽量减少对业务的影响,就会先设置从库可写。

接着我们来做一个测试:

3.2版本主库执行的操作,主库的过期键正常过期。

3.2版本可写从库执行以下操作,从库的过期键并不会过期。

4.0rc3版本可写从库执行以下操作,从库的过期键却能够过期。

其实可写从库过期键问题包含两个问题:

•(1)从库中的过期键由主库同步过来的,过期操作由主库执行(未变更过)。

•(2)从库中的过期键的设置是从库上操作的。

redis4.0rc3之前,存在过期键泄露的问题。当expire直接在从库上操作,这个key是不会过期的。作者也在https://redis.io/topics/replication 提到过:

However note that writable slaves before version 4.0 were incapable of expiring keys with a time to live set. This means that if you use EXPIRE or other commands that set a maximum TTL for a key, the key will leak, and while you may no longer see it while accessing it with read commands, you will see it in the count of keys and it will still use memory. So in general mixing writable slaves (previous version 4.0) and keys with TTL is going to create issues.

过期键泄露问题在https://github.com/antirez/redis/commit/c65dfb436e9a5a28573ec9e763901b2684eadfc4中得到了解决。

四.总结

1、针对过期键读操作

(1) Redis2.8主从不一致

(2) Redis3.2-rc1主从除exists之外都一致: https://github.com/antirez/redis/commit/06e76bc3e22dd72a30a8a614d367246b03ff1312

(3) Redis4.0.11主从一致:

https://github.com/antirez/redis/commit/32a7a2c88a8b8cca8119b849eee7976b8ada8936

2、针对过期键的写操作:

Redis2.8~4.0都只返回物理结果。

3、从库中对key执行expire操作,key不会过期。

Redis4.0 rc3解决从库中设置的过期键不过期问题 https://github.com/antirez/redis/commit/c65dfb436e9a5a28573ec9e763901b2684eadfc4

4、如果slave非读写分离、上述迁移使用,基本本文问题不会出现。还有就是Redis 4非常靠谱,后面也会有文章介绍相关内容。(付磊)

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • Redis中主键失效的原理及实现机制剖析

    作为一种定期清理无效数据的重要机制,主键失效存在于大多数缓存系统中,Redis 也不例外.在 Redis 提供的诸多命令中,EXPIRE.EXPIREAT.PEXPIRE.PEXPIREAT 以及 SETEX 和 PSETEX 均可以用来设置一条 Key-Value 对的失效时间,而一条 Key-Value 对一旦被关联了失效时间就会在到期后自动删除(或者说变得无法访问更为准确).可以说,主键失效这个概念还是比较容易理解的,但是在具体实现到 Redis 中又是如何呢?最近本博主就对 Redis

  • 详解Redis命令和键_动力节点Java学院整理

    Redis命令用于在redis服务器上执行某些操作. 要在Redis服务器上运行的命令,需要一个Redis客户端. Redis客户端在Redis的包,这已经我们前面安装使用过了. 语法 Redis客户端的基本语法如下: $redis-cli 例子 下面举例说明如何使用Redis客户端. 要启动redis客户端,打开终端,输入命令Redis命令行:redis-cli.这将连接到本地服务器,现在就可以运行各种命令了. $redis-cli redis 127.0.0.1:6379> redis 12

  • Redis中键的过期删除策略深入讲解

    如果一个键过期了,那么它什么时候会被删除呢? 这个问题有三种可能的答案,它们分别代表了三种不同的删除策略: 定时删除:在设置键的过期时间的同时,创建一个定时器( timer ). 让定时器在键的过期时间来临时,立即执行对键的删除操作. 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键. 定期删除: 每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键.至于要删除多少过期键,以及要检查多少个数据库, 则由算法决定

  • Redis使用Eval多个键值自增的操作实例

    在PHP上使用Redis 给多个键值进行自增,示例如下: $set['money'] = $this->redis->hIncrByFloat($key, $hour .'_money', $data['money']); $set['ip'] = $this->redis->hIncrBy($key, $hour .'_ip', $data['ip']); $set['uv'] = $this->redis->hIncrBy($key, $hour .'_uv', $

  • Redis 不使用 keys 命令获取键值信息的方法

    1. 问题来源 这个问题可能看起来很奇怪,但很多 redis 集群会有一个统一的入口,入口会作兼容 redis 命令的代理,一般出于新能考虑是禁止使用 keys 命令来获取键值信息的,但是可以通过 scan 命令来代替 keys 2. 使用 keys 的方法 127.0.0.1:6379> KEYS * 1) "_kombu.binding.test_queue" 2) "a8e620b9-e52e-3498-8a1c-448f35783058" 3) &qu

  • Redis 2.8-4.0过期键优化过程全纪录

    前言 之前 白馨(陌陌-技术保障部存储工程师 )在Redis技术交流群里,总结了一下Redis从2.8~4.0关于过期键相关的fix记录,非常有帮助,但有些东西未尽详细,本文将进行详细说明. 先从一个问题来看,运行环境如下: Redis: 2.8.19 db0:keys=10000000,expires=10000000 主从结构 从下图中可以看到,在从节点get hello非空,在主节点get hello为空,之后从节点get hello为空,经排查主从同步offset基本正常,但出现了主从不

  • redis学习之RDB、AOF与复制时对过期键的处理教程

    生成RDB文件 在执行SAVE命令或者BGSAVE命令创建一个新的RDB文件时,程序会对数据库中的键进行检查,已过期的键不会被保存到新创建的RDB文件中. 举个例子,如果数据库中包含三个键k1.k2.k3,并且k2已经过期,那么当执行SAVE命令或者BGSAVE命令时,程序只会将k1和k3的数据保存到RDB文件中,而k2则会被忽略. 因此,数据库中包含过期键不会对生成新的RDB文件造成影响. 可参考rdb.c中函数rdbSave()函数源码: /* Iterate this DB writing

  • 大家都应该知道的Redis过期键与过期策略

    今天,我和大家分享一篇关于 Redis 有关过期键的内容,主要有四个内容: 如何设置过期键 如何取消设置的过期时间 过期键的过期策略是怎样的 RDB.AOF 和复制对过期键的处理又是怎样的 设置键的生存时间或过期时间 redis 一共有 4 个命令来设置键的生存时间(可以存活多久)或过期时间(什么时候被删除) expire <key> <ttl>:将 key 的生存时间设置为 ttl 秒 pexpire <key> <ttl>:将 key 的生存时间设置为

  • 使用redis实现延迟通知功能(Redis过期键通知)

    Redis 过期监听场景 业务中有类似等待一定时间之后执行某种行为的需求 , 比如 30 分钟之后关闭订单 . 网上有很多使用 Redis 过期监听的 Demo redis配置 把notify-keyspace-events Ex 这一行的注释打开 项目demo工程 项目结构如下图 maven依赖 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apa

  • 浅谈Redis中的内存淘汰策略和过期键删除策略

    目录 8种淘汰策略 过期键的删除策略 总结 redis是我们现在最常用的一个工具,帮助我们建设系统的高可用,高性能. 而且我们都知道redis是一个完全基于内存的工具,这也是redis速度快的一个原因,当我们往redis中不断缓存数据的时候,其内存总有满的时候(而且内存是很贵的东西,尽量省着点用),所以尽可能把有用的数据,或者使用频繁的数据缓存在redis中,物尽其用. 那么如果正在使用的redis内存用完了,我们应该怎么取舍redis中已存在的数据和即将要存入的数据呢,我们要怎么处理呢? re

  • 浅谈Redis对于过期键的三种清除策略

    目录 Pre Redis Key的超时设置处理 被动删除 主动删除 当前已用内存超过maxmemory限定时,触发主动清理策略 对于过期键一般有三种删除策略 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作: 惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键:如果没有过期,那就返回该键: 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键.至于删除多少过期

  • Redis中过期键如何删除示例详解

    目录 前言 Redis 中 key 的过期删除策略 1.定时删除 2.惰性删除 3.定期删除 Redis 中过期删除策略 从库是否会脏读主库创建的过期键 内存淘汰机制 内存淘汰触发的最大内存 有哪些内存淘汰策略 内存淘汰算法 LRU LFU 为什么数据删除后内存占用还是很高 内存碎片如何产生 碎片率的意义 如何清理内存碎片 总结 参考 前言 Redis 中的 key 设置一个过期时间,在过期时间到的时候,Redis 是如何清除这个 key 的呢? 这来分析下 Redis 中的过期删除策略和内存淘

  • Redis的过期键删除策略原理说明

    目录 惰性删除 定期删除 惰性删除策略 定期删除策略的实现 Redis服务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地在合理使用CPU时间和避免浪费内存空间之间取得平衡. 惰性删除 惰性删除策略对CPU时间来说是最友好的:程序只会在取出键时才对键进行过期检查,这可以保证删除过期键的操作只会在非做不可的情况下进行,并且删除的目标仅限于当前处理的键,这个策略不会在删除其他无关的过期键上花费任何CPU时间. 惰性删除策略的缺点是,它对内存是最不友好的:如果一个

  • Redis有效时间设置以及时间过期处理操作

    本文对redis的过期处理机制做个简单的概述,让大家有个基本的认识. Redis中有个设置时间过期的功能,即对存储在redis数据库中的值可以设置一个过期时间.作为一个缓存数据库,这是非常实用的.如我们一般项目中的token或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能. 一.有效时间设置: redis对存储值的过期处理实际上是针对该值的键(key)处理的,即时间的设置也是设置key的有效时间.Expires字典保存

  • Python操作Redis之设置key的过期时间实例代码

    Expire 命令用于设置 key 的过期时间.key 过期后将不再可用. r.set('2', '4028b2883d3f5a8b013d57228d760a93') #成功就返回True 失败就返回False,下面的20表示是20秒 print r.expire('2',20) #如果时间没事失效我们能得到键为2的值,否者是None print r.get('2') 对于一个已经存在的key,我们可以设置其过期时间,到了那个时间后,当你再去访问时,key就不存在了 有两种方式可以设置过期时间

随机推荐