美文网首页
删除大key导致redis主从切换

删除大key导致redis主从切换

作者: 程序员的自我修养 | 来源:发表于2023-02-08 11:59 被阅读0次

原文地址:https://blog.csdn.net/luoyu_/article/details/83090576

1. 问题简述

前几天接收到报警,同时Redis团队监控到redis集群发生了主从切换;

最终分析原因是,删除大key,导致redis主服务器阻塞,sentinel哨兵认为主服务器宕机,进行了故障转移;如下图所示:

在Redis集群中,应用程序尽量避免使用大键;直接影响容易导致集群的容量和请求出现”倾斜问题“,同时在删除大键或者打键过期时,容易出现故障切换和应用程序雪崩的故障;

查询线上有一个集合键,集合oea_set_star_ol_2017元素个数达到4300万;当删除这个键,或者键过期时,会阻塞redis主进程,从而发生了主从切换;(集合中的每个元素对象都要释放内存空间,时间复杂度比较高)

2. 解决方案

众所周知,Redis是单进程执行命令请求的;集合已经有4000多万元素了,想要删除这个集合,肯定不能直接删除,否则必会阻塞主进程;

我们可以一点一点删除集合中的元素;

Redis 2.8以上版本提供了这么一个命令:SCAN 命令,其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令;

它们每次执行都只会返回少量元素;(而不会出现像 KEYS命令、 SMEMBERS 命令带来问题 —— 当 KEYS 命令被用于处理一个大的数据库时, 又或者 SMEMBERS 命令被用于处理一个大的集合键时, 它们可能会阻塞服务器达数秒之久。)

我们可以这样做:通过HSCAN,每次获取500个字段,再用HDEL命令,每次删除1个字段;

这样虽然删除过程时间复杂度也很高(提高客户端复杂度,需要多次获取key,批量执行删除命令),但是至少不会阻塞redis服务器。

3. 更好的解决方案

redis也发现了这个问题:直接使用del命令删除大key会导致Redis主进程阻塞;分批次删除,客户端复杂度又比较高;

因此在Redis 4.0 的时候,提出了惰性删除lazyfree:当用户删除集key时,或者集合key过期需要删除时,检测如果集合元素大于64个,则使用惰性删除,只解除集合对象与数据库字典的关系,将集合对象放入待删除队列中,后台现成依次获取队列中的对象,并真正的删除;

redis 4.0 引入了lazyfree的机制,它可以将删除键或数据库的操作放在后台线程里执行, 从而尽可能地避免服务器阻塞。

lazyfree的原理不难想象,就是在删除对象时只是进行逻辑删除,然后把对象丢给后台,让后台线程去执行真正的destruct,避免由于对象体积过大而造成阻塞

下面我们深入redis源码,分析redis惰性删除策略;我们分析两个方面:客户端使用命令删除大key,大key过期删除;

3.1 客户端使用命令删除大key

redis 4.0删除元素有两个命令,del和unlink;del和之前版本一样,直接删除对象,可能会阻塞主进程,unlink就是惰性删除;

下面看看del和unlink命令的代码逻辑:

{"unlink",unlinkCommand,-2,"wF",0,NULL,1,-1,1,0,0}

{"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},

void delCommand(client *c) {

    delGenericCommand(c,0);

}

void unlinkCommand(client *c) {

    delGenericCommand(c,1);

}

delGenericCommand函数第二个参数是lazy标志;0同步删除,1惰性/异步删除,先解除对象数据库字典关联关系,再调用后台线程释放对象空间;

//lazy表示是否懒删除

void delGenericCommand(client *c, int lazy) {

    int numdel = 0, j;

    for (j = 1; j < c->argc; j++) {

        expireIfNeeded(c->db,c->argv[j]); //校验对象是否过期(顺便说一下,redis数据库有两个字典:对象字典 存储键值对,过期时间字典 存储键和过期时间)

        int deleted  = lazy ? dbAsyncDelete(c->db,c->argv[j]) : //根据lazy表示执行同步/异步删除操作

                              dbSyncDelete(c->db,c->argv[j]);

        if (deleted) {

            signalModifiedKey(c->db,c->argv[j]);

            notifyKeyspaceEvent(NOTIFY_GENERIC,

                "del",c->argv[j],c->db->id);

            server.dirty++;

            numdel++;

        }

    }

    addReplyLongLong(c,numdel);

}

删除命令之前如果检测到这个key已过期,则执行过期删除操作;

int expireIfNeeded(redisDb *db, robj *key) {

    mstime_t when = getExpire(db,key);

    mstime_t now;

    if (when < 0) return 0; //key没有配置过期时间

    //正在加载db,直接返回

    if (server.loading) return 0;

    //slave机器,不处理

    if (server.masterhost != NULL) return now > when;

    //没有到期

    if (now <= when) return 0;

    //删除

    server.stat_expiredkeys++;

    propagateExpire(db,key,server.lazyfree_lazy_expire); //传播到期删除命令给aof和slaves

    notifyKeyspaceEvent(NOTIFY_EXPIRED,

        "expired",key,db->id);

    return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : //根据过期删除策略决定同步/异步删除(用户可配置)

                                        dbSyncDelete(db,key);

}

惰性删除时,会执行异步删除函数

//异步删除函数:

#define LAZYFREE_THRESHOLD 64

int dbAsyncDelete(redisDb *db, robj *key) {

    //删除过期字典

    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

    //从字典删除键值对,并返回

    dictEntry *de = dictUnlink(db->dict,key->ptr);

    if (de) {

        robj *val = dictGetVal(de);

        size_t free_effort = lazyfreeGetFreeEffort(val); //获得当前对象长度(列表元素数目,hash对象键值对数目。。。)

        //当对象元素超过64个,且对象引用计数为1,才会懒删除;

        //开启bio后台线程删除

        if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {

            atomicIncr(lazyfree_objects,1);

            bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);  //子线程删除

            dictSetVal(db->dict,de,NULL);

        }

    }

    //释放键值对(假如懒释放,这里只释放键对象)

    if (de) {

        dictFreeUnlinkedEntry(db->dict,de);

        if (server.cluster_enabled) slotToKeyDel(key);

        return 1;

    } else {

        return 0;

    }

}

//同步删除函数,直接删除

int dbSyncDelete(redisDb *db, robj *key) {

    /* Deleting an entry from the expires dict will not free the sds of

    * the key, because it is shared with the main dictionary. */

    if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);

    if (dictDelete(db->dict,key->ptr) == DICT_OK) {

        if (server.cluster_enabled) slotToKeyDel(key);

        return 1;

    } else {

        return 0;

    }

}

3.2 过期删除

对于过期键有三种检测策略:

1.添加定时器:设置key过期时间时,添加定时器,定时执行过期删除(没有这么做)

2.周期性检测:周期性检测若干key过期时间,过期则删除;

3.访问这个key时,如果已经过期,则删除

redis结合2和3两种策略,实现过期键的检测;

过期键删除函数如下所示:

//过期键删除函数

int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {

    long long t = dictGetSignedIntegerVal(de);

    if (now > t) {

        sds key = dictGetKey(de);

        robj *keyobj = createStringObject(key,sdslen(key)); //数据库字典key存储的是字符串对象;过期字典key存储的是sds

        //代码基本与删除key代码相同;

        propagateExpire(db,keyobj,server.lazyfree_lazy_expire);

        if (server.lazyfree_lazy_expire)    //过期删除时,是否执行异步删除操作,由用户配置,server.lazyfree_lazy_expire

            dbAsyncDelete(db,keyobj);

        else

            dbSyncDelete(db,keyobj);

        notifyKeyspaceEvent(NOTIFY_EXPIRED,

            "expired",keyobj,db->id);

        decrRefCount(keyobj);

        server.stat_expiredkeys++;

        return 1;

    } else {

        return 0;

    }

}

4.总结

对于大key删除,上面提出了两种方案

对于低版本redis 2.8以上 4.0以下:使用scan命令分批次获得大key中的元素,分批次删除,直到删除大key中的所有元素;

客户端删除大key时,使用unlink命令,其会执行惰性删除策略,只是逻辑删除大key,真正的删除是在后台线程进行的;而对于过期删除,则需要用户配置server.lazyfree_lazy_expir,这样redis在删除过期键时,才会执行惰性删除策略。

相关文章

网友评论

      本文标题:删除大key导致redis主从切换

      本文链接:https://www.haomeiwen.com/subject/irjqkdtx.html