Redis作为缓存应该是我们工作中最广泛的应用了,这篇文章将总结归纳一下使用redis作为缓存的一点点知识,更多的是缓存方面的相关知识。
一、 为什么选Redis作为缓存,它对比其他缓存的优势以及劣势?
我们之所以要使用缓存,主要的原因还是因为我们的数据库的网络和磁盘I/O能力有限,使用缓存可以帮数据库分担I/O压力,同时,由于缓存一般都是基于内存的,所以也能提高我们的系统的并发能力。
目前我们工作中用的缓存主要有三种,本地缓存、Memcached以及Redis。
1)本地缓存,是将数据缓存在每个进程单独拥有的内存中,所以它的大小也受限于进程所拥有的内存。
2)Memcached。基于内存的分布式缓存,处理请求时候是使用多线程异步I/O的方式,所以存取速度快。但是只支持String一种数据类型,并且每个key最大是250字节,每个value最大只能占用1M字节。还有一个致命缺点,不支持持久化,也就意味着当它down的时候,里面的所有数据就没了,并且无法恢复。
3)Redis。基于内存的分布式缓存,处理请求时候是用单线程处理(redis6.0版本后,网络I/O部分开始采用多线程,但处理请求还是单线程),即使是单线程,它的每秒并发数也能达到10w。支持丰富的数据类型:String/List/Hash/Set/Sort Set等等。同时,Redis还支持持久化,这就是为什么它被称为NoSql,而不只是一个缓存的原因。同时,Redis官方支持Cluster集群模式,提供高可用服务。
二、 Redis作为缓存的优缺点
优点:支持丰富的数据类型,支持高并发,可以持久化。
缺点:相比于本地缓存,使用Redis作为缓存要进行网络通信,这也就意味着网络的任何问题都会直接影响到我们的Redis
服务,所以要考虑到我们服务的高可用等问题。
作为缓存时候,如果缓存大小达到了redis配置的最大内存大小时候,就会使用对应的淘汰策略来清清理出空间。Redis有以下淘汰策略:
noeviction:默认的淘汰策略。当内存满的时候,写请求会出错,删除和读请求正常。
allkeys-lru:淘汰掉最近最少使用的key。
volatile-lru:在设置了过期时间的key中淘汰掉最近最少使用的。
allkeys-random:随机淘汰。
volatile-random:从设置了过期时间的key中随机淘汰。
volatile-ttl:从设置了过期时间的key中淘汰掉剩余存活时间最短的。
从Redis4.0开始,新增了一种淘汰策略。LFU,即Least Frequently Used,最少访问频率。
allkeys-lfu:在所有key中淘汰掉访问频率最低的。
volatile-lfu:在设置了过期时间的key中淘汰掉访问频率最低的。
三、缓存三大问题
在我们使用了缓存的的业务系统中,读写顺序最经典的就是Cache Aside Pattern,
读:
先从缓存里取,如果有就直接返回,没有就从数据库查---->从数据库查出数据,设置到缓存中并返回。
写:
先写数据库---> 再删除缓存
3.1 缓存雪崩
1)什么是缓存雪崩?
缓存雪崩的时候,没有一个程序猿是无辜的...开个玩笑...
缓存雪崩指的是当我们的缓存服务宕机或者缓存的key大面积失效时候,如果此时刚好有大量的请求同时进来,那么大量的请求就会直接打到我们的数据库上,因为我们的数据库的网络和磁盘I/O能力有限,所以很容易导致数据库查询缓慢甚至宕机。
2)如何避免?
既然缓存雪崩的原因是缓存服务宕机或者key同时大面积失效,并且失效的瞬间有大量的请求同时访问,那么我们久可以从这三方面来做一些处理,避免雪崩。
从缓存服务宕机的方面来看,要避免宕机,我们就需要保证服务的高可用,所以,如果是用Redis的话,最好搭建Redis cluster集群,如果服务压力不大,也可以采用Sentinel的方式。
但如果运气不好,集群全部挂了呢?
这时候还可以设置多级缓存,也就是在请求redis缓存之前,设置一层本地缓存,如果从本地缓存找不到,再从redis取。这样就算redis挂了,本地缓存也能保证我们的缓存服务是可用的,请求压力不会直接落到我们的数据库上。
从key同时大面积失效这方面来看,要避免key的过期时间设置都是一样的,所以在设置缓存时候,我们可以给key的过期时间加上一个随机数,避免同时失效。
从大量请求同时进来这方面来看,要避免这种情况,如果系统允许,我们可以牺牲一定的并发处理能力来换取服务的稳定可用性,可以使用Hystrix或者Alibaba Sentinel等对我们的服务进行限流熔断等处理来减少到达数据库的请求量。
3.2 缓存穿透
1)什么是缓存穿透?
当我们的缓存和数据库中都不存在某个数据的时候,如果有人利用这些不存在的数据恶意的向我们的后台发起大量请求,那么这些请求都会落到我们的数据库上去,造成数据库查询缓慢甚至宕机,这就是缓存穿透。
2)如何避免?
可以先考虑从源头解决问题,既然请求的是不存在的key,那么我们可以在controller层做一些参数校验,肯定不存在的请求就可以直接过滤掉。比如用户的id不能为负数,当请求的userid=-1时候我们就直接返回参数错误信息。
可以把不存在数据的value设置为null放到缓存中,这样如果再次请求同样的数据,请求就只能到达缓存,不能到达数据库了。这种处理适用于请求的不存在的key重复率很高的情况,如果是恶意请求,那么请求的key基本上不会重复,缓存这种不会再请求第二次的key也没有太大意义。
布隆过滤器。先把我们数据库中所有的key放到布隆过滤器中,当请求进来时候,我们可以先用布隆过滤器过滤,如果不存在,就直接返回。(布隆过滤器相关介绍可以自己Google,我的Redis数据类型介绍的文章中也简单介绍了)。
3.3 缓存击穿
1)什么是缓存击穿?
缓存击穿和缓存雪崩类似,不过缓存击穿针对的是单个或者说少量数据。当我们缓存中的某个热点key失效的时候,如果刚好在失效的瞬间,大量请求同时查询这个key,那么就会造成我们的数据库压力瞬间变大,导致查询缓慢或者直接宕机...
3)如何避免?
加互斥锁。在热点key查询的时候,如果我们加了互斥锁,就算有再多请求进来,那也只会有一个请求能到达数据库,所以并不会对我们的数据库造成影响。
既然击穿是因为热点key失效引起的,那我们就可以设置热点key永不失效。
四、 如何保证Redis缓存中都是热点数据?
我们使用缓存的目的,就是为了让数据再被重复请求时候,直接从缓存中返回,不用再查数据库。但是如果我们缓存中的数据都不会被访问第二次...那我们的缓存就失去了意义。所以,如何保证缓存中都是热点数据很重要....
一般来说有两种方法:
预先估计热点key占用的内存大小,然后把redis内存限制为预估的大小,然后设置redis的过期key淘汰策略为lru。
所有key都必须设置过期时间,每次访问一个key时候,都延长它的失效时间。
五、缓存和数据库的一致性问题
其实当我们要用到缓存的时候,就代表我们对缓存和数据库的一致性作出了一些妥协,换来的是更好的系统性能的提升。只要我们的key有过期时间,那么数据库和缓存还是能保证最终一致性的...但是以前我面试被问到过如何保证缓存和数据库的强一致性...基于这个问题...有以下两种方案:
首先来分析下我们用Cache Aside Pattern模式读写缓存时候可能会带来的一致性问题...
当一个请求要更新数据库的时候,那么它先会更新数据库,再删除缓存...这是两步操作,意味着在更新数据前到删除缓存之间,这段时间内,缓存中的数据就和数据库中是不一样的,如果这段时间内有其他的请求读取了缓存中的数据,那么读到的数据就是脏数据...
1)分布式读写锁。
用分布式锁来保证读和写是串行的。当更新数据库的某个key时候,需要先申请写锁,写锁是互斥的,申请了写锁以后,不能申请写锁,也不能申请读锁。当读取数据时候,需要先申请读锁,读锁是共享的,读锁存在时候,还能继续读锁,但不能申请写锁。
如果有了实现上述机制的分布式锁,如果加写锁成功,说明要修改的数据没有读锁,也就是没有其他请求在读它了,这样就能保证不会有其他请求读到脏数据,更新成功并且把缓存删除,操作成功了以后,再释放写锁,这样就能保证强一致性...不会出现任何脏数据...但是...由于读写串行了,所以系统的并发性会下降很多...
2)一致性协议
一致性协议,比如2/3PC等,或者更强大的...Paxos(2/3阶段提交只能算是残次的Paxos)和Raft,都能保证这种强一致性...但是,实现起来太难了,而且性能非常低下...
总之...为了实现强一致性,付出的代价得不偿失...根据不同场景选择弱一致性或者最终一致性就好。
六、更新缓存的经典Design Pattern
Cache Aside Pattern 先写数据库,写成功后删除缓存
Read Through Pattern 在查询操作中更新缓存,也就是说,当缓存失效的时候(过期或LRU换出),Cache Aside是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对应用方是透明的。
Write Through Pattern 有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)
Write Behind Caching Pattern 在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ),因为异步,write backg还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。
七、与文无关
写完了,感觉写的乱七八糟的哈哈哈...最后补一点在查找资料过程中看到的...我们平时用的更新缓存套路一般都是Cache Aside Pattern,但是...删除缓存这个操作是不是会有概率导致缓存击穿呢哈哈哈。最近看了挺多东西...感觉写代码或者很多程序上的设计...确实是按下葫芦起了瓢,为了解决一个问题,又会引入了很多其他问题...比如我们要解决一致性问题,那就必须要牺牲系统性能...要性能,又不能要强一致性...以及CAP理论也是这样...一致性、可用性、分区容忍性只能三者取其二...
程序中的取舍和生活中很像....或许这就是“人有悲欢离合,月有阴晴圆缺,此事古难全”吧...所以...祝看到这篇文章的你“但愿人长久,千里共婵娟”。
有错误或者描述不准确的地方还请指出...
参考资料:
《缓存更新的套路》--coolshell.cn/articles/17…
《聊聊数据库与缓存数据一致性问题》--juejin.cn/post/684490…
《3 major problems and solutions in the cache world》--medium.com/@mena.meseh…