SpringBoot集成Redis缓存,提升接口性能的五大实战策略

SpringBoot集成Redis缓存,提升接口性能的五大实战策略

前言

在高并发、高吞吐量的互联网应用中,数据库往往是系统的瓶颈所在。Redis 作为一款高性能的内存数据结构存储系统,常被用作 缓存 层,以扛住 90% 以上的业务流量。

但简单的 setget 远远不够。如何防止缓存雪崩?如何处理缓存穿透?如何保证数据一致性?本文将基于 Spring Boot 3.x ,结合 Mermaid 架构图 ,为你深度剖析集成 Redis 的五大实战策略,助你将接口性能提升 10 倍以上。

一、 缓存架构全景

在介绍具体策略前,我们需要明确 Redis 在系统中的位置。标准的缓存架构通常遵循"Cache Aside Pattern(旁路缓存模式)"。
应用层

  1. 查询 2. 读取缓存 命中
    未命中
    返回数据
  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,而是一场与数据一致性、并发安全博弈的过程。

  1. 基础层 :熟练使用 Spring Cache
  2. 防御层 :利用 布隆过滤器 拦截恶意穿透,利用 随机过期时间 防止雪崩。
  3. 高并发层 :利用 分布式锁 解决击穿问题。
  4. 数据层 :先更 DB,再删 Cache,并配合 MQ 重试保证最终一致性。
    掌握这五大策略,你的 Spring Boot 接口将具备生产级的"抗揍"能力!
相关推荐
晓13132 小时前
第一章:Redis 安装与入门
redis·json·nosql
康小庄2 小时前
浅谈Java中的volatile关键字
java·开发语言·jvm·spring boot·spring·jetty
vx_bisheyuange2 小时前
基于SpringBoot的海鲜市场系统
java·spring boot·后端·毕业设计
それども3 小时前
为什么要加@ResponseBody
java·开发语言·spring boot
李慕婉学姐3 小时前
【开题答辩过程】以《基于Spring Boot和大数据的医院挂号系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
大数据·spring boot·后端
晓13135 小时前
第二章:Redis常见命令与Java客户端
java·数据库·redis
invicinble5 小时前
对于springboot
java·spring boot·后端
填满你的记忆5 小时前
【从零开始——Redis 进化日志|Day7】双写一致性难题:数据库与缓存如何不再“打架”?(附 Canal/读写锁实战)
java·数据库·redis·缓存·面试
Coder_Boy_6 小时前
基于SpringAI的在线考试系统-知识点管理模块完整优化方案
java·前端·人工智能·spring boot