使用redis中引来了常见的三种缓存问题,本篇就常见的解决方案来展开分析。

缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
所谓穿透,就是直接透过了redis,直接透到数据库
常见的解决方案有两种:
缓存空对象
布隆过滤
对空值缓存:如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟。之后再访问这个数据将会从缓存中获取,保护了后端数据源;

采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的**二进制向量(位图)**和一系列随机映射函数(哈希函数)。

布隆过滤器可以用于检索一个元素是否在一个集合中:
将所有可能存在的数据哈希到一个足够大的bitmaps中,一定不存在的数据会被这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。
误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突,数据x和数据y的哈希结果一样,如果只有x,判断y是否存在的时候,由于跟x的哈希值一样,导致布隆过滤器误以为y也存在
布隆过滤器由「初始值都为 0 的位图数组」和「 N 个哈希函数」两部分组成。当我们在写入数据库数据时,在布隆过滤器里做个标记,这样下次查询数据是否在数据库时,只需要查询布隆过滤器,如果查询到数据没有被标记,说明不在数据库中。
布隆过滤器会通过 3 个操作完成标记:
举个例子,假设有一个位图数组长度为 8,哈希函数 3 个的布隆过滤器。

在数据库写入数据 x 后,把数据 x 标记在布隆过滤器时,数据 x 会被 3 个哈希函数分别计算出 3 个哈希值,然后在对这 3 个哈希值对 8 取模,假设取模的结果为 1、4、6,然后把位图数组的第 1、4、6 位置的值设置为 1。当应用要查询数据 x 是否数据库时,通过布隆过滤器只要查到位图数组的第 1、4、6 位置的值是否全为 1,只要有一个为 0,就认为数据 x 不在数据库中。
注意:此处为什么需要3个hash函数?
若只有1个hash函数,冲突的概率是很大的,都hash到同一个位置,导致误判的概率很大 因此使用多个hash函数,hash到多个位置,只有这几个位置都是1,才说明x存在,误判的概率会降低些
布隆过滤器由于是基于哈希函数实现查找的,高效查找的同时存在哈希冲突的可能性,比如数据 x 和数据 y 可能都落在第 1、4、6 位置,而事实上,可能数据库中并不存在数据 y,存在误判的情况。
所以,查询布隆过滤器说数据存在,并不一定证明数据库中存在这个数据,但是查询到数据不存在,数据库中一定就不存在这个数据。
本质上是一个很大的位图,存储值的时候,用多个hash函数计算出他要存储的位置,比如x,对应hash后的结果是1,2,4, 把这几个位置标记为1。
查询的时候,对x做多次hash,只有所有hash后的位置标记位都是1,才可能存在
因为可能x被删除后,又进来了一个y,y的hash结果跟x一模一样,此时会出现误判。
比如现在要删除x,对x做hash,找到了他的存储位置分别是【1,3,9】,我们如果直接把这三个位置改为0,可能会导致“删除”了其他元素
🎈如何解决呢?
最简单的做法就是加一个计数器,就是说位数组的每个位如果不存在就是0,存在几个元素就存具体的数字,而不仅仅只是存1。
那么这就有一个问题,本来存1,一位就可以满足了,但是如果要存具体的数字,可能需要更多的位数,所以带有计数器的布隆过滤器会占用更大的空间。
使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。
缓存穿透产生的原因是什么?
缓存穿透的解决方案有哪些?
缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。
常见的解决方案有两种:
逻辑分析:假设线程1在查询缓存之后,本来应该去查询数据库,然后把这个数据重新加载到缓存的,此时只要线程1走完这个逻辑,其他线程就都能从缓存中加载这些数据了,但是假设在线程1没有走完的时候,后续的线程2,线程3,线程4同时过来访问当前这个方法, 那么这些线程都不能从缓存中查询到数据,那么他们就会同一时刻来访问查询缓存,都没查到,接着同一时间去访问数据库,同时的去执行数据库代码,对数据库访问压力过大

因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用 **tryLock方法 + double check **来解决这样的问题。
只有查询缓存没有命中的情况下,才去加锁
假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行更新缓存的逻辑 假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就进入休眠,隔一段时间后再**重试【再次调用自己】**直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。

我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间【永不过期】,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。
我们把过期时间设置在 redis的value中
注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。
假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程A会开启一个 线程去进行重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。
这种方案巧妙在于,异步的构建缓存【新开一个线程】
缺点在于在构建完缓存之前,返回的都是脏数据。

前者是牺牲了可用性,保证了一致性 后者是牺牲了一致性,保证了可用性 CAP
互斥锁方案:由于保证了互斥性,所以数据一致,且实现简单,因为仅仅只需要加一把锁而已,也没其他的事情需要操心,所以没有额外的内存消耗 缺点在于有锁就有死锁问题的发生,且只能串行执行性能肯定受到影响
逻辑过期方案: 线程读取过程中不需要等待,性能好,有一个额外的线程持有锁去进行重构数据,但是在重构数据完成前,其他的线程只能返回旧数据,且实现起来麻烦

定时任务,提前从数据库查询出来,存到缓存里边,而不是等到用户高并发访问了,再去查询数据库,设置缓存
缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
分散,不会同时过期,一起失效
nginx缓存 + redis缓存 +其他缓存(ehcache等)
如果 Redis 缓存的主节点故障宕机,从节点可以切换成为主节点,继续提供缓存服务,避免了由于 Redis 故障宕机而导致的缓存雪崩问题。
降级
由于爆炸性的流量冲击,对一些服务进行有策略的放弃,以此缓解系统压力,保证目前主要业务的正常运行。它主要是针对非正常情况下的应急服务措施:当此时一些业务服务无法执行时,给出一个统一的返回结果。
降级方式
在固定时间窗口内,接口调用超时比率达到一个阈值,会开启熔断。
【当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。】
当经过了规定时间之后,服务将从熔断状态恢复过来,再次接受调用方的远程调用。
关于降级和限流,后续我们再单独开专栏来详细谈谈
