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

事情是这样的,前一段时间小黑哥公司生产交易偶发报错,一番排查下来最终原因是因为 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

血的教训!千万别在生产使用这些 redis 指令的更多相关文章

  1. HttpClient -- 血的教训

    HttpClient -- 血的教训 千万别用httpClient 不支持httpVersion2.0 因为这个导致项目重做

  2. 血的教训 password写成passward,教训应该从首页赋值 参数名

    血的教训 password写成passward,教训应该从首页赋值 参数名

  3. LCD屏背光驱动调试心得---血的教训

    开发板:明远智睿MY-IMX6-EK140 内核源码:linux-3.14.52 背光驱动IC:MP3202 调光原理:通过开发板的核心板PWM4引脚控制MP3202的EN脚,输出不同的占空比从而达到 ...

  4. 血的教训--如何正确使用线程池submit和execute方法

    血的教训之背景:使用线程池对存量数据进行迁移,但是总有一批数据迁移失败,无异常日志打印 凶案起因 ​ 听说parallelStream并行流是个好东西,由于日常开发stream串行流的场景比较多,这次 ...

  5. 系统环境: CentOS 64位+千万不要在生产环境中升级glibc!

    # cd /lib64# LD_PRELOAD=/lib64/libc-2.15.so ln -sf /lib64/libc-2.15.so libc.so.6 libc-2.15.so 这个文件名根 ...

  6. FPGA笔试题集锦(血的教训)

    1.名词解释: FPGA:现场可编程门阵列,一般工艺SRAM(易失性),所以要外挂配置芯片. CPLD:复杂可编程逻辑器件,一般工艺Flash(不易失). ASIC:专用集成电路 SOC:片上系统 S ...

  7. 【血的教训】玩 Ubuntu 遇到的致命问题(进不了系统)及 解决方案

    [问题1] 按照文章“U盘安装Windows 7 + Ubuntu 14 双系统笔记”在 Windows 7 基础上安装了 Ubuntu 14 系统,实现双系统切换,某一天, 通过如下命令行 sudo ...

  8. ubuntu 下修改文件访问权限chmod 777 -R *血的教训!没事别乱开权限!用谁开谁的就行。。。最后不要用这个命令,文件操作全部改用终端

    本文转自: 个人建议 Ubuntu下修改目录权限命令如下:chmod 600 name (只有所有者有读和写的权限)chmod 644 name (所有者有读和写的权限,组用户只有读的权限)chmod ...

  9. 血的教训:Protocol http not supported or disabled in libcurl

    报错显示:http not supported or disabled in libcurl 查看配置 curl -V ---------------------------------------- ...

  10. 双系统linux+win之血的教训

    绝对不要用win的软件来直接调整linux分区!!!!! 除非你不想要这个linux分区里的数据了...

随机推荐

  1. sandy bridge

      SANDY BRIDGE SPANS GENERATIONS Intel Focuses on Graphics, Multimedia in New Processor Design By Li ...

  2. 六个超大规模Hadoop(前景)

    http://cloud.zol.com.cn/441/4415033_all.html 希望自己可以尽快把Hadoop学好

  3. GCC常用命令行一览表

    GCC常用命令行一览表 这些常用的 gcc/g++ 命令行参数,你都知道么?1. gcc -E source_file.c-E,只执行到预编译.直接输出预编译结果. 2. gcc -S source_ ...

  4. Python用特殊符号切割字符串并生成list(简单)

    采用re模块,可以指定字符进行切割,例如切割IP地址: import socket import re localIP = socket.gethostbyname(socket.gethostnam ...

  5. OpenCv 2.4.9 (二) 核心函数

    前言 经过前面一节的怎样读取图片,我们可以做一些有趣的图像变换,下面我们首先介绍使用遍历的方法实现,然后我们使用内置的函数实现. 矩阵掩码实现 矩阵掩码,和卷积神经网络中的卷积类似.一个例子如下: 现 ...

  6. stl中的容器、迭代器和算法----vector中的find实现

    来源 http://blog.csdn.net/huangyimin/article/details/6133650 stl包括容器.迭代器和算法: 容器 用于管理一些相关的数据类型.每种容器都有它的 ...

  7. 手动打包MVC项目成Web Deploy包,发布至服务器

    ①确保服务器上安装了Web Deploy,可以使用微软Web Paltform Installer安装.https://www.microsoft.com/web/downloads/platform ...

  8. Velocity(7)——velocity进阶用法

    1.截取部分字段substring 1 原始字符串:$!ag.tagValue,也许很长,前端页面展示时需要截取字符串. 2 #set($str=$!ag.tagValue) 3 4 #if($str ...

  9. postcss

    一.简介 PostCSS 本身是一个功能比较单一的工具.它提供了一种方式用 JavaScript 代码来处理 CSS.它负责把 CSS 代码解析成抽象语法树结构(Abstract Syntax Tre ...

  10. Qt5中创建临时的后台线程。

    有个需求就是,GUI图形界面在上传文件到服务器的时候,需要用zip命令行打包,因为文件很多的时候,zip命令打包需要计算很长时间,所以把这样计算量大的任务分离到后台线程比较合适,然后任务完成,以信号槽 ...