前言
原创公众号:bigsaijava
对于Web来讲,用户量和访问量增必定程度上推进项目技术和架构的更迭和进步。可能会有如下的一些情况:redis
- 页面并发量和访问量并很少,MySQL
足以支撑
本身逻辑业务的发展。那么其实能够不加缓存。最多对静态页面进行缓存便可。 - 页面的并发量显著增多,数据库有些压力,而且有些数据更新频率较低
反复被查询
或者查询速度较慢
。那么就能够考虑使用缓存技术优化。对高命中的对象存到key-value形式的Redis中,那么,若是数据被命中,那么能够不通过效率很低的db。从高效的redis中查找到数据。 - 固然,可能还会遇到其余问题,你还经过静态页面缓存页面、cdn加速、甚至负载均衡这些方法提升系统并发量。这里就不作介绍。
缓存思想无处不在
咱们从一个算法问题开始了解缓存的意义。算法
问题1:sql
- 输入一个数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组数据(可能成百上千),每组一个xi(xi<20),求
xi!
;
分析2:缓存
- 若是使用
递归
,输入t组数据,每次输入为xi,那么每次都要执行次数为:
当每次输入的Xi过大或者t过大都会形成不小的负担!时间复杂度为O(n2) - 那么可否换个思想的。没错、是
打表
。打表经常使用于ACM算法中,经常使用于解决多组输入输出、图论搜索结果、路径储存问题。那么,对于这个求阶乘。咱们只须要申请一个数组,按照编号从前日后将在需求的数存到数组中,后面再取得时候直接输出数组值就能够,思想很明确吧:
import java.util.Scanner; public class test { 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提升系统吞吐和并发量。多线程
对于经常使用网站和场景,关系数据库主要可能慢在两个地方:架构
- 读写IO性能较差
- 一个数据可能经过较大量计算获得
因此使用缓存可以减小磁盘IO次数和关系数据库的计算次数。读取上速度快也从两个方面体现:并发
- 基于内存,读写较快
- 使用哈希算法直接定位结果不须要计算
因此对于像样的,有点规模的网站,缓存是很 necessary
的,而Redis无疑是最好的选择之一。
须要注意的问题
缓存使用不当会带来不少问题。因此须要对一些细节进行认真考量和设计。固然最可贵数据一致性在下面单独分析。
是否用缓存
项目不能为了用缓存而用缓存,缓存并必定适合全部场景,若是对数据一致性要求极高,又或者数据频繁更改而查询并很少,又或者根本没并发量的、查询简单的不必定须要缓存,还可能浪费资源使得项目变得臃肿难维护,而且使用redis缓存多多少少可能会遇到数据一致性问题须要考虑。
缓存合理设计
在设计缓存的时候,极可能会遇到多表查询,若是遇到多表查询缓存的键值对就须要合理考虑,是拆分仍是合在一块儿?固然若是组合种类多但常出现的很少也能够直接缓存,具体的设计要根据项目业务需求来看,并无一个很是绝对的标准。
过时策略选择
- 缓存装的是相对热点和经常使用的数据,Redis资源也是有限,须要选择一个合理的策略让缓存过时删除。咱们学过
操做系统
也知道在计算机的缓存实现中有先进先出的算法(FIFO);最近最少使用算法(LRU);最佳淘汰算法(OPT);最少访问页面算法(LFR)等磁盘调度算法。设计Redis缓存时候也能够借鉴。根据时间来的FIFO是最好实现的。且Redis在全局key
支持过时策略。 - 而且过时时间也要根据系统状况合理设置,若是硬件好点当前能够稍微久一点,可是过时时间太久或者太短可能都不太好,太短可能缓存命中率不高,而太久极可能形成不少冷门数据存储在Redis中不释放。
数据一致性问题★
上面其实提到数据一致性问题。若是对一致性要求极高那么不建议使用缓存。下面稍微梳理一下缓存的数据。
在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数据一致性问题的学习和自我发散(胡扯)学习。若是有解释理解不合理或者还请各位大佬指正!
最后,感受不错的话还请一键三连,欢迎关注原创公众号:「bigsai」,在这里,不只能学到知识和干货,还给你准备了不少进阶资料,回复「bigsai」口令便可领取!