1. 项目缘起:为什么是SpringBoot + Lettuce + Redis?
如果你正在开发一个Java Web应用,尤其是基于SpringBoot的,那么引入Redis作为缓存或数据存储几乎是标配。但当你打开SpringBoot的官方文档,或者搜索“SpringBoot集成Redis”时,大概率会看到两种客户端:Jedis和Lettuce。几年前,Jedis凭借其简单直接、社区成熟,是很多人的首选。但如今,尤其是在SpringBoot 2.x及更高版本中,Lettuce已经成为了默认的Redis客户端。这不是一个随意的选择,背后有非常实际的技术考量。
我最初接触Lettuce时也心存疑虑,毕竟Jedis用惯了。但经过几个高并发项目的实战洗礼,尤其是在处理连接池管理、异步支持和资源消耗等问题上,Lettuce的优势就非常明显了。简单来说,Jedis采用的是阻塞式I/O,每个连接实例在任意时刻只能处理一个操作,要支持并发就需要依赖连接池。而Lettuce底层基于Netty,是一个高性能、非阻塞的客户端,一个连接实例就可以处理大量的并发请求,并且原生支持响应式编程模型。
对于现代微服务架构,特别是那些对响应延迟和资源利用率有要求的场景,Lettuce几乎是更优解。SpringBoot团队将其设为默认,也代表了技术栈演进的方向。所以,今天这篇内容,我就从一个实际开发者的角度,带你从零开始,手把手完成SpringBoot与Lettuce的集成,并分享几个我踩过坑的真实案例,让你不仅能“跑起来”,更能“用得好”。
2. 环境准备与基础依赖配置
在开始写代码之前,我们需要把项目的基础架子搭好。这里假设你已经有了一个SpringBoot项目(我用的版本是3.x,但2.7.x及以上版本的核心配置是类似的)。整个过程的核心,其实就是pom.xml(或Gradle构建文件)和application.yml(或application.properties)这两个文件。
2.1 Maven依赖的精准引入
打开你的pom.xml文件。很多人会直接引入spring-boot-starter-data-redis,这没错,但我们需要理解它背后带来了什么。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>引入这个starter后,SpringBoot会自动帮我们引入几个核心的库:
spring-data-redis: Spring Data对Redis的抽象和封装,提供了RedisTemplate、StringRedisTemplate等核心操作类。lettuce-core: 这就是我们今天的主角,非阻塞的Redis客户端。spring-core等相关Spring基础依赖。
这里有一个关键的细节:SpringBoot 2.x开始,这个starter默认就使用Lettuce,不再需要你显式排除Jedis并引入Lettuce。你可以通过查看依赖树(mvn dependency:tree)来确认。这省去了很多配置上的麻烦。
但是,仅仅有这个starter,在一些场景下可能还不够。比如,你可能需要连接Redis集群(Cluster)或哨兵模式(Sentinel),或者你想使用连接池来优化资源使用(尽管Lettuce单连接很强,但在某些特定场景下连接池仍有价值)。这时,我们通常还会引入一个连接池依赖。我推荐使用Apache Commons Pool2,因为Lettuce对它支持得最好。
<dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency>引入后,SpringBoot的自动配置会识别到它,并允许我们在配置文件中启用和配置Lettuce的连接池。至此,依赖部分就准备好了。记住,在SpringBoot的世界里,“约定大于配置”,正确的依赖引入是自动配置生效的前提。
2.2 配置文件的核心参数详解
接下来是重头戏:application.yml的配置。很多连接问题、性能问题都源于这里配置不当。我会把配置拆解成几个部分,并解释每个关键参数的意义。
spring: data: redis: # 1. 基础连接信息 host: 127.0.0.1 # Redis服务器地址 port: 6379 # 端口,默认6379 password: yourpassword # 如果Redis设置了密码,这里填写。无密码则删除此行或留空。 database: 0 # 使用的数据库索引,默认0,范围0-15 # 2. Lettuce客户端特定配置 lettuce: pool: enabled: true # 启用连接池(需要commons-pool2依赖) # 连接池大小配置(这些值需要根据你的应用压力调整,下面给的是通用参考值) max-active: 8 # 连接池最大连接数(使用负值表示没有限制)。默认8。 max-idle: 8 # 连接池中的最大空闲连接。默认8。 min-idle: 0 # 连接池中的最小空闲连接。默认0。 max-wait: -1ms # 连接池最大阻塞等待时间(使用负值表示无限等待)。默认-1。 shutdown-timeout: 100ms # 关闭客户端时,等待连接处理完成的超时时间 # 3. 连接和读写超时配置(非常重要!) timeout: 2000ms # 连接Redis服务器的超时时间(单位毫秒) connect-timeout: 1000ms # Socket连接超时时间 # lettuce默认使用非阻塞I/O,所以没有单独的读写超时配置,超时控制主要在客户端命令层面。配置参数深度解析:
spring.data.redis.lettuce.pool: 这是启用和配置Lettuce连接池的地方。即使Lettuce单连接性能强,但在传统的Servlet容器(如Tomcat)中,每个请求一个线程的模型下,使用连接池可以避免频繁创建和销毁连接的开销,尤其是在突发流量下。max-active不宜设置过大,否则会消耗过多服务器资源;min-idle设置一个较小正数(如2),可以在应用启动后预热连接,避免第一次请求的延迟。timeoutvsconnect-timeout: 这是两个容易混淆的参数。connect-timeout指的是建立TCP连接的超时时间。而timeout在Spring Data Redis的语境下,通常指的是命令执行超时,即一个Redis操作(如GET,SET)等待响应的最长时间。如果你的某个操作很慢,超过这个时间就会抛出RedisCommandTimeoutException。生产环境需要根据业务容忍度合理设置。database: 默认使用0号库。虽然Redis有16个库,但在微服务架构中,更推荐的做法是不同服务使用不同的Redis实例(或通过Key前缀隔离),而不是共用同一个实例的不同database。因为FLUSHDB、SELECT等命令是全局性的,混用容易导致误操作,且Redis Cluster模式不支持多个database。
如果你的Redis部署模式不是单机,配置会有所不同:
哨兵模式(Sentinel)配置示例:
spring: data: redis: sentinel: master: mymaster # 主节点名称 nodes: sentinel1:26379,sentinel2:26379,sentinel3:26379 # 哨兵节点地址列表 password: yourpassword lettuce: pool: enabled: true # ... 池配置同上集群模式(Cluster)配置示例:
spring: data: redis: cluster: nodes: redis-node1:6379,redis-node2:6379,redis-node3:6379 # 集群节点列表(至少一个) max-redirects: 3 # 执行命令时最大重定向次数 password: yourpassword lettuce: pool: enabled: true # ... 池配置同上 cluster: refresh: adaptive: true # 开启自适应拓扑刷新,当集群拓扑变化时自动更新 period: 2000ms # 定期刷新拓扑的时间间隔配置完成后,SpringBoot的自动配置就会为我们创建一个RedisConnectionFactory(默认是LettuceConnectionFactory)以及RedisTemplate等Bean,我们可以直接注入使用了。
3. 核心操作:RedisTemplate与StringRedisTemplate的使用
配置好了连接,接下来就是如何在代码中操作Redis。Spring Data Redis提供了两个最常用的模板类:RedisTemplate和StringRedisTemplate。理解它们的区别是正确使用的第一步。
3.1 两者区别与选用策略
简单来说:
StringRedisTemplate:是RedisTemplate<String, String>的特化版本。它的Key和Value的序列化器默认都是StringRedisSerializer。这意味着你存进去和取出来的,都是人类可读的字符串。这也是它名字的由来。在绝大多数情况下,尤其是Key和Value都是字符串的场景,我强烈推荐使用它。因为它在Redis命令行客户端(如redis-cli)里可以直接看到内容,调试非常方便。RedisTemplate:是一个泛型类RedisTemplate<K, V>。默认情况下,它使用JdkSerializationRedisSerializer。这个序列化器会把Java对象序列化成二进制数据存储。存进去的数据在redis-cli里看是一串乱码(\xac\xed\x00\x05t\x00\x03foo这种格式)。它的优势是可以直接存储复杂的Java对象,但带来了可读性差、存储体积大、不同JVM可能不兼容等问题。
我的经验是:除非你有明确的理由要存储序列化对象(并且能接受其缺点),否则一律使用StringRedisTemplate。对于复杂对象,我们可以在应用层将其转换为JSON字符串再存储,这样数据透明、跨语言也可读。SpringBoot已经为我们自动配置了StringRedisTemplate的Bean,直接注入即可。
3.2 基础数据类型的操作实战
让我们通过一个Service类,来看看如何使用StringRedisTemplate执行各种操作。我会为每个操作附上解释和注意事项。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import java.util.concurrent.TimeUnit; @Service public class RedisBasicOpsService { @Autowired private StringRedisTemplate stringRedisTemplate; // ========== String(字符串)操作 ========== /** * 设置一个字符串键值对,并设置过期时间(单位:秒) * 这是缓存最常用的操作。 */ public void setStringWithExpire(String key, String value, long timeoutSeconds) { // opsForValue() 返回针对String类型操作的对象 stringRedisTemplate.opsForValue().set(key, value, timeoutSeconds, TimeUnit.SECONDS); // 小技巧:如果不设置过期时间,数据会永久存储,可能导致Redis内存耗尽。 } public String getString(String key) { return stringRedisTemplate.opsForValue().get(key); } /** * 如果key不存在则设置(SET if Not eXists),常用于分布式锁的简单实现。 * @return true表示设置成功(key原先不存在),false表示key已存在,设置失败。 */ public Boolean setIfAbsent(String key, String value, long timeoutSeconds) { return stringRedisTemplate.opsForValue().setIfAbsent(key, value, timeoutSeconds, TimeUnit.SECONDS); } // ========== Hash(哈希表)操作 ========== /** * 存储一个Hash结构,适合存储对象。 * 例如,存储用户信息:key="user:1001", hashKey="name", value="张三"。 */ public void putHash(String key, String hashKey, String value) { stringRedisTemplate.opsForHash().put(key, hashKey, value); } public Object getHash(String key, String hashKey) { return stringRedisTemplate.opsForHash().get(key, hashKey); } // ========== List(列表)操作 ========== /** * 从列表左侧插入元素。可用于消息队列、最新N条记录等场景。 */ public Long leftPush(String key, String value) { return stringRedisTemplate.opsForList().leftPush(key, value); } public String rightPop(String key) { return stringRedisTemplate.opsForList().rightPop(key); } // ========== Set(集合)操作 ========== /** * 向集合添加成员。集合具有去重特性。 * 可用于标签系统、共同好友等。 */ public Long addToSet(String key, String... values) { return stringRedisTemplate.opsForSet().add(key, values); } public Boolean isMember(String key, String value) { return stringRedisTemplate.opsForSet().isMember(key, value); } // ========== ZSet(有序集合)操作 ========== /** * 向有序集合添加成员,并指定分数(score)。按分数排序。 * 可用于排行榜、延迟队列等。 */ public Boolean addToZSet(String key, String value, double score) { return stringRedisTemplate.opsForZSet().add(key, value, score); } // ========== 通用键操作 ========== public Boolean deleteKey(String key) { return stringRedisTemplate.delete(key); } public Boolean expireKey(String key, long timeoutSeconds) { return stringRedisTemplate.expire(key, timeoutSeconds, TimeUnit.SECONDS); } public Boolean hasKey(String key) { return stringRedisTemplate.hasKey(key); } }操作中的关键点:
opsForXXX()方法:这是入口,它返回一个针对特定数据类型的操作接口。清晰地区分了不同数据结构的API。- 过期时间:
set操作的重载方法可以直接设置过期时间,这是最常用的。对于已存在的Key,可以用expire方法单独设置。务必为缓存数据设置合理的过期时间,这是防止数据无限增长、保持数据新鲜度的基本手段。 - 返回值:注意不同操作的返回值类型。例如
setIfAbsent返回Boolean,leftPush返回操作后列表的长度(Long)。正确处理返回值对于业务逻辑判断很重要。 - 序列化一致性:由于我们使用的是
StringRedisTemplate,所有存入的value都必须是String类型。如果你有一个User对象,需要手动将其转换为JSON字符串(例如使用Jackson的ObjectMapper)。
3.3 事务与管道(Pipeline)的谨慎使用
Redis支持事务(MULTI/EXEC)和管道(Pipeline),它们都可以批量执行命令,但目的不同。
- 事务:目的是确保一组命令的原子性执行(即不被其他客户端命令打断)。在Spring中,可以通过
SessionCallback或RedisTemplate的execute方法实现。但请注意,Redis的事务并不是关系型数据库那种严格的事务,它不支持回滚。如果事务中的某条命令失败,其他命令依然会执行。List<Object> results = stringRedisTemplate.execute(new SessionCallback<List<Object>>() { @Override public List<Object> execute(RedisOperations operations) throws DataAccessException { operations.multi(); // 开启事务 operations.opsForValue().set("key1", "value1"); operations.opsForValue().increment("counter", 1); return operations.exec(); // 执行事务,返回结果列表 } }); - 管道:主要目的是提升性能。它将多个命令打包一次性发送给Redis服务器,减少了网络往返时间(RTT)。适用于需要连续执行大量独立命令且不需要中间结果依赖的场景。
List<Object> results = stringRedisTemplate.executePipelined(new SessionCallback<Object>() { @Override public Object execute(RedisOperations operations) throws DataAccessException { for (int i = 0; i < 1000; i++) { operations.opsForValue().set("pipeline:key:" + i, "value" + i); } // 注意:在管道内,命令的返回值是null,最终结果会在executePipelined返回的List中 return null; } });
经验之谈:在Web应用中,除非有明确的强一致性批量操作需求,否则使用事务的场景并不多。而管道在数据批量导入、初始化缓存等场景下性能提升显著,但要注意,管道内的命令如果过多,会占用客户端和服务器内存,并阻塞其他请求,需要根据实际情况分批进行。
4. 实战案例:封装一个简易的分布式锁
分布式锁是Redis一个非常经典的应用场景。虽然市面上有Redisson这样成熟的框架,但理解其基本原理和手动实现一个简易版本,对于掌握Redis操作和应对面试都大有裨益。这里我们用StringRedisTemplate实现一个基于SET key value NX PX命令的锁。
4.1 分布式锁的核心诉求与Redis方案选择
一个可靠的分布式锁至少需要满足以下几点:
- 互斥性:在任意时刻,只有一个客户端能持有锁。
- 防死锁:即使锁的持有者崩溃,锁也能在一定时间后自动释放,避免资源被永久锁定。
- 容错性:只要大部分Redis节点存活,客户端就能获取和释放锁。
- 解铃还须系铃人:加锁和解锁必须是同一个客户端,不能误删其他客户端的锁。
Redis实现分布式锁,最初常用SETNX(SET if Not eXists)命令,但它需要配合EXPIRE来设置超时,这两个命令不是原子的,可能在中间过程客户端崩溃导致死锁。因此,Redis 2.6.12之后,推荐使用单条SET命令配合NX和PX选项,这是一个原子操作。
4.2 基于SET NX PX的锁实现与代码解析
下面我们实现一个DistributedLockHelper类。为了确保解锁操作的安全性,我们为每个锁设置一个唯一的随机值(value),解锁时通过Lua脚本验证value匹配后才删除Key。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.script.DefaultRedisScript; import org.springframework.stereotype.Component; import java.util.Collections; import java.util.UUID; import java.util.concurrent.TimeUnit; @Component public class DistributedLockHelper { @Autowired private StringRedisTemplate stringRedisTemplate; // 解锁的Lua脚本。保证判断锁归属和删除锁的原子性。 private static final String UNLOCK_LUA_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then " + " return redis.call('del', KEYS[1]) " + "else " + " return 0 " + "end"; /** * 尝试获取分布式锁 * @param lockKey 锁的Key * @param requestId 请求标识(可以使用UUID),用于标识加锁的客户端 * @param expireMillis 锁的过期时间(毫秒) * @return 是否获取成功 */ public boolean tryLock(String lockKey, String requestId, long expireMillis) { // 关键操作:原子性地设置键值对,仅当Key不存在时,并设置过期时间。 Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, expireMillis, TimeUnit.MILLISECONDS); // setIfAbsent 返回的是 Boolean 包装类,需要处理null情况。成功返回true。 return Boolean.TRUE.equals(success); } /** * 释放分布式锁 * @param lockKey 锁的Key * @param requestId 请求标识,必须与加锁时传入的requestId一致 * @return 是否释放成功(只有锁存在且requestId匹配时才成功) */ public boolean unlock(String lockKey, String requestId) { // 使用Lua脚本执行原子化的解锁操作 DefaultRedisScript<Long> script = new DefaultRedisScript<>(); script.setScriptText(UNLOCK_LUA_SCRIPT); script.setResultType(Long.class); // 脚本返回删除操作影响的行数,1或0 Long result = stringRedisTemplate.execute(script, Collections.singletonList(lockKey), requestId); // 返回1表示解锁成功(锁存在且值匹配),0表示失败(锁已过期或被其他客户端持有/释放) return result != null && result == 1L; } /** * 便捷方法:使用自动生成的UUID作为requestId进行加锁 */ public boolean tryLockWithUuid(String lockKey, long expireMillis) { String requestId = UUID.randomUUID().toString(); return tryLock(lockKey, requestId, expireMillis); } // 注意:使用tryLockWithUuid加锁,必须配套使用下面的unlockWithUuid,或者自己保存requestId。 // 这里为了示例,我们假设调用者会保存返回的requestId。更健壮的做法是返回一个包含requestId的锁对象。 }代码关键点解读:
tryLock方法:核心是setIfAbsent方法,它对应Redis的SET key value NX PX timeout命令。NX保证了互斥性,PX设置了过期时间防止死锁。requestId(一个随机值)保证了锁的客户端标识。unlock方法:这是最容易出错的地方。如果简单地使用redisTemplate.delete(lockKey),可能会发生以下情况:客户端A加锁(过期时间30s),但业务执行了35s,锁已自动释放。此时客户端B获得了锁。接着客户端A执行到delete,就会误删客户端B的锁。因此,我们必须验证requestId。而验证和删除必须是原子操作,否则在验证通过后、删除前,锁可能因过期被其他客户端获取。Lua脚本在Redis中单线程执行,完美解决了这个问题。DefaultRedisScript:Spring Data Redis执行Lua脚本的类。需要指定脚本内容和返回值类型。
4.3 锁的使用模式与注意事项
有了锁工具类,我们来看一个典型的使用模式——“尝试获取,失败重试”。
@Service public class OrderService { @Autowired private DistributedLockHelper lockHelper; private static final String ORDER_LOCK_PREFIX = "order:lock:"; public void createOrder(Long orderId) { String lockKey = ORDER_LOCK_PREFIX + orderId; String requestId = UUID.randomUUID().toString(); int maxRetryTimes = 3; long lockExpire = 30000; // 锁30秒过期 boolean locked = false; try { // 尝试获取锁,最多重试3次 for (int i = 0; i < maxRetryTimes; i++) { locked = lockHelper.tryLock(lockKey, requestId, lockExpire); if (locked) { break; // 获取成功,跳出循环 } // 获取失败,等待一段时间后重试(避免活锁) Thread.sleep(100); // 等待100毫秒 } if (!locked) { throw new RuntimeException("系统繁忙,请稍后重试"); } // ===== 临界区代码:开始执行受保护的业务逻辑 ===== // 例如:检查库存、创建订单、扣减库存等 System.out.println("成功获取锁,开始处理订单: " + orderId); Thread.sleep(1000); // 模拟业务处理耗时 // ===== 临界区代码结束 ===== } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("业务处理被中断", e); } finally { // 无论如何,最终都要尝试释放锁 if (locked) { boolean released = lockHelper.unlock(lockKey, requestId); if (!released) { // 这里可以记录日志,但通常不抛异常,因为锁可能已超时自动释放 log.warn("释放分布式锁失败,lockKey: {}, requestId: {}", lockKey, requestId); } } } } }重要注意事项(踩坑点):
- 锁的过期时间:这个时间必须大于业务逻辑的执行时间。否则,业务还没执行完,锁就失效了,其他客户端会拿到锁,导致数据不一致。但又不能设置得过长,以防客户端宕机后锁长期不释放。这是一个需要权衡的点。更高级的方案是使用“看门狗”(watch dog)机制,在业务执行期间自动续期,Redisson就实现了这个机制。
- 一定要在finally块中释放锁:确保即使业务逻辑抛出异常,锁也能被释放,避免死锁。
- 释放锁时要验证requestId:如前所述,这是防止误删他人锁的关键。
- 重试与等待:直接获取锁失败后,简单的重试机制是必要的,但要配合随机等待(如
Thread.sleep(100)),避免多个客户端同时重试导致活锁。 - 非阻塞与超时:上面的例子是阻塞式重试。在生产环境中,更推荐设置一个总的获取锁超时时间,超过这个时间就快速失败,避免线程长时间挂起。
这个简易锁适用于很多并发量不是极端高的场景。但对于超高并发、要求绝对可靠的场景,建议直接使用经过大量验证的客户端库,如Redisson,它实现了可重入锁、公平锁、联锁、红锁(RedLock)等多种分布式锁方案,更为健壮。
5. 生产环境进阶配置与问题排查
当你的应用从本地开发环境走向生产环境时,关于Redis的配置和运维就变得复杂起来。这里分享几个我实践中遇到的典型问题和优化点。
5.1 连接池配置调优
在application.yml里我们配置了连接池参数,但那些默认值真的适合你的生产环境吗?未必。调优连接池需要结合监控数据。
spring: data: redis: lettuce: pool: enabled: true max-active: 20 # 增大最大连接数。需要根据应用实例数、QPS和Redis服务器性能调整。 max-idle: 10 # 最大空闲连接,建议设为max-active的50%-70%。 min-idle: 5 # 最小空闲连接,保持一定预热连接,避免突发请求的延迟。 max-wait: 5000ms # 获取连接的最大等待时间。设置一个合理值(如5秒),避免线程无限等待。 time-between-eviction-runs: 60000ms # 空闲连接逐出检查的时间间隔(默认-1不检查)。建议开启,如60秒。如何调优?
- 观察监控:通过
redis-cli --stat命令或Redis的INFO命令查看connected_clients,通过应用监控(如Spring Boot Actuator的/metrics端点,或连接池自身的JMX)查看活跃/空闲连接数。 max-active:如果发现频繁出现Cannot get Jedis connection异常或获取连接等待时间过长,可能需要调大。但不要盲目调大,一个连接对应Redis服务器端的一个文件描述符,连接数过多会消耗服务器资源。计算公式可粗略估算为:(应用实例数 * 最大并发线程数) * 安全系数(如1.2)。min-idle:设置一个正值(如5),可以让应用启动后立即建立一些连接,避免第一个请求的冷启动延迟。max-wait:必须设置!默认-1(无限等待)在生产环境是危险的,可能导致线程池耗尽。设置一个业务可接受的超时时间(如2-5秒),超时后抛出异常,进行降级或快速失败。
5.2 序列化方案选择与自定义
前面我们一直用StringRedisSerializer,它简单可靠。但如果你确实需要用RedisTemplate存储对象,那么序列化器的选择就至关重要。
默认的JdkSerializationRedisSerializer问题很多:序列化后的二进制数据体积大、不可读、不同JVM版本可能不兼容。生产环境强烈不推荐。
推荐方案:使用Jackson2JsonRedisSerializer或GenericJackson2JsonRedisSerializer。
@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); // 使用Jackson序列化器替代默认的JDK序列化器 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper objectMapper = new ObjectMapper(); // 设置一些Jackson的常用配置,如忽略未知属性、日期格式等 objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); serializer.setObjectMapper(objectMapper); // 设置Key和HashKey的序列化器为String template.setKeySerializer(RedisSerializer.string()); template.setHashKeySerializer(RedisSerializer.string()); // 设置Value和HashValue的序列化器为Jackson template.setValueSerializer(serializer); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } }这样配置后,存储的对象会被序列化成JSON字符串,可读性好,且能被其他语言客户端读取。注意,反序列化时,由于Object.class是泛型,取出的对象默认是LinkedHashMap类型。如果你需要明确的类型,可以使用TypeReference或在调用opsForValue().get时指定类型。
5.3 常见异常与排查链路
集成过程中难免遇到问题,这里列举几个典型异常和排查思路。
异常1:RedisConnectionFailureException: Unable to connect to Redis
- 可能原因:网络不通、Redis服务未启动、防火墙拦截、配置的主机端口错误。
- 排查步骤:
ping一下Redis服务器IP,检查网络连通性。- 登录服务器,用
redis-cli -h 127.0.0.1 -p 6379看能否本地连接。 - 检查服务器防火墙是否开放了Redis端口(默认6379)。
- 检查SpringBoot配置文件的
host和port是否正确。 - 如果使用哨兵或集群,检查节点地址列表配置。
异常2:InvalidDataAccessApiUsageException: ERR invalid password
- 可能原因:密码错误,或Redis配置了密码但客户端未配置。
- 排查步骤:
- 检查
application.yml中的password配置是否正确,注意前后空格。 - 通过
redis-cli连接后,使用AUTH yourpassword命令验证密码。 - 检查Redis配置文件
redis.conf中的requirepass指令。
- 检查
异常3:RedisCommandTimeoutException: Command timed out
- 可能原因:网络延迟高、Redis服务器负载过高、执行了慢查询命令、客户端设置的
timeout太短。 - 排查步骤:
- 检查应用和Redis服务器之间的网络延迟。
- 登录Redis服务器,使用
redis-cli --latency查看延迟,使用INFO commandstats查看命令耗时统计。 - 使用
SLOWLOG GET 10查看最近的慢查询,优化相关命令(如避免大Key、使用SCAN替代KEYS等)。 - 适当调大配置文件中的
spring.data.redis.timeout值(如从2秒调到5秒),但根本还是要优化慢查询。
异常4: 连接池耗尽Cannot get Jedis connection; nested exception is redis.clients.jedis.exceptions.JedisException: Could not get a resource from the pool
- 可能原因:连接泄露(获取连接后未归还)、
max-active设置过小、业务并发量突增。 - 排查步骤:
- 检查代码,确保每次从
RedisTemplate执行操作后,连接被正确关闭(RedisTemplate会自动管理,但如果你直接操作RedisConnection,需要手动关闭)。 - 检查连接池配置
max-active是否合理,根据监控调整。 - 检查是否有慢查询导致连接被长时间占用。
- 启用连接池的监控,查看活跃连接、空闲连接、等待线程数等指标。
- 检查代码,确保每次从
通用排查命令:
redis-cli INFO:查看Redis服务器综合信息。redis-cli INFO stats:查看命令统计、网络流量等。redis-cli INFO clients:查看客户端连接信息。redis-cli MONITOR:实时打印所有执行的命令(调试用,对性能有影响,生产慎用)。
6. 性能监控与健康检查
对于一个生产级应用,仅仅能运行是不够的,我们还需要知道它运行得怎么样。Spring Boot Actuator为我们提供了强大的监控能力。
6.1 集成Actuator监控Redis指标
首先,在pom.xml中添加依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>然后,在application.yml中暴露相关的监控端点:
management: endpoints: web: exposure: include: health,metrics,info # 暴露健康检查、指标和信息端点 metrics: export: prometheus: enabled: true # 如果需要集成Prometheus endpoint: health: show-details: always # 健康检查显示详细信息访问/actuator/health端点,你会看到类似下面的信息,其中包含了Redis的连接状态:
{ "status": "UP", "components": { "redis": { "status": "UP", "details": { "version": "6.2.6" } } } }如果Redis连接失败,这里会显示"status": "DOWN"。
访问/actuator/metrics端点,你可以找到很多redis.*开头的指标,例如:
redis.connections.active:活跃连接数。redis.connections.idle:空闲连接数。redis.connections.max:最大连接数。redis.commands.executed:已执行的命令数量。redis.commands.failed:执行失败的命令数量。
这些指标可以帮助你了解Redis客户端的运行状况和性能瓶颈。
6.2 自定义健康检查与告警
除了内置的健康检查,你还可以自定义更细致的检查。例如,检查Redis是否不仅可连接,还能正常执行命令。
import org.springframework.boot.actuate.health.Health; import org.springframework.boot.actuate.health.HealthIndicator; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; @Component("redis") // 命名为redis,会覆盖或增强默认的redis健康指示器 public class RedisHealthIndicator implements HealthIndicator { private final StringRedisTemplate stringRedisTemplate; public RedisHealthIndicator(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public Health health() { try { // 执行一个简单的PING命令来检查连通性和响应能力 String pong = stringRedisTemplate.getConnectionFactory().getConnection().ping(); if ("PONG".equals(pong)) { // 可以添加更多检查,如执行一个简单的SET/GET // stringRedisTemplate.opsForValue().set("health:check", "ok", 10, TimeUnit.SECONDS); // String value = stringRedisTemplate.opsForValue().get("health:check"); return Health.up() .withDetail("version", stringRedisTemplate.getRequiredConnectionFactory().getConnection().info("server").get("redis_version")) .build(); } else { return Health.down().withDetail("ping", "Unexpected response: " + pong).build(); } } catch (Exception e) { return Health.down(e).build(); } } }这样,当访问/actuator/health时,如果Redis出现问题,你会得到更明确的错误信息。结合监控平台(如Prometheus + Grafana)和告警系统(如AlertManager),你可以在连接数异常、错误率升高、延迟变大时及时收到通知,从而在影响用户之前解决问题。
从环境搭建、基础操作,到实战案例、生产调优,再到监控告警,围绕SpringBoot集成Lettuce操作Redis的核心链路基本就清晰了。技术选型没有银弹,Lettuce在大多数现代SpringBoot应用中是一个平衡了性能、资源和易用性的不错选择,理解其原理和最佳实践,能让你在项目中更得心应手。