1. Redis 6 为什么引入了多线程
Redis 6 引入多线程其核心原因就是突破网络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的热点访问,速度最快
- 缺点:数据一致性难以保证,需要设置合理的过期时间或通过消息队列来通知更新。适用于业务上能容忍一定时间数据不一致的场景
- 本地缓存(JVM Cache):这是最常用、最有效的方案。在业务应用中使用Caffeine,将热Key对应的数据缓存到应用进程的内存中
- 架构层面:分散压力
- 二级缓存:如果本地缓存不合适,可以引入一个二级分布式缓存(如另一个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上,也就是分散到了集群不同的分片上
- 缺点:代码复杂度增加,数据一致性维护更麻烦
- 给热Key增加随机后缀:这是解决分布式热点问题的经典套路。在秒杀场景中,我们可以把一个热Key(如sk:1000)业务端拆成多个Key,比如sk:1000:1、sk:1000:2 … sk:1000:10
3. Redis的大Key问题如何解决
Redis的大Key问题通常指的是某个Key对应的Value值过大,从而引发一系列性能和稳定性问题。一般来说,我们通常认为:
- 一个String类型的Value值超过10KB
- 非String类型(List, Hash, Set, ZSet)的元素数量超过5000个,或者其总价值超过10MB
就可以被视为大Key。它的危害比热Key更为严重和直接,主要体现在以下几个方面:
一、大Key的巨大危害
- 客户端响应缓慢/超时:读取一个100MB的Key,网络传输和反序列化会非常耗时,直接导致客户端超时,甚至阻塞其他命令。
- 服务器内存压力不均:在集群模式下,某个分片可能因为存放大Key而导致内存使用率远高于其他节点,无法平衡数据分布。可能导致该节点OOM(内存溢出)。
- 操作阻塞,引发雪崩:使用
DEL命令删除一个包含数百万元素的Big Key会非常耗时(因为Redis是单线程的),这会阻塞主线程,导致期间所有其他请求被挂起,可能引发服务雪崩。对于列表(List)、集合(Set)的某些操作,如LRANGE key 0 -1,同样会长时间阻塞服务器。 - 网络拥塞:对大Key进行频繁读取会占用大量网络带宽,影响其他服务的网络性能。
- 数据迁移困难:在集群扩容、缩容或主从切换时,大Key会导致数据迁移时间过长,增加服务不稳定的风险。
二、如何发现大Key?
- 使用
redis-cli --bigkeys命令:这是最常用、最快速的扫描工具。它会扫描整个数据库,并统计出每种数据类型中最大的Key。缺点:它只能返回每个类型中最大的一个Key,并且扫描过程是阻塞式的,最好在从节点或低峰期执行。 - 使用开源工具:比如阿里云开源的
rdr(redis-rdb-tools),它可以离线分析RDB文件,生成非常详细的内存报告,精准找出所有大Key。这是最推荐的方式,对线上服务零影响。 - 使用云服务商控制台:阿里云、腾讯云的Redis控制台一般都自带大Key分析功能。
- 编程扫描:使用
SCAN命令编写脚本,逐步迭代所有Key,并用MEMORY USAGE或STRLEN、HLEN、LLEN等命令估算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。
- 例如一个巨大的Hash:存储了百万级字段的用户信息。可以按用户ID取模,拆分成
2. 异步删除与清理
严禁直接使用 DEL命令删除生产环境的大Key!
- 使用惰性删除:Redis 4.0及以上版本支持异步惰性删除。对于删除操作,使用
UNLINK命令代替DEL。UNLINK会将Key从 keyspace 中立即删除,但实际的内存回收会在后台线程中异步进行,不会阻塞主线程。 - 使用渐进式删除:对于更早的版本,如果需要自己删除大Key,需要编写脚本:
- 删除大Hash:使用
HSCAN加HDEL分批删除字段。 - 删除大List:使用
LTRIM每次只删除少量元素。 - 删除大Set:使用
SSCAN加SREM。 - 删除大ZSet:使用
ZSCAN加ZREM。
- 删除大Hash:使用
3. 数据结构优化(治本之策)
从业务设计上就避免产生大Key。
- 使用更高效的数据结构:
- 案例:需要存储用户是否点击过某个文章,原来用Set存储文章ID,用户量巨大时这个Set会非常大。
- 优化:改用 Bitmap,每一位代表一篇文章,可以极大地节省内存。
- 案例:需要统计独立用户数(UV)。
- 优化:使用 HyperLogLog,只需要12KB内存就能统计上亿的UV,虽有微小误差,但大多业务可接受。
- 案例:状态统计、布隆过滤器。
- 优化:使用 Bloom Filter,用很小的空间进行大规模数据的存在性判断。
4. 数据压缩与过期
- 压缩Value:如果Value是文本(如JSON、XML),可以在写入前使用GZIP、Snappy等算法进行压缩,读取时再解压。这是一种用CPU换网络和内存的策略。
- 设置合理的TTL:给数据设置过期时间,让其自动过期,避免无用数据堆积成大Key。

被折叠的 条评论
为什么被折叠?



