Redis之常见问题

缓存击穿

什么是缓存击穿

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

怎么解决缓存击穿问题

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

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

缓存穿透

什么是缓存穿透

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

如何解决缓存穿透问题

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

缓存空对象

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

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

布隆过滤器

通过布隆过滤器详解这篇文章我相信大家应该能够通过redis实现一个简单布隆过滤器了。那如何通过布隆过滤器解决缓存穿透问题呢?
其实很简单,将我们所有的redis 的key都提前加入到布隆过滤器,然后判断请求过来的key是否在集合中,如果不存在就可以认为是非法的key直接结束请求就可以了。这样就可以提早过滤掉绝大多数的请求,从而起到保护数据库的作用。

缓存雪崩

什么是缓存雪崩

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

如何解决缓存雪崩问题

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

  • 数据预热
    为了防止缓存雪崩可以提前对热点数据进行预热,这样可以避免项目一上线就面对大量请求,而缓存中又没有对应数据的情况。数据预热的含义就是在正式部署之前,把可能的数据预先访问一遍,手动触发加载缓存不同的key,这样部分可能大量被访问的数据就会加载到缓存中。

  • 错峰过期
    错峰过期防止缓存雪崩最简单的预防手段,也就是说,在设置 key 过期时间的时候,在加上一个短的随机过期时间,使得缓存失效时间尽量的均匀,这样就能避免大量缓存在同一时间过期引起的缓存雪崩。

  • 做二级缓存
    A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。

  • 限流降级
    这个解决方案的思想是:在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等或者直接进行降级。

  • redis高可用
    缓存雪崩有一种极端情况,就是所有的Redis服务器都无法对外提供服务,针对这种情况,我们要做就是提高Redis的高可用性。高可用的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。

Redis和DB数据双写

什么是数据双写

如何解决数据双写问题

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

  • 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异步删除缓存。这里所讲的方案和上面所讲的思想其实可以认为是互补的,这三个方案也包含不同的设计思想,但它们更倾向于具体的逻辑实现。

延时双删策略

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

问题一:休眠时间如何确定?
答:休眠的时间是根据自己项目的读数据业务逻辑的耗时来确定的。这样做主要是为了保证在写请求完成之前确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
可能出现场景:A写请求先删除了缓存数据,但在A还没有更新数据库之前又来了一个B读请求,且B先读到了数据库中的老数据,然后A更新了数据库,如果A休眠时间很短的话,很快A就完成休眠进行了第二次删除缓存,但此时由于B读到数据后,需要一堆计算,这样等B计算完成将数据更新进缓存时,A可能已经完成了第二次删除,这就出现了数据不一致的情况。

问题二:如果使用的读写分离架构该怎么办?
答:其实仍然可以使用延时双删策略,只是休眠时间修改为在主从同步的延时时间基础上,加几百ms即可。

问题三:采用这种同步淘汰策略,吞吐量降低怎么办?
答:可以将第二次删除通过异步方式实现。也就是自己起一个线程,异步删除,这样写的请求就不用沉睡一段时间后再返回。这么做,加大吞吐量。

删除缓存重试机制

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

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

读取biglog异步删除缓存

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

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

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

Redis中竞争并发问题

什么是Redis中竞争并发问题

这个也是线上非常常见的一个问题,就是多客户端同时并发写一个key,可能本来应该先到的数据后到了,导致数据版本错了,或者是多客户端同时获取一个key,修改值之后再写回去,只要顺序错了,数据就错了。这些场景统称为“Redis竞争并发问题”。需要说明的是,Redis竞争并发问题一般出现在Redis面对海量请求的时候,比如一秒钟有几万的读写请求,对于低请求量的应用中基本不会出现在这个问题。

如何解决Redis中竞争并发问题

从上面的描述我们可知,竞争并发问题的场景比较多,不同的场景有不同的解决方案。

场景一
有多个客户端需要同时操作keyA,但是keyA的值需要通过数据库查询后再写入缓存,这是需要注意两点:一从数据库查询和写入缓存要原子性,二多个客户端请求的顺序性。

场景二
有多个请求一起去对某个商品减库存操作:1.取出当前库存值2.计算新库存值 3.写入新库存值
。但是会出现一种错误情况:A请求获取库存为30,B请求获取库存也为30,然后A减去5,库存剩余25,B减去5,库存剩余25,此时对更新库存值的顺序并没有要求,但是很显然最后的库存值和我们期望的是不一样的。

使用分布式锁+时间戳方案

这种方案适用于对顺序有要求的场景。例如,场景一。
此时使用分布式锁+时间戳方式是可以解决的,使用分布式锁可以解决第一个点,使用时间戳可以实现顺序性,当后面的请求执行set操作时先判断时间戳,如果发现已存在值的时间戳大于自己的时间戳,就直接放弃本次修改,如果小于就直接修改。

但是这种方案要求各系统的时间是一致的,否则时间戳就没有意义了,可以使用版本号来代替。

使用Redis的watch

这种方案适用于对顺序有要求的场景。例如,场景二。
注意不要在分片集群中使用
深入理解Redis事务

使用消息队列

这种方式在一些高并发的场景中算是一种通用的解决方案。无论是否要求有序,都可以使用这种方案。

在并发量过大的情况下,可以通过消息中间件进行处理,把并行读写进行串行化。把Redis.set操作放在队列中使其串行化,必须的一个一个执行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值