《Java 100 天进阶之路》第93篇:Redis实战应用:缓存策略与分布式锁(2026版)

第93篇:Redis实战应用:缓存策略与分布式锁(2026版)

📌 系列导航《Java 100 天进阶之路》完整目录 |

⬅️ 上一篇:第92篇:Redis高级特性深度解析 |

➡️ 下一篇:第94篇:Redis面试高频题


一、核心知识点

  • 缓存穿透:概念、危害、解决方案(布隆过滤器、缓存空对象)
  • 缓存击穿:概念、危害、解决方案(互斥锁、逻辑过期)
  • 缓存雪崩:概念、危害、解决方案(随机TTL、高可用集群、熔断降级)
  • 分布式锁SETNX 手写锁的问题、Redisson 实现原理(看门狗机制)
  • Spring Boot 整合 Redis:配置、序列化、缓存注解
  • 多级缓存架构:本地缓存(Caffeine)+ Redis,热点 Key 治理

二、通俗讲解(1分钟开心学)

1. 缓存三大杀手

术语 比喻 一句话解释
缓存穿透 有人专门查你数据库里没有的身份证号 请求的数据不存在于缓存和数据库,直接穿透到 DB
缓存击穿 明星过生日,粉丝瞬间涌入蛋糕店 某个热点 key 过期瞬间,大量并发请求打爆 DB
缓存雪崩 多家店同时关门,顾客全涌向唯一开门的店 大量 key 同时过期或 Redis 宕机,DB 瞬间压力暴增

2. 分布式锁------秒杀防超卖

多台服务器同时修改同一数据(如库存)时需要加锁,保证同一时刻只有一个线程能操作。

生活类比

小区只有一个篮球,一群孩子想玩。谁先拿到球谁玩,玩完放回。Redisson 就是那个"自动计时、公平分配"的智能篮球架。


三、实操代码案例 + 场景说明

项目环境 :Spring Boot 2.7+,依赖 spring-boot-starter-data-redisredisson-spring-boot-starter

3.1 解决缓存穿透(布隆过滤器)

布隆过滤器原理:位数组 + 多个哈希函数。一个 key 存在时可能误判,不存在时一定准确。

方案一:Guava 内存布隆过滤器(单机/测试用)
java 复制代码
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;

@Component
public class GuavaBloomFilterService {
    private BloomFilter<String> bloomFilter;
    private static final int EXPECTED_INSERTIONS = 1000000;
    private static final double FPP = 0.01;

    @PostConstruct
    public void init() {
        bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),
                EXPECTED_INSERTIONS, FPP);
        loadAllProductIds();
    }
    public boolean mightExist(String key) { return bloomFilter.mightContain(key); }
}

⚠️ 注意 :Guava 布隆过滤器数据存储在 JVM 内存中,服务重启后数据会丢失,且无法在集群间共享。生产环境建议使用方案二

方案二:Redisson 分布式布隆过滤器(生产推荐)

Redisson 的 RBloomFilter 底层基于 Redis 的 BitMap,数据持久化、可跨 JVM 共享。

xml 复制代码
<!-- Redisson 已引入,无需额外依赖 -->
java 复制代码
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

@Component
public class RedissonBloomFilterService {
    @Autowired
    private RedissonClient redissonClient;
    private RBloomFilter<String> bloomFilter;
    private static final String FILTER_NAME = "product:bloom";

    @PostConstruct
    public void init() {
        bloomFilter = redissonClient.getBloomFilter(FILTER_NAME);
        // 初始化:预计插入100万,误判率1%
        bloomFilter.tryInit(1000000L, 0.01);
        // 预加载数据(仅首次执行)
        if (bloomFilter.count() == 0) {
            loadAllProductIds();
        }
    }

    public boolean mightExist(String id) {
        return bloomFilter.contains(id);
    }

    public void add(String id) {
        bloomFilter.add(id);
    }

    private void loadAllProductIds() {
        // 从 DB 批量加载所有商品ID
        List<String> ids = productDao.getAllIds();
        ids.forEach(bloomFilter::add);
    }
}

查询逻辑(两种方案通用):

java 复制代码
@GetMapping("/product/{id}")
public Product getProduct(@PathVariable String id) {
    // 1. 布隆过滤器拦截
    if (!bloomFilterService.mightExist(id)) {
        return null;
    }
    // 2. 查缓存、回写 DB(同前文)
    // ...
}
3.2 解决缓存击穿
方案一:互斥锁(简单有效,推荐)

参见前文的 getProductWithLock 实现,使用 Redisson 或 Redis setIfAbsent

方案二:逻辑过期(高性能,容忍短暂不一致)

适用场景:热点数据允许短时间内不一致(如商品详情、配置信息)。

核心思想 :缓存中存储 value + expireTime,不设置 Redis 的 TTL。读取时判断是否逻辑过期,若过期则异步去 DB 更新,其他请求直接返回旧值。

java 复制代码
public class ProductWithLogicExpire {
    private Product product;
    private Long expireTime;  // 逻辑过期时间戳
}

public Product getProductLogicExpire(String id) {
    String key = "product:logic:" + id;
    ProductWithLogicExpire wrapper = (ProductWithLogicExpire) redisTemplate.opsForValue().get(key);
    if (wrapper == null) {
        // 缓存不存在,走互斥锁更新(第一次加载)
        return loadFromDBAndSetCache(id);
    }

    // 判断是否逻辑过期
    if (wrapper.getExpireTime() > System.currentTimeMillis()) {
        return wrapper.getProduct();  // 未过期,直接返回
    }

    // 逻辑过期,尝试获取互斥锁去异步更新
    String lockKey = "lock:product:refresh:" + id;
    Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);
    if (Boolean.TRUE.equals(locked)) {
        // 异步线程去 DB 加载并更新缓存
        CompletableFuture.runAsync(() -> {
            try {
                Product newProduct = productDao.findById(id);
                // 重新设置缓存,逻辑过期时间 = 当前时间 + 随机TTL
                ProductWithLogicExpire newWrapper = new ProductWithLogicExpire();
                newWrapper.setProduct(newProduct);
                newWrapper.setExpireTime(System.currentTimeMillis() + 3600 * 1000 + new Random().nextInt(600) * 1000);
                redisTemplate.opsForValue().set(key, newWrapper);
            } finally {
                redisTemplate.delete(lockKey);
            }
        });
    }
    // 返回旧数据(容忍短暂不一致)
    return wrapper.getProduct();
}

优缺点对比

  • 互斥锁:强一致,但会阻塞部分请求,适合写少读多或一致性要求高。
  • 逻辑过期:无阻塞,性能极高,但可能读到旧数据,适合如"商品库存"以外的展示类数据。
3.3 解决缓存雪崩(随机 TTL)
java 复制代码
int baseTTL = 3600; // 基础1小时
Random random = new Random();
int ttl = baseTTL + random.nextInt(600); // 增加0~600秒随机值
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
3.4 Redisson 分布式锁(生产标准)

扣库存示例(秒杀场景)

java 复制代码
@Service
public class SeckillService {
    @Autowired
    private RedissonClient redissonClient;

    public boolean deductStock(String productId, int quantity) {
        String lockKey = "lock:seckill:" + productId;
        RLock lock = redissonClient.getLock(lockKey);
        try {
            // 尝试加锁,最多等待3秒,锁自动释放时间30秒(看门狗会续期)
            boolean locked = lock.tryLock(3, 30, TimeUnit.SECONDS);
            if (!locked) return false;

            // 业务逻辑:查库存、扣减
            String stockKey = "stock:" + productId;
            Integer stock = (Integer) redisTemplate.opsForValue().get(stockKey);
            if (stock == null || stock < quantity) return false;
            redisTemplate.opsForValue().decrement(stockKey, quantity);
            return true;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

Redisson 看门狗原理:锁默认30秒过期,若业务未完成,看门狗每10秒自动续期,避免锁过期提前释放。

3.5 多级缓存架构(Caffeine + Redis)
java 复制代码
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;

@Component
public class MultiLevelCache {
    private Cache<String, Object> localCache;

    @PostConstruct
    public void init() {
        localCache = Caffeine.newBuilder()
                .maximumSize(10000)
                .expireAfterWrite(60, TimeUnit.SECONDS)
                .build();
    }

    public Object get(String key) {
        // 1. 本地缓存
        Object value = localCache.getIfPresent(key);
        if (value != null) return value;
        // 2. Redis
        value = redisTemplate.opsForValue().get(key);
        if (value != null) {
            localCache.put(key, value);
            return value;
        }
        // 3. DB 查询并回写
        value = queryDB(key);
        if (value != null) {
            redisTemplate.opsForValue().set(key, value, 3600, TimeUnit.SECONDS);
            localCache.put(key, value);
        }
        return value;
    }
}

四、生产环境避坑清单

分类 错误/误区 后果 正确做法
缓存穿透 使用 Guava 布隆过滤器生产环境部署 服务重启数据丢失,集群不共享 生产用 Redisson 的 RBloomFilter(持久化)
缓存穿透 不校验请求参数合法性 恶意请求打爆 DB 布隆过滤器 + 缓存空对象 + 参数校验
缓存击穿 仅用互斥锁,忽略逻辑过期 高并发下排队,性能下降 结合场景:强一致用互斥锁;容忍短暂不一致用逻辑过期
缓存雪崩 所有 key 相同 TTL 大量 key 同时失效 随机 TTL + Redis 集群
分布式锁 手写 SETNX 未设置过期时间 死锁 使用 Redisson(自动续期)
分布式锁 释放锁未校验持有者 误删其他线程的锁 Redisson 自动处理
多级缓存 本地缓存和 Redis 数据不一致 脏数据 监听 Redis 变更消息,主动失效本地缓存

五、面试高频考点

Q1:布隆过滤器的原理与误判率?

位数组 + 多个哈希函数。不存在一定准确,存在可能误判。误判率与位数组大小和哈希函数个数有关,可配置(如 1%)。生产推荐 Redisson 的 RBloomFilter(持久化、分布式)。

Q2:Redisson 分布式锁的看门狗机制?

锁默认过期时间 30 秒,若业务未完成,看门狗线程每 10 秒检查并续期,业务完成后主动释放。避免了手动续期的复杂性。

Q3:缓存击穿互斥锁 vs 逻辑过期如何选择?

互斥锁保证强一致,适合库存、订单等;逻辑过期无阻塞、性能高,适合商品详情、配置等可容忍短暂不一致的数据。

Q4:多级缓存如何保证一致性?

① 更新时删除本地缓存和 Redis 缓存;② 使用 Canal + MQ 监听 MySQL 变更;③ 本地缓存设置较短 TTL。

Q5:Redis 分布式锁与 Zookeeper 锁对比?

Redis 性能更高,适合高并发场景;Zookeeper 保证强一致性,适合对可靠性要求极高的场景。Redisson 支持可重入、公平锁等高级特性。


六、练习题

  1. 代码题:基于 Redisson 实现一个可重入分布式锁,模拟 10 个线程同时扣减库存,保证最终库存正确。
  2. 设计题 :一个新闻 App 首页热点新闻缓存,要求不击穿 DB,且可容忍短暂不一致,请给出方案。 💡 思路:逻辑过期 + 互斥锁。缓存不带 TTL,记录逻辑过期时间,查询时若逻辑过期,获取锁去 DB 更新,其他请求返回旧数据。
  3. 故障排查:某系统上线后 CPU 飙升,发现大量线程在等待 Redisson 锁,可能原因是什么?

📊 你的学习进度

  • 当前:第93篇 / 共108篇 · 进阶篇:缓存与消息队列(第91~96篇)
  • ✅ 已完成:基础篇44篇 + 第91~93篇
  • 📖 正在学:第93篇
  • ⏳ 待学习:第94~108篇

👉 📚 完整目录 & 学习指南 | 🔥 订阅本专栏,不错过每一篇

💡 本专栏每篇都包含:避坑表 + 面试高频考点 + 练习题。每天30分钟,100天拿offer!


👉 下一篇文章预告

《第94篇:Redis面试高频题(2026版)》

内容简介:汇总 30+ 道 Redis 大厂面试真题(含分布式锁、持久化选型、集群架构、缓存三大杀手、淘汰策略等),每道题附标准话术 + 加分回答,助你轻松应对面试。

💡 学完这篇,你将直接背诵面试答案,从"会用"到"碾压面试官"。

🎁 福利提醒:评论区留言"Redis面试"可获取《Redis 面试高频题 PDF》及 Redisson 配置模板。

📌 《Java 100 天进阶之路 | 从入门到上岗就业》 每天一篇,建议收藏 + 关注 ,一起100天拿offer!

👉 点击关注我,更新后第一时间收到推送!

相关推荐
瓦特what?1 小时前
位运算核心技巧与应用
java·jvm·算法
人道领域1 小时前
【LeetCode刷题日记】90.子集Ⅱ--- 归纳题解
java·开发语言·leetcode
IT策士1 小时前
Redis 从入门到精通:数据结构Set 与 Sorted
数据结构·数据库·redis
软件技术新观察1 小时前
2026年北京大数据分析系统外包机构甄别、架构评估与售后保障
架构
ch.ju2 小时前
Java Programming Chapter 4——Characteristics of inheritance
java·开发语言
就叫_这个吧2 小时前
tomcat在idea控制台乱码问题解决
java·tomcat·intellij-idea
填满你的记忆2 小时前
10万QPS下,Redis缓存如何避免雪崩?
数据库·redis·缓存
霸道流氓气质2 小时前
Spring AI Alibaba Skills 完整实战:从零构建智能会议助手
java·人工智能·spring
heimeiyingwang2 小时前
【架构实战】网关架构设计:微服务的统一入口
微服务·云原生·架构