Redis在服务端分别为不同的db index维护一个dict,这个dict称为key space键空间。每一个RedisClient只能属于一个db index,在Redis服务端会维护每一个链接的RedisClient。
typedef struct redisClient { uint64_t id; int fd; redisDb *db; } redisClient;
在服务端每一个Redis客户端都会有一个指向redisDb的指针。
typedef struct redisDb { dict *dict; dict *expires; dict *blocking_keys; dict *ready_keys; dict *watched_keys; struct evictionPoolEntry *eviction_pool; int id; long long avg_ttl; } redisDb;
key space键空间就是这里的redisDb->dict。redisDb->expires是维护所有键空间的每一个key的过期时间。
1、键过期时间、生存时间
对于一个key我们可以设置它多少秒、毫秒之后过期,也可以设置它在某个具体的时间点过期,后者是一个时间戳。例如:
EXPIRE命令可以设置某个key多少秒之后过期;
PEXPIRE命令可以设置某个key多少毫秒之后过期;
EXPIREAT命令可以设置某个key在多少秒时间戳之后过期;
PEXPIREAT命令可以设置某个key在多少毫秒时间戳之后过期;
PERSIST命令可以移除键的过期时间。
其实上述命令最终都会被转换成对PEXPIREAT命令。在redisDb->expires指向的key字典中维护着一个到期的毫秒时间戳。
TTL、PTTL可以通过这两个命令查看某个key的过期秒、毫秒数。
Redis内部有一个事件循环,这个事件循环会检查键的过期时间是否小于当前时间,如果小于则会删除这个键。
2、过期键删除策略
在使用Redis的时候我们最关心的就是键是如何被删除的,如何高效准时地删除某个键。其实Redis提供了两个方案来完成这件事情:惰性删除、定期删除双重删除策略。
惰性删除:当我们访问某个key的时候,Redis会检查它是否过期。
robj *lookupKeyRead(redisDb *db, robj *key) { robj *val; expireIfNeeded(db,key); val = lookupKey(db,key); 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 */ if (server.loading) return 0; now = server.lua_caller ? server.lua_time_start : mstime(); if (server.masterhost != NULL) return now > when; /* Return when this key has not expired */ if (now <= when) return 0; /* Delete the key */ server.stat_expiredkeys++; propagateExpire(db,key); notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,"expired",key,db->id); return dbDelete(db,key); }
定期删除:Redis通过事件循环,周期性地执行key的过期删除动作。
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) { /* Handle background operations on Redis databases. */ databasesCron(); } void databasesCron(void) { /* Expire keys by random sampling. Not required for slaves * as master will synthesize DELs for us. */ if (server.active_expire_enabled && server.masterhost == NULL) activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW); }
要注意的是:
惰性删除是每次只要有读取、写入都会触发惰性删除代码;
定期删除是由Redis EventLoop来触发的。Redis内部很多维护性工作都是基于EventLoop。
3、AOF、RDB处理过期键策略
既然键会随时存在过期问题,那么涉及到持久化Redis是如何帮我们处理的?
当Redis使用RDB方式持久化时,每次持久化的时候就会检查这些即将被持久化的key是否已经过期,如果过期将直接忽略,持久化那些没有过期的键。
当Redis作为master主服务器启动的时候,载入rdb持久化键时也会检查这些键是否过期,将忽略过期的键,只载入没过期的键。
当Redis使用AOF方式持久化时,每次遇到过期的key Redis会追加一条DEL命令到AOF文件,也就是说只要我们顺序载入执行AOF命令文件就会删除过期的键。
如果Redis作为从服务器启动的话,它一旦与master主服务器建立链接就会清空所有数据进行完整同步。当然新版本的Redis支持SYCN2的半同步,如果是已经建立了master/slave主从同步之后,主服务器会发送DEL命令给所有从服务器执行删除操作。
4、Redis LRU算法
在使用Redis的时候我们会设置maxmemory选项,64位的默认是0不限制。线上的服务器必须要设置的,要不然很有可能导致Redis宿主服务器直接内存耗尽,最后链接都上不去。
所以基本要设置两个配置:
maxmemory最大内存阈值;
maxmemory-policy到达阈值的执行策略。
可以通过CONFIG GET maxmemory/maxmemory-policy分别查看这两个配置值,也可以通过CONFIG SET去分别配置。
maxmemory-policy有一组配置,可以用在很多场景下:
.noeviction:客户端尝试执行会让更多内存被使用的命令直接报错;
.allkeys-lru:在所有key里执行lru算法;
.volatile-lru:在所有已经过期的key里执行lru算法;
.allkeys-random:在所有key里随机回收;
.volatile-random:在已经过期的key里随机回收;
.volatile-ttl:回收已经过期的key,并且优先回收存活时间(TTL)较短的键。
关于cache的命中率可以通过info命令查看键空间的命中率和未命中率。
# Stats keyspace_hits:33 keyspace_misses:5
maxmemory在到达阈值的时候会采用一定的策略去释放内存,这些策略我们可以根据自己的业务场景来选择,默认是noeviction 。
Redis LRU算法有一个取样的优化机制,可以通过一定的取样因子来加强回收的key的准确度。CONFIG GET maxmemory-samples查看取样配置,具体可以参考更加详细的文章。