一. 缓存原理java
高并发情境下首先考虑到的第一层优化方案就是增长缓存,尤为是经过Redis将本来在数据库中的数据复制一份放到内存中,能够减小对数据库的读操做,数据库的压力下降,同时也会加快系统的响应速度,可是一样的也会带来其余的问题,好比须要考虑数据的一致性、还须要预防可能的缓存击穿、穿透和雪崩问题等等。redis
1. 实现步骤算法
先查询缓存中有没有要的数据,若是有,就直接返回缓存中的数据。若是缓存中没有要的数据,才去查询数据库,将获得数据更新到缓存再返回,若是数据库中也没有就能够返回空。数据库
考虑数据一致性,缓存处的代码逻辑都较为标准化,首先取Redis,击中则返回,未击中则经过数据库来进行查询和同步。缓存
public Result query(String id) { Result result = null; //1.从Redis缓存中取数据 result = (Result)redisTemplate.opsForValue().get(id); if (null != result) { System.out.println("缓存中获得数据"); return result; } //2.经过DB查询,有则同步更新redis,不然返回空 System.out.println("数据库中获得数据"); result = Dao.query(id); 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(entity); return entity; } public Entity add(Entity entity) { redisTemplate.delete(entity.getId()); Dao.insert(entity); return entity; }
2. 缓存更新策略异步
适用于作缓存的场景通常都是:访问频繁、读场景较多而写场景少、对数据一致性要求不高。若是上面三个条件都不符合,那维护一套缓存数据的意义并不大了,实际应用中一般都须要针对业务场景来选择合适的缓存方案,下面给出了四种缓存策略,由上到下就是按照一致性由强到弱的顺序。分布式
更新策略特色适用场景高并发
实时更新同步更新保证强一致性,与业务强侵入强耦合金融转帐业务等优化
弱实时异步更新(MQ/发布订阅/观察者模式),业务解耦,弱一致性存在延迟不适合写频繁场景
失效机制设置缓存失效,有必定延迟,可能存在雪崩适用读多写少,能接受必定的延时
任务调度经过定时任务进行全量更新统计类业务,访问频繁且按期更新
二. 缓存雪崩和击穿
1. 缓存雪崩概念
缓存雪崩是指在咱们设置缓存时采用了相同的过时时间,致使缓存在某一时刻同时失效,请求所有转发到DB,DB瞬时压力太重雪崩。和缓存击穿不一样的是,缓存击穿指并发查同一条数据,缓存雪崩是不一样数据都过时了,不少数据都查不到从而查数据库。
解决方案
将缓存失效时间分散开,好比咱们能够在原有的失效时间基础上增长一个随机值,好比1-5分钟随机,这样每个缓存的过时时间的重复率就会下降,就很难引起集体失效的事件。
用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。
第一种方案比较容易实现,第二种的思路主要是从加阻塞式的排它锁来实现,在缓存查询不到的状况下,每此只容许一个线程去查询DB,这样可避免同一个ID的大量并发请求都落到数据库中。
public Result query(String id) { // 1.从缓存中取数据 Result result = null; result = (Result)redisTemplate.opsForValue().get(id); if (result ! = null) { logger.info("缓存中获得数据"); return result; } //2.加锁排队,阻塞式锁 doLock(id);//多少个id就可能有多少把锁 try { //一次只有一个线程 //双重校验,第一次获取到后面的均可以从缓存中直接击中 result = (Result)redisTemplate.opsForValue().get(id); if (result != null) { logger.info("缓存中获得数据"); return result;//第二个线程,这里返回 } result = dao.query(id); // 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(userCode); 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(); } }
注意:加锁排队的解决方式在处理分布式环境的并发问题,有可能还要解决分布式锁的问题;线程还会被阻塞,用户体验不好!所以,在真正的高并发场景下不多使用!
2. 缓存击穿概念
一个存在的key,在缓存过时的一刻,同时有大量的请求,这些请求都会击穿到DB,形成瞬时DB请求量大、压力骤增。
解决方案
在访问key以前,采用SETNX(set if not exists)来设置另外一个短时间key来锁住当前key的访问,访问结束再删除该短时间key。
三. 缓存穿透
1. 缓存穿透概念
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户极可能是攻击者,攻击会致使数据库压力过大。
解决方案:布隆过滤器
布隆过滤器的使用方法,相似java的SET集合,用来判断某个元素(key)是否在某个集合中。和通常的hash set不一样的是,这个算法无需存储key的值,对于每一个key,只须要k个比特位,每一个存储一个标志,用来判断key是否在集合中。
使用步骤:
将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()); } }
访问通过布隆过滤器,存在才能够往db中查询
public Provinces query(String id) { //先判断布隆过滤器中是否存在该值,值存在才容许访问缓存和数据库 if(!bf.mightContain(id)) { Log.info("非法访问"+System.currentTimeMillis()); return null; } Log.info("数据库中获得数据"+System.currentTimeMillis()); Entity entity= super.query(id); return entity; }
这样当外界有恶意攻击时,不存在的数据请求就能够直接拦截在过滤器层,而不会影响到底层数据库系统。