摘要.
# 前言
Redis 作为一个高性能的 key-value 数据库,在项目中我们经常会用到。尤其需要知道一些前缀的 key 值,那我们怎么去查看呢?通常情况下,Redis 中的数据都是海量的,我们访问 Redis 中的数据时,一定要做到心中有数,避免数据量超过预期时的一些事故。
# 问题产生
系统中有个项目需要登陆时统计在线用户的数量,由于使用 spring session 做了分布式 Session,把所有 session 都存储到了 Redis 中。而 spring session 又不支持获取所有在线用户的操作,所以只能查看 session 在 Redis 中的存储格式来自己统计。经过分析,spring session 在 Redis 中存储的 session 值时都是以 **【spring:session:sessions:】**作为前缀,所以我最初的解决方式是,使用 keys 命令获取所有的 session,手动统计 session 中的用户数 。
后来系统上线发现登陆操作变得非常慢,查看日志定位到问题,发现就是 keys 命令导致。
# 分析原因
后来发现线上 Redis 中 session 的数量有上百万,因为 一个用户可能会产生多个 session,导致 Redis 中实际 sesion 的数量远大于登陆用户数。
keys 命令是遍历算法,发复杂度为 O(n) ,数据量达到几百万,keys 这个命令就会耗时较长。甚至会导致 Redis 服务卡顿,假死,因为 Redis 是单线程程序,其它指令必须等到当前的 keys 指令执行完之后才可以继续。
# 解决方案
既然 keys 命令产生了性能问题,那有没有其它更好的 命令来代替呢?去 Redis 的官网查看 keys 指令的作用时,发现有一个 Warring:
Warning: consider KEYS as a command that should only be used in production environments with extreme care. It may ruin performance when it is executed against large databases. This command is intended for debugging and special operations, such as changing your keyspace layout. Don’t use KEYS in your regular application code. If you’re looking for a way to find keys in a subset of your keyspace, consider using SCAN or sets.
建议我们生产环境最好不要使用 keys 指令,应该使用 scan 或者 sets 。
查看文档我们发现 scan 命令有如下特点:
- 复杂度虽然也是 O(n),但是它是通过游标分步进行的,不会阻塞线程。
- 提供 count 参数,不是结果数量,是 Redis 单次遍历字典槽位数量 (约等于)。
- 同 keys 一样,它也提供模式匹配功能。
- 服务器不需要为游标保存状态,游标的唯一状态就是 scan 返回给客户端的游标整数。
- 返回的结果可能会有重复,需要客户端去重复,这点非常重要。
- 单次返回的结果是空的并不意味着遍历结束,而要看返回的游标值是否为零。
说白了就是 scan 命令允许增量迭代,每次调用只返回少量元素,因此可以在生产中使用它们,而不会像 keys 或者 smembers 这样的命令可能会在调用时长时间阻塞 Redis 服务器,或者返回一个超大集合,压垮压垮服务器内存。
# SCAN 命令使用
# scan 命令格式
SCAN cursor [MATCH pattern] [COUNT count]
# 命令解释
scan 游标 MATCH <返回和给定模式相匹配的元素> count 每次迭代所返回的元素数量。
SCAN 命令返回的是一个游标,从 0 开始遍历,到 0 结束遍历。
# 示例
127.0.0.1:6379[1]> scan 0 match spring:session:clinic:sessions:* count 10
1) "10"
2) 1) "spring:session:clinic:sessions:expires:a797bc05-d53b-40ad-81e8-bcac472b639e"
2) "spring:session:clinic:sessions:2ab96e94-a35a-4508-bd0c-6aa6a9d34b9c"
3) "spring:session:clinic:sessions:27c89ae9-adee-4641-9186-67b18036f540"
4) "spring:session:clinic:sessions:expires:f961a498-902a-40ec-b9e9-8179a4b9c33a"
5) "spring:session:clinic:sessions:expires:2ab96e94-a35a-4508-bd0c-6aa6a9d34b9c"
6) "spring:session:clinic:sessions:expires:580feda6-ebe8-4bed-b4f4-53b8302ac9a8"
7) "spring:session:clinic:sessions:expires:27c89ae9-adee-4641-9186-67b18036f540"
SCAN 返回值是一个包含两个值的数组:第一个值是在下一个调用中使用的新游标,第二个值是元素数组。
上面命令表示:从 0 开始遍历,返回了游标 10 ,又返回了数据,继续 scan 遍历,就要从 10 开始
需要注意的是:SCAN系列函数不保证每次调用返回的元素数量在给定范围内。这些命令也允许返回零元素,只要返回的游标不为零,客户端就不应该认为迭代完成。
# 总结
keys 和 scan 的用法我们要搞清楚,生产环境不要使用 keys,会存在安全隐患,这是我们在工作的过程经常会忽略的。
# 最后
你以为我最后 使用了 scan 统计了在线用户数??? 不不不,我还是用的 keys 命令(手动狗头),因为我换了一个 key,这个 key 的数据量就是登陆用户数,见 分布式 Session 之 Spring Session 架构与设计
什么??? 你说我不怕这个 key 的数量也会变得很大吗?是谁给我的勇气?
当然是梁… 哦不对, 是官网上有句话给了我这个勇气:
While the time complexity for this operation is O(N), the constant times are fairly low. For example, Redis running on an entry level laptop can scan a 1 million key database in 40 milliseconds.
虽然此操作的时间复杂度为 O(N),但恒定时间相当低。例如,在入门级笔记本电脑上运行的 Redis 可以在 40 毫秒内扫描 100 万个 key。