redis 在我们的日常开发中其实还是用的比较多的,常用来做缓存或者存放一些热点数据,主要为了提高系统的响应速度。在我们平常使用中,我们可能都习惯的使用 keys * 命令来查询数据库中的数据存储情况。
这时也许我们都是在开发环境执行,开发环境 redis 中的数据量不是很多,redis 也很快的能给我们响应。但是如果你要查询的 redis 中存储了非常大的数据量,这时候你还能愉快的使用这个命令吗?
首先,我们来大概了解下 keys 命令是用来干嘛的,它是用来查找所有符合给定模式 pattern 的 key ,上面的 * 表示匹配所有数据,同时还有下面几种用法:
- keys re?is 匹配 redis,recis
- keys re*is 匹配 redis, reddddis
- keys re[cd]is 匹配 redis,recis,不匹配 reais,reddis
keys 命令的时间复杂度是 O(N),N 是 redis 库中的数据量,也就是说这个命令的执行时间是随着库中的数据量增多呈正比的,数据量越大,执行时间越长。所以尽管这个命令的速度很快,但如果是一个数据量很大的库,还是会造成性能问题的。
我之前就在一次项目上线演练时,当时在一个 redis 库中初始化一批数据,当时我为了验证数据库中是否已经初始化过了这些数据,我顺手就敲了一个 keys abc*,其中 abc 是我要初始化的这批数据的 key 前缀(当然不是真的就是 abc,这里是随便起的一个)。
然后并没有像往常开发中一样很快给我响应,我立马就意识到不对了,这个 redis 库数据量很大,不能使用 keys 命令去查询,我随即立马按下 command + c 尝试终止,但其实已经没什么用了,因为 redis 已经在执行我刚才发送的命令,终端已经没响应,我记得我我当时还索性把终端都给关了,这纯属病急乱投医了。
随后呢我另起一个终端,尝试连接 redis,看看刚才的命令是否执行完了,但不幸的是我连都连不上,连接命令敲完没反应,当时这会心里真的慌了。
不过幸运的是这只是一次新系统正式上线前的演练,而且并没有用户在使用。至于为什么没有用户在用怎么有这么多数据在里面,那是因为在我初始化我这批数据之前,我同事已经将老系统中的数据迁移过来了。
但我还是很慌的原因是我具体不知道老系统迁移过来有多少数据,只知道数据量很大,不知道要执行多久。然后虽然说那只是新系统正式上线前一次演练而已,但是我们这次上线是有很多流程,所以才有了这次演练。
对于流程中的每个环节都要记录操作时间,下次正式上线时需要按之前演练上线的流程来执行,正式上线时如果某一环节出问题了,就要采取回滚流程进行回滚,下次上线要是因为我这个数据初始化环节出问题了,那这锅岂不背定了。
所以说当时真的是慌的一匹,但最后还是过了两三分钟之后我重新连上了,此时应该是将我刚才的命令执行完了,随后我也不管里面是不是已经初始化了,直接初始化数据,大不了覆盖好了,反正 key 是相同的,因为这部分数据只有我的程序要用到,所以没什么问题。后面正式上线时有了演练的教训,也就没有出啥问题,也没有再作死去敲 keys 命令了。
如果我们真的要在大数据量的 redis 库中查询匹配的数据怎么办呢?其实 redis 是有相应的替代命令的,就是 scan 命令,以及对应的数据类型的 sscan,hscan 和 zscan,他们都用于增量地迭代库中的元素。
- scan 命令用于迭代当前库中的所有 key
- sscan 命令用于迭代集合键中的元素
- hscan 命令用于哈希键中的键值对
- zscan 命令用于迭代有序集合中的元素(包括元素成员和元素分值)
上面的命令都是支持增量式迭代,每次执行都只会返回少量元素,这些命令每次执行的时间复杂度是 O(1),完整的一次迭代过程才是 O(N),因此它是可以在生产环境中执行的,而不会像 keys 命令一样,如果正在处理的是一个大数据量的库,会阻塞 redis 服务很久。
scan 命令它是一个基于游标的迭代器(cursor based iterator),scan 命令每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 scan 命令的游标参数, 以此来延续之前的迭代过程。当 scan 命令的游标参数被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表示一次完整的迭代过程结束。
scan 命令是迭代库中所有的 key,基本使用方式是:
1 | SCAN cursor [MATCH pattern] [COUNT count] |
而 sscan 命令、 hscan 命令和 zscan 命令的第一个参数是一个数据库键:
1 | SSCAN/HSCAN/ZSCAN key cursor [MATCH pattern] [COUNT count] |
cursor 就是游标参数,初始我们可以设置 0,开始一次迭代过程,一次迭代后的返回值作为下次迭代的游标值,当我们看到 redis 的返回值是 0 时,意味着完整的迭代过程结束。
看到这里你应该能发现,如果在迭代的中间过程中,redis 的数据发生了变化,那么我们得到的数据可能是不正确的,这是无法得到保证的。
而 scan 命令只会给我们保证在整个遍历过程中,一直存在于数据集中的元素都会被遍历返回,也就是说,如果一个元素如果在遍历开始到结束都存在于要遍历的数据集中,那么这个元素肯定会在某一次迭代过程中得到返回。
后面两个中括号中的参数是可选的,可通过 match 来指定只返回匹配对应模式的元素,而 count 可以指定每次迭代返回的元素最大数量,默认值是 10。
但要注意虽然指定了返回的元素最大数量,这个 count 参数只是用户给 scan 迭代命令的一种提示,尽管这种提示在很多情况下是有效的,但也要注意不一定每次都是返回指定的数量,有可能会比指定的要多一些。
对于 scan 以及其相关的 sscan,hscan,zscan 命令都会返回两个元素,第一个元素是下次迭代的初始游标,第二个元素是本次迭代返回的结果。
- scan 得到的结果是库中键元素 key
- sscan 得到的结果是集合中的成员
- hscan 得到的结果是键值对
- zscan 得到的结果是有序元素集合,集合中元素由一个成员和一个分值组成。
上面就是 scan 命令的大体介绍,我是觉得这个命令其实很适合清理 redis 中无效数据,通过 scan 命令扫一遍 redis 库,很容易就做到数据清理的工作。
后来想想那次小事故其实就是因为自己平时用 keys 命令用习惯了,也没多想不假思索随手就敲下去了。所以说我们在日常 redis 的使用中,不能只是简单的就想也不想的就直接 keys 命令敲下去,长期以往会出事故的,我还好不是在线上使用的系统中执行的这个命令,不然后果真的很严重。
试想如果是在线上,在 redis 阻塞的那段时间,系统的访问请求继续打到 redis 服务器上,导致 redis 服务器 CPU 直接飙涨,很有可能服务器就直接宕机了,再进一步的话,程序无法从 redis 中获取数据,所有的请求都直接一下全到数据库去了,也有可能造成数据库直接挂了。
所以说我们应该养成这样的习惯,在需要用 keys 查询 redis 中的数据时,脑海里先确认下当时的环境以及库中的数据量大小。
当然 redis 不仅仅是只有 keys 这一个比较危险的命令,还有 hgetall,lrange,smembers,zrange 等命令,这些命令的时间复杂度都是 O(N),使用这些命令都需要注意 N 的大小。当然其实线上对于 keys,flushdb,flushall 这几个命令最好是通过配置文件 redis.conf 来直接禁用它,以防万一。