缓存架构的设计和常见问题

Posted by ChenJY on February 23, 2019 | Viewed times
本站图床基于新浪微博,图片加载异常请强制刷新或直接访问语雀空间查阅文章备份

缓存架构的设计

缓存层通常架设在 DB 和业务逻辑层之间,核心功能就是从DB生成一条缓存数据,可以在后续若干次查询请求过程中不再访问DB,请求在缓存层直接命中后返回,从而可以达到加快访问速度的作用,同时也能在流量暴增时充当保护层防止DB被打垮。

缓存的引入虽然有优势,但也会带来一些问题,例如缓存穿透、缓存雪崩等,所以哪些场景适合使用缓存需要斟酌。读多写少的场景是很适合的,可以显著降低 DB 的压力;同样,需要进行计数统计的场景,例如访问量等也是适合的,直接内存统计后异步落盘,实时性更好。

缓存穿透

缓存层没有命中数据,从而需要到DB查询,然后将返回结果写入缓存再返回。上述是理想条件,如果DB也没有查询到,那么下一次请求依然不命中缓存,还是会达到DB。因此在流量大的时候,如果缓存迟迟没有数据命中,很可能将 DB 拖垮

解决方法可以是一旦 DB 查询为空,那么也往缓存中写入一条记录,只不过 value 是配置的默认值或者是空值,并设置过期时间,这样的好处是过期时间之内的请求都可以直接在缓存层返回,不会再打到 DB,一旦过期时间到了,再次去查询 DB,此时可能 DB 中已经有数据了(因为其他服务更新了),再将其写入缓存。本质上是一种削峰的思想。

缓存雪崩

缓存的值的过期时间设置得相近,导致在某个时间点,大量缓存同时过期,导致大批量请求在缓存层无法命中,全部打到 DB,从而造成大量查询。

解决的办法就是根据所需要的业务场景,可以让过期时间增加一个随机值,使得过期时间互相错开,防止同一时间大量过期的情况产生。

并发更新

一个缓存值失效,导致大量线程短时间内都去查询 DB 然后频繁更新缓存,因为这段时间内谁都不知道对方已经在查询 DB 更新缓存的路上了。

解决方法自然是加锁,限制缓存不命中后仅能允许一个线程去查询 DB 然后更新缓存的值,其他线程获取锁失败可以直接返回默认值,或者是等待,视具体场景而定。在分布式系统场景下,还要引入分布式锁来解决。

缓存热点

请求大量集中在某些数据、某个分片上,导致 QPS 奇高,给节点带来压力,如果你熟悉哈希算法的话,其实这时候简单的扩容机器也无法接近热点。

缓存热点的出现很常见,通常分为“可预知”的和“不可预知”两种,例如双十一这种就是可以预知的,那么缓存可以提前进行预热,或者将部分资源提前下载至本地,引入 CDN 等等,确保到时真正到达缓存层的请求没有那么大,这种处理方式其实就不是单纯地涉及缓存了,而是如何应对大流量了,各个系统都有涉及;

微博热点就属于后者,完全不可预知(我也好奇具体微博怎么处理的),这时的解决办法,我思考下来是在缓存层前方加长响应链路,这些链路中可以做流量预警、可以是历史内容缓存,总之期望这些突发的流量能被今早发现,然后链路进行延缓,帮助缓存层进行扩容、多副本等操作,之后到达缓存层。

一般情况下,热点可以通过多副本,代码中将 Key 打散在多个分片上平摊流量压力来解决,问题是更新缓存时怎么去做?如果其中一个副本更新失败怎么办,这就需要业务方自己去权衡应用场景是否适合这样做,也对业务代码改造带来一定的复杂性;或者也可以将更新不频繁的数据直接缓存在业务机器本地,短时间内不再去查询缓存。

一些面试问题

  1. 如何解决 DB 和缓存的一致性 我并不赞成一上来就是 “双写”、“MySQL binlog 回填” 等方法。我觉得业务首先要明确自己的场景需不需要特别强的一致性,如果是那当然只能先写 DB 然后更新缓存,无论什么时候先确保数据不丢,如果更新缓存失败可以重试,如果缓存宕机可以异步去探活再重试;如果不需要强一致,缓存仅是用来加快速度或者是做一些计数,那么先写缓存再异步刷进 DB 也未尝不可,如果这时候缓存宕机了,缓存的数据没来得及进 DB ,除了丢失一些统计数据,并不会对业务产生什么影响的话,那也可以。

License


Comment