缓存

缓存一致性、缓存击穿、缓存雪崩

缓存一致性是指业务在引入分布式缓存系统后,业务对数据的更新除了要更新存储以外还需要同时更新缓存,对两个系统进行数据更新就要先解决分布式系统中的隔离性和原子性难题。目前大多数业务在引入分布式缓存后都是通过牺牲小概率的一致性来保障业务性能,因为要在业务层严格保障数据的一致性,代价非常高,业务引入分布式缓存主要是为了解决性能问题,所以在性能和一致性面前,通常选择牺牲小概率的一致性来保障业务性能。

缓存击穿是指查询请求没有在缓存层命中而将查询透传到存储 DB 的问题,当大量的请求发生缓存击穿时,将给存储 DB 带来极大的访问压力,甚至导致 DB 过载拒绝服务。空数据查询(黑客攻击)和缓存污染(网络爬虫)是常见的引发缓存击穿的原因。什么是空数据查询?空数据查询通常指攻击者伪造大量不存在的数据进行访问(比如不存在的商品信息、用户信息)。缓存污染通常指在遍历数据等情况下冷数据把热数据驱逐出内存,导致缓存了大量冷数据而热数据被驱逐。缓存污染的场景我们目前还没有发现较好的解决方案,但是在空数据查询问题上我们可以改造业务,通过以下方式防止缓存击穿:

  • 通过 bloomfilter 记录 key 是否存在,从而避免无效 Key 的查询;
  • 在 Redis 缓存不存在的 Key,从而避免无效 Key 的查询;

缓存雪崩是指由于大量的热数据设置了相同或接近的过期时间,导致缓存在某一时刻密集失效,大量请求全部转发到 DB,或者是某个冷数据瞬间涌入大量访问,这些查询在缓存 MISS 后,并发的将请求透传到 DB,DB 瞬时压力过载从而拒绝服务。目前常见的预防缓存雪崩的解决方案,主要是通过对 key 的 TTL 时间加随机数,打散 key 的淘汰时间来尽量规避,但是不能彻底规避。

缓存读取

缓存更新

数据库的分布式事务的问题,需要处理数据可靠性的挑战,并发更新带来的隔离性挑战,和数据更新原子性的挑战。

数据可靠性

  • 先更新成功存储,再更新缓存;
  • 先更新成功缓存,再跟新存储,如果存储更新失败,删除缓存;
  • 操作隔离性。

一条数据的更新涉及到存储和缓存两套系统,如果多个线程同时操作一条数据,并且没有方案保证多个操作之间的有序执行,就可能会发生更新顺序错乱导致数据不一致的问题。

更新原子性

业务层缓存更新方案

  • Step1:更新存储,保证数据可靠性;
  • Step2:更新缓存,2 个策略怎么选:
  • 惰性更新:删除缓存,等待下次读 MISS 再缓存(推荐方案);
  • 积极更新:将最新的值更新到缓存(不推荐);

外部组件更新缓存

缓存 MISS 处理方案

  • 第一:需要监控存储的日志,或者通过 Triger 来监控存储数据的变更,需要对存储系统非常熟悉;
  • 第二:需要对更新进行过滤,我们的目的是缓存热数据,但是像 DDL、批量更新这一系列的操作是不需要更新缓存的,要把非业务更新操作过滤;
  • 第三:同步组件需要理解数据,不通用;
  • 先更新存储,由第三方组件异步更新缓存;

其他缓存更新方案 在实际的生产中,我们还会看到很多先更新缓存,然后通过第三方组件更新存储的场景,但是这个方案也会面临数据一致性和数据可靠性的挑战,虽然不推荐,但是确实还是能看到有在使用这个方案的,我们拿出来探讨下。

  • 这个场景数据可靠性,不及先更新存储的方案,但是写入性能高,延迟低;
  • 这个方案 APP 和第三方组件都会更新 Cache,会存在数据一致性的问题,因为很难保障两个组件更新的时序。

缓存淘汰

  • 主动淘汰,这是推荐的方式,我们通过对 Key 设置 TTL 的方式来让 Key 定期淘汰,以保障冷数据不会长久的占有内存。TTL 的策略可以保证冷数据一定被淘汰,但是没有办法保障热数据始终在内存,这个我们在后面会展开;
  • 被动淘汰,这个是保底方案,并不推荐,Redis 提供了一系列的 Maxmemory 策略来对数据进行驱逐,触发的前提是内存要到达 maxmemory(内存使用率 100%),在 maxmemory 的场景下缓存的质量是不可控的,因为每次缓存一个 Key 都可能需要去淘汰一个 Key。