乐趣区

关于java:血的教训千万别在生产使用这些-redis-指令

哎,最近小黑哥又双叒叕犯事了。

事件是这样的,前一段时间小黑哥公司生产交易偶发报错,一番排查下来最终起因是因为 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 客户端执行一条命令的状况:

站在客户端的视角,执行一条命令分为三步:

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

然而这仅仅客户端本人认为的过程,然而实际上同一时刻,可能存在很多客户端发送命令给 Redis,而 Redis 咱们都晓得它采纳的是单线程模型。

为了解决同一时刻所有的客户端的申请命令,Redis 外部采纳了队列的形式,排队执行。

于是客户端执行一条命令理论须要四步:

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

因为 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 的响应,看起来就很阻塞了一样。

所以不要在生产执行 keyssmembershgetallzrange这类可能造成阻塞的指令,如果真须要执行,能够应用相应的scan 命令渐进式遍历,能够无效避免阻塞问题。

欢送关注我的公众号:程序通事,取得日常干货推送。如果您对我的专题内容感兴趣,也能够关注我的博客:studyidea.cn

退出移动版