Redis 8.0发布之后,我们团队把几个核心业务的Redis集群升级到了新版本。

升级之后,整体性能有提升,但也遇到了一些性能问题。有一个业务场景,Redis的响应时间突然变慢,从原来的1毫秒以内变成了几十毫秒,严重影响了业务。

我负责了这次性能优化的工作。经过一周的排查和优化,我们把这个业务场景的Redis响应时间从几十毫秒降到了1毫秒以内,吞吐量也提升了好几倍。

这篇文章我想分享一下这次Redis 8.0性能优化的实战经验。从慢查询定位到参数调优,从数据结构优化到集群架构,聊聊如何把Redis的性能从慢优化到快。如果你也在用Redis,或者遇到了Redis性能问题,希望这篇文章能给你一些参考。

先说明一下,Redis 8.0是比较新的版本,本文提到的一些特性和参数,可能在后续版本中会有变化。但Redis性能优化的基本思路和方法,应该是通用的。

问题背景

先说说问题的背景。

我们有一个核心业务,用Redis做缓存和计数。这个业务的QPS很高,峰值能到每秒十几万次操作。之前用Redis 7.0的时候,性能一直很稳定,响应时间在1毫秒以内,没有出现过性能问题。

升级到Redis 8.0之后,刚开始一切正常。但运行了几天之后,这个业务的Redis响应时间突然变慢了。从监控上看,平均响应时间从1毫秒变成了20多毫秒,峰值的时候甚至超过了100毫秒。

响应时间变慢,直接影响了业务。用户的请求延迟变高,页面加载变慢,甚至出现了超时错误。业务方反馈很强烈,要求我们尽快解决。

我们第一时间开始排查。刚开始以为是Redis 8.0的bug,或者是升级过程中出了问题。但经过仔细排查,发现不是版本的问题,而是我们的使用方式和数据结构,在高并发和大数据量的情况下,出现了性能瓶颈。

Redis 8.0本身的性能是很好的,甚至比7.0有提升。但如果使用不当,再好的版本也会慢。

第一步:监控和定位问题

做性能优化的第一步,不是上来就改配置,而是先监控和定位问题。

我们用了几个工具来定位问题。

第一个工具是Redis的慢查询日志。Redis有一个slowlog功能,能记录执行时间超过阈值的命令。我们把慢查询阈值设为1毫秒,然后查看慢查询日志,看看哪些命令执行得慢。

查看慢查询日志之后,我们发现了几个问题。有一些HGETALL命令,执行时间超过了10毫秒。还有一些KEYS命令,执行时间超过了50毫秒。另外,有一些SORT命令,执行时间也比较长。

这些慢查询,就是导致响应时间变慢的主要原因。

第二个工具是Redis的INFO命令。INFO命令能查看Redis的各种统计信息,比如内存使用、连接数、命中率、CPU使用率等。我们查看了INFO信息,发现了几个异常。

内存使用率很高,达到了80%以上。连接数也很高,峰值的时候超过了最大连接数的限制。CPU使用率也很高,特别是单核CPU,经常达到100%。

第三个工具是Redis的MONITOR命令。MONITOR命令能实时查看Redis执行的所有命令。我们用MONITOR看了一段时间,发现了一些问题。有一些客户端,频繁地执行KEYS命令,每次都扫描整个数据库。还有一些客户端,执行大量的HGETALL,每次都读取整个哈希表。

第四个工具是业务代码审查。我们审查了业务代码,发现了一些不合理的使用方式。比如,用Redis的哈希结构存储了大量的字段,一个哈希有几万个字段;用KEYS命令来做模糊查询;在循环中频繁地执行Redis命令,没有用批量操作。

通过这些工具和审查,我们定位了问题的根源。主要有几个方面:数据结构不合理、命令使用不当、参数配置不优、集群架构有瓶颈。

第二步:数据结构优化

定位了问题之后,我们开始针对性地优化。第一个优化方向是数据结构。

Redis的性能,很大程度上取决于数据结构的选择。不同的数据结构,适合不同的场景。如果数据结构选错了,性能会差很多。

我们发现的第一个问题是,哈希结构存储了太多的字段。有一个业务,用一个哈希存储了用户的所有信息,一个哈希有几万个字段。每次用HGETALL读取这个哈希,都要读取几万个字段,非常耗时。

我们的优化方法是:拆分哈希。把一个大的哈希,拆分成多个小的哈希。比如,按用户ID的前缀来拆分,每个哈希只存储几百个用户。这样每次读取的字段数大大减少,HGETALL的执行时间从几十毫秒降到了1毫秒以内。

另外,我们还评估了是否真的需要用HGETALL。很多时候,业务只需要读取其中的几个字段,不需要读取整个哈希。我们把一些HGETALL改成了HMGET,只读取需要的字段,性能提升也很明显。

第二个问题是,用列表结构做了不适合的事情。有一个业务,用列表存储了消息队列,然后用LRANGE来读取所有消息,再在业务代码中过滤。列表的长度有几万条,每次LRANGE都要读取大量数据,非常慢。

我们的优化方法是:换用更合适的数据结构。消息队列的场景,用列表做队列是可以的,但不应该用LRANGE读取所有消息再过滤。我们改成了用LPOP/RPOP来消费消息,或者用有序集合来存储需要排序和过滤的消息。

另外,我们还引入了Redis的Stream数据结构。Stream是Redis 5.0引入的,专门用来做消息队列,比列表更适合。我们把一些消息队列的场景,从列表迁移到了Stream,性能和功能都有提升。

第三个问题是,用有序集合存储了过多的成员。有一个业务,用有序集合存储排行榜,一个集合有几百万个成员。每次插入和删除,都要更新排序,性能很差。

我们的优化方法是:分片。把一个大的有序集合,拆分成多个小的有序集合。比如,按分数范围来分片,每个分片只存储一定分数范围的成员。这样每个集合的成员数大大减少,操作性能提升很多。查询的时候,根据分数范围查询对应的分片,再合并结果。

第四个问题是,字符串值太大。有一个业务,用字符串存储了大的JSON对象,一个值有几MB。每次读取这个值,都要传输大量数据,网络带宽和Redis的性能都受到影响。

我们的优化方法是:拆分大对象。把一个大的JSON对象,拆分成多个小的字段,用哈希来存储。或者把大对象压缩之后再存储,减少数据量。另外,评估是否真的需要把整个对象存在Redis里,有些不常用的字段,可以存在数据库里,只把热数据存在Redis里。

第三步:命令使用优化

第二个优化方向是命令使用。

Redis的命令很多,不同的命令性能差异很大。如果用了不恰当的命令,或者用了性能很差的命令,会严重影响性能。

我们发现的第一个问题是,用KEYS命令做模糊查询。KEYS命令会遍历整个数据库,性能很差,数据量大的时候会阻塞Redis。我们的业务代码中,有几处用了KEYS来做模糊查询,每次执行都要几十毫秒。

我们的优化方法是:用SCAN代替KEYS。SCAN命令是迭代式的,不会阻塞Redis,每次只返回一部分结果。虽然SCAN也需要遍历整个数据库,但它是分多次进行的,不会长时间阻塞Redis。

另外,我们还评估了是否真的需要模糊查询。很多时候,可以用更合适的数据结构来代替模糊查询。比如,用集合或者有序集合来存储需要查询的键,然后直接读取,不需要模糊匹配。

第二个问题是,循环中执行单个命令。有一些业务代码,在循环中频繁地执行Redis命令,比如循环1000次,每次执行一个GET。这样做的性能很差,因为每次命令都有网络往返的开销。

我们的优化方法是:用批量操作。把多个命令合并成一个批量操作,比如用MGET来批量读取多个键,用MSET来批量写入,用Pipeline来批量执行任意命令。批量操作能大大减少网络往返的次数,提升性能。

我们把循环中的单个GET改成了MGET,性能提升了几十倍。原来需要1秒的操作,现在只需要几十毫秒。

第三个问题是,用了性能差的命令。比如,用SORT命令对大量数据排序,用SUNIONSTORE计算大集合的并集,用HGETALL读取大哈希等。这些命令在数据量大的时候,性能很差。

我们的优化方法是:避免在大数据量上使用这些命令,或者换用更高效的方式。比如,需要排序的数据,提前用有序集合存储好,查询的时候直接读取,不需要用SORT命令。需要计算集合交集并集的,在写入的时候就计算好,存储结果,查询的时候直接读取。

第四个问题是,没有用连接池。有一些业务代码,每次执行Redis命令都新建一个连接,执行完就关闭。这样做的性能很差,因为建立连接需要时间。

我们的优化方法是:用连接池。连接池能复用连接,避免频繁地建立和关闭连接。我们用的Redis客户端库,都支持连接池,配置一下就好了。用了连接池之后,连接建立的开销大大减少,性能有提升。

第四步:参数配置优化

第三个优化方向是参数配置。

Redis的默认配置,是通用的配置,不一定适合所有场景。根据业务场景,调整一些关键参数,能显著提升性能。

我们调整的第一个参数是最大内存和淘汰策略。Redis的默认最大内存是不限制的,如果内存用满了,会导致性能下降甚至OOM。我们根据服务器的内存大小,设置了合理的最大内存,并且选择了合适的淘汰策略。

我们的业务是缓存场景,所以选择了allkeys-lru淘汰策略,也就是在所有键中,淘汰最近最少使用的键。这样能保证热数据留在内存中,提高缓存命中率。

调整之后,内存使用率稳定在合理的范围,不会因为内存满了而影响性能。

第二个参数是慢查询阈值。Redis的默认慢查询阈值是10毫秒,对于我们的高并发场景来说,这个阈值太大了。我们把阈值改成了1毫秒,这样能更及时地发现慢查询。

发现慢查询之后,及时优化,避免慢查询积累导致性能问题。

第三个参数是持久化配置。Redis的RDB和AOF持久化,会对性能有一定影响。特别是RDB的fork操作,在内存大的时候,会消耗大量CPU和内存。

我们根据业务的特点,调整了持久化策略。对于可以容忍少量数据丢失的缓存场景,我们关闭了AOF,只保留RDB,并且把RDB的保存频率调低,减少fork的次数。对于不能容忍数据丢失的场景,我们用AOF,但选择了everysec策略,平衡性能和安全性。

调整之后,持久化对性能的影响大大减少。

第四个参数是网络相关的参数。比如,tcp-backlog、timeout、tcp-keepalive等。我们根据高并发的场景,调整了这些参数,提高了网络处理能力。

比如,把tcp-backlog从默认的511改成了1024,提高了连接队列的大小,避免高并发时连接被拒绝。把timeout从默认的0改成了300秒,自动关闭空闲连接,避免连接数过多。

第五个参数是内存优化相关的参数。比如,hash-max-ziplist-entries、hash-max-ziplist-value、list-max-ziplist-size等。这些参数控制了小数据量时使用的紧凑编码,能节省内存。

我们根据业务的数据特点,调整了这些参数,让更多的小数据使用紧凑编码,节省内存。内存节省了,Redis的性能也会提升,因为内存越小,缓存命中率越高,GC(虽然Redis没有GC,但内存回收)的压力也越小。

第五步:集群架构优化

第四个优化方向是集群架构。

当单台Redis的性能不够的时候,就需要用集群来横向扩展。我们的业务,单台Redis的CPU已经成为瓶颈,所以我们用了Redis Cluster集群。

但集群架构如果设计不好,也会有性能问题。我们发现了几个问题。

第一个问题是,热点键集中在一个节点。有一些热门的键,访问量特别大,都集中在一个节点上,导致这个节点的CPU很高,成为瓶颈。

我们的优化方法是:热点键拆分。把一个热点键,拆分成多个子键,分布在不同的节点上。比如,把一个计数器拆成10个子计数器,写入的时候随机选一个,读取的时候把10个加起来。这样访问量就分散到了10个节点上,不会集中在一个节点。

第二个问题是,大键导致节点不均衡。有一些键的值特别大,占用了大量内存,导致这个节点的内存使用率很高,而其他节点的内存使用率很低。内存不均衡,会导致某些节点先达到内存上限,影响整个集群的性能。

我们的优化方法是:拆分大键。把大的键拆分成多个小的键,分布在不同的节点上。这样内存分布更均衡,每个节点的负载也更均衡。

第三个问题是,跨节点的批量操作。Redis Cluster不支持跨节点的批量操作,比如MGET,如果键分布在不同的节点上,就需要客户端分别请求多个节点,然后合并结果。这样性能会比单节点差。

我们的优化方法是:合理设计键的分布。把需要一起批量操作的键,设计成相同的hash tag,这样它们会分布在同一个节点上。比如,把同一个用户的所有键,都加上{user_id}的hash tag,这样它们都在同一个节点上,批量操作的时候只需要请求一个节点。

第四个问题是,集群节点数不够。我们的集群只有3个主节点,高并发的时候,每个节点的CPU都很高。

我们的优化方法是:增加节点数。把集群从3个主节点扩展到6个主节点,每个节点的负载降低了一半,性能提升明显。扩展节点的时候,用Redis Cluster的resharding功能,在线迁移数据,不需要停机。

第六步:业务层面优化

第五个优化方向是业务层面。

很多时候,Redis的性能问题,根源在业务层面。比如,缓存命中率低、缓存穿透、缓存击穿、缓存雪崩等。

我们发现的第一个问题是,缓存命中率低。有一些业务,缓存设计得不好,导致缓存命中率很低,大量请求都打到了数据库,同时也给Redis带来了很大的压力。

我们的优化方法是:优化缓存策略。分析哪些数据是热数据,哪些是冷数据,把热数据放在Redis里,冷数据放在数据库里。设置合理的过期时间,不要太短也不要太长。用布隆过滤器来防止缓存穿透,用互斥锁来防止缓存击穿,用随机过期时间来防止缓存雪崩。

优化之后,缓存命中率从60%提升到了95%,Redis的压力大大减少,数据库的压力也减少了。

第二个问题是,不必要的Redis操作。有一些业务代码,执行了很多不必要的Redis操作。比如,每次请求都读取相同的配置,而这些配置很少变化。比如,在循环中重复读取相同的键。

我们的优化方法是:减少不必要的操作。把很少变化的配置,放在本地缓存里,不需要每次都读Redis。在循环中,把相同的键合并成批量操作,或者缓存结果,避免重复读取。

第三个问题是,没有合理设置过期时间。有一些键,没有设置过期时间,永远不会过期,导致内存越用越多。还有一些键,过期时间设置得不合理,太长或者太短。

我们的优化方法是:合理设置过期时间。所有的缓存键,都要设置过期时间,根据数据的更新频率和重要性,设置合理的过期时间。定期清理无用的键,释放内存。

第四个问题是,读写分离不够。我们的集群,所有的请求都打到主节点上,从节点只用来备份,没有承担读请求。主节点的CPU很高,而从节点的CPU很低。

我们的优化方法是:读写分离。把读请求分散到从节点上,主节点只处理写请求。这样主节点的压力大大减少,整个集群的吞吐量提升。我们用的Redis客户端,支持读写分离的配置,改一下配置就好了。

优化效果

经过以上几个方面的优化,我们的Redis性能有了显著的提升。

响应时间方面,平均响应时间从20多毫秒降到了0.8毫秒,降低了95%以上。峰值响应时间从100多毫秒降到了5毫秒以内。

吞吐量方面,集群的QPS从每秒十几万提升到了每秒五十多万,提升了3倍多。

内存方面,通过数据结构优化和合理的过期时间,内存使用率从80%降到了50%,有了更多的余量。

稳定性方面,通过参数优化和集群架构优化,Redis的运行更稳定了,很少出现慢查询和超时。

业务方的反馈也很好,用户的请求延迟大大降低,页面加载速度提升,超时错误消失了。

这次性能优化,让我们对Redis 8.0有了更深的理解。Redis 8.0本身的性能是很好的,但需要合理地使用和配置,才能发挥出它的最佳性能。

经验总结

最后总结一下这次Redis性能优化的经验。

第一,先监控定位,再动手优化。不要上来就改配置,先用工具定位问题,找到瓶颈在哪里,再针对性地优化。盲目优化,可能花了很多时间,效果却不好。

第二,数据结构是关键。Redis的性能,很大程度上取决于数据结构的选择。选对数据结构,性能会好很多;选错了,再怎么调参数也没用。要根据业务场景,选择最合适的数据结构。

第三,避免使用性能差的命令。比如KEYS、HGETALL大哈希、SORT大数据量等。这些命令在数据量大的时候,会严重影响性能。用更高效的命令或者方式来代替。

第四,批量操作和连接池。批量操作能减少网络往返,连接池能减少连接建立的开销。这两个是提升Redis性能的基本操作,一定要做好。

第五,合理配置参数。根据业务场景,调整最大内存、淘汰策略、持久化、网络等参数。默认配置不一定适合所有场景,需要根据实际情况调整。

第六,集群架构要设计好。用集群的时候,要注意热点键、大键、跨节点操作等问题。合理设计键的分布,扩展节点数,读写分离,都能提升集群性能。

第七,业务层面优化。很多Redis性能问题,根源在业务层面。优化缓存策略,减少不必要的操作,合理设置过期时间,能从根本上减少Redis的压力。

写在最后

Redis是一个优秀的内存数据库,性能很好,但如果使用不当,也会出现性能问题。

这次Redis 8.0的性能优化,让我们从慢到快,解决了业务的性能问题。整个过程,从监控定位到数据结构优化,从命令优化到参数调优,从集群架构到业务层面,每个环节都很重要。

如果你也遇到了Redis的性能问题,希望这篇文章的经验能给你一些参考。记住,性能优化没有银弹,需要根据实际情况,具体问题具体分析。

最后用一句话来结束这篇文章:"Redis性能优化,三分靠版本,七分靠使用。"

愿你能用好Redis,让你的应用又快又稳。