第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 内存布隆过滤器(单机/测试用)
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 共享。

<!-- Redisson 已引入,无需额外依赖 -->
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);
    }
}

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

@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 更新,其他请求直接返回旧值。

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)
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 分布式锁(生产标准)

扣库存示例(秒杀场景)

@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)
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!
👉 点击关注我,更新后第一时间收到推送!

更多推荐