缓存进阶使用指南

​ 在目前大多数系统中,数据库还无法很好的支持横向扩展,所以在高流量下,都要依靠缓存来抗住高并发的访问请求,因此缓存就成为了一个至关重要的组件,对于开发人员来讲,如何正确使用缓存也变得至关重要。


1 缓存的选型

目前在我们项目开发过程中,缓存(这里特指redis)的使用场景主要有以下三种

  • 单纯做缓存
  • 处理业务逻辑,例如利用其高性能读写,实时累加用户停留时长、保存用户最近浏览数据
  • 分布式锁

以上三种场景所要求的中间件能力其实是有很大不同的。

纯缓存场景:需要具有超高可用(4个9)、超高性能,并不需要具有数据持久化能力。

业务处理场景:需要具有高可用、可靠存储(数据不丢失)、高性能。

分布式锁:需要具有高可用、可靠存储(数据不丢失)。

而目前不管是哪一种redis集群模式,都是明显无法同时具备以上三种场景的要求的。所以当业务发展到一定程度,有了更多的系统性能要求,往往会将缓存拆成三个组件分别用于以上三种场景。

  • 纯缓存:可以直接使用redis-cluster,这时候也可以把redis的持久化日志功能给关闭,进一步的提升性能

  • 业务处理场景:可靠存储的缓存中间件目前并没有好用的开源推荐,所以在很多中小型公司这块依然会使用redis去存储。

    但是在这种场景下使用redis就不要无脑使用了,需要开发人员思考一些持久化的对策。

    比方说存储用户的在线时长,可以每天把用户昨日的累积时长持久化到数据库中(历史数据不会再变化了,做冷热分离),今日一直在变化的时长用redis做处理,这样丢数据也只会丢今天的数据。

    再比方说,用户的实时排行榜,可以采用定时任务的方式,读取数据库中的数据生成排行榜刷到缓存中,来防止缓存中数据丢失或者数据不一致的问题。

  • 分布式锁:可以选用zookeeper,zk天生就支持分布式,并且数据一致性也做的比较好(就是性能差)。而redis中的redLock官方都不建议使用,也有些公司直接使用的单节点redis当做分布式锁组件,来避免分布式下的问题。

2 缓存挂了,卷铺盖走人的故障

缓存目前可以说是我们后端中最重要的中间件之一了,特别是在一些高流量的场景下,会对开发人员提出更高的正确使用缓存的要求。

以下这些使用关注点如果都能清楚认知,会让其他的开发人员感觉你是一个非常专业靠谱的人~

2.1 缓存限流

很多人并没有对缓存有限流的意识,缓存也是有自己的处理能力上限的,如果连缓存都承受不住用户的流量,那么可想而知,这些请求直接到数据库层面上会有什么后果。

所以对缓存要压测,要设置限流,一定要保障缓存的高可用

2.2 缓存穿透

在正常的业务处理中,当查询到数据后才会向缓存写入数据,查询不到的话就不会放,来进行一个数据一致性的保障。

但是,这种处理会带来一个问题,如果有人使用数据库中不存在的数据频繁访问,那么缓存层相当于没有起到作用,这就是缓存穿透。

我个人总结了如下需要防缓存穿透的典型场景

  1. 首页配置信息(banner、弹窗、配置项、排行榜)

    首页的信息可以说是访问量最大的接口之一,假设有个配置项关闭或者删掉了,那就相当于所有流量不断的在请求数据库,后果就是直接走人~

    这类信息的另一类特点就是数据时效性不敏感,不需要更改以后就要立即生效,我配个banner,等个十分钟再看到也不是不可以。

    处理方式就比较简单一些,一定要缓存空值,并设置一个短时间的有效期即可。

  2. 访问已下架(不存在)的商品详情或者店铺详情

    这种情况也是要把空值缓存起来的,但是有一个细节点,要在给前端调用的http接口上做缓存,而不应该在内部查询商品详情做缓存。

    至于数据时效性,如果要追求高时效,可以在商品上架时增加缓存删除逻辑。

2.3 缓存击穿

缓存击穿是指在缓存过期的瞬间,有高并发的请求全涌过来直接进入数据库查询,这种场景我还真就碰到了一次,这次我直接奉上真实案例。

我们有一个排行榜的数据,当时的做法是直接查库的(要查7-10S),缓存是设置了六小时的过期时间,然后结果刚过期后的几秒钟内,有上百个请求过来,数据库直接报警,幸好访问量还是小,没有把数据库打挂酿成严重后果。

请求频次高业务场景都是有必要做缓存击穿防范的

对于缓存击穿有以下两种解决方法,

  1. 缓存不设置有效期,在数据更新时,直接更新缓存。我当时排行榜的解决方案就是用的这种,我起了个定时任务定时去刷新缓存数据,缓存不过期就没有击穿的风险。

  2. 在缓存失效去查询数据库时,增加拦截,只让部分请求可以去访问数据库,其他请求返回空值或者报错处理。

    只让部分请求通过可以使用本地锁的方式去实现。

2.4 缓存雪崩

举个例子,店铺详情的缓存过期时间统一设置为当日0点,如果这个时候正好大促开始,大批量的店铺都会被人所访问,这时候防止了缓存穿透依然无法解决问题,这就会产生缓存雪崩。

但是在实际场景中,缓存雪崩的场景太难碰到了,谁会把缓存设置成一个统一的过期时间。。

所以该问题基本可以忽略。

2.5 热点key

目前来看,对缓存做限流的话更多的是对整个集群做限流。

缓存集群通常会有分区的设计,而分区就会引发数据访问不均衡的问题出现,比如说有的店是旗舰店访问量大,而有的只是淘宝小店,几乎没有什么访问量,这些访问量大的店铺在缓存系统中就是热点key。

热点key会造成该key所在的分区负载很高,甚至超出了自己所能承受的负载量直接挂掉,比如说每年都要挂上一次的某博,而对热点key的处理其实是要分两个阶段的。

第一是首先要能发现热点key,有些热点key是我们可以提前预知的,而更多的热点key是开发、运营人员无法感知到的,谁知道明天会突然冒出什么大新闻然后就成热点key了,所以感知热点key是解决该问题最重要的一环。目前比较好的做法时在缓存中间件上加埋点日志去实时统计来感知

感知到了热点key之后就是解决了,我目前见过很有效的做法有两种

  • 缓存客户端发现热点key以后,改为使用本地缓存来降低缓存的读写。
  • 如果能提前感知到热点key的出现,比如说热门店铺,那么对这些热门店铺的缓存key进行特殊处理,比方说再hash让缓存再次分散,或者对这些商家特殊放置到一个性能更高的缓存集群里。

2.6 大key

大key问题的话可能我们服务端人员就比较少见了,但是我之前见过有些整个的静态网页数据是直接缓存到redis中的(nginx可以直接配redis),那一个网页下来可能有几MB的大小。

大key问题同样会造成单个分区出现负载过高的情况,并且redis是使用单线程处理读写的,对于大key处理速度会明显下降从而造成其他缓存key的读取速度下降。

对于大key的发现,像redis就自带了bigkey命令(但是其使用scan命令来扫描所有key,可能会导致redis性能下降),可以用于分析当前存在的大key。更好的发现大key的手段是使用缓存导出快照进行扫描,因为大key的感知不像热点key那样要求实时性,可以过一段时间再去感知到。

而对大key的处理基本上手段都只有一种,就是拆,一个key的缓存内容拆成多个,比方说之前提到的网页缓存就可以拆成不同的内容来降低单个key的大小。

(这里提一个场景大家可以去思考下,如果我一个list的key有百万个元素,要怎么拆才能方便读写呢🤔)

3 展示自己的价值–别忘记了本地缓存

我觉得这个可以说是很重要的一节,所以我放到了压轴来讲。

就拿本节的主题来讲,一个开发人员在系统中使不使用本地缓存,本质上是没有影响的,无非就是用本地缓存性能会高一点,甚至这些性能我可以靠扩容缓存集群来解决吗。

但是这也正是我们开发人员的价值所在,我可以在技术层面上,在硬件条件不变的情况上,使用本地缓存来提升系统的性能。做到这种事情,一说出去就是脸上有光的事情,而且考核的时候也更好展现价值不是。

能有效展示自己的价值,是很重要的软实力之一,这可能会要求我们在开发过程中,去做一些很复杂的设计去应对可能不会发生的场景。但是没有办法,我们不能改变它,就只能去适应