缓存穿透指一个一定不存在的数据,由于缓存未命中这条数据,就会去查询数据库,数据库也没有这条数据,所以返回结果是 null。如果每次查询都走数据库,则缓存就失去了意义,就像穿透了缓存一样。
3.1.2 带来的风险利用不存在的数据进行攻击,数据库压力增大,最终导致系统崩溃。
3.1.3 解决方案对结果 null 进行缓存,并加入短暂的过期时间。
3.2 缓存雪崩 3.2.1 缓存雪崩的概念缓存雪崩是指我们缓存多条数据时,采用了相同的过期时间,比如 00:00:00 过期,如果这个时刻缓存同时失效,而有大量请求进来了,因未缓存数据,所以都去查询数据库了,数据库压力增大,最终就会导致雪崩。
3.2.2 带来的风险尝试找到大量 key 同时过期的时间,在某时刻进行大量攻击,数据库压力增大,最终导致系统崩溃。
3.2.3 解决方案在原有的实效时间基础上增加一个碎挤汁,比如 1-5 分钟随机,降低缓存的过期时间的重复率,避免发生缓存集体实效。
3.3 缓存击穿 3.3.1 缓存击穿的概念某个 key 设置了过期时间,但在正好失效的时候,有大量请求进来了,导致请求都到数据库查询了。
3.3.2 解决方案大量并发时,只让一个请求可以获取到查询数据库的锁,其他请求需要等待,查到以后释放锁,其他请求获取到锁后,先查缓存,缓存中有数据,就不用查数据库。
四、加锁解决缓存击穿怎么处理缓存穿透、雪崩、击穿的问题呢?
对空结果进行缓存,用来解决缓存穿透问题。
设置过期时间,且加上随机值进行过期偏移,用来解决缓存雪崩问题。
加锁,解决缓存击穿问题。另外需要注意,加锁对性能会带来影响。
这里我们来看下用代码演示如何解决缓存击穿问题。
我们需要用 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.将序列化后的数据存入缓存中,并返回数据库查询结果。
五、本地锁的问题本地锁只能锁定当前服务的线程,如下图所示,部署了多个题目微服务,每个微服务用本地锁进行加锁。
本地锁在一般情况下没什么问题,但是当用来锁库存就有问题了:
1.当前总库存为 100,被缓存在 Redis 中。
2.库存微服务 A 用本地锁扣减库存 1 之后,总库存为 99。
3.库存微服务 B 用本地锁扣减库存 1 之后,总库存为 99。
4.那库存扣减了 2 次后,还是 99,就超卖了 1 个。
那如何解决本地加锁的问题呢?
缓存实战(中篇):实战分布式锁。