Redis keys

Redis 执行命令的原理

为什么keys 指令会导致其他命令执行变慢?
先看下 Redis 客户端执行一条命令的情况:
keys指令造成Redis查询阻塞 - 图1
站在客户端的视角,执行一条命令分为三步:

  1. 发送命令
  2. 执行命令
  3. 返回结果

但是这仅仅客户端自己以为的过程,但是实际上同一时刻,可能存在很多客户端发送命令给 Redis,而 Redis 采用的是单线程模型。
为了处理同一时刻所有的客户端的请求命令,Redis 内部采用了队列的方式,排队执行。
keys指令造成Redis查询阻塞 - 图2
于是客户端执行一条命令实际需要四步:

  1. 发送命令
  2. 命令排队
  3. 执行命令
  4. 返回结果

由于 Redis 单线程执行命令,只能顺序从队列取出任务开始执行。
只要 3 这个过程执行命令速度过慢,队列其他任务不得不进行等待,这对外部客户端看来,Redis 好像就被阻塞一样,一直得不到响应。
所以使用 Redis 过程切勿执行需要长时间运行的指令,这样可能导致 Redis 阻塞,影响执行其他指令。

KEYS 原理

为什么Keys 指令查询会这么慢?
回想一下 Redis 底层存储结构。
Redis 底层使用字典这种结构,这个结构与 Java HashMap 底层比较类似。
keys指令造成Redis查询阻塞 - 图3
keys命令需要返回所有的符合给定模式 pattern 的 Redis 中键,为了实现这个目的,Redis 不得不遍历字典中 ht[0]哈希表底层数组,这个时间复杂度为 「O(N)」(N 为 Redis 中 key 所有的数量)。
如果 Redis 中 key 的数量很少,那么这个执行速度还是也会很快。等到 Redis key 的数量慢慢更加,上升到百万、千万、甚至上亿级别,那这个执行速度就会很慢很慢。
使用 lua 脚本往 Redis 中增加 10W 个 key,然后使用 keys 查询所有键,这个查询大概会阻塞十几秒的时间。

  1. eval "for i=1,100000 do redis.call('set',i,i+1) end" 0

SCAN 简介

SCAN 命令及其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都用于增量地迭代(incrementally iterate)一集元素(a collection of elements):

  • SCAN 命令用于迭代当前数据库中的数据库键。
  • SSCAN 命令用于迭代集合键中的元素。
  • HSCAN 命令用于迭代哈希键中的键值对。
  • ZSCAN 命令用于迭代有序集合中的元素(包括元素成员和元素分值)。

基本用法可以参考:http://doc.redisfans.com/key/scan.html

SCAN和KEYS的区别

KEYS 命令被用于处理一个大的数据库时, 又或者 SMEMBERS 命令被用于处理一个大的集合键时, 它们会锁定Redis库, 可能会阻塞服务器达数秒之久。在高并发下会导致请求大量堆积进而导致服务雪崩。有些公司在生产环境直接禁用kyes *命令。但是在redis服务器key的数量不大的情况下,使用keys也是没啥问题的。
SCAN 命令及其相关的 SSCAN 命令、 HSCAN 命令和 ZSCAN 命令都用于增量地迭代 ,它们每次执行都只会返回少量元素,不会阻塞服务器, 所以这些命令可以用于生产环境, 而不会出现像 KEYS 命令、 SMEMBERS 命令带来的问题。
SCAN一样有它自己的问题:

  1. 因为是分段获取key,所以它会多次请求redis服务器,这样势必取同样的key,scan耗时更长。
  2. 在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证。
    1. SCAN cursor [MATCH pattern] [COUNT count]

    SCAN 原理

    为什么scan 指令就没有问题?
    这是因为 scan命令采用一种黑科技-「基于游标的迭代器」
    每次调用 scan 命令,Redis 都会向用户返回一个新的游标以及一定数量的 key。下次再想继续获取剩余的 key,需要将这个游标传入 scan 命令, 以此来延续之前的迭代过程。
    简单来讲,scan 命令使用分页查询 Redis 。
    下面是一个 scan 命令的迭代过程示例:
    scan 命令使用游标这种方式,巧妙将一次全量查询拆分成多次,降低查询复杂度。
    虽然 scan 命令时间复杂度与 keys一样,都是 「O(N)」,但是由于 scan 命令只需要返回少量的 key,所以执行速度会很快。
    最后,虽然scan 命令解决 keys不足,但是同时也引入其他一些缺陷:
  • 同一个元素可能会被返回多次,这就需要应用程序增加处理重复元素功能。
  • 如果一个元素在迭代过程增加到 Redis ,或者说在迭代过程被删除,那个这个元素会被返回,也可能不会。

以上这些缺陷,在开发中需要考虑这种情况。
除了 scan以外,redis 还有其他几个用于增量迭代命令: :::info

  • sscan:用于迭代当前数据库中的数据库键,用于解决 smembers 可能产生阻塞问题
  • hscan命令用于迭代哈希键中的键值对,用于解决 hgetall 可能产生阻塞问题。
  • zscan:命令用于迭代有序集合中的元素(包括元素成员和元素分值),用于产生 zrange 可能产生阻塞问题。 :::

    总结

    Redis 使用单线程执行操作命令,所有客户端发送过来命令,Redis 都会现放入队列,然后从队列中顺序取出执行相应的命令。
    如果任一任务执行过慢,就会影响队列中其他任务的,这样在外部客户端看来,迟迟拿不到 Redis 的响应,看起来就很阻塞了一样。
    所以不要在生产执行 keyssmembershgetallzrange这类可能造成阻塞的指令,如果真需要执行,可以使用相应的scan 命令渐进式遍历,可以有效防止阻塞问题。