文章目录
缓存三兄弟
缓存穿透
什么是缓存穿透
请求非法 Key,缓存和数据库都无,请求都到了数据库中(穿透了缓存和数据库)
解决方法
1)缓存无效 Key:如果缓存和数据库都查不到某个 Key 的数据就写一个到 Redis 中去并设置一个短暂的 ttl。
2)布隆过滤器:过滤无效 Key,不让请求打到数据库中。
3)接口限流:根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制。
缓存击穿
什么是缓存击穿
热点 Key 过期,在数据库存在,在缓存中不存在(击穿了缓存)。
解决方案
1)热点数据永不过期:对秒杀、抢购等已知热点数据,ttl 设置为-1。
2)缓存预热:使用 HotKey 等手段,探测热点数据库,针对热点数据提前放入缓存。
3)加锁:保证只有一个请求去访问数据库和重建缓存。
缓存雪崩
什么是缓存雪崩
缓存在同一时间大面积的失效(像雪崩一样)。
解决方案
1)设置随机过期时间:在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时过期,从而减少缓存雪崩的风险。
2)缓存预热:针对热点数据提前预热,将其存入缓存中并设置合理的过期时间。
实战缓存穿透、击穿、雪崩
1. 引入 Redisson 依赖并配置
1)引入 Redis 和 Redisson 的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<verison>3.21.3</verison>
</dependency>
2)在配置文件application.yml|yaml|properties
中配置redis相关参数
# yml
spring:
data:
redis:
host: 127.0.0.1
port: 6379
password: # 有就写
2. 配置布隆过滤器
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 布隆过滤器配置
* 错误率越低,位数组越长,布隆过滤器的内存占用越大
* 错误率越低,散列 Hash 函数越多,计算耗时较长
*/
@Configuration(value = "rBloomFilterConfiguration")
public class RBloomFilterConfiguration {
/**
* 防止用户注册查询数据库的布隆过滤器
*/
@Bean
public RBloomFilter<String> myBloomFilter(RedissonClient redissonClient) {
RBloomFilter<String> cachePenetrationBloomFilter = redissonClient.getBloomFilter("rCachePenetrationBloomFilter");
// expectedInsertions:预估布隆过滤器存储的元素长度 = 1亿
// falseProbability:运行的误判率 = 设置为千分之一的误判率
cachePenetrationBloomFilter.tryInit(100000000L, 0.001);
return cachePenetrationBloomFilter;
}
}
3. 当缓存不存在时,查询布隆过滤器
没有命中缓存,这时先去布隆看看是否真的存在与数据库中,当布隆过滤器说不存在时,那么这条数据一定不存在与数据库中,可以直接告诉客户端不存在这条数据了
但是布隆过滤器说有这条数据,那么这条数据不一定存在数据库中
@Autowired
private RBloomFilter<String> myBloomFilter;
@SneakyThrows
@Override
public Object query(RequestParam requestParam) {
String redisKey = String.format("business_key_prefix_%s", requestParam.getId());
// 从缓存中查询
Object redisData = stringRedisTemplate.opsForValue().get(redisKey));
if (redisData != null) {
return redisData;
}
// 从布隆过滤器中判断是否存在-误判(布隆有不一定有, 布隆无就一定无)
boolean contains = myBloomFilter.contains(redisKey);
if (!contains) {
return null;
}
// ...
}
4. 判断是否已缓存了无效的 Key
如果缓存和数据库都查不到某个 Key 的数据就写一个到 Redis 中去并设置一个短暂的 ttl,这样做是为了防止有人一直用无效的 Key 恶意攻击,让每次请求都打到数据库中。
@Autowired
private RBloomFilter<String> myBloomFilter;
@SneakyThrows
@Override
public Object query(RequestParam requestParam) {
String redisKey = String.format("business_key_prefix_%s", requestParam.getId());
// 从缓存中查询
Object redisData = stringRedisTemplate.opsForValue().get(redisKey));
if (redisData != null) {
return redisData;
}
// 从布隆过滤器中判断是否存在-误判(布隆有不一定有, 布隆无就一定无)
boolean contains = myBloomFilter.contains(redisKey);
if (!contains) {
return null;
}
// 判断key是否存在空值-解决缓存穿透(不存在于缓存中,也不存在于数据库中)
redisData = stringRedisTemplate.opsForValue().get("data_is_null"+redisKey);
if (redisData != null) {
return null;
}
// ...
}
4. 加锁查数据库,若无数据则缓存空值
这里使用了 Redisson 中的 getLock.lock()
该方法会一直阻塞,直到锁释放,这样就能做到仅允许一个请求访问数据库。
注:当锁释放后其他线程查缓存即可,因为获取到锁的线程已经重建缓存了,如果不这样做,所有等待的线程还是会查数据库,对数据库造成压力。
@Autowired
private RBloomFilter<String> myBloomFilter;
@SneakyThrows
@Override
public Object query(RequestParam requestParam) {
String redisKey = String.format("business_key_prefix_%s", requestParam.getId());
// 从缓存中查询
Object redisData = stringRedisTemplate.opsForValue().get(redisKey));
if (redisData != null) {
return redisData;
}
// 从布隆过滤器中判断是否存在-误判(布隆有不一定有, 布隆无就一定无)
boolean contains = myBloomFilter.contains(redisKey);
if (!contains) {
return null;
}
// 判断key是否存在空值-解决缓存穿透(不存在于缓存中,也不存在于数据库中)
redisData = stringRedisTemplate.opsForValue().get(redisKey);
if (redisData != null) {
return null;
}
RLock lock = redissonClient.getLock(String.format("business_lock_key_%s", requestParam.getId()));
// 上锁-查询数据库-防止被布隆过滤器误判的请求大量打到数据库
try {
lock.lock();
// 再次从缓存中查询
redisData = stringRedisTemplate.opsForValue().get(String.format(GOTO_SHORT_LINK_KEY, fullShortUrl));
if (redisData != null) {
return redisData;
}
// 从数据库中查询
Object DbData = XxxMapper.selectOne(requestParam);
// 数据库中没有数据
if (DbData == null) {
// 缓存无效的 key,防止缓存穿透
stringRedisTemplate.opsForValue().set(
String.format("business_lock_key_%s", requestParam.getId()),
"-", 5, TimeUnit.MINUTES
);
return null;
}
// 重建缓存
stringRedisTemplate.opsForValue().set(
String.format("business_lock_key_%s", requestParam.getId()),
DbData,
// 固定时间+随机时间(防止缓存雪崩)
10000 + new Random().nextLong(1800 + 1),
TimeUnit.MICROSECONDS
);
return DbData;
} finally {
lock.unlock();
}
}
当然上述这种方案还是会存在问题,若有恶意用户一直使用随机值来请求,这样不仅查了数据库还缓存了无效的 Key,所以还需要配置限流的手段。
可以看看我写过的限流相关的博客:基于Spring AOP与Redisson的令牌桶限流注解实践