声明:本文内容经过大量的 AI 生成和辅助。
项目代码,点我下载
在使用 Redis 保护底层数据库(如 MySQL)的高并发场景中,通常需要防范三个经典问题:缓存穿透、缓存击穿和缓存雪崩。下面简单梳理这三个问题的产生原因及应对思路,并提供一套整合的参考代码。
1. 核心问题与解决思路
缓存穿透
描述:查询一条数据库和缓存中都不存在的数据。如果不做处理,这类请求每次都会打到数据库,恶意攻击者常利用此漏洞压垮系统。
解决方案:
布隆过滤器:作为第一道防线,将所有存在的记录 ID 放入布隆过滤器。查询前先判断,如果过滤器判断不存在,直接拦截。
缓存空值:作为兜底方案,如果请求穿过了布隆过滤器且数据库确实查不到数据,就在 Redis 中存入一个空字符串,并设置较短的过期时间。
缓存击穿
描述:某个高频访问的热点数据在缓存中突然过期,导致瞬间产生的大量并发请求直接涌入数据库去重建缓存。
解决方案:使用互斥锁(分布式锁)。当缓存失效时,强制所有的请求去抢锁,只允许拿到锁的一个线程去数据库查询并把数据写回缓存。其他没抢到锁的线程稍微等待后,重新尝试从缓存获取。
缓存雪崩
描述:大量的缓存数据在同一时间点集中过期,或者 Redis 服务宕机,导致原本被缓存挡住的流量全部砸向数据库。
解决方案:在给数据设置基础过期时间(TTL)时,额外加上一个随机的波动时间(例如 1 到 5 分钟),将过期时间打散,避免集体失效。
2. 核心工具类代码实现
下面是一个将上述三种防护策略封装在一起的工具类。业务代码只需调用该方法即可。
Java
package com.lj2508.redis_demo.Util;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Function;
import tools.jackson.databind.ObjectMapper; // 假设使用 Jackson 序列化
@Slf4j
@Component
public class CacheClient {
private final StringRedisTemplate stringRedisTemplate;
private final RedissonClient redissonClient;
private final ObjectMapper objectMapper;
// 缓存空值的默认过期时间(短时间,防穿透)
private static final Long CACHE_NULL_TTL = 2L;
public CacheClient(StringRedisTemplate stringRedisTemplate, RedissonClient redissonClient, ObjectMapper objectMapper) {
this.stringRedisTemplate = stringRedisTemplate;
this.redissonClient = redissonClient;
this.objectMapper = objectMapper;
}
/**
* 终极防护版:解决穿透、击穿、雪崩
*
* @param keyPrefix 缓存 Key 前缀
* @param id 查询的 ID
* @param returnType 返回值类型
* @param dbFallback 数据库查询回调函数 (当缓存未命中时执行)
* @param time 缓存基础过期时间
* @param unit 时间单位
* @param bloomFilterName 布隆过滤器名称
* @return 业务数据
*/
public <R, ID> R queryWithCompleteProtection(
String keyPrefix, ID id, Class<R> returnType,
Function<ID, R> dbFallback, Long time, TimeUnit unit, String bloomFilterName) {
String key = keyPrefix + id;
// ================= 1. 缓存穿透防护:布隆过滤器拦截 =================
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(bloomFilterName);
if (bloomFilter.isExists() && !bloomFilter.contains(key)) {
log.warn("布隆过滤器拦截了非法请求: {}", key);
return null;
}
try {
// ================= 2. 尝试查询缓存 =================
String json = stringRedisTemplate.opsForValue().get(key);
// 命中有效数据
if (json != null && !json.isEmpty()) {
return objectMapper.readValue(json, returnType);
}
// 命中空字符串(缓存穿透的兜底防护:之前查过DB确实没有,缓存了空串)
if (json != null && json.isEmpty()) {
return null;
}
// ================= 3. 缓存击穿防护:获取分布式锁 =================
String lockKey = "lock:" + key;
RLock lock = redissonClient.getLock(lockKey);
// 尝试加锁,最多等待50ms,上锁以后10秒自动解锁
boolean isLock = lock.tryLock(50, 10000, TimeUnit.MILLISECONDS);
if (!isLock) {
// 加锁失败,说明其他线程正在查DB重建缓存。休眠50ms后重试本方法
Thread.sleep(50);
return queryWithCompleteProtection(keyPrefix, id, returnType, dbFallback, time, unit, bloomFilterName);
}
try {
// 获取锁成功后,必须【双重检查】缓存,防止其他线程刚刚重建好
json = stringRedisTemplate.opsForValue().get(key);
if (json != null && !json.isEmpty()) {
return objectMapper.readValue(json, returnType);
}
if (json != null && json.isEmpty()) {
return null;
}
// ================= 4. 查询数据库 =================
R result = dbFallback.apply(id);
// ================= 5. 缓存雪崩 & 穿透收尾处理 =================
if (result == null) {
// DB查不到:缓存空值(防穿透),设置极短TTL
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
} else {
// DB查到了:写入缓存。
// 防雪崩:在基础TTL上,加上 1~5 分钟的随机抖动时间
long randomJitter = ThreadLocalRandom.current().nextLong(1, 6);
long finalTtlMinutes = unit.toMinutes(time) + randomJitter;
stringRedisTemplate.opsForValue().set(
key,
objectMapper.writeValueAsString(result),
finalTtlMinutes,
TimeUnit.MINUTES
);
}
return result;
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
} catch (Exception e) {
log.error("缓存处理异常", e);
throw new RuntimeException("缓存处理异常", e);
}
}
}3. 代码实现
3.1 调用实例 (ProductService)
在 Service 层中,结合 Spring Data JPA 进行调用,代码会保持非常简洁。
Java
package com.lj2508.redis_demo.service;
import com.lj2508.redis_demo.Util.CacheClient;
import com.lj2508.redis_demo.entity.Product;
import com.lj2508.redis_demo.repository.ProductRepository;
import jakarta.transaction.Transactional;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
@Slf4j
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private CacheClient cacheClient;
@Autowired
private RedissonClient redissonClient;
/**
* 获取商品详情:整合 Redis 三大防护 + JPA
*/
public Product getProductById(Long id) {
return cacheClient.queryWithCompleteProtection(
"cache:product:", // 缓存前缀
id, // 查询 ID
Product.class, // 目标类型
// 使用 JPA 查询数据库 (核心:如果 DB 没数据,返回 null)
productId -> productRepository.findById(productId).orElse(null),
30L, // 基础过期时间
TimeUnit.MINUTES, // 时间单位
"productBloomFilter" // 布隆过滤器名称
);
}
/**
* 新增商品并同步布隆过滤器
*/
@Transactional
public Product createProduct(Product product) {
// 1. 保存到数据库
Product savedProduct = productRepository.save(product);
// 2. 同步到布隆过滤器
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("productBloomFilter");
bloomFilter.add("cache:product:" + savedProduct.getId());
return savedProduct;
}
}3.2 初始预热与增量同步
项目启动预热:
Java
@Component
public class BloomFilterInitializer implements CommandLineRunner {
@Autowired private ProductRepository productRepository;
@Autowired private RedissonClient redissonClient;
@Override
public void run(String... args) {
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter("productBloomFilter");
bloomFilter.tryInit(100000L, 0.01);
// 全量同步 ID
productRepository.findAll().forEach(p -> bloomFilter.add("cache:product:" + p.getId()));
}
}
新增数据同步:
Java
@Transactional
public Product saveProduct(Product product) {
Product saved = productRepository.save(product);
// 新增时必须填充布隆过滤器
redissonClient.getBloomFilter("productBloomFilter").add("cache:product:" + saved.getId());
return saved;
}4. 维护与优化说明
关于删除操作: 标准布隆过滤器采用位映射,不支持物理删除(因为多个数据的 Hash 可能映射到同一个位,删除一个会影响其他数据)。
为什么不用处理?:当数据库删除数据后,布隆过滤器依然会放行该请求,但请求进入缓存层会发现没有数据,进入数据库层也查不到,此时会缓存一个空对象。这仅仅增加了极少量的无效查询,不会对数据库造成压力。
关于凌晨重建: 如果数据库频繁删除数据,导致布隆过滤器的“假阳性(误判率)”逐渐增高,建议在凌晨流量低谷期,通过异步任务清空并重新从数据库加载 ID,以保证过滤器的准确性。
声明:本文内容经过大量的 AI 生成和辅助。仅为了给遇到此问题的朋友一个简单的思路,如果有更好的方法,欢迎大家评论区交流

评论区