我记得在2016年,2017年的时候,我们使用2.8的集群。当时业务有个需求,要求某个接口一天调用不能超过1000次,当时开发使用一个key: biz:total 来限制。
当时出现的问题是,第二天,接口实际调用量为0,但是从redis里获取到的值还是1000。
当时直接问的阿里云技术支持,反馈这种情况有两种,一种是定期删除,没有达到删除条件,一种是cpu压力过大,不会执行删除策略。
当时也没有深究这个问题,就想了个解决方案,直接把key改为 每天一个的 key: biz20170125:total 来控量,算是解决了这个问题。
最近在看redis的源码,顺带翻了下2.8和3.2的源码,真实的了解下问题的根本愿意。
redis2.8中 redis.c文件中
struct redisCommand redisCommandTable[] = {
{"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
....
}
string.c文件中
void getCommand(redisClient *c) {
getGenericCommand(c);
}
int getGenericCommand(redisClient *c) {
robj *o;
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
return REDIS_OK;
if (o->type != REDIS_STRING) {
addReply(c,shared.wrongtypeerr);
return REDIS_ERR;
} else {
addReplyBulk(c,o);
return REDIS_OK;
}
}
db.c
robj *lookupKeyReadOrReply(redisClient *c, robj *key, robj *reply) {
robj *o = lookupKeyRead(c->db, key);
if (!o) addReply(c,reply);
return o;
}
robj *lookupKeyRead(redisDb *db, robj *key) {
robj *val;
//过期处理
expireIfNeeded(db,key);
//不管结果,直接查,查到就返回
val = lookupKey(db,key);
//为空,miss计数
if (val == NULL)
server.stat_keyspace_misses++;
else
//命中计数
server.stat_keyspace_hits++;
return val;
}
int expireIfNeeded(redisDb *db, robj *key) {
// 返回key的过期时间,如果这个key没有设置过期时间返回-1
mstime_t when = getExpire(db,key);
mstime_t now;
//key没有设置过期时间,返回0
if (when < 0) return 0; /* No expire for this key */
//持久化机制如果在loading,返回0
if (server.loading) return 0;
//获取当前时间
now = server.lua_caller ? server.lua_time_start : mstime();
//如果不是master节点,过期返回1,未过期返回0
if (server.masterhost != NULL) return now > when;
//未过期返回0
if (now <= when) return 0;
/* Delete the key */
// 过期key计数
server.stat_expiredkeys++;
//将过期机制同步到从库
propagateExpire(db,key);
//发布过期事件
notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
"expired",key,db->id);
//返回删除结果
return dbDelete(db,key);
}
到3.2 移除了redis.c文件 转到了server.c文件
struct redisCommand redisCommandTable[] = {
{"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
......
}
string.c中
void getCommand(client *c) {
getGenericCommand(c);
}
void getsetCommand(client *c) {
if (getGenericCommand(c) == C_ERR) return;
c->argv[2] = tryObjectEncoding(c->argv[2]);
setKey(c->db,c->argv[1],c->argv[2]);
notifyKeyspaceEvent(NOTIFY_STRING,"set",c->argv[1],c->db->id);
server.dirty++;
}
int getGenericCommand(client *c) {
robj *o;
if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL)
return C_OK;
if (o->type != OBJ_STRING) {
addReply(c,shared.wrongtypeerr);
return C_ERR;
} else {
addReplyBulk(c,o);
return C_OK;
}
}
db.c 文件中
robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) {
robj *o = lookupKeyRead(c->db, key);
if (!o) addReply(c,reply);
return o;
}
robj *lookupKeyRead(redisDb *db, robj *key) {
return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);
}
robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
robj *val;
//2.8 是直接放过,这块是有逻辑处理
if (expireIfNeeded(db,key) == 1) {
//不是master节点,直接返回null
if (server.masterhost == NULL) return NULL;
//只读模式也返回null
if (server.current_client &&
server.current_client != server.master &&
server.current_client->cmd &&
server.current_client->cmd->flags & CMD_READONLY)
{
return NULL;
}
}
//查找key
val = lookupKey(db,key,flags);
if (val == NULL)
server.stat_keyspace_misses++;
else
server.stat_keyspace_hits++;
return val;
}
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 */
//持久化的 loading时,直接返回0
if (server.loading) return 0;
//获取当前时间
now = server.lua_caller ? server.lua_time_start : mstime();
// 从库,已过期返回1,未过期返回0
if (server.masterhost != NULL) return now > when;
//未过期返回0
if (now <= when) return 0;
//删除逻辑
server.stat_expiredkeys++;
propagateExpire(db,key);
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",key,db->id);
//4.0以及以后的删除增加了异步删除
return dbDelete(db,key);
}
int dbDelete(redisDb *db, robj *key) {
//在过期集合中,删除
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;
}
}
最终将对应的逻辑梳理下了,同时也将redis的过期策略整理了下。
- 3.2以下的版本,没有对过期处理的结果进行处理,就会导致在从库上返回值;
- 4.0以后过期的情况下,又对expireifNeeded进行了处理(豆沙绿那块),如果不是主库就返回null
- 周期性的删除每次轮训一个周期(具体多久,看中间其他的执行),过期处理最多占用25毫秒(4.0以后的源码)
后续