面试Redis:一次搞定 Redis 三大难题:多线程原理、热Key发现与治理、大Key拆分策略

1. Redis 6 为什么引入了多线程

Redis 6 引入多线程其核心原因就是突破网络I/O的瓶颈,充分发挥单核的性能,从而提升整体的吞吐量

为了我们更好的理解,我们对比下之前和之后的架构变化,下面的流程图清晰地展示了这一关键区别:

Redis 6 及之后多线程I/O模型
连接分发
已读出的请求
待回复的数据
主线程
接受连接
子线程池
处理网络
主线程
命令执行单线程
Redis 6 之前单线程模型
网络I/O
读取请求
命令解析
与执行
网络I/O
写入回复

Redis 6 之前的单线程并不是完全的单线程,像持久化(RDB快照、AOF 重写),异步删除(UNLINK命令)等任务是由后台线程处理的。

我们通常所说的 “单线程”是指其核心的网络I/O、命令解析与执行、数据同步等操作,是由一个主线程串行处理的

早期采用这种设计主要有一下几点:

  • 避免锁竞争:单线程不存在并发控制问题,简化了实现,保证了原子性。
  • 性能极高:在内存操作下,单线程处理速度已经非常快,瓶颈往往出现在网络IO上,而不是CPU
  • 简单稳定: 模型简单,已于开发和维护

在Redis6 需要做出改变,其主要是因为出现了性能瓶颈,随着网络硬件的发展,以及应用对超高吞吐零的需求,瓶颈发生了变化:瓶颈从CPU 转移到了网络IO

在一个告诉网络环境中,单个线程需要独立完成所有的事情:

  • 从网络套接字中读取五客户端的请求数据
  • 解析命令
  • 执行命令
  • 将响应数据写入网络套接字,发回给客户端。

其中,步骤1与步骤3(网络IO)是阻塞操作,虽然使用了像epoll这样的I/O多路复用来管理大量的连接,但是数据的读写本身(数据从网卡拷贝到内存,或从内存拷贝到网卡)需要消耗CPU的时间。

在单线程模型下,当连接数非常多或流量巨大时,这个主线程就会花费大量的时间在数据的读写上,而真正执行命令的时间被挤压,导致CPU 无法被充分利用,吞吐量上不去,综上所述,在Redis 6 之后就引入了多线程。而这种解决方案非常巧妙,它并不是把命令执行改成了多线程,而是采用了"多线程I/O,单线程命令处理"的混合模型,具体来说如下:

  • 主线程负责接收连接,然后它将网络读(读取数据请求)和网络写(发送回复数据)这两个最耗时的I/O任务,交给一组独立的I/O线程并行处理
  • 当I/O网络把请求数据读好后,主线程依然会单线程地、按顺序地执行所有命令。这完美保留了Redis 单线程执行,无锁竞争的核心优势。
  • 命令执行完毕后,主线程再将回复的数据交给I/O线程去并行写回网络

当然还有一个非常重要的点,这个功能默认是关闭的,需要配置io-threads 和 io-threads-do-reads。因为对于连接数不多的场景,开启了多线程反而可能会因线程切换导致出现额外开销。它的主要目标是提高高并发场景下的吞吐量,而不是降低单个请求的延迟。

2. Redis 的热Key问题如何解决

关于热Key的问题,可以理解为:当某个Key的访问频率显著高于其他Key,导致大量请求其中打到一个Redis实例的某个分片上,从而造成该分片CPU、网络带宽等资源耗尽,引发性能瓶颈或服务不可用

热Key的危害非常大,主要体现在:

  • 流量倾斜,服务器压力大:大量请求集中在一台Redis服务器上,可能打满其CPU和网络带宽
  • 连接数耗尽:单个实例的连接数有上限,可能导致无法建立新的连接
  • 缓存击穿风险:如果热Key突然过期,海量请求会直接穿透到数据库,瞬间可能压垮数据库

如何发现热Key

  • 使用Redis自带的命令
    • redis-cli --hotkeys:Redis4.0以上版本支持,可以快速找到当前实例的热Key
    • monitor命令:可以实时打印出所有命令,但是性能损耗极大,严禁在生产环境长期使用,仅用于临时排查
  • 借助开源工具:比如美团开源的RedisFullCheck等工具可以进行热点分析
  • 通过代理层或者客户端监控
  • 云端服务商提供的功能:阿里云、腾讯云等云数据库Redis版一般都自带热Key的监控功能

如何解决热Key问题

解决方案需要根据具体的情况分层级、分阶段地进行

  • 业务层面:最简单的方案往往最有效
    • 本地缓存(JVM Cache):这是最常用、最有效的方案。在业务应用中使用Caffeine,将热Key对应的数据缓存到应用进程的内存中
      • 优点:彻底解决对Redis的热点访问,速度最快
      • 缺点:数据一致性难以保证,需要设置合理的过期时间或通过消息队列来通知更新。适用于业务上能容忍一定时间数据不一致的场景
  • 架构层面:分散压力
    • 二级缓存:如果本地缓存不合适,可以引入一个二级分布式缓存(如另一个Redis集群、Memcached),将热Key数据备一份,查询时先随机访问二级缓存。
    • 使用Redis集群代理:像tweproxy或predixy这样的代理,可以支持在配置中指定一个热Key,并将自动复制到多个Redis实例上(多个副本),查询时随机访问一个副本,将压力分散
    • 读写分离:如果热Key是读多写少,可以利用Redis集群的从节点做读写分离,将读请求分散到多个从节点。
  • 代码层面调优:优化访问模式
    • 批处理(MGet/Mset)或管道(Pipeline):如果需要对热Key需要多次操作,可以使用批处理或管道减少网络开销,但这对解决并发访问压力的帮助有限
    • 避免使用大Key:热Key如果同时是大Key(Value很大),危害会加倍。需要优化数据结构,避免Value过大。
  • 终极方案:直接修改热Key
    • 给热Key增加随机后缀:这是解决分布式热点问题的经典套路。在秒杀场景中,我们可以把一个热Key(如sk:1000)业务端拆成多个Key,比如sk:1000:1、sk:1000:2 … sk:1000:10
      • 写数据时:同时更新这10个Key(或通过广播方式)
      • 读数据时:随机从10个Key中选一个来读,这样就将请求压力分散到10个不同的Key上,也就是分散到了集群不同的分片上
      • 缺点:代码复杂度增加,数据一致性维护更麻烦

3. Redis的大Key问题如何解决

Redis的大Key问题通常指的是某个Key对应的Value值过大,从而引发一系列性能和稳定性问题。一般来说,我们通常认为:

  • 一个String类型的Value值超过10KB
  • 非String类型(List, Hash, Set, ZSet)的元素数量超过5000个,或者其总价值超过10MB

就可以被视为大Key。它的危害比热Key更为严重和直接,主要体现在以下几个方面:

一、大Key的巨大危害
  1. 客户端响应缓慢/超时:读取一个100MB的Key,网络传输和反序列化会非常耗时,直接导致客户端超时,甚至阻塞其他命令。
  2. 服务器内存压力不均:在集群模式下,某个分片可能因为存放大Key而导致内存使用率远高于其他节点,无法平衡数据分布。可能导致该节点OOM(内存溢出)。
  3. 操作阻塞,引发雪崩:使用 DEL命令删除一个包含数百万元素的Big Key会非常耗时(因为Redis是单线程的),这会阻塞主线程,导致期间所有其他请求被挂起,可能引发服务雪崩。对于列表(List)、集合(Set)的某些操作,如 LRANGE key 0 -1,同样会长时间阻塞服务器。
  4. 网络拥塞:对大Key进行频繁读取会占用大量网络带宽,影响其他服务的网络性能。
  5. 数据迁移困难:在集群扩容、缩容或主从切换时,大Key会导致数据迁移时间过长,增加服务不稳定的风险。
二、如何发现大Key?
  1. 使用 redis-cli --bigkeys命令:这是最常用、最快速的扫描工具。它会扫描整个数据库,并统计出每种数据类型中最大的Key。缺点:它只能返回每个类型中最大的一个Key,并且扫描过程是阻塞式的,最好在从节点或低峰期执行。
  2. 使用开源工具:比如阿里云开源的 rdr(redis-rdb-tools),它可以离线分析RDB文件,生成非常详细的内存报告,精准找出所有大Key。这是最推荐的方式,对线上服务零影响。
  3. 使用云服务商控制台:阿里云、腾讯云的Redis控制台一般都自带大Key分析功能。
  4. 编程扫描:使用 SCAN命令编写脚本,逐步迭代所有Key,并用 MEMORY USAGESTRLENHLENLLEN等命令估算Key的大小。
三、如何解决与优化大Key问题?

发现大Key后,我们不能简单地一删了之,需要根据其类型和业务场景选择合适的方案。

1. 拆分(最根本、最常用的方案)

将一个大Key拆分成多个小Key。

  • String类型:如果一个大的String存储的是用户信息,可以将其拆分成多个Hash结构的Key,例如 user:1000:base_info, user:1000:ext_info。或者直接按业务逻辑拆分,如 content:1000:part1, content:1000:part2
  • Hash/List/Set/ZSet类型:将存有大量元素的Key按一定规则分片。
    • 例如一个巨大的Hash:存储了百万级字段的用户信息。可以按用户ID取模,拆分成 user_info:1, user_info:2, … user_info:10
    • 例如一个巨大的List:存储了消息流水。可以按时间拆分,如 message_list:202310, message_list:202311

2. 异步删除与清理

严禁直接使用 DEL命令删除生产环境的大Key!

  • 使用惰性删除:Redis 4.0及以上版本支持异步惰性删除。对于删除操作,使用 UNLINK命令代替 DELUNLINK会将Key从 keyspace 中立即删除,但实际的内存回收会在后台线程中异步进行,不会阻塞主线程
  • 使用渐进式删除:对于更早的版本,如果需要自己删除大Key,需要编写脚本:
    • 删除大Hash:使用 HSCANHDEL分批删除字段。
    • 删除大List:使用 LTRIM每次只删除少量元素。
    • 删除大Set:使用 SSCANSREM
    • 删除大ZSet:使用 ZSCANZREM

3. 数据结构优化(治本之策)

从业务设计上就避免产生大Key。

  • 使用更高效的数据结构
    • 案例:需要存储用户是否点击过某个文章,原来用Set存储文章ID,用户量巨大时这个Set会非常大。
    • 优化:改用 Bitmap,每一位代表一篇文章,可以极大地节省内存。
    • 案例:需要统计独立用户数(UV)。
    • 优化:使用 HyperLogLog,只需要12KB内存就能统计上亿的UV,虽有微小误差,但大多业务可接受。
    • 案例:状态统计、布隆过滤器。
    • 优化:使用 Bloom Filter,用很小的空间进行大规模数据的存在性判断。

4. 数据压缩与过期

  • 压缩Value:如果Value是文本(如JSON、XML),可以在写入前使用GZIP、Snappy等算法进行压缩,读取时再解压。这是一种用CPU换网络和内存的策略。
  • 设置合理的TTL:给数据设置过期时间,让其自动过期,避免无用数据堆积成大Key。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

漠然~~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值