通常咱们对缓存读操做的时候有这么一个固定的套路:html
代码例子:java
1 @Override 2 public R selectOrderById(Integer id) { 3 //查询缓存 4 Object redisObj = valueOperations.get(String.valueOf(id)); 5 6 //命中缓存 7 if(redisObj != null) { 8 //正常返回数据 9 return new R().setCode(200).setData(redisObj).setMsg("OK"); 10 } 11 Order order = orderMapper.selectOrderById(id); 12 if (order != null) { 13 valueOperations.set(String.valueOf(id), order); //加入缓存 14 return new R().setCode(200).setData(order).setMsg("OK"); 15 } 16 return new R().setCode(500).setData(new NullValueResultDO()).setMsg("查询无果"); 17 }
但这样写的代码是不行的,这代码里就有咱们缓存的三大问题的两大问题.穿透,击穿.git
第一种状况:Redis挂掉了,请求所有走数据库.github
第二种状况:缓存数据设置的过时时间是相同的,而后恰好这些数据删除了,所有失效了,这个时候所有请求会到数据库redis
缓存雪崩若是发生了,颇有可能会把咱们的数据库搞垮,致使整个服务器瘫痪.算法
对于第二种状况,很是好解决:sql
在存缓存的时候给过时时间加上一个随机值,这样大幅度的减小缓存同时过时.shell
第一种状况:数据库
事发前:实现Redis的高可用(主从架构+Sentinel 或者Redis Cluster),尽可能避免Redis挂掉这种状况发生。
事发中:万一Redis真的挂了,咱们能够设置本地缓存(ehcache)+限流(hystrix),尽可能避免咱们的数据库被干掉(起码能保证咱们的服务仍是能正常工做的)
事发后:redis持久化,重启后自动从磁盘上加载数据,快速恢复缓存数据。设计模式
好比你抢了你同事的女神,你同事很气,想搞你,在你的项目里,每次请求的ID为负数.这个时候缓存确定是没有的,缓存就没用了,请求就会所有找数据库,但数据库也没用这个值.因此每次返回空出去.
缓存穿透是指查询一个必定不存在的数据。因为缓存不命中,而且出于容错考虑,若是从数据库查不到数据则不写入缓存,这将致使这个不存在的数据每次请求都要到数据库去查询,失去了缓存的意义。
这就是缓存穿透:
请求的数据在缓存大量不命中,致使请求走数据库。
缓存穿透若是发生了,也可能把咱们的数据库搞垮,致使整个服务瘫痪!
解决缓存穿透也有两种方案:
缓存空对象代码例子:
1 public R selectOrderById(Integer id) { 2 return cacheTemplate.redisFindCache(String.valueOf(id), 10, TimeUnit.MINUTES, new CacheLoadble<Order>() { 3 @Override 4 public Order load() { 5 return orderMapper.selectOrderById(id); 6 } 7 },false); 8 }
1 public R redisFindCache(String key, long expire, TimeUnit unit, CacheLoadble<T> cacheLoadble, boolean b) { 2 //查询缓存 3 Object redisObj = valueOperations.get(String.valueOf(key)); 4 //命中缓存 5 if (redisObj != null) { 6 if(redisObj instanceof NullValueResultDO){ 7 return new R().setCode(500).setData(new NullValueResultDO()).setMsg("查询无果"); 8 } 9 //正常返回数据 10 return new R().setCode(200).setData(redisObj).setMsg("OK"); 11 } 12 try { 13 T load = cacheLoadble.load();//查询数据库 14 if (load != null) { 15 valueOperations.set(key, load, expire, unit); //加入缓存 16 return new R().setCode(200).setData(load).setMsg("OK"); 17 }else{ 18 valueOperations.set(key,new NullValueResultDO(),expire,unit); 19 } 20 21 } finally { 22 23 } 24 return new R().setCode(500).setData(new NullValueResultDO()).setMsg("查询无果"); 25 }
这里封装了一个模板redisFindCache,否则每个方法都要写这个流程.注意在命中缓存时,要判断数据是不是空对象.
空对象:
1 @Getter 2 @Setter 3 @ToString 4 public class NullValueResultDO{ 5 6 }
缓存空对象的缺点:有大量的空数据占用redis的内存.治标不治本.
布隆过滤器:
有谷歌的guava,可是是单机版的,不支持分布式.
也能够用redis的位数组bit手写一个分布式布隆过滤器,代码就不写了.过程就是先把id(好比你是用id为key的)存进布隆过滤器(会通过特定的算法),当咱们请求接口的时候先让它查询布隆过滤器,判断数据是否存在.
上面的代码还有个缓存击穿(缓存当中没有,数据库中有)问题,就是并发的时候.好比99我的同时请求,仍是会打印99条sql语句,仍是会找数据库.
这里的代码是用的分布式锁(互斥锁)
1 public R redisFindCache(String key, long expire, TimeUnit unit, CacheLoadble<T> cacheLoadble,boolean b){ 2 //判断是否走过滤器 3 if(b){ 4 //先走过滤器 5 boolean bloomExist = bloomFilter.isExist(String.valueOf(key)); 6 if(!bloomExist){ 7 return new R().setCode(600).setData(null).setMsg("查询无果"); 8 } 9 } 10 //查询缓存 11 Object redisObj = valueOperations.get(String.valueOf(key)); 12 //命中缓存 13 if(redisObj != null) { 14 //正常返回数据 15 return new R().setCode(200).setData(redisObj).setMsg("OK"); 16 } 17 // RLock lock0 = redisson.getLock("{taibai0}:" + key); 18 // RLock lock1 = redisson.getLock("{taibai1}:" + key); 19 // RLock lock2 = redisson.getLock("{taibai2}:" + key); 20 // RedissonMultiLock lock = new RedissonMultiLock(lock0,lock1, lock2); 21 try { 22 redisLock.lock(key);//上锁 23 // lock.lock(); 24 //查询缓存 25 redisObj = valueOperations.get(String.valueOf(key)); 26 //命中缓存 27 if(redisObj != null) { 28 //正常返回数据 29 return new R().setCode(200).setData(redisObj).setMsg("OK"); 30 } 31 T load = cacheLoadble.load();//查询数据库 32 if (load != null) { 33 valueOperations.set(key, load,expire, unit); //加入缓存 34 return new R().setCode(200).setData(load).setMsg("OK"); 35 } 36 return new R().setCode(500).setData(new NullValueResultDO()).setMsg("查询无果"); 37 }finally { 38 redisLock.unlock(key);//解锁 39 // lock.unlock(); 40 } 41 }
若是仅仅查询的话,缓存的数据和数据库的数据是没问题的。可是,当咱们要更新时候呢?各类状况极可能就形成数据库和缓存的数据不一致了。
从理论上说,只要咱们设置了键的过时时间,咱们就能保证缓存和数据库的数据最终是一致的。由于只要缓存数据过时了,就会被删除。随后读的时候,由于缓存里没有,就能够查数据库的数据,而后将数据库查出来的数据写入到缓存中。
除了设置过时时间,咱们还须要作更多的措施来尽可能避免数据库与缓存处于不一致的状况发生。
通常来讲,执行更新操做时,咱们会有两种选择:
首先,要明确的是,不管咱们选择哪一个,咱们都但愿这两个操做要么同时成功,要么同时失败。因此,这会演变成一个分布式事务的问题。
因此,若是原子性被破坏了,可能会有如下的状况:
若是第一步已经失败了,咱们直接返回Exception出去就行了,第二步根本不会执行。
下面咱们具体来分析一下吧。
操做缓存也有两种方案:
通常咱们都是采起删除缓存缓存策略的,缘由以下:
基于这两点,对于缓存在更新时而言,都是建议执行删除操做!
正常状况是这样的:
若是原子性被破坏了:
若是在高并发的场景下,出现数据库与缓存数据不一致的几率特别低,也不是没有:
要达成上述状况,仍是说一句几率特别低:
由于这个条件须要发生在读缓存时缓存失效,并且并发着有一个写操做。而实际上数据库的写操做会比读操做慢得多,并且还要锁表,而读操做必需在写操做前进入数据库操做,而又要晚于写操做更新缓存,全部的这些条件都具有的几率基本并不大。
对于这种策略,实际上是一种设计模式:Cache Aside Pattern
删除缓存失败的解决思路:
正常状况是这样的:
若是原子性被破坏了:
看起来是很美好,可是咱们在并发场景下分析一下,就知道仍是有问题的了:
因此也会致使数据库和缓存不一致的问题。
并发下解决数据库与缓存不一致的思路:
咱们能够发现,两种策略各自有优缺点:
在高并发下表现不如意,在原子性被破坏时表现优异
在高并发下表现优异,在原子性被破坏时表现不如意
能够用databus或者阿里的canal监听binlog进行更新。
参考资料:
https://coolshell.cn/articles/17416.html
https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-consistence.md
https://zhuanlan.zhihu.com/p/48334686
https://blog.csdn.net/z50l2o08e2u4aftor9a/article/details/81008933