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以后的源码

 

posted @ 2023-06-01 15:53  Boblim  阅读(453)  评论(0)    收藏  举报