没缓存的日子:
对于web来说,是用户量和访问量支持项目技术的更迭和前进。随着服务用户提升。可能会出现一下的一些状况:
- 页面并发量和访问量并不多,mysql
足以支撑
自己逻辑业务的发展。那么其实可以不加缓存。最多对静态页面进行缓存即可。 - 页面的并发量显著增多,数据库有些压力,并且有些数据更新频率较低
反复被查询
或者查询速度较慢
。那么就可以考虑使用缓存技术优化。对高命中的对象存到key-value形式的redis中,那么,如果数据被命中,那么可以省经效率很低的db。从高效的redis中查找到数据。 - 当然,可能还会遇到其他问题,你可以需要静态页面本地缓存,cdn加速,甚至负载均衡这些方法提高系统并发量。这里就不做介绍。
缓存思想无处不在
我们从一个算法问题开始了解缓存的意义。
问题1:
- 输入一个数n(n<20),求
n!
;
分析1:
- 单单考虑算法,不考虑数值越界问题。
当然我们知道n!=n * (n-1) * (n-2) * ... * 1= n * (n-1)!
;
那么我们可以用一个递归函数解决问题。
static long jiecheng(int n)
{
if(n==1||n==0)return 1;
else {
return n*jiecheng(n-1);
}
}
这样每输入求一次需要执行n
次。
问题2:
- 输入t组数据(可能成百上千),每组一个x(n<20),求
x!
;
分析2:
- 如果使用
递归
,输入t组数据,每个位x,那么每次都要执行
$sum_{i=0}^t$Xi
当Xi过大或者n过大都会造成不小的负担!时间复杂度
为O(n2) - 那么能否换个思想的。没错、是
打表
(也可以理解位动态规划
)。打表常用于ACM算法中,常用于解决多组输入输出、图论搜索结果、路径储存问题。那么,对于这个求阶乘。我们只需要申请一个数组。每个数据为前一个数据
*当前index
。那么思想很明确啦!
import java.util.Scanner;
public class test3 {
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc=new Scanner(System.in);
int t=sc.nextInt();
long jiecheng[]=new long[21];
jiecheng[0]=1;
for(int i=1;i<21;i++)
{
jiecheng[i]=jiecheng[i-1]*i;
}
for(int i=0;i<t;i++) {
int x=sc.nextInt();
System.out.println(jiecheng[x]);
}
}
}
- 时间复杂度才O(n)。这里的思想就和
缓存
思想差不多。先将数据在jiecheng[21]数组中储存。执行一次计算。当后面继续访问的时候就相当于当问静态数组值。为O(1)。就能大大的减少查询、执行成本啦!
缓存的应用场景
- 缓存适用于高并发的场景,提升服务容量。主要是将从
经常被访问的数据
或者查询成本较高
从慢的介质中存到比较快的介质中,比如从硬盘
—>内存
。我们知道大多数关系数据库是基于硬盘读写
的,其效率和资源有限,而redis等非关系型就是基于内存存储。其效率差别很大。当然,缓存也分为本地缓存和服务端缓存,这里只讲redis的服务端缓存。 - 举个例子。例如如果一个接口sql查询
需要2s
。你每次查询都会2s并且加载的时候都会等在,这个长期等待给用户的体验是非常糟糕
的。而用户能够接受的往往是第一次
的等待。如果你用了缓存技术。你第一次查询放到redis里面。然后数据再从redis返回给你。后面当你继续访问这个数据的时候。查询到redis中有备份,那么不需要通过db
直接能从redis
中获取数据。那么,你想想,从一个key value的Nosql中取一个value能要多久呢! - 所以对于像样的,有点规模的网站,缓存is
necessary
的.redis也是必不可少的。并且服务端的缓存设计也是要根据业务有所区别的。也要防止占用内存过大,redis雪崩等问题。
需要注意的问题
- 缓存使用不当会带来很多问题。所以需要对一些细节进行认真考量和设计。笔者对于分布式的经验并不是很丰富,就相对于笔者的眼中谈谈缓存设计不好会带来那些问题。
是否用缓存
- 现在不少项目,为了缓存而缓存,然而缓存并不是适合所有场景,比如如果对
数据一致性
要求极高,又或者数据频繁
更改而查询并不多。有的可以不需要缓存。因为如果使用redis缓存多多少少可能会遇到数据一致性问题。那你可以考虑使用redis做成分布式锁去锁sql的数据。同样如果频繁更新数据,那么redis能起到的作用就仅仅是多了一层中转站。反而浪费资源
。使得传输过程臃肿。
过期策略选择
- 大部分场景
不适合缓存一致存在
,首先,你的sql数据库的内容可能很多就不说了,另外,返回给你的对象如果是完整的pojo对象还好,但是如果是使用不同参数各种关联查询出来的结果那么redis中会储存太多冷数据。占用资源而得不到销毁。我们学过操作系统
也知道在计算机的缓存实现
中有)先进先出的算法(FIFO);最近最少使用算法(LRU);最佳淘汰算法(OPT);最少访问页面算法(LFR)等磁盘调度算法。对于web开发也可以借鉴。根据时间来的FIFO是最好实现的。因为redis在全局key
支持过期策略。 - 而开发中可能还会遇到
其他问题
。比如过期时间的选择上,如果过久会导致数据聚集。而过少可能导致频繁查询数据库甚至可能会导致缓存雪崩等问题。 - 所以,过期策略一定要设置。并且对于
关键key
一定要小心谨慎设计
。
数据一致性问题★
上面其实提到数据一致性问题。如果对一致性要求极高那么不建议使用缓存。下面稍微梳理一下缓存的数据。
在redis缓存中经常会遇到数据一致性问题。对于一个缓存。下面罗列逼仄
读
read
:从redis中读取,如果redis中没有,那么就从mysql中获取更新redis缓存。
该流程图
描述常规场景。一般没啥争议。
写1:先更新数据库,再更新缓存(普通低并发)
- 更新数据库信息,再更新redis缓存。这是常规做法,缓存基于数据库,取自数据库。但是其中可能遇到一些问题。例如上述如果更新缓存失败(宕机等其他状况),将会使得数据库和redis
数据不一致
。造成DB新数据,缓存旧数据。
写2:先删除缓存,再写入数据库(低并发优化)
解决的问题
- 这种情况能够有效避免写1中防止写入redis失败的问题。将缓存删除进行更新。理想是让下次访问redis为空去
mysql
取得最新值到缓存中。但是这种情况仅限于低并发的场景中而不适用
高并发场景。
存在的问题
- 写2虽然能够
看似写入redis异常的问题
。看似较为好的解决方案但是在高并发的方案中其实还是有问题的。我们在写1讨论过如果更新库成功,缓存更新失败会导致脏数据。我们理想是删除缓存让下一个线程
访问适合更新缓存。问题是:如果这下一个线程来的太早、太巧了呢?
- 因为多线程你也不知道谁先谁后,谁快谁慢。如上图所示情况,将会出现redis缓存数据和mysql不一致。当然你可以对key进行
上锁
。但是锁这种重量级的东西对并发功能影响太大,能不用锁就别用!上述情况就高并发下依然会造成缓存是旧数据,DB是新数据。并且如果缓存没有过期这个问题会一致存在。
写3:延时双删策略
- 这个就是延时双删策略,能过缓解在写2中在更新mysql过程中有读的线程进入造成redis缓存与mysql数据不一致。方法就是
删除缓存
->更新缓存
->延时(几百ms)(可异步)再次删除缓存
。即使在更新缓存途中发生写2的问题。造成数据不一致,但是延时(具体实间根据业务来,一般几百ms)再次删除也能很快的解决不一致。 - 但是就写的方案其实还是有漏洞的,比如
第二次删除错误
、多写多读高并发
情况下对mysql访问的压力等等。当然你可以选择用mq等消息队列异步解决。其实实际的解决很难顾及到万无一失,所以不少大佬在设计
这一环节可能会因为一些纰漏
会被喷
。作为菜菜的笔者在这里就更不献丑了,策略只是提供大纲,具体设计还是需要自己团队实践和摸索。并且也对一致性的要求级别有所区别。
写4:直接操作缓存,定期写入sql(适合高并发)
- 当有
一堆并发(写)
扔过来的后,前面几个方案即使使用消息队列异步通信但也很难给用户一个舒适的体验。并且对大规模操作sql对系统也会造成不小的压力。所以还有一种方案就是直接操作缓存,将缓存定期写入sql。因为redis这种非关系数据库又基于内存操作KV相比传统关系型要快很多(找值最多多碰撞几次)。
- 上面适用于高并发情况下业务设计,这个时候以redis数据为主,mysql数据为辅助。定期插入(好像数据备份库一样)。当然,这种高并发往往会因为业务对
读
、写
的顺序等等可能有不同要求,可能还要借助消息队列
以及锁
完成针对业务上对数据和顺序可能会因为高并发、多线程
带来的不确定性和不稳定性。提高业务可靠性。
总之,越是高并发
、越是对数据一致性要求高
的方案在数据一致性的设计方案需要考虑和顾及
的越复杂、越多
。上述也是笔者针对redis数据一致性问题的学习和自我发散(胡扯)学习。如果有解释理解不合理或者还请联系告知!
缓存穿透、缓存雪崩和缓存击穿
如果不了解,可能对这几个概念都不了解,听着感觉太高大上,至少笔者刚开始是这么觉得,本文并不是详细介绍如何解决和完美解决,更主要的是认识和认知吧。
redis缓存穿透
理解
- 重在
穿透
吧,也就是访问透过redis直接经过mysql,通常是一个不存在的key
,在数据库查询为null
。每次请求落在数据库、并且高并发。数据库扛不住会挂掉。
解决方案
- 可以将查到的null设成该key的缓存对象。
- 当然,也可以根据明显错误的key在逻辑层就就行
验证
。 - 同时,你也可以分析用户行为,是否为故意请求或者爬虫、攻击者。针对用户访问做限制。
- 其他等等,比如看到其他人用布隆过滤器(超大型hashmap)过滤。
redis缓存雪崩
理解
- 雪崩,就是某
东西蜂拥而至
的意思,像雪崩一样。在这里,就是redis缓存集体大规模集体失效
,在高并发情况下突然使得key大规模访问mysql,使得数据库崩掉。可以想象下国家人口老年化
。以后那天人集中在70-80岁,就没人干活了。国家劳动力就造成压力。
解决方案
- 通常的解决方案是将key的过期时间后面加上一个
随机数
,让key均匀的失效。 - 考虑用队列或者锁让程序执行在压力范围之内,当然这种方案可能会影响并发量。
redis缓存击穿
理解
- 击穿和穿透不同,穿透的意思是想法
绕过
redis去使得数据库崩掉。而击穿你可以理解为正面刚
击穿,这种通常为大量并发对一个key进行大规模的读写操作。这个key在缓存失效期间大量请求数据库,对数据库造成太大压力使得数据库崩掉。就比如
在秒杀场景下10000块钱的mac和100块的mac这个100块的那个订单肯定会被抢到爆。所以缓存击穿就是针对某个常用key大量请求导致数据库崩溃。
解决方案
- 能够达到这种场景的公司其实不多,我也不清楚他们的具体处理方法,但是一个锁拦截请求总是能防止数据库崩掉吧。
总结与感悟
-
其实缓存看起来,理解起来看似简单然而实际上的设计方案非常
有学问
。在细节设计上还会遇到消息队列、布隆过滤器、分布式锁、服务降级、熔断、分流这些。在缓存处理上甚至还有缓存预热
(提前缓存部分热点数据防止刚开始缓存全部命中导致服务崩掉)等其他热门名词和问题这里就不做介绍了。 -
另外在缓存设计方面个人感觉和
操作系统
的存储管理以及可能遇到的锁的设计上与读者优先、写者优先有着很大关系,大家可以参考和交流! -
当然,redis的内容深度很深,笔者水平有限可能有地方有错误还请大佬指出或者交流。当然本文基本为笔者个人理解难免有疏漏。同时写本文前也阅读了一些前辈的文章学习(转来转去不知道谁是原创就不放链接)还请多多指教!
-
如果对
后端、爬虫、数据结构算法
等感性趣欢迎关注我的个人公众号交流:bigsai