ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot整合Redis全攻略:从环境搭建到序列化、缓存与分布式锁实战

Spring Boot整合Redis全攻略:从环境搭建到序列化、缓存与分布式锁实战 1. 整合前必须想清楚的三件事每次看到群里有人发“Spring Boot整合Redis”的求助帖我基本能猜到问题出在哪要么是Redis装上了连不上要么是存进去的中文变成乱码要么是存了一个对象取出来直接类型转换报错。这几个问题几乎是新手整合Redis时绕不过去的坎而这背后的核心原因其实就一个很多人只关注怎么把Redis“跑起来”没有先想清楚它在你项目里的定位是什么。先说清楚你要解决什么场景的问题。Redis在Spring Boot项目里最常见的用法有三类一是做缓存把热点数据验证码、用户信息、商品详情放到Redis里挡住数据库的查询压力二是做分布式场景下的共享数据存储比如多个服务实例需要读写同一份数据用Redis的String、Hash结构做共享会话或计数三是做分布式锁解决多实例并发下的资源竞争问题。这三类场景对Redis的配置要求不太一样但基础配置和工具类是可以共用的所以本文我会先把底座搭好再针对高频场景给到可直接落地的写法。再说第二个问题你打算用到什么程度如果只是本地开发联调Windows装个Redis Desktop Manager连上就能搞如果是要上生产Docker部署Redis、配置持久化、设置密码这些都得考虑进去如果是做毕业设计或者小型项目那提升点主要放在缓存注解、过期策略和序列化方案上这些能在答辩时讲出东西来。所以我建议你按“本地可运行 → 配置规范化 → 工具类封装 → 缓存场景实战”这个路径走而不是上来就啃源码那样反而容易被劝退。第三个问题也很关键你对Spring Boot的自动装配到底了解多少很多人整合Redis失败是因为不知道Spring Boot的Redis依赖是基于“约定大于配置”的思想工作的。你只要引入了spring-boot-starter-data-redisSpring Boot会自动帮你创建RedisTemplate和StringRedisTemplate这两个Bean并且自动读取application.yml里的连接配置。但你如果直接用Spring Boot默认创建的RedisTemplate多半会在存储对象时踩到序列化的坑。所以与其说“整合Redis”不如说“你要学会接管RedisTemplate的默认行为”这也是这篇文章要讲清楚的主线。2. Redis环境准备别再纠结Windows版了2.1 Redis的本地安装与启动先从最基础的环境说起。Redis官方其实并不支持WindowsWindows版是微软团队维护的一个移植版本版本号相对落后而且存在一些坑。但你说非要本地跑起来做开发测试Windows版也完全够用。这里有两种方式我都试过给你对比一下方式优点缺点适合场景Windows安装包tporadowski/redis双击安装一键启动版本滞后无法模拟生产本地快速联调Docker容器运行Redis版本随官方更新环境隔离需要安装Docker初次配置略有门槛接近生产环境WSL2里装Linux版Redis与官方环境一致内存开销稍大需要折腾WSL追求环境一致性如果你用Windows安装包安装完后记得打开安装目录找到redis-server.exe双击启动即可默认端口6379不需要密码。这里有个经验之谈开发环境的Redis不要设置密码Spring Boot配置里少一个password字段少一个可能的坑。等部署到测试环境再考虑加密码。如果是用Docker方式执行这一行就够了docker run -d --name redis -p 6379:6379 redis:7.0这条命令会拉取Redis 7.0官方镜像并启动容器把宿主机的6379端口映射到容器内的6379端口。启动后可以用docker ps确认容器状态再用redis-cli ping验证返回PONG说明启动成功。生产环境部署我会在后面“常见问题”章节再展开因为涉及持久化配置、资源限制和主从结构这里先不铺开讲。2.2 可视化工具Redis Desktop Manager的平替选择连接工具我推荐用Another Redis Desktop Manager简称ARDM跨平台界面清爽不像老牌的Redis Desktop Manager那样从某个版本开始收费。ARDM支持查看所有数据结构、实时监控命令、执行Lua脚本对日常开发调试足够用了。下载安装后新建连接Host填127.0.0.1Port填6379没有密码就留空点测试连接能通就完事。这个工具在后面的序列化验证环节特别有用。你往Redis里写数据后用ARDM直接看key和value的实际存储形式能非常直观地看出序列化方案到底有没有生效。比如你存一个User对象序列化配置不对的话key会变成一堆\xac\xed\x00\x05t\x00开头的乱码value是Java序列化的二进制串看着就头疼。配置对了以后key就是可读的字符串value是清晰的JSON调试效率完全不是一个级别。2.3 Spring Boot项目引入Redis依赖新建Spring Boot项目时记得在pom.xml里加上这个依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency这里要注意版本管理如果你是Spring Boot 2.xstarter-data-redis底层集成的是Lettuce客户端如果是Spring Boot 3.x依赖于Java 17以上功能差异主要体现在API上。如果你用的是Spring Boot 2.4之前的老版本默认的连接池配置是空闲的需要额外配置commons-pool2依赖和连接池参数。现在新建的项目基本都是2.7或者3.x了连接池默认开启只需要在配置文件里指定参数即可。spring: data: redis: host: 127.0.0.1 port: 6379 database: 0 timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: -1ms这段配置的意思是说Redis服务器跑在本地6379端口使用0号数据库连接超时3秒连接池最大活跃连接数8个最大空闲8个最小空闲0个获取连接不限制等待时间。这里我想强调一个容易被忽略的点连接池参数并不是越大越好max-active设得太大反而会浪费资源还会给Redis本身造成压力。一般的业务系统8到16个连接足够了除非你有非常高的并发需求比如每秒上千次的缓存操作可以适当往上调。3. RedisConfig配置类编写把序列化问题一次解决3.1 为什么要自定义RedisConfig而不是直接用默认配置现在到了整篇文章的核心RedisConfig配置类。Spring Boot默认会创建一个RedisTemplateObject, Object但它使用的是JdkSerializationRedisSerializer来做序列化。这意味着什么呢就是你存进去的key和value都会先被Java序列化成二进制字节流再写入Redis。带来的问题有三个第一key在Redis里显示为一长串不可读的乱码排查数据时非常痛苦第二Java序列化后的字节体积比原始数据大很多浪费Redis内存第三如果不同服务之间共用同一个RedisJava序列化的数据其他语言根本解析不了。所以我们的目标很明确把key的序列化器改成StringRedisSerializer把value的序列化器改成JSON格式的Jackson2JsonRedisSerializer。这样存进去的key就是可读的字符串value就是标准的JSON任何语言任何工具都能看得懂、能解析。这也是Redis在真实项目中最为推荐的序列化方案。3.2 完整的RedisConfig配置类代码直接上代码这是我实践下来最稳的一套写法import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; 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.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); // 使用Jackson序列化器处理value Jackson2JsonRedisSerializerObject jacksonSerializer new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); jacksonSerializer.setObjectMapper(objectMapper); // key使用String序列化器 StringRedisSerializer stringSerializer new StringRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(jacksonSerializer); template.setHashValueSerializer(jacksonSerializer); template.afterPropertiesSet(); return template; } }说一下这段代码里几个关键配置的作用。setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY)是让Jackson能识别所有访问修饰符的字段包括private字段这样对象的属性才能被正常序列化。activateDefaultTyping这段配置有点绕作用是在序列化后的JSON里带上类名信息。比如你存一个User对象序列化结果会变成[com.example.entity.User,{id:1,name:张三}]这样。这么做的好处是反序列化时Jackson能知道要还原成什么类型直接redisTemplate.opsForValue().get(key)就能得到User对象坏处是JSON里多了一串类名有点丑。如果你确定取数据的时候会手动转换类型也可以不加这段类型信息JSON会更干净。3.3 StringRedisTemplate和RedisTemplate怎么选很多初学者会问RedisTemplate和StringRedisTemplate到底有什么区别答案是StringRedisTemplate其实就是key和value都用String序列化器的RedisTemplate。它继承自RedisTemplateString, String是Spring Boot提供的现成封装适合存取纯字符串的场景比如验证码、token、普通文本。而咱们自定义的这个RedisTemplateString, Objectkey用String序列化value用JSON序列化适合存取对象数据。所以我一般这么建议存字符串、数字、简单文本 → 直接注入StringRedisTemplate存对象、Map、List这种结构 → 注入自定义的redisTemplate两个Bean是共存的不冲突。你可以在Service里同时注入两者按需使用。3.4 序列化方案对比为什么不用GenericJackson2JsonRedisSerializer网上还有一种流行写法是用GenericJackson2JsonRedisSerializer它和Jackson2JsonRedisSerializer功能类似都是JSON序列化区别在于对比项Jackson2JsonRedisSerializerGenericJackson2JsonRedisSerializer序列化结果需手动配置ObjectMapper自动带上类型信息灵活性高可自定义序列化规则较低类型信息固定写法存储体积JSON 类型信息JSON 类型信息易用性配置略繁琐一行代码搞定如果你追求最简代码可以直接用GenericJackson2JsonRedisSerializer代码短一截。但我个人更倾向于显式配置Jackson2JsonRedisSerializer原因也很实在手动配置ObjectMapper后后续想调整日期格式、空值处理、字段命名策略这些细节都有入口而用Generic版本相当于把控制权交给了框架。真实项目里日期格式化这种需求几乎是必然出现的以后你会感谢这个配置的。4. Redis工具类编写封装常用操作4.1 工具类的设计思路配置类解决的是序列化问题工具类解决的是“少写重复代码”的问题。Redis操作的方法非常多opsForValue、opsForHash、opsForList、opsForSet、opsForZSet加上各种过期时间的设置如果每次用都在Service层写一遍代码会显得很啰嗦。所以我通常封装一个RedisUtils工具类把高频操作收敛成常用的方法比如设置缓存、获取缓存、删除缓存、设置过期时间、分布式锁、缓存自增这些。封装思路遵循两个原则一是方法命名要见名知意set、get、delete、expire这些和Redis原生命令对齐二是健壮性优先比如设置缓存时可以同时指定过期时间获取缓存时可以处理空值情况避免NPE。下面是核心代码import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.util.concurrent.TimeUnit; Component public class RedisUtils { Resource private RedisTemplateString, Object redisTemplate; /** * 设置缓存永久有效 */ public void set(String key, Object value) { redisTemplate.opsForValue().set(key, value); } /** * 设置缓存带过期时间 */ public void set(String key, Object value, long timeout, TimeUnit unit) { redisTemplate.opsForValue().set(key, value, timeout, unit); } /** * 获取缓存 */ public Object get(String key) { return redisTemplate.opsForValue().get(key); } /** * 删除缓存 */ public boolean delete(String key) { return Boolean.TRUE.equals(redisTemplate.delete(key)); } /** * 设置过期时间 */ public boolean expire(String key, long timeout, TimeUnit unit) { return Boolean.TRUE.equals(redisTemplate.expire(key, timeout, unit)); } /** * 判断key是否存在 */ public boolean hasKey(String key) { return Boolean.TRUE.equals(redisTemplate.hasKey(key)); } /** * 自增用于计数器 */ public long increment(String key, long delta) { return redisTemplate.opsForValue().increment(key, delta); } }这里特别说明一下Boolean.TRUE.equals(redisTemplate.delete(key))这种写法redisTemplate.delete(key)返回的是Boolean对象直接if (redisTemplate.delete(key))在Java里会拆箱如果返回null会触发NPE。包装一层Boolean.TRUE.equals()就能把null情况安全处理掉。这个细节我见过不少同事踩坑写工具类时养成这个习惯能避免很多线上问题。4.2 缓存穿透、缓存击穿、缓存雪崩的解决方案工具类写好了接下来要说的就是Redis做缓存时必考、必用的三大经典问题。这既是面试高频题目也是真实项目里一定会遇到的难题。缓存穿透指的是查询一个不存在的数据缓存和数据库中都没有导致每次请求都打到数据库。解决方案是缓存空值当数据库查不到某个key的数据时依然往Redis里写一个空对象并设置较短的过期时间比如3到5分钟。public Object getWithNullCache(String key, long timeout, TimeUnit unit, CallableObject loader) { Object value redisTemplate.opsForValue().get(key); if (value ! null) { return value; } // 从数据库加载数据 Object realValue loader.call(); if (realValue null) { // 缓存空值防止缓存穿透 redisTemplate.opsForValue().set(key, , timeout, unit); } else { redisTemplate.opsForValue().set(key, realValue, timeout, unit); } return realValue; }缓存击穿指的是某个热点key在过期的一瞬间大量并发请求同时打到数据库。解决方案是用分布式锁重建缓存保证只有一个线程去查询数据库并回填缓存其他线程等待锁释放后再查缓存。这个场景下我一般直接写一个带锁的版本public Object getWithLock(String key, long timeout, TimeUnit unit, CallableObject loader) { Object value redisTemplate.opsForValue().get(key); if (value ! null) { return value; } String lockKey lock: key; String requestId UUID.randomUUID().toString(); try { // 尝试获取锁10秒内没有获取到就直接查库 boolean locked tryLock(lockKey, requestId, 10, TimeUnit.SECONDS); if (!locked) { return loader.call(); } // 双重检查获取到锁后再次查缓存 value redisTemplate.opsForValue().get(key); if (value ! null) { return value; } Object realValue loader.call(); redisTemplate.opsForValue().set(key, realValue, timeout, unit); return realValue; } finally { releaseLock(lockKey, requestId); } }缓存雪崩指的是大量key在同一时间过期导致大量请求同时打到数据库。解决方案通常有三种思路一是设置过期时间时加上随机值打破key过期的“同一性”二是热点数据不设过期时间由后台线程或定时任务手动更新三是用多级缓存兜底。加随机值的方式最简单代码里就是在设置过期时间时加一个随机数long expireTime baseTimeout new Random().nextInt(300); redisTemplate.opsForValue().set(key, value, expireTime, TimeUnit.SECONDS);4.3 基于RedisTemplate的分布式锁实现前文提到了分布式锁这里把完整的代码补上。分布式锁的核心要求有三点互斥性同一时刻只能有一个线程持有锁、安全性持有锁的线程宕机后锁能自动释放不会被死锁卡死、可重入或可识别释放锁时只能释放自己加的锁。基于Redis命令SET key value NX EX就能很好地实现public boolean tryLock(String lockKey, String requestId, long expireTime, TimeUnit unit) { return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, unit)); } public void releaseLock(String lockKey, String requestId) { String value (String) redisTemplate.opsForValue().get(lockKey); if (requestId.equals(value)) { redisTemplate.delete(lockKey); } }setIfAbsent就是SETNX命令只有当key不存在时才设置成功返回truekey已存在则返回false说明锁被占用。设置过期时间保证死锁情况下锁自动释放。释放锁之前对比requestId确保只能释放自己持有的锁防止误删其他线程的锁。这套简易分布式锁能满足大部分单Redis场景的需求。如果你们的Redis是集群部署或者对可靠性要求极高建议直接引入Redisson框架它内置了看门狗机制锁续期、可重入、公平锁等都帮你处理好了不至于自己造轮子踩坑。5. 缓存注解的配置与使用EnableCaching与自定义Key策略5.1 开启缓存注解工具类和手动操作之外Spring Boot还提供了声明式缓存注解能让你的代码更简洁。只需要三条注解Cacheable查询时先查缓存缓存没有才执行方法、CachePut执行方法后更新缓存、CacheEvict执行方法后删除缓存。使用注解前需要在启动类或配置类上加上EnableCachingSpringBootApplication EnableCaching public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }这一步很容易被忽略不加这个注解的话下面的缓存注解全部不生效而且没有任何报错排查起来非常迷惑。5.2 常用缓存注解示例以一个用户服务为例Service public class UserService { Resource private UserMapper userMapper; Cacheable(value user, key #id) public User getUserById(Long id) { // 方法体内的逻辑只有缓存没有命中时才会执行 return userMapper.selectById(id); } CachePut(value user, key #user.id) public User updateUser(User user) { userMapper.updateById(user); return user; } CacheEvict(value user, key #id) public void deleteUser(Long id) { userMapper.deleteById(id); } }这里的value user相当于缓存分区的一个前缀最终生成的Redis key是user::1这种格式。key #id是SpEL表达式取方法参数id的值作为key。当用户更新时CachePut会自动把返回的User对象写入缓存保证缓存和数据库一致删除时CacheEvict自动把对应缓存清掉。这里有一个使用注解时必须注意的坑Spring缓存注解的key和value会被CacheManager进一步包装。如果你同时自定义了RedisTemplate但缓存管理器用的是RedisCacheManager的默认配置它默认使用的序列化器仍是JDK序列化。所以很多文章会建议在引入缓存注解时同时自定义RedisCacheManager。坦白说如果项目里用注解比较多这一步确实值得做如果注解用得少直接用工具类手动管理缓存反而更可控。我在项目里的经验是查询类操作多用注解读写复杂逻辑用手动工具类这样代码最简洁也最灵活。5.3 RedisCacheManager的序列化配置如果你决定走缓存注解的路这个自定义CacheManager的配置可以帮你省掉很多排查乱码的时间Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .prefixCacheNameWith(cache:) .entryTtl(Duration.ofMinutes(30)) .serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(new StringRedisSerializer())) .serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(new GenericJackson2JsonRedisSerializer())); return RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); }这个配置的关键点在于给缓存key加上了cache:前缀设置了默认30分钟过期key用String序列化value用JSON序列化。这样缓存注解产生的数据就和手动操作的数据保持一样的存储风格不会出现一套项目里两种数据格式并存的混乱局面。6. 高频问题排查与经验总结6.1 连接失败类问题问题1Unable to connect to Redis这个报错最常见的原因是Redis服务没有启动。Windows下检查任务管理器里是否有redis-server.exe进程Docker方式就docker ps确认容器状态。其次检查端口是否被占用或者配置的host/port是否与Redis实际监听不一致。最后如果你给Redis设置了密码需要在spring.data.redis.password里填上否则认证失败。问题2连接超时RedisConnectionFailureException: Unable to connect to localhost:6379先ping一下宿主机IP和端口telnet 127.0.0.1 6379能通说明网络没问题问题出在配置上。如果是在Docker容器里启动的Redis注意是否做了端口映射-p 6379:6379缺一不可。6.2 数据乱码与类型转换问题问题3key显示为一串乱码这是典型的没走自定义序列化器。检查你是否注入了自定义的RedisTemplate以及配置类是否被Spring扫描到。有些情况下你注入了RedisTemplateString, Object但代码里用了Autowired注入StringRedisTemplate两者都有但你没有意识到它们的数据格式不同读出来的数据自然对不上。可以用Redis Desktop Manager看看key的实际存储格式乱码就是没配序列化器可读字符串说明配置生效了。问题4存对象取出来变成LinkedHashMap这是Jackson序列化的经典问题。原因是反序列化时Jackson读到的是没有类型信息的JSON它不知道要还原成User对象还是Map所以默认当成LinkedHashMap返回。解决方案是往Redis里存的时候带上类型信息就是前面代码里activateDefaultTyping那一段配置的开销所在。如果你已经存了一堆没有类型信息的旧数据一个快速的处理方式是获取时手动转类型Object value redisUtils.get(user:1); User user new ObjectMapper().convertValue(value, User.class);6.3 缓存一致性问题与性能建议问题5数据库更新了缓存还是旧数据这是缓存类项目的高频问题本质上是缓存一致性没有处理好。我的经验是读多写少的数据可以在更新数据库后主动删除缓存下次查询时重新写入也就是CacheEvict的思路这样做简单有效要求高实时性的数据可以采用“先更新数据库再删缓存”的经典策略并且给所有缓存都设置合理的过期时间作为兜底。千万不要用“先更新缓存再写数据库”这种顺序一旦数据库更新失败缓存和数据库就永久不一致了。问题6热点数据导致Redis CPU飙升如果某个key的访问量特别大可以考虑使用本地缓存如Caffeine加Redis两级缓存热点请求先走本地缓存Miss了再走Redis能显著降低Redis压力。这个方案适合那种读频率极高、数据量不大、允许短时间数据不精确的场景。6.4 生产环境的Redis部署建议最后聊一下生产环境部署。如果项目小单机Redis加AOF持久化就够了如果并发上来建议用哨兵模式做主从架构再往上就是Redis Cluster集群。Docker部署时务必挂载数据卷否则容器重建后数据全丢同时配置appendonly yes开启AOF让Redis在重启后能恢复数据。内存方面建议给Redis配置maxmemory和maxmemory-policy allkeys-lru防止数据无限增长打爆服务器内存。按我个人的项目习惯生产环境的Redis配置一般长这样spring: data: redis: host: redis-production.internal port: 6379 password: ${REDIS_PASSWORD} timeout: 3000ms lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2这套配置走的是“内部域名 环境变量注入密码 连接池边界控制”的思路密码不硬编码在配置文件里既安全又灵活。7. 写在最后的实操体会这套Spring Boot整合Redis的方案我在好几个项目里落地过从单机开发环境到集群部署都有涉及最深的体会是Redis本身的部署并不难难的是序列化方案的选型和缓存场景的边界控制。如果你能在一开始就把key和value的序列化风格定好后面所有数据的大屏监控、日志排查、跨服务调用都会顺畅得多。另外一个小技巧开发阶段可以在application.yml里加一行配置把Redis的操作日志打出来这样你能实时看到每个命令的执行情况。logging: level: org.springframework.data.redis.core: DEBUG这行配置对平时排查问题特别有帮助比如你发现缓存没生效一看日志就知道是查询命令没发出去还是结果被拦截了不用瞎猜。最后建议你把配置类、工具类、缓存管理器这三块当成一套固定模板沉淀到自己的项目脚手架里以后新建项目直接复制改一改省下的时间够你多写好几个业务功能。记住整合Redis这件事不是装个中间件那么简单把序列化、工具封装、缓存策略这些细节想清楚才是项目能不能长期稳定跑下去的关键。
返回列表