尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Spring Boot整合Redis实战:缓存、分布式锁与问题排查

Spring Boot整合Redis实战:缓存、分布式锁与问题排查 做Java后端的朋友应该都有过这种体会Spring Boot项目里除了MySQL最常打交道的中间件就是Redis了。不管是拿它做缓存扛高并发还是用它做分布式锁解决资源竞争哪怕是存个验证码、做排行榜Redis总能在项目里找到自己的位置。“Spring Boot整合Redis”听起来像是老生常谈但真正动手做的时候你会遇到序列化乱码、连接超时、缓存穿透、key设计不合理、数据对不上等一系列问题。我这些年在前端后端项目里来回折腾踩过的坑不少也沉淀出了一些可复用的实践套路。这篇文章就把Spring Boot整合Redis的完整步骤、核心原理、常见场景和排障经验一次性讲清楚希望能帮你在自己的项目里少走弯路。这篇内容适合正在做毕业设计、课程设计的同学也适合刚入职需要接手老项目的初级开发以及那些想系统梳理Redis使用方式的工程师。我会从环境准备、依赖集成、序列化配置、缓存场景、分布式锁到问题排查按实际操作顺序展开尽量做到“按着做就能跑起来”。1. 整体设计思路先搞清楚Redis在你的项目里到底扮演什么角色1.1 为什么几乎所有Spring Boot项目都绕不开RedisRedis本质上是一个基于内存的键值存储系统读写速度可以达到微秒级别单机QPS轻松破十万。相比MySQL这类磁盘型数据库Redis天然适合承载“读多写少”“高频访问”“短时效数据”这类流量模型。Spring Boot作为目前最主流的Java快速开发框架提供了spring-boot-starter-data-redis这个官方启动器把Redis的操作封装得极其简单——你只需要在配置文件里填几个参数注入一个RedisTemplate就能直接调用它的API完成各种业务操作。我见过很多项目最开始数据量不大、并发不高所有热点数据都直接查MySQL。但一旦用户量涨上来SQL慢查询就会开始报错数据库连接池也会被占满。这时候最简单的优化方式就是把热点数据放进Redis用内存换时间。再往后当项目从单机变成多实例部署Session共享、分布式锁、接口幂等这些需求也会跟着出现而这些恰恰都是Redis比较拿手的场景。可以说在Java服务端技术栈里Redis不是“要不要用”的问题而是“怎么用更合理”的问题。1.2 选型和前置思考别急着写代码先回答三个问题我在帮一些初学朋友看代码时发现很多人一上来就直接redisTemplate.opsForValue().set(key, value)结果到后面到处都是硬编码的key缓存过期时间随意甚至出现数据污染。其实整合Redis之前先花一点时间想清楚三件事之后会顺畅很多数据类型选对了吗Redis有String、Hash、List、Set、ZSet五大基本类型。缓存一个用户信息用String还是Hash做排行榜用ZSet是不是更合适统计UV用Set还是HyperLogLog数据模型没想清楚后面会出现大量中间代码。缓存边界在哪哪些数据需要进Redis缓存多大过期时间多久一致性要求多高如果每一次修改都要求Redis和数据库完全同步那是设计上出了问题而不是代码不够努力。部署形态是什么本地开发用单机Redis就行但生产环境要考虑主从、哨兵或集群这些都会影响配置方式和客户端选择。把这些问题想清楚了再去看Spring Boot怎么引入Redis会发现所有步骤都是有逻辑的依赖解决“用什么操作Redis”配置解决“怎么连上Redis”序列化解决“数据以什么形式存进去”而后面的场景代码则回答“存进去以后拿来干什么”。2. 环境准备Redis安装与可视化客户端选型2.1 本地开发环境的Redis安装Windows与macOS实测很多教程默认你在Linux上装Redis但实际情况是大量本地开发环境是Windows或macOS。Windows下装Redis最省事的方式是去GitHub上的tporadowski/redis项目下载免安装版或MSI安装包它长期维护Windows原生移植版版本跟得上Redis 5.x之后的主流功能。下载后直接解压redis-server.exe就是服务端双击启动即可。默认端口6379启动后命令行会打印“Ready to accept connections tcp”说明已经把服务拉起来了。为了操作方便我建议把Redis目录加入系统PATH这样你在任意目录都能执行redis-cli。macOS下更简单直接brew install redis装完用brew services start redis命令设置成后台常驻或者用redis-server /usr/local/etc/redis.conf手动拉起。这里提醒一句启动后会占用当前终端窗口开发时建议加daemonize yes配置或直接使用brew的services方式省得你每次开终端都要重来一次。2.2 生产环境推荐Docker部署版本别乱选线上部署我基本不会直接在宿主机上装Redis。Docker容器化最大的好处是环境隔离和快速迁移而且官方镜像redis质量非常高配置和版本清清楚楚。一条命令就能起一个单机Redisdocker run -d --name redis \ -p 6379:6379 \ -v /data/redis/redis.conf:/etc/redis/redis.conf \ -v /data/redis/data:/data \ --restartalways \ redis:7.2-alpine \ redis-server /etc/redis/redis.conf注意几个细节。第一版本标签不要用latest生产环境必须固定版本我一般用7.0或7.2系列的alpine版本镜像体积小安全更新及时。第二宿主机目录挂载时redis.conf要提前准备好否则容器把目录当成文件会直接报错。第三内存持久化配置要在conf里开启默认appendonly no如果不设置Redis重启后数据就全丢了这在生产环境是不能接受的。如果你的场景是“docker安装redis主从”或者更复杂的哨兵集群我建议先在一个节点上把单机跑熟再复制出多个容器配置好主从关系。主从配置并不复杂从节点配置文件里加上replicaof 主节点IP 6379主节点的数据就会自动同步到从节点。注意从节点默认是只读的写入操作会被拒绝这是正常现象。2.3 可视化客户端Redis Desktop Manager还是Another Redis Desktop Manager命令行redis-cli固然强大但日常开发和调试时我还是建议装一个可视化客户端。市面上最出名的Redis Desktop ManagerRDM前几年开始收费社区版已经很旧了新用户我不推荐。目前我更习惯用Another Redis Desktop Manager开源免费跨平台UI流畅功能也够用。这个工具连接Redis很简单填IP、端口、密码就能连上。它有几点功能特别实用支持按正则扫描key不用KEYS *就能批量处理内置Terminal面板可以直接敲Redis命令还可以查看内存分析、慢日志这对后续排查Big Key问题很有帮助。如果你习惯用IDEA做开发那Iedis或IDEA自带的database插件也可以连Redis但功能不如独立客户端丰富看个人习惯。3. Spring Boot整合Redis核心步骤依赖、配置与序列化3.1 引入依赖与基础配置Spring Boot整合Redis的核心依赖只有一个在pom.xml里加入dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency这个starter默认使用Lettuce客户端是Spring官方推荐的支持同步、异步和响应式三种模式线程安全配置合理。国内有些老项目还在用Jedis它简单直接但默认线程不安全需要配合连接池使用多线程环境下踩坑概率更高。如果你是刚起步直接用Lettuce就好。配置文件application.yml中基础项如下spring: data: redis: host: localhost port: 6379 password: 你的密码 database: 0 timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0有几点要解释清楚。第一spring.redis和spring.data.redis的区别在于版本——Spring Boot 2.x用spring.redisSpring Boot 3.x改成了spring.data.redis。如果你升级了Boot大版本却发现配置不生效大概率是这个问题。第二连接池参数不是越大越好max-active设成8在绝大多数场景够用设太大反而会浪费连接资源增加Redis服务端压力。第三database默认0号库如果项目里多业务共用一个Redis实例建议按库隔离0、1、2号库分开但生产环境更推荐用不同的key前缀来隔离因为SELECT切库在集群模式下是不支持的。3.2 序列化器选择乱码问题的根源和解决办法如果你直接往Redis里塞一个User对象再用客户端一看发现存的是\xAC\xED\x00\x05t...这种乱码那说明你踩到了序列化器配置的坑。原因很简单Spring Boot默认的RedisTemplate用的是JdkSerializationRedisSerializer它可以把任意Java对象转成二进制字节但存进Redis后人类不可读而且跨语言兼容性极差——你用Java存的数据其他系统根本没法解析。更麻烦的是这种序列化方式会把类的全限定名一起存进去一旦实体类包名或字段结构变化反序列化直接报错。解决思路是统一替换序列化器。实际项目里我常用StringRedisSerializer处理key配合GenericJackson2JsonRedisSerializer处理value。这样key变成可读的字符串value保存的是JSON文本直观且跨语言友好。具体配置如下Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); StringRedisSerializer keySerializer new StringRedisSerializer(); GenericJackson2JsonRedisSerializer valueSerializer new GenericJackson2JsonRedisSerializer(); template.setKeySerializer(keySerializer); template.setHashKeySerializer(keySerializer); template.setValueSerializer(valueSerializer); template.setHashValueSerializer(valueSerializer); template.afterPropertiesSet(); return template; } }注意HashKeySerializer和HashValueSerializer很容易被忽略如果用到Hash结构而没设置就会出现“HashMap的key是乱码”的诡异问题。另外使用GenericJackson2JsonRedisSerializer时会在JSON里额外写入class信息虽然方便反序列化但也会增加一点存储开销。如果数据敏感度比较高、不希望JSON里带类全名可以考虑用Jackson2JsonRedisSerializer配合手动指定ObjectMapper不过写法繁琐一些新手更推荐前者。还有一个经常被拿来做对比的是StringRedisTemplate。它默认key和value都用String序列化所以不存在乱码问题但这也意味着value存入前必须手动JSON.toJSONString(...)取出后需要JSON.parseObject(...)解析。说白了StringRedisTemplate更底层灵活RedisTemplate配好JSON序列化后更省事。我建议缓存对象用自定义的RedisTemplate缓存简单字符串验证码、token用StringRedisTemplate两者并存往往是最顺手的方式。3.3 Redis读取工具类封装不只为了“少写两行代码”项目里直接到处写redisTemplate.opsForValue().get(key)不是不行但很快会发现问题——key前缀没人统一、空值判断到处重复、过期时间随机设置。我更建议在整合阶段就顺手封装一个RedisUtil把公共逻辑收敛进去。这个类的大致结构是通用方法set(key, value)、get(key)、delete(key)、expire(key, timeout)底层调用redisTemplate完成。带前缀方法常量CACHE_KEY_PREFIX如project:user:每次操作key时自动拼接业务前缀。防穿透方法查询缓存为空时区分“数据不存在”和“缓存未命中”后者可以短暂缓存空值来防止下次查询继续打DB。分布式锁方法基于SETNX实现tryLock(key, timeout)和unlock(key)简单场景够用。具体实现代码网上很多模板这里不展开但封装时有一个原则值得记住不要在工具类里塞入过多业务逻辑它只处理通用的Redis数据结构操作业务层自己决定怎么串起来。这样工具类稳定、易测试、可复用。4. 核心应用场景实操从缓存到分布式锁4.1 缓存实战从手动代码到Spring Cache注解最直接的缓存用法是手动操作。比如查询用户信息前先看Redis有没有有就直接返回没有就查数据库再塞回Redis还顺手加一个随机过期时间。代码大概是public UserVO getUserById(Long userId) { String key user: userId; Object cache redisUtil.get(key); if (cache ! null) { return JSON.parseObject(cache.toString(), UserVO.class); } UserVO user userMapper.selectById(userId); if (user ! null) { // 过期时间加随机值避免同一时间大量缓存同时失效 redisUtil.set(key, JSON.toJSONString(user), 300 new Random().nextInt(60)); } return user; }这套逻辑足够清晰但问题在于每次写缓存都要重复造轮子。更优雅的方式是使用Spring Cache在启动类加上EnableCaching在方法上打Cacheable、CacheEvict、CachePut注解由框架自动管理缓存读写。比如Cacheable(cacheNames user, key #userId) public UserVO getUserById(Long userId) { return userMapper.selectById(userId); } CacheEvict(cacheNames user, key #userId) public void updateUser(Long userId) { // 修改数据库后删除缓存下次访问重新加载 }这里有一个很多新手会忽略的点cacheNames对应Redis key的前缀部分key是SpEL表达式最终Redis里的key是user::1这种带双冒号的字符串。命名规范建议统一业务模块 冒号分隔 业务ID。用Spring Cache虽然省事但别忽略序列化器配置默认的JDK序列化依然会导致不可读问题。缓存场景里还常遇到缓存一致性课题常见思路是“先更新数据库再删除缓存”。为什么不是先删缓存再更新数据库呢因为如果先删缓存紧接着有并发读请求就会把旧数据重新加载进缓存而数据库此时还没更新完缓存里就长期挂着脏数据。先更新DB再删缓存也有窗口期但概率低得多。更严格的场景可以上用“延迟双删”策略更新完数据库后删除一次缓存过几百毫秒再删一次把读请求可能写回的旧数据清掉。4.2 分布式锁从SETNX到Redisson别在简单锁上栽跟头单机部署时用Java的synchronized或JUC锁就能解决线程安全问题。一旦服务多实例部署JVM锁就失效了这就要用分布式锁。Redis实现分布式锁最常见的命令是SETNX但不要直接setnx key value然后expire单独设过期时间因为这两条命令不是原子的进程在两步之间挂了锁永远不释放。正确姿势是用一条命令把锁和过期时间绑在一起// 先尝试加锁同时设置过期时间防止死锁 Boolean success stringRedisTemplate.opsForValue().setIfAbsent( lockKey, clientId, Duration.ofSeconds(30));释放锁时也容易踩坑。如果你只执行delete(lockKey)有可能删掉的是别人后来抢到的锁。正确做法是加锁时value存一个唯一标识比如UUID或当前线程ID释放前先用Lua脚本比较value是否一致一致才删除。Spring Data Redis提供了DefaultRedisScript支持把“比较删除”写成Lua脚本原子执行强烈建议这么做。再进一步生产上直接用Redisson框架更省心。它提供了RLock使用上与JUC的ReentrantLock高度相似并且内建了“看门狗”机制如果锁的持有者没执行完看门狗会自动续期避免业务执行时间超过锁的过期时间导致提前释放。用法如下Autowired private RedissonClient redissonClient; public void deductStock() { RLock lock redissonClient.getLock(stock:lock); lock.lock(30, TimeUnit.SECONDS); try { // 扣减库存的核心业务逻辑 } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } }这里提醒一点lock.lock()是阻塞等待设置leaseTime之后即使正确释放前崩溃也不会死锁。具体用哪把锁要看业务容忍度。简单场景SETNX足够高并发秒杀和库存扣减建议Redisson。4.3 高频业务场景计数器、排行榜与Session共享计数与限流INCR和EXPIRE配合可以轻松实现接口访问次数统计、登录失败锁定、短信发送频率限制。比如限流一分钟最多10次用INCR后如果值为1设置过期时间60秒之后判断计数是否超过阈值即可。排行榜ZSet有序集合几乎是天然为排行榜设计的。ZADD添加玩家的积分ZREVRANGE直接取前N名ZINCRBY更新分数性能极佳比在MySQL里做ORDER BY score LIMIT N快得多。Session共享引入spring-session-data-redis这个依赖并把server.servlet.session.store-type设为redisTomcat的Session就自动存到Redis里了。多实例部署后用户在A实例登录请求到B实例也不会掉线这是业务集群化后很常见的需求。5. 缓存治理与性能调优真正拉开差距的地方5.1 缓存穿透、击穿、雪崩三大经典问题这三个词在Redis面试题里出现频率极高实际项目中更是真实存在必须提前治理。缓存穿透指查询一个不存在的keyRedis里没有MySQL里也没有请求直接打到数据库恶意攻击会拖垮DB。解决思路有三种一是对参数做基础校验不合法直接拦截二是缓存空值比如null也写入Redis过期时间短一些3~5分钟三是用布隆过滤器把可能存在的数据标记进过滤器查不到就快速返回。实际项目里最常见的是第二种简单有效。缓存击穿指一个热点key过期的一瞬间大量请求同时打向数据库。解决思路可以用互斥锁当发现缓存没命中时先尝试获取分布式锁拿到锁的线程去查数据库并回写缓存其他线程短暂等待后重新读缓存。也可以用逻辑过期方案缓存value里额外存一个过期时间戳每次读取时判断是否逻辑过期过期则异步去更新数据但实现略复杂一般并发量没到那个量级时不急着上。缓存雪崩指大量key在同一时间段集体失效或Redis服务整体挂了导致请求全部打到数据库。解决思路很朴素key的过期时间加一个随机偏移量比如300秒加0~60秒随机数避免同一秒过期再给每个key设置合理的过期时间别全部设置成同一个值。至于Redis服务高可用那就靠主从哨兵机制了文章里不展开。5.2 Big Key与热点Key识别与处理存储一个几百KB甚至几MB的value就是典型的Big Key。它会带来什么影响大key在读取时占用网络带宽在删除时可能阻塞Redis单线程在迁移时也会拖慢整个集群。识别方式最简单的是用redis-cli --bigkeys扫描Redis内所有key客户端工具里也有内存分析功能可以直接看到占内存最大的key排名。发现Big Key之后处理思路一般是拆。一个几MB的JSON字符串拆成多个hash field或者压缩后再存储一个存储了大量用户ID的List或Set切成多个小key分片存储。热点Key的治理思路正好相反它是某个key的访问量极高单台Redis扛不住。常见做法是做本地缓存Caffeine挡一部分读请求或者把热点key复制N份加上随机后缀分散到不同分片。这里补一句关于Spring Boot集成Caffeine实际上可以在Redis之上再加一层本地缓存形成两级缓存架构。热点数据读本地的Caffeine内存查不到再走RedisRedis没有最后到MySQL。本地缓存命中率极高能显著降低Redis压力。缺点是多了一层数据一致性维护成本适合并发量极高、数据时效性要求又不算苛刻的团队。5.3 内存策略、慢日志与日志切面Redis不是无限内存的生产环境必须配置淘汰策略。maxmemory-policy常见选择有allkeys-lru所有key按LRU淘汰和volatile-lru仅对设置了过期时间的key按LRU淘汰。如果业务里大部分数据都设置了过期时间volatile-lru更安全那些永久key不会被误删。慢日志也是一个容易被忽视的监控点。在redis.conf中设置slowlog-log-slower-than 10000单位微秒超过10毫秒才记录slowlog-max-len 128就能通过SLOWLOG GET查看慢命令。大多数慢命令都是因为KEYS命令遍历所有key导致这也是为什么我前面强调生产环境绝不能用KEYS *要用SCAN代替。调优层面还有一个不容易注意到的点是连接池与线程池配合。Lettuce线程安全理论上可以只用一个共享连接但高并发下还是推荐启用连接池。连接数配置要结合Redis的tcp-keepalive和业务压力做调整如果频繁出现连接超时可以适当调大max-active同时观察Redis端INFO clients命令确认已用连接数。排查问题之前最好把Redis的访问日志也接进来。除了Redis自身loglevel notice级别的运行日志我建议在应用层做一层操作切面统一记录缓存操作的key、耗时、结果这样线上出问题时立刻能定位是哪条缓存操作慢、哪个key访问频率异常。Spring AOP即可实现也方便你在测试环境验证缓存策略。6. 常见问题与排查技巧实录6.1 连接不上Redis从ping到防火墙一网打尽Spring Boot项目启动时如果报Unable to connect to Redis或Connection refused很多人第一反应是代码配置错了但其实八成是环境或网络层面的问题。按我排查的顺序走先确认Redis进程活着在Redis所在机器上执行redis-cli ping如果返回PONG说明Redis本身没问题如果命令都找不到去看进程是不是没启动。确认绑定地址默认Redis只允许本机连接也就是bind 127.0.0.1。如果应用和Redis不在同一台机器上必须把bind改成0.0.0.0或具体内网IP否则从外部连不上。检查保护模式Redis 3.2之后默认开启protected-mode yes即使bind了0.0.0.0没有配置密码且监听在公网地址时也会拒绝外部访问。解决方法是设置requirepass密码后重启或者显式关闭保护模式不推荐除非内网环境且可信。看防火墙和安全组本机telnet 127.0.0.1 6379如果通换到应用服务器上再试不行大概率是云服务器安全组规则没放行6379端口。看Spring配置是否生效尤其注意Spring Boot 2.x与3.x配置前缀差异前面已提过以及ConfigurationProperties是否正常绑定。6.2 数据乱码或类型不匹配先查序列化器如果你看到Redis里存的是\xAC\xED\x00\x05t不用怀疑就是JDK默认序列化的锅。解决办法前面已经给过把key换成StringRedisSerializervalue换成GenericJackson2JsonRedisSerializer。但如果你是从老项目接手线上已经有大量JDK序列化脏数据那要先把Redis里的数据清掉再换配置。清理时用redis-cli --scan --pattern user:* | xargs redis-cli del按规则批量删别直接FLUSHALL那会把所有库都给清了生产环境千万别这么干。还有一种情况是“类型不匹配”报错比如存的是String取的时候用opsForHash操作。这时要检查业务代码里是否对同一个key混用了不同类型操作。这种问题不好排查因为报错不会第一时间指向具体位置。建议在封装的RedisUtil里强制规范类型操作并让key带上前缀从源头减少冲突。6.3 缓存与数据库一致性问题先定标准再出方案缓存和数据库的数据不一致严格来说没有完美的实时解决方案因为任何删除缓存的操作都有窗口期。比较务实的做法是明确哪些数据要求秒级一致哪些数据允许分钟级最终一致。前者直接用“先更新DB再删缓存”必要时加延迟双删后者设置相对短的过期时间比如5分钟过期后自然触发重新加载脏数据最多存5分钟。如果你在面试中或者项目中需要量化方案时可以遵循一条硬规则以数据库为准缓存只是加速层。任何业务流程中先写数据库再删缓存缓存重建时只允许通过数据库查询结果回填不允许客户端直接写缓存。这条规则配合过期时间兜底能覆盖绝大多数场景。实在不行引入消息队列做异步的缓存更新但这已经是更重的架构设计项目规模到那一步再说也不迟。7. 关于版本与生态的扩展思考Spring Boot版本的迭代节奏很快目前新项目已经普遍用上Spring Boot 3.xJava 17甚至Java 21都可以跑得很稳。如果你用的是Java 21 Spring Boot 3.5想要启用虚拟线程Redis客户端也完全兼容Lettuce本身支持异步非阻塞配合虚拟线程的实际收益可能不像纯IO密集任务那么夸张但配置成本很低。Spring Boot 3.x中启用虚拟线程大致只需设置spring.threads.virtual.enabledtrue值得一试。另一个真实存在的扩展点是Spring Security配置迁移问题。Spring Boot 2.x中基于WebSecurityConfigurerAdapter的写法在3.x中已经废弃如果项目同时用Redis做token存储或会话管理升级时要同时处理两个兼容性变化。推荐的做法是尽早把安全配置迁移到基于SecurityFilterChain的写法上这样和Redis的新版spring-data-redis客户端配合也更顺畅。旧代码里还有大量基于spring.redis.*的写法升级后同样需要同步改为spring.data.redis.*否则直接启动报错。说到业务场景我看到热词里有“基于Spring Boot的校园讲座预约系统”和“商城类设计题目”这类项目在毕设和课程设计中很常见。它们使用Redis的场景通常是讲座或商品的剩余名额做缓存和防超卖预约码和下单验证码存在Redis并设置过期时间热点讲座列表做缓存。如果你的项目也属于这一类前面提到的缓存穿透、分布式锁基本都是核心得分点文章里的代码和思路可以直接套用。我个人的实际建议是先从最基础的单机Redis RedisTemplate开始把缓存读写、过期时间、序列化搞明白然后逐步接触Spring Cache、Redisson、集群部署。这些东西一个个理解透面试和实战都会轻松不少。最后再分享一个小技巧所有缓存key的前缀命名格式统一为“项目名:业务名:ID”比如mall:product:10001。这个习惯配合Redis Desktop Manager的按前缀搜索功能排查效率能翻一倍。
返回列表