缓存一致性策略以及雪崩、穿透问题

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

 一. 缓存原理

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

[[260641]]

1. 实现步骤

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

 

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

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

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

  1. public void update(Entity entity) { 
  2. redisTemplate.delete(entity.getId()); 
  3. Dao.update(entity); 
  4. return entity; 
  5.  
  6. public Entity add(Entity entity) { 
  7. redisTemplate.delete(entity.getId()); 
  8. Dao.insert(entity); 
  9. return entity; 

2. 缓存更新策略

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

更新策略特点适用场景

实时更新同步更新保证强一致性,与业务强侵入强耦合金融转账业务等

弱实时异步更新(MQ/发布订阅/观察者模式),业务解耦,弱一致性存在延迟不适合写频繁场景

失效机制设置缓存失效,有一定延迟,可能存在雪崩适用读多写少,能接受一定的延时

任务调度通过定时任务进行全量更新统计类业务,访问频繁且定期更新

二. 缓存雪崩和击穿

1. 缓存雪崩概念

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

解决方案

将缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

用加锁或者队列的方式保证缓存的单线程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。

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

  1. public Result query(String id) { 
  2. // 1.从缓存中取数据 
  3. Result result = null
  4. result = (Result)redisTemplate.opsForValue().get(id); 
  5. if (result ! = null) { 
  6. logger.info("缓存中得到数据"); 
  7. return result; 
  8.  
  9. //2.加锁排队,阻塞式锁 
  10. doLock(id);//多少个id就可能有多少把锁 
  11. try{ 
  12. //一次只有一个线程 
  13. //双重校验,第一次获取到后面的都可以从缓存中直接击中 
  14. result = (Result)redisTemplate.opsForValue().get(id); 
  15. if (result != null) { 
  16. logger.info("缓存中得到数据"); 
  17. return result;//第二个线程,这里返回 
  18.  
  19. result = dao.query(id); 
  20. // 3.从数据库查询的结果不为空,则把数据放入缓存中,方便下次查询 
  21. if (null != result) { 
  22. redisTemplate.opsForValue().set(id,result); 
  23. redisTemplate.expire(id,20000, TimeUnit.MILLISECONDS); 
  24. return provinces; 
  25. } catch(Exception e) { 
  26. return null
  27. } finally { 
  28. //4.解锁 
  29. releaseLock(provinceid); 
  30.  
  31. private void releaseLock(String userCode) { 
  32. ReentrantLock oldLock = (ReentrantLock) locks.get(userCode); 
  33. if(oldLock !=null && oldLock.isHeldByCurrentThread()){ 
  34. oldLock.unlock(); 
  35.  
  36. private void doLock(String lockcode) { 
  37. //id有不同的值 
  38. //id相同的,加一个锁,不是同一个key,不能用同一个锁 
  39. ReentrantLock newLock = new ReentrantLock();//创建一个锁 
  40. //若已存在,则newLock直接丢弃 
  41. Lock oldLock = locks.putIfAbsent(lockcode, newLock); 
  42. if(oldLock == null){ 
  43. newLock.lock(); 
  44. }else
  45. oldLock.lock(); 
  46. ?? 

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

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数据装载入布隆过滤器中

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

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

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

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

责任编辑:武晓燕 来源: 零壹技术栈
相关推荐

2019-02-13 11:04:42

系统缓存软件

2022-12-14 08:23:30

2020-05-12 10:43:22

Redis缓存数据库

2020-06-01 22:09:48

缓存缓存同步缓存误用

2023-04-13 08:15:47

Redis缓存一致性

2024-04-11 13:45:14

Redis数据库缓存

2023-08-15 09:31:01

分布式缓存

2017-07-25 14:38:56

数据库一致性非锁定读一致性锁定读

2023-06-26 07:17:48

负载均衡策略Dubbo

2022-09-06 15:30:20

缓存一致性

2024-11-14 07:10:00

2021-06-11 09:21:58

缓存数据库Redis

2023-08-14 08:10:33

CPU缓存RFO

2021-02-02 12:40:50

哈希算法数据

2021-02-05 08:00:48

哈希算法​机器

2022-03-29 10:39:10

缓存数据库数据

2020-10-26 19:25:23

CPU缓存Cache

2024-01-15 10:38:20

多级缓存数据一致性分布式缓存

2024-10-28 12:41:25

2024-04-23 08:22:58

Meta缓存系统
点赞
收藏

51CTO技术栈公众号