因为csdn只支持这种文档形式:不支持思维导图:
更好友好的阅读:可以看我的飞书--------思维导图(这样食用更加):
缓存穿透
问题原因
每次从缓存中都查不到数据,而需要查询数据库,同时数据库中也没有查到该数据,也没法放入缓存
如何解决
布隆过滤器
原理:布隆过滤器可以用于检索一个元素是否在一个集合中。
结构:概率型数据结构
使用
方式一:通过自己写算法实现布隆过滤器
方式二:使用网上存在的开源包
推荐指数:⭐⭐⭐(这个技术有些过时了,但是可以用)
优点:算法简单,可以自己配置
缺点:
存在一定的误判率
不支持删除操作
查找性能比布谷鸟底
空间利用率低
布谷鸟过滤器
要求条件:适用于对误判率要求较高的场景。
如网络路由、存储系统等
原理:
最简单的布谷鸟哈希结构是一维数组结构,会有两个 hash 算法将新来的元素映射到数组的两个位置。如果两个位置中有一个位置为空,那么就可以将元素直接放进去。但是如果这两个位置都满了,它就不得不「鸠占鹊巢」,随机踢走一个,然后自己霸占了这个位置。
近似集合数据结构
如何使用
方式一:自己写一个算法(我试过,比较复杂)
方式二:调用别人写的库
情况
优点:
相较于布隆过滤器具有更低的误判率
支持删除操作
查找性能高
空间利用率高
缺点:
删除不完美,存在误删的概率。
删除的时候只是删除了一份指纹副本,并不能确定此指纹副本是要删除的key的指纹。同时这个问题也导致了假阳性的情况。
插入复杂度比较高
随着插入元素的增多,复杂度会越来越高,因为存在桶满,踢出的操作,所以需要重新计算,但综合来讲复杂度还是常数级别。
存储空间的大小必须为2的指数
让空间效率打了折扣
同一个元素最多插入kb次(k指哈希函数的个数,b指的桶中能装指纹的个数)
如果布谷鸟过滤器支持删除,则必须存储同一项的多个副本。 插入同一项kb+1次将导致插入失败。 这类似于计数布隆过滤器,其中重复插入会导致计数器溢出。
推荐指数:⭐⭐⭐⭐⭐(绝对好用)
缓存空对象
方案一
要求条件:适中
推荐指数:⭐⭐⭐⭐(很常见的方式)
设置缓存的时候,同时设置一个过期时间,这样过期之后,就会重新去数据库查询最新的数据并缓存起来
方案二
要求条件:实时性要求非常高
推荐指数:⭐⭐⭐(要结合实际情况,考虑双写一致性的问题)
如果对的话,那就写数据库的时候,同时写缓存。这样可以保障实时性
方案三
要求条件:实时性要求不是那么高
推荐指数:⭐⭐⭐⭐(非常实用的方式)
那就写数据库的时候给消息队列发一条数据,让消息队列再通知处理缓存的逻辑去数据库取出最新的数据
校验参数
原理:
我们可以对用户id做检验。(请求合法性校验)
比如权限管理
推荐指数:⭐⭐⭐⭐⭐(这个必须做)
缓存击穿
问题原因
缓存击穿问题是由于key过期了导致的
如何解决
加锁
原理:在缓存失效的瞬间,通过加锁机制来保证只有一个请求能够访问数据库,其他请求等待获取缓存数据。
使用:
同一时刻只有一个请求才能访问某个信息
并且另一个线程将数据库中查询到的结果,又重新放入缓存中
推荐指数:⭐⭐⭐(非必要不加锁)
自动续期
原理:在key快要过期之前,就自动给它续期
推荐指数:⭐⭐⭐(本来要过期,但是续期的指令如何识别)
缓存不失效
原理:对于很多热门key,其实是可以不用设置过期时间,让其永久有效的。
推荐指数:⭐⭐⭐⭐(正对某些业务可以设置)
缓存雪崩
原因:有多个热门key同时失效(非常严重):归根结底都是有大量的请求,透过缓存,而直接访问数据库了
有大量的热门缓存,同时失效。会导致大量的请求,访问数据库。而数据库很有可能因为扛不住压力,而直接挂掉。
缓存服务器宕机了,可能是机器硬件问题,或者机房网络问题。总之,造成了整个缓存的不可用。
解决办法
过期时间加随机数
推荐指数:⭐⭐⭐⭐(较为安全)
要求不要设置相同的过期时间
即使在高并发的情况下,多个请求同时设置过期时间,由于有随机数的存在,也不会出现太多相同的过期key
高可用(分布式部署)
推荐指数:⭐⭐⭐⭐(能在源头处理最好)
深层原因:针对缓存服务器宕机的情况,
原理:通过多个节点来分担缓存的压力,提高系统的容灾性
解决办法:
在前期做系统设计时,可以做一些高可用架构
redis可以使用哨兵模式,或者集群模式,避免出现单节点故障导致整个redis服务不可用的情况
服务降级(备份机制)
推荐指数:⭐⭐⭐⭐(做一个数据兜底)
更深的原因:做了高可用架构,redis服务还是挂了,该怎么办呢?
解决办法:配置一些默认的兜底数据。
程序中有个全局开关,比如有10个请求在最近一分钟内,从redis中获取数据失败,则全局开关打开。就直接从配置中心中获取默认的数据。
还需要有个线程,每隔一定时间去从redis中获取数据
数据预热
原理:数据预热就是系统上线后,将相关的缓存数据直接加载到缓存系统,这样就可以避免在用户请求的时候先查询数据库
方式:在系统低峰期,提前对热点数据进行加载和缓存,避免大量数据同时失效
推荐指数:⭐⭐⭐⭐(不要等高峰了做预案)
双层缓存策略
C1为原始缓存,C2为拷贝缓存,C1失效时可以访问C2,C1缓存失效时间设置为短期,C2缓存失效时间设置为长期
推荐指数:⭐⭐⭐(万一都宕机就完蛋了)
定时更新缓存策略
原理:失效性要求不高的缓存,容器启动初始化加载,采用定时任务更新或移除缓存
使用:另起一个线程,去做任务处理
推荐指数:⭐⭐⭐⭐⭐(前期预防永远比后期解决来的好)
双写一致性(redis与mysql)
原因:分布式常见问题
数据库和缓存双写,就必然会存在不一致的问题
最终一致性:
要保证数据库与缓存可以先后一致
强一致性:
有强一致性要求的数据,不能放缓存
解决方法:
更新缓存
先写缓存,写数据库
问题程度:上上(很严重)
问题原因:刚写完缓存,突然网络出现了异常,导致写数据库失败了
推荐指数:⭐⭐(用的不多)
造成影响:容易诞生假数据
先写数据库,在写缓存
问题情况:写缓存失败了
问题程度:上 (严重)
问题原因:写数据库和写缓存,都属于远程操作。(通常建议写数据库和写缓存不要放在同一个事务中)
写数据库成功了,但写缓存失败了,数据库中已写入的数据不会回滚
推荐指数:⭐⭐ ⭐(接口性能要求不太高的系统)
造成影响:数据库是新数据,而缓存是旧数据,两边数据不一致的情况
问题情况:高并发先写数据库,在写缓存
问题程度:上上 (很严重)
问题原因:请求b在缓存中的新数据,被请求a的旧数据覆盖了。(请求互相覆盖)
推荐指数:⭐⭐(高并发场景,不适合)
造成影响:可能会出现数据库是新值,而缓存中是旧值,两边数据不一致的情况
问题情况:浪费系统资源
问题程度:上(严重)
问题原因:每写一次缓存,都需要经过一次非常复杂的计算
推荐指数:⭐⭐(计算要求低的情况)
造成影响:浪费cpu和内存资源
删除缓存
先删缓存,在写数据库
高并发下,单删
问题程度:中上(适度)
问题原因:请求A删除完缓存数据Data后,网络卡顿,写入数据库操作暂停。请求B读数据Data,缓存中没有,去数据库读,读的是旧数据。请求A网络好了,写入新数据到数据库
推荐指数:⭐⭐⭐(低并发下)
造成影响:请求A的新值并没有被请求B写入缓存,同样会导致缓存和数据库的数据不一致的情况
高并发下,缓存双删
问题程度:中下 (良好)
问题原因:写数据库之前删除缓存一次,写完数据库后,再删除缓存一次
推荐指数:⭐⭐⭐⭐(看情况推荐使用)
使用要点:第二次删除缓存,并非立马就删,而是要在一定的时间间隔之后。
原因:在另一个请求未生效前,删除这就没有意义了
造成影响:第二次删除可能失败
解决办法:重试机制
原理:更新了数据库成功了,但更新缓存失败了,可以立刻重试N次。如果其中有任何一次成功,则直接返回成功。如果N次都失败了,则写入数据库,准备后续再处理。
同步重试
推荐指数:⭐⭐⭐()
原因:接口并发量比较高的时候,可能有点影响接口性能
异步重试
推荐指数:⭐⭐⭐⭐⭐
方式一:每次都单独起一个线程,该线程专门做重试的工作
推荐指数:⭐⭐⭐
原因:在高并发的场景下,可能会创建太多的线程,导致系统OOM问题
方式二:将重试的任务交给线程池处理
推荐指数:⭐⭐⭐⭐
原因:如果服务器重启,部分数据可能会丢失
方式三:将重试数据写表存入数据库(定时任务)
推荐指数:⭐⭐⭐⭐(不适合实时性要求特别高的业务场景)
使用:定时任务进行重试
重试表(至少得有一下字段):
重试次数字段
是否成功的状态字段
原理:设置初始值,每次删除重试次数字段加1,只要任意有一次成功就返回成功,同时修改状态,等待后续进一步处理
缺点:实时性没那么高
优点:数据是落库的,不会丢数据
方式四:将重试的请求写入mq等消息中间件
推荐指数:⭐⭐⭐⭐⭐(实时性还是比较高的)
使用:mq的consumer中处理
原理:
操作写完数据库,但删除缓存失败了,产生一条mq消息,发送给mq服务器
mq消费者读取mq消息,如果其中有任意一次成功了,则返回成功。重试N此后还是失败,写入死信队列中,后续需要人工处理
优点:mq的实时性还是比较高的
优化:删除缓存可以完全走异步。即用户的写操作,在写完数据库之后,不用立刻删除一次缓存。而直接发送mq消息,到mq服务器,然后有mq消费者全权负责删除缓存的任务。
方式五:订阅mysql的binlog
推荐指数:⭐⭐⭐⭐⭐(强烈推荐)
原理:订阅者中,如果发现了更新数据请求,则删除相应的缓存(监听binlog)
使用:canal等中间件,实现
原理:业务接口中写数据库之后,就不管了,直接返回成功,mysql服务器会自动把变更的数据写入binlog中,binlog订阅者获取变更的数据,然后删除缓存
问题:也会删除失败
解决办法:重试机制
推荐使用方式三和方式四(当推荐方式四)
先写数据库,再删缓存
问题程度:下上 (优秀)
问题原因:缓存过期失效,请求A查询数据发现缓存没有,去数据库查询,但是网络原因,没有及时更新缓存。请求B先写数据库,然后删除缓存。请求A恢复,更新缓存。
诞生条件(同时满足概率小)
缓存刚好自动失效
请求A查询数据库旧值以及更新缓存数据的时间,比请求B写入数据库以及删除缓存数据的时间长
一般数据库查询比数据库写入时间短(那种复合,聚合查询就不好说了)
推荐指数:⭐⭐⭐⭐⭐ (推荐使用)
使用指南:请求查询不要太过复杂
造成影响:最差的情况就是读取到旧数据(问题不算太大,刷新一下,重新请求,数据就对了),删除可能失效
解决办法:重试机制
原理:更新了数据库成功了,但更新缓存失败了,可以立刻重试N次。如果其中有任何一次成功,则直接返回成功。如果N次都失败了,则写入数据库,准备后续再处理。
同步重试
推荐指数:⭐⭐⭐(适合低并发)
原因:接口并发量比较高的时候,可能有点影响接口性能
异步重试
推荐指数:⭐⭐⭐⭐⭐(常见操作)
方式一:每次都单独起一个线程,该线程专门做重试的工作
推荐指数:⭐⭐⭐
原因:在高并发的场景下,可能会创建太多的线程,导致系统OOM问题
方式二:将重试的任务交给线程池处理
推荐指数:⭐⭐⭐⭐
原因:如果服务器重启,部分数据可能会丢失
方式三:将重试数据写表存入数据库(定时任务)
推荐指数:⭐⭐⭐⭐(不适合实时性要求特别高的业务场景)
使用:定时任务进行重试
重试表(至少得有一下字段):
重试次数字段
是否成功的状态字段
原理:设置初始值,每次删除重试次数字段加1,只要任意有一次成功就返回成功,同时修改状态,等待后续进一步处理
缺点:实时性没那么高
优点:数据是落库的,不会丢数据
方式四:将重试的请求写入mq等消息中间件
推荐指数:⭐⭐⭐⭐⭐(实时性还是比较高的)
使用:mq的consumer中处理
原理:
操作写完数据库,但删除缓存失败了,产生一条mq消息,发送给mq服务器
mq消费者读取mq消息,如果其中有任意一次成功了,则返回成功。重试N此后还是失败,写入死信队列中,后续需要人工处理
优点:mq的实时性还是比较高的
优化:删除缓存可以完全走异步。即用户的写操作,在写完数据库之后,不用立刻删除一次缓存。而直接发送mq消息,到mq服务器,然后有mq消费者全权负责删除缓存的任务。
方式五:订阅mysql的binlog
推荐指数:⭐⭐⭐⭐⭐(强烈推荐)
原理:订阅者中,如果发现了更新数据请求,则删除相应的缓存(监听binlog)
使用:canal等中间件,实现
原理:业务接口中写数据库之后,就不管了,直接返回成功,mysql服务器会自动把变更的数据写入binlog中,binlog订阅者获取变更的数据,然后删除缓存
问题:也会删除失败
解决办法:重试机制
推荐使用方式三和方式四(当推荐方式四)
缓存的并发竞争
原因:多个redis的client同时set key引起的并发问题
多客户端同时并发写一个key,一个key的值是1,本来按顺序修改为2,3,4,最后是4,但是顺序变成了4,3,2,最后变成了2。
解决方案:
乐观锁
推荐指数:⭐⭐⭐(乐观锁适用于大家一起抢着改同一个key)
使用:watch 命令可以方便的实现乐观锁
缺点:如果 redis 使用了数据分片的方式,那么这个方法就不适用了
什么是数据分片?
原理:Redis的分片机制允许数据拆分存放在不同的Redis实例上,每个Redis实例只包含所有键的子集
优点:可以减轻单台Redis的压力,提升Redis扩展能力和计算能力
分布式锁+时间戳
原理:加锁的目的实际上就是把并行读写改成串行读写的方式,从而来避免资源竞争
用一个状态值表示锁,对锁的占用和释放通过状态值来标识
加锁使用方法:
使用:使用redis中setnx()函数
返回1,则客户端获得锁,把锁的键值设置为时间值,表示该键已经被锁定,可以通过DEL lock.foo来释放该锁
返回 0 ,则表明该锁已经被其他客户端取得,这时返回重试等待对方完成,或者等待锁超时,在或者返回
推荐指数:⭐⭐⭐⭐(适合分布式环境)
优点:不用关心 redis 是否为分片集群模式
时间戳使用:(适合有序场景)
原理:系统B先抢到锁,将key1设置为{ValueB 7:05}。接下来系统A抢到锁,发现自己的key1的时间戳早于缓存中的时间戳(7:00<7:05),那就不做set操作了
推荐指数:⭐⭐⭐(适合对于有序情况)
优点:业务处理有序
消息队列
原理:把Redis.set操作放在队列中使其串行化,及一个一个执行(较为通用的解决方案)
推荐指数:⭐⭐⭐⭐(高并发场景中)
优点:串行化