Redis之常见问题

本文阅读 18 分钟
首页 代码,Java 正文

什么是缓存击穿

缓存击穿是指一个请求要访问的数据,缓存中没有,但数据库中有的情况。 这种情况一般来说就是缓存过期了。但是这时由于并发访问这个缓存的用户特别多,这是一个热点 key,这么多用户的请求同时过来,在缓存里面没有取到数据,所以又同时去访问数据库取数据,引起数据库流量激增,压力瞬间增大, 所以一个数据有缓存,每次请求都从缓存中快速的返回了数据,但是某个时间点缓存失效了,某个请求在缓存中没有请求到数据,这时候我们就说这个请求就"击穿"了缓存。

怎么解决缓存击穿问题

解决缓存击穿问题,大致有三种思路:

  • 只放行一个请求到数据库,然后做构建缓存的操作 就借助 Redis setNX 命令设置一个标志位就行。设置成功的放行,设置失败的就轮询等待。放行的请求回去构建缓存操作。
  • 后台续命 这个方案的思想就是,后台开一个定时任务,专门主动更新即将过期的数据。
  • 永不过期 缓存为什么会被击穿,是不是因为设置了超时时间,然后被回收了?直接设置不过期,简单暴力!

什么是缓存穿透

缓存穿透是指一个请求要访问的数据,缓存和数据库中都没有,而用户短时间、高密度的发起这样的请求,每次都请求到数据库服务上,给数据库造成了压力。 一般来说这样的请求属于恶意请求。就是明知到我这里没有这个数据(缓存和数据都没有),但还是发送这样请求,如果这种请求过多,很容易压垮数据库。

如何解决缓存穿透问题

一般有两种解决方案:缓存空对象布隆过滤器

缓存空对象

当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源。 缺点:如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。如何避免?

  • 对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该key对应的数据insert了之后清理缓存。
  • 对一定不存在的key进行过滤。可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。

布隆过滤器

如果要想讲清楚使用布隆过滤器的解决缓存穿透的问题,需要用大量的篇幅,后面我们会专门写文章,这里不再赘述。

什么是缓存雪崩

缓存雪崩是指在非常短的时间内大量的缓存数据到达过期时间,同时查询这些缓存数据的请求又非常多,导致所有请求直接打到数据库上,引起数据库流量激增,进而引起数据库崩溃的情况。这时又是缓存中没有数据,数据库中有数据的情况了。

如何解决缓存雪崩问题

解决方案大致有以下几种:

  • 数据预热 为了防止缓存雪崩可以提前对热点数据进行预热,这样可以避免项目一上线就面对大量请求,而缓存中又没有对应数据的情况。数据预热的含义就是在正式部署之前,把可能的数据预先访问一遍,手动触发加载缓存不同的key,这样部分可能大量被访问的数据就会加载到缓存中。
  • 错峰过期 错峰过期防止缓存雪崩最简单的预防手段,也就是说,在设置 key 过期时间的时候,在加上一个短的随机过期时间,使得缓存失效时间尽量的均匀,这样就能避免大量缓存在同一时间过期引起的缓存雪崩。
  • 做二级缓存 A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。
  • 限流降级 这个解决方案的思想是:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等或者直接进行降级。
  • redis高可用 缓存雪崩有一种极端情况,就是所有的Redis服务器都无法对外提供服务,针对这种情况,我们要做就是提高Redis的高可用性。高可用的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。

什么是数据双写

如何解决数据双写问题

使用缓存可以提升性能、缓解数据库压力,但是使用缓存也会导致数据不一致性的问题。为了解决缓存和数据库一致性问题,一般有有三种经典的模式:

  • Cache-Aside Pattern 即旁路缓存模式,它的提出是为了尽可能地解决缓存与数据库的数据不一致问题。此种模式分为:读流程写流程读流程:读的时候,先读缓存,如果缓存命中,直接返回数据;如果缓存没有命中,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应。 写流程:更新的时候,先更新数据库,然后再删除缓存。
  • Read-Through/Write through Read/Write Through模式中,服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互,都是通过抽象缓存层完成的。此种模式分为:读流程写流程读流程:从缓存读取数据,读到直接返回;如果读取不到,从数据库加载,写入缓存后,再返回响应。这个流程和Cache-Aside Pattern模式很像,其实Read-Through就是多了一层Cache-Provider。实际上,Read-Through只是在Cache-Aside之上进行了一层封装,它会让程序代码变得更简洁,同时也减少数据源上的负载。 写流程:Write-Through模式下,当发生写请求时,也是由缓存抽象层完成数据源和缓存数据的更新。
  • Write behind Write behind跟Read-Through/Write-Through有相似的地方,都是由Cache Provider来负责缓存和数据库的读写。它两又有个很大的不同:Read/Write Through是同步更新缓存和数据的,Write Behind则是只更新缓存,不直接更新数据库,通过批量异步的方式来更新数据库。

三种模式的比较

  • Cache Aside 更新模式实现起来比较简单,但是需要维护两个数据存储:一个是缓存(Cache),一个是数据库(Repository)。
  • Read/Write Through 的写模式需要维护一个数据存储(缓存),实现起来要复杂一些。
  • Write Behind Caching 更新模式和Read/Write Through 更新模式类似,区别是Write Behind Caching 更新模式的数据持久化操作是异步的,但是Read/Write Through 更新模式的数据持久化操作是同步的。
  • Write Behind Caching 的优点是直接操作内存速度快,多次操作可以合并持久化到数据库。缺点是数据可能会丢失,例如系统断电等。

Cache-Aside的问题

事实上,我们实际开发中使用最多的是Cache-Aside Pattern模式,接下来我们就来相信分析一下使用Cache-Aside Pattern模式可能遇到的问题。

问题一:更新数据的时候,Cache-Aside是删除缓存呢,还是应该更新缓存? 答案是删除缓存而是不是更新缓存。我们看个例子,假设有两个线程A和B,同时进行写操作。A线程先更新了数据库,但是由于网络的原因,此时B线程又更新了数据库,同时更新了缓存,最后A线程才更新缓存,这种场景下就出现了数据库数据和缓存数据不一致的场景。如果是删除缓存取代更新缓存则不会出现这个脏数据问题。 更新缓存相对于删除缓存,还有两点劣势:

  • 如果你写入的缓存值,是经过复杂计算才得到的话,更新缓存频率高的话,就会很浪费性能。
  • 在写多读少的情况下,数据很多时候还没被读取到,又被更新了,这也浪费了性能(实际上,写多的场景,用缓存也不是很划算了)

问题二:双写的情况下,先操作数据库还是先操作缓存? 答案是先操作数据库。举例来说,假设有A、B两个请求,请求A做更新操作,请求B做查询读取操作,如果是先操作缓存,两个线程的操作顺序可能会出现如下场景:

  1. 线程A发起一个写操作,第一步删除缓存
  2. 此时线程B发起一个读操作,发现缓存中没有数据
  3. 线程B继续读DB,读出来一个老数据
  4. 然后线程B把老数据设置入缓存
  5. 线程A写入DB最新的数据

这种场景下就出现了数据库是新数据,但是缓存是老数据,这就导致了数据不一致的情况。

保证数据双写一致性的方案

大致有三种方案保证Redis和DB数据一致性:延时双删策略删除缓存重试机制读取biglog异步删除缓存。这里所讲的方案和上面所讲的思想其实可以认为是互补的,这三个方案也包含不同的设计思想,但它们更倾向于具体的逻辑实现。

延时双删策略

所谓延迟双删就是先删除缓存,再更新数据库,休眠一会,再次删除缓存。 弊端:结合双删策略+缓存超时设置,这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

问题一:休眠时间如何确定? 答:休眠的时间是根据自己项目的读数据业务逻辑的耗时来确定的。这样做主要是为了保证在写请求之前确保读请求结束,写请求可以删除读请求造成的缓存脏数据。 问题二:如果使用的读写分离架构该怎么办? 答:其实仍然可以使用延时双删策略,只是休眠时间修改为在主从同步的延时时间基础上,加几百ms即可。 问题三:采用这种同步淘汰策略,吞吐量降低怎么办? 答:可以将第二次删除通过异步方式实现。也就是自己起一个线程,异步删除,这样写的请求就不用沉睡一段时间后再返回。这么做,加大吞吐量。

删除缓存重试机制

不管是延时双删还是Cache-Aside的先操作数据库再删除缓存,如果第二步的删除缓存失败呢?如果删除缓存失败,那同样会出现数据不一致的情况。这时可以引入删除缓存重试机制来解决这个问题,大致步骤如下:

  • 写请求更新数据库
  • 缓存因为某些原因,删除失败
  • 把删除失败的key放到消息队列
  • 消费消息队列的消息,获取要删除的key
  • 重试删除缓存操作

读取biglog异步删除缓存

删除缓存重试机制可以解决删除缓存失败带来的数据不一致的问题,但是会造成很多业务代码入侵。其实,还可以通过数据库的binlog来异步淘汰key。以mysql为例,可以使用阿里的canal将binlog日志采集发送到MQ队列里面,然后编写一个简单的缓存删除消息者订阅binlog日志,根据更新log删除缓存,并且通过ACK机制确认处理这条更新log,保证数据缓存一致性。

其实,大家很容易看出来读取biglog异步删除缓存其实就是删除缓存重试机制的另一种实现方式。

这里需要注意主从延迟带来的问题,如果一个读请求在写请求更新完主库同时删除了缓存数据后进来了,但此时主从复制有延迟导致从库中还是老的数据,这时读请求就会到从库中拿到过期的数据更新到缓存中,这仍然会造成数据不一致。这种情况的解决方案是:读取从库的日志。有同学可能会有疑问,如果一个主库有多个从库,我们该订阅哪个从库的日志呢?要想解决这个疑问我们就需要了解开源软件canel的原理:canal是一个伪装成slave订阅mysql的binlog,实现数据同步的中间件。

本文为互联网自动采集或经作者授权后发布,本文观点不代表立场,若侵权下架请联系我们删帖处理!文章出自:https://blog.csdn.net/qq_38571892/article/details/122453171
-- 展开阅读全文 --
Web安全—逻辑越权漏洞(BAC)
« 上一篇 03-13
Redis底层数据结构--简单动态字符串
下一篇 » 04-10

发表评论

成为第一个评论的人

热门文章

标签TAG

最近回复