SpringBoot+Redis:缓存穿透、雪崩、击穿一次搞定

23 阅读15分钟

每天5分钟,掌握一个SpringBoot实战技能。大家好,我是SpringBoot指南的小坏。从今天开始,我们进入第二周的实战进阶篇,首先解决缓存这个"老大难"问题!

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

一、真实案例:缓存引发的"血案"

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

上个月,我们公司搞了一个"1元秒杀"活动:

活动开始前

  • 预估流量:10万QPS
  • Redis集群:准备充分
  • 数据库:加了索引,备了从库

活动开始瞬间

  • 0点00分:正常
  • 0点01分:Redis挂了!
  • 0点02分:数据库挂了!
  • 0点05分:整个系统挂了!

原因分析

  1. 热点商品被瞬间缓存击穿
  2. 大量请求直接打到数据库
  3. 数据库连接池撑爆
  4. Redis重启后,缓存雪崩

损失

  • 活动失败,用户投诉
  • 数据库恢复用了2小时
  • 公司品牌受损

今天,我就用实战教会你如何避免这种悲剧!

二、Redis快速集成

2.1 3分钟搞定Redis

第一步:加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

第二步:配连接

# application.yml
spring:
  redis:
    host: localhost
    port: 6379
    password: 123456  # 生产环境一定要设密码!
    database: 0
    
    # 连接池配置(关键!)
    lettuce:
      pool:
        max-active: 20      # 最大连接数
        max-idle: 10        # 最大空闲连接
        min-idle: 5         # 最小空闲连接
        max-wait: 3000ms    # 获取连接最大等待时间

第三步:直接使用

@RestController
public class TestController {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @GetMapping("/test")
    public String test() {
        // 存数据
        redisTemplate.opsForValue().set("key", "value");
        
        // 取数据
        String value = redisTemplate.opsForValue().get("key");
        
        return "Redis测试成功,值:" + value;
    }
}

三、缓存穿透:查不存在的数据

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

3.1 什么是缓存穿透?

场景: 用户请求一个不存在的商品ID,比如:/product/999999

过程

  1. 查缓存 → 没有
  2. 查数据库 → 也没有
  3. 返回空结果

问题: 如果大量请求不存在的ID,每次都会打到数据库,数据库压力巨大!

3.2 解决方案:缓存空对象

@Service
public class ProductService {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private RedisTemplate<String, Product> redisTemplate;
    
    /**
     * 防穿透:缓存空对象
     */
    public Product getProductSafe(Long productId) {
        String cacheKey = "product:" + productId;
        
        // 1. 先查缓存
        Product product = redisTemplate.opsForValue().get(cacheKey);
        
        // 2. 缓存有数据
        if (product != null) {
            // 如果是空对象标记,返回null
            if (isNullObject(product)) {
                return null;
            }
            return product;
        }
        
        // 3. 缓存没有,查数据库
        product = productRepository.findById(productId).orElse(null);
        
        // 4. 数据库有,缓存正常数据
        if (product != null) {
            redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
            return product;
        }
        
        // 5. 数据库没有,缓存空对象(防止穿透)
        Product nullProduct = createNullObject();
        redisTemplate.opsForValue().set(cacheKey, nullProduct, 5, TimeUnit.MINUTES); // 短时间缓存
        
        return null;
    }
    
    /**
     * 创建空对象标记
     */
    private Product createNullObject() {
        Product product = new Product();
        product.setId(-1L);  // 用特殊ID标识空对象
        product.setName("NULL_OBJECT");
        return product;
    }
    
    /**
     * 判断是否是空对象标记
     */
    private boolean isNullObject(Product product) {
        return product != null && product.getId() != null && product.getId() == -1L;
    }
}

3.3 增强方案:布隆过滤器

@Component
public class BloomFilterService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 初始化布隆过滤器
     */
    public void initBloomFilter() {
        String key = "bloom:product:ids";
        
        // 从数据库加载所有存在的商品ID
        List<Long> allProductIds = productRepository.findAllIds();
        
        // 将每个ID添加到布隆过滤器
        for (Long id : allProductIds) {
            add(key, String.valueOf(id));
        }
    }
    
    /**
     * 判断元素是否存在
     */
    public boolean mightContain(String key, String value) {
        long[] hashIndexes = getHashIndexes(value);
        
        // 检查所有位是否都为1
        for (long index : hashIndexes) {
            Boolean bit = redisTemplate.opsForValue().getBit(key, index);
            if (bit == null || !bit) {
                return false;
            }
        }
        return true;
    }
    
    /**
     * 添加元素到布隆过滤器
     */
    public void add(String key, String value) {
        long[] hashIndexes = getHashIndexes(value);
        
        for (long index : hashIndexes) {
            redisTemplate.opsForValue().setBit(key, index, true);
        }
    }
    
    /**
     * 计算哈希位置(模拟多个哈希函数)
     */
    private long[] getHashIndexes(String value) {
        // 实际可以用多个哈希函数
        // 这里简化处理
        long hash1 = Math.abs(value.hashCode());
        long hash2 = Math.abs(value.hashCode() * 31);
        
        // 映射到0-99999的范围
        return new long[] { hash1 % 100000, hash2 % 100000 };
    }
}

// 使用布隆过滤器
@Service
public class ProductServiceWithBloom {
    
    @Autowired
    private BloomFilterService bloomFilterService;
    
    public Product getProductWithBloom(Long productId) {
        // 先用布隆过滤器判断
        boolean mightExist = bloomFilterService.mightContain(
            "bloom:product:ids", 
            String.valueOf(productId)
        );
        
        if (!mightExist) {
            // 一定不存在,直接返回null
            return null;
        }
        
        // 可能存在,继续正常流程
        return getProductSafe(productId);
    }
}

四、缓存击穿:热点key过期

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

4.1 什么是缓存击穿?

场景: 一个热点商品(比如:iPhone 15)的缓存过期了,瞬间有1万个请求同时来查这个商品。

过程

  1. 缓存过期
  2. 1万个请求同时发现缓存没有
  3. 1万个请求同时去查数据库
  4. 数据库瞬间被打爆

4.2 解决方案:互斥锁

@Service
public class ProductServiceWithLock {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 使用互斥锁防止缓存击穿
     */
    public Product getProductWithLock(Long productId) {
        String cacheKey = "product:" + productId;
        String lockKey = "lock:product:" + productId;
        
        // 1. 先查缓存
        Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
        if (product != null && !isNullObject(product)) {
            return product;
        }
        
        // 2. 尝试获取锁
        Boolean locked = tryLock(lockKey);
        
        try {
            if (locked) {
                // 3. 拿到锁,查数据库
                product = productRepository.findById(productId).orElse(null);
                
                if (product != null) {
                    // 4. 写缓存,设置随机过期时间(防雪崩)
                    int expireSeconds = 1800 + new Random().nextInt(600); // 30-40分钟
                    redisTemplate.opsForValue().set(
                        cacheKey, product, expireSeconds, TimeUnit.SECONDS
                    );
                } else {
                    // 缓存空对象
                    Product nullProduct = createNullObject();
                    redisTemplate.opsForValue().set(
                        cacheKey, nullProduct, 300, TimeUnit.SECONDS
                    );
                }
            } else {
                // 5. 没拿到锁,等待50ms后重试
                Thread.sleep(50);
                return getProductWithLock(productId);
            }
        } finally {
            // 6. 释放锁
            if (locked) {
                releaseLock(lockKey);
            }
        }
        
        return product;
    }
    
    /**
     * 尝试获取锁(setnx实现)
     */
    private Boolean tryLock(String lockKey) {
        // 使用setnx命令,只有key不存在时才能设置成功
        return redisTemplate.opsForValue().setIfAbsent(
            lockKey, 
            "1", 
            10, TimeUnit.SECONDS  // 10秒自动过期,防止死锁
        );
    }
    
    /**
     * 释放锁
     */
    private void releaseLock(String lockKey) {
        redisTemplate.delete(lockKey);
    }
}

4.3 更简单的方案:永不过期 + 后台更新

@Service
public class ProductServiceWithBackgroundRefresh {
    
    @Autowired
    private RedisTemplate<String, Product> redisTemplate;
    
    /**
     * 方案:缓存永不过期,后台定时更新
     */
    public Product getProductWithBackgroundRefresh(Long productId) {
        String cacheKey = "product:" + productId;
        
        // 1. 先查缓存
        Product product = redisTemplate.opsForValue().get(cacheKey);
        
        if (product == null) {
            // 2. 缓存没有,查数据库并写入缓存(永不过期)
            product = productRepository.findById(productId).orElse(null);
            if (product != null) {
                redisTemplate.opsForValue().set(cacheKey, product);
                // 记录需要更新的时间
                recordUpdateTime(productId);
            }
        } else {
            // 3. 检查是否需要更新
            if (needRefresh(productId)) {
                // 异步更新缓存
                refreshCacheInBackground(productId);
            }
        }
        
        return product;
    }
    
    /**
     * 异步更新缓存
     */
    @Async
    public void refreshCacheInBackground(Long productId) {
        String cacheKey = "product:" + productId;
        
        try {
            Product freshProduct = productRepository.findById(productId).orElse(null);
            if (freshProduct != null) {
                redisTemplate.opsForValue().set(cacheKey, freshProduct);
                updateRefreshTime(productId);
            }
        } catch (Exception e) {
            log.error("后台更新缓存失败,productId: {}", productId, e);
        }
    }
    
    /**
     * 判断是否需要更新
     */
    private boolean needRefresh(Long productId) {
        // 根据最后更新时间判断,比如超过30分钟
        Long lastUpdateTime = getLastUpdateTime(productId);
        if (lastUpdateTime == null) {
            return true;
        }
        
        long now = System.currentTimeMillis();
        return now - lastUpdateTime > 30 * 60 * 1000; // 30分钟
    }
}

五、缓存雪崩:大量key同时过期

5.1 什么是缓存雪崩?

场景: 大量缓存在同一时间过期,比如:半夜12点,所有缓存设置的是24小时过期。

过程

  1. 00:00:大量缓存同时过期
  2. 大量请求同时打到数据库
  3. 数据库瞬间崩溃

5.2 解决方案:随机过期时间

@Service
public class ProductServiceWithRandomExpire {
    
    @Autowired
    private RedisTemplate<String, Product> redisTemplate;
    
    /**
     * 设置缓存,使用随机过期时间
     */
    public void setProductWithRandomExpire(Long productId, Product product) {
        String cacheKey = "product:" + productId;
        
        // 基础过期时间:30分钟
        int baseExpireSeconds = 30 * 60; // 1800秒
        
        // 随机增加0-600秒(0-10分钟)
        int randomAddition = new Random().nextInt(600);
        
        int totalExpireSeconds = baseExpireSeconds + randomAddition;
        
        redisTemplate.opsForValue().set(
            cacheKey, 
            product, 
            totalExpireSeconds, 
            TimeUnit.SECONDS
        );
    }
    
    /**
     * 批量设置缓存,分散过期时间
     */
    public void batchSetProductsWithRandomExpire(Map<Long, Product> productMap) {
        List<Long> productIds = new ArrayList<>(productMap.keySet());
        
        // 打乱顺序,让相邻的商品ID过期时间分散
        Collections.shuffle(productIds);
        
        for (Long productId : productIds) {
            Product product = productMap.get(productId);
            setProductWithRandomExpire(productId, product);
        }
    }
}

5.3 增强方案:缓存预热

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

@Component
public class CacheWarmUp {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private RedisTemplate<String, Product> redisTemplate;
    
    /**
     * 系统启动时预热缓存
     */
    @PostConstruct
    public void warmUpCache() {
        log.info("开始缓存预热...");
        
        // 1. 查询热点商品(比如:销量前1000的商品)
        List<Product> hotProducts = productRepository.findHotProducts(1000);
        
        // 2. 分批写入缓存
        int batchSize = 100;
        for (int i = 0; i < hotProducts.size(); i += batchSize) {
            int end = Math.min(i + batchSize, hotProducts.size());
            List<Product> batch = hotProducts.subList(i, end);
            
            // 异步写入,不影响启动速度
            writeBatchToCache(batch);
        }
        
        log.info("缓存预热完成,共预热{}个商品", hotProducts.size());
    }
    
    /**
     * 定时更新缓存
     */
    @Scheduled(cron = "0 0 2 * * ?")  // 每天凌晨2点执行
    public void scheduledCacheRefresh() {
        log.info("开始定时缓存更新...");
        
        // 1. 查询需要更新的商品
        List<Product> productsToUpdate = productRepository.findProductsUpdatedToday();
        
        // 2. 更新缓存
        for (Product product : productsToUpdate) {
            String cacheKey = "product:" + product.getId();
            redisTemplate.opsForValue().set(
                cacheKey, 
                product, 
                30 + new Random().nextInt(10), 
                TimeUnit.MINUTES
            );
        }
        
        log.info("定时缓存更新完成,共更新{}个商品", productsToUpdate.size());
    }
    
    @Async
    public void writeBatchToCache(List<Product> products) {
        for (Product product : products) {
            String cacheKey = "product:" + product.getId();
            redisTemplate.opsForValue().set(
                cacheKey,
                product,
                30 + new Random().nextInt(10),  // 30-40分钟
                TimeUnit.MINUTES
            );
        }
    }
}

六、分布式锁实战:商品秒杀

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

6.1 秒杀场景分析

需求

  • 1000件商品
  • 10万人同时抢
  • 不能超卖
  • 高性能

问题

  • 库存扣减的并发问题
  • 防止重复购买
  • 保证公平性

6.2 基于Redis的分布式锁实现

@Service
@Slf4j
public class SeckillService {
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    @Autowired
    private ProductService productService;
    
    @Autowired
    private OrderService orderService;
    
    /**
     * 秒杀抢购(使用分布式锁)
     */
    public SeckillResult seckill(Long productId, Long userId) {
        String lockKey = "lock:seckill:" + productId;
        String stockKey = "stock:" + productId;
        String boughtKey = "bought:" + productId + ":" + userId;
        
        // 1. 检查是否已经购买过
        Boolean bought = redisTemplate.hasKey(boughtKey);
        if (bought != null && bought) {
            return SeckillResult.fail("您已经购买过了");
        }
        
        // 2. 获取分布式锁
        String requestId = UUID.randomUUID().toString();
        boolean locked = false;
        
        try {
            // 尝试获取锁,最多等待100ms
            locked = tryLock(lockKey, requestId, 100);
            
            if (!locked) {
                return SeckillResult.fail("系统繁忙,请稍后重试");
            }
            
            // 3. 检查库存
            String stockStr = redisTemplate.opsForValue().get(stockKey);
            if (stockStr == null) {
                // 初始化库存到Redis
                Product product = productService.getProduct(productId);
                if (product == null) {
                    return SeckillResult.fail("商品不存在");
                }
                stockStr = String.valueOf(product.getStock());
                redisTemplate.opsForValue().set(stockKey, stockStr);
            }
            
            int stock = Integer.parseInt(stockStr);
            if (stock <= 0) {
                return SeckillResult.fail("商品已售罄");
            }
            
            // 4. 扣减库存
            Long newStock = redisTemplate.opsForValue().decrement(stockKey);
            if (newStock < 0) {
                // 库存不足,恢复库存
                redisTemplate.opsForValue().increment(stockKey);
                return SeckillResult.fail("商品已售罄");
            }
            
            // 5. 记录购买状态(防止重复购买)
            redisTemplate.opsForValue().set(boughtKey, "1", 24, TimeUnit.HOURS);
            
            // 6. 创建订单(异步)
            orderService.createSeckillOrderAsync(productId, userId);
            
            return SeckillResult.success("抢购成功");
            
        } finally {
            // 7. 释放锁
            if (locked) {
                releaseLock(lockKey, requestId);
            }
        }
    }
    
    /**
     * 尝试获取分布式锁
     */
    private boolean tryLock(String lockKey, String requestId, long waitMillis) {
        long start = System.currentTimeMillis();
        
        while (true) {
            // 使用setnx命令尝试获取锁
            Boolean success = redisTemplate.opsForValue().setIfAbsent(
                lockKey, 
                requestId, 
                10, TimeUnit.SECONDS  // 10秒自动过期,防止死锁
            );
            
            if (success != null && success) {
                return true;
            }
            
            // 检查是否超时
            if (System.currentTimeMillis() - start > waitMillis) {
                return false;
            }
            
            // 等待50ms后重试
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
        }
    }
    
    /**
     * 释放分布式锁(使用Lua脚本保证原子性)
     */
    private void releaseLock(String lockKey, String requestId) {
        String luaScript = """
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end
            """;
        
        DefaultRedisScript<Long> script = new DefaultRedisScript<>();
        script.setScriptText(luaScript);
        script.setResultType(Long.class);
        
        redisTemplate.execute(script, Collections.singletonList(lockKey), requestId);
    }
}

6.3 使用Redisson简化分布式锁

<!-- 添加Redisson依赖 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.2</version>
</dependency>
@Service
public class SeckillServiceWithRedisson {
    
    @Autowired
    private RedissonClient redissonClient;
    
    /**
     * 使用Redisson分布式锁(更简单可靠)
     */
    public SeckillResult seckillWithRedisson(Long productId, Long userId) {
        String lockKey = "lock:seckill:" + productId;
        String stockKey = "stock:" + productId;
        
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 尝试加锁,最多等待100ms,锁持有时间10秒
            boolean locked = lock.tryLock(100, 10000, TimeUnit.MILLISECONDS);
            
            if (!locked) {
                return SeckillResult.fail("系统繁忙,请稍后重试");
            }
            
            // 执行业务逻辑...
            RAtomicLong stock = redissonClient.getAtomicLong(stockKey);
            
            // 扣减库存
            if (stock.decrementAndGet() < 0) {
                stock.incrementAndGet();  // 恢复库存
                return SeckillResult.fail("商品已售罄");
            }
            
            // 创建订单...
            return SeckillResult.success("抢购成功");
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return SeckillResult.fail("系统异常");
        } finally {
            // 释放锁
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

七、缓存一致性:先更新数据库还是先删缓存?

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

7.1 两种方案对比

方案一:先更新数据库,再删除缓存

public void updateProduct(Product product) {
    // 1. 更新数据库
    productRepository.update(product);
    
    // 2. 删除缓存
    String cacheKey = "product:" + product.getId();
    redisTemplate.delete(cacheKey);
}

问题:删除缓存可能失败,导致缓存是旧数据

方案二:先删除缓存,再更新数据库

public void updateProduct(Product product) {
    String cacheKey = "product:" + product.getId();
    
    // 1. 删除缓存
    redisTemplate.delete(cacheKey);
    
    // 2. 更新数据库
    productRepository.update(product);
}

问题:更新数据库期间,可能有请求读到旧数据并重新缓存

7.2 最佳实践:延迟双删

@Service
public class ProductServiceWithDoubleDelete {
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private RedisTemplate<String, Product> redisTemplate;
    
    @Autowired
    private ThreadPoolTaskExecutor taskExecutor;
    
    /**
     * 延迟双删策略
     */
    public void updateProductWithDoubleDelete(Product product) {
        String cacheKey = "product:" + product.getId();
        
        // 1. 第一次删除缓存
        redisTemplate.delete(cacheKey);
        
        // 2. 更新数据库
        productRepository.update(product);
        
        // 3. 异步延迟第二次删除缓存
        taskExecutor.execute(() -> {
            try {
                // 延迟500ms,等数据库主从同步完成
                Thread.sleep(500);
                
                // 第二次删除缓存
                redisTemplate.delete(cacheKey);
                
                log.info("延迟双删完成,productId: {}", product.getId());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });
    }
}

7.3 终极方案:监听数据库Binlog

@Component
public class CacheSyncWithBinlog {
    
    @Autowired
    private RedisTemplate<String, Product> redisTemplate;
    
    /**
     * 监听数据库变更,实时更新缓存
     */
    @EventListener
    public void onDatabaseChange(DatabaseChangeEvent event) {
        if (event.getTableName().equals("product")) {
            Long productId = event.getRowId();
            String cacheKey = "product:" + productId;
            
            if (event.getOperation() == Operation.DELETE) {
                // 删除操作:删除缓存
                redisTemplate.delete(cacheKey);
            } else {
                // 更新或插入:异步更新缓存
                updateCacheInBackground(productId);
            }
        }
    }
    
    @Async
    public void updateCacheInBackground(Long productId) {
        try {
            // 等待100ms,确保数据库事务提交
            Thread.sleep(100);
            
            // 查询最新数据
            Product product = productRepository.findById(productId).orElse(null);
            if (product != null) {
                String cacheKey = "product:" + productId;
                redisTemplate.opsForValue().set(cacheKey, product, 30, TimeUnit.MINUTES);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (Exception e) {
            log.error("更新缓存失败,productId: {}", productId, e);
        }
    }
}

八、实战:电商商品系统完整缓存方案

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

8.1 多级缓存架构

用户请求 → Nginx本地缓存 → Redis集群缓存 → 数据库
                ↓                    ↓
            热点数据             全量数据
             1ms                 5ms

8.2 完整代码示例

@Service
@Slf4j
public class ProductCacheService {
    
    // 本地缓存(Caffeine)
    private Cache<Long, Product> localCache = Caffeine.newBuilder()
        .maximumSize(1000)                // 最多缓存1000个商品
        .expireAfterWrite(1, TimeUnit.MINUTES)  // 1分钟过期
        .build();
    
    @Autowired
    private RedisTemplate<String, Product> redisTemplate;
    
    @Autowired
    private ProductRepository productRepository;
    
    @Autowired
    private BloomFilterService bloomFilterService;
    
    /**
     * 多级缓存查询
     */
    public Product getProductWithMultiLevelCache(Long productId) {
        // 1. 布隆过滤器判断是否存在
        if (!bloomFilterService.mightContain("product:ids", String.valueOf(productId))) {
            return null;
        }
        
        // 2. 查本地缓存
        Product product = localCache.getIfPresent(productId);
        if (product != null) {
            log.debug("本地缓存命中,productId: {}", productId);
            return product;
        }
        
        // 3. 查Redis缓存(防击穿)
        product = getProductFromRedis(productId);
        if (product != null && !isNullObject(product)) {
            // 刷新本地缓存
            localCache.put(productId, product);
            return product;
        }
        
        // 4. 查数据库(防穿透、防雪崩)
        product = getProductFromDatabase(productId);
        
        // 5. 更新各级缓存
        if (product != null) {
            updateAllCaches(productId, product);
        }
        
        return product;
    }
    
    /**
     * 从Redis获取商品(带互斥锁防击穿)
     */
    private Product getProductFromRedis(Long productId) {
        String cacheKey = "product:" + productId;
        String lockKey = "lock:product:" + productId;
        
        // 先查Redis
        Product product = redisTemplate.opsForValue().get(cacheKey);
        if (product != null) {
            return product;
        }
        
        // 获取分布式锁
        RLock lock = redissonClient.getLock(lockKey);
        try {
            boolean locked = lock.tryLock(50, 5000, TimeUnit.MILLISECONDS);
            
            if (locked) {
                // 再次检查缓存(Double Check)
                product = redisTemplate.opsForValue().get(cacheKey);
                if (product != null) {
                    return product;
                }
                
                // 查数据库
                product = productRepository.findById(productId).orElse(null);
                
                if (product != null) {
                    // 写入Redis,设置随机过期时间
                    int expireSeconds = 1800 + new Random().nextInt(600);
                    redisTemplate.opsForValue().set(
                        cacheKey, product, expireSeconds, TimeUnit.SECONDS
                    );
                } else {
                    // 缓存空对象(防穿透)
                    Product nullProduct = createNullObject();
                    redisTemplate.opsForValue().set(
                        cacheKey, nullProduct, 300, TimeUnit.SECONDS
                    );
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
        
        return product;
    }
    
    /**
     * 更新所有缓存层级
     */
    private void updateAllCaches(Long productId, Product product) {
        // 1. 更新本地缓存
        localCache.put(productId, product);
        
        // 2. 更新Redis缓存(异步)
        updateRedisCacheAsync(productId, product);
        
        // 3. 更新布隆过滤器(如果新增商品)
        bloomFilterService.add("product:ids", String.valueOf(productId));
    }
    
    @Async
    public void updateRedisCacheAsync(Long productId, Product product) {
        String cacheKey = "product:" + productId;
        int expireSeconds = 1800 + new Random().nextInt(600);
        
        redisTemplate.opsForValue().set(
            cacheKey, product, expireSeconds, TimeUnit.SECONDS
        );
    }
}

九、缓存性能监控

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

9.1 监控关键指标

@Component
@Slf4j
public class CacheMonitor {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private MeterRegistry meterRegistry;
    
    // 监控指标
    private Counter cacheHitCounter;
    private Counter cacheMissCounter;
    private Timer cacheTimer;
    
    @PostConstruct
    public void init() {
        // 初始化监控指标
        cacheHitCounter = Counter.builder("cache.hits")
            .description("缓存命中次数")
            .register(meterRegistry);
            
        cacheMissCounter = Counter.builder("cache.misses")
            .description("缓存未命中次数")
            .register(meterRegistry);
            
        cacheTimer = Timer.builder("cache.latency")
            .description("缓存操作延迟")
            .register(meterRegistry);
    }
    
    /**
     * 监控缓存查询
     */
    public <T> T monitorCacheQuery(String cacheKey, Supplier<T> cacheQuery, Supplier<T> dbQuery) {
        long start = System.currentTimeMillis();
        
        try {
            // 先查缓存
            T result = cacheQuery.get();
            
            if (result != null && !isNullObject(result)) {
                // 缓存命中
                cacheHitCounter.increment();
                return result;
            } else {
                // 缓存未命中
                cacheMissCounter.increment();
                
                // 查数据库
                result = dbQuery.get();
                
                // 更新缓存
                if (result != null) {
                    updateCache(cacheKey, result);
                }
                
                return result;
            }
        } finally {
            long cost = System.currentTimeMillis() - start;
            cacheTimer.record(cost, TimeUnit.MILLISECONDS);
            
            // 记录日志
            if (cost > 100) {
                log.warn("缓存查询较慢,key: {}, 耗时: {}ms", cacheKey, cost);
            }
        }
    }
    
    /**
     * 定期输出缓存统计报告
     */
    @Scheduled(fixedRate = 60000)  // 每分钟一次
    public void printCacheStats() {
        double hitRate = cacheHitCounter.count() / 
                        (cacheHitCounter.count() + cacheMissCounter.count());
        
        log.info("缓存统计 - 命中率: {:.2%}, 命中次数: {}, 未命中次数: {}", 
            hitRate, cacheHitCounter.count(), cacheMissCounter.count());
        
        // 输出到监控系统
        Metrics.gauge("cache.hit.rate", hitRate);
    }
}

十、今日实战挑战

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

挑战题目: 设计一个"限时秒杀"系统的缓存方案,要求:

  1. 支持10万QPS
  2. 不能超卖
  3. 防止机器人刷单
  4. 保证公平性(先到先得)

设计要求

  1. 画出系统架构图
  2. 写出关键代码
  3. 考虑容灾方案
  4. 设计监控指标

在评论区提交你的设计方案,我会选出3个最佳方案,送出《Redis设计与实现》纸质书!


明日预告:《SpringBoot+RabbitMQ:订单超时取消的完美实现》—— 消息队列的四种模式深度解析!

缓存工具包:关注公众号回复"缓存实战",获取完整的多级缓存实现代码和性能测试脚本!


公众号运营小贴士:

零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目 资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。

🎁 系列福利

  1. 连续打卡7天,送《SpringBoot实战全家桶》课程
  2. 最佳方案作者,直推合作企业面试机会
  3. 随机抽10位幸运读者,送定制技术周边

👥 社群引导: "加入SpringBoot进阶群,获取完整学习路径+实战项目+面试指导"

🔥 明日剧透: "死信队列实现订单30分钟自动取消,RabbitMQ四种模式深度解析!"