缓存实战
1、缓存穿透
先来了解一个小图,
1.1 概念:
缓存穿透指一个一定不存在的数据,由于缓存未命中这条数据,就会去查询数据库,数据库也没有这条数据,所以返回结果是 null。如果每次查询都走数据库,则缓存就失去了意义,就像穿透了缓存一样。
1.2 带来的风险
利用不存在的数据进行攻击,数据库压力增大,最终导致系统崩溃。
1.3 解决方案
对结果 null 进行缓存,并加入短暂的过期时间。
2、缓存雪崩
2.1 概念
缓存雪崩是指我们缓存多条数据时,采用了相同的过期时间,比如 00:00:00 过期,如果这个时刻缓存同时失效,而有大量请求进来了,因未缓存数据,所以都去查询数据库了,数据库压力增大,最终就会导致雪崩。
2.2 带来的风险
尝试找到大量 key 同时过期的时间,在某时刻进行大量攻击,数据库压力增大,最终导致系统崩溃。
2.3 解决方案
在原有的失效时间基础上增加一个随机值,比如 1-5 分钟随机,降低缓存的过期时间的重复率,避免发生缓存集体失效。
3、缓存击穿
3.1 概念
某个 key 设置了过期时间,但在正好失效的时候,有大量请求进来了,导致请求都到数据库查询了。
3.2 解决方案
大量并发时,只让一个请求可以获取到查询数据库的锁,其他请求需要等待,查到以后释放锁,其他请求获取到锁后,先查缓存,缓存中有数据,就不用查数据库。
4、 加锁解决缓存击穿
怎么处理缓存穿透、雪崩、击穿的问题呢?
对空结果进行缓存,用来解决缓存穿透问题。
设置过期时间,且加上随机值进行过期偏移,用来解决缓存雪崩问题。
加锁,解决缓存击穿问题。另外需要注意,加锁对性能会带来影响。
这里我们来看下用代码演示如何解决缓存击穿问题。
我们需要用 synchronized 来进行加锁。当然这是本地锁的方式
public List<TypeEntity> getTypeEntityListByLock() {
synchronized (this) {
// 1.从缓存中查询数据
String typeEntityListCache = stringRedisTemplate.opsForValue().get("typeEntityList");
if (!StringUtils.isEmpty(typeEntityListCache)) {
// 2.如果缓存中有数据,则从缓存中拿出来,并反序列化为实例对象,并返回结果
List<TypeEntity> typeEntityList = JSON.parseObject(typeEntityListCache, new TypeReference<List<TypeEntity>>(){});
return typeEntityList;
}
// 3.如果缓存中没有数据,从数据库中查询数据
System.out.println("The cache is empty");
List<TypeEntity> typeEntityListFromDb = this.list();
// 4.将从数据库中查询出的数据序列化 JSON 字符串
typeEntityListCache = JSON.toJSONString(typeEntityListFromDb);
// 5.将序列化后的数据存入缓存中,并返回数据库查询结果
stringRedisTemplate.opsForValue().set("typeEntityList", typeEntityListCache, 1, TimeUnit.DAYS);
return typeEntityListFromDb;
}
}
1.从缓存中查询数据。
2.如果缓存中有数据,则从缓存中拿出来,并反序列化为实例对象,并返回结果。
3.如果缓存中没有数据,从数据库中查询数据。
4.将从数据库中查询出的数据序列化 JSON 字符串。
5.将序列化后的数据存入缓存中,并返回数据库查询结果。
5、本地锁的问题
本地锁只能锁定当前服务的线程,如下图所示,部署了多个题目微服务,每个微服务用本地锁进行加锁。
本地锁在一般情况下没什么问题,但是在某些情况下就会出问题:
比如在高并发情况下用来锁库存就有问题了:
1.比如当前总库存为 100,被缓存在 Redis 中。
2.库存微服务 A 用本地锁扣减库存 1 之后,总库存为 99。
3.库存微服务 B 用本地锁扣减库存 1 之后,总库存为 99。
4.那库存扣减了 2 次后,还是 99,就超卖了 1 个。
那如何解决本地加锁的问题呢?请看下一篇:
https://editor.csdn.net/md?not_checkout=1&articleId=132869070