Redis 常见的性能问题和优化方案

目录

Redis不同数据结构的时间复杂度

为什么单线程Redis能那么快? 

Redis 实例有哪些阻塞点?

Redis的异步子线程机制是怎么执行的?

redis优化方案

1、redis变慢了怎么办?

2、redis优化建议

3、关于热key和大key的优化

【热key】

【大key】


Redis不同数据结构的时间复杂度

按照查找的时间复杂度给redis的数据结构分下类:

一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。不管值是 String,还是集合类型,哈希桶中的元素都是指向它们的指针。

哈希表可以用 O(1) 的时间复杂度快速查找到键值对(即使有100万个key,也只需要一次计算就能找到相应的value), 但是往 Redis 中 写入大量数据后有时候会变慢,原因是哈希表的冲突问题和 rehash 可能带来的操作阻塞。

Redis 解决哈希冲突的方式,就是链式哈希。链式哈希也很容易理解,就是指同一个哈希 桶中的多个元素用一个链表来保存,它们之间依次用指针连接。

Redis 会对哈希表做 rehash 操作。rehash 也就是增加现有的哈希桶数量,让逐渐 增多的 entry 元素能在更多的桶之间分散保存,减少单个桶中的元素数量,从而减少单个 桶中的冲突。

【快速记住集合常见操作的复杂度】

  • 单元素操作是基础:单元素操作,是指每一种集合类型对单个数据实现的增删改查操作。例如,Hash 类型的 HGET、HSET 和 HDEL,Set 类型的 SADD、SREM、SRANDMEMBER 等。复杂度都是 O(1)。集合类型HMSET 增加 M 个元 素时,复杂度就从 O(1) 变成 O(M) 了。
  • 范围操作非常耗时:指集合类型中的遍历操作,可以返回集合中的所有数据,比如 Hash 类型的 HGETALL 和 Set 类型的 SMEMBERS,或者返回一个范围内的部分数据,比如 List 类型的 LRANGE 和 ZSet 类型的 ZRANGE。这类操作的复杂度一般是 O(N),比较耗时, 我们应该尽量避免。(scan实现了渐进式遍历,每次只返回有限数量的数据,效率较高)
  • 统计操作通常高效:指集合类型对集合中所有元素个数的记录,例如 LLEN 和 SCARD。这类操作复杂度只有 O(1)。
  • 例外情况只有几个:指某些数据结构的特殊记录,例如压缩列表和双向链表都会记录表头 和表尾的偏移量。这样一来,对于 List 类型的 LPOP、RPOP、LPUSH、RPUSH 这四个操 作来说,它们是在列表的头尾增删元素,这就可以通过偏移量直接定位,所以它们的复杂 度也只有 O(1),可以实现快速操作。

为什么单线程Redis能那么快? 

Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的, 但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。实际上,能够处理高并发的单线程应用不仅仅是 Redis,还有 NodeJS、Nginx 等等也是单线程。

并发访问控制一直是多线程开发中的一个难点问题,如果没有精细的设计,即使增加了线程,大部分线程也在等待获取访问共享资源的互斥锁,并行变串行,系统吞吐率并没有随着线程的增加而增加。为了避免这些问题,Redis 直接采用了单线程模式。

Redis 却能使用单线程模型达到每 秒数十万级别的处理能力,这是为什么呢?

  • Redis 的大部分操作在内存上完成;
  • 采用了高效的数据结构,例如哈希 表和跳表;
  • Redis 采用了多路复用机制,使其在网络 IO 操作中能并发处理大量的客户端请求,实现高吞吐率。Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。

简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。正因为此,Redis 可以同时和多个客户端连接并处理请求,从而提升并发性。

总结:Redis 单线程是指它对网络 IO 和数据读写的操作采用了一个线程,而采用单线程的一个核心原因是避免多线程开发的并发控制问题。单线程的 Redis 也能获得 高性能,跟多路复用的 IO 模型密切相关。

Redis 实例有哪些阻塞点?

Redis 的网络 IO 和键值对读写是由主线程完成的。那么如果在主线程上执行的操作消耗的时间太长,就会引起主线程阻塞。和 Redis 实例交互的对象,以及交互时会发生的操作:

  • 客户端:网络 IO,键值对增删改查操作,数据库操作;
  • 磁盘:生成 RDB 快照,记录 AOF 日志,AOF 日志重写;
  • 主从节点:主库生成、传输 RDB 文件,从库接收 RDB 文件、清空数据库、加载 RDB 文件;
  • 切片集群实例:向其他实例传输哈希槽信息,数据迁移。

redis中的五个阻塞点:(不可以异步的是只能在主线程执行,可以异步的是能在子线程执行)

  • 集合全量查询和聚合操作:HGETALL、SMEMBERS,以及集合的聚合统计操作,例如求交、并和差集,复杂度都是O(N)。读操作是典型的关键路径操作 —— 不可以异步。
  • bigkey 删除:对数据做删除,并不在关键路径上,可以用子线程。——可以异步。
  • 清空数据库(FLUSHDB 和 FLUSHALL):涉及到 删除和释放所有的键值对。—— 可以异步。
  • AOF 日志同步写:可以启动一个子线程来执行 AOF 日志的同步写。—— 可以异步。
  • 从库加载 RDB 文件:—— 不可以异步。(从库要想对客户端提供数据存取 服务,就必须把 RDB 文件加载完成。所以,这个操作也属于关键路径上的操作,我们必须 让从库的主线程来执行。)

不会出现阻塞的两个情况说明:

  • 网络 IO 有时候会比较慢,但是 Redis 使用了 IO 多路复用机制,避免了主线程一直处在等 待网络连接或请求到来的状态,所以,网络 IO 不是导致 Redis 阻塞的因素。
  • redis用子进程的方式生成 RDB 和 AOF,因此这两个操作不会阻塞主线程。

为了避免阻 塞式操作,Redis 提供了异步线程机制。

        Redis 会启动一些子线程,然后把一些任务交给这些子线程,让它们在后台完成,可以避免阻塞主线程。

        对于 Redis 的五大阻塞点来说,除了“集合全量查询和聚合操作”和“从库加载 RDB 文件”,其他三个阻塞点涉及的操作都不在关键路径上,所以可以使用 Redis 的异步 子线程机制来实现 bigkey 删除,清空数据库,以及 AOF 日志同步写。当你遇到 bigkey 删除时,建议先使用集合类型提供的 SCAN 命令读取数据, 然后再进行删除。因为用 SCAN 命令可以每次只读取一部分数据并进行删除,这样可以避 免一次性删除大量 key 给主线程带来的阻塞。

【AOF日志: 宕机了,Redis如何避免数据丢失? 】

AOF记录的是 Redis 收到的每一条命令,以文本形式保存的。AOF 还有一个好处:它是在命令执行后才记录日志,所以不会阻塞当前的写操作。不过,AOF 也有两个潜在的风险。

  • 如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就会丢失;
  • AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险,这是因 为AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。

AOF的三种写回策略(AOF 配置项 appendfsync 的三个可选值):

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘,可靠性高,但是对系统性能影响较大。
  • Everysec,每秒写回:每隔一秒把缓冲区中的内容写入磁盘,宕机时丢失1秒数据,性能适中。
  • No,操作系统控制的写回:由操作系统决定何时将缓冲区内容写回磁盘,宕机时丢失数据较多,性能好。

一定要小心 AOF 文件过大带来的性能问题:AOF 重写机制。比如:set k v,然后set k v2,最终只AOF记录后者。

AOF 日志由主线程写回,但是重写过程是由后台线程 bgrewriteaof 来完成的,这也是为了避免阻塞主线程。

【rdb内存快照,实现快速恢复】

Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave。RDB 文件是二进制文件。

  • save:在主线程中执行,会导致阻塞;
  • bgsave:创建一个子进程专门写入RDB 文件,避免了主线程的阻塞,这是 Redis RDB 文件生成的默认配置。

快照时数据能修改吗?  bgsave 子进程是由主线程 fork 生成的,这个操作是子进程在后台完成的,因此主线程同时可以修改数据。

关于持久化的总结:

  • 数据不能丢失时,内存快照和 AOF 的混合使用是一个很好的选择;
  • 如果允许分钟级别的数据丢失,可以只使用 RDB;
  • 如果只用 AOF,优先使用 everysec 的配置选项,因为它在可靠性和性能之间取了一个 平衡。

Redis的异步子线程机制是怎么执行的?

        Redis 主线程启动后,会使用操作系统提供的 pthread_create 函数创建 3 个子线程,分别 由它们负责 AOF 日志写操作、键值对删除以及文件关闭的异步执行。主线程通过一个链表形式的任务队列和子线程进行交互。当收到键值对删除和清空数据库的操作时,主线程会把这个操作封装成一个任务,放入到任务队列中,然后给客户端返回一个完成信息,表明删除已经完成。但实际上,这个时候删除还没有执行,等到后台子线程从任务队列中读取任务后,才开始 实际删除键值对,并释放相应的内存空间。这被称为:惰性删除。

        当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封 装成一个任务,也放到任务队列中。后台子线程读取任务后,开始自行写入 AOF 日志,这 样主线程就不用一直等待 AOF 日志写完了。

redis优化方案

1、redis变慢了怎么办?

  • 从慢查询命令开始排查,并且根据业务需求替换慢查询命令(比如:集合类和并集、交集、差集的查询);
  • 排查过期 key 的时间设置,并根据实际使用需求,设置不同的过期时间。
  • 当需要执行排序、交集、并集操作时,可以在客户端完成,而不要用 SORT、 SUNION、SINTER 这些命令,以免拖慢 Redis 实例。
  • 因为 KEYS 命令需要遍历存储的键值对,所以操作延时高。所以,KEYS 命令一般不被建议用于生产环境中(使用scan代替)。

2、redis优化建议

  • master 最好不要做持久化工作,如 RDB 内存快照和 AOF 日志文件
  • 如果数据比较重要,某个 slave 开启 AOF 备份,策略设置成每秒同步一次
  • 为了主从复制的速度和连接的稳定性,master 和 Slave 最好在一个局域网内
  • 尽量避免在压力大得主库上增加从库
  • 主从复制不要采用网状结构,尽量是线性结构,Master

3、关于热key和大key的优化

【热key】

◆ 热key占用大量的CPU资源,使其效率降低,影响其它业务
◆ 热key所在的节点访问量大,容易造成物理网卡瓶颈
◆ 超出redis承受能力后,容易造成击穿,这时大量访问打到数据库上,造成数据库瓶颈

解决办法:

◆ 对热key根据一定的规则,增加后缀,让它变成好几个key分散到不同的节点中,减少一个节点的压力,但是这样也有一定的问题,比如数据一致性问题。更新一个key变成更新多个, 业务代码也要修改,增加工作量。

◆ 对为热点key单独做集群,他们会有独立的热点key redis集群,和全量redis隔离开。当然这样产生费用也是比较大的。

◆ 使用读写分离,这样可以避免对写操作的影响,但是读写分离会有延迟的情况

◆ 本地缓存,可以客户端对热key进行收集做缓存,比如浏览器的localstorage ,安卓ios自己的数据库,然后设置一个比较短的过期时间,比如1分钟,用来解决信息的更新问题
◆ 可以系统服务来做,可以代码自己来实现,比如java的 ehcache ,也可以在系统服务的服务器上安装redis和 memcache ,这样访问热key的时候就不用和redis交互了

【大key】

比如下面这些都属于大key:

◆ string中的数据大于10K
◆ list set zset中的数据个数大于5000个
◆ 根据使用场景和业务场景来确定

大key 带来的问题:删除大key时会阻塞。解决办法:

◆ 低访问时间段删除
◆ list set zset hash可以分批次删除
◆ 使用unlink替代del命令,unlink是在异步线程中执行删除,不会阻塞主线程

【问题】假如 Redis 里面有 1 亿个 key,其中有 10万 个 key 是以某个固定的已知的前缀开头的,如何将它们全部找出来?

【回答】使用 keys 指令可以扫出指定模式的 key 列表。

【追问】如果这个 redis 正在给线上的业务提供服务,那使用 keys 指令会有 什么问题?

【回答】redis 是单线程的,keys 指令会导致线程 阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在 客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。从 Redis2.8 中开始,引入了 scan。scan 具备 keys 的功能,但是不会阻塞线程,而且可以控制每次返回的结果数。示例:scan 0 match k8* count 1000

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浮尘笔记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值