SpringBoot集成Redis缓存,提升接口性能的五大实战策略
前言
在高并发、高吞吐量的互联网应用中,数据库往往是系统的瓶颈所在。Redis 作为一款高性能的内存数据结构存储系统,常被用作 缓存 层,以扛住 90% 以上的业务流量。
但简单的 set 和 get 远远不够。如何防止缓存雪崩?如何处理缓存穿透?如何保证数据一致性?本文将基于 Spring Boot 3.x ,结合 Mermaid 架构图 ,为你深度剖析集成 Redis 的五大实战策略,助你将接口性能提升 10 倍以上。
一、 缓存架构全景
在介绍具体策略前,我们需要明确 Redis 在系统中的位置。标准的缓存架构通常遵循"Cache Aside Pattern(旁路缓存模式)"。
应用层
- 查询 2. 读取缓存 命中
未命中
返回数据 - 写入缓存 4. 返回数据 客户端
Spring Boot 应用
Redis
MySQL 数据库
二、 策略一:Spring Cache 注解化开发(基础策略)
对于简单的查询场景,Spring 提供了 spring-boot-starter-data-redis 配合 spring-boot-starter-cache,让我们像使用本地变量一样使用 Redis。
2.1 核心注解
- @Cacheable:查询时先查缓存,没有则查 DB 并写入缓存。
- @CachePut:执行方法体,并将结果更新到缓存(确保执行)。
- @CacheEvict:移除缓存(用于更新或删除操作)。
2.2 实战代码
java
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
// value 是命名空间,key 是缓存的唯一键(支持 SpEL)
@Cacheable(value = "product", key = "#id")
public Product getProductById(Long id) {
System.out.println("查询数据库...");
return productMapper.selectById(id);
}
@CachePut(value = "product", key = "#product.id")
public Product updateProduct(Product product) {
productMapper.updateById(product);
return product;
}
@CacheEvict(value = "product", key = "#id")
public void deleteProduct(Long id) {
productMapper.deleteById(id);
}
}
三、 策略二:防止缓存穿透(布隆过滤器)
问题 :攻击者大量请求一个不存在的数据 (ID 为 -1),导致请求直接穿透 Redis,打到数据库,可能导致数据库瞬间崩溃。
方案 :使用 布隆过滤器 。它是一个占空间极小的二进制向量,能高效判断一个数据"一定不存在 "或"可能存在"。
3.1 原理图解
渲染错误: Mermaid 渲染失败: Parse error on line 7: ...eq --> BF BF -->{判断是否存在?} BF -- ----------------------^ Expecting 'AMP', 'COLON', 'PIPE', 'TESTSTR', 'DOWN', 'DEFAULT', 'NUM', 'COMMA', 'NODE_STRING', 'BRKT', 'MINUS', 'MULT', 'UNICODE_TEXT', got 'DIAMOND_START'
3.2 实战实现
利用 Redisson(Redis 客户端)可以轻松集成布隆过滤器。
java
@Autowired
private RedissonClient redissonClient;
public void initBloomFilter() {
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter("productFilter");
// 预计元素数量 10000,误判率 0.01
bloomFilter.tryInit(10000, 0.01);
// 初始化时将所有合法 ID 放入
for (long i = 1; i <= 10000; i++) {
bloomFilter.add(i);
}
}
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable Long id) {
RBloomFilter<Long> bloomFilter = redissonClient.getBloomFilter("productFilter");
if (!bloomFilter.contains(id)) {
throw new RuntimeException("商品 ID 不存在"); // 拦截非法请求
}
// ... 正常走 Redis 查询逻辑
}
四、 策略三:防止缓存雪崩(随机过期时间)
问题 :如果某一批热门商品在 同一时刻全部过期 ,成千上万的请求会瞬间击穿 Redis,像雪崩一样压垮数据库。
方案 :设置过期时间时,加上一个随机值,让过期时间分散开来。
java
// 错误写法:统一过期 1 小时
redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);
// 正确写法:基础时间 + 随机时间 (如 5 分钟内的随机数)
long baseExpire = 3600;
long randomExpire = ThreadLocalRandom.current().nextLong(0, 300);
redisTemplate.opsForValue().set(key, value, baseExpire + randomExpire, TimeUnit.SECONDS);
五、 策略四:解决缓存击穿(互斥锁)
问题 :某一个极度热点 的 Key(如秒杀商品 ID=1001)过期了,此时有 1 万个并发请求涌入。它们发现 Redis 没有,都会同时去查数据库,这就是缓存击穿 。
方案 :使用 Redis 分布式锁。只允许一个线程查数据库,回写缓存,其他线程等待并读缓存。
5.1 互斥锁流程图
Database Redis 请求 2...N 请求 1 Database Redis 请求 2...N 请求 1 只有 R1 可以查库 查询 Key 1 缓存未命中 2 尝试 SET NX (加锁) 3 锁获取成功 4 查询 Key 5 缓存未命中 6 尝试 SET NX (加锁) 7 锁获取失败 8 查询数据 9 返回结果 10 写入缓存 11 释放锁 12 稍后重试查询 13 缓存命中 (R1 写入的) 14
5.2 实战代码
使用 SET key value NX EX seconds 原性加锁。
java
public String getProductWithLock(String key) {
// 1. 查缓存
String value = (String) redisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(value)) return value;
// 2. 获取锁
String lockKey = "lock:" + key;
Boolean isLocked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(isLocked)) {
try {
// Double Check: 防止在获取锁期间,其他线程已经写入缓存
value = (String) redisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(value)) return value;
// 3. 查库
value = db.query();
// 4. 写缓存
redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);
return value;
} finally {
// 5. 释放锁 (需确保只释放自己的锁,这里简化为直接删除)
redisTemplate.delete(lockKey);
}
} else {
// 获取锁失败,休眠 100ms 重试,或者返回默认值
return getProductWithLock(key);
}
}
六、 策略五:缓存一致性(双写策略)
问题 :修改了数据库数据,缓存如果不及时更新,会导致"脏读"。
方案 :通常采用 延时双删 策略,或者订阅 Binlog (推荐方案,如 Canal)。这里介绍最基础的 Cache Aside Pattern 更新逻辑:先更新 DB,再删除缓存。
为什么先更 DB?
- 如果先删缓存:线程 A 删了 -> 线程 B 读 DB 并写缓存(旧值) -> 线程 A 更新 DB。此时库新,缓存旧,不一致。
- 如果先更 DB:线程 A 更新 DB(失败回滚) -> A 删缓存(成功)。如果 A 更新 DB 成功,B 删缓存失败?概率很低,且可以通过重试解决。
6.1 一致性流程图
失败
成功
成功
失败
更新商品请求
更新数据库
事务回滚, 结束
删除缓存
操作成功
发送失败消息到 MQ
消费者重试删除
七、 总结
集成 Redis 并不是简单的 set/get,而是一场与数据一致性、并发安全博弈的过程。
- 基础层 :熟练使用
Spring Cache。 - 防御层 :利用 布隆过滤器 拦截恶意穿透,利用 随机过期时间 防止雪崩。
- 高并发层 :利用 分布式锁 解决击穿问题。
- 数据层 :先更 DB,再删 Cache,并配合 MQ 重试保证最终一致性。
掌握这五大策略,你的 Spring Boot 接口将具备生产级的"抗揍"能力!