大数跨境
0
0

11条提高Redis性能的技巧

11条提高Redis性能的技巧 Spring全家桶实战案例
2024-12-19
0
导读:Redis优化

Spring Boot 3实战案例合集》现已囊括超过60篇精选实战文章,并且此合集承诺将永久持续更新,为您带来最前沿的技术资讯与实践经验。欢迎积极订阅,享受不断升级的知识盛宴!订阅用户将特别获赠合集内所有文章的最终版MD文档(详尽学习笔记),以及完整的项目源码,助您在学习道路上畅通无阻。

环境:SpringBoot3.2.5



1. 简介

当系统选择Redis作为数据存储或缓存解决方案时,这通常是因为它具备卓越的性能。单个Redis节点能够处理高达10万QPS(每秒查询率)的流量,这样的高性能表现自然让人期待它能始终保持响应迅速。

然而,任何高性能系统都可能在使用中出现延迟,Redis也不例外。为了确保Redis能够持续发挥其高性能优势,我们必须关注如何避免操作延迟的发生。

为此,我们总结了以下11条建议,旨在帮助你在使用Redis时保持其高性能状态,这些建议将帮助你更好地利用Redis,确保其始终保持高效稳定的运行状态。

2. 优化建议

2.1 避免大Key

大key不仅占用过多内存,而且对Redis的性能也有很大影响。由于Redis是单线程处理请求的,当应用写入一个大key时,会在“内存分配”上消耗更多时间,此时操作延迟会增加。

同样地,在删除一个大key时,“内存释放”也会花费时间。此外,当你读取这个大key时,会在“网络数据传输”上花费更多时间。这时,后续待执行的请求会排队等待,导致Redis的性能下降。

当你无法避免大key时,你可以将大key进行分片存储。拆分为多个小key。

2.2 避免使用过于复杂的命令

Redis采用单线程模型处理请求。除了上面所说的大key在处理请求时会出现排队情况外,执行复杂度过高的命令时也会出现这种情况。

因为执行复杂度过高的命令会消耗更多的CPU资源,而主线程中的其他请求只能等待,此时就会发生排队延迟。

因此,我们应该避免执行诸如SORT、SINTER、SINTERSTORE、ZUNIONSTORE和ZINTERSTORE等聚合命令。对于这类聚合操作,我建议在客户端执行,不要让Redis承担过多的计算工作。

不论是Redis还是DB,我们都应该多加考虑下,是否在程序中处理性能反而会有所提升呢?

2.3 DEL命令时间复杂度问题

在删除键时,如果操作不当,也可能会影响Redis的性能。在删除键时,我们通常使用DEL命令。回想一下,你认为DEL命令的时间复杂度是多少?

O(1)?实际上,未必如此。

当你删除一个String类型的键时,时间复杂度确实是O(1)。但是,当你要删除的键是List/Hash/Set/ZSet类型时,其复杂度实际上是O(N),其中N代表元素的数量。也就是说,在删除键时,元素越多,DEL的执行速度就越慢!

原因是,在删除大量元素时,需要依次回收每个元素的内存。元素越多,所需时间就越长!而且,这个过程默认在主线程中执行,这不可避免地会阻塞主线程,导致性能问题。

那么,我们应该如何处理删除包含较多元素的键呢?

  • List类型:多次执行LPOP/RPOP,直到所有元素都被删除。

  • Hash/Set/ZSet类型:首先执行HSCAN/SSCAN/SCAN查询元素,然后依次执行HDEL/SREM/ZREM删除每个元素。

     

更优的解决方案

  • 对于包含较多元素的键,更好的做法可能是使用UNLINK命令代替DELUNLINK命令在Redis 4.0版本引入,它的行为类似于DEL,但它将实际的内存回收工作放到后台线程中进行,因此不会阻塞主线程。这样可以在不影响其他操作的情况下异步地释放内存。

  • 如果你需要渐进式地删除元素,比如为了控制对性能的影响,可以考虑结合SCAN命令来分批获取并删除元素,或者使用Lua脚本来批量处理,不过这仍然会在一定程度上影响性能,并且可能不如UNLINK那样直接有效。

     

2.4 启用惰性释放机制

如果你无法避免存储大键,那么建议你启用Redis的惰性释放机制。(该机制在4.0及以上版本中受支持。)

当启用此机制时,Redis在删除一个大key时,释放内存的耗时操作将在后台线程中执行,这样可以最大程度地避免对主线程的影响。

开启方式:

# 是否在内存淘汰时使用惰性释放。lazyfree-lazy-eviction yes# 是否在键过期时使用惰性释放lazyfree-lazy-expire yes# 是否在服务器端删除键时使用惰性释放lazyfree-lazy-server-del yes

注意:启用惰性释放机制可以减少大键删除或过期时对Redis主线程的阻塞,但也可能导致内存释放不及时。因此,在使用时需要权衡利弊。

2.5 执行O(N)命令时,要注意N的大小

是不是避免了使用复杂度过高的命令就可以高枕无忧了呢?答案是否定的。

当你执行O(N)命令时,同样需要注意N的大小。如果你一次性查询过多数据,在网络传输过程中也会耗费很长时间,增加操作延迟。

因此,对于容器类型(List/Hash/Set/ZSet),当元素数量未知时,切勿盲目执行如LRANGE key 0 -1、HGETALL、SMEMBERS、ZRANGE key 0 -1等命令。

在查询数据时,应遵循以下原则:

  • 首先查询数据元素的数量(涉及命令:LLEN、HLEN、SCARD、ZCARD)

  • 如果元素数量较少,可以一次性查询所有数据

  • 如果元素数量非常多,则应分批查询数据(涉及命令:LRANGE、HSCAN、SSCAN、ZSCAN)

     

2.6 使用批量命令

当你需要同时操作多个key时,应该使用批量命令来处理。与多个单独操作相比,批量操作的优势在于可以显著减少客户端与服务器之间网络I/O的往返次数。

通常采用如下建议:

  • 对于String/Hash类型,使用MGET/MSET替代GET/SET,以及HMGET/HMSET替代HGET/HSET。

  • 对于其他数据类型,使用Pipeline将多个命令打包并一次性发送给服务器执行。

     

如下,在Spring Boot中应用管道进行批量操作:

@Servicepublic class RedisPipeliningService {
private final StringRedisTemplate stringRedisTemplate ; public RedisPipeliningService(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate ; } public List<Object> pipe() { List<Object> results = stringRedisTemplate.executePipelined( new RedisCallback<Object>() { public Object doInRedis(RedisConnection connection) throws DataAccessException { StringRedisConnection stringRedisConn = (StringRedisConnection)connection; for(int i = 0; i < 10; i++) { stringRedisConn.rPush("i" + i, String.valueOf(i)) ; } return null; } }); return results ;  }}

一次IO发送多条命令。

2.7 避免Key同时过期

Redis以定时和惰性的方式清理过期键,并且这个过程是在主线程中执行的。如果你的业务中有大量的键集中过期,那么在Redis清理这些过期键时,也存在阻塞主线程的风险,还很可能出现雪崩

为了避免这种情况,在设置过期时间时,你可以添加一个随机时间,以分散这些键的过期时间,从而减少集中过期对主线程的影响。

2.8 使用长连接&合理配置连接池

你的业务应该使用长连接来操作Redis,避免使用短连接。在使用短连接操作Redis时,每次都需要进行TCP三次握手和四次挥手,这个过程会增加操作的时间消耗。

同时,你的客户端应该以连接池的方式访问Redis,并设置合理的参数。当长时间不操作Redis时,应及时释放连接资源。

如下基于 Spring Boot 配置的Redis连接池:

spring:  data:    redis:      host: localhost      port: 6379      password: xxxooo      connect-timeout: 10000      timeout: 10000      lettuce:        pool:          max-wait: -1          max-active: 8          max-idle: 8          min-idle: 8

根据业务场景合理的配置连接池大小 & 超时配置。

2.9 只使用DB 0

尽管Redis提供了16个数据库,但仅推荐你使用db 0。为什么呢?总结了以下三个原因:

  • 当在一个连接上操作多个数据库的数据时,你每次都需要先执行SELECT命令,这会给Redis带来额外的压力。

  • 使用多个数据库的目的是根据不同的业务线存储数据。那么,为什么不将它们拆分并存储在多个实例中呢?部署多个实例并拆分存储,可以使多条业务线互不影响,同时还能提高Redis的访问性能。

  • Redis Cluster仅支持db 0。如果你将来想迁移到Redis Cluster,这将增加迁移成本。

     

2.10 使用读写分离+分片集群

如果你的业务的读请求量非常大,那么你可以部署多个从数据库来实现读写分离,让Redis的从数据库共同分担读压力,从而提高性能。

如果你的业务的写请求量非常大,单个Redis实例已经无法再支持如此大的写流量,那么此时你需要使用分片集群来分担写压力。

2.11 AOF持久化策略

对于对数据丢失不敏感的业务,建议不要启用AOF,以避免AOF写入磁盘导致的Redis性能下降。

如果确实需要启用AOF,那么建议你将其配置为appendfsync everysec(每秒同步一次),并将数据持久化的磁盘刷新操作放入后台线程执行,以最大限度地减少对Redis写入磁盘时性能的影响。

以上是本篇文章的全部内容,如对你有帮助帮忙点赞+转发+收藏

推荐文章

简单!详解@RefreshScope动态刷新核心原理

避坑!为了性能Spring挖了一个大坑

优雅!Spring Boot 这样记录操作日志非常灵活强大

高级开发!Spring Boot 自定义SQL日志记录(包括, 参数,耗时),支持MyBatis,JPA等

太强了!SpringBoot结合Camel实现各种协议路由规则定义

性能优化!Spring大事务7条优化建议及示例

3个经典案例,详解Spring Boot实时推送技术

@Order注解,你理解错了!

查漏补缺!OpenFeign整合Resilience4j,你真的会用吗?

Jackson在Spring Boot高级应用技巧【Long精度丢失, @JsonValue, 数据脱敏】

考察你对 Spring 基本功掌握能力

性能提升!@Async与CompletableFuture优雅应用

自定义注解+SpEL实现强大的权限管理

实体与DTO如何转换?这个工具很厉害

【声明】内容源于网络
0
0
Spring全家桶实战案例
Java全栈开发,前端Vue2/3全家桶;Spring, SpringBoot 2/3, Spring Cloud各种实战案例及源码解读
内容 832
粉丝 0
Spring全家桶实战案例 Java全栈开发,前端Vue2/3全家桶;Spring, SpringBoot 2/3, Spring Cloud各种实战案例及源码解读
总阅读195
粉丝0
内容832