哎,最近阿粉又双叒叕犯事了。git
事情是这样的,前一段时间阿粉公司生产交易偶发报错,一番排查下来最终缘由是由于 Redis 命令执行超时。github
但是使人不解的是,生产交易仅仅使用 Redis set 这个简单命令,这个命令讲道理是不可能会执行这么慢。web
那究竟是什么致使这个问题那?面试
为了找出这个问题,咱们查看分析了一下 Redis 最近的慢日志,最终发现耗时比较多命令为 keys XX*
redis
看到这个命令操做的键的前缀,阿粉才发现这是本身负责的应用。但是阿粉排查一下,虽然本身的代码并无主动去使用 keys
命令,可是底层使用框架却在间接使用,因而就有了今天这个问题。spring
问题缘由
阿粉负责的应用是一个管理后台应用,权限管理使用 Shiro 框架,因为存在多个节点,须要使用分布式 Session,因而这里使用 Redis 存储 Session 信息。数据库
❝画外音:不知道分布式 Session ,能够看看阿粉以前写的 一口气说出 4 种分布式一致性 Session 实现方式,面试杠杠的~数组
❞
因为 Shiro 并无直接提供 Redis 存储 Session 组件,阿粉不得不使用 Github 一个开源组件 shiro-redis。微信
因为 Shiro 框架须要按期验证 Session 是否有效,因而 Shiro 底层将会调用 SessionDAO#getActiveSessions
获取全部的 Session 信息。app
而 shiro-redis
正好继承 SessionDAO
这个接口,底层使用用keys
命令查找 Redis 全部存储的 Session
key。
public Set<byte[]> keys(byte[] pattern){
checkAndInit();
Set<byte[]> keys = null;
Jedis jedis = jedisPool.getResource();
try{
keys = jedis.keys(pattern);
}finally{
jedis.close();
}
return keys;
}
找到问题缘由,解决办法就比较简单了,github 上查找到解决方案,升级一下 shiro-redis
到最新版本。
在这个版本,shiro-redis
采用 scan
命令代替 keys
,从而修复这个问题。
public Set<byte[]> keys(byte[] pattern) {
Set<byte[]> keys = null;
Jedis jedis = jedisPool.getResource();
try{
keys = new HashSet<byte[]>();
ScanParams params = new ScanParams();
params.count(count);
params.match(pattern);
byte[] cursor = ScanParams.SCAN_POINTER_START_BINARY;
ScanResult<byte[]> scanResult;
do{
scanResult = jedis.scan(cursor,params);
keys.addAll(scanResult.getResult());
cursor = scanResult.getCursorAsBytes();
}while(scanResult.getStringCursor().compareTo(ScanParams.SCAN_POINTER_START) > 0);
}finally{
jedis.close();
}
return keys;
}
虽然问题成功解决了,可是阿粉内心仍是有点不解。
为何keys
指令会致使其余命令执行变慢?
为何Keys
指令查询会这么慢?
为何Scan
指令就没有问题?
Redis 执行命令的原理
首先咱们来看第一个问题,为何keys
指令会致使其余命令执行变慢?
回答这个问题,咱们首先看下 Redis 客户端执行一条命令的状况:

站在客户端的视角,执行一条命令分为三步:
-
发送命令 -
执行命令 -
返回结果
可是这仅仅客户端本身觉得的过程,可是实际上同一时刻,可能存在不少客户端发送命令给 Redis,而 Redis 咱们都知道它采用的是单线程模型。
为了处理同一时刻全部的客户端的请求命令,Redis 内部采用了队列的方式,排队执行。

因而客户端执行一条命令实际须要四步:
-
发送命令 -
命令排队 -
执行命令 -
返回结果
因为 Redis 单线程执行命令,只能顺序从队列取出任务开始执行。
只要 3 这个过程执行命令速度过慢,队列其余任务不得不进行等待,这对外部客户端看来,Redis 好像就被阻塞同样,一直得不到响应。
因此使用 Redis 过程切勿执行须要长时间运行的指令,这样可能致使 Redis 阻塞,影响执行其余指令。
KEYS 原理
接下来开始回答第二个问题,为何Keys
指令查询会这么慢?
回答这个问题以前,请你们回想一下 Redis 底层存储结构。
不太清楚朋友的也不要紧,你们能够回看一下阿粉以前的文章「阿里面试官:HashMap 熟悉吧?好的,那就来聊聊 Redis 字典吧!」。
这里阿粉复制以前文章内容,Redis 底层使用字典这种结构,这个结构与 Java HashMap 底层比较相似。

keys
命令须要返回全部的符合给定模式 pattern
的 Redis 中键,为了实现这个目的,Redis 不得不遍历字典中 ht[0]
哈希表底层数组,这个时间复杂度为 「O(N)」(N 为 Redis 中 key 全部的数量)。
若是 Redis 中 key 的数量不多,那么这个执行速度仍是也会很快。等到 Redis key 的数量慢慢更加,上升到百万、千万、甚至上亿级别,那这个执行速度就会很慢很慢。
下面是阿粉本地作的一次实验,使用 lua 脚本往 Redis 中增长 10W 个 key,而后使用 keys
查询全部键,这个查询大概会阻塞十几秒的时间。
eval "for i=1,100000 do redis.call('set',i,i+1) end" 0
❝这里阿粉使用 Docker 部署 Redis,性能可能会稍差。
❞
SCAN 原理
最后咱们来看下第三个问题,为何scan
指令就没有问题?
这是由于 scan
命令采用一种黑科技-「基于游标的迭代器」。
每次调用 scan
命令,Redis 都会向用户返回一个新的游标以及必定数量的 key。下次再想继续获取剩余的 key,须要将这个游标传入 scan 命令, 以此来延续以前的迭代过程。
简单来说,scan
命令使用分页查询 redis 。
下面是一个 scan 命令的迭代过程示例:
scan
命令使用游标这种方式,巧妙将一次全量查询拆分红屡次,下降查询复杂度。
虽然 scan
命令时间复杂度与 keys
同样,都是 「O(N)」,可是因为 scan
命令只须要返回少许的 key,因此执行速度会很快。
最后,虽然scan
命令解决 keys
不足,可是同时也引入其余一些缺陷:
-
同一个元素可能会被返回屡次,这就须要咱们应用程序增长处理重复元素功能。 -
若是一个元素在迭代过程增长到 redis,或者说在迭代过程被删除,那个这个元素会被返回,也可能不会。
以上这些缺陷,在咱们开发中须要考虑这种状况。
除了 scan
之外,redis 还有其余几个用于增量迭代命令:
-
sscan
:用于迭代当前数据库中的数据库键,用于解决smembers
可能产生阻塞问题 -
hscan
命令用于迭代哈希键中的键值对,用于解决hgetall
可能产生阻塞问题。 -
zscan
:命令用于迭代有序集合中的元素(包括元素成员和元素分值),用于产生zrange
可能产生阻塞问题。
总结
Redis 使用单线程执行操做命令,全部客户端发送过来命令,Redis 都会现放入队列,而后从队列中顺序取出执行相应的命令。
若是任一任务执行过慢,就会影响队列中其余任务的,这样在外部客户端看来,迟迟拿不到 Redis 的响应,看起来就很阻塞了同样。
因此不要在生产执行 keys
、smembers
、hgetall
、zrange
这类可能形成阻塞的指令,若是真须要执行,可使用相应的scan
命令渐进式遍历,能够有效防止阻塞问题。
< END >
若是你们喜欢咱们的文章,欢迎你们转发,点击在看让更多的人看到。也欢迎你们热爱技术和学习的朋友加入的咱们的知识星球当中,咱们共同成长,进步。
RabbitMQ 集群高可用原理及实战部署介绍
本文分享自微信公众号 - Java极客技术(Javageektech)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。