Redis
当 key 达到过期时间,Redis 就会马上删除么?
先说结论:并不会立马删除。Redis 有两种删除过期数据的策略:

  • 定期选取部分数据删除;
  • 惰性删除;

该命令在 Redis 2.4 版本,过期时间并不是很精确,它可能在零到一秒之间。
从 Redis 2.6 开始,过期错误为 0 到 1 毫秒。
EXPIRE key seconds [ NX | XX | GT | LT] 指令可以将指定的 key 设置过期时间,如果没有设置过期时间, key 将一直存在,除非明确将其删除,比如执行 DEL 指令。
所谓”狡兔死,走狗烹“,没用了就干掉,跟 35 岁就“毕业”是一个道理。
好慌……
从 Redis 版本 7.0.0 开始:EXPIRE 添加了选项:NX、XX和GT、LT 选项。

  • NX:当 key 没有过期时才设置过期时间;
  • XX:只有 key 已过期的时候才设置过期时间;
  • GT:仅当新的到期时间大于当前到期时间时才设置过期时间;
  • LT:仅在新到期时间小于当前到期时间才设置到过期时间。

    过期与持久化

    主从或者集群架构中,两台机器的时钟严重不同步,会有什么问题么?
    key 过期信息是用 Unix 绝对时间戳表示的。
    为了让过期操作正常运行,机器之间的时间必须保证稳定同步,否则就会出现过期时间不准的情况。
    比如两台时钟严重不同步的机器发生 RDB 传输, slave 的时间设置为未来的 2000 秒,假如在 master 的一个 key 设置 1000 秒存活,当 Slave 加载 RDB 的时候 key 就会认为该 key 过期(因为 slave 机器时间设置为未来的 2000 s),并不会等待 1000 s 才过期。
    机器时钟不同步导致过期混乱

    惰性删除

    惰性删除很简单,就是当有客户端的请求查询该 key 的时候,检查下 key 是否过期,如果过期,则删除该 key。
    比如当 Redis 收到客户端的GET movie:小#……利亚.rmvb 请求,就会先检查 key = movie:小#……利亚.rmvb 是否已经过期,如果过期那就删除。
    删除过期数据的主动权交给了每次访问请求。
    该实现通过 expireIfNeeded函数实现,源码路径:src/db.c。

    1. int expireIfNeeded(redisDb *db, robj *key, int force_delete_expired) {
    2. // key 没有过期,return 0
    3. if (!keyIsExpired(db,key)) return 0;
    4. if (server.masterhost != NULL) {
    5. if (server.current_client == server.master) return 0;
    6. if (!force_delete_expired) return 1;
    7. }
    8. if (checkClientPauseTimeoutAndReturnIfPaused()) return 1;
    9. /* Delete the key */
    10. deleteExpiredKeyAndPropagate(db,key);
    11. return 1;
    12. }

    定期删除

    仅仅靠客户端访问来判断 key 是否过期才执行删除肯定不够,因为有的 key 过期了,但未来再也没人访问,这些数据要怎么删除呢?
    不能让这些数据「占着茅坑不拉屎」。
    所谓定期删除,也就是 Redis 默认每 1 秒运行 10 次(每 100 ms 执行一次),每次随机抽取一些设置了过期时间的 key,检查是否过期,如果发现过期了就直接删除。
    注意:并不是一次运行就检查所有的库,所有的键,而是随机检查一定数量的键。
    具体步骤如下:
    定时删除

  1. 从所有设置了过期时间的 key 集合中随机选择 20 个 key
  2. 删除「步骤 1」发现的所有过期 key 数据;
  3. 「步骤 2 」结束,过期的 key 超过 25%,则继续执行「步骤 1」。

删除的源码 expire.c 的 activeExpireCycle 函数实现。
这也就意味着在任何时候,过期 key 的最大数量等于每秒最大写入操作量除以 4。
为啥不检查所有设置过期时间的 key?
假设 Redis 里存放了 100 w 个 key,都设置了过期时间,每隔 100 毫秒就检查 100 w 个 key,CPU 全浪费在检查过期 key 上了,Redis 也就废了。
注意了:不管是定时删除,还是惰性删除。当数据删除后,master 会生成删除的指令记录到 AOF 和 slave 节点
如果过期的数据太多,定时删除无法删除完全(每次删除完过期的 key 还是超过 25%),同时这些 key 也再也不会被客户端请求,也就是无法走惰性删除,会怎样?
会不会导致 Redis 内存耗尽,怎么破?
答案是走内存淘汰机制
在 Redis 帝国中,整个帝国的国法、家法和军法等都记录在 redis.conf中,它控制着整个帝国的运行。
公务员占用的国家地盘资源大小限定由名叫「maxmemory」的司法官员制定,一共有两种方式实现:

  • 在运行时使用 CONFIG SET maxmemory 4gb指定帝国官职人员最大地盘资源为 4GB;
  • 将 maxmemory 4gb法令记录到 redis.conf「法典」中,在帝国运转指定使用该「法典」运行。

需要注意的是,如果 maxmemory 为 0 ,在 64 位「空间」上则没有限制,而 32 位「空间」则有 3GB 的隐式限制。

Redis 内存淘汰策略

设置了帝国官职地盘资源限制,每年选拔新人就会导致没有地盘资源可以使用怎么办?如何选择一些公务员淘汰?
在 Redis 4.0 时代,一共有 6 种淘汰策略,之后,又新增了 2 种策略。
总体上可以根据是否需要淘汰可以分为两大类:

  • 不执行淘汰策略,noeviction;
  • 根据不同法则淘汰的其他 7 种策略。

    noeviction 不退伍策略

    默认情况下,资源超过 maxmemory 的值也不会执行淘汰,不允许新人加入。
    随着官职人员的新增,由于不会淘汰,资源容量迟早会满。满了以后,当有「新人」想要进来的时候,Redis 直接返回错误,并罢工

    各式各样的淘汰策略

    剩下的 7 种策略还可以根据淘汰的候选集合和淘汰范围分为两大类:

  • 有设置任职过期时间的职员进行淘汰,没有设定任职过期时间的不会淘汰,淘汰策略如下:

    • volatile-lru:淘汰最近最少上一线干活的人员;
    • volatile-lfu:4.0 之后新增的策略,淘汰上一线干活次数最少的人员;
    • volatile-random:随机淘汰,腾出坑位给新人;
    • volatile-ttl:淘汰设置了任期时间的公务员,谁最接近任期时间就先淘汰谁。
  • 所有类型人员淘汰,不管是永久 vip 的皇亲国戚还是设置了任职过期时间的人员。
    • allkeys-lru:淘汰最近最少上一线干活的职员;
    • allkeys-lfu:淘汰最少上一线干活的公务员;
    • allkeys-random:随机淘汰职员,为新兵腾出空位。

接下来分享下在实际 Redis 中如何选择合适的淘汰策略和设置最佳缓存大小给大家。
淘汰执行过程如下图所示:
redis-eviction

  • 客户端发送新命令到服务端;
  • 服务端收到客户端命令,Redis 检查内存使用情况,如果大于 maxmemory 限制,则根据策略驱逐数据。
  • 执行新命令。

    allkeys-lru 使用场景

    假如应用存在明显的冷热数据区别,根据经验推荐使用这个策略,充分利用 LRU 算法把最近最常访问的数据保留,有限的内存提高访问性能。

    allkeys-random 使用场景

    假如数据没有明显的冷热分别,所有的数据分布查询比较均衡,这些数据都会被随机查询,那就使用 allkeys-random 策略,让其随机选择淘汰数据。

    volatile-lru 使用场景

    业务场景有一些数据不能删除,比如置顶新闻、视频,这时候为这些数据不设置过期时间,这样的话数据就不会被删除,该策略就会去根据 LRU 算法去淘汰那些设置了过期时间且最近最少被访问的数据。
    有一个点需要注意下,为 key 执行 expire 设置过期时间会消耗一些内存,所以使用 allkeyds-lru 会提高内存效率。
    将需要持数据不能删除的和全都可以淘汰数据的业务系统分别使用不同的 Redis 实例集群是更好的方案。
    针对业务场景有一些数据不能删除的使用 volatile-lru策略,另一类则可以使用 allkyes-lru 或者 allkeys-random。

    Redis 容量设置多大合适

    缓存并不是越大越好,用最小的代价去获得最高的收益才是老板想要的。
    数据访问有局部性,根据「二八原理」:通常 20% 的数据能支撑 80% 的访问请求。
    所以可不可以把缓存容量大小设置为总数据量的 20%?
    当然,不能这么绝对,这是理想状态。因为可能存在一些个性化需求,不同的用户访问的数据可能差别很大,不完全具备「二八原理」。
    应当结合实际的访问特点和成本来综合评估。根据经验建议将容量设置成总数据量的 15%~30%。
    其他淘汰规则比较简单,volatile-lru 和 volatile-lfu 则比较复杂,他们的算法是怎样的?
    volatile-lru 使用了 LRU 算法,淘汰最近最少使用的数据。而 volatile-lfu 使用了 LFU 算法,它在 LRU 算法基础上同时考虑了数据的时效性和访问频率,最少访问的 key 会被删除。

    参考资料

    https://redis.io/docs/manual/eviction/