策略 - 过期删除策略
概述
Redis 可以对 key 设置过期时间的,为了防止过期的key长期占用内存,需要相应的过期删除策略将过期的key删除
Redis设置过期时间
- setex key1 5 value1:创建记录的时候指定过期时间,设置key1在5秒后过期
其实Redis这是一种基于创建时间来判定是否过期的机制,也即常规上说的TTL策略
,当设定了过期时间之后不管有没有被使用都会到期被强制清理掉。但有很多场景下也会期望数据能够按照TTI(指定时间未使用再过期)的方式来过期清理,如用户鉴权场景:
假设用户登录系统后生成token并存储到Redis中,指定token有效期30分钟,那么如果用户一直在使用系统的时候突然时间到了然后退出要求重新登录,这个体验感就会很差。正确的预期应该是用户连续操作的时候就不要退出登录,只有连续30分钟没有操作的时候才过期处理。
略有遗憾的是,Redis并不支持按照TTI机制来做数据过期处理。但是作为补偿,Redis提供了一个重新设定某个key值过期时间的方法,可以通过expire
方法来实现指定key的续期操作,以一种曲线救国的方式满足诉求。
实现缓存续期
- expire <key> <n>:设置 key 在 n 秒后过期,比如 expire key 100 表示设置 key 在 100 秒后过期;
- pexpire <key> <n>:设置 key 在 n 毫秒后过期,比如 pexpire key2 100000 表示设置 key2 在 100000 毫秒(100 秒)后过期。
- expireat <key> <n>:设置 key 在某个时间戳(精确到秒)之后过期,比如 expireat key3 1683187646 表示 key3 在时间戳 1683187646 后过期(精确到秒);
- pexpireat <key> <n>:设置 key 在某个时间戳(精确到毫秒)之后过期,比如 pexpireat key4 1683187660972 表示 key4 在时间戳 1683187660972 后过期(精确到毫秒)
对于上面说的用户token续期的诉求,可以这样来操作:
用户首次登录成功后,会生成一个token令牌,然后将令牌与用户信息存储到redis中,设定30分钟有效期。 每次请求接口中携带token来鉴权,每次get请求的时候,就重新通过expire操作将token的过期时间重新设定为30分钟。 持续30分钟无请求后,此条token缓存信息过期失效。同样实现了
TTI
的效果。
ttl查看过期时间
# 查看 key1 过期时间还剩多少
> ttl key
(integer) 56
# 取消 key 的过期时间
> persist key
(integer) 1
//永不过期返回-1
> ttl key
(integer) -1
如何判断过期时间
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* 键值对的过期时间 */
//……
} redisDb;
dict是hash表
- *dict是键值对,指向的是数据库中保存的具体的key value对象,key是String类型,value是具体的数据类型
- *expires是过期字典,key与*dict中的key一致,value则是一个 long long 类型的整数,这个整数保存了 key 的过期时间;
过期字典的数据结构如下图所示:
简单地总结来说就是,设置了失效时间的key和具体的失效时间全部都维护在 expires 这个字典表中。未设置失效时间的key不会出现在expires字典表中。
所以当查询一个 key 时,Redis 首先检查该 key 是否存在于expires过期字典中:
- 如果不在,则正常读取键值;
- 如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 已过期。
过期删除策略
定时删除
在设置key的过期时间的同时,为该key创建一个定时器,让定时器在key的过期时间来临时,对key进行删除。
优点:保证内存被尽快释放,对内存友好
缺点:若过期key很多,删除这些key会占用很多的CPU时间,在CPU时间紧张的情况下,CPU不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些key,定时器的创建耗时,若为每一个设置过期时间的key创建一个定时器(将会有大量的定时器产生),性能影响严重。对CPU不友好
结论:此方法基本上没人用
惰性删除
过期的key并不一定会马上删除,还会占用着内存。 当你真正查询这个key时,redis会检查一下,这个设置了过期时间的key是否过期了? 如果过期了就会删除,返回空。这就是惰性删除。
优点:删除操作只发生在从数据库取出key的时候发生,而且只删除当前key,所以对CPU时间的占用是比较少的,对 CPU友好
缺点:若大量的key在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露。对内存不友好
定期删除
每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键,至于要删除多少过期键,以及要检查多少个数据库,由算法决定
优点:通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
缺点:
- 内存清理方面没有定时删除效果好,同时没有惰性删除使用的系统资源少。
- 难以确定删除操作执行的时长和频率。如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不能及时得到释放
Redis的过期删除策略
redis实际使用的过期键删除策略是定期删除策略
和惰性删除策略
:
- 定期删除策略:redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定时遍历这个字典来删除到期的 key。
- 惰性删除策略:只有当访问某个key时,才判断这个key是否已过期,如果已经过期,则从实例中删除
定时删除是集中处理,惰性删除是零散处理。
定期删除策略
Redis内部维护一个定时任务,默认每秒进行10次(也就是每隔100毫秒一次)过期扫描,过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。
- 从过期字典中随机取出20个key
- 删除这 20 个 key 中已经过期的 key;
- 如果这20个key中过期key的比例超过了25%,则重复步骤1
为了保证过期扫描不会出现循环过度,导致线程卡死现象,算法还增加了扫描时间的上限,默认不会超过 25ms。
为什么key集中过期时,其它key的读写效率会降低?
Redis的定期删除策略是在Redis
主线程
中执行的,也就是说如果在执行定期删除的过程中,出现了需要大量删除过期key的情况,那么在业务访问时,必须等这个定期删除任务执行结束,才可以处理业务请求。此时就会出现,业务访问延时增大的问题,最大延迟为25毫秒。为了尽量避免这个问题,在设置过期时间时,可以给过期时间设置一个随机范围,避免同一时刻过期。
惰性删除策略
int expireIfNeeded(redisDb *db, robj *key, int flags) {
//检查是否开启惰性删除策略
if (server.lazy_expire_disabled) return 0;
if (!keyIsExpired(db,key)) return 0;//检查key是否过期,没过期不用删除
//……
//删除失效key
deleteExpiredKeyAndPropagate(db,key);
return 1;
}
int keyIsExpired(redisDb *db, robj *key) {
//假如Redis服务器正在从RDB文件中加载数据,暂时不进行失效主键的删除,直接返回0
if (server.loading) return 0;
//获取主键的失效时间 get当前时间-创建时间>ttl
mstime_t when = getExpire(db,key);
mstime_t now;
//假如失效时间为负数,说明该主键未设置失效时间(失效时间默认为-1),直接返回0
if (when < 0) return 0; /* No expire for this key */
now = commandTimeSnapshot();
//如果以上条件都不满足,就将主键的失效时间与当前时间进行对比,如果发现指定的key还未失效就返回0
return now > when;
}
过期key对持久化的影响
RDB:
- 生成rdb文件:生成时,程序会对key进行检查,过期key不放入rdb文件。
- 载入rdb文件:载入时,如果以主服务器模式运行,程序会对文件中保存的key进行检查,未过期的key会被载入到数据库中,而过期key则会忽略;如果以从服务器模式运行,无论键过期与否,均会载入数据库中,过期key会通过与主服务器同步而删除。
AOF:
- 当服务器以AOF持久化模式运行时,如果数据库中的某个key已经过期,但它还没有被惰性删除或者定期删除,那么AOF文件不会因为这个过期key而产生任何影响
- 当过期key被惰性删除或者定期删除之后,程序会向AOF文件追加一条DEL命令,来显示的记录被该key已经发被删除
- 在执行AOF重写的过程中,程序会对数据库中的key进行检查,已过期的key不会被保存到重写后的AOF文件中
主从复制:当服务器运行在复制模式下时,从服务器的过期删除动作由主服务器控制:
- 主服务器在删除一个过期key后,会显式地向所有从服务器发送一个del命令,告知从服务器删除这个过期key;
- 从服务器在执行客户端发送的读命令时,即使碰到过期key也不会将过期key删除,而是继续像处理未过期的key一样来处理过期key;
- 从服务器只有在接到主服务器发来的del命令后,才会删除过期key。