目录
解决方案一:缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存
缓存
“缓存的原始意义,是指访问速度比一般随机存取存储器快的一种高速存储器。简单来说,缓存就是数据交换的缓冲区。当我们的硬件需要读取数据的时候,一般会先在缓存中查找想要的数据,这样速度比较快。如果找不到的话,就会在内存中查找,但这样会降低电脑的运行速度。所以缓存的作用,就是帮助我们的电脑更快的运行。可以说缓存的设置,是所有计算机系统可以发挥高性能的重要因素之一。”
缓存穿透
缓存穿透:查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库
解决方案一:缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存
优点:简单
缺点:消耗内存,可能会发生不一致的问题
例: 一个get请求:api/news/getById/1
解决方案二:布隆过滤器
优点:内存占用较少,没有多余key
缺点:实现复杂,存在误判
例: 一个get请求:api/news/getById/1
布隆过滤器
bitmap(位图):相当于是一个以(bit)位为单位的数组,数组中每个单元只能存储二进制数0或1
布隆过滤器作用:布隆过滤器可以用于检索一个元素是否在一个集合中。
布隆过滤器存储数据时,会通过多个hash函数获取hash值,根据hash计算数组对应位置改为1。
误判率:数组越小误判率就越大,数组越大误判率就越小,但是同时带来了更多的内存消耗。
查询数据时,使用相同的hash函数获取hash值,判断对应位置是否都为1,如果是,则该数据存在redis中;反之,不在,需要访问DB。这种情况会存在误判的情况,如下:
布隆过滤器的使用
布隆过滤器目前有3种实现方式
- google的 guava
- redisson
- redis的 reBloom.so插件
这里我们使用Redisson来实现
依赖引入
在pom.xml文件中引入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.10.6</version>
</dependency>
代码实现
@Service
public class BloomFilterService{
@Autowired
private RedissonClient redissonClient;
private static long size = 10000000L;//预计要插入多少数据
private static double fpp = 0.05;//期望的误判率
// 自定义布隆过滤器的 key
private String BLOOM_FILTER_KEY = "BachelorHT";
/**
*
* 向布隆过滤器中添加数据, 模拟向布隆过滤器中添加10亿个数据
*/
public void addToBloomFilter() {
// 获取布隆过滤器
RBloomFilter<Integer> bloomFilter = redissonClient.getBloomFilter(BLOOM_FILTER_KEY);
// 初始化,容量为10亿, 误判率为0.05
bloomFilter.tryInit(size,fpp);
// 模拟向布隆过滤器中添加10亿个数据
for (int i = 1; i <= size; i++) {
bloomFilter.add(i);
}
}
/**
*
* 判断数据是否存在
*/
public boolean contains(int value) {
// 获取布隆过滤器
RBloomFilter<Integer> bloomFilter = redissonClient.getBloomFilter(BLOOM_FILTER_KEY);
// 判断是否存在
return bloomFilter.contains(value);
}
}
缓存击穿
缓存击穿:给某一个key设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮
解决方案一:互斥锁
互斥锁
锁具有 互斥性
,加锁之后线程从原来的 并行
变成了 串行
。第一个线程过来访问,获得锁,只有第一个线程能够去直接访问数据库,然后把数据写入缓存。第二个线程过来,没得到锁,只能不断重试去获得锁,直至第一个线程释放锁,然后第二个线程就能够直接从缓存中获得数据。
注:金融业务(涉及钱),需要保证数据强一致性,使用互斥锁。
互斥锁
流程图如下:
实现代码如下:
Controller:
public Result queryById(Long id) {
//缓存穿透
//互斥锁解决缓存击穿
Shop shop = serviceImpl.queryWithMutex(id);
if (shop == null) {
return Result.fail("数据不存在!");
}
//返回
return Result.ok(shop);
}
ServiceImpl:
@Autowired
private StringRedisTemplate stringRedisTemplate;
public Shop queryWithMutex(Long id) {
String key = CACHE_BOOK_KEY + id;
//1.从redis查询缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if (StrUtil.isNotBlank(shopJson)) {
//3.存在,直接返回
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return shop;
}
if (shopJson != null) {
return null;
}
//4.实现缓存重建
//4.1获取互斥锁
String lockKey = "lock:book:" + id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
//4.2判断是否获取成功
if (!isLock) {
//4.3失败,则休眠并重试
Thread.sleep(50);
//递归
return queryWithMutex(id);
}
//4.4成功,根据id查询数据库
shop = getById(id);
//5.不存在,返回错误
if (shop == null) {
//缓存击穿问题
//将空值写入redis
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//6.存在,写入redis
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_BOOK_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
//7.释放互斥锁
unlock(lockKey);
}
//8.返回
return shop;
}
//获得锁
public boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}
//释放锁
public void unlock(String key) {
stringRedisTemplate.delete(key);
}
解决方案二:逻辑过期
逻辑过期
不设置失效时间,而是在value
中添加一个时间值,每次访问时,获取当前时间,与过期时间比较,如果当前时间小于过期时间,没过期。反之,逻辑过期,此时获取互斥锁,并开启新线程,返回过期数据。在新线程中,查询DB重建缓存数据,将其写入缓存并重置过期时间,释放锁。
若在线程3没有释放互斥锁时,线程3获取锁失败后,直接返回过期时间。
该方法保证了高可用,提高了性能。
如图所示:
当然两种方案各有利弊:
>如果选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么高,锁需要等,也有可能产生死锁的问题
>如果选择key的逻辑删除,则优先考虑的高可用性,性能比较高,但是数据同步这块做不到强一致。
缓存雪崩
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
解决方案:
- 给不同的Key的TTL添加随机值:
将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
- 利用Redis集群提高服务的可用性 哨兵模式、集群模式
- 给缓存业务添加降级限流策略 ngxin或spring cloud gateway
- 给业务添加多级缓存 Guava或Caffeine
总结:
《缓存三兄弟》
穿透无中生有key,布隆过滤null隔离。
缓存击穿过期key,锁与非期解难题。
雪崩大量过期key,过期时间要随机。
面试必考三兄 弟 ,可用限流来保底。