redis的key过期了还能取出来?
转自:https://blog.csdn.net/f80407515/article/details/121648537
参考:https://blog.csdn.net/qq_49723651/article/details/125903754
我记得在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文件中
1 struct redisCommand redisCommandTable[] = { 2 {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0}, 3 .... 4 }
string.c文件中
1 void getCommand(redisClient *c) { 2 getGenericCommand(c); 3 } 4 5 int getGenericCommand(redisClient *c) { 6 robj *o; 7 8 if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL) 9 return REDIS_OK; 10 11 if (o->type != REDIS_STRING) { 12 addReply(c,shared.wrongtypeerr); 13 return REDIS_ERR; 14 } else { 15 addReplyBulk(c,o); 16 return REDIS_OK; 17 } 18 }
db.c
1 robj *lookupKeyReadOrReply(redisClient *c, robj *key, robj *reply) { 2 robj *o = lookupKeyRead(c->db, key); 3 if (!o) addReply(c,reply); 4 return o; 5 } 6 robj *lookupKeyRead(redisDb *db, robj *key) { 7 robj *val; 8 //过期处理 9 expireIfNeeded(db,key); 10 //不管结果,直接查,查到就返回 11 val = lookupKey(db,key); 12 //为空,miss计数 13 if (val == NULL) 14 server.stat_keyspace_misses++; 15 else 16 //命中计数 17 server.stat_keyspace_hits++; 18 return val; 19 } 20 21 int expireIfNeeded(redisDb *db, robj *key) { 22 // 返回key的过期时间,如果这个key没有设置过期时间返回-1 23 mstime_t when = getExpire(db,key); 24 mstime_t now; 25 26 //key没有设置过期时间,返回0 27 if (when < 0) return 0; /* No expire for this key */ 28 29 //持久化机制如果在loading,返回0 30 if (server.loading) return 0; 31 32 //获取当前时间 33 now = server.lua_caller ? server.lua_time_start : mstime(); 34 35 //如果不是master节点,过期返回1,未过期返回0 36 if (server.masterhost != NULL) return now > when; 37 38 //未过期返回0 39 if (now <= when) return 0; 40 41 /* Delete the key */ 42 // 过期key计数 43 server.stat_expiredkeys++; 44 //将过期机制同步到从库 45 propagateExpire(db,key); 46 //发布过期事件 47 notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED, 48 "expired",key,db->id); 49 //返回删除结果 50 return dbDelete(db,key); 51 }
到3.2 移除了redis.c文件 转到了server.c文件
1 struct redisCommand redisCommandTable[] = { 2 {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0}, 3 ...... 4 }
string.c中
1 void getCommand(client *c) { 2 getGenericCommand(c); 3 } 4 5 void getsetCommand(client *c) { 6 if (getGenericCommand(c) == C_ERR) return; 7 c->argv[2] = tryObjectEncoding(c->argv[2]); 8 setKey(c->db,c->argv[1],c->argv[2]); 9 notifyKeyspaceEvent(NOTIFY_STRING,"set",c->argv[1],c->db->id); 10 server.dirty++; 11 } 12 int getGenericCommand(client *c) { 13 robj *o; 14 15 if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.nullbulk)) == NULL) 16 return C_OK; 17 18 if (o->type != OBJ_STRING) { 19 addReply(c,shared.wrongtypeerr); 20 return C_ERR; 21 } else { 22 addReplyBulk(c,o); 23 return C_OK; 24 } 25 }
db.c 文件中
1 robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) { 2 robj *o = lookupKeyRead(c->db, key); 3 if (!o) addReply(c,reply); 4 return o; 5 } 6 7 robj *lookupKeyRead(redisDb *db, robj *key) { 8 return lookupKeyReadWithFlags(db,key,LOOKUP_NONE); 9 } 10 11 // 以读操作取出key的值对象,没找到返回NULL 12 // 调用该函数的副作用如下: 13 // 1.如果一个键的到达过期时间TTL,该键被设置为过期的 14 // 2.键的使用时间信息被更新 15 // 3.全局键 hits/misses 状态被更新 16 // 注意:如果键在逻辑上已经过期但是仍然存在,函数返回NULL 17 robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) { 18 robj *val; 19 20 //2.8 是直接放过,这块是有逻辑处理 21 if (expireIfNeeded(db,key) == 1) { 22 // 键已过期,如果是主节点环境,表示key已经绝对被删除 23 if (server.masterhost == NULL) return NULL; 24 // 如果我们在从节点环境, expireIfNeeded()函数不会删除过期的键,它返回的仅仅是键是否被删除的逻辑值 25 // 过期的键由主节点负责,为了保证主从节点数据的一致 26 if (server.current_client && 27 server.current_client != server.master && 28 server.current_client->cmd && 29 server.current_client->cmd->flags & CMD_READONLY) 30 { 31 return NULL; 32 } 33 } 34 //查找key 35 val = lookupKey(db,key,flags); 36 if (val == NULL) 37 server.stat_keyspace_misses++; 38 else 39 server.stat_keyspace_hits++; 40 return val; 41 } 42 43 int expireIfNeeded(redisDb *db, robj *key) { 44 mstime_t when = getExpire(db,key); 45 mstime_t now; 46 //没有设置过期时间 47 if (when < 0) return 0; /* No expire for this key */ 48 //持久化的 loading时,直接返回0 49 if (server.loading) return 0; 50 51 //获取当前时间 52 now = server.lua_caller ? server.lua_time_start : mstime(); 53 54 // 从库,已过期返回1,未过期返回0 55 if (server.masterhost != NULL) return now > when; 56 57 //未过期返回0 58 if (now <= when) return 0; 59 60 //删除逻辑 61 server.stat_expiredkeys++; 62 propagateExpire(db,key); 63 notifyKeyspaceEvent(NOTIFY_EXPIRED, 64 "expired",key,db->id); 65 //4.0以及以后的删除增加了异步删除 66 return dbDelete(db,key); 67 } 68 69 int dbDelete(redisDb *db, robj *key) { 70 //在过期集合中,删除 71 if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr); 72 //直接删除 73 if (dictDelete(db->dict,key->ptr) == DICT_OK) { 74 //同步从 75 if (server.cluster_enabled) slotToKeyDel(key); 76 return 1; 77 } else { 78 return 0; 79 } 80 }
最终将对应的逻辑梳理下了,同时也将redis的过期策略整理了下。
- 3.2以下的版本,没有对过期处理的结果进行处理,就会导致在从库上返回值;
- 4.0以后过期的情况下,又对expireifNeeded进行了处理(豆沙绿那块),如果不是主库就返回null
- 周期性的删除每次轮训一个周期(具体多久,看中间其他的执行),过期处理最多占用25毫秒(4.0以后的源码


浙公网安备 33010602011771号