☰
SpringBoot整合Redis实战:序列化、缓存穿透与分布式锁
2026/9/26 22:52:43 网站建设 项目流程

1. 先把环境弄明白:Redis装不好,后面全是坑

这个月好几个朋友问我SpringBoot连Redis的事,有的是连不上,有的是存进去取出来乱码,还有一些是存个对象直接报序列化错误。问题五花八门,但归根结底,大部分人在动手之前根本没用过Redis,也不知道Redis装起来到底是个什么东西。

这里先给个整体概念。Redis是内存数据库,跑起来之后默认监听6379端口。SpringBoot要做的事情,就是通过spring-boot-starter-data-redis这个依赖,让你的Java程序能连上它、发命令、收数据。SpringBoot本身没有任何魔法,它只是把Jedis或者Lettuce(这两个都是Java的Redis客户端)包了一层,让你少写一堆样板代码。

这个内容能帮你解决什么?环境搭建、SpringBoot配置、序列化问题、分布式锁、多环境部署,还有我在实际项目里踩过的那些配置文件和工作排查的坑。

这两个客户端的区别值得说一下。SpringBoot 2.x之后默认用的Lettuce,它底层是Netty,多路复用,高并发下连接利用率高。Jedis是阻塞IO,并发高的时候得靠连接池撑着。网上很多教程还在教怎么配Jedis连接池,其实SpringBoot 2.x以后,用Lettuce的话,默认共享连接就是多路复用的,你配一个很大的池子意义不大,除非你有特殊场景。

接下来先搞定Redis本体。这个前置步骤完不成,后面SpringBoot层面全是白搭。

1.1 Linux环境安装最顺畅的姿势

看官方文档我个人最推荐的方式是源码编译安装。别怕编译这两个字,其实就是三行命令的事。先到Redis官网下载稳定版,一般次版本号是偶数的那个就是稳定版,比如6.2.x、7.0.x。奇数版本号(比如6.3、7.1)是开发版,生产环境别碰。

wget https://download.redis.io/releases/redis-7.0.11.tar.gz tar xzf redis-7.0.11.tar.gz cd redis-7.0.11 make make install

编译完,make install会把可执行文件放到/usr/local/bin目录下,包括redis-server和redis-cli。然后你起服务的时候,不要直接裸跑redis-server,那会在前台一直挂着,终端一关就没了。标准做法是改配置文件然后后台启动:

cp redis.conf /etc/redis.conf sed -i 's/^daemonize no/daemonize yes/' /etc/redis.conf redis-server /etc/redis.conf

这里daemonize yes是让Redis在后台运行。用这种方式启动,日志默认会打到stdout,但后台模式下就没有stdout了,所以生产环境还必须把logfile的路径配上,比如logfile /var/log/redis.log,不然出了故障你连排查的依据都找不到。

1.2 用Docker的人,主从配置能省一半事

要是你平时就是Docker环境开发,装Redis其实更省事,尤其想做主从复制或者集群的话。一条命令跑单机:

docker run -d --name redis \ -p 6379:6379 \ -v /myredis/data:/data \ -v /myredis/conf/redis.conf:/etc/redis/redis.conf \ redis:7.0 \ redis-server /etc/redis/redis.conf

注意挂载配置文件的写法。不挂载配置启动,就是默认配置,持久化策略、密码、最大内存全都没有,裸奔状态。另一个细节,Docker里的Redis容器默认是以root跑的,如果配置文件里开了protected-mode,你在宿主机上用客户端连,会被当成外部访问给弹回来。实际项目里还是要配密码和bind策略,别图省事。

1.3 Windows用户的实际操作路径

我一直觉得Windows下的原生Redis官方包就停留在一个老版本上(目前Windows版Redis停在了5.0.x),公司电脑没法装WSL或者不想用Docker的时候,有两个选择:

  1. 去Redis官方GitHub仓库的Windows分支找归档zip包,双击运行redis-server.exe就能用。
  2. 用choco install redis-64或者直接去下载Redis绿色解压版,运行方式都一样。

Windows下用redis-cli做连通性测试:

redis-cli -h 127.0.0.1 -p 6379 ping

这个命令如果能回复PONG,说明你的Redis服务已经站起来了,可以进入下一步。

1.4 redis-cli和可视化客户端,调试阶段的命根子

官方自带redis-cli是做故障排查最快的方式之一。比如你想直接看SpringBoot塞进去的key到底长什么样:

redis-cli > KEYS * > GET yourKeyName > TTL yourKeyName

我见过太多人在SpringBoot里一脸懵,RedisDesktopManager(那个现在都叫Another Redis Desktop Manager了)或者Redis Insight这类可视化工具一打开,整个数据结构一目了然。尤其是查key过期时间、看哪些key占内存,可视化工具比敲命令直观太多。

2. SpringBoot集成Redis:配置项和后端代码一次走通

2.1 依赖引入和版本隐性问题

新建SpringBoot项目时,选依赖直接勾Spring Data Redis就行。如果用的Maven,手动加这一段:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>

需要在pom里额外引入common-pool2吗?以前很多人会加,因为在旧版本里,如果不在类路径上放一个Apache Commons Pool2,你无法使用连接池。但是SpringBoot 2.x之后内置了Lettuce,这个依赖不再必须。除非你主动配置了commons-pool2相关的连接池参数,否则不需要加。加上了也不会出错,但没必要。

因为RedisTemplate需要Jackson序列化的话,那样还得再引入jackson的依赖,这个后面序列化章节会细说。要注意的隐性问题,SpringBoot的starter版本要跟你的SpringBoot主版本对应,这个一般不用管,SpringBoot BOM会管好。真正容易出问题的,是你自己引了jedis版本,跟SpringBoot管理版本冲突导致运行时NoSuchMethodError,这个是我的亲身经历,代码里加了jedis坐标但没写version,后来被SpringBoot BOM指到了老版本,方法找不到直接炸了。

2.2 YAML配置:单机、密码、多库全参数

SpringBoot里Redis的自动配置只需要在application.yml里写spring.redis开头的配置,这里给一份我常用的完整参数:

spring: data: redis: host: 127.0.0.1 port: 6379 password: yourpassword database: 0 timeout: 3s lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 max-wait: 2s

注意SpringBoot版本差异:SpringBoot 2.x用spring.redis,SpringBoot 3.x用spring.data.redis。这个问题是在项目升级的时候最容易被忽略的,迁移到3.x之后原本的配置静默失效,然后你在那边排查半天为什么连了别人的Redis。

参数解释一下:

参数作用建议值
hostRedis服务地址局域网用内网IP,别用localhost
port6379默认按需修改
password连接密码生产必配
database默认16个库,用哪个单机环境0-15,建议各业务分开
timeout连接超时别配太长,3秒足够
pool.max-active最大活跃连接数16-32之间我就满意了
pool.max-wait拿到连接的最大等待毫秒数2秒上限,别让线程死等

关于timeout参数,Redis官方凉了,就是客户端跑个ping,对方没回就等。你把它设成分钟级,等客户端把连接都给耗死,系统就直接卡死了,听我的,3s到头了。

2.3 缓存注解的开关:EnableCaching别漏

很多刚用Redis做缓存的兄弟,在Service方法加@Cacheable,然后发现根本没生效。原因多半是忘了在主类加@EnableCaching注解。

@SpringBootApplication @EnableCaching public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }

这个注解是Spring的Cache抽象总开关,不加它,Spring容器里根本不会注册RedisCacheManager,所有的缓存注解都会形同虚设。别问我是怎么知道的,我一开始也是漏了它,排查了两小时,日志干干净净,最后发现是总开关没开。

2.4 StringRedisTemplate和RedisTemplate到底用哪个

SpringBoot默认配好了两个Bean:RedisTemplate<Object, Object>和StringRedisTemplate。很多人上来就直接@Autowired RedisTemplate,觉得模板名字大就完事了。但实际上,RedisTemplate默认用的是JdkSerializationRedisSerializer,存进去的字符串会带一堆乱码类型前缀,数据看着就是\xAC\xED\x00\x05t\x00\x05hello这种,非常难看,而且跨语言客户端(比如Python去读)根本解析不了。

而StringRedisTemplate所有key和value都按String处理,读写干净、直观。

我的建议:能用StringRedisTemplate就用,尤其做缓存、存JSON、计数器这种场景。如果非要用RedisTemplate存对象,那你必须自定义序列化器,这个下一节展开讲。

@Service public class UserService { @Autowired private StringRedisTemplate stringRedisTemplate; public void cacheUser(String userId, String json) { stringRedisTemplate.opsForValue().set("user:" + userId, json, 30, TimeUnit.MINUTES); } }

比如热点用户信息,你在Service层已经把对象转成了JSON字符串,这种情况下存String就够了,根本不需要序列化器介入。

3. 序列化问题深挖:乱码、ClassCastException和可读性

3.1 同样存一个字符串,进Redis的差别有多大

直接上一段对比。用RedisTemplate和StringRedisTemplate各写一个字符串:

stringRedisTemplate.opsForValue().set("hello", "world"); redisTemplate.opsForValue().set("hello", "world");

然后用可视化客户端一看:

  • StringRedisTemplate写入的:key是hello,value是world,非常干净。
  • RedisTemplate写入的:key是\xAC\xED\x00\x05t\x00\x05hello,value是\xAC\xED\x00\x05t\x00\x05world。

这个\xAC\xED开头的东西,就是Java序列化写入的Magic Number(STREAM_MAGIC)。Redis只是个存储方,它不知道你存的是Java序列化字节还是字符串。你拿Redis命令行或者其它语言客户端去读,看到的自然是一堆乱码。

如果两边都用SpringBoot程序自己读,倒还能读回来,因为反序列化用的也是同一套Java序列化规则。但读出来的东西是Object,用的时候要强转,而且如果你实体类字段变了、版本号变了,反序列化直接报错,缓存直接废掉。

3.2 自定义RedisTemplate序列化配置的正确打开方式

更通用做法是自定义一个RedisTemplate,key用StringRedisSerializer,value用Jackson2JsonRedisSerializer,这样存的value就是可读的JSON字符串。配置类大概长这样:

@Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); StringRedisSerializer stringSerializer = new StringRedisSerializer(); GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(stringSerializer); template.setHashKeySerializer(stringSerializer); template.setValueSerializer(jsonSerializer); template.setHashValueSerializer(jsonSerializer); template.afterPropertiesSet(); return template; } }

这里我推荐用GenericJackson2JsonRedisSerializer,它跟Jackson2JsonRedisSerializer的区别在于,前者会在JSON里带上@class类型信息,这样反序列化的时候才能还原成对应的Java对象。后者需要你自己指定目标类型,不指定类型它反序列化出来就是个LinkedHashMap。

但是注意,带类型信息也意味着写入的内容里每条数据体积会大一些。数据量小的无所谓,海量数据、追求极致内存效率的场景,就要考虑这个额外开销了。

3.3 常见报错LocalDateTime序列化失败

用Jackson序列化框架处理实体类,如果实体里有LocalDateTime、LocalDate这种Java 8时间类型,直接序列化会报错,因为Jackson默认不支持这些类型。解决办法是注册JavaTimeModule,并且对日期格式做统一处理。

最省事的方式是全局配置ObjectMapper:

@Configuration public class JacksonConfig { @Bean public ObjectMapper objectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new JavaTimeModule()); mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); return mapper; } }

这样整个项目里的RedisTemplate、消息队列、HTTP消息转换器都会统一走一套序列化规则。推荐把这个配置放在项目公共模块里,而不是每个微服务都单独写一份,不然各自为政,很容易出现A服务写入的格式跟B服务读出来的格式对不上。

3.4 跨语言/跨模块读数据的注意点

如果你的Redis数据要被多个服务甚至多语言栈共享,Java序列化字节流就会成为一个灾难。Python、Go、Node.js都没法直接解析Java的序列化内容,它们拿到的就是一坨字节,还得手工剥。所以,任何在服务间共享的数据,我强烈建议统一JSON格式。

另外一个小坑是泛型擦除。GenericJackson2JsonRedisSerializer反序列化时是靠写入的@class字段来还原类型的,你读的时候再指定泛型其实没太大意义。但是如果你用的是Jackson2JsonRedisSerializer并且指定了对象类型,比如new Jackson2JsonRedisSerializer<>(User.class),那么存入List<User>的时候,反序列化出来就会变成一个LinkedHashMap集合,然后你在强转的时候直接ClassCastException。这个中文社区里被问烂了,用Generic版本能绕过去。

4. 实际业务里怎么把Redis用好:缓存击穿、分布式锁与其它

Redis在SpringBoot里除了当缓存,做分布式锁和限流是高频场景。这一部分不给你抄代码就讲怎么设计,我贴几个能直接落地的方案。

4.1 商品详情页的缓存更新与过期策略

典型场景是商品详情页。数据放Redis里,key形如product:info:{id},缓存时间能定30分钟。但商品修改了价格,那就得做到先更新数据库,再删除Redis缓存。

public void updateProduct(Product product) { // 1. 先更新数据库 productMapper.updateById(product); // 2. 再删除缓存 stringRedisTemplate.delete("product:info:" + product.getId()); }

为什么要删除而不直接更新缓存?因为更新缓存要处理并发写入顺序,容易跟库里的数据不一致;删了缓存,下次请求进来自然会重新加载数据库最新值。这个套路叫Cache Aside Pattern,简单可靠。

但这里又有新的问题,读到热点数据,并发高时大量请求同时发现缓存失效,全部打到数据库,就叫缓存击穿。可以用逻辑过期结合互斥锁去玩,简单的做法是加锁,只放一个请求去DB里拉数据并回填缓存,其余请求等待或走降级。

public Product getProduct(String id) { String json = stringRedisTemplate.opsForValue().get("product:info:" + id); if (json != null) { return JSON.parseObject(json, Product.class); } String lockKey = "lock:product:" + id; String lockValue = UUID.randomUUID().toString(); Boolean locked = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 10, TimeUnit.SECONDS); if (Boolean.TRUE.equals(locked)) { try { // double check json = stringRedisTemplate.opsForValue().get("product:info:" + id); if (json != null) { return JSON.parseObject(json, Product.class); } Product product = productMapper.selectById(id); stringRedisTemplate.opsForValue().set("product:info:" + id, JSON.toJSONString(product), 30, TimeUnit.MINUTES); return product; } finally { // 防止误删别人的锁 if (lockValue.equals(stringRedisTemplate.opsForValue().get(lockKey))) { stringRedisTemplate.delete(lockKey); } } } else { // 没抢到锁,等一下再取缓存(也可以降级返回旧值) try { Thread.sleep(50); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return getProduct(id); } }

这段代码里,会注意到我在finally里做了一次“值匹配再删除”。如果直接stringRedisTemplate.delete(lockKey),存在一个经典场景:线程A加锁,处理时间超过锁的过期时间,锁自动释放,线程B拿到锁,然后线程A处理完,把B的锁给删了,这时候并发就彻底失控了。所以删除锁之前要先确认持有者是自己。这个场景真的是面试高频题,实际项目里也真实存在。

锁的过期时间要设置多久?我一般遵循“预估最大执行时间的3到5倍”这个原则。比如我预估数据库查询和回填最多500ms,我就设2到3秒。设太短,业务还没跑完锁就没了;设太长,万一宕机锁一直挂着,别的请求全都过不来。

4.2 Redisson:不想手写锁,就让别人帮你踩完坑

如果觉得上面那个写法工程量大,直接用Redisson。Redisson是一个Redis客户端框架,它对分布式锁做了非常多底层细节处理,包括看门狗自动续期。

<dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.27.2</version> </dependency>

基本用法:

RLock lock = redissonClient.getLock("product:" + id); boolean locked = lock.tryLock(2, 10, TimeUnit.SECONDS); if (locked) { try { // 业务操作 } finally { lock.unlock(); } }

Redisson做的最好的一点是,看门狗机制:只要你的业务线程还活着,默认锁的有效期30秒,但Redisson会在锁快过期时自动续期,避免业务执行超过锁时间导致锁提前释放。这是手写Redis分布式锁时特容易踩的坑。

但不建议为了用一个锁就全局引入Redisson。如果你的项目只需要简单的短任务互斥,用原生RedisTemplate配合原子操作就够了,Redisson的功能确实多,但是会跟着额外开销和分布式场景之外的隐形复杂度。

4.3 不同环境切换:dev、test、prod和配置文件

我们最早讲过SpringBoot 2.x和3.x的配置前缀问题。那当你从开发环境切到生产环境,密码、地址、数据库编号全都变了,怎么管理?

用spring.profiles.active来做多环境配置是最干净的做法:

# application.yml spring: profiles: active: dev
# application-dev.yml spring: data: redis: host: 127.0.0.1 port: 6379 database: 0
# application-prod.yml spring: data: redis: host: 10.0.0.15 port: 6379 password: ${REDIS_PASSWORD} database: 0

生产密码从环境变量里读取,不要明文写在git里。这个习惯可以救你一命。我遇到过最揪心的,就是同事把生产Redis密码硬编码到配置文件里,然后整个配置文件的模板在内部仓库里漂了两年,换个领导就能看到。可以用${REDIS_PASSWORD}这种占位符,在部署平台上注入环境变量,这样配置文件即使泄露也没有密码原文。

4.4 那种复杂查询的结果还需要Redis吗

事务型操作、复杂SQL查询结果,能塞Redis缓存吗?当然能,但缓存之后缓存一致性成本需要理清楚。查出结果集二十行,缓存TTL设30分钟,业务能容忍30分钟以内的延迟。可业务要求实时性非常强的话,缓存就变成负担了。

我的经验是,能当缓存用的数据,天然容忍脏读一段时间。比如文章阅读量、商品库存(准确说库存不能只看缓存),这种就适合放。如果每个请求都要求读到实时的数据,你老老实实走数据库,别硬上Redis。Redis是锦上添花,不是雪中送炭。

5. 进阶:Redis常见故障场景和联合治理

5.1 Redis连接瞬间爆掉:连接池参数进阶策略

真实生产环境中,经常会遇到Redis连接数突然飙升到连接池上限,然后报RedisConnectionFailureException: Unable to connect to Redis。这种问题的根源多半不是Redis本身挂了,而是连接池看门狗没起作用,或者上游一个慢查询拽了整个服务。

排查思路分几步走:

  1. 先把连接数情况查看一遍:redis-cli info clients看看connected_clients最大多少。
  2. 看最大连接数配置:redis.conf里的maxclients,默认是10000。
  3. 排查你的客户端是Lettuce还是Jedis,有没有开连接池验证:Lettuce底层可以复用同一个连接,如果某个命令很慢,这个连接的后续命令全部排队,一堵全堵。

当你确认是应用侧连接池配置不合理时,再调整池参数。但也要注意,Lettuce的共享连接模式跟连接池参数并不完全等同,它是对Netty事件循环的抽象。我之前有个项目并发很高,默认值也够用,但后来突然挂了一次,最后一查,是有一个定时任务每30秒扫描一次全量keys,命令阻塞了Redis单线程,其他请求全部排队。这是Redis单线程模型最怕的场景:慢命令。

5.2 Keys命令的替代方案:生产环境的SLA要靠Scan保证

先强调一个核心认知,Redis是单线程架构,所有的命令都是串行执行的。KEYS *会把所有满足条件的key一次性扫描出来,当key数量达到几百万,这条命令就会阻塞Redis事件循环,从几十毫秒到几秒不等,期间所有客户端请求都会排队。线上这么玩一次,你的业务方就要来敲你桌子了。

线上如果要查哪些key匹配某个前缀,用SCAN命令。它采用的是游标方式,每次返回一小部分key,不会阻塞整个服务。

redis-cli > SCAN 0 MATCH user:* COUNT 100

这个COUNT 100是提示命令每次大概扫描的桶数,不是返回100条。用客户端脚本循环调用直到游标返回0为止。包括SpringBoot里,也推荐配合ScanOptions去做扫描,而不是直接keys()。

Java侧的正确写法:

Set<String> keys = new HashSet<>(); ScanOptions options = ScanOptions.scanOptions().match("user:*").count(100).build(); try (Cursor<byte[]> cursor = stringRedisTemplate.getConnectionFactory().getConnection() .scan(options)) { while (cursor.hasNext()) { keys.add(new String(cursor.next())); } }

这个方式的优点是每次拉一批、不阻塞,适合生产环境安全地做数据清理或者模糊匹配。

5.3 大key和热key:Redis变慢的两大元凶

Redis里单个key的value特别大(超过1MB或者集合元素超过1万),就称作大key。像直接将用户一整年的流水全部塞进一个list,访问一次就要传输大体积数据,网络IO和内存IO一起遭殃。而且删除大key也可能卡顿。

实践中,对集合类大key,可以拆成多个分片key,或者转移部分数据到数据库。热点key的典型场景是秒杀商品、微博热搜,同一个key被海量请求读,Redis单实例CPU突然暴涨。这时候可以做本地缓存兜底,比如在SpringBoot应用里加一层Caffeine缓存,短时间的重复请求直接在应用内命中,Redis的压力一下就能降下来。

5.4 缓存穿透:Redis和数据库双重重压的防御方案

穿透和击穿是两兄弟,穿透是说每次请求查一个根本不存在的数据,Redis拿不到、数据库也没有,然后每次请求都打到数据库上。如果有人故意用不存在的ID批量刷,数据库会被拖垮。

常用防御手段:

  1. 空值缓存:数据库查不到的时候,在Redis里存一个空对象,TTL设短一点(比如60秒),这样后续相同请求直接命中空值,不会穿透到数据库。
  2. 布隆过滤器:在缓存前面加一道过滤器,把不存在的key直接挡在门外。SpringBoot里可以用Redisson自带的RBloomFilter,也可以自己用BitMap做。
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("productFilter"); bloomFilter.tryInit(1000000L, 0.01); boolean mightExist = bloomFilter.contains("product:99999");

布隆过滤器说“不存在”就一定不存在(它的误判只会发生在“存在”这一侧),所以拿这个来挡绝对穿透是很可靠的。不过布隆过滤器的维护有一个点,就是数据新增时要记得往过滤器里加对应的key,不然新数据会被自己误伤。

6. 从开发到上线:改完配置之后,验证手段和常见问题排查

6.1 本地自测的五个动作

每次改完Redis配置,个人建议最少跑一遍下面的验证:

  1. redis-cli -h host -p port ping,确认Redis可达。
  2. redis-cli -h host -p port auth password,确认密码正确。
  3. 启动SpringBoot应用,看日志里有没有报连接错误。
  4. 用StringRedisTemplate写一个测试接口,写入一个key再读出来,确认读写正常。
  5. 用内存监控工具(如INFO memory查看used_memory)确认没写怪东西。

6.2 连接超时、密码错误和序列化失败的三类现场

连接超时:

报错特征:RedisConnectionFailureException: Unable to connect to Redis; nested exception is io.lettuce.core.RedisConnectionException

排查顺序:

  • Redis服务是否真的在运行?ps -ef | grep redis。
  • 端口是否被占用?ss -lntp | grep 6379。
  • 防火墙是否放行?云服务器的话,安全组规则也别忘了。
  • SpringBoot配置的host是不是指向了错误的地方?

在IDEA里跑SpringBoot然后连不上,最常见原因是localhost和127.0.0.1的区别,以及本机Redis用了非默认端口但配置里写了6379。

密码错误:

报错特征:ERR Client sent AUTH, but no password is set(Redis没设密码但你发AUTH了)或者WRONGPASS invalid username-password pair(密码真的不对)。

这两种情况都看一眼配置文件和Redis端requirepass配置。另外,SpringBoot 3.x的spring.data.redis.password如果没设置,Lettuce会尝试AUTH吗?不会,只有你显示配置了才发。所以这个报错一般是配置了对不上。

序列化错误:

报错特征:java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.xxx.User

这其实就是之前讲的,读取时用Jackson2JsonRedisSerializer指定了类型,但实际集合类型丢了。解决方案:要么用GenericJackson2JsonRedisSerializer重新序列化,要么读取时不要直接强转集合泛型,手动用ObjectMapper去转。

6.3 一个完整项目里,Redis相关代码的目录结构设计

有些兄弟把Redis操作全写在Controller里,看起来感觉很爽。但是项目一大了之后,更建议沉淀出单独的RedisService,甚至按业务域拆RedisKey常量类。这样集中管理key前缀、过期时间、序列化方式,后面改起来不痛苦。

一个参考的目录结构:

com.example.project ├── common │ ├── redis │ │ ├── RedisConfig.java │ │ ├── RedisKeyConstants.java │ │ └── RedisService.java ├── service ├── controller

RedisKeyConstants里把key的设计统一管理起来:

public class RedisKeyConstants { public static final String PRODUCT_INFO = "product:info:"; public static final String USER_TOKEN = "user:token:"; public static final String LOCK_PREFIX = "lock:"; }

这样至少能保证key的格式在交付时是一致且有规律的,排查效率也高很多。

7. 我踩过最贵的几个坑,全贴在这里

最后分享几个我实际遇到过、网上教程通常不会教你的细节。

  1. RedisTemplate的afterPropertiesSet不能少。自定义RedisTemplate时,如果只是setConnectionFactory然后直接返回,没有调用template.afterPropertiesSet(),在部分Spring版本下序列化器不会初始化成功,后面读写数据就会以null serializer运行,结果是什么?运行时IllegalArgumentException: RedisConnectionFactory must not be null。这个错特别唬人,其实你明明设置了connectionFactory,但生命周期方法没触发导致产物校不过去。

  2. bigkey的思想要用到所有字段。不只是Redis单个key大,一个业务里所有key的总数量、单个key的过期时间管理、内存占比,都需要做治理。线上Redis内存可用INFO memory去看used_memory_human,如果达到了maxmemory配置,Redis会按淘汰策略(默认noeviction)拒绝写入请求,返回OOM command not allowed when used memory > 'maxmemory'。然后整个缓存系统就开始雪崩,新值写不进去,老值还在慢慢过期。

  3. 不要在事务型场景里依赖Redis事务。Redis的MULTI/EXEC事务没有回滚,也没有乐观锁,它只是简单的排队执行。你要是想保证账户余额不超扣,老老实实用原子操作DECR、INCR,或者用Lua脚本。SpringBoot的@Transactional管不了Redis,很多刚入行的同事以为开了事务数据库和缓存就能一起回滚,这是误解,Redis根本不参与数据库事务。

  4. 配置文件的空格和缩进,是YAML最大的敌人。我见过不止一次,同事从网页复制了一段Redis配置,缩进是一个Tab加四个空格,SpringBoot直接启动不了,报错mapper.parser.ParseException: while parsing a block mapping。YAML对缩进要求极度严格,要么统一用空格,要么统一用Tab,而且嵌套层级必须对齐。IDEA装一个YAML插件马上就能看见格式问题。

  5. Redis的淘汰策略选择要明白优化目标。缓存框架只默认noeviction,意思是内存满了不淘汰,直接拒绝新写入。这是一个安全策略,但对大多数缓存场景不合适。如果只是做缓存,建议配allkeys-lru,这样所有key参与淘汰,最近最少使用的先被移除,能极大降低冷数据堆积。这个配置要在redis.conf里体现,或者启动时用命令行参数传入。

关于选型我要再提一嘴。如果你的项目是Spring Boot 3.x开始的新项目,客户端选型上直接考虑用Lettuce而不是Jedis,这不是说Jedis不好,而是Lettuce的响应式支持、连接复用能力更适合现代应用架构。Jedis的优势在于简单、API直观、同步阻塞模型好理解,但SpringBoot默认配料就是Lettuce,没必要非要去换。真遇到并发上不去,先用Redis慢日志查慢命令,SLOWLOG GET,再回头调客户端配置。

最后,所有Redis相关的配置变更,我的习惯是做一次记录一次,尤其是从SpringBoot 2.x迁移到3.x的版本,spring.redis改spring.data.redis这种替换,最容易被当成无关紧要的升级忽略。你用@Value("${spring.redis.host}")这种写法在3.x里取不到值,默认值就把你带到本地去了,一个不小心,测试环境连的就是本地的Redis,数据奇奇怪怪,排查两三天才发现是配置前缀没跟上版本。升级框架这种事,配置文件的隐性变动一定要放到checklist里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询