www.129028.com金沙缓存一致性策略以及雪崩、穿透问题

日期:2020-05-07编辑作者:Web前端

为了表述方便,本文以数据库查询缓存为例,使用缓存可以减小对数据库的压力。

一、缓存原理

高并发情境下首先考虑到的第一层优化方案就是增加缓存,尤其是通过Redis将原本在数据库中的数据复制一份放到内存中,可以减少对数据库的读操作,数据库的压力降低,同时也会加快系统的响应速度,但是同样的也会带来其他的问题,比如需要考虑数据的一致性、还需要预防可能的缓存击穿、穿透和雪崩问题等等。

先查询缓存中有没有要的数据,如果有,就直接返回缓存中的数据。如果缓存中没有要的数据,才去查询数据库,将得到数据更新到缓存再返回,如果数据库中也没有就可以返回空。

www.129028.com金沙 1缓存实现步骤

考虑数据一致性,缓存处的代码逻辑都较为标准化,首先取Redis,击中则返回,未击中则通过数据库来进行查询和同步。

 public Result query(String id) { Result result = null; //1.从Redis缓存中取数据 result = redisTemplate.opsForValue; if (null != result){ System.out.println("缓存中得到数据"); return result; } //2.通过DB查询,有则同步更新redis,否则返回空 System.out.println("数据库中得到数据"); result = Dao.query; if (null != result){ redisTemplate.opsForValue().set(id,result); redisTemplate.expire(id,20000, TimeUnit.MILLISECONDS); } return result; }

其他的新增、删除和更新操作,可以直接采用先清空该Key下的缓存值再进行DB操作,这样逻辑清晰简单,维护的复杂度会降低,而付出代价就是多查询一次。

 public void update(Entity entity) { redisTemplate.delete(entity.getId; Dao.update; return entity; } public Entity add(Entity entity) { redisTemplate.delete(entity.getId; Dao.insert; return entity; }

适用于做缓存的场景一般都是:访问频繁、读场景较多而写场景少、对数据一致性要求不高。如果上面三个条件都不符合,那维护一套缓存数据的意义并不大了,实际应用中通常都需要针对业务场景来选择合适的缓存方案,下面给出了四种缓存策略,由上到下就是按照一致性由强到弱的顺序。

更新策略 特点 适用场景
实时更新 同步更新保证强一致性,与业务强侵入强耦合 金融转账业务等
弱实时 异步更新(MQ/发布订阅/观察者模式),业务解耦,弱一致性存在延迟 不适合写频繁场景
失效机制 设置缓存失效,有一定延迟,可能存在雪崩 适用读多写少,能接受一定的延时
任务调度 通过定时任务进行全量更新 统计类业务,访问频繁且定期更新

关于缓存MQ异步更新策略,可参考:《实现缓存最终一致性的两种方案》

在使用数据库缓存的时候,读和写的流程往往是这样的:

二、缓存雪崩和击穿

缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

    1. 将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
    1. 用加锁或者队列的方式保证缓存的单线程写,从而避免失效时大量的并发请求落到底层存储系统上。

第一种方案比较容易实现,第二种的思路主要是从加阻塞式的排它锁来实现,在缓存查询不到的情况下,每此只允许一个线程去查询DB,这样可避免同一个ID的大量并发请求都落到数据库中。

 public Result query(String id) { // 1.从缓存中取数据 Result result = null; result = redisTemplate.opsForValue; if (result ! = null) { logger.info("缓存中得到数据"); return result; } //2.加锁排队,阻塞式锁 doLock;//多少个id就可能有多少把锁 try{ //一次只有一个线程 //双重校验,第一次获取到后面的都可以从缓存中直接击中 result = redisTemplate.opsForValue; if (result != null) { logger.info("缓存中得到数据"); return result;//第二个线程,这里返回 } result = dao.query; // 3.从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询 if (null != result){ redisTemplate.opsForValue().set(id,result); redisTemplate.expire(id,20000, TimeUnit.MILLISECONDS); } return provinces; }catch(Exception e){ return null; }finally{ //4.解锁 releaseLock(provinceid); } } private void releaseLock(String userCode) { ReentrantLock oldLock = (ReentrantLock) locks.get; if(oldLock !=null && oldLock.isHeldByCurrentThread{ oldLock.unlock(); } } private void doLock(String lockcode) { //id有不同的值 //id相同的,加一个锁,不是同一个key,不能用同一个锁 ReentrantLock newLock = new ReentrantLock();//创建一个锁 //若已存在,则newLock直接丢弃 Lock oldLock = locks.putIfAbsent(lockcode, newLock); if(oldLock == null){ newLock.lock(); }else{ oldLock.lock(); } }

注意:加锁排队的解决方式在处理分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验很差!因此,在真正的高并发场景下很少使用!

一个存在的key,在缓存过期的一刻,同时有大量的请求,这些请求都会击穿到DB,造成瞬时DB请求量大、压力骤增。

在访问key之前,采用SETNX(set if not exists)来设置另一个短期key来锁住当前key的访问,访问结束再删除该短期key。

缓存击穿指的是,一个key的访问量非常大,比如某秒杀活动,有1w/s的并发量。这个key在某一时刻过期,那这些大量的请求就会一瞬间到数据库,数据库可能会直接崩溃。

三、缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。

布隆过滤器的使用方法,类似java的SET集合,用来判断某个元素是否在某个集合中。和一般的hash set不同的是,这个算法无需存储key的值,对于每个key,只需要k个比特位,每个存储一个标志,用来判断key是否在集合中。

www.129028.com金沙,使用步骤:1、将List数据装载入布隆过滤器中

 private BloomFilter<String> bf =null; //PostConstruct注解对象创建后,自动调用本方法 @PostConstruct public void init(){ //在bean初始化完成后,实例化bloomFilter,并加载数据 List<Entity> entities= initList(); //初始化布隆过滤器 bf = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), entities.size; for (Entity entity : entities) { bf.put(entity.getId; } }

2、访问经过布隆过滤器,存在才可以往db中查询

 public Provinces query(String id) { //先判断布隆过滤器中是否存在该值,值存在才允许访问缓存和数据库 if(!bf.mightContain{ Log.info("非法访问"+System.currentTimeMillis; return null; } Log.info("数据库中得到数据"+System.currentTimeMillis; Entity entity= super.query; return entity; }

这样当外界有恶意攻击时,不存在的数据请求就可以直接拦截在过滤器层,而不会影响到底层数据库系统。

source:《缓存专题》、《缓存问题及解决方案分析》

对请求参数做校验,比如用户鉴权校验,id做基础校验,id = 0的直接拦截。如果查询到数据库没有值,也将对应的key存进缓存中,value为null。这样下次查询就直接从缓存返回了。但这里的key的缓存时间应该比较短,比如30s。防止后面在数据库插入了这条数据,而用户获取不到。使用布隆过滤器,判断一个key是否已经查过了,如果已经查过了,就不去数据库查询。缓存击穿

还有一种解决方案是使用对一个数据使用一个队列,使读写操作串行化。比如对id为n的数据建立一个队列。对这条数据的写操作,删除缓存后,放进一个队列;然后另一个线程过来了,发现没有缓存,则把这个读操作也放进这个队列里面。

缓存雪崩指的是,在某一时刻,多个key失效。这样就会有大量的请求从缓存中获取不到值,全部到数据库。还有另一种情况,就是缓存服务器宕机,也算做缓存雪崩。

我们在使用缓存时,往往先尝试去缓存中取值,如果没有,再去数据库取值,如果数据库也没有值,则根据业务需求,返回空或者抛异常。

原文

缓存击穿的解决方案也有几种,可以配合使用:

以下几种解决方案:

解决方案

时间: 2019-09-07阅读: 122标签: 缓存

缓存穿透

对于热点数据,慎重考虑过期时间,确保热点期间key不会过期,甚至有些可以设置永不过期。使用互斥锁(比如Java的多线程锁机制),第一个线程访问key的时候就锁住,等查询数据库返回后,把值插入到缓存后再释放锁,这样后面的请求就可以直接取缓存里面的数据了。缓存雪崩

本文由www.129028.com金沙发布于Web前端,转载请注明出处:www.129028.com金沙缓存一致性策略以及雪崩、穿透问题

关键词:

深入理解定时器系列第二篇——被誉为神器的requestAnimationFrame

时间: 2019-09-09阅读: 127标签: 动画 前面的话 与 setTimeout 和 setInterval 不同,requestAnimationFrame不需要设置时间间隔。这有...

详细>>

深入学习jQuery样式操作【www.129028.com金沙】,深入学习jquery样式

时间: 2019-09-07阅读: 108标签: 函数 内容目录 深入学习jQuery样式操作,深入学习jquery样式 × toggleClass()函数用于切换当前...

详细>>

【www.129028.com金沙】说说这碗青春饭

时间: 2019-09-08阅读: 311标签: 程序员 前不久,又听到有人说程序员这一行是碗青春饭,言语之中充满了鄙夷。当然他并...

详细>>

Js如何获取某一天所在的星期www.129028.com金沙?

时间: 2019-09-10阅读: 159标签: 日期 我们会遇到的需求的是,获取今天或者某一天所在星期的开始和结束日期。 我们这...

详细>>