惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
WordPress大学
WordPress大学
N
Netflix TechBlog - Medium
宝玉的分享
宝玉的分享
V
Visual Studio Blog
S
Securelist
P
Palo Alto Networks Blog
A
Arctic Wolf
T
Tor Project blog
P
Proofpoint News Feed
I
InfoQ
博客园 - 三生石上(FineUI控件)
T
Threat Research - Cisco Blogs
G
GRAHAM CLULEY
M
MIT News - Artificial intelligence
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Microsoft Security Blog
Microsoft Security Blog
MongoDB | Blog
MongoDB | Blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
Apple Machine Learning Research
Apple Machine Learning Research
S
Secure Thoughts
Cyberwarzone
Cyberwarzone
Blog — PlanetScale
Blog — PlanetScale
Simon Willison's Weblog
Simon Willison's Weblog
博客园 - 【当耐特】
大猫的无限游戏
大猫的无限游戏
腾讯CDC
Latest news
Latest news
Project Zero
Project Zero
V
Vulnerabilities – Threatpost
Y
Y Combinator Blog
S
SegmentFault 最新的问题
Schneier on Security
Schneier on Security
C
CERT Recently Published Vulnerability Notes
W
WeLiveSecurity
罗磊的独立博客
Stack Overflow Blog
Stack Overflow Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
L
LINUX DO - 热门话题
N
News and Events Feed by Topic
PCI Perspectives
PCI Perspectives
C
Cisco Blogs
Application and Cybersecurity Blog
Application and Cybersecurity Blog
爱范儿
爱范儿
T
The Exploit Database - CXSecurity.com
The Last Watchdog
The Last Watchdog
人人都是产品经理
人人都是产品经理
GbyAI
GbyAI
Know Your Adversary
Know Your Adversary
U
Unit 42

祈雨的笔记

安全多方计算MPC spark原理解析 kueue执行源码分析 spark on k8s执行源码分析 spark-operator源码解析 系统压测遇到的缓存击穿问题 我的世界PC与安卓联机 蚂蚁金服流量投放平台的AIG改造 G1大对象致Old区占用率高 日志打印导致接口响应率下跌分析 Groovy加载类导致OOM分析 ERROR日志打印导致CPU满载 记OceanBase死锁超时 应用发版期间服务响应超时 Ark Serverless初探 系统优化复盘一二三 The user specified as a definer does not exist Kong网关初探 API网关选型调研 CPU火焰图常用工具 配置中心选型调研 root操作Nginx导致用户组错误 基于Proxifier使用代理 FastJSON字段智能匹配踩坑 Nacos初探 记一次Nginx服务器CPU满荷载故障 基于券系统分库分表的思考 limit不参与SQL成本计算致索引失效 Linux常用性能监控命令 golang低版本http2偶现400 hostname in certificate didn't match 常见对称加密原理以及应用 tcp_tw_recycle引起的TCP握手失败 记一次mysql执行DDL导致锁表 mysql磁盘占用查看 mysql对text字段update致磁盘增长 elasticsearch报错index read-only TIME_WAIT与Http的Keep-Alive 记一次TIME_WAIT导致连接数报警 记一次生产事故OOM问题排查 redis分布式锁RedissonLock的实现细节 webservice复杂加密签名(2)java调用 webservice复杂加密签名(1)SoapUI mysql延时关联 利用中间人拦截实现APP内H5窜改 MySQL表字符集不同导致关联查询索引失效 通过SSH隧道远程办公 数据落盘方案 BeanDefinitionRegistryPostProcessor扩展 mysql空间索引 HTTPS攻击 spring循环依赖过程解析 elasticsearch性能优化 mysql IS NULL 使用索引 mysql字符集utf8mb4失效踩坑 常用加密算法 xml与javaBean转换 初探InnoDB MVCC源码实现 mysql索引原理 redis之list源码分析 redis之string源码分析 redis之hash源码分析 线程池之ThreadPoolExecutor mysql数据页结构 Using temporary与Using filesort mysql回表致索引失效 springboot(28)HTTP连接池 定时任务之ScheduledThreadPoolExecutor elasticsearch常用script聚合 elasticsearch实现like查询 elasticsearch实现乐观锁 elasticsearch准实时原理 springboot(27)自定义缓存读写机制CachingConfigurerSupport optimizer tracing arthas常用命令 HTTP和HTTPS详解 redis集群选举机制 kafka消息重试 一点压力测试的经验 kafka架构概念 explain分析sql语句字段的解释 JVM问题分析处理手册 logstash过滤器(2)date logstash过滤器(3)dissect logstash编码器(1)json logstash编码器(2)multiline logstash表达式 logstash输入(1)通用选项 logstash输入(3)file logstash过滤器(1)通用选项 logstash输入(2)stdin logstash安装 记一次前端vConsole导致JSON序列化错误排查 解决多个异步操作嵌套问题 fastjson反序列化失败autoType is not support RTMP串流服务 POI自动调整列宽错误 Nginx+Lua实现动态黑名单 使用curl命令模拟POST和GET请求 ResponseEntity下载文件
redis之key过期源码分析
祈雨的笔记 · 2019-07-08 · via 祈雨的笔记

简述

redis的所有数据结构都可以设置过期时间,当key过期后再查询该key返回null。

redis实现key自动过期是通过额外保存需要自动过期的key和该key的过期时间,然后通过主动删除和定时任务删除两种机制来将过期的key移除并回收内存。在redis4.0版本引入了异步删除的机制,对于删除对象大小大于64字节的key,先通过Unlink方法软删除后放入回收队列中,由其他线程异步回收内存空间,减少主线程的在内存回收上消耗的时间。

1
2
3
4
5
6
7
8
9
10
typedef struct redisDb {
dict *dict;
dict *expires;
dict *blocking_keys;
dict *ready_keys;
dict *watched_keys;
int id;
long long avg_ttl;
list *defrag_later;
} redisDb;

image

设置过期

redis添加自动过期的key时,先向hash结构数据redisDb.dict添加该key和key对应的值。添加成功后,调用setExpire方法向hash结构数据redisDb.expires添加该key和key对应的过期时间,过期时间的单位为毫秒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
long long milliseconds = 0;


if (expire) {
if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
return;
if (milliseconds <= 0) {
addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
return;
}
if (unit == UNIT_SECONDS) milliseconds *= 1000;
}

if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
(flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))
{
addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
return;
}

setKey(c->db,key,val);
server.dirty++;

if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC,
"expire",key,c->db->id);
addReply(c, ok_reply ? ok_reply : shared.ok);
}

void setExpire(client *c, redisDb *db, robj *key, long long when) {
dictEntry *kde, *de;


kde = dictFind(db->dict,key->ptr);
serverAssertWithInfo(NULL,key,kde != NULL);

de = dictAddOrFind(db->expires,dictGetKey(kde));
dictSetSignedIntegerVal(de,when);

int writable_slave = server.masterhost && server.repl_slave_ro == 0;
if (c && writable_slave && !(c->flags & CLIENT_MASTER))
rememberSlaveKeyWithExpire(db,key);
}

自动过期

过期key的删除回收分为主动删除和定时删除两种策略:

  • 主动删除:对key删改查操作时先判断该key是否已过期,如过期则删除回收该key
  • 定时删除:主线程每100毫秒从redisDb.expires中随机选择20个key,删除其中已过期的key。如果过期的key的比例超过1/4则重复该操作直到达到时间上限(默认25毫秒)

主动删除

对于key的删改查操作,先查询hash表redisDb.expires判断该key是否已经过期,如果key已过期,则默认调用dbAsyncDelete方法异步删除回收该key的内存空间;若key未过期,继续进行删改查操作。

另外在redis主从架构下,从节点不处理过期机制,通过等待主节点的指令直接删除对应的key。因此当网络延迟较大时,存在主节点中已过期的key能在从节点查询出来的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
robj *lookupKeyRead(redisDb *db, robj *key) {

return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);
}

robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
robj *val;



if (expireIfNeeded(db,key) == 1) {
if (server.masterhost == NULL) {
server.stat_keyspace_misses++;
notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
return NULL;
}

if (server.current_client &&
server.current_client != server.master &&
server.current_client->cmd &&
server.current_client->cmd->flags & CMD_READONLY)
{
server.stat_keyspace_misses++;
notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
return NULL;
}
}

val = lookupKey(db,key,flags);
if (val == NULL) {
server.stat_keyspace_misses++;
notifyKeyspaceEvent(NOTIFY_KEY_MISS, "keymiss", key, db->id);
}
else
server.stat_keyspace_hits++;
return val;
}

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

if (!keyIsExpired(db,key)) return 0;



if (server.masterhost != NULL) return 1;

server.stat_expiredkeys++;
propagateExpire(db,key,server.lazyfree_lazy_expire);
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",key,db->id);

return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
}

定时删除

databasesCron方法由主线程每100毫秒调用一次,其中的activeExpireCycle方法则是删除回收过期key的关键方法,其入参有两个值:ACTIVE_EXPIRE_CYCLE_FASTACTIVE_EXPIRE_CYCLE_SLOW

  • ACTIVE_EXPIRE_CYCLE_FAST:表示快速删除回收过期key,该场景下每次删除回收的时间上限为1毫秒,当主线程处理完外部请求后等待新请求前阻塞时会使用该参数;
  • ACTIVE_EXPIRE_CYCLE_SLOW:表示慢删除回收过期key,该场景下每次删除回收的时间上限为25毫秒,当主线程每100毫秒执行定时任务时使用该参数;

由于主线程每100毫秒会调用一次activeExpireCycle方法回收过期key,因此存在极端情况下同一时刻redis中大量数据同时过期,会导致每100毫秒一次的定时任务activeExpireCycle需要花费25毫秒的时间删除回收过期key,从而出现客户端请求等待阻塞的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
void databasesCron(void) {
if (server.active_expire_enabled) {
if (server.masterhost == NULL) {

activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
} else {
expireSlaveKeys();
}
}
...
}

#define ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 20
void activeExpireCycle(int type) {
...


timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
timelimit_exit = 0;
if (timelimit <= 0) timelimit = 1;



if (type == ACTIVE_EXPIRE_CYCLE_FAST)
timelimit = ACTIVE_EXPIRE_CYCLE_FASTf_DURATION;

long total_sampled = 0;
long total_expired = 0;

for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
int expired;
redisDb *db = server.db+(current_db % server.dbnum);

current_db++;

do {
unsigned long num, slots;
long long now, ttl_sum;
int ttl_samples;
iteration++;

if ((num = dictSize(db->expires)) == 0) {
db->avg_ttl = 0;
break;
}
slots = dictSlots(db->expires);
now = mstime();

if (num && slots > DICT_HT_INITIAL_SIZE &&
(num*100/slots < 1)) break;

expired = 0;
ttl_sum = 0;
ttl_samples = 0;


if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;

while (num--) {
dictEntry *de;
long long ttl;


if ((de = dictGetRandomKey(db->expires)) == NULL) break;
ttl = dictGetSignedIntegerVal(de)-now;

if (activeExpireCycleTryExpire(db,de,now)) expired++;
if (ttl > 0) {
ttl_sum += ttl;
ttl_samples++;
}
total_sampled++;
}
total_expired += expired;

if (ttl_samples) {
long long avg_ttl = ttl_sum/ttl_samples;

if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
}


if ((iteration & 0xf) == 0) {
elapsed = ustime()-start;
if (elapsed > timelimit) {
timelimit_exit = 1;
server.stat_expired_time_cap_reached_count++;
break;
}
}

} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
}
...
}

异步删除

在redis4.0版本默认调用dbAsyncDelete方法删除回收key,但实际上dbAsyncDelete会判断该key的对象大小,如果key的对象大小超过64字节时才会真正使用异步删除逻辑,将该key放入BIO队列由其他线程删除回收内存空间;否则仍然使用同步删除逻辑直接回收内存空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#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);


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;
}
}