Redis 系列 过期清除

开启 redis 探索新篇章

Posted by lichao modified on May 29, 2020

Redis 作为缓存使用最主要的一个特性就是可以为键值对设置过期时间。

Redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定期遍历这个字典来删除到期的 key。除了定期遍历之外,它还会使用惰性策略来删除过期的 key,所谓惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期了就立即删除。定期删除是集中处理,惰性删除是零散处理。

在 redisDb 中使用了 dict *expires 来存储过期时间。其中 key 指向了 keyspace 中的 key(c 语言中的指针),value 是一个 long long 类型的时间戳,表示这个 key 过期的时间点,单位是毫秒。关联文档-字典

> redis PEXPIREAT mobile 1521469812000 # 如果为 mobile 增加一个过期时间。

这个时候就会在过期的字典中增加一个键值对。如下图: 过期存储示意图

对于过期的判断逻辑就很简单:

  1. 在字典 expires 中 key 是否存在。
  2. 如果 key 存在,value 的时间戳是否小于当前系统时间戳。

在 Redis 中与过期时间有关的命令:

  • EXPIRE 设置 key 的存活时间单位秒
  • EXPIREAT 设置 key 的过期时间点单位秒
  • PEXPIRE 设置 key 的存活时间单位毫秒
  • PEXPIREAT 设置 key 的过期时间点单位毫秒 其实这些命令,底层的命令都是由 REXPIREAT 实现的。

过期删除策略

常见的过期策略有三种:

  1. 定时过期:每个设置过期时间的 key 都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  2. 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除。该策略可以最大化地节省CPU资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
  3. 定期过期:Redis 定时扫描过期键,但是只删除部分,至于删除多少键,根据当前 Redis 的状态决定。

这三种策略对时间和空间有不同的倾向。Redis 为了平衡时间和空间,采用了惰性过期和定期过期后两种策略

定期删除

Redis 默认会每秒进行十次过期扫描(100ms一次),过期扫描不会遍历过期字典中所有的 key,而是采用了一种简单的贪心策略。步骤比较复杂,总结一下:(这里都是以默认配置描述)

  1. Redis 会用最多 25% 的 cpu 时间处理键的过期(每次扫描时间的上限,默认不会超过 25ms)。
  2. 遍历所有的 redisDb
  3. 在每个 redisDb 中如果数据中没有过期键或者过期键比例过低就直接进入下一个 redisDb。
  4. 从过期字典中随机 20 个 key,删除这 20 个 key 中已经过期的 key,如果删除的键的个数达到 20 个 key 的 25% ,重复此步骤。

所以业务开发人员一定要注意过期时间,如果有大批量的 key 过期,要给过期时间设置一个随机范围,而不能全部在同一时间过期。

Redis 单实例中包含若干个(默认16个)redisDb 数据库,客户端连接时默认连接数据库 0。Redis 集群模式下只有 db0,不支持多 db。

从库的过期策略

从库不会进行过期扫描,从库对过期的处理是被动的。主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。

因为指令同步是异步进行的,所以主库过期的 key 的 del 指令没有及时同步到从库的话,会出现主从数据的不一致,主库没有的数据在从库里还存在.

读取过期数据问题

如果客户端从主库上读取留存的过期数据,主库会触发删除操作,此时,客户端并不会读到过期数据。但是,从库本身不会执行删除操作,如果客户端在从库中访问留存的过期数据,从库并不会触发数据删除。

如果使用的是 Redis 3.2 之前的版本,那么,从库在服务读请求时,并不会判断数据是否过期,而是会返回过期数据。在 3.2 版本后,Redis 做了改进,如果读取的数据已经过期了,从库虽然不会删除,但是会返回空值,这就避免了客户端读到过期数据。所以,在应用主从集群时,尽量使用 Redis 3.2 及以上版本。

Redis 3.2 后的版本也并不能完全保证读不到过期数据,这跟 Redis 用于设置过期时间的命令有关系,有些命令给数据设置的过期时间在从库上可能会被延后,导致应该过期的数据又在从库上被读取到了,原因如下。

设置数据过期时间的命令一共有 4 个,可以把它们分成两类:

  • EXPIRE 和 PEXPIRE:它们给数据设置的是从命令执行时开始计算的存活时间;
  • EXPIREAT 和 PEXPIREAT:它们会直接把数据的过期时间设置为具体的一个时间点。

当主从库全量同步时,如果主库接收到了一条 EXPIRE 命令,那么,主库会直接执行这条命令。这条命令会在全量同步完成后,发给从库执行。而从库在执行时,就会在当前时间的基础上加上数据的存活时间,这样一来,从库上数据的过期时间就会比主库上延后了。

为了避免这种情况,在业务应用中使用 EXPIREAT/PEXPIREAT 命令,把数据的过期时间设置为具体的时间点,避免读到过期数据。

参考文献

Redis 数据库、键过期的实现