Redis缓存设计

缓存设计

什么是缓存击穿、缓存穿透、缓存雪崩?

缓存击穿

一个并发访问量比较大的key在某个时间过期,导致所有的请求直接打在DB上。

缓存击穿


解决⽅案:

  1. 加锁更新,⽐如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写⼊缓存,再返回给⽤户,这样后⾯的请求就可以从缓存中拿到数据了。

    加锁更新

  2. 将过期时间组合写在value中,通过异步的⽅式不断的刷新过期时间,防⽌此类现象。

缓存穿透

缓存穿透指的查询缓存和数据库中都不存在的数据,这样每次请求直接打到数据库,就好像缓存不存在一样。

缓存穿透


缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。

缓存穿透可能会使后端存储负载加大,如果发现大量存储层空命中,可能就是出现了缓存穿透问题。

缓存穿透可能有两种原因:

  1. 自身业务代码问题
  2. 恶意攻击,爬虫造成空命中

它主要有两种解决办法:

  • 缓存空值/默认值

一种方式是在数据库不命中之后,把一个空对象或者默认值保存到缓存,之后再访问这个数据,就会从缓存中获取,这样就保护了数据库。

缓存空值/默认值

缓存空值/默认值

缓存空值有两大问题:

  1. 空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法是针对这类数据设置一个较短的过期时间,让其自动剔除。

  2. 缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。
    例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致。
    这时候可以利用消息队列或者其它异步方式清理缓存中的空对象。

  • 布隆过滤器
    除了缓存空对象,我们还可以在存储和缓存之前,加一个布隆过滤器,做一层过滤。

布隆过滤器里会保存数据是否存在,如果判断数据不不能再,就不会访问存储。
 

布隆过滤器


两种解决方案的对比:

缓存空对象核布隆过滤器方案对比

缓存雪崩

某⼀时刻发⽣⼤规模的缓存失效的情况,例如缓存服务宕机、大量key在同一时间过期,这样的后果就是⼤量的请求进来直接打到DB上,可能导致整个系统的崩溃,称为雪崩。

缓存雪崩


缓存雪崩是三大缓存问题里最严重的一种,我们来看看怎么预防和处理。

  • 提高缓存可用性
  1. 集群部署:通过集群来提升缓存的可用性,可以利用Redis本身的Redis Cluster或者第三方集群方案如Codis等。
  2. 多级缓存:设置多级缓存,第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。
  • 过期时间
  1. 均匀过期:为了避免大量的缓存在同一时间过期,可以把不同的 key 过期时间随机生成,避免过期时间太过集中。
  2. 热点数据永不过期。
  • 熔断降级
  1. 服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统。
  2. 服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的 fallback(退路)错误处理信息。

能说说布隆过滤器吗?

布隆过滤器,它是一个连续的数据结构,每个存储位存储都是一个bit,即0或者1, 来标识数据是否存在。

存储数据的时时候,使用K个不同的哈希函数将这个变量映射为bit列表的的K个点,把它们置为1。

布隆过滤器

我们判断缓存key是否存在,同样,K个哈希函数,映射到bit列表上的K个点,判断是不是1:

  • 如果全不是1,那么key不存在;
  • 如果都是1,也只是表示key可能存在。

布隆过滤器也有一些缺点:

  1. 它在判断元素是否在集合中时是有一定错误几率,因为哈希算法有一定的碰撞的概率。
  2. 不支持删除元素。

如何保证缓存和数据库数据的⼀致性?

根据CAP理论,在保证可用性和分区容错性的前提下,无法保证一致性,所以缓存和数据库的绝对一致是不可能实现的,只能尽可能保存缓存和数据库的最终一致性。

选择合适的缓存更新策略

1. 删除缓存而不是更新缓存

当一个线程对缓存的key进行写操作的时候,如果其它线程进来读数据库的时候,读到的就是脏数据,产生了数据不一致问题。

相比较而言,删除缓存的速度比更新缓存的速度快很多,所用时间相对也少很多,读脏数据的概率也小很多。

删除缓存和更新缓存

  1. 先更数据,后删缓存
    先更数据库还是先删缓存?这是一个问题。

更新数据,耗时可能在删除缓存的百倍以上。在缓存中不存在对应的key,数据库又没有完成更新的时候,如果有线程进来读取数据,并写入到缓存,那么在更新成功之后,这个key就是一个脏数据。

毫无疑问,先删缓存,再更数据库,缓存中key不存在的时间的时间更长,有更大的概率会产生脏数据。

先更数据库还是先删缓存

目前最流行的缓存读写策略cache-aside-pattern就是采用先更数据库,再删缓存的方式。

缓存不一致处理

如果不是并发特别高,对缓存依赖性很强,其实一定程序的不一致是可以接受的。

但是如果对一致性要求比较高,那就得想办法保证缓存和数据库中数据一致。

缓存和数据库数据不一致常见的两种原因:

  • 缓存key删除失败
  • 并发导致写入了脏数据

缓存一致性

缓存一致性

消息队列保证key被删除
可以引入消息队列,把要删除的key或者删除失败的key丢尽消息队列,利用消息队列的重试机制,重试删除对应的key。

消息队列保证key被删除

这种方案看起来不错,缺点是对业务代码有一定的侵入性。

数据库订阅+消息队列保证key被删除
可以用一个服务(比如阿里的 canal)去监听数据库的binlog,获取需要操作的数据。

然后用一个公共的服务获取订阅程序传来的信息,进行缓存删除操作。
 

数据库订阅+消息队列保证key被删除


这种方式降低了对业务的侵入,但其实整个系统的复杂度是提升的,适合基建完善的大厂。

延时双删防止脏数据
还有一种情况,是在缓存不存在的时候,写入了脏数据,这种情况在先删缓存,再更数据库的缓存更新策略下发生的比较多,解决方案是延时双删。

简单说,就是在第一次删除缓存之后,过了一段时间之后,再次删除缓存。

延时双删

延时双删

这种方式的延时时间设置需要仔细考量和测试。

设置缓存过期时间兜底

这是一个朴素但是有用的办法,给缓存设置一个合理的过期时间,即使发生了缓存数据不一致的问题,它也不会永远不一致下去,缓存过期的时候,自然又会恢复一致。

如何保证本地缓存和分布式缓存的一致?

PS:这道题面试很少问,但实际工作中很常见。

在日常的开发中,我们常常采用两级缓存:本地缓存+分布式缓存。

所谓本地缓存,就是对应服务器的内存缓存,比如Caffeine,分布式缓存基本就是采用Redis。

那么问题来了,本地缓存和分布式缓存怎么保持数据一致?
 

延时双删


Redis缓存,数据库发生更新,直接删除缓存的key即可,因为对于应用系统而言,它是一种中心化的缓存。

但是本地缓存,它是非中心化的,散落在分布式服务的各个节点上,没法通过客户端的请求删除本地缓存的key,所以得想办法通知集群所有节点,删除对应的本地缓存key。

本地缓存/分布式缓存保持一致

可以采用消息队列的方式:

  1. 采用Redis本身的Pub/Sub机制,分布式集群的所有节点订阅删除本地缓存频道,删除Redis缓存的节点,同事发布删除本地缓存消息,订阅者们订阅到消息后,删除对应的本地key。
    但是Redis的发布订阅不是可靠的,不能保证一定删除成功。
  2. 引入专业的消息队列,比如RocketMQ,保证消息的可靠性,但是增加了系统的复杂度。
  3. 设置适当的过期时间兜底,本地缓存可以设置相对短一些的过期时间。

怎么处理热key?

什么是热Key?
所谓的热key,就是访问频率比较的key。

比如,热门新闻事件或商品,这类key通常有大流量的访问,对存储这类信息的 Redis来说,是不小的压力。

假如Redis集群部署,热key可能会造成整体流量的不均衡,个别节点出现OPS过大的情况,极端情况下热点key甚至会超过 Redis本身能够承受的OPS。

怎么处理热key?

热key处理


对热key的处理,最关键的是对热点key的监控,可以从这些端来监控热点key:

  1. 客户端
    客户端其实是距离key“最近”的地方,因为Redis命令就是从客户端发出的,例如在客户端设置全局字典(key和调用次数),每次调用Redis命令时,使用这个字典进行记录。

  2. 代理端
    像Twemproxy、Codis这些基于代理的Redis分布式架构,所有客户端的请求都是通过代理端完成的,可以在代理端进行收集统计。

  3. Redis服务端
    使用monitor命令统计热点key是很多开发和运维人员首先想到,monitor命令可以监控到Redis执行的所有命令。

只要监控到了热key,对热key的处理就简单了:

  1. 把热key打散到不同的服务器,降低压⼒

  2. 加⼊⼆级缓存,提前加载热key数据到内存中,如果redis宕机,⾛内存查询

缓存预热怎么做呢?

所谓缓存预热,就是提前把数据库里的数据刷到缓存里,通常有这些方法:

1、直接写个缓存刷新页面或者接口,上线时手动操作

2、数据量不大,可以在项目启动的时候自动进行加载

3、定时任务刷新缓存.

热点key重建?问题?解决?

开发的时候一般使用“缓存+过期时间”的策略,既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。

但是有两个问题如果同时出现,可能就会出现比较大的问题:

  • 当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。

  • 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次IO、多个依赖等。 在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。

怎么处理呢?

要解决这个问题也不是很复杂,解决问题的要点在于:

  • 减少重建缓存的次数。
  • 数据尽可能一致。
  • 较少的潜在危险。

所以一般采用如下方式:

  1. 互斥锁(mutex key)
    这种方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。
  2. 永远不过期
    “永远不过期”包含两层意思:
  • 从缓存层面来看,确实没有设置过期时间,所以不会出现热点key过期后产生的问题,也就是“物理”不过期。
  • 从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。

无底洞问题吗?如何解决?

什么是无底洞问题?

2010年,Facebook的Memcache节点已经达到了3000个,承载着TB级别的缓存数据。但开发和运维人员发现了一个问题,为了满足业务要求添加了大量新Memcache节点,但是发现性能不但没有好转反而下降了,当时将这 种现象称为缓存的“无底洞”现象。

那么为什么会产生这种现象呢?

通常来说添加节点使得Memcache集群 性能应该更强了,但事实并非如此。键值数据库由于通常采用哈希函数将 key映射到各个节点上,造成key的分布与业务无关,但是由于数据量和访问量的持续增长,造成需要添加大量节点做水平扩容,导致键值分布到更多的 节点上,所以无论是Memcache还是Redis的分布式,批量操作通常需要从不同节点上获取,相比于单机批量操作只涉及一次网络操作,分布式批量操作会涉及多次网络时间。

无底洞问题如何优化呢?

先分析一下无底洞问题:

  • 客户端一次批量操作会涉及多次网络操作,也就意味着批量操作会随着节点的增多,耗时会不断增大。

  • 网络连接数变多,对节点的性能也有一定影响。

常见的优化思路如下:

  • 命令本身的优化,例如优化操作语句等。

  • 减少网络通信次数。

  • 降低接入成本,例如客户端使用长连/连接池、NIO等。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

1b( ̄▽ ̄)d 

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值