副标题:防御缓存攻击,守护系统稳定!🛡️
🎬 开场:那些年,我们遇到的缓存灾难
真实事故案例
案例1:某电商平台双11故障 💥
23:58 流量开始暴增
23:59 缓存大量key同时过期
00:00 数据库瞬间被打爆
00:01 响应时间从10ms飙升到5000ms
00:02 系统雪崩,服务不可用
00:05 紧急切换到降级页面
损失:约2000万交易额
根因:缓存雪崩 ❄️
案例2:某社交网站被攻击 🔥
10:00 黑客发起攻击
大量查询不存在的用户ID
user_999999999, user_888888888...
10:01 缓存全部未命中
10:02 请求全部打到数据库
10:03 数据库CPU 100%
10:05 数据库连接池耗尽
10:10 整个服务瘫痪
根因:缓存穿透 🕳️
📚 三大问题对比
| 问题类型 | 现象 | 原因 | 影响 |
|---|---|---|---|
| 缓存穿透 🕳️ | 查询不存在的数据 | 缓存和DB都没有数据 | DB压力大 |
| 缓存击穿 💥 | 热点key过期 | 单个key过期 | 瞬时压力 |
| 缓存雪崩 ❄️ | 大量key同时过期 | 批量过期 | 系统崩溃 |
🕳️ 问题1:缓存穿透
什么是缓存穿透?
正常流程:
客户端 → 查询 user:1000
↓
缓存命中
↓
返回数据 ✅
缓存穿透:
客户端 → 查询 user:999999999 (不存在的ID)
↓
缓存未命中 ❌
↓
查询数据库
↓
数据库也没有 ❌
↓
无法缓存 (NULL不缓存)
↓
每次都打到数据库 💥
攻击示例
/**
* 模拟缓存穿透攻击
*/
public class CachePenetrationAttack {
public static void attack() {
ExecutorService executor = Executors.newFixedThreadPool(1000);
// 发起10万次查询不存在的数据
for (int i = 0; i < 100000; i++) {
final int id = 999999900 + i; // 不存在的ID
executor.submit(() -> {
// 每次都穿透到数据库
userService.getUser(id);
});
}
// 结果:数据库被打爆!💥
}
}
解决方案
方案1:缓存空对象 ⭐⭐⭐
@Service
public class UserService {
@Autowired
private RedisTemplate<String, User> redisTemplate;
@Autowired
private UserMapper userMapper;
// 空对象标记
private static final User NULL_USER = new User();
/**
* 缓存空对象
*/
public User getUser(Long userId) {
String cacheKey = "user:" + userId;
// 1. 查询缓存
User user = redisTemplate.opsForValue().get(cacheKey);
// 2. 如果是空对象,直接返回null
if (NULL_USER.equals(user)) {
log.info("命中空对象缓存: {}", userId);
return null;
}
if (user != null) {
return user;
}
// 3. 查询数据库
user = userMapper.selectById(userId);
if (user != null) {
// 缓存真实数据
redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
} else {
// 缓存空对象(设置较短过期时间)
redisTemplate.opsForValue().set(cacheKey, NULL_USER, 5, TimeUnit.MINUTES);
log.info("缓存空对象: {}", userId);
}
return user;
}
}
优缺点:
优点 ✅:
- 实现简单
- 保护数据库
缺点 ❌:
- 占用缓存空间
- 可能缓存大量无效数据
- 攻击者可以构造大量不同的key
适用场景:
- 随机攻击
- 误操作
方案2:布隆过滤器 ⭐⭐⭐⭐⭐
/**
* 布隆过滤器防止缓存穿透
*/
@Component
public class BloomFilterService {
@Autowired
private RedisTemplate redisTemplate;
// 布隆过滤器key
private static final String BLOOM_FILTER_KEY = "user:bloom:filter";
// 预期插入数量
private static final long EXPECTED_INSERTIONS = 10000000L; // 1000万
// 误判率
private static final double FPP = 0.01; // 1%
/**
* 初始化布隆过滤器
*/
@PostConstruct
public void initBloomFilter() {
// 使用Redisson的布隆过滤器
RBloomFilter<Long> bloomFilter =
redissonClient.getBloomFilter(BLOOM_FILTER_KEY);
// 初始化
bloomFilter.tryInit(EXPECTED_INSERTIONS, FPP);
// 加载所有用户ID
List<Long> allUserIds = userMapper.selectAllIds();
allUserIds.forEach(bloomFilter::add);
log.info("布隆过滤器初始化完成,加载{}个用户ID", allUserIds.size());
}
/**
* 检查用户是否存在
*/
public boolean userExists(Long userId) {
RBloomFilter<Long> bloomFilter =
redissonClient.getBloomFilter(BLOOM_FILTER_KEY);
return bloomFilter.contains(userId);
}
}
@Service
public class UserServiceWithBloom {
@Autowired
private BloomFilterService bloomFilterService;
/**
* 使用布隆过滤器
*/
public User getUser(Long userId) {
// 1. 先用布隆过滤器判断
if (!bloomFilterService.userExists(userId)) {
log.info("布隆过滤器拦截: {}", userId);
return null; // 一定不存在
}
// 2. 可能存在,查询缓存
String cacheKey = "user:" + userId;
User user = redisTemplate.opsForValue().get(cacheKey);
if (user != null) {
return user;
}
// 3. 查询数据库
user = userMapper.selectById(userId);
if (user != null) {
redisTemplate.opsForValue().set(cacheKey, user, 30, TimeUnit.MINUTES);
}
return user;
}
}
布隆过滤器原理:
布隆过滤器原理:
1. 数据结构:
位数组 + 多个hash函数
例如:100位的数组
[0,0,0,0,0,0,0,0,0,0,...,0,0,0]
2. 添加元素 "user:1000":
hash1("user:1000") % 100 = 23 → 置1
hash2("user:1000") % 100 = 67 → 置1
hash3("user:1000") % 100 = 89 → 置1
[0,0,...,1,...,1,...,1,...,0]
↑23 ↑67 ↑89
3. 检查元素 "user:1000":
hash1("user:1000") % 100 = 23 → 检查位23
hash2("user:1000") % 100 = 67 → 检查位67
hash3("user:1000") % 100 = 89 → 检查位89
如果3个位都是1 → 可能存在
如果任何一个位是0 → 一定不存在 ✅
4. 误判:
可能把不存在的判断为存在(hash碰撞)
但绝不会把存在的判断为不存在
本地布隆过滤器(Guava):
/**
* 本地布隆过滤器(适合数据量小的场景)
*/
@Component
public class LocalBloomFilter {
private BloomFilter<Long> bloomFilter;
@PostConstruct
public void init() {
// 创建布隆过滤器
this.bloomFilter = BloomFilter.create(
Funnels.longFunnel(),
10000000L, // 预期元素数量
0.01 // 误判率
);
// 加载数据
List<Long> allUserIds = userMapper.selectAllIds();
allUserIds.forEach(bloomFilter::put);
log.info("本地布隆过滤器初始化完成");
}
public boolean mightContain(Long userId) {
return bloomFilter.mightContain(userId);
}
/**
* 添加新用户时更新布隆过滤器
*/
public void addUser(Long userId) {
bloomFilter.put(userId);
}
}
优缺点:
优点 ✅:
- 内存占用极小
- 查询速度快 O(k)
- 能拦截大部分不存在的key
缺点 ❌:
- 存在误判(约1%)
- 无法删除元素
- 需要定期重建
适用场景:
- 大量数据
- 可以容忍小概率误判
- 读多写少
方案3:参数校验 ⭐⭐⭐⭐
/**
* 严格的参数校验
*/
@RestController
@RequestMapping("/user")
public class UserController {
/**
* 参数校验
*/
@GetMapping("/{userId}")
public User getUser(@PathVariable Long userId) {
// 1. 参数合法性校验
if (userId == null || userId <= 0) {
throw new IllegalArgumentException("用户ID非法");
}
// 2. 范围校验
if (userId > 100000000) { // 最大用户ID
throw new IllegalArgumentException("用户ID超出范围");
}
// 3. 格式校验
if (!isValidUserId(userId)) {
throw new IllegalArgumentException("用户ID格式错误");
}
return userService.getUser(userId);
}
/**
* 校验用户ID格式
*/
private boolean isValidUserId(Long userId) {
// 例如:用户ID必须是10位
return userId >= 1000000000L && userId < 10000000000L;
}
}
💥 问题2:缓存击穿
什么是缓存击穿?
场景:热点商品详情
正常情况:
10万QPS → 缓存 → 返回 ✅
某一时刻:
缓存key过期!❌
↓
10万QPS → 数据库
↓
数据库崩溃!💥
解决方案
方案1:互斥锁 ⭐⭐⭐⭐
@Service
public class ProductService {
@Autowired
private RedisTemplate<String, Product> redisTemplate;
@Autowired
private ProductMapper productMapper;
/**
* 使用互斥锁防止缓存击穿
*/
public Product getProduct(Long productId) {
String cacheKey = "product:" + productId;
// 1. 查询缓存
Product product = redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 2. 缓存未命中,尝试获取锁
String lockKey = "lock:product:" + productId;
try {
// 尝试获取分布式锁
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
// 获取锁成功,查询数据库
log.info("获取锁成功,查询数据库: {}", productId);
// 双重检查:再次查询缓存
product = redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
return product;
}
// 查询数据库
product = productMapper.selectById(productId);
if (product != null) {
// 写入缓存
redisTemplate.opsForValue().set(
cacheKey,
product,
30,
TimeUnit.MINUTES
);
}
return product;
} else {
// 获取锁失败,等待后重试
log.info("获取锁失败,等待重试: {}", productId);
Thread.sleep(50);
return getProduct(productId); // 递归重试
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("查询商品失败", e);
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
}
}
更优雅的实现(Redisson):
@Service
public class ProductServiceWithRedisson {
@Autowired
private RedissonClient redissonClient;
/**
* 使用Redisson分布式锁
*/
public Product getProduct(Long productId) {
String cacheKey = "product:" + productId;
// 1. 查询缓存
Product product = getFromCache(cacheKey);
if (product != null) {
return product;
}
// 2. 获取锁
String lockKey = "lock:product:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁,最多等待10秒,锁超时时间30秒
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (locked) {
// 双重检查
product = getFromCache(cacheKey);
if (product != null) {
return product;
}
// 查询数据库
product = productMapper.selectById(productId);
if (product != null) {
setToCache(cacheKey, product, 30, TimeUnit.MINUTES);
}
return product;
} else {
// 获取锁超时
throw new RuntimeException("系统繁忙,请稍后重试");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("查询失败", e);
} finally {
// 释放锁
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
}
流程图:
并发10000个请求查询同一个商品:
线程1 → 获取锁成功 ✅
↓
查询数据库
↓
写入缓存
↓
释放锁
线程2-10000 → 获取锁失败 ❌
↓
等待50ms
↓
重试查询
↓
命中缓存 ✅
结果:只有1次数据库查询!
优缺点:
优点 ✅:
- 完全避免击穿
- 只有一个线程查询数据库
- 保护数据库
缺点 ❌:
- 加锁影响性能
- 等待时间长
- 分布式锁实现复杂
适用场景:
- 超热点数据
- 数据库压力大
方案2:逻辑过期 ⭐⭐⭐⭐⭐
/**
* 带逻辑过期时间的缓存对象
*/
@Data
public class CacheData<T> {
private T data; // 实际数据
private Long expireTime; // 逻辑过期时间(时间戳)
public boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
}
@Service
public class ProductServiceWithLogicalExpire {
@Autowired
private RedisTemplate<String, CacheData<Product>> redisTemplate;
// 重建缓存的线程池
private final ExecutorService rebuildExecutor = Executors.newFixedThreadPool(10);
/**
* 逻辑过期方案
*/
public Product getProduct(Long productId) {
String cacheKey = "product:" + productId;
// 1. 查询缓存
CacheData<Product> cacheData = redisTemplate.opsForValue().get(cacheKey);
if (cacheData == null) {
// 缓存未命中(这种情况很少,因为永不过期)
return loadAndCache(productId);
}
// 2. 检查逻辑过期时间
if (!cacheData.isExpired()) {
// 未过期,直接返回
return cacheData.getData();
}
// 3. 已过期,异步重建缓存
String lockKey = "lock:rebuild:" + productId;
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
// 获取锁成功,异步重建缓存
rebuildExecutor.submit(() -> {
try {
log.info("异步重建缓存: {}", productId);
rebuildCache(productId);
} finally {
redisTemplate.delete(lockKey);
}
});
}
// 4. 返回旧数据(虽然过期,但还能用)
log.info("返回过期数据: {}", productId);
return cacheData.getData();
}
/**
* 重建缓存
*/
private void rebuildCache(Long productId) {
String cacheKey = "product:" + productId;
// 查询数据库
Product product = productMapper.selectById(productId);
if (product != null) {
// 封装为CacheData
CacheData<Product> cacheData = new CacheData<>();
cacheData.setData(product);
cacheData.setExpireTime(
System.currentTimeMillis() + 30 * 60 * 1000 // 30分钟后过期
);
// 写入缓存(不设置Redis过期时间,永不过期)
redisTemplate.opsForValue().set(cacheKey, cacheData);
log.info("缓存重建完成: {}", productId);
}
}
/**
* 初次加载并缓存
*/
private Product loadAndCache(Long productId) {
Product product = productMapper.selectById(productId);
if (product != null) {
String cacheKey = "product:" + productId;
CacheData<Product> cacheData = new CacheData<>();
cacheData.setData(product);
cacheData.setExpireTime(
System.currentTimeMillis() + 30 * 60 * 1000
);
redisTemplate.opsForValue().set(cacheKey, cacheData);
}
return product;
}
}
流程图:
请求到来:
查询缓存 → 命中 → 检查逻辑过期时间
↓
未过期
↓
直接返回 ✅
查询缓存 → 命中 → 检查逻辑过期时间
↓
已过期
↓
尝试获取锁
↓
┌─────────┴─────────┐
获取成功 获取失败
↓ ↓
异步重建缓存 直接返回旧数据 ✅
↓
返回旧数据 ✅
特点:
- 永远不会阻塞
- 最多返回略微过期的数据
- 异步更新,不影响性能
优缺点:
优点 ✅:
- 性能最好(不阻塞)
- 永远有数据返回
- 缓存永不过期
缺点 ❌:
- 数据可能略微不一致
- 实现复杂
- 占用额外内存(存储过期时间)
适用场景:
- 对一致性要求不高
- 性能要求极高
- 热点数据
方案3:热点数据永不过期 ⭐⭐⭐
@Service
public class HotDataService {
/**
* 热点数据永不过期
*/
@PostConstruct
public void initHotData() {
// 预热热点数据
List<Long> hotProductIds = getHotProductIds();
for (Long productId : hotProductIds) {
Product product = productMapper.selectById(productId);
if (product != null) {
String cacheKey = "product:hot:" + productId;
// 不设置过期时间
redisTemplate.opsForValue().set(cacheKey, product);
}
}
log.info("热点数据预热完成,共{}个", hotProductIds.size());
}
/**
* 定时刷新热点数据
*/
@Scheduled(fixedDelay = 600000) // 每10分钟
public void refreshHotData() {
List<Long> hotProductIds = getHotProductIds();
for (Long productId : hotProductIds) {
Product product = productMapper.selectById(productId);
if (product != null) {
String cacheKey = "product:hot:" + productId;
redisTemplate.opsForValue().set(cacheKey, product);
}
}
log.info("热点数据刷新完成");
}
}
❄️ 问题3:缓存雪崩
什么是缓存雪崩?
场景:大促活动准备
20:00 运营人员批量导入10万个商品
所有商品设置1小时缓存
过期时间:21:00
21:00 10万个key同时过期!❌
↓
大量请求打到数据库
↓
数据库CPU 100%
↓
响应超时
↓
系统雪崩!💥
解决方案
方案1:随机过期时间 ⭐⭐⭐⭐⭐
@Service
public class ProductServiceWithRandomExpire {
/**
* 设置随机过期时间
*/
public void cacheProduct(Product product) {
String cacheKey = "product:" + product.getId();
// 基础过期时间:30分钟
long baseExpire = 30 * 60;
// 随机增加 0-10分钟
long randomExpire = ThreadLocalRandom.current().nextLong(0, 10 * 60);
// 最终过期时间:30-40分钟之间
long expireSeconds = baseExpire + randomExpire;
redisTemplate.opsForValue().set(
cacheKey,
product,
expireSeconds,
TimeUnit.SECONDS
);
log.info("商品缓存,过期时间: {}秒", expireSeconds);
}
/**
* 更通用的方法
*/
public <T> void cacheWithRandomExpire(String key, T value,
long baseMinutes, double randomPercent) {
// baseMinutes: 基础分钟数
// randomPercent: 随机百分比,例如0.2表示增加0-20%的时间
long baseSeconds = baseMinutes * 60;
long randomSeconds = (long) (baseSeconds * Math.random() * randomPercent);
long expireSeconds = baseSeconds + randomSeconds;
redisTemplate.opsForValue().set(
key,
value,
expireSeconds,
TimeUnit.SECONDS
);
}
}
// 使用示例
public void example() {
// 过期时间:30分钟 + 随机(0-6分钟)
cacheWithRandomExpire("key1", value1, 30, 0.2);
// 过期时间:60分钟 + 随机(0-12分钟)
cacheWithRandomExpire("key2", value2, 60, 0.2);
}
效果对比:
固定过期时间:
21:00:00 10万个key同时过期 💥
随机过期时间:
21:00:00 1000个key过期
21:00:01 980个key过期
21:00:02 1020个key过期
...
21:10:00 最后一批key过期
结果:压力分散到10分钟内!✅
方案2:多级缓存 ⭐⭐⭐⭐
/**
* 多级缓存架构
*/
@Service
public class MultiLevelCacheService {
@Autowired
private RedisTemplate<String, Product> redisTemplate;
// 本地缓存(Caffeine)
private final LoadingCache<Long, Product> localCache;
public MultiLevelCacheService() {
this.localCache = Caffeine.newBuilder()
.maximumSize(10000) // 最多1万个
.expireAfterWrite(5, TimeUnit.MINUTES) // 5分钟过期
.build(this::loadFromRedis);
}
/**
* 三级缓存查询
*/
public Product getProduct(Long productId) {
// Level 1: 本地缓存
try {
Product product = localCache.get(productId);
if (product != null) {
log.info("L1缓存命中: {}", productId);
return product;
}
} catch (Exception e) {
log.warn("L1缓存查询失败", e);
}
// Level 2: Redis缓存
Product product = loadFromRedis(productId);
if (product != null) {
log.info("L2缓存命中: {}", productId);
return product;
}
// Level 3: 数据库
product = loadFromDB(productId);
if (product != null) {
log.info("L3数据库命中: {}", productId);
// 回写缓存
saveToRedis(productId, product);
}
return product;
}
private Product loadFromRedis(Long productId) {
String cacheKey = "product:" + productId;
return redisTemplate.opsForValue().get(cacheKey);
}
private Product loadFromDB(Long productId) {
return productMapper.selectById(productId);
}
private void saveToRedis(Long productId, Product product) {
String cacheKey = "product:" + productId;
long randomExpire = 30 + ThreadLocalRandom.current().nextInt(10);
redisTemplate.opsForValue().set(
cacheKey,
product,
randomExpire,
TimeUnit.MINUTES
);
}
}
缓存架构图:
┌─────────────┐
│ 客户端 │
└──────┬──────┘
│
① 请求
│
┌──────▼─────────────┐
│ L1: 本地缓存 │ 容量:1万
│ (Caffeine) │ 过期:5分钟
│ 命中率:60% │
└──────┬─────────────┘
│ 未命中
② 查询L2
│
┌──────▼─────────────┐
│ L2: Redis缓存 │ 容量:100万
│ 命中率:35% │ 过期:30分钟
└──────┬─────────────┘
│ 未命中
③ 查询L3
│
┌──────▼─────────────┐
│ L3: MySQL数据库 │ 命中率:5%
└────────────────────┘
总命中率:95%
数据库查询:只有5%
方案3:缓存预热 ⭐⭐⭐⭐
@Component
public class CacheWarmUp {
@Autowired
private ProductMapper productMapper;
@Autowired
private RedisTemplate<String, Product> redisTemplate;
/**
* 启动时预热缓存
*/
@PostConstruct
public void warmUp() {
log.info("开始缓存预热...");
// 加载热点数据
List<Product> hotProducts = productMapper.selectHotProducts(1000);
int count = 0;
for (Product product : hotProducts) {
String cacheKey = "product:" + product.getId();
// 设置随机过期时间
long expireMinutes = 30 + ThreadLocalRandom.current().nextInt(30);
redisTemplate.opsForValue().set(
cacheKey,
product,
expireMinutes,
TimeUnit.MINUTES
);
count++;
}
log.info("缓存预热完成,共预热{}个商品", count);
}
/**
* 大促前预热
*/
public void warmUpBeforePromotion() {
log.info("大促预热开始...");
// 分批加载,避免数据库压力
int batchSize = 1000;
int offset = 0;
while (true) {
List<Product> products = productMapper.selectByPage(offset, batchSize);
if (products.isEmpty()) {
break;
}
// 批量写入Redis
for (Product product : products) {
cacheProduct(product);
}
offset += batchSize;
// 休息一下,避免数据库压力
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
log.info("大促预热完成");
}
}
方案4:降级熔断 ⭐⭐⭐⭐⭐
/**
* 降级熔断保护
*/
@Service
public class ProductServiceWithCircuitBreaker {
@Autowired
private ProductMapper productMapper;
/**
* 使用Hystrix/Resilience4j熔断
*/
@HystrixCommand(
fallbackMethod = "getProductFallback",
commandProperties = {
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "5000")
}
)
public Product getProduct(Long productId) {
// 查询数据库
return productMapper.selectById(productId);
}
/**
* 降级方法
*/
public Product getProductFallback(Long productId, Throwable throwable) {
log.warn("触发降级,返回默认数据: {}", productId, throwable);
// 返回降级数据
Product product = new Product();
product.setId(productId);
product.setName("商品暂时无法查看");
product.setPrice(BigDecimal.ZERO);
return product;
}
}
🎯 综合防御方案
实际项目架构
/**
* 生产级缓存服务
*/
@Service
public class ProductionCacheService {
@Autowired
private RedissonClient redissonClient;
@Autowired
private ProductMapper productMapper;
// 本地缓存
private final Cache<Long, Product> localCache = Caffeine.newBuilder()
.maximumSize(10000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
// 布隆过滤器
private RBloomFilter<Long> bloomFilter;
@PostConstruct
public void init() {
// 初始化布隆过滤器
bloomFilter = redissonClient.getBloomFilter("product:bloom");
bloomFilter.tryInit(10000000L, 0.01);
// 加载所有商品ID
List<Long> allIds = productMapper.selectAllIds();
allIds.forEach(bloomFilter::add);
}
/**
* 综合方案查询商品
*/
public Product getProduct(Long productId) {
// 1. 参数校验
if (productId == null || productId <= 0) {
throw new IllegalArgumentException("商品ID非法");
}
// 2. 布隆过滤器(防穿透)
if (!bloomFilter.contains(productId)) {
log.info("布隆过滤器拦截: {}", productId);
return null;
}
// 3. 本地缓存
Product product = localCache.getIfPresent(productId);
if (product != null) {
return product;
}
// 4. Redis缓存(带逻辑过期,防击穿)
product = getFromRedisWithLogicalExpire(productId);
if (product != null) {
localCache.put(productId, product);
return product;
}
// 5. 数据库(加互斥锁)
product = loadFromDBWithLock(productId);
return product;
}
/**
* 从Redis获取(逻辑过期)
*/
private Product getFromRedisWithLogicalExpire(Long productId) {
// 实现逻辑过期方案...
return null;
}
/**
* 从数据库加载(加锁)
*/
private Product loadFromDBWithLock(Long productId) {
String lockKey = "lock:product:" + productId;
RLock lock = redissonClient.getLock(lockKey);
try {
if (lock.tryLock(5, 30, TimeUnit.SECONDS)) {
Product product = productMapper.selectById(productId);
if (product != null) {
// 缓存(随机过期时间,防雪崩)
cacheWithRandomExpire(productId, product);
} else {
// 缓存空对象(防穿透)
cacheNullValue(productId);
}
return product;
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
return null;
}
/**
* 缓存(随机过期时间)
*/
private void cacheWithRandomExpire(Long productId, Product product) {
long baseSeconds = 30 * 60; // 30分钟
long randomSeconds = ThreadLocalRandom.current().nextLong(0, 10 * 60); // 0-10分钟
RBucket<Product> bucket = redissonClient.getBucket("product:" + productId);
bucket.set(product, baseSeconds + randomSeconds, TimeUnit.SECONDS);
}
/**
* 缓存空对象
*/
private void cacheNullValue(Long productId) {
RBucket<String> bucket = redissonClient.getBucket("product:null:" + productId);
bucket.set("NULL", 5, TimeUnit.MINUTES); // 5分钟过期
}
}
🎉 总结
对比表
| 问题 | 解决方案 | 推荐度 | 适用场景 |
|---|---|---|---|
| 缓存穿透 🕳️ | 布隆过滤器 | ⭐⭐⭐⭐⭐ | 海量数据 |
| 缓存空对象 | ⭐⭐⭐ | 小规模数据 | |
| 参数校验 | ⭐⭐⭐⭐ | 所有场景 | |
| 缓存击穿 💥 | 互斥锁 | ⭐⭐⭐⭐ | 强一致性 |
| 逻辑过期 | ⭐⭐⭐⭐⭐ | 高性能 | |
| 永不过期 | ⭐⭐⭐ | 超热点数据 | |
| 缓存雪崩 ❄️ | 随机过期 | ⭐⭐⭐⭐⭐ | 所有场景 |
| 多级缓存 | ⭐⭐⭐⭐ | 高并发 | |
| 降级熔断 | ⭐⭐⭐⭐⭐ | 保护系统 |
记忆口诀 📝
缓存三灾要记牢,
穿透击穿和雪崩。
穿透攻击不存在,
布隆过滤来防范。
空对象也可缓存,
参数校验第一关。
击穿热点突然过期,
互斥锁来保护数据库。
逻辑过期更优雅,
返回旧数异步更新。
雪崩批量同时过期,
随机时间来分散。
多级缓存增可用,
降级熔断保系统!
愿你的缓存固若金汤,系统永不崩溃! 🛡️✨