《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 kaito-kidd.com/2020/06/25/redis-randomkey-issue/ 「kaito-kidd」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

大家好,我是基基。

最近在公司对Redis做一些二次开发时,发现一个randomkey命令可能导致整个redis实例长时间阻塞的问题,Redis版本为3.2.9,以此记录。

问题

由于我们公司使用的是redis集群版Codis,Codis内置的redis版本比较低,为3.2.9版本。

我们近期在做Codis双机房时,需要对redis增加一些功能以此支持双机房,在开发和测试中发现,执行randomkey命令有可能导致整个redis长时间阻塞的问题。

randomkey主要功能是在redis中随机返回一个key出来,它随机选取key的代码如下。

robj *dbRandomKey(redisDb *db) {
dictEntry *de;

// 死循环从哈希表中找到一个不过期的key
while(1) {
sds key;
robj *keyobj;

// 从实例的哈希表里随机一个元素
de = dictGetRandomKey(db->dict);
if (de == NULL) return NULL;

// 获取这个元素的key
key = dictGetKey(de);
keyobj = createStringObject(key,sdslen(key));
// 如果key已经过期 则把这个key从实例中删除
// 注意:expireIfNeeded对于过期的key只针对master有效
// 如果是slave则永远不会删除key
if (dictFind(db->expires,key)) {
if (expireIfNeeded(db,keyobj)) {
decrRefCount(keyobj);
continue;
}
}
return keyobj;
}
}

从上面代码可以看出来,如果当前是个slave,并且整个实例中存在大量已经过期的key(key已过期,但redis还未来得及删除key),执行randomkey命令时,由于找不到不过期的key,那么这个逻辑就会陷入死循环,阻塞住整个实例,整个实例不可用。

如果当前是master,执行randomkey命令时,redis会一直随机选择key,直到找到一个不过期的key,同时会把已经过期的key从整个实例中删除。也就是说,在这种场景下,虽然不会长时间阻塞整个实例,但也会比执行一个普通的命令耗时要久。如果你在一个大量已过期的实例上执行randomkey命令,那可能会导致业务访问redis变慢。

解决

我们对比了官方最新版的redis,已经针对此问题进行了修复。

robj *dbRandomKey(redisDb *db) {
dictEntry *de;
// 当前实例全部都是过期key 最大循环100次
int maxtries = 100;
int allvolatile = dictSize(db->dict) == dictSize(db->expires)

// 死循环从哈希表中找到一个不过期的key
while(1) {
sds key;
robj *keyobj;

// 从实例的哈希表里随机一个元素
de = dictGetRandomKey(db->dict);
if (de == NULL) return NULL;

// 获取这个元素的key
key = dictGetKey(de);
keyobj = createStringObject(key,sdslen(key));
// 如果key已经过期 则把这个key从实例中删除
// 注意:expireIfNeeded对于过期的key只针对master有效
// 如果是slave则永远不会删除key
if (dictFind(db->expires,key)) {
// 如果整个实例都是过期key 在slave上执行此命令最多循环100次 避免长时间阻塞
if (allvolatile && server.masterhost && --maxtries == 0) {
return keyobj;
}
if (expireIfNeeded(db,keyobj)) {
decrRefCount(keyobj);
continue;
}
}
return keyobj;
}
}

解决方案就是增加一个最大重试次数,如果整个实例都是过期key,那么最多寻找maxtries次就返回,避免阻塞整个实例。

注意点

但要注意的是,如果达到了maxtries,那么返回的key是已经过期的key,你虽然在randomkey中看到了这个key,但对这个key执行其他命令时,还是拿不到这个key的。

这个方案只针对slave上执行这个命令进行了修复,也就是不会再让redis陷入死循环。

但在master上执行这个命令还是会发生上述的变慢问题,如果你在使用redis时,经常使用这个命令,同时实例中存在大量已经过期的key,那么redis变慢很有可能是这个问题导致的。

文章目录
  1. 1. 问题
  2. 2. 解决
  3. 3. 注意点