Redis2.8-4.0过期键优化详解

本文首次发表在微信公众号:Redis开发运维实战
先从一个问题来看,运行环境如下:

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

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


image.png

原因先不说,本文来探讨下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中具体实现,还是挺有意思的,下图是个示意图


image.png
 if (server->masterhost == NULL) activeExpireCycle();

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

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

综上所述:

主库:

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

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

从库:

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

三、过期读写问题

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

1、过期键读操作

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;
}

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

b)对于从库,执行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返回结果的判断:

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

b)对于从库,执行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版本主库执行的操作,主库的过期键正常过期。


image.png

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


image.png

image.png

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

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

(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非读写分离、上述迁移使用,基本本文问题不会出现。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,544评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,430评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,764评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,193评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,216评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,182评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,063评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,917评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,329评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,543评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,722评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,425评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,019评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,671评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,825评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,729评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,614评论 2 353