一、前言

我们在用缓存的时候,不管是Redis或者Memcached,基本上会通用遇到以下三个问题:

  • 缓存穿透

  • 缓存并发

  • 缓存失效

缓存穿透

上面三个图会有什么问题呢?

我们在项目中使用缓存通常都是先检查缓存中是否存在,如果存在直接返回缓存内容,如果不存在就直接查询数据库然后再缓存查询结果返回。这个时候如果我们查询的某一个数据在缓存中一直不存在,就会造成每一次请求都查询DB,这样缓存就失去了意义,在流量大时,可能DB就挂掉了。 那这种问题有什么好办法解决呢?

要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。

有一个比较巧妙的作法是,可以将这个不存在的key预先设定一个值。

比如,"key" , “&&”。

在返回这个&&值的时候,我们的应用就可以认为这是不存在的key,那我们的应用就可以决定是否继续等待继续访问,还是放弃掉这次操作。如果继续等待访问,过一个时间轮询点后,再次请求这个key,如果取到的值不再是&&,则可以认为这时候key有值了,从而避免了透传到数据库,从而把大量的类似请求挡在了缓存之中。

缓存并发

有时候如果网站并发访问高,一个缓存如果失效,可能出现多个进程同时查询DB,同时设置缓存的情况,如果并发确实很大,这也可能造成DB压力过大,还有缓存频繁更新的问题。

我现在的想法是对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询。

这种情况和刚才说的预先设定值问题有些类似,只不过利用锁的方式,会造成部分请求等待。

缓存失效

引起这个问题的主要原因还是高并发的时候,平时我们设定一个缓存的过期时间时,可能有一些会设置1分钟啊,5分钟这些,并发很高时可能会出在某一个时间同时生成了很多的缓存,并且过期时间都一样,这个时候就可能引发一当过期时间到后,这些缓存同时失效,请求全部转发到DB,DB可能会压力过重。

那如何解决这些问题呢?

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

我们讨论的第二个问题时针对同一个缓存,第三个问题时针对很多缓存。

总结来看:

  1. 缓存穿透:查询一个必然不存在的数据。比如文章表,查询一个不存在的id,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成影响。

  2. 缓存失效:如果缓存集中在一段时间内失效,DB的压力凸显。这个没有完美解决办法,但可以分析用户行为,尽量让失效时间点均匀分布。

当发生大量的缓存穿透,例如对某个失效的缓存的大并发访问就造成了缓存雪崩。

问题汇总

问题1:

如何解决DB和缓存一致性问题?

答:当修改了数据库后,有没有及时修改缓存。这种问题,以前有过实践,修改数据库成功,而修改缓存失败的情况,最主要就是缓存服务器挂了。而因为网络问题引起的没有及时更新,可以通过重试机制来解决。而缓存服务器挂了,请求首先自然也就无法到达,从而直接访问到数据库。那么我们在修改数据库后,无法修改缓存,这时候可以将这条数据放到数据库中,同时启动一个异步任务定时去检测缓存服务器是否连接成功,一旦连接成功则从数据库中按顺序取出修改数据,依次进行缓存最新值的修改。

问题2:

问下缓存穿透那块!例如,一个用户查询文章,通过ID查询,按照之前说的,是将缓存的KEY预先设置一个值,,如果通过ID插过来,发现是预先设定的一个值,比如说是“&&”,那之后的继续等待访问是什么意思,这个ID什么时候会真正被附上用户所需要的值呢?

答:我刚说的主要是咱们常用的后面配置,前台获取的场景。前台无法获取相应的key,则等待,或者放弃。当在后台配置界面上配置了相关key和value之后,那么以前的key &&也自然会被替换掉。你说的那种情况,自然也应该会有一个进程会在某一个时刻,在缓存中设置这个ID,再有新的请求到达的时候,就会获取到最新的ID和value。

问题3:

其实用redis的话,那天看到一个不错的例子,双key,有一个当时生成的一个附属key来标识数据修改到期时间,然后快到的时候去重新加载数据,如果觉得key多可以把结束时间放到主key中,附属key起到锁的功能。

答:这种方案,之前我们实践过。这种方案会产生双份数据,而且需要同时控制附属key与key之间的关系,操作上有一定复杂度。

问题4:

多级缓存是什么概念呢?

答:多级缓存就像我今天之前给大家发的文章里面提到了,将ehcache与redis做二级缓存,就像我之前写的文章 http://www.jianshu.com/p/2cd6ad416a5a 提到过的。但同样会存在一致性问题,如果我们需要强一致性的话,缓存与数据库同步是会存在时间差的,所以我们在具体开发的过程中,一定要根据场景来具体分析,二级缓存更多的解决是,缓存穿透与程序的健壮性,当集中式缓存出现问题的时候,我们的应用能够继续运行。

说明:本文中提到的缓存可以理解为Redis。

二、缓存穿透与并发方案

上文中介绍了关于缓存穿透、并发的一些常用思路,但是没有明确一些思路的使用场景,下面继续深入探讨。相信不少朋友之前看过很多类似的文章,但是归根结底就是二个问题:

  • 如何解决穿透

  • 如何解决并发

当并发较高的时候,其实我是不建议使用缓存过期这个策略的,我更希望缓存一直存在,通过后台系统来更新缓存系统中的数据达到数据的一致性目的,有的朋友可能会质疑,如果缓存系统挂了怎么办,这样数据库更新了但是缓存没有更新,没有达到一致性的状态。

解决问题的思路是: 如果缓存是因为网络问题没有更新成功数据,那么建议重试几次,如果依然没有更新成功则认为缓存系统出错不可用,这时候客户端会将数据的KEY插入到消息系统中,消息系统可以过滤相同的KEY,只需保证消息系统不存在相同的KEY,当缓存系统恢复可用的时候,依次从mq中取出KEY值然后从数据库中读取最新的数据更新缓存。注意:更新缓存之前,缓存中依然有旧数据,所以不会造成缓存穿透。

下图展示了整个思路的过程:

看完上面的方案以后,又会有不少朋友提出疑问,如果我是第一次使用缓存或者缓存中暂时没有我需要的数据,那又该如何处理呢?

解决问题的思路: 在这种场景下,客户端从缓存中根据KEY读取数据,如果读到了数据则流程结束,如果没有读到数据(可能会有多个并发都没有读到数据),这时候使用缓存系统中的setNX方法设置一个值(这种方法类似加个锁),没有设置成功的请求则sleep一段时间,设置成功的请求读取数据库获取值,如果获取到则更新缓存,流程结束,之前sleep的请求这时候唤醒后直接再从缓存中读取数据,此时流程结束。

在看完这个流程后,我想这里面会有一个漏洞,如果数据库中没有我们需要的数据该怎么处理,如果不处理则请求会造成死循环,不断的在缓存和数据库中查询,这时候我们会沿用我之前文章中的如果没有读到数据则往缓存中插入一个NULL字符串的思路,这样其他请求直接就可以根据“NULL”进行处理,直到后台系统在数据库成功插入数据后同步更新清理NULL数据和更新缓存。

流程图如下所示:

总结:在实际工作中,我们往往将上面二个方案组合使用才能达到最佳效果,虽然第二种方案也会造成请求阻塞,但是只是在第一次使用或者缓存暂时没有数据的情况下才会产生,在生产中经过检验在TPS没有上万的情况下是不会造成问题的。

三、热点缓存解决方案

1、缓存使用背景:

我们拿用户中心的一个案例来说明: 每个用户都会首先获取自己的用户信息,然后再进行其他相关的操作,有可能会有如下一些场景情况:

  • 会有大量相同用户重复访问该项目。

  • 会有同一用户频繁访问同一模块。

2、思路解析

  • 因为用户本身是不固定的而且用户数量也有几百万尤其上千万,我们不可能把所有的用户信息全部缓存起来,通过第一个场景情况可以看到一些规律,那就是有大量的相同用户重复访问,但是究竟是哪些用户重复访问我们也并不知道。

  • 如果有一个用户频繁刷新读取项目,那么对数据库本身也会造成较大压力,当然我们也会有相关的保护机制来确实恶意攻击,可以从前端控制,也可以有采黑名单等机制,这里不在赘述。如果用缓存的话,我们又该如何控制同一用户繁重读取用户信息呢。

请看下图:

我们会通过缓存系统做一个排序队列,比如1000个用户,系统会根据用户的访问时间更新用户信息的时间,越是最近访问的用户排名越排前,系统会定期过滤掉排名最后的200个用户,然后再从数据库中随机取出200个用户加入队列,这样请求每次到达的时候,会先从队列中获取用户信息,如果命中则根据userId,再从另一个缓存数据结构中读取用户信息,如果没有命中则说明该用户请求频率不高。

JAVA伪代码如下所示:

  1. for (int i = 0; i < times; i++) {

  2.     user = new ExternalUser();

  3.     user.setId(i+"");

  4.     user.setUpdateTime(new Date(System.currentTimeMillis()));

  5.     CacheUtil.zadd(sortKey, user.getUpdateTime().getTime(), user.getId());

  6.     CacheUtil.putAndThrowError(userKey+user.getId(), JSON.toJSONString(user));

  7. }

  8. Set<String> userSet = CacheUtil.zrange(sortKey, 0, -1);

  9. System.out.println("[sortedSet] - " + JSON.toJSONString(userSet) );

  10. if(userSet == null || userSet.size() == 0)

  11.     return;

  12. Set<Tuple> userSetS = CacheUtil.zrangeWithScores(sortKey, 0, -1);

  13. StringBuffer sb = new StringBuffer();

  14. for(Tuple t:userSetS){

  15.     sb.append("{member: ").append(t.getElement()).append(", score: ").append(t.getScore()).append("}, ");

  16. }

  17. System.out.println("[sortedcollect] - " + sb.toString().substring(0, sb.length() - 2));

  18. Set<String> members = new HashSet<String>();

  19. for(String uid:userSet){

  20.     String key = userKey + uid;

  21.     members.add(uid);

  22.     ExternalUser user2 = CacheUtil.getObject(key, ExternalUser.class);

  23.     System.out.println("[user] - " + JSON.toJSONString(user2) );

  24. }

  25. System.out.println("[user] - "  + System.currentTimeMillis());

  26. String[] keys = new String[members.size()];

  27. members.toArray(keys);

  28. Long rem = CacheUtil.zrem(sortKey, keys);

  29. System.out.println("[rem] - " + rem);

  30. userSet = CacheUtil.zrange(sortKey, 0, -1);

  31. System.out.println("[remove - sortedSet] - " + JSON.toJSONString(userSet));

作者:小程故事多 原文:https://www.jianshu.com/p/d96906140199

新书推荐:《深入分布式缓存:从原理到实践》

推荐阅读

那些有趣又实用的开源人工智能项目 Top 10

自学编程需要注意什么?

Spring干货汇总(含Spring Boot与Spring Cloud)

IntelliJ IDEA插件系列:五大装逼神器

我最常用的Intellij IDEA快捷键

最好用的 IntelliJ 插件 Top 10

点击 “阅读原文” 看看本号其他精彩内容

缓存穿透、缓存并发、热点缓存之最佳招式相关推荐

  1. redis有值查询返回null_Redis缓存穿透、缓存并发、热点缓存之最佳招式

    一.前言 我们在用缓存的时候,不管是Redis或者Memcached,基本上会通用遇到以下三个问题: 缓存穿透 缓存并发 缓存失效 缓存穿透 注: 上面三个图会有什么问题呢? 我们在项目中使用缓存通常 ...

  2. guava 缓存查询_阿里Java二面难点:Redis缓存穿透、击穿、缓存雪崩方案

    一.缓存穿透 1. 什么是缓存穿透? 为了缓解持久层数据库的压力,在服务器和存储层之间添加了一层缓存: 一个简单的正常请求:当客户端发起请求时,服务器响应处理,会先从redis缓存层查询客户端需要的请 ...

  3. Redis 缓存穿透、雪崩、缓存数据库不一致、持久化方式、分布式锁、过期策略

    1. Redis 缓存穿透 1.1 Redis 缓存穿透概念 访问了不存在的 key,缓存未命中,请求会穿透到 DB,量大时可能会对 DB 造成压力导致服务异常. 由于不恰当的业务功能实现,或者外部恶 ...

  4. 最佳实践 缓存穿透,瞬间并发,缓存雪崩的解决方法

    一.缓存穿透: 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透. 解决办法:     ...

  5. 缓存穿透,瞬间并发,缓存雪崩的解决方法

    一.缓存穿透: 缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透. 解决办法: 1.布 ...

  6. redis mysql 雪崩_Redis缓存雪崩、缓存穿透、并发等5大难题,你有没有解决方案

    缓存雪崩 数据未加载到高速缓存中,或者高速缓存同时在较大区域中失效,这将导致所有请求都去查找数据库,从而导致数据库CPU和内存负载过高,甚至会出现宕机. 比如雪崩的一个简单过程: 1.redis集群大 ...

  7. 缓存穿透、并发和失效的解决方案

    我们在用缓存的时候,不管是Redis或者Memcached,基本上会通用遇到以下三个问题: 缓存穿透 缓存并发 缓存失效 缓存穿透 注:上面三个图会有什么问题呢? 我们在项目中使用缓存通常都是先检查缓 ...

  8. 缓存穿透、并发和雪崩那些事

    作者:李艳鹏,阿里资深技术专家!著有<可伸缩服务架构>,<分布式服务架构>等作品,在区块链,聚合支付,电商等领域有一定的积累! 0 题记 缓存穿透.缓存并发和缓存雪崩是常见的由 ...

  9. 高并发热点缓存数据可能出现问题及解决方案

    背景 电商场景促销活动的会场页由于经常集中在某个时间点进行"秒杀"促销,这些页面的QPS(服务器每秒可以处理的请求量)往往特别高,数据库通常无法直接支撑如此高QPS的请求,常见的解 ...

最新文章

  1. 字节输出流写多个字节的方法
  2. 在IOS XR上配置BFD
  3. 一个html多个html文件怎么打开网页,Firefox如何单窗口多页面浏览
  4. jar包直接拷贝到WEB-INF/lib下和以userLibrary引入的区别
  5. Tomcat系列(6)——Tomcat处理一个HTTP请求的过程
  6. 气缸标识上vr什么意思_汽车VR传感器是什么意思?
  7. 使用 laravel Passport 做 API 认证
  8. python项目小案例:一个网页响应超时提醒小闹钟
  9. 对口升学计算机网络网络试题及答案,2011-2015计算机对口升学网络试题汇总
  10. python 大智慧自定义数据_大智慧扩展数据、自定义数据.doc
  11. 10的n次方换算关系 10^N 计算机存储单位的换算关系
  12. 使用iconv进行GBK到BIG5编码转/简繁转换遇到的问题
  13. 给移动互联网创业公司的六条建议
  14. 【JavaScript】新浪微博如何快速批量取消关注?
  15. Java+Jsp+MySQL高校选课系统设计与实现(附源码下载地址)
  16. 互联网无处不在的“推荐算法”解析
  17. 解决NVIDIA GeForce系列显卡NVENC并发Session数目限制问题
  18. Android 加固应用
  19. violate关键字---java高并发
  20. nba2k 服务器支持,NBA2K Online篮球在线官方网站-拼出你的传奇-腾讯游戏

热门文章

  1. linux 图形用户界面 x window system 简介
  2. python3 去除列表中重复的元素 保存原来顺序
  3. python3 AES 加解密
  4. 复制windows对话框内容的方法
  5. 构建Hadoop伪分布式环境
  6. linux c 之sigsuspend 进程阻塞
  7. matlab的边缘检测方法,MATLAB应用—Matlab多种图像边缘检测方法
  8. c语言太极图编程语言,利用C语言的Cairo图形库绘制太极图实例教程.pdf
  9. 问题集锦(1-10)
  10. python客户端和服务端实验_python实现socket客户端和服务端简单示例