ARTICLE DETAIL

资讯详情

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

Spring Boot整合Redis实战:RedisConfig、序列化与分布式锁全解析

Spring Boot整合Redis实战:RedisConfig、序列化与分布式锁全解析 在实际项目里待久了你会发现Redis 从来不是引入依赖就能跑那么简单。Spring Boot 整合 Redis 这件事网上教程一搜一大把但几乎每个团队落地的代码都不一样最关键的分水岭就在RedisConfig和工具类的写法上。这篇东西不打算讲那种复制粘贴能跑就行的配置而是把序列化、连接工厂、缓存管理器、工具类封装这一整条链路拆开讲清楚每一步为什么这么设计以及哪些地方最容易踩坑。先说清楚这篇内容解决什么问题如果你正准备把 Redis 集成进 Spring Boot 项目或者项目里已经用了 Redis 但总觉得 key 乱码、序列化性能差、工具类用起来别扭那么这篇内容基本覆盖了从 Redis 安装、依赖引入、配置类编写、工具类封装到分布式锁和缓存注解配合的完整路径。Spring Boot 版本差异、RedisDesktopManager 查看工具、缓存穿透击穿雪崩这些高频问题也会顺带说一嘴。1. 为什么要单独写一个 RedisConfig直接用默认配置不行吗很多初学者觉得 Spring Boot 自动装配已经把事情都干了确实引入spring-boot-starter-data-redis之后容器里自动就有了RedisTemplate和StringRedisTemplate两个 Bean拿出来就能opsForValue().set()一把梭。但麻烦就麻烦在能用和好用之间隔着一层序列化方式。1.1 默认序列化方式的坑一存进去就是 \xac\xed 乱码Spring Data Redis 默认使用 JDK 自带的序列化机制来处理 RedisTemplate 的 key 和 value。什么意思就是你把一个User对象扔进去Redis 里存的可读性几乎为零打开 Redis Desktop Manager 或者keys *看一眼满屏都是\xac\xed\x00\x05t\x00...这玩意儿。JDK 序列化的问题有三层可读性极差线上排查数据的时候没法直接用肉眼判断某个 key 存的是什么。序列化后的体积明显偏大占用内存带宽压力也大。只能 Java 语言自己反序列化如果将来团队引入 Node.js、Go 或者其他语言的服务想共用同一份缓存数据JDK 序列化的数据根本没法跨语言解析。所以 RedisConfig 的第一个使命就是把默认的序列化方案替换成可读性强、体积可控、跨语言友好的方案。业界最常见的是用 JSON 格式序列化 value用StringRedisSerializer来处理 key这样 Redis 里看过去是清爽的user:1001这种格式value 也是正常的 JSON 字符串。1.2 RedisTemplate 与 StringRedisTemplate 到底该用哪个Spring Boot 自动配置里提供了两种 Template很多人搞不清区别StringRedisTemplatekey 和 value 的序列化方式固定用的是 String 序列化所以只能存 String 类型数据。适合存 JSON 字符串、普通字符串。RedisTemplateObject, Object默认是 JDK 序列化但是可以通过自定义 RedisConfig 来调整它的序列化器让它支持存对象、存列表、存 Map 等更复杂的数据结构。实际项目里我是两个都保留。StringRedisTemplate用来做简单的字符串读写RedisTemplate 用来操作对象类型的数据。但是默认的 RedisTemplate 一定要经过配置类改造否则就是一个看着能用、用起来全是乱码的隐形炸弹。2. 环境准备阶段Redis 安装与依赖引入的版本问题代码写得再好环境起不来也是白搭。这一节把 Redis 安装、依赖引入和 Spring Boot 版本匹配的问题集中理一遍这些都是新手最容易卡住的地方。2.1 本地开发环境的 Redis 怎么来生产环境一般用 Linux 服务器安装 RedisDocker 是主流方案一个docker run -d -p 6379:6379 --name redis redis就搞定了。但如果是在 Windows 上做本地开发就有两种思路第一种直接用 Windows 版本的 Redis。GitHub 上有开源项目维护了 Windows 版本的 Redis 构建产物下载解压后直接redis-server.exe就能启动简单粗暴适合本地调试。第二种用 Docker Desktop 跑容器。如果你的电脑装了 Docker用 Docker 跑 Redis 更接近生产环境而且换版本、改配置都方便。docker run -d -p 6379:6379 --name local-redis redis:7.0一条命令就够了。我个人的习惯是能用 Docker 就用 Docker版本可控清理也干净。Windows 原生版主要问题是跟进版本慢还有时候会被杀毒软件误报本地调试图省事可以接受。2.2 Maven 依赖与 Spring Boot 版本的匹配问题项目里引入 Redis 依赖很简单dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency但是这里有一个非常关键的版本问题就是 Spring Boot 2.x 和 3.x 的底层差异。Spring Boot 2.x 用的还是javax.*命名空间Spring Boot 3.x 切换到了jakarta.*这直接影响了部分第三方库的兼容性。如果你用的是 Spring Boot 3.x依赖本身没毛病但要注意连接池相关的东西。Starter 里默认集成的连接工厂是 Lettuce而不是老牌的 Jedis。Lettuce 基于 Netty 实现连接复用能力更好性能也更好这一点在 Spring Boot 2.0 之后就已经是官方默认选择。如果你确实想用 Jedis需要额外排除 Lettucedependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId exclusions exclusion groupIdio.lettuce/groupId artifactIdlettuce-core/artifactId /exclusion /exclusions /dependency dependency groupIdredis.clients/groupId artifactIdjedis/artifactId /dependency不过从实操角度看绝大多数项目用 Lettuce 就够了没必要额外折腾 Jedis。Lettuce 在高并发下的连接复用能力确实强很多Jedis 的连接池模型在 Spring Boot 默认超时配置下反而容易出现连接池耗尽的问题。接下来在application.yml里配一下连接参数spring: data: redis: host: 127.0.0.1 port: 6379 password: database: 0 timeout: 3000ms lettuce: pool: max-idle: 8 min-idle: 0 max-active: 8 max-wait: -1ms注意Spring Boot 2.x 的配置前缀是spring.redisSpring Boot 3.x 改成了spring.data.redis。这个细节在升级版本的时候特别容易踩到很多人项目启动后连不上 Redis先查的就是这个配置前缀。3. RedisConfig 核心逻辑拆解序列化、连接工厂、Bean 管理到了整篇文章最关键的部分。RedisConfig 的核心任务就两个定制 RedisTemplate 的序列化方案以及声明一些需要被容器管理的基础组件。很多博客贴出来的配置类长得很像但仔细看细节会发现各有各的问题。3.1 序列化方式的选择与组合先理清楚序列化器有哪几个StringRedisSerializer最基础key 和 value 都会变成纯字符串。简单直接性能也好。GenericJackson2JsonRedisSerializer把对象序列化成 JSON 格式并且在 JSON 里带上class类型信息反序列化的时候能还原成原始类型。Jackson2JsonRedisSerializer也是 JSON 序列化但是需要你手动指定一个类型比如new Jackson2JsonRedisSerializer(User.class)。GenericFastJsonRedisSerializer/FastJson2JsonRedisSerializer引入了 Fastjson 依赖才能用性能和泛型支持上有些争议现在项目里用得少了。组合方案上业界用得最多的配置是key 使用StringRedisSerializerhash 的 key 也使用StringRedisSerializervalue 使用GenericJackson2JsonRedisSerializer这样 Redis Desktop Manager 里看到的 key 是user:1001这样的字符串value 是{id:1001,name:张三}这样的 JSON。这里有个容易忽略的点为什么要单独给 hash key 配置序列化器因为你用 Hash 结构存数据时field 往往也是业务字段比如hset user:1001 field1 value1如果 field 也被 JDK 序列化那 Hash 结构在 Redis 里根本没法用命令行查看和操作。所以 config 里一般四种序列化器都要设置一遍。3.2 完整 RedisConfig 代码逐行解析下面是一份我在实际项目中用了很久的 RedisConfig注释都标清楚了Configuration public class RedisConfig { Bean ConditionalOnMissingBean(name redisTemplate) public RedisTemplateObject, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateObject, Object template new RedisTemplate(); template.setConnectionFactory(factory); // 使用 StringRedisSerializer 来序列化 key StringRedisSerializer stringRedisSerializer new StringRedisSerializer(); // 使用 GenericJackson2JsonRedisSerializer 来序列化 value GenericJackson2JsonRedisSerializer jsonRedisSerializer new GenericJackson2JsonRedisSerializer(); // key 采用 String 序列化 template.setKeySerializer(stringRedisSerializer); // hash 的 key 也采用 String 序列化 template.setHashKeySerializer(stringRedisSerializer); // value 采用 JSON 序列化 template.setValueSerializer(jsonRedisSerializer); // hash 的 value 采用 JSON 序列化 template.setHashValueSerializer(jsonRedisSerializer); template.afterPropertiesSet(); return template; } }ConditionalOnMissingBean(name redisTemplate)这个注解很重要。它的意思是如果容器里已经有名为redisTemplate的 Bean就跳过这个配置。这样设计的好处是如果你在某种特殊场景下想手动注册一个自定义的 RedisTemplate不会被这份默认配置覆盖。afterPropertiesSet()这行很多人会漏掉。RedisTemplate实现了InitializingBean接口在afterPropertiesSet()里会检查序列化器是否已经设置完成如果序列化器为空则使用默认值。手动 new 出来的 RedisTemplate 不调用这个方法可能出现序列化器设置不生效的诡异问题。但这里有个细节要提醒GenericJackson2JsonRedisSerializer在反序列化的时候依赖 JSON 里的class字段来还原类型。这带来了两个问题。第一数据里会多出一个class字段比如{class:com.example.entity.User,id:1001,name:张三}在 Redis 里看着有些冗余。第二如果反序列化时类路径发生变化比如包名改了、类名改了老数据就会报类找不到。这一点在缓存数据有长期存活需求时要特别注意。如果你担心class字段的问题另一个思路是统一用Jackson2JsonRedisSerializerObject但它默认不带类型信息反序列化的时候拿到的是 LinkedHashMap需要额外配置ObjectMapper来开启多态信息。配置复杂一截但可控性更强。新手建议先用 Generic 版本简单省事。3.3 Lettuce 连接工厂的配置细节RedisConfig 里还能顺手管理连接池工厂。有些场景需要定制连接工厂参数比如分库、SSL、自定义超时时间。用 Lettuce 的话可以通过继承LettuceConnectionFactory的方式在配置类里自定义连接信息Bean public RedisConnectionFactory redisConnectionFactory() { RedisStandaloneConfiguration config new RedisStandaloneConfiguration(); config.setHostName(127.0.0.1); config.setPort(6379); config.setDatabase(0); config.setPassword(RedisPassword.of(yourpassword)); LettuceClientConfiguration clientConfig LettuceClientConfiguration.builder() .commandTimeout(Duration.ofSeconds(5)) .build(); return new LettuceConnectionFactory(config, clientConfig); }这个 Bean 声明之后Spring 会把容器里的RedisConnectionFactory自动注入到 RedisTemplate 中。如果你既自定义了RedisConnectionFactory又在 yml 里配了连接参数Bean 定义优先。实际项目里我不太建议把连接参数写死在配置类里能走 ymlspring.data.redis就尽量走 yml配置类只负责处理连接池的扩展参数。毕竟运维和开发协作的时候配置集中管理才是最省心的。考虑到并发场景RedisConfig 里还可以顺手注册一个RedisTemplate的泛型版本。有些团队喜欢用一个RedisTemplateString, Object而不是默认的RedisTemplateObject, Object这样在使用时 key 的类型更明确也不容易误传一个对象进去。写法上只是泛型不同序列化配置和上面的完全一样看团队规范自行选择。4. RedisUtil 工具类的封装从常用方法到泛型设计光有一个配置好的 RedisTemplate 还不够如果每个 Service 里都直接注入 RedisTemplate然后用opsForValue()、opsForHash()这些 API代码会显得很散而且很多重复逻辑判空、过期时间设置、类型转换会到处粘贴。实际项目中基本都要再包一层工具类统一入口。4.1 工具类的设计原则与整体结构封装 RedisUtil 的时候我的建议是不要贪多求全把高频的方法做进去就够了字符串读写set、get、setIfAbsent分布式锁的基础过期时间操作expire、getExpire删除操作delete、deleteKeys自增自减incr、decrHash 结构操作hset、hget、hgetAll判断存在hasKey整套工具类基于注入的RedisTemplateString, Object实现。我把 key 的泛型固定为 String就是为了让 key 的类型清晰避免调用方误传一个对象进去。4.2 核心方法实现与说明直接给一份我实际在用的工具类核心代码不是全量但主链路都在Component public class RedisUtil { Resource private RedisTemplateString, Object redisTemplate; public boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { log.error(Redis set error, key:{}, key, e); return false; } } public boolean set(String key, Object value, long timeout, TimeUnit unit) { try { redisTemplate.opsForValue().set(key, value, timeout, unit); return true; } catch (Exception e) { log.error(Redis set error, key:{}, key, e); return false; } } public Object get(String key) { return key null ? null : redisTemplate.opsForValue().get(key); } public T T get(String key, ClassT clazz) { Object value get(key); if (value null) { return null; } if (value instanceof String) { return JSON.parseObject((String) value, clazz); } return JSON.parseObject(JSON.toJSONString(value), clazz); } public boolean delete(String key) { return Boolean.TRUE.equals(redisTemplate.delete(key)); } public long delete(CollectionString keys) { Long count redisTemplate.delete(keys); return count null ? 0 : count; } public boolean expire(String key, long timeout, TimeUnit unit) { return Boolean.TRUE.equals(redisTemplate.expire(key, timeout, unit)); } public long getExpire(String key, TimeUnit unit) { Long expire redisTemplate.getExpire(key, unit); return expire null ? -2 : expire; } public boolean hasKey(String key) { return Boolean.TRUE.equals(redisTemplate.hasKey(key)); } public long incr(String key, long delta) { try { return redisTemplate.opsForValue().increment(key, delta); } catch (Exception e) { log.error(Redis incr error, key:{}, key, e); return 0; } } public boolean hset(String key, String field, Object value) { try { redisTemplate.opsForHash().put(key, field, value); return true; } catch (Exception e) { log.error(Redis hset error, key:{}, field:{}, key, field, e); return false; } } public Object hget(String key, String field) { return redisTemplate.opsForHash().get(key, field); } public MapObject, Object hgetAll(String key) { return redisTemplate.opsForHash().entries(key); } }这里有两个容易踩坑的地方。第一get(String key, ClassT clazz)这个方法我的反序列化用到了 Fastjson。如果你不想引 Fastjson 依赖可以换成 Jackson 的ObjectMapper。但核心思路一样Redis 里存的 value 是 JSON 字符串取出来之后需要手动转换成目标对象。用GenericJackson2JsonRedisSerializer的时候因为 JSON 里自带class类型信息有时候redisTemplate.opsForValue().get(key)直接就能返回对象。但为了代码更健壮建议统一走一次显式转换。第二incr方法里注意increment返回的是 Long如果你存的是字符串数字也可以直接用它来加。但要注意如果 key 的 value 不是数字类型increment会直接抛异常所以在调用之前最好自己先确认数据结构。工具类的另外一层价值是统一了缓存操作失败不影响主流程的策略。很多团队会把 Redis 当作可降级的组件Redis 挂了业务不能挂。所以在工具类里 catch 住所有异常并且返回降级值比如false、null。这样 Redis 抖动的时候业务系统不会因为缓存异常直接报错而是走回数据库查询。5. 从缓存到分布式锁RedisConfig 的进阶应用单单一个工具类只能算基础建设Redis 在 Spring Boot 里的高级玩法主要集中在两块缓存管理器与注解的结合使用以及分布式锁。5.1 缓存管理器与 Cacheable 注解的配合很多团队会在配置类里加一个RedisCacheManager这样就能直接在 Service 方法上用Cacheable注解来管理缓存。Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) .disableCachingNullValues(); return RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); }entryTtl定义了缓存的默认过期时间disableCachingNullValues的意思是缓存 null 值不被允许。后者要特别注意因为很多查询方法在数据不存在时会返回 null如果允许缓存 null下次查询直接命中缓存的 null业务逻辑上可能会出问题。日常开发中用了Cacheable之后缓存 key 的自动生成策略默认是参数组合可读性很差。所以一般我会建议在注解里显式指定 key比如Cacheable(value user, key #userId) public User getUserById(Long userId) { return userMapper.selectById(userId); }这样 Redis 里存的 key 就是user::123。但如果你在 RedisConfig 里把 key 的序列化器配成了 String 序列化这个::分隔符是保留的。很多公司会额外配置CacheKeyPrefix把分隔符改成冒号具体看团队规范。5.2 基于 RedisTemplate 的分布式锁实现分布式锁是 Redis 使用率最高的场景之一。简单方案就是用setIfAbsent加过期时间public boolean tryLock(String lockKey, String requestId, long expireTime, TimeUnit unit) { return redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expireTime, unit); } public boolean releaseLock(String lockKey, String requestId) { String value (String) redisTemplate.opsForValue().get(lockKey); if (requestId.equals(value)) { return redisTemplate.delete(lockKey); } return false; }setIfAbsent那个方法在 Redis 里就是SET key value NX EX seconds的封装原子性有保障。requestId用来标识持有锁的客户端释放锁的时候判断一下是自己才能删防止误删别人的锁。实际生产环境中这种简单的分布式锁有一个需要注意的点如果业务执行时间超过了 TTL锁自动过期其他线程就可以获取到锁导致并发问题。应对方案是把 TTL 设得足够长或者用 Redisson 的看门狗机制自动续期。前者简单后者更优雅。如果你的项目引入了 Redisson那么这个配置类里的 RedisTemplate 就很自然地变成了 RedissonClient 与其他组件并行使用的关系。另外分布式锁的 key 也建议遵循统一的命名规范比如lock:order:pay:1001方便排查问题。6. 项目实战中的几个常见坑乱码、key 规范、性能问题最后总结几个在真实项目里反复出现的问题每一个都是我或者团队同事反复踩过的。如果说前面的内容是在教你怎么做对那这一节就是专门告诉你做错了会怎样。6.1 乱码问题排查如果你发现 Redis 里存的 key 长这样\xac\xed\x00\x05t\x00\x0euser:1001那基本就是 key 的序列化器没生效。优先检查两处RedisConfig 里setKeySerializer是否真的设置了StringRedisSerializer。代码中使用的 RedisTemplate 是不是容器里的那个 RedisTemplate。很多人在 Service 里直接new RedisTemplate()结果完全绕开了 Spring 的容器管理序列化配置自然就全丢了。排查的时候打开 Redis Desktop Manager 看最直观。如果没有可视化工具直接启动 redis-cli 用keys *也能看出问题来。6.2 key 命名规范与过期策略缓存 key 的命名直接决定排障效率。建议格式业务线:模块:功能:唯一标识 user:info:1001 order:pay:status:1001用冒号做分隔符是 Redis 社区的惯例既有层次感也能在可视化工具里分组收起。另外有些团队会用前缀区分环境比如dev:user:info:1001、prod:user:info:1001这样在同一个 Redis 实例上跑多环境时互不干扰。过期策略上不同类型的缓存 TTL 差异很大。用户基础信息可以设 30 分钟Token 类可以设 2 小时热点新闻可以设 5 分钟到一个小时。我一般的做法是读多写少、容忍短暂不一致的数据缓存时间拉长高频更新、实时性要求高的数据短一点的 TTL 甚至做强一致性方案。6.3 缓存穿透、击穿、雪崩的简单应对这三个名词每个做后端的人都被面试官问过。实际项目中应对方案也很成熟缓存穿透查询一个不存在的 key每次请求都打到数据库。最简单的方案是空值缓存就是把这个不存在的 key 也缓存起来TTL 设置短一些比如 60 秒。另外布隆过滤器是更高级的方案但需要额外引入依赖。缓存击穿某个热点 key 过期瞬间有大量请求打到数据库。解决方案是互斥锁或者逻辑过期策略说白了就是让单个请求去查库并重建缓存其他请求暂时阻塞等待。缓存雪崩大量 key 在同一时间过期导致数据库压力骤增。解决思路是过期时间加随机因子比如基础 TTL 加上new Random().nextInt(300)秒的偏移量或者采用多级缓存。这一节讲的很多内容其实是架构层面的策略和 RedisConfig 没有直接的代码关系但配置类里有一个隐藏的关联点disableCachingNullValues()这个配置会影响缓存穿透的应对方式。如果你完全依赖于Cacheable的缓存管理器来管理缓存而配置里又禁用了 null 值的缓存那么缓存穿透的空值缓存方案就没法通过注解实现了。所以空值缓存要么用 RedisUtil 手动 set要么就在配置里把disableCachingNullValues()去掉但那样又要小心缓存 null 可能带来的业务判断问题。两者各有利弊团队内部需要约定统一的做法。最后补充一点和工具封装相关的经验要区分好RedisUtil中哪些方法是给业务代码直接调用的哪些是给封装类内部使用的方法粒度不要太细。太细了会导致调用方需要组合好几个方法才能完成一个操作太粗了则不够灵活。我的经验是一类业务一个方法的粒度最舒服比如setUserCache、getUserCache这种按业务场景封装的接口比纯通用的set、get在长期维护上省心得多。Spring Boot 整合 Redis 看起来是一个很小的技术环节但把配置、序列化、工具类、缓存管理器、分布式锁串起来之后它就变成了基础设施的一部分。把基础打扎实后续无论做缓存、做会话共享、做分布式锁都是在已有的这块地基上长出来的能力。
返回列表