1. 问题简述前几天接收到报警,同时Redis团队监控到redis集群发生了主从切换;最终分析原因是,删除大key,导致redis主服务器阻塞,sentinel哨兵认为主服务器宕机,进行了故障转移;如下图所示:在Redis集群中,应...
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在删除过期键时,才会执行惰性删除策略。
本文标题为:【redis源码】删除大key导致redis主从切换
基础教程推荐
- python中pandas库的iloc函数用法解析 2023-07-28
- 【Redis】数据持久化 2023-09-12
- SQLServer 清理日志的实现 2023-07-29
- Sql Server Management Studio连接Mysql的实现步骤 2023-07-29
- Mysql主从三种复制模式(异步复制,半同步复制,组复 2022-09-01
- Python常见库matplotlib学习笔记之多个子图绘图 2023-07-27
- Mysql查询所有表和字段信息的方法 2023-07-26
- 关于MySQL中explain工具的使用 2023-07-27
- 如何将excel表格数据导入postgresql数据库 2023-07-20
- Redis如何实现延迟队列 2023-07-13