1. 先搞清楚需求,再选客户端写Java应用连Redis,第一步不是去翻依赖、复制代码,而是先想清楚:你的业务到底需要Redis做什么,以及你的应用是什么形态。这话可能听起来像废话,但我在实际项目里见过太多人一上来就选了某个客户端,结果用到一半发现功能不够、线程模型对不上、或者扩展能力跟不上,最后不得不重构。Redis的Java客户端不是只有一个,也不是“哪个最火就无脑用哪个”。主流的无非是三个:Jedis、Lettuce和Redisson。它们不是简单的“谁能连Redis”的区别,而是各自站在不同的抽象层级上,解决的是不同维度的问题。先说一个最容易理解的类比。假如Redis是一个快递仓库,那这些客户端就是你手里的提货单或者说对接工具。Jedis像一张最基础的提货单,你拿着它去仓库取件,一次取一件,取完再取下一件,简单直接,但每次都得自己跑一趟。Lettuce像一张可以反复使用的通行卡,你可以同时安排好几个任务,让仓库并行处理,效率更高。Redisson则不只是一张提货单或者通行卡,它是直接把仓库里那些常见的操作,比如“把某个货物锁定”“给某批货物做定期盘点”都封装成了现成的工具箱,你不需要自己设计流程。所以你会看到,这三个客户端的定位是呈阶梯状的:Jedis:最底层、最简单,直连Redis,API风格贴近原生命令,适合小型项目、单机部署、以及想对Redis操作有完全掌控权的场景。Lettuce:基于Netty的高性能客户端,线程安全、支持异步和响应式编程,自带连接复用能力,是大多数Spring Boot项目的默认选择。Redisson:直接把你带到了“分布式应用”层面,提供分布式锁、分布式集合、延迟队列、信号量等开箱即用的高级数据结构,适合中大型分布式系统。这个定位差异不是谁替代谁的关系,而是在不同需求下各自有不可替代的优势。接下来我会把这几个客户端的底层机制、核心配置、常见坑和选型建议一次性讲透,全都来自实际项目里的经验,不是官方文档的复读。1.1 先看需求清单,再对应选型做选型前,我建议你先列一个简单的问题清单。不必搞什么复杂评估表,就回答几个关键问题:你的项目是单机部署还是分布式集群?你的业务对Redis操作的延迟和并发要求有多高?你是否需要分布式锁、延迟队列这种高级能力?你的团队更熟悉同步编程还是异步响应式编程?你用的是Spring Boot还是纯Java应用?版本是多少?这几个问题的答案,基本就能锁定你该用哪个客户端。比如单机小项目,没有太高并发,本地起个Redis做缓存,那用Jedis就够了。比如Spring Boot项目,又需要比较高的吞吐量,那Lettuce是天然的选择,因为Spring Data Redis默认就是它。比如你要做分布式锁、需要读写锁、公平锁、信号量这些,那直接用Redisson,别自己用Jedis去实现Lua脚本,那是在重复造轮子。我这几个问题不是凭空想出来的,而是因为这三个客户端的本质区别就体现在这些维度上。下面我逐一把它们的底层逻辑、配置细节、实操经验拆开来讲。2. 三个主流客户端的核心差异,理解清楚了才会选2.1 Jedis:小而美的直连式客户端Jedis是Redis官方推荐的Java客户端之一,历史悠久,使用广泛。它的设计思路非常直接:每次使用时,创建一个Jedis对象,执行命令,然后关闭连接。API设计非常贴近Redis命令本身,比如jedis.set("key", "value")、jedis.hgetAll("hashKey"),几乎是“所见即所得”。Jedis的底层是阻塞式IO,一个连接同一时刻只能执行一个命令。因为JDBC时代遗留下来的“一个连接一个线程独占”的模型,很容易被Java开发者理解。但它有一个关键的先天特性:非线程安全。多个线程不能共享同一个Jedis实例,每个线程必须持有自己的连接。这就引出了一个实操中必须面对的麻烦:在高并发场景下,你必须在每次操作时从连接池里借用连接,操作完成后归还连接。连接池配置得不好,或者使用姿势不对,性能会很差,甚至出现连接泄露导致服务不可用。Jedis适合什么样的人?我觉得是这几类:刚刚接触Redis的初学者,想用最简单的方式理解Redis命令和数据结构。单机部署、并发量不大、逻辑简单的项目。对性能有极致追求且团队有能力管理好连接池的开发者,因为直连模式省去了一层间接调用,在某些特殊场景下延迟确实更低。想完全掌控连接行为,不想被框架封装“黑盒化”的人。Jedis有两个经常被配置出来的坑,我在项目里都踩过。第一个是连接池大小设置不当,默认的maxTotal是8,生产环境如果并发超过这个数,每次操作都要排队等连接,延迟直线上升。第二个是拿连接时没有设置maxWaitMillis,默认值是-1,也就是拿不到连接时无限等待,一旦连接池耗尽,整个线程就卡死了。2.2 Lettuce:基于Netty的弹性连接复用神器Lettuce和Jedis最大的区别在于,它天生就是线程安全的,而且底层基于Netty实现多路复用。这意味着一个连接可以被多个线程共享使用,不需要频繁地创建和销毁连接,这一点在高并发场景下非常值钱。Netty的IO模型是事件驱动的非阻塞模型,Lettuce把命令请求异步化和多路复用,单个连接可以并发地发送命令而不必等上一个命令的结果返回。打个比方:Jedis是一个人跑一趟仓库取一件货,取完再跑下一趟;Lettuce是开着车一次把好几件货的订单都交给仓库管理员,仓库管理员自己调配合适的装卸工人来处理。吞吐量的差距在并发高的时候体现得非常明显。Lettuce支持同步、异步、响应式三种编程模式:同步:看起来和Jedis差不多,但底层已经不是简单的一请求一应答。异步:调用redisAsyncCommands.set()会立即返回一个RedisFuture,你可以订阅结果。响应式:基于Reactor的Mono、Flux类型,适合与Spring WebFlux整合,实现全程非阻塞。Lettuce在Spring Boot里是默认的Redis客户端,原因也很简单:Spring Data Redis这个抽象层本身需要支持高并发、非阻塞IO、并且能在Spring Boot的自动装配体系里稳定运行。所以,如果你用的是Spring Boot,哪怕什么都不配,拉一个spring-boot-starter-data-redis依赖,底层就是Lettuce。不过,Lettuce有一个很容易被忽略的问题:它虽然线程安全,但如果你用的是共享连接,又同时执行SELECT切换数据库,或者订阅发布模式下切换订阅频道,就会污染其他线程的状态。在集群模式下,某些命令的读写分离配置也要注意。还有一个在Spring Boot场景下的经典问题:Lettuce的共享连接在空闲超时后会被服务端主动断开,但客户端有时不会及时感知,导致第一个请求触发重建连接时出现短暂超时。这个我在生产环境遇到过几次,后面会在故障排查部分详细写。2.3 Redisson:从“连Redis”到“用Redis”的跨越Redisson是三者中抽象层级最高的一个。它并不满足于只提供执行Redis命令的API,而是把Redis当成了一个分布式计算和存储的基础设施,在上面构建了一套分布式数据结构和服务框架。我举几个Redisson开箱即用的能力:分布式锁:RLock接口,支持阻塞锁、非阻塞锁、公平锁、读写锁,还有一个看门狗(Watchdog)机制自动续期,不用担心锁在业务执行期间自动过期。分布式集合:RMap、RSet、RList、RQueue,使用体验接近本地Java的Map、Set、List,但底层数据在Redis里,不同实例之间可以共享。延迟队列:RDelayedQueue,可以用来实现订单30分钟未支付自动关闭这种典型业务。信号量与计数锁:RSemaphore、RCountDownLatch,用于分布式场景下的并发控制。分布式对象:RBucket、RBitSet、RAtomicLong等。也就是说,如果你用了Redisson,你写的代码风格会从“操作Redis命令”变成“使用分布式Java集合对象”。这对业务开发来说非常友好,很多代码看起来就和单机Java程序一样,只是底层是分布式的。Redisson基于Netty,线程安全,也支持异步和响应式编程。它的分布式锁实现里用了Lua脚本做原子操作,这套实现是经过了大量生产环境验证的,比自己用setnx加expire的组合可靠得多。自己在Jedis或Lettuce上实现分布式锁,要考虑原子性、误删、过期续期、可重入性等一堆问题,Redisson基本都帮你处理好了。但Redisson也不是银弹。因为它封装程度高,很多细节容易被忽略。比如锁的leaseTime设置问题,不设置时看门狗会每10秒检测一次并自动续期到30秒,但如果业务执行超过30秒且网络抖动导致看门狗续期失败,锁还是会失效。还有Redisson的线程模型和内部调度机制相对复杂,出现问题时的排查难度比Jedis高出不少。2.4 三个客户端横向对比表我先给你一张整理好的对比表,方便快速决策:特性维度JedisLettuceRedisson定位直连式原生客户端高性能多路复用客户端分布式对象/服务框架底层模型阻塞式IO,连接独占Netty,多路复用共享连接Netty,多路复用共享连接线程安全性非线程安全,需连接池线程安全线程安全编程模式同步同步、异步、响应式同步、异步、响应式高级功能无无(需要自行封装)分布式锁、延迟队列等上手难度低中偏高Spring Boot默认否是否(可手动集成)适合场景小型项目、命令级控制高并发、Spring Boot项目分布式系统、微服务从这张表能看出来,它们的区分度很明显。如果你要的是“客户端”,那就是Jedis或者Lettuce二选一;如果你要的是“开发框架”,那就直接上Redisson。后面我会进入实操环节,把连接配置、API使用、常见坑一步一步讲到。3. 实操上手:连接、配置、调优与Spring Boot集成所以这一部分我会从最简单的连接Redis开始,逐步讲到连接池参数设计、Spring Boot集成、序列化问题、以及Redisson和Lettuce在实战中的正确使用姿态。每一步都有可以复制的代码和需要避开的坑。3.1 用Jedis写第一段连Redis的代码先看Jedis最基础的连接方式。假设Redis服务跑在本地6379端口,没有设密码:import redis.clients.jedis.Jedis; public class JedisQuickStart { public static void main(String[] args) { // 创建Jedis对象,等价于建立一条TCP连接到Redis try (Jedis jedis = new Jedis("127.0.0.1", 6379)) { // 执行set命令 String result = jedis.set("myKey", "helloRedis"); System.out.println("set结果: " + result); // 执行get命令 String value = jedis.get("myKey"); System.out.println("get结果: " + value); } // try-with-resources会自动归还连接或关闭连接 } }这段代码如果在单线程、低频访问的场景下跑,完全没问题。但如果你把它扔到多线程环境里,就会出现问题,因为Jedis实例不是线程安全的,多个线程共用同一个jedis对象会导致连接状态互相干扰,命令可能串台、异常。正确的多线程用法是配合连接池。这里我给一个标准的连接池配置示例,同时也把每个参数的含义说清楚:import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class JedisPoolDemo { public static void main(String[] args) { // 连接池配置 JedisPoolConfig poolConfig = new JedisPoolConfig(); // 最大连接数:按业务峰值并发数估算,同时考虑Redis服务端可承受的连接数 poolConfig.setMaxTotal(50); // 最大空闲连接数:控制空闲连接占用的资源 poolConfig.setMaxIdle(20); // 最小空闲连接数:低于这个数量时,连接池会逐步创建连接 poolConfig.setMinIdle(5); // 获取连接时最大等待时间(毫秒),超过则抛出异常,避免线程无限阻塞 poolConfig.setMaxWaitMillis(3000); // 用JedisPool包装配置和地址信息 try (JedisPool pool = new JedisPool(poolConfig, "127.0.0.1", 6379)) { // 从池中获取连接,Jedis实例由池管理 try (Jedis jedis = pool.getResource()) { jedis.set("k", "v"); System.out.println(jedis.get("k")); } // 这里归还连接,而不是真正关闭连接 } } }这里有一个非常关键的细节:pool.getResource()返回的Jedis对象,用完之后必须要close(),这看起来像是关闭连接,但实际上如果你是“从连接池中借出来的”,close()的行为是归还给连接池,而不是断开。如果忘了close(),连接就会一直被占用,最终连接池耗尽,整个应用就卡住了。这也是为什么我强烈建议用try-with-resources的写法,它保证无论代码里是否抛异常,连接都会归还。3.2 Jedis连接池参数设计的计算逻辑连接池参数不能拍脑袋定,得有依据。我见过的很多配置错误都是这样出现的:setMaxTotal(8)不动,或者复制网上的最大值1000不管实际并发量。这里我给一个可操作的估算方法。假设你的接口峰值QPS是2000,每个请求平均需要执行2次Redis操作,每次操作平均耗时1ms。那么同时需要的Redis连接数为:并发连接数估算 = QPS × 平均每请求Redis操作次数 × 单次操作耗时 = 2000 × 2 × 0.001 = 4理论上4个连接就够了,但这是理想情况。考虑到网络抖动、Redis慢查询、连接池获取和归还的开销、GC停顿等因素,我会在这个理论值的基础上乘以一个安全系数,常见做法是3倍左右。所以上面这个场景,maxTotal设为12到20是比较合理的。再留点余量应对突发流量,设到20到30也不算过分。但要注意,连接数并不是越大越好。每个连接在Redis服务端都对应一个文件描述符,也占用内存。如果你把maxTotal设到500甚至1000,而Redis本身的maxclients配置才512,那必然会有连接被服务端拒绝。而且过多的空闲连接还会带来不必要的资源消耗。所以连接池参数的设置,本质是在“够用”和“不浪费”之间找平衡。JedisPoolConfig里还有几个容易被忽略但很实用的配置:setTestOnBorrow(true):在从连接池借用连接时,发送一个PING命令验证连接是否可用。好处是能及时剔除失效连接,坏处是多了一次额外的网络IO。setTestOnReturn(true):在归还连接时验证连接状态,适合对连接稳定性要求很高的场景。setJmxEnabled(false):如果在本地开发调试,可以关掉JMX导出,否则会有很多连接池的JMX Bean刷屏。我在生产环境一般只打开testOnBorrow,testOnReturn基本不开,因为归还时的验证不影响后续请求,而且会增加耗时。testWhileIdle也不开,因为底层的空闲连接扫描已经有了。3.3 Lettuce上手:连接复用与异步/响应式打法Lettuce的代码写起来和Jedis风格完全不同。它不是“每次拿一个连接”,而是先创建一个连接对象,然后让多个线程共享使用。基础的连接示例:import io.lettuce.core.RedisClient; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.sync.RedisCommands; public class LettuceQuickStart { public static void main(String[] args) { // 创建RedisClient,默认线程池由内部管理 RedisClient redisClient = RedisClient.create("redis://127.0.0.1:6379"); // 获取一个长期存活的连接 try (StatefulRedisConnection<String, String> connection = redisClient.connect()) { // 获取同步命令执行器 RedisCommands<String, String> commands = connection.sync(); commands.set("key", "value"); System.out.println(commands.get("key")); } // 连接使用完毕后关闭 redisClient.shutdown(); } }Lettuce的同步API只是它在多路复用模型之上封装的一层“伪装同步”,它的用法看起来简单,但底层是在一个连接上并发调度命令。所以多个线程可以安全地共享同一个StatefulRedisConnection对象。如果你需要用异步方式,把connection.sync()换成connection.async()即可,它会返回RedisFuture,可以注册回调,适合不想阻塞当前线程的场景:RedisFuture<String> future = asyncCommands.get("key"); future.thenAccept(value -> System.out.println("异步拿到: " + value));如果是响应式编程,换connection.reactive(),配合Project Reactor:Mono<String> mono = reactiveCommands.get("key"); mono.subscribe(value -> System.out.println("响应式拿到: " + value));选择哪种编程模式,取决于你的应用架构。Spring MVC这种传统Servlet容器,用同步就够了。WebFlux这种非阻塞架构,必须用响应式,不然阻塞调用会拖垮整个事件循环线程。Lettuce还有一个特性值得单独提:它支持Redis Cluster、Redis Sentinel的透明连接管理。你在连接URI里指定集群节点或主从节点,它内部会自动处理节点发现、路由和故障转移。对业务代码来说,只需要一个连接对象,不用自己关心请求到底去了哪个节点。这种“隐性能力”在做高可用架构时非常有帮助。3.4 Redisson的标准进阶用法Redisson的Maven依赖需要按自己使用的Redis版本来选,这一点和Jedis/Lettuce不太一样。比如Redis 3.0和Redis 6.0的协议支持有差异。基础连接配置:import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; public class RedissonQuickStart { public static void main(String[] args) { // 创建配置 Config config = new Config(); // 使用单节点模式,也可以useClusterServers()配置集群 config.useSingleServer() .setAddress("redis://127.0.0.1:6379") .setConnectionMinimumIdleSize(10) .setConnectionPoolSize(50) .setIdleConnectionTimeout(60000) .setConnectTimeout(10000) .setTimeout(3000); // 创建RedissonClient RedissonClient redisson = Redisson.create(config); // 使用分布式Map RMap<String, String> map = redisson.getMap("userMap"); map.put("u1", "张三"); System.out.println(map.get("u1")); // 使用分布式锁 RLock lock = redisson.getLock("orderLock"); lock.lock(); // 阻塞等待获取锁 try { // 业务逻辑 Thread.sleep(2000); } finally { lock.unlock(); } // 使用延迟队列 RDelayedQueue<String> delayedQueue = redisson.getDelayedQueue( redisson.getBlockingQueue("delayedQueue")); delayedQueue.offer("订单关闭消息", 30, TimeUnit.MINUTES); redisson.shutdown(); } }这里有一个关于分布式锁的重要细节。上面示例直接调了lock.lock(),没有传leaseTime参数,这时候Redisson会启动一个看门狗任务:如果锁在10秒内没有被主动unlock(),看门狗会自动把锁的过期时间续期到30秒,并且每10秒检测一次是否需要继续续期。这个机制能防止业务执行时间较长时,锁被提前释放导致并发问题。但如果你在lock.lock(10, TimeUnit.SECONDS)里手动指定了过期时间,看门狗就不会启动,到时间锁自动释放。这两种模式各有适用场景,手动指定适用于可以预估业务耗时的场景,看门狗适用于耗时不可控且允许一直持有锁的场景。但看门狗也有本身的局限:它基于当前客户端实例内部的定时任务执行续期,如果客户端进程自身发生长时间的GC停顿,或者网络长时间不问,续期可能失败。分布式锁本身不是“完美无锁”,它是在可用性和一致性之间做的权衡。所以在关键业务里,锁的代码必须配合合理的异常处理、超时控制和业务幂等性设计,不能把分布式锁当成唯一防线。3.5 Spring Boot集成时的选择与序列化迷宫绝大多数Java项目都跑在Spring Boot上,这里面的集成方式决定了你在生产环境里到底有多省心。先明确一点:Spring Data Redis是一个抽象层,它本身不实现连接,而是委托给底层客户端。spring-boot-starter-data-redis默认引入Lettuce,所以如果你引入这个依赖后配置了spring.redis.*,你实际上就在用Lettuce。Spring Data Redis中有两个核心组件:RedisTemplate和StringRedisTemplate。后者只能处理字符串,前者是泛型模板,默认使用JDK序列化。JDK序列化有两个痛点:数据可读性极差,在Redis里看到的是类似\xAC\xED\x00\x05t...的乱码;而且序列化后的字节数组体积巨大,浪费内存。因此,几乎每个Spring Boot项目都要自定义RedisTemplate的序列化方式。我给出一个常用的配置类:import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // key使用字符串序列化,可读性好 StringRedisSerializer stringSerializer = new StringRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); // value使用JSON序列化,避免JDK序列化的乱码和体积膨胀 GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }为什么key和value要分开设置序列化器?因为大多数场景下,key就是业务标识,用字符串保存便于在Redis客户端里直观检查;value是业务对象,用JSON序列化让它具备可读性,同时体积远小于JDK序列化。GenericJackson2JsonRedisSerializer相比Jackson2JsonRedisSerializer更推荐,因为前者在反序列化时会携带@class类型信息,避免多态类型丢失的问题。踩过的坑提醒一下:如果Value是null,某些序列化器会直接抛异常;如果Value是LocalDateTime这样的Java 8时间类型,Jackson默认序列化会报错,需要额外注册JavaTimeModule。在实践中我一般会封装一个RedisService类,对外提供泛型方法,把类型转换和序列化细节封装在里面,这样业务代码不用碰RedisTemplate的原始API。3.6 在Spring Boot里手动集成RedissonRedisson和Spring Boot的集成可以不用第三方starter。自己写一个配置类,把RedissonClient注入Spring容器,然后需要的地方直接@Autowired或者@Resource,这种方式最灵活,不依赖starter版本兼容性。一个示例配置:import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RedissonConfig { @Bean(destroyMethod = "shutdown") public RedissonClient redissonClient( @Value("${spring.redis.host:127.0.0.1}") String host, @Value("${spring.redis.port:6379}") int port, @Value("${spring.redis.password:}") String password) { Config config = new Config(); config.useSingleServer() .setAddress("redis://" + host + ":" + port); if (password != null && !password.isEmpty()) { config.useSingleServer().setPassword(password); } // 其他参数按需配置 return Redisson.create(config); } }然后业务代码里就可以这么用:@Service public class OrderService { @Resource private RedissonClient redissonClient; public void closeOrder(String orderId) { RLock lock = redissonClient.getLock("close:order:" + orderId); lock.lock(); try { // 执行关单逻辑 } finally { lock.unlock(); } } }这里要特别注意@Bean(destroyMethod = "shutdown")的写法,确保Spring容器关闭时自动释放Redisson的线程池和连接,如果不加这个,进程退出时可能会因为非守护线程存活而无法正常结束。4. 常见问题排查与避坑实录这部分我按实际项目里遇到的高频问题整理成了一张故障速查表,每个问题都附带了排查思路和解决方法。这些坑在官方文档里基本不会写,但你在生产环境里几乎一定会碰到。4.1 经典故障速查表现象可能原因排查路径解决方案应用启动后偶发连接超时Lettuce空闲连接被服务端断开,客户端未及时感知查看异常栈中的io.lettuce.core.RedisConnectionException;用netstat检查连接状态配置合适的timeout和validateConnection;在连接工厂中启用validateConnection;升级Lettuce版本连接池耗尽,请求全部卡死maxTotal太小或连接泄露未归还查看JMX连接池指标;检查代码中getResource()与close()是否成对调大maxTotal;排查是否在异常分支漏了close();启用testOnBorrow自动剔除坏连接设置了Redis密码后Jedis连接失败密码配置方式不正确检查连接URI和Jedis构造参数new Jedis(host, port, timeout, timeout, password)或连接池构造传入密码Spring Boot存取对象后读到乱码序列化器未设置,使用默认JDK序列化在Redis Desktop Manager里查看value的二进制内容自定义RedisTemplate序列化器,改用JSON序列化用opsForValue().setIfAbsent()实现分布式锁,偶发失效setnx没有和expire绑定为原子操作;或加锁后业务时间超过锁过期时间检查锁的过期时间设置;检查异常日志改用Redisson的RLock;如果坚持自研,必须用Lua脚本保证原子性,且设置足够长的过期时间使用Lettuce时使用select切换数据库后数据写入错误共享连接上切换了数据库,污染了其他线程的状态检查代码中是否有select调用避免在多线程共享连接上使用select;不同数据库使用不同的连接;或者使用Redis Cluster/SentinelRedisson分布式锁偶尔失效看门狗续期受GC停顿或网络长时间阻塞影响查看业务执行耗时和GC日志将锁过期时间设为业务最长耗时的1.5倍以上;手动指定leaseTime;业务设计上支持幂等重试高并发下Jedis性能急剧下降连接池通过排队机制放大了RT,线程大量阻塞用jstack查看线程栈,确认线程在等待连接换用Lettuce或Redisson;或者优化连接池参数,如打开fair公平锁模式4.2 几个特别容易踩的细节先说一个关于Jedis的坑。Jedis对象执行完命令后如果忘了close(),尤其是在异常抛出时没有走finally块,连接会一直留在连接池的“已租出”状态。这个问题很隐蔽,因为单个连接泄露时,应用看起来完全正常,直到QPS上来之后突然发现连接池被耗尽,所有请求开始排队,连接池排队又加剧超时,超时又导致更多异常被抛出,形成雪崩效应。我建议每个Jedis连接操作都采用try-with-resources或者try-finally的规范写法,并且用带JMX的连接池配置,方便实时监控“借出连接数”。再看Lettuce在Spring Boot下的一个经典问题:Spring Boot 2.x默认的Lettuce连接在Redis服务器设置了timeout(比如空闲连接5秒被回收)后,客户端并不会主动感知服务端已经断开,等到下次请求到来,从连接池拿到一条死连接,直接抛出异常或触发重建连接。解决方案有两个:一是在配置里给LettucePoolingClientConfiguration设置validateConnection为true,这样每次拿连接时会主动验证连接状态;二是调整Lettuce的shutdownTimeout和shutdownQuietPeriod参数。实际上Spring Boot中更简单的做法是配置spring.redis.lettuce.pool.max-active等参数,同时确保服务端timeout不要设置得太短,否则连接频繁被断开,重建开销不可忽视。Redisson方面我想重点说的是分布式锁的“可重入性”。Redisson的RLock默认就是可重入锁,同一个线程可以多次加锁,但加锁几次必须解锁几次。如果你只解锁一次,锁的“重入计数”没有归零,锁不会真正释放,别的线程永远拿不到锁。这个坑在代码里很容易出现:我们常常在方法A里加锁,然后方法A内部调用了方法B,方法B也加了同一把锁,最后方法A和B的finally里都调用了unlock(),结果第二次解锁直接抛IllegalMonitorStateException,因为锁已经释放了。正确做法是每个lock()对应一个unlock(),并且确保解锁次数与加锁次数一致。还有一个关于延迟队列的坑。Redisson的RDelayedQueue本质上是依赖一个后台线程和Redis的ZSET实现的,它的消息不会“精确到毫秒”地准时到来,实际消费时间可能会有几百毫秒甚至数秒的偏差。如果你的业务对时间精度要求极高,比如精确到秒级别的定时任务,建议直接使用更专业的任务调度平台,而不要把延迟队列当作时间精确的定时器。4.3 高效排查的工具与思路排查Redis客户端问题,我按经验分成三个步骤。第一步,看监控指标。连接数、命令执行失败数、命令平均延迟、TCP重传率、Redis服务端的instantaneous_ops_per_sec、connected_clients这几个指标,基本能定位到“是客户端问题”还是“是服务端问题”。比如connected_clients很高但ops很低,说明客户端建了连接却没用,可能是连接池配置过大或连接泄露。反过来ops很高且延迟大,先看慢查询日志,确定有没有大KEY操作或复杂命令拖慢了Redis。第二步,抓线程栈。当应用出现卡顿、超时,用jstack抓一下Java进程的线程栈,重点关注阻塞在Jedis连接获取、Netty事件线程、以及Redis命令执行上的线程。如果大量线程阻塞在JedisPool.getResource(),说明连接池不够或耗尽了;如果阻塞在SocketInputStream.read(),说明命令发出去了但响应很慢,问题在Redis服务端或网络链路。第三步,开日志。把Lettuce的日志级别调到DEBUG(尤其是io.lettuce.core.protocol),可以看到每个命令的发送和响应详情;Redisson可以通过org.redisson包下的日志观察锁的获取和释放过程。生产环境可以临时开短时间的DEBUG日志,定位后立刻关闭。5. 选型和切换的最终建议写到这里,我把这三个客户端从底层原理、代码示例到排查经验都过了一遍。如果让我给一个最终的选型建议,我的倾向是这样的:如果你在做简单项目、工具类脚本、或者是对Redis命令级别的操作要求很细的控制,用Jedis,简单直接。如果你在做Spring Boot项目,默认用Lettuce就好,它是官方默认,线程安全,性能也好,日常缓存、分布式会话、计数器这些场景完全够用。如果你的系统已经上了微服务架构,需要分布式锁、分布式集合、延迟队列这些能力,别自己造轮子,直接用Redisson。不用过分纠结“哪个好哪个坏”,这三个没有技术代差,只有定位差异。我自己就维护过不同时期的不同项目,有的项目用Lettuce做基础缓存,同时在关键分布式锁场景里单独引入Redisson,两者共存也没有问题。只要你理解每个客户端的底层模型和适用边界,它们在同一个系统里各司其职是完全合理的。最后分享一个实操上的体会:不管选哪个客户端,一定要把连接配置参数写清楚注释,并且纳入监控面板。连接池和连接超时这类参数,平时不出问题,一出就是大问题,而且常常是在凌晨出问题。把参数设计得有据可依,把监控提前做好,比换来换去选型要重要得多。
1. 先搞清楚需求,再选客户端写Java应用连Redis,第一步不是去翻依赖、复制代码,而是先想清楚:你的业务到底需要Redis做什么,以及你的应用是什么形态。这话可能听起来像废话,但我在实际项目里见过太多人一上来就选了某个客户端,结果用到一半发现功能不够、线程模型对不上、或者扩展能力跟不上,最后不得不重构。Redis的Java客户端不是只有一个,也不是“哪个最火就无脑用哪个”。主流的无非是三个:Jedis、Lettuce和Redisson。它们不是简单的“谁能连Redis”的区别,而是各自站在不同的抽象层级上,解决的是不同维度的问题。先说一个最容易理解的类比。假如Redis是一个快递仓库,那这些客户端就是你手里的提货单或者说对接工具。Jedis像一张最基础的提货单,你拿着它去仓库取件,一次取一件,取完再取下一件,简单直接,但每次都得自己跑一趟。Lettuce像一张可以反复使用的通行卡,你可以同时安排好几个任务,让仓库并行处理,效率更高。Redisson则不只是一张提货单或者通行卡,它是直接把仓库里那些常见的操作,比如“把某个货物锁定”“给某批货物做定期盘点”都封装成了现成的工具箱,你不需要自己设计流程。所以你会看到,这三个客户端的定位是呈阶梯状的:Jedis:最底层、最简单,直连Redis,API风格贴近原生命令,适合小型项目、单机部署、以及想对Redis操作有完全掌控权的场景。Lettuce:基于Netty的高性能客户端,线程安全、支持异步和响应式编程,自带连接复用能力,是大多数Spring Boot项目的默认选择。Redisson:直接把你带到了“分布式应用”层面,提供分布式锁、分布式集合、延迟队列、信号量等开箱即用的高级数据结构,适合中大型分布式系统。这个定位差异不是谁替代谁的关系,而是在不同需求下各自有不可替代的优势。接下来我会把这几个客户端的底层机制、核心配置、常见坑和选型建议一次性讲透,全都来自实际项目里的经验,不是官方文档的复读。1.1 先看需求清单,再对应选型做选型前,我建议你先列一个简单的问题清单。不必搞什么复杂评估表,就回答几个关键问题:你的项目是单机部署还是分布式集群?你的业务对Redis操作的延迟和并发要求有多高?你是否需要分布式锁、延迟队列这种高级能力?你的团队更熟悉同步编程还是异步响应式编程?你用的是Spring Boot还是纯Java应用?版本是多少?这几个问题的答案,基本就能锁定你该用哪个客户端。比如单机小项目,没有太高并发,本地起个Redis做缓存,那用Jedis就够了。比如Spring Boot项目,又需要比较高的吞吐量,那Lettuce是天然的选择,因为Spring Data Redis默认就是它。比如你要做分布式锁、需要读写锁、公平锁、信号量这些,那直接用Redisson,别自己用Jedis去实现Lua脚本,那是在重复造轮子。我这几个问题不是凭空想出来的,而是因为这三个客户端的本质区别就体现在这些维度上。下面我逐一把它们的底层逻辑、配置细节、实操经验拆开来讲。2. 三个主流客户端的核心差异,理解清楚了才会选2.1 Jedis:小而美的直连式客户端Jedis是Redis官方推荐的Java客户端之一,历史悠久,使用广泛。它的设计思路非常直接:每次使用时,创建一个Jedis对象,执行命令,然后关闭连接。API设计非常贴近Redis命令本身,比如jedis.set("key", "value")、jedis.hgetAll("hashKey"),几乎是“所见即所得”。Jedis的底层是阻塞式IO,一个连接同一时刻只能执行一个命令。因为JDBC时代遗留下来的“一个连接一个线程独占”的模型,很容易被Java开发者理解。但它有一个关键的先天特性:非线程安全。多个线程不能共享同一个Jedis实例,每个线程必须持有自己的连接。这就引出了一个实操中必须面对的麻烦:在高并发场景下,你必须在每次操作时从连接池里借用连接,操作完成后归还连接。连接池配置得不好,或者使用姿势不对,性能会很差,甚至出现连接泄露导致服务不可用。Jedis适合什么样的人?我觉得是这几类:刚刚接触Redis的初学者,想用最简单的方式理解Redis命令和数据结构。单机部署、并发量不大、逻辑简单的项目。对性能有极致追求且团队有能力管理好连接池的开发者,因为直连模式省去了一层间接调用,在某些特殊场景下延迟确实更低。想完全掌控连接行为,不想被框架封装“黑盒化”的人。Jedis有两个经常被配置出来的坑,我在项目里都踩过。第一个是连接池大小设置不当,默认的maxTotal是8,生产环境如果并发超过这个数,每次操作都要排队等连接,延迟直线上升。第二个是拿连接时没有设置maxWaitMillis,默认值是-1,也就是拿不到连接时无限等待,一旦连接池耗尽,整个线程就卡死了。2.2 Lettuce:基于Netty的弹性连接复用神器Lettuce和Jedis最大的区别在于,它天生就是线程安全的,而且底层基于Netty实现多路复用。这意味着一个连接可以被多个线程共享使用,不需要频繁地创建和销毁连接,这一点在高并发场景下非常值钱。Netty的IO模型是事件驱动的非阻塞模型,Lettuce把命令请求异步化和多路复用,单个连接可以并发地发送命令而不必等上一个命令的结果返回。打个比方:Jedis是一个人跑一趟仓库取一件货,取完再跑下一趟;Lettuce是开着车一次把好几件货的订单都交给仓库管理员,仓库管理员自己调配合适的装卸工人来处理。吞吐量的差距在并发高的时候体现得非常明显。Lettuce支持同步、异步、响应式三种编程模式:同步:看起来和Jedis差不多,但底层已经不是简单的一请求一应答。异步:调用redisAsyncCommands.set()会立即返回一个RedisFuture,你可以订阅结果。响应式:基于Reactor的Mono、Flux类型,适合与Spring WebFlux整合,实现全程非阻塞。Lettuce在Spring Boot里是默认的Redis客户端,原因也很简单:Spring Data Redis这个抽象层本身需要支持高并发、非阻塞IO、并且能在Spring Boot的自动装配体系里稳定运行。所以,如果你用的是Spring Boot,哪怕什么都不配,拉一个spring-boot-starter-data-redis依赖,底层就是Lettuce。不过,Lettuce有一个很容易被忽略的问题:它虽然线程安全,但如果你用的是共享连接,又同时执行SELECT切换数据库,或者订阅发布模式下切换订阅频道,就会污染其他线程的状态。在集群模式下,某些命令的读写分离配置也要注意。还有一个在Spring Boot场景下的经典问题:Lettuce的共享连接在空闲超时后会被服务端主动断开,但客户端有时不会及时感知,导致第一个请求触发重建连接时出现短暂超时。这个我在生产环境遇到过几次,后面会在故障排查部分详细写。2.3 Redisson:从“连Redis”到“用Redis”的跨越Redisson是三者中抽象层级最高的一个。它并不满足于只提供执行Redis命令的API,而是把Redis当成了一个分布式计算和存储的基础设施,在上面构建了一套分布式数据结构和服务框架。我举几个Redisson开箱即用的能力:分布式锁:RLock接口,支持阻塞锁、非阻塞锁、公平锁、读写锁,还有一个看门狗(Watchdog)机制自动续期,不用担心锁在业务执行期间自动过期。分布式集合:RMap、RSet、RList、RQueue,使用体验接近本地Java的Map、Set、List,但底层数据在Redis里,不同实例之间可以共享。延迟队列:RDelayedQueue,可以用来实现订单30分钟未支付自动关闭这种典型业务。信号量与计数锁:RSemaphore、RCountDownLatch,用于分布式场景下的并发控制。分布式对象:RBucket、RBitSet、RAtomicLong等。也就是说,如果你用了Redisson,你写的代码风格会从“操作Redis命令”变成“使用分布式Java集合对象”。这对业务开发来说非常友好,很多代码看起来就和单机Java程序一样,只是底层是分布式的。Redisson基于Netty,线程安全,也支持异步和响应式编程。它的分布式锁实现里用了Lua脚本做原子操作,这套实现是经过了大量生产环境验证的,比自己用setnx加expire的组合可靠得多。自己在Jedis或Lettuce上实现分布式锁,要考虑原子性、误删、过期续期、可重入性等一堆问题,Redisson基本都帮你处理好了。但Redisson也不是银弹。因为它封装程度高,很多细节容易被忽略。比如锁的leaseTime设置问题,不设置时看门狗会每10秒检测一次并自动续期到30秒,但如果业务执行超过30秒且网络抖动导致看门狗续期失败,锁还是会失效。还有Redisson的线程模型和内部调度机制相对复杂,出现问题时的排查难度比Jedis高出不少。2.4 三个客户端横向对比表我先给你一张整理好的对比表,方便快速决策:特性维度JedisLettuceRedisson定位直连式原生客户端高性能多路复用客户端分布式对象/服务框架底层模型阻塞式IO,连接独占Netty,多路复用共享连接Netty,多路复用共享连接线程安全性非线程安全,需连接池线程安全线程安全编程模式同步同步、异步、响应式同步、异步、响应式高级功能无无(需要自行封装)分布式锁、延迟队列等上手难度低中偏高Spring Boot默认否是否(可手动集成)适合场景小型项目、命令级控制高并发、Spring Boot项目分布式系统、微服务从这张表能看出来,它们的区分度很明显。如果你要的是“客户端”,那就是Jedis或者Lettuce二选一;如果你要的是“开发框架”,那就直接上Redisson。后面我会进入实操环节,把连接配置、API使用、常见坑一步一步讲到。3. 实操上手:连接、配置、调优与Spring Boot集成所以这一部分我会从最简单的连接Redis开始,逐步讲到连接池参数设计、Spring Boot集成、序列化问题、以及Redisson和Lettuce在实战中的正确使用姿态。每一步都有可以复制的代码和需要避开的坑。3.1 用Jedis写第一段连Redis的代码先看Jedis最基础的连接方式。假设Redis服务跑在本地6379端口,没有设密码:import redis.clients.jedis.Jedis; public class JedisQuickStart { public static void main(String[] args) { // 创建Jedis对象,等价于建立一条TCP连接到Redis try (Jedis jedis = new Jedis("127.0.0.1", 6379)) { // 执行set命令 String result = jedis.set("myKey", "helloRedis"); System.out.println("set结果: " + result); // 执行get命令 String value = jedis.get("myKey"); System.out.println("get结果: " + value); } // try-with-resources会自动归还连接或关闭连接 } }这段代码如果在单线程、低频访问的场景下跑,完全没问题。但如果你把它扔到多线程环境里,就会出现问题,因为Jedis实例不是线程安全的,多个线程共用同一个jedis对象会导致连接状态互相干扰,命令可能串台、异常。正确的多线程用法是配合连接池。这里我给一个标准的连接池配置示例,同时也把每个参数的含义说清楚:import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; public class JedisPoolDemo { public static void main(String[] args) { // 连接池配置 JedisPoolConfig poolConfig = new JedisPoolConfig(); // 最大连接数:按业务峰值并发数估算,同时考虑Redis服务端可承受的连接数 poolConfig.setMaxTotal(50); // 最大空闲连接数:控制空闲连接占用的资源 poolConfig.setMaxIdle(20); // 最小空闲连接数:低于这个数量时,连接池会逐步创建连接 poolConfig.setMinIdle(5); // 获取连接时最大等待时间(毫秒),超过则抛出异常,避免线程无限阻塞 poolConfig.setMaxWaitMillis(3000); // 用JedisPool包装配置和地址信息 try (JedisPool pool = new JedisPool(poolConfig, "127.0.0.1", 6379)) { // 从池中获取连接,Jedis实例由池管理 try (Jedis jedis = pool.getResource()) { jedis.set("k", "v"); System.out.println(jedis.get("k")); } // 这里归还连接,而不是真正关闭连接 } } }这里有一个非常关键的细节:pool.getResource()返回的Jedis对象,用完之后必须要close(),这看起来像是关闭连接,但实际上如果你是“从连接池中借出来的”,close()的行为是归还给连接池,而不是断开。如果忘了close(),连接就会一直被占用,最终连接池耗尽,整个应用就卡住了。这也是为什么我强烈建议用try-with-resources的写法,它保证无论代码里是否抛异常,连接都会归还。3.2 Jedis连接池参数设计的计算逻辑连接池参数不能拍脑袋定,得有依据。我见过的很多配置错误都是这样出现的:setMaxTotal(8)不动,或者复制网上的最大值1000不管实际并发量。这里我给一个可操作的估算方法。假设你的接口峰值QPS是2000,每个请求平均需要执行2次Redis操作,每次操作平均耗时1ms。那么同时需要的Redis连接数为:并发连接数估算 = QPS × 平均每请求Redis操作次数 × 单次操作耗时 = 2000 × 2 × 0.001 = 4理论上4个连接就够了,但这是理想情况。考虑到网络抖动、Redis慢查询、连接池获取和归还的开销、GC停顿等因素,我会在这个理论值的基础上乘以一个安全系数,常见做法是3倍左右。所以上面这个场景,maxTotal设为12到20是比较合理的。再留点余量应对突发流量,设到20到30也不算过分。但要注意,连接数并不是越大越好。每个连接在Redis服务端都对应一个文件描述符,也占用内存。如果你把maxTotal设到500甚至1000,而Redis本身的maxclients配置才512,那必然会有连接被服务端拒绝。而且过多的空闲连接还会带来不必要的资源消耗。所以连接池参数的设置,本质是在“够用”和“不浪费”之间找平衡。JedisPoolConfig里还有几个容易被忽略但很实用的配置:setTestOnBorrow(true):在从连接池借用连接时,发送一个PING命令验证连接是否可用。好处是能及时剔除失效连接,坏处是多了一次额外的网络IO。setTestOnReturn(true):在归还连接时验证连接状态,适合对连接稳定性要求很高的场景。setJmxEnabled(false):如果在本地开发调试,可以关掉JMX导出,否则会有很多连接池的JMX Bean刷屏。我在生产环境一般只打开testOnBorrow,testOnReturn基本不开,因为归还时的验证不影响后续请求,而且会增加耗时。testWhileIdle也不开,因为底层的空闲连接扫描已经有了。3.3 Lettuce上手:连接复用与异步/响应式打法Lettuce的代码写起来和Jedis风格完全不同。它不是“每次拿一个连接”,而是先创建一个连接对象,然后让多个线程共享使用。基础的连接示例:import io.lettuce.core.RedisClient; import io.lettuce.core.api.StatefulRedisConnection; import io.lettuce.core.api.sync.RedisCommands; public class LettuceQuickStart { public static void main(String[] args) { // 创建RedisClient,默认线程池由内部管理 RedisClient redisClient = RedisClient.create("redis://127.0.0.1:6379"); // 获取一个长期存活的连接 try (StatefulRedisConnection<String, String> connection = redisClient.connect()) { // 获取同步命令执行器 RedisCommands<String, String> commands = connection.sync(); commands.set("key", "value"); System.out.println(commands.get("key")); } // 连接使用完毕后关闭 redisClient.shutdown(); } }Lettuce的同步API只是它在多路复用模型之上封装的一层“伪装同步”,它的用法看起来简单,但底层是在一个连接上并发调度命令。所以多个线程可以安全地共享同一个StatefulRedisConnection对象。如果你需要用异步方式,把connection.sync()换成connection.async()即可,它会返回RedisFuture,可以注册回调,适合不想阻塞当前线程的场景:RedisFuture<String> future = asyncCommands.get("key"); future.thenAccept(value -> System.out.println("异步拿到: " + value));如果是响应式编程,换connection.reactive(),配合Project Reactor:Mono<String> mono = reactiveCommands.get("key"); mono.subscribe(value -> System.out.println("响应式拿到: " + value));选择哪种编程模式,取决于你的应用架构。Spring MVC这种传统Servlet容器,用同步就够了。WebFlux这种非阻塞架构,必须用响应式,不然阻塞调用会拖垮整个事件循环线程。Lettuce还有一个特性值得单独提:它支持Redis Cluster、Redis Sentinel的透明连接管理。你在连接URI里指定集群节点或主从节点,它内部会自动处理节点发现、路由和故障转移。对业务代码来说,只需要一个连接对象,不用自己关心请求到底去了哪个节点。这种“隐性能力”在做高可用架构时非常有帮助。3.4 Redisson的标准进阶用法Redisson的Maven依赖需要按自己使用的Redis版本来选,这一点和Jedis/Lettuce不太一样。比如Redis 3.0和Redis 6.0的协议支持有差异。基础连接配置:import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; public class RedissonQuickStart { public static void main(String[] args) { // 创建配置 Config config = new Config(); // 使用单节点模式,也可以useClusterServers()配置集群 config.useSingleServer() .setAddress("redis://127.0.0.1:6379") .setConnectionMinimumIdleSize(10) .setConnectionPoolSize(50) .setIdleConnectionTimeout(60000) .setConnectTimeout(10000) .setTimeout(3000); // 创建RedissonClient RedissonClient redisson = Redisson.create(config); // 使用分布式Map RMap<String, String> map = redisson.getMap("userMap"); map.put("u1", "张三"); System.out.println(map.get("u1")); // 使用分布式锁 RLock lock = redisson.getLock("orderLock"); lock.lock(); // 阻塞等待获取锁 try { // 业务逻辑 Thread.sleep(2000); } finally { lock.unlock(); } // 使用延迟队列 RDelayedQueue<String> delayedQueue = redisson.getDelayedQueue( redisson.getBlockingQueue("delayedQueue")); delayedQueue.offer("订单关闭消息", 30, TimeUnit.MINUTES); redisson.shutdown(); } }这里有一个关于分布式锁的重要细节。上面示例直接调了lock.lock(),没有传leaseTime参数,这时候Redisson会启动一个看门狗任务:如果锁在10秒内没有被主动unlock(),看门狗会自动把锁的过期时间续期到30秒,并且每10秒检测一次是否需要继续续期。这个机制能防止业务执行时间较长时,锁被提前释放导致并发问题。但如果你在lock.lock(10, TimeUnit.SECONDS)里手动指定了过期时间,看门狗就不会启动,到时间锁自动释放。这两种模式各有适用场景,手动指定适用于可以预估业务耗时的场景,看门狗适用于耗时不可控且允许一直持有锁的场景。但看门狗也有本身的局限:它基于当前客户端实例内部的定时任务执行续期,如果客户端进程自身发生长时间的GC停顿,或者网络长时间不问,续期可能失败。分布式锁本身不是“完美无锁”,它是在可用性和一致性之间做的权衡。所以在关键业务里,锁的代码必须配合合理的异常处理、超时控制和业务幂等性设计,不能把分布式锁当成唯一防线。3.5 Spring Boot集成时的选择与序列化迷宫绝大多数Java项目都跑在Spring Boot上,这里面的集成方式决定了你在生产环境里到底有多省心。先明确一点:Spring Data Redis是一个抽象层,它本身不实现连接,而是委托给底层客户端。spring-boot-starter-data-redis默认引入Lettuce,所以如果你引入这个依赖后配置了spring.redis.*,你实际上就在用Lettuce。Spring Data Redis中有两个核心组件:RedisTemplate和StringRedisTemplate。后者只能处理字符串,前者是泛型模板,默认使用JDK序列化。JDK序列化有两个痛点:数据可读性极差,在Redis里看到的是类似\xAC\xED\x00\x05t...的乱码;而且序列化后的字节数组体积巨大,浪费内存。因此,几乎每个Spring Boot项目都要自定义RedisTemplate的序列化方式。我给出一个常用的配置类:import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // key使用字符串序列化,可读性好 StringRedisSerializer stringSerializer = new StringRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); // value使用JSON序列化,避免JDK序列化的乱码和体积膨胀 GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }为什么key和value要分开设置序列化器?因为大多数场景下,key就是业务标识,用字符串保存便于在Redis客户端里直观检查;value是业务对象,用JSON序列化让它具备可读性,同时体积远小于JDK序列化。GenericJackson2JsonRedisSerializer相比Jackson2JsonRedisSerializer更推荐,因为前者在反序列化时会携带@class类型信息,避免多态类型丢失的问题。踩过的坑提醒一下:如果Value是null,某些序列化器会直接抛异常;如果Value是LocalDateTime这样的Java 8时间类型,Jackson默认序列化会报错,需要额外注册JavaTimeModule。在实践中我一般会封装一个RedisService类,对外提供泛型方法,把类型转换和序列化细节封装在里面,这样业务代码不用碰RedisTemplate的原始API。3.6 在Spring Boot里手动集成RedissonRedisson和Spring Boot的集成可以不用第三方starter。自己写一个配置类,把RedissonClient注入Spring容器,然后需要的地方直接@Autowired或者@Resource,这种方式最灵活,不依赖starter版本兼容性。一个示例配置:import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RedissonConfig { @Bean(destroyMethod = "shutdown") public RedissonClient redissonClient( @Value("${spring.redis.host:127.0.0.1}") String host, @Value("${spring.redis.port:6379}") int port, @Value("${spring.redis.password:}") String password) { Config config = new Config(); config.useSingleServer() .setAddress("redis://" + host + ":" + port); if (password != null && !password.isEmpty()) { config.useSingleServer().setPassword(password); } // 其他参数按需配置 return Redisson.create(config); } }然后业务代码里就可以这么用:@Service public class OrderService { @Resource private RedissonClient redissonClient; public void closeOrder(String orderId) { RLock lock = redissonClient.getLock("close:order:" + orderId); lock.lock(); try { // 执行关单逻辑 } finally { lock.unlock(); } } }这里要特别注意@Bean(destroyMethod = "shutdown")的写法,确保Spring容器关闭时自动释放Redisson的线程池和连接,如果不加这个,进程退出时可能会因为非守护线程存活而无法正常结束。4. 常见问题排查与避坑实录这部分我按实际项目里遇到的高频问题整理成了一张故障速查表,每个问题都附带了排查思路和解决方法。这些坑在官方文档里基本不会写,但你在生产环境里几乎一定会碰到。4.1 经典故障速查表现象可能原因排查路径解决方案应用启动后偶发连接超时Lettuce空闲连接被服务端断开,客户端未及时感知查看异常栈中的io.lettuce.core.RedisConnectionException;用netstat检查连接状态配置合适的timeout和validateConnection;在连接工厂中启用validateConnection;升级Lettuce版本连接池耗尽,请求全部卡死maxTotal太小或连接泄露未归还查看JMX连接池指标;检查代码中getResource()与close()是否成对调大maxTotal;排查是否在异常分支漏了close();启用testOnBorrow自动剔除坏连接设置了Redis密码后Jedis连接失败密码配置方式不正确检查连接URI和Jedis构造参数new Jedis(host, port, timeout, timeout, password)或连接池构造传入密码Spring Boot存取对象后读到乱码序列化器未设置,使用默认JDK序列化在Redis Desktop Manager里查看value的二进制内容自定义RedisTemplate序列化器,改用JSON序列化用opsForValue().setIfAbsent()实现分布式锁,偶发失效setnx没有和expire绑定为原子操作;或加锁后业务时间超过锁过期时间检查锁的过期时间设置;检查异常日志改用Redisson的RLock;如果坚持自研,必须用Lua脚本保证原子性,且设置足够长的过期时间使用Lettuce时使用select切换数据库后数据写入错误共享连接上切换了数据库,污染了其他线程的状态检查代码中是否有select调用避免在多线程共享连接上使用select;不同数据库使用不同的连接;或者使用Redis Cluster/SentinelRedisson分布式锁偶尔失效看门狗续期受GC停顿或网络长时间阻塞影响查看业务执行耗时和GC日志将锁过期时间设为业务最长耗时的1.5倍以上;手动指定leaseTime;业务设计上支持幂等重试高并发下Jedis性能急剧下降连接池通过排队机制放大了RT,线程大量阻塞用jstack查看线程栈,确认线程在等待连接换用Lettuce或Redisson;或者优化连接池参数,如打开fair公平锁模式4.2 几个特别容易踩的细节先说一个关于Jedis的坑。Jedis对象执行完命令后如果忘了close(),尤其是在异常抛出时没有走finally块,连接会一直留在连接池的“已租出”状态。这个问题很隐蔽,因为单个连接泄露时,应用看起来完全正常,直到QPS上来之后突然发现连接池被耗尽,所有请求开始排队,连接池排队又加剧超时,超时又导致更多异常被抛出,形成雪崩效应。我建议每个Jedis连接操作都采用try-with-resources或者try-finally的规范写法,并且用带JMX的连接池配置,方便实时监控“借出连接数”。再看Lettuce在Spring Boot下的一个经典问题:Spring Boot 2.x默认的Lettuce连接在Redis服务器设置了timeout(比如空闲连接5秒被回收)后,客户端并不会主动感知服务端已经断开,等到下次请求到来,从连接池拿到一条死连接,直接抛出异常或触发重建连接。解决方案有两个:一是在配置里给LettucePoolingClientConfiguration设置validateConnection为true,这样每次拿连接时会主动验证连接状态;二是调整Lettuce的shutdownTimeout和shutdownQuietPeriod参数。实际上Spring Boot中更简单的做法是配置spring.redis.lettuce.pool.max-active等参数,同时确保服务端timeout不要设置得太短,否则连接频繁被断开,重建开销不可忽视。Redisson方面我想重点说的是分布式锁的“可重入性”。Redisson的RLock默认就是可重入锁,同一个线程可以多次加锁,但加锁几次必须解锁几次。如果你只解锁一次,锁的“重入计数”没有归零,锁不会真正释放,别的线程永远拿不到锁。这个坑在代码里很容易出现:我们常常在方法A里加锁,然后方法A内部调用了方法B,方法B也加了同一把锁,最后方法A和B的finally里都调用了unlock(),结果第二次解锁直接抛IllegalMonitorStateException,因为锁已经释放了。正确做法是每个lock()对应一个unlock(),并且确保解锁次数与加锁次数一致。还有一个关于延迟队列的坑。Redisson的RDelayedQueue本质上是依赖一个后台线程和Redis的ZSET实现的,它的消息不会“精确到毫秒”地准时到来,实际消费时间可能会有几百毫秒甚至数秒的偏差。如果你的业务对时间精度要求极高,比如精确到秒级别的定时任务,建议直接使用更专业的任务调度平台,而不要把延迟队列当作时间精确的定时器。4.3 高效排查的工具与思路排查Redis客户端问题,我按经验分成三个步骤。第一步,看监控指标。连接数、命令执行失败数、命令平均延迟、TCP重传率、Redis服务端的instantaneous_ops_per_sec、connected_clients这几个指标,基本能定位到“是客户端问题”还是“是服务端问题”。比如connected_clients很高但ops很低,说明客户端建了连接却没用,可能是连接池配置过大或连接泄露。反过来ops很高且延迟大,先看慢查询日志,确定有没有大KEY操作或复杂命令拖慢了Redis。第二步,抓线程栈。当应用出现卡顿、超时,用jstack抓一下Java进程的线程栈,重点关注阻塞在Jedis连接获取、Netty事件线程、以及Redis命令执行上的线程。如果大量线程阻塞在JedisPool.getResource(),说明连接池不够或耗尽了;如果阻塞在SocketInputStream.read(),说明命令发出去了但响应很慢,问题在Redis服务端或网络链路。第三步,开日志。把Lettuce的日志级别调到DEBUG(尤其是io.lettuce.core.protocol),可以看到每个命令的发送和响应详情;Redisson可以通过org.redisson包下的日志观察锁的获取和释放过程。生产环境可以临时开短时间的DEBUG日志,定位后立刻关闭。5. 选型和切换的最终建议写到这里,我把这三个客户端从底层原理、代码示例到排查经验都过了一遍。如果让我给一个最终的选型建议,我的倾向是这样的:如果你在做简单项目、工具类脚本、或者是对Redis命令级别的操作要求很细的控制,用Jedis,简单直接。如果你在做Spring Boot项目,默认用Lettuce就好,它是官方默认,线程安全,性能也好,日常缓存、分布式会话、计数器这些场景完全够用。如果你的系统已经上了微服务架构,需要分布式锁、分布式集合、延迟队列这些能力,别自己造轮子,直接用Redisson。不用过分纠结“哪个好哪个坏”,这三个没有技术代差,只有定位差异。我自己就维护过不同时期的不同项目,有的项目用Lettuce做基础缓存,同时在关键分布式锁场景里单独引入Redisson,两者共存也没有问题。只要你理解每个客户端的底层模型和适用边界,它们在同一个系统里各司其职是完全合理的。最后分享一个实操上的体会:不管选哪个客户端,一定要把连接配置参数写清楚注释,并且纳入监控面板。连接池和连接超时这类参数,平时不出问题,一出就是大问题,而且常常是在凌晨出问题。把参数设计得有据可依,把监控提前做好,比换来换去选型要重要得多。