一图把握服务端缓存架构设计要领

在程序设计中,缓存是典型的用空间换时间来获得程序性能提升的手段。对互联网服务端架构来说,缓存还是一种应对高并发的有效手段(延伸阅读《高并发架构设计》)。缓存不是简单的KV读写,引入缓存会增加全局系统架构的一致性复杂度,缓存架构设计不到位,反而会带来数据不一致问题。下图是关于服务端缓存设计的核心技术点。

 

何时需要缓存?

《高并发架构设计》一文指出当QPS大于50,将达到单机DB性能的极限,需要引入多DB负载或缓存。

多DB负载的典型技术方案是采用主从架构(延伸阅读《高可用架构设计》),实现读写分离。这种方案在一定程度上能缓解读数压力,但是对性能的提升和扩展性比较局限。第一,虽然DB也会做一些内存缓存,但是大部分情况还是需要从磁盘读数据,磁盘速度比内存速度要慢几个数量级,所以读性能提升有限。第二,从节点不能无节制扩展,因为主从架构下,为保证数据一致性,主节点通常会等收到从节点的ACK回复后才提交事务,在一定程度影响了写数据性能。

缓存基于内存空间实现,独立于DB部署,不影响存储层的架构,也不受DB架构限制,可以随并发量的提升弹性伸缩。缓存转移了大量的读请求,能对存储层起到缓冲作用。但是,缓存与DB独立的架构增加了数据一致性的复杂度。最经典的一个问题:何时更新缓存?

 

数据一致性:何时更新缓存?

缓存与DB更新数据的设计模式有四种:Read Through,Write Through,Write Behind Caching,Cache Aside。

Read / Write Through 这两种模式都是把缓存与DB看作一个存储整体,在内部解决数据一致性问题。一般存储系统在实现时都会做一些缓存设计,但由于跟存储紧耦合,无法弹性扩展。Write Behind Caching 和 Cache Aside 这两种模式把缓存与 DB 看作独立的架构体系。前者以 Cache 为主,DB为辅,数据先写Cache,再同步到DB持久化。后者正好相反,它以DB为主, Cache为辅。Write Behind Caching 模式虽然性能很好,但存在丢数风险。互联网服务端的缓存架构主要选择 Cache Aside 模式。Cache 的数据一致性问题就是怎样保证在写DB后,Cache中的数据与DB达成一致。

单机DB中,事务的ACID特性可以保证数据的强一致性。分布式DB,无论主从还是主备模式,都可以由主节点做全局协调,保证数据的最终一致性。但是要跨越DB和缓存两个独立的架构体系,实现数据一致性就没那么简单了。大部分场景下,我们不会用2PC、TCC等强一致性方案,因为这种方案会牺牲可用性,而且严重影响写数性能。那最终一致性该怎样实现呢?

先看下这两种更新策略:

  • 写缓存 - 写DB:有问题。写缓存成功,写DB失败。

  • 写DB - 写缓存:有问题。写DB成功,写缓存失败,缓存还是老数据。

     

怎么写缓存都有问题,难道不写缓存?没错,就是不写。但不等于不处理缓存,而是只删除缓存。

  • 删缓存 - 写DB:删成功,写失败,多一次缓存miss而已,可行。

  • 写DB - 删缓存:有问题。写成功,删失败,缓存还是老数据。

     

看来只能先删缓存再写DB了。别急,这个策略真的完全没问题吗?看下面这个例子。

 

两个并发请求Q1和Q2,Q1采用了先删缓存再写DB策略。不巧的是,Q2在中间时间点发现缓存miss又读了DB老数据更新了缓存,导致缓存的还是老数据。那该怎么办呢?保险起见,Q1在写DB之后再做一次删缓存,也就是删缓存-写DB-删缓存。但第二次删缓存还是有可能失败。所以还需要一个兜底方案:设置缓存过期时间。

缓存过期的时间一般不会设得太短,比如设置在分钟、小时甚至天级别。这个兜底方案的不一致时间显然太长了。不过删缓存-写DB-删缓存这个策略虽然在理论上还是会存在不一致问题,但实际发生的概率非常低。如果业务场景无法接受,还有其他的工程优化方案,比如产品设计上增加手动更新按钮,对第二次删缓存失败增加监控再处理机制。对于一致性要求更高的场景还可以采用CP模型实现强一致性。

 

缓存的过期实现

缓存的过期删除不仅是保证数据一致性的兜底方案,也是节省内存资源的必要手段。主流的缓存中间件如Redis、Memcache都提供了过期实现,实际应用中只需调相关API即可。但是,作为有追求的程序员,还是应该了解一下缓存过期具体是怎么实现的。这里以普遍使用的Redis中间件为例,看一下它是如何实现缓存过期删除的。

Redis删除过期key有两种方式:定时删除惰性删除。定时删除由一个线程定期(默认100ms)从缓存中随机筛选一批key进行检查,如过期则删除。显然这样无法保证所有过期key都按时删除。惰性删除是在访问到某个key时再进行过期检查。即使加了惰性删除,对于那些长时间不被访问到过期 key 依然无法及时删除。为什么定时删除不把所有过期key都删除呢?原因是这样做会影响redis本身的服务性能。试想一下,每隔100ms全 key 扫描一遍是多么可怕的事情。

不能及时删除过期 key,那当内存不足时,岂不是会影响新key的写入?没错,所以还需要有内存淘汰机制。可通过 maxmemory-policy 参数配置Redis的内存淘汰策略,共有以下6种:

  • volatile-lru:从已设置过期时间的内存数据集中挑选最近最少使用的数据 淘汰

  • volatile-ttl: 从已设置过期时间的内存数据集中挑选即将过期的数据 淘汰

  • volatile-random:从已设置过期时间的内存数据集中任意挑选数据 淘汰

  • allkeys-lru:从内存数据集中挑选最近最少使用的数据 淘汰

  • allkeys-random:从数据集中任意挑选数据 淘汰

  • no-enviction:不淘汰数据(默认淘汰策略。当redis内存数据达到maxmemory,在该策略下,直接返回OOM错误)

 

缓存雪崩问题

缓存雪崩问题是指大批key在同一时间失效,导致这个时间点缓存对DB没有起到很好的缓冲保护,造成DB高并发访问的问题。通常只有对存量数据进行全量缓存才会出现雪崩问题。简单处理方案就是在缓存过期设置时加上一段随机时间,错开各key的过期时间,或者在缓存过期之前,提前更新一遍。复杂但更稳定的方案是对缓存miss读DB时加锁排队。

 

缓存穿透问题

缓存穿透是指访问缓存中不存在的key,对DB造成请求压力。正常的业务逻辑出现缓存miss会及时用DB数据更新,不会出现高并发的穿透问题。如果出现,大概率是被构造的非法请求进行网络攻击了。所以首先要解决的问题是如何屏蔽掉非法key。解法是使用布隆过滤器(Bloom Filter)

布隆过滤器本质上是一种概率型数据结构,它可以告诉你 “某样东西一定不存在或者可能存在”。详细的原理这里不赘述,可以自行百度。过滤非法key正是使用布隆过滤器可以高效地判断某个key一定不存在这个特性。

除了非法key攻击外,在一些正常的业务逻辑也会出现穿透,比如缓存雪崩。对于合法key的缓存穿透该如何处理呢?一种方法是像雪崩问题解法那样对缓存miss读DB时加锁排队。另一种更简单的方案是缓存一个过期时间比较短的空值,可使用JVM本地缓存实现。

 

热点缓存问题

热点缓存是指缓存集群中某些热点key承载巨大并发量,造成集群局部高并发。这类情况还是比较常见的,比如秒杀商品、热点新闻等。本质上这是由于数据的热度不同,造成的负载失衡。破解之法是首先识别出热点缓存。可以通过实时监控统计来感知热点key。

捕获到热点key之后,后续的优化方案就比较多了。比如,可以将热点key加载为JVM本地缓存,减少对缓存集群的并发量。还可以通过再hash的方式,把热点key离散成多个key来平衡集群负载。此外,还可以采用服务稳定性治理的常规手段如限流、熔断机制来进行流量控制。

 

延伸阅读

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值