做后端开发这些年只要聊到缓存Redis基本是绕不开的选项。Spring Boot 作为 Java 生态里最主流的开发框架和 Redis 的组合几乎成了企业级项目的标准配置。这篇文章我想把 Spring Boot 集成 Redis 做缓存的完整链路梳理一遍从环境安装、依赖配置、序列化方案、注解缓存到缓存一致性、分布式锁、常见坑排查都给你过一遍。不管你是刚接触 Spring Boot 的新手还是已经在用 Redis 但老是遇到缓存不生效、key 乱码这类问题的老手这篇文章应该都能给你一些可以落地的参考。我不会从零开始讲什么是 Redis因为这类文章太多了。我重点想说的是在实际项目里怎么把 Redis 缓存用好、用稳、用得不踩坑。毕竟 Redis 本身不难难的是缓存和数据库之间的数据一致性难的是缓存穿透、击穿、雪崩这些线上故障怎么防难的是你写出来的缓存代码过两个月再看还能不能看懂。1. 缓存设计思路先想清楚什么该缓存再动手写代码1.1 缓存解决的核心问题与适用场景在写任何一行缓存代码之前先问自己一个问题这个数据真的需要缓存吗很多新手一上来就把所有查询结果往 Redis 里塞结果内存暴涨、数据不一致最后还得连夜删缓存。我见过不止一次这样的线上事故。适合缓存的数据通常有三个特征读多写少、实时性要求不高、数据量可控。典型的例子就是用户信息、商品详情、配置项、热点新闻列表。这些数据的特点是用户请求量大每次查数据库都是同样的 SQL、同样的结果查一百次和查一次结果几乎一样那为什么不把第一次查出来的结果放到内存里后面直接取呢反过来不适合缓存的数据也很明显频繁更新的数据比如库存数量、强一致要求的数据比如账户余额、一次性使用且无法复用的数据比如动态生成的验证码虽然也放 Redis但那是另一种用途。把这些硬要塞进缓存只会给自己找麻烦。生活里有个类比很贴切缓存就像是你办公桌上常放的几本工具书。你天天要翻的那几本放在手边能省不少事但你一整年都用不上一回的厚字典放到手边只会占地方还不如放回书柜里。这就是缓存的局部性原理——程序访问数据的规律也类似90% 的流量往往集中在 10% 的数据上。1.2 Spring Boot 里做缓存的主流方案选型现在 Java 生态里做缓存主流方向就两个本地缓存Caffeine、Guava Cache和分布式缓存Redis、Memcached。两者没有谁绝对好要看场景。本地缓存的优势是快因为它就在应用进程内没有网络开销微秒级别就能拿到数据。但问题也很明显应用多实例部署时每个实例的缓存是独立的A 实例更新了缓存B 实例还是旧数据这就导致数据不一致。而且本地缓存占用的是 JVM 堆内存堆内存就那么大缓存多了会影响应用本身的性能。Redis 作为分布式缓存正好解决本地缓存的痛点。它是独立部署的服务所有应用实例共享同一份缓存数据天然就是一致的。而且 Redis 支持持久化、过期策略、丰富的数据结构和分布式锁功能上完全碾压本地缓存。当然代价就是多了一次网络 IO一般也就 1ms 左右对于绝大多数业务场景完全可接受。在 Spring Boot 里接入 Redis 缓存核心引入的依赖是这个dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency这个 starter 会帮你把 RedisTemplate、StringRedisTemplate、RedisConnectionFactory 这些核心组件全部自动配置好。如果你还需要使用 Spring Cache 抽象就是下面要讲的 Cacheable 那一套注解还需要加上dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-cache/artifactId /dependency所以整个方案选型就是Redis 存数据Spring Cache 管流程RedisTemplate 干细活。前者解决数据放哪里的问题后两者解决怎么放、怎么取、怎么维护的问题。这套组合在企业项目里经得起实践检验网上能搜到的成熟方案也基本都是这个套路。2. 环境准备与基础集成先把 Redis 跑起来2.1 Redis 安装的三种方式与推荐要开发调试 Redis你本地总得先有一个能跑的 Redis 实例。安装方式主要有三种第一种本机直接安装。Windows 用户可以去 Redis 官网下载 MSI 安装包不过 Redis 官方其实对 Windows 支持不积极目前 Windows 版本的维护是由别的团队在跟进下载时注意看版本Linux 用户可以直接apt install redis-serverUbuntu/Debian或者yum install redisCentOS。装完之后redis-cli ping返回 PONG 就说明启动成功了。第二种Docker 方式。这是我个人最推荐的开发方式好处是环境隔离、版本切换方便、不会污染本机。一条命令就搞定docker run -d --name redis \ -p 6379:6379 \ -v /data/redis:/data \ redis:7.0 \ --requirepass yourpassword --appendonly yes这里解释一下几个参数-p 6379:6379把容器内的 6379 端口映射到宿主机这样本机应用能直接访问-v挂载数据目录防止容器删了数据全丢--requirepass设置访问密码--appendonly yes开启 AOF 持久化。如果是本地开发密码可以先用简单的生产环境密码必须复杂而且建议关掉一些危险命令。想看 Redis 里到底存了什么 key、key 的过期时间还剩多久我习惯配一个可视化工具。网上搜 Redis Desktop Manager现在改名叫 Another Redis Desktop Manager的版本很多选一个开源免费的下下来填上 host、port、password 就能连上。它对排查序列化乱码这类问题特别有用你可以直观地看到 key 是什么样的、value 是怎样的格式比在命令行里敲 keys 命令直观得多。第三种云服务。如果你的项目已经部署在云上直接用云厂商提供的 Redis 服务最省事主从、持久化、监控都帮你做好了就是得花钱。开发环境还是本地起一个吧。2.2 Spring Boot 接入 Redis 的基础配置依赖引好了Redis 服务也跑起来了接下来就是在 Spring Boot 里做配置。下面是 application.yml 里的基础配置我加了注释说明每个参数的作用spring: data: redis: host: 127.0.0.1 port: 6379 password: yourpassword # 没有密码就留空 database: 0 # 默认连 0 号库多业务可隔离 timeout: 3000ms # 连接超时时间 lettuce: pool: max-active: 16 # 连接池最大连接数 max-idle: 8 # 连接池最大空闲连接 min-idle: 2 # 连接池最小空闲连接 max-wait: 3000ms # 获取连接超时时间注意这里的缩进是spring.data.redisSpring Boot 3.x 的写法不是spring.redis。如果你用的还是 Spring Boot 2.x那前缀是spring.redis。这是个很隐蔽的坑版本升级之后配置文件没改结果连接不上 Redis报错你都不一定反应得过来。关于连接池Spring Boot 2.x 之后默认用的是 Lettuce 而不是 Jedis。为什么换因为 Jedis 是阻塞式 IO在多线程环境下性能不够好Lettuce 基于 Netty底层是异步非阻塞 IO虽然你用的时候感知不到但高并发下性能差距是实打实的。这个选型是 Spring 官方做的你不用自己纠结直接用默认的就行。唯一要留意的就是连接池参数默认的 8 个连接在并发高的时候可能不够压测的时候注意观察。配置完成后怎么验证连通性最直接的办法是写个测试用例跑一下SpringBootTest class RedisConnectionTest { Autowired private StringRedisTemplate stringRedisTemplate; Test void testConnection() { stringRedisTemplate.opsForValue().set(test:conn, hello-redis); String value stringRedisTemplate.opsForValue().get(test:conn); System.out.println(获取到的值为: value); Assertions.assertEquals(hello-redis, value); } }能正常打印出来说明你的 Spring Boot 和 Redis 已经打通了。这里我用的是 StringRedisTemplate 而不是 RedisTemplate原因下一节详细讲。3. 缓存核心实操从序列化方案到注解缓存3.1 序列化方案对比为什么你的 key 全是乱码很多初次在 Spring Boot 里用 Redis 的开发者都会遇到一个怪问题用 RedisTemplate 存了个 key用 Redis Desktop Manager 一看key 变成了类似\xac\xed\x00\x05t\x00\x04name这样的一堆看不懂的东西value 也完全没法读。这就是序列化问题。Redis 本身只认字节Java 对象要存进去得先序列化成字节数组从 Redis 取出来得反序列化成 Java 对象。这个过程用什么序列化器直接决定了你存进去的东西长什么样。Spring Boot 默认提供的 RedisTemplate用的序列化器是JdkSerializationRedisSerializer也就是 JDK 原生的序列化方式。这种方式的优点是实现简单、不用引入额外的库但缺点也很致命序列化后的数据里带着类名、包名这些元信息体积大、可读性差而且跨语言、跨版本都不友好。这就是你看到一堆乱码的根本原因。解决方案是自定义序列化器把 key 序列化成字符串把 value 序列化成 JSON。主流做法是 key 用StringRedisSerializervalue 用GenericJackson2JsonRedisSerializer。key 用字符串是为了可读性你可以在 Redis Desktop Manager 里直接看到user:info:1001这样的 key一眼就知道它是干嘛的value 用 JSON 是为了体积可控、跨语言通用。下面是配置类的完整代码Configuration public class RedisConfig { Bean public RedisTemplateString, Object redisTemplate(RedisConnectionFactory factory) { RedisTemplateString, Object template new RedisTemplate(); template.setConnectionFactory(factory); // key 使用字符串序列化 StringRedisSerializer stringRedisSerializer new StringRedisSerializer(); // value 使用 JSON 序列化 GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer new GenericJackson2JsonRedisSerializer(); // 设置 key 和 hash key 的序列化器 template.setKeySerializer(stringRedisSerializer); template.setHashKeySerializer(stringRedisSerializer); // 设置 value 和 hash value 的序列化器 template.setValueSerializer(jackson2JsonRedisSerializer); template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } }注意两个细节。第一hashKey和hashValue的序列化器也要单独设置如果你用 Redis 的 Hash 结构存数据比如存一个对象的多个字段不设置的话还是会走默认的 JDK 序列化key 照样乱。第二GenericJackson2JsonRedisSerializer序列化后的 value 会额外存一个class字段里面是类的全限定名这是为了反序列化时能知道目标类型。这也带来一个小问题类路径变了或者类名改了老数据就反序列化不了了。GenericJackson2JsonRedisSerializer和Jackson2JsonRedisSerializer的区别也值得说一句。前者在序列化时会带上类型信息支持多态同一个 key 存进去的是 User 对象读出来还能正确转成 User。后者你需要手动指定类型比如new Jackson2JsonRedisSerializer(User.class)性能和紧凑性更好但只能处理指定类型的对象。实际项目中如果你只缓存固定类型的对象用后者更稳缓存类型不固定就用前者。3.2 用 Spring Cache 注解实现声明式缓存上面说了 RedisTemplate 的用法但实际项目里我更推荐用 Spring Cache 抽象层来写缓存代码。为什么因为 RedisTemplate 是命令式的你要自己判断缓存有没有命中、手动往 Redis 里塞数据、手动删数据代码侵入性很强。而 Spring Cache 是声明式的你只需要在方法上标注注解缓存逻辑框架自动帮你完成业务代码干净很多。用之前先开启缓存功能在启动类或者配置类上加EnableCachingEnableCaching SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }然后就可以在 Service 层方法上写注解了。最常用的三个注解是Cacheable、CachePut、CacheEvict区别我整理成了表格注解执行时机适用场景Cacheable方法执行前先查缓存命中直接返回不执行方法未命中才执行方法并缓存结果查询操作CachePut方法执行后把返回值放到缓存里不干扰方法本身执行新增/更新操作后刷新缓存CacheEvict方法执行后从缓存中移除指定 key删除操作后移除缓存一个典型的示例是这样的Service public class UserService { Autowired private UserMapper userMapper; Cacheable(cacheNames user:info, key #id) public User getUserById(Long id) { // 第一次查询会进到这里走数据库 return userMapper.selectById(id); } CachePut(cacheNames user:info, key #user.id) public User updateUser(User user) { userMapper.updateById(user); // 方法执行后返回值自动写入缓存保证缓存是最新的 return user; } CacheEvict(cacheNames user:info, key #id) public void deleteUser(Long id) { userMapper.deleteById(id); } }这里几个参数要解释一下。cacheNames是缓存分区的名字相当于给缓存 key 一个命名空间最终 Redis 里的 key 会是user:info::1001这种形式两个冒号是默认的分隔符。key用 SpEL 表达式从参数中取值#id就表示参数列表里的 id 变量。为什么要加 cacheNames因为不同业务的缓存 key 可能会有交集用 cacheNames 区分开就不会互相覆盖后续排查也好查。还有一些高级用法值得掌握。比如condition和unless两个都能做条件过滤但机制不同// 只有当 id 大于 0 时才走缓存逻辑 Cacheable(cacheNames user:info, key #id, condition #id 0) // 只有当返回值不为 null 时才放入缓存 Cacheable(cacheNames user:info, key #id, unless #result null)一个常见误区是把condition和unless搞混。condition是在方法执行前判断决定了要不要执行缓存逻辑unless是在方法执行后判断决定了返回值要不要放进缓存。如果查询结果是 null你却把它缓存了后面频繁查这个不存在的 id缓存里就一直存着 null就相当于变相放大了缓存穿透问题。所以除非你刻意要做空值缓存否则建议习惯性加上unless #result null。3.3 缓存的过期时间策略与主动失效用注解缓存的时候一个容易忽略的问题是过期时间怎么配置Cacheable 注解本身没有直接设置 TTL 的参数默认情况下缓存是不过期的。这就很危险如果你缓存的数据发生了变化而你又忘了调用 CacheEvict 删缓存那用户拿到的就是永久过期的脏数据。正确的做法是通过RedisCacheManager统一配置过期时间。代码是这样的Configuration public class CacheConfig { Bean public RedisCacheManager cacheManager(RedisConnectionFactory factory) { RedisCacheConfiguration config RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(30)) // 默认过期时间 30 分钟 .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) .computePrefixWith(name - name ::); // key 前缀分隔符 return RedisCacheManager.builder(factory) .cacheDefaults(config) .withInitialCacheConfigurations(customTtlMap()) // 为特定缓存单独设置过期时间 .build(); } private MapString, RedisCacheConfiguration customTtlMap() { MapString, RedisCacheConfiguration map new HashMap(); map.put(user:info, RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofHours(2))); map.put(hot:list, RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofMinutes(5))); return map; } }这样配置之后user:info里的缓存 2 小时过期hot:list里的缓存 5 分钟过期其他缓存默认 30 分钟过期。不同的数据有不同的时效性分开设置更合理。除了过期缓存还需要主动失效的场景。比如用户修改了头像你希望立即删掉旧缓存下次请求再加载新的。这时候用CacheEvict就能实现。它的beforeInvocation参数值得注意默认是 false也就是方法执行成功后才删缓存如果设为 true会在方法执行前就删除缓存。什么时候用 true当方法可能抛异常但你又想在异常时也把缓存清掉或者你想先删缓存再执行数据库操作以避免并发下读到旧数据就可以用 true。还有一种比较重的用法是多条缓存一起清Caching(evict { CacheEvict(cacheNames user:info, key #id), CacheEvict(cacheNames user:list, allEntries true) }) public void deleteUserAllCache(Long id) { userMapper.deleteById(id); }allEntries true表示清空整个user:list分区下的所有缓存。这种操作要慎用因为如果分区下的 key 很多Redis 内部要遍历删除在高并发下会造成短暂的卡顿。不过对于数据量不大、变更不频繁的业务这样的粗暴反而省心。关于缓存预热如果你的项目启动后有一批热点数据马上会被大量访问可以在启动时预先把数据加载到 Redis 里避免第一个用户请求时全部打到数据库。实现方式很简单用ApplicationRunner或者CommandLineRunner在 Spring Boot 启动完成后执行预加载逻辑再从数据库查出来写入 Redis。如果数据会周期性变化还可以配合Scheduled定时刷新。4. 缓存一致性、分布式锁与线上故障排查4.1 缓存一致性问题的本质与常用方案缓存一致性是整个缓存体系里最让人头疼的问题。简单说就是数据库里的数据和 Redis 里的数据不一致了用户读到的可能是旧的、甚至错的数据。为什么会出现不一致根本原因是同一个数据有两份存储数据库一份、缓存一份而更新这两份的操作不可能做到原子。比如你先更新数据库再删缓存这两步之间如果有一个请求正好读缓存读到的就是旧值。反过来说你先删缓存再更新数据库中间这一步如果有请求进来发现缓存没有就去数据库读数据然后写回缓存写回的却是旧数据等数据库更新完缓存还留着旧的。业界常用的数据一致性方案有几种我按实践中的使用频率排序方案一Cache Aside Pattern旁路缓存。这是最主流、最推荐的做法。读的时候先读缓存读不到再读数据库然后回填缓存写的时候先更新数据库再删除缓存而不是更新缓存。为什么删除而不是更新因为删除的代价远小于更新而且可以避免并发写时缓存里存了旧值的情况。这个方案简单可靠缺点是有短暂的不一致窗口但对大多数业务完全够用。方案二延迟双删。在 Cache Aside 的基础上更新数据库后先删一次缓存然后延迟几百毫秒再删一次。这个双删就是为了处理高并发下的竞态条件第一次删除后一个旧请求读到旧数据并写回缓存第二次删除把这个脏数据清掉。这个方案能进一步降低不一致的概率但它是一个概率性方案不是百分之百可靠而且延迟时间怎么定是个玄学需要根据业务压测来调。方案三订阅 Binlog 异步更新缓存。用 Canal 这类工具监听 MySQL 的 Binlog数据变更后异步把最新的数据推送到 Redis。这个方案解耦性最好应用层完全不用写缓存更新逻辑但引入 Cannel 增加了系统复杂度一般中大型项目才值得做。实践中我的建议是中小项目直接用 Cache Aside 合适的过期时间。过期时间就是兜底方案即使中间某一次缓存没删干净到了过期时间也会自动失效不会永远不一致。线上出问题后优先检查是不是有缓存忘记删了再考虑要不要上更复杂的方案。4.2 缓存穿透、击穿、雪崩的防御与分布式锁实现这三个是缓存领域最经典的故障模式我在文章里把它们放一起讲因为它们的本质都是缓存失效或未命中导致请求打到数据库但原因和应对手段完全不同。缓存穿透查询一个根本不存在的数据。由于数据不存在缓存永远不可能命中每次请求都打到数据库。如果有人恶意用一个不存在的 id 反复刷接口数据库压力会非常大。防御手段有两个一是把空值也缓存起来设置较短的过期时间比如 30 秒代码里就是unless #result null不生效改成判断 null 后手动写入一个空对象二是用布隆过滤器先判断 id 是否存在不存在直接返回但布隆过滤器有误判率且不支持删除实现成本略高。我建议先用空值缓存简单有效大部分场景够用。缓存击穿某个热点 key 刚好过期此时大量并发请求同时发现缓存未命中全部打到数据库。和穿透的区别是穿透是数据本来就不存在击穿是数据存在但缓存恰好失效了。防御的核心思想是互斥锁只让一个请求去数据库加载数据并回填缓存其他请求等缓存回填后再读缓存。用 Redis 实现一个分布式锁就派上用场了。Redis 分布式锁的经典实现是SETNX 过期时间。简单说就是利用 Redis 的原子操作只有一个客户端能成功设置某个 key 的值设置成功的人拿到锁其他人拿不到。Spring Boot 里用 RedisTemplate 可以这样写public boolean tryLock(String lockKey, String requestId, long expireSeconds) { // 利用 Redis SET NX EX 原子操作避免并发问题 // 设置成功返回 true说明拿到锁 return redisTemplate.opsForValue() .setIfAbsent(lockKey, requestId, Duration.ofSeconds(expireSeconds)); } public void unlock(String lockKey, String requestId) { // 这里要注意只能释放自己持有的锁通过 requestId 校验 String value redisTemplate.opsForValue().get(lockKey); if (requestId.equals(value)) { redisTemplate.delete(lockKey); } }这个实现还有几个细节要注意key 必须加过期时间防止拿到锁的线程崩溃导致死锁释放锁时必须比较 value 是不是自己的用 requestId 标识防止误删了别人后来获取的锁。如果你不想自己封装这些细节直接引入 Redisson它把可重入锁、自动续期这些都实现了开箱即用这也是当前企业项目里最常用的分布式锁方案。缓存雪崩大量 key 在同一时间过期或者 Redis 服务整体挂了导致所有请求全部打到数据库数据库扛不住就也挂了。防御思路一是给过期时间加随机值比如entryTtl(Duration.ofMinutes(30 RandomUtil.randomInt(10)))让 key 的过期时间错开避免同时失效二是做多级缓存本地缓存 Redis 兜底Redis 挂了本地还能挡一下三是 Redis 做高可用部署主从加哨兵不用裸奔的单机 Redis。4.3 缓存监控与排查线上出问题别慌按这个顺序查最后聊聊线上问题的排查思路。缓存相关的线上故障最常见的无非这么几类连接超时、缓存不生效、key 乱码、缓存穿透压垮数据库、Redis 内存暴涨。遇到问题先别慌按下面的顺序排查。第一步确认 Redis 连接状态和健康情况。Spring Boot Actuator 默认提供了 Redis 的健康检查指标配置一下就能从/actuator/health看到 Redis 状态management: endpoints: web: exposure: include: health,info,metrics endpoint: health: show-details: always访问这个接口如果 Redis 挂了你会直接看到status: DOWN。这是最快判断 Redis 是否活着的方式。第二步确认缓存有没有命中。如果你用了 Redis Desktop Manager直接看缓存 key 是否存在。如果 key 存在但值不对那是数据一致性问题如果 key 不存在说明缓存没生效或者已经被清掉了要去查代码逻辑。第三步确认序列化是否正常。如果你发现 Redis Desktop Manager 里 key 是乱码那就是序列化器配错了。解决办法就是前面说的自定义序列化器key 用 StringRedisSerializer。这里再分享一个特别常见的问题使用 Cacheable 缓存了对象但取出来转型报ClassCastException。原因通常是序列化器配置不一致存的时候用的是 JSON 序列化读的时候因为类型信息丢了或者类路径变了反序列化出的类型不对。排查方法就是看代码里 RedisTemplate 和 RedisCacheManager 的序列化器是不是一致的检查有没有地方用的是默认的 JdkSerializationRedisSerializer。我把常见问题整理成了一张速查表方便你遇到问题直接对照问题现象可能原因排查与解决key 显示为乱码RedisTemplate 默认 JDK 序列化自定义序列化器key 用 String 序列化Cacheable 不生效没加 EnableCaching检查启动类或配置类是否开启缓存缓存数据一直不变没配过期时间或没主动清缓存配置 TTL 结合 CacheEvict 清理数据库压力大可能存在缓存穿透或击穿空值缓存/布隆过滤/分布式锁连接池报错连接池太小或没释放连接调大 max-active、max-wait 参数Redis 内存暴涨大量 key 未设置过期时间统一使用 setex / 检查 TTL 配置ClassCastException序列化类型不匹配统一序列化器确认类型信息排查缓存问题有一个通用原则先看缓存层有没有数据再看数据对不对最后看代码逻辑通不通。不要一上来就翻代码先用工具把缓存层的情况摸清楚大部分问题就已经能找到方向了。5. 进阶实践Redis 数据类型、主从与扩展思路5.1 Redis 数据类型选型别只会用 String 存一切聊到 Redis 缓存大部分开发者第一反应就是 String 类型。但实际上 Redis 提供了丰富的数据结构选对了类型能给业务带来很大的便利。String 是用的最多的类型适合存简单的键值对比如用户信息 JSON、配置项、验证码等。Hash 类型适合存对象的多个字段比如你需要单独修改用户头像字段而不用把整个对象序列化再读出来改一遍再序列化回去Hash 就高效得多hset user:info:1001 avatar new.jpg只改一个字段不用动整个对象。List 类型适合做消息队列或者简单的顺序列表比如秒杀场景的请求排队。Set 类型天然去重适合做标签系统、好友关系这类需要交集并集运算的场景。ZSet有序集合带 score 排序适合排行榜这类高频业务ZADD 加分数、ZREVRANGE 查前 N 名都是 O(logN) 的复杂度非常高效。实际项目中一个对象该用 String 存 JSON 还是 Hash 存字段要看你的访问模式。如果读取时总是全字段查询String 存 JSON 更合适如果经常要修改局部字段Hash 更合适。没有绝对的对错关键看你的业务访问模式。5.2 Redis 主从复制与生产环境部署如果你的项目正式上线了我强烈不建议再使用单机 Redis。单机意味着单点故障Redis 挂了你的缓存全部失效数据库直接裸奔。生产环境至少要做主从复制一台主节点负责写一台或多台从节点负责读主节点挂了从节点可以顶上来。Docker 部署 Redis 主从也很方便大致思路是启动两台 Redis 容器在从节点的配置文件里指定主节点的地址和密码# 启动主节点 docker run -d --name redis-master -p 6379:6379 redis:7.0 # 启动从节点并指定主节点地址 docker run -d --name redis-slave -p 6380:6379 redis:7.0 \ redis-server --slaveof 127.0.0.1 6379这只是个演示命令实际配置里主从都需要设置密码从节点的masterauth要配上。更高级的高可用方案是 Redis 哨兵Sentinel或者 Redis Cluster哨兵负责监控和自动故障转移Cluster 则解决了大数据量分片存储的问题。这些属于运维层面的进阶内容有兴趣可以再深入研究但了解思路对理解 Redis 的定位很有帮助。5.3 从缓存到分布式锁Redis 的更多应用场景文章到了这里Spring Boot 集成 Redis 实现缓存的主线已经讲完了。但 Redis 在实际项目里的价值远不止缓存。分布式锁就是一个最常见的高级应用我在前面讲缓存击穿时已经提过很多业务场景都需要它多个服务实例处理同一个订单、分布式定时任务防止重复执行、秒杀扣减库存避免超卖。Redis 分布式锁之所以受欢迎是因为它实现简洁、性能高比基于数据库的乐观锁更适合同一时刻大量请求的场景。还有限流利用 Redis 的 INCR 加过期时间可以方便地实现固定窗口限流用 ZSet 可以实现滑动窗口限流配合 Lua 脚本还能实现更复杂的令牌桶算法。Redis 的原子性保证了这些计数的准确性又因为单线程模型天然避免并发竞争在限流场景下非常可靠。Redis 作为缓存组件在企业级 Java 项目中的地位几乎和 Spring Boot 本身一样重要。从 Spring Boot 2.x 到 3.x官方对 Redis 的支持越来越完善lettuce 客户端 Spring Cache 抽象 自定义序列化方案的组合已经是我在项目里最顺手的搭配。缓存设计的核心不是 Redis 怎么配而是想清楚什么数据该缓存、缓存多久、怎么保证一致把这些想明白了后面的代码水到渠成。最后分享一个我踩过不少坑之后的经验缓存代码一定要能快速人工干预。比如提供一个管理接口可以手动查缓存、删缓存、刷新缓存出问题的时候你不用重启应用就有救火手段。这个接口平时不起眼但关键时刻能救命。另外一个建议是日志里把缓存命中和未命中打出来加个开关控制日志级别线上排查的时候你会感谢当初多写了这两行日志。
阅读完成 · 觉得有帮助?