哎,最近小黑哥又双叒叕犯事了。
事件是这样的,前一段时间小黑哥公司生产交易偶发报错,一番排查下来最终起因是因为 Redis 命令执行超时。
可是令人不解的是,生产交易仅仅应用 Redis set 这个简略命令,这个命令讲道理是不可能会执行这么慢。
那到底是什么导致这个问题那?
为了找出这个问题,咱们查看剖析了一下 Redis 最近的慢日志,最终发现耗时比拟多命令为 keys XX*
看到这个命令操作的键的前缀,小黑哥才发现这是本人负责的利用。可是小黑哥排查一下,尽管本人的代码并没有被动去应用 keys
命令,然而底层应用框架却在间接应用,于是就有了明天这个问题。
问题起因
小黑哥负责的利用是一个治理后盾利用,权限治理应用 Shiro 框架,因为存在多个节点,须要应用分布式 Session,于是这里应用 Redis 存储 Session 信息。
画外音:不晓得分布式 Session,能够看看小黑哥之前写的 一口气说出 4 种分布式一致性 Session 实现形式,面试杠杠的~
因为 Shiro 并没有间接提供 Redis 存储 Session 组件,小黑哥不得不应用 Github 一个开源组件 shiro-redis。
因为 Shiro 框架须要定期验证 Session 是否无效,于是 Shiro 底层将会调用 SessionDAO#getActiveSessions
获取所有的 Session 信息。
而 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
命令渐进式遍历,能够无效避免阻塞问题。
欢送关注我的公众号:程序通事,取得日常干货推送。如果您对我的专题内容感兴趣,也能够关注我的博客:studyidea.cn