Redis时延问题,慢查询优化

通过连接池的方式实现查询优化

使用普通jedis对象和jedis连接池之间的差别

关于Redis中比较耗时的命令,禁止在线上环境使用

  keys、sort,exists等命令

  keys [pattern]模式查询 O(n) 禁用 

  通过scan模式查询 SCAN cursor [MATCH pattern] [COUNT count] 

  sort 主要对List,Set,Zset来进行排序。

  sort命令的时间复杂度是O(n+mlog(m)),其中n是排序列表(集合和有序集合)中元素的个数,m是返回元素的个数。Redis在排序前会建立一个长度为n的的容器来存储待排序元素,虽然是一个临时的过程,但是多个较大数据的排序操作则会严重影响系统的性能。(这里返回的元素的个数才是真正需要排序的,但底层到底采用的什么算法来排序的呢?
  在使用这个命令的时候:
  1.尽可能减少排序键中的元素个数,降低n
  2.使用Limit参数只获取需要的数据,降低n
  3.如果要排序的数据量较大,尽可能使用store名来缓存结果。
  对于以上问题的解决方案:
       在redis的设计中体现的非常明显,redis的纯内存操作,epoll非阻塞IO事件处理,这些快的放在一个线程中搞定,而持久化,AOF重写、Master-slave同步数据这些耗时的操作就单开一个进程来处理,不要慢的影响到快的;
        同样,既然需要使用keys这些耗时的操作,那么我们就将它们剥离出去,比如单开一个redis slave结点,专门用于keys、sort等耗时的操作,这些查询一般不会是线上的实时业务,查询慢点就慢点,主要是能完成任务,而对于线上的耗时快的任务没有影响

  exists key_name:查询key是否存在

    redis本身是key-value的形式,时间复杂度本来是O(1),但是为什么会超时呢?

       我们发现在EXISTS命令处理函数中实现了清除过期key的主动策略,会先调用expireIfNeeded函数检查要访问的key是否过期,如果过期就delete掉这个key。del命令在删除元素很多的复合数据类型(list、hash、zset、set)时是一个很耗时的操作。由于存在元素很多的zset,和ZADD一样,在删除zset时需要一个一个遍历所有元素,时间复杂度是大O(n)。由于这个删除操作在EXISTS命令的处理函数中执行,所以导致EXISTS耗时过长。

    上面主要的原因还是我们处理过期key的方式的问题,我们处理过期key:

     1.定期删除:redis默认是每隔 100ms 就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!

      2.惰性删除 :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。然后我们在做每一个操作的时候,都会执行exists keyname 的操作,如果遇到了过期key,我们会在这里面执行删除key的操作,所以非常花时间。

    针对删除大key这个问题:

   1.我们可以选择适当增加过期时间,但不能从根本上解决问题

   2.redis作者提供了解决方案,具体就是使用异步线程对大key进行删除操作,避免阻塞主线程。

 

 smembers,hgetall,lrange,smembers,zrange,mget, mset等命令:时间复杂度O(n),如果集合本身数据量非常大也会影响查询

  解决方案:

    和sort,keys等命令不一样,smembers可能是线上实时应用场景中使用频率非常高的一个命令,这里分流一招并不适合,我们更多的需要从设计层面来考虑;
    在设计时,我们可以控制集合的数量,将集合数一般保持在500个以内;

    比如原来使用一个键来存储一年的记录,数据量大,我们可以使用12个键来分别保存12个月的记录,或者365个键来保存每一天的记录,将集合的规模控制在可接受的范围;

    如果不容易将集合划分为多个子集合,而坚持用一个大集合来存储,那么在取集合的时候可以考虑使用SRANDMEMBER key [count];随机返回集合中的指定数量,当然,如果要遍历集合中的所有元素,这个命令就不适合了;

  生成RDB快照文件时,save命令会带来阻塞:

     当然我们采用bgsave来fork()一个子进程来做数据持久化的bgsave,虽然在redis底层我们采用写时复制策略copy-on-write(为子进程创建虚拟空间结构,复制父进程的虚拟空间结构,不分配物理内存,就有点像只复制地址,这样可以极大的提高redis性能,但如果对父进程有写入操作了,那么我们还是要对子进程复制父进程的物理内存,这是非常耗时的,所以在bgsave命令的时候不要对父进程写入)。

      在极端的情况下,父进程内存空间特别大,它的页表大小也会有点大,即使不复制物理内存,也可能很耗时哦。

 

redis慢查询分析

对于一个redis命令生命周期:

 

 慢查询的配置:

1.慢查询的语句会被放在一个队列里面

  slowlog-max-len这个慢查询队列的长度,这个队列放在内存中不会被持久化(需要定期持久化慢查询)

2.慢查询阈值

  slowlog-log-slower-than(微秒)当大于这个时间的时候会被放在慢查询队列里面

慢查询命令:

  slowlog get [n]:获取慢查询队列,获取前n条慢查询的数据。

  slowlog len:获取慢查询队列长度

  slowlog reset:清空慢查询队列

 对于AOF阻塞定位:

  info persistence

  有aof_delayed_fsync:100可以看到进行了多少个这样的命令。

pipeline:解决耗时操作的最简单的操作:(简单来说就是减少了客户端发送请求的数量,可以在一定程度上帮助提高性能)

  Redis 管道技术可以在服务端未响应时,客户端可以继续向服务端发送请求,并最终一次性读取所有服务端的响应,简单描述就是一次性可以发送多个请求。

  1.pipeline不是原子操作,其中的所有请求还是按原来的顺序,但中间可能插入其他的请求。

  2.pipeline每次只能作用在一个Redis节点上

没有pipline

     Jedis jedis = new Jedis("127.0.0.1",6379);
     for(int i=0;i<1000;i++) {
         jedis.hset("hashkey:"+i,"field"+i,"value"+i);
     }

使用pipline

     Jedis jedis = new Jedis("127.0.0.1",6379);
     for(int i=0;i<100;i++) {
         Pipeline pipeline = jedis.pipelined();
         for(int j=i*100;j<(i+1)*100;j++) {
             pipeline.hset("hashkey:"+i,"field"+i,"value"+i);
         }
         pipeline.syncAndReturnAll();
     }

我们也可以通过pipline高效插入:方在2.6版本推出了一个新的功能-pipe mode,即将支持Redis协议的文本文件直接通过pipe导入到服务端。

1.新建文本文件,创建redis命令

SET Key0 Value0
SET Key1 Value1
...
SET KeyN ValueN

2. 将这些命令转化成Redis Protocol。

因为Redis管道功能支持的是Redis Protocol,而不是直接的Redis命令。

3.利用管道插入:

cat data.txt | redis-cli --pipe

posted @   LeeJuly  阅读(5854)  评论(0编辑  收藏  举报
编辑推荐:
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
点击右上角即可分享
微信分享提示