细想了一下,这篇标题上这些东西,每一块拿出来都能长篇大论。那我就,长话挑着说,中间件用链接。
官方套话我就不多说了,做后端的朋友多多少少肯定耳濡目染了。
redis
是一个NOSQL类型数据库,
是一个高性能的key-value数据库,
是为了解决高并发、高可用、大数据存储等一系列的问题而产生的数据库解决方案,
是一个非关系型的数据库,
但是,它也是不能替代关系型数据库,只能作为特定环境下的扩充。
这么说可还算中肯?没有神话它,也把它的情况点出来了。
总的来说,它是一个很好用且应用范围很广的数据库中间件,缓存中间件。
redis由于数据的读取和操作都在内存当中操作,读写的效率较高,所以经常被用来做数据的缓存。把一些需要频繁访问的数据,而且在短时间之内不会发生变化的,放入redis中进行操作,从而提高用户的请求速度和降低MySQL数据库(后面都默认数据库 = MySQL)的负载。
弄了两张对比图:
没有用redis时,服务器对数据库的访问情况是这样的:
用了redis之后,服务器对数据的访问是这样的:
了解设计模式吗? 我以“享元模式”(传送门)的思想来理解。
为什么要这么做?别急,等我们看完“缓存穿透”就知道了。
博主不辞辛劳翻译了一下redis.conf配置文件,感觉里面东西还是挺好的。
redis.conf翻译与配置(一)【redis6.0.6】
redis.conf翻译与配置(二)【redis6.0.6】
redis.conf翻译与配置(三)【redis6.0.6】
redis.conf翻译与配置(四)【redis6.0.6】
redis.conf翻译与配置(五)【redis6.0.6】
redis.conf翻译与配置(六)【redis6.0.6】
翻译亦是不易,大家多多支持
业务系统要查询的数据根本就存在!当业务系统发起查询时,按照上述流程,首先会前往缓存中查询,由于缓存中不存在,然后再前往数据库中查询。由于该数据压根就不存在,因此数据库也返回空。这就是缓存穿透。
如果存在海量请求查询压根就不存在的数据,那么这些海量请求都会落到数据库中,数据库压力剧增,可能会导致系统崩溃(你要知道,目前业务系统中最脆弱的就是IO,稍微来点压力它就会崩溃,所以我们要想种种办法保护它)。
那么我们现在再来想想,为什么需要用缓存。答案已经很明显了,不用缓存,相当于直接击穿。
这个方案简单讲一下。
之所以发生缓存穿透,是因为缓存中没有存储这些空数据的key,导致这些请求全都打到数据库上。
那么,我们可以稍微修改一下业务系统的代码,将数据库查询结果为空的key也存储在缓存中。当后续又出现该key的查询请求时,缓存直接返回null,而无需查询数据库。
使用布隆过滤器。在缓存之前在加一层布隆过滤器,在查询的时候先去布隆过滤器查询 key 是否存在,如果不存在就直接返回,存在再查缓存和DB。
关于布隆过滤器,如果要讲的话又可以来一篇博客了,但是不了解又不好,这是一篇我之前转的布隆过滤器的文章:传送门,既然讲到这里,那后面我会去在原文基础上再进行修改,地址不会变。
布隆过滤器特性:
如果布隆过滤器判断该元素存在,那么该元素大概率存在,如果布隆过滤器判断该元素不存在,那么该元素则一定不存在。
第一种方案:容易出现缓存太多空值占用了更多的空间,得不偿失。
对于空数据的key各不相同、key重复请求概率低的场景而言,应该选择第二种方案。而对于空数据的key数量有限、key重复请求概率较高的场景而言,应该选择第一种方案。
简单的说:第二种方案比较应景。
通过上文可知,缓存其实扮演了一个保护数据库的角色。它帮数据库抵挡大量的查询请求,从而避免脆弱的数据库受到伤害。
如果缓存因某种原因发生了宕机,那么原本被缓存抵挡的海量查询请求就会像疯狗一样涌向数据库。此时数据库如果抵挡不了这巨大的压力,它就会崩溃。
这就是缓存雪崩。
某一个时间段内,缓存大量失效或者缓存服务器挂掉(重启)时,导致大量请求直接去访问数据库,导致数据库崩溃。
雪崩时,每一片雪花都在想着勇闯天涯! – 看,未来
这就像下班高峰期一样,高速路、大马路、小马路通通堵上了,交通陷入了困境。。。
开个玩笑啊,这个方法简直是,没话说。咱还没到富得流油的时候,没那么多内存空间啊。
既然让大家“不畏惧上班,不想着下班”是不现实的,又要解决下班高峰期的问题,怎么办?
之前不是有个方法,叫流量错峰嘛,错开高峰期,比方说你六点下班,我五点下班,他九点半,是吧。
key的失效期分散开,不同的key设置不同的有效期,这样就可以有效的避免大量key值下班而导致的窘境了。
加一层本地缓存(例如Guava Cache、ECache等),采用本地缓存+分布式缓存redis的方式。
这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。
依赖隔离组件为后端限流并降级。在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
个人认为,二、四比较好一些。
其实理解了前面的缓存穿透和缓存雪崩之后,就很好理解缓存击穿了。
如果某一个热点数据失效,那么当再次有该数据的查询请求时就会前往数据库查询。但是,从请求发往数据库,到该数据更新到缓存中的这段时间中,由于缓存中仍然没有该数据,因此这段时间内到达的查询请求都会落到数据库上,这将会对数据库造成巨大的压力。此外,当这些请求查询完成后,都会重复更新缓存。
此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可.
当某一个热点数据失效后,只有第一个数据库查询请求发往数据库,其余所有的查询请求均被阻塞,从而保护了数据库。但是,由于采用了互斥锁,其他请求将会阻塞等待,此时系统的吞吐量将会下降。这需要结合实际的业务考虑是否允许这么做。
互斥锁可以避免某一个热点数据失效导致数据库崩溃的问题,而在实际业务中,往往会存在一批热点数据同时失效的场景。那么,对于这种场景该如何防止数据库过载呢?
参考“雪崩”,错峰。
在处理雪崩问题上这个方法会比较扯,但是在处理热键问题是可以考虑的。
互斥锁 (mutex key):这种方案思路比较简单,但是存在一定的隐患,如果构建缓存过程出现问题或者时间较长,可能会存在死锁和线程池阻塞的风险,但是这种方法能够较好的降低后端存储负载并在一致性上做的比较好。
” 永远不过期 “:这种方案由于没有设置真正的过期时间,实际上已经不存在热点 key 产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。
读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存和数据库间的数据一致性问题。不管是先写数据库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。举个例子:
1、如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。2、如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不一致的问题。如何解决?
结合前面例子的两种删除情况,我们就考虑前后双删加懒加载模式。
就是当业务读取数据的时候再从存储层加载的模式,而不是更新后主动刷新。
如果你有伸展树(传送门)的基础,那理解这个“懒加载”就会融会贯通。
在写库前后都进行redis.del(key)操作,并且第二次删除通过延迟的方式进行。
异步延迟删除:
1)先删除缓存;2)再写数据库;3)触发异步写入串行化mq(也可以采取一种key+version的分布式锁);4)mq接受再次删除缓存。
异步删除对线上业务无影响,串行化处理保障并发情况下正确删除。
db更新分为两个阶段,更新前及更新后,更新前的删除很容易理解,在db更新的过程中由于读取的操作存在并发可能,会出现缓存重新写入数据,这时就需要更新后的删除。
1、设置缓存过期时间
从理论上来说,给缓存设置过期时间,是保证最终一致性的解决方案。所有的写操作以数据库为准,只要到达缓存过期时间,则后面的读请求自然会从数据库中读取新值然后回填缓存。
结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致。
2、重试方案
重试方案有两种实现,一种在业务层做,另外一种实现中间件负责处理。
业务层实现重试如下:
然而,该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
中间件实现重试如下:
大key指的是存储的值(Value)非常大。
大key会大量占用内存,在集群中无法均衡 Redis的性能下降,主从复制异常 在主动删除或过期删除时会操作时间过长而引起服务阻塞
redis-cli --bigkeys命令。可以找到某个实例5种数据类型(String、hash、list、set、zset)的最大key。但如果Redis 的key比较多,执行该命令会比较慢。 获取生产Redis的rdb文件,通过rdbtools分析rdb生成csv文件,再导入MySQL或其他数据库中进行分析统计,根据size_in_bytes统计big key
string类型的big key,尽量不要存入Redis中,可以使用文档型数据库MongoDB或缓存到CDN上。如果必须用Redis存储,最好单独存储,不要和其他key一起存储。采用一主一从或多从。 单个简单key存储的value很大,可以尝试将对象分拆成几个key-value, 使用mget获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多次操作中,降低对redis的IO影响。 hash, set,zset,list 中存储过多的元素,可以将这些元素分拆。
先到这儿啦,如果觉得点进来不亏,不妨顺手来个关注收藏。