likes
comments
collection
share

高并发系统-缓存(二)-如何使用缓存

作者站长头像
站长
· 阅读数 12

1. 缓存读写策略

缓存必定是对某些数据或者运算结果的存储在读写速度更高级别的存储介质中。这就会直接涉及到两个不同存储介质,也会导致一个经典的读写不一致场景。 下面主要描述缓存相关的读写策略。

1.1 Cache Aside(旁路缓存)

主要以缓存+数据库方式说明 以数据库中的数据为准,缓存中的数据是按需加载的。分为读策略和写策略。 读策略

  • 从缓存中读取数据
  • 如果缓存命中,则直接返回数据
  • 如果缓存不命中,则从数据库中查询数据
  • 查询到数据后,将数据写入到缓存中,并且返回给用户

写策略

  • 更新数据库中的记录
  • 删除缓存记录

整体流程时序如下: 高并发系统-缓存(二)-如何使用缓存

上述做法基本能够满足日常需求,但是在极端情况下还是会导致缓存和数据库数据不一致,如下图所示

高并发系统-缓存(二)-如何使用缓存 不过这种情况很极端,一般数据库的读写操作和缓存的读写操作不在一个数量级上,一般情况很难会出现读请求读取完数据后,设置A2在读写请的写数据库和删A1缓存之后才操作,除非有代码问题^_^

针对这种情况,可以采用如下两种方式

  • 双删缓存+缓存设置有效期方式来规避极端并发问题。

双删:在对数据库操作前后各删除一次缓存,数据库操作后删除缓存的时间一般比一次请求好使(上图写A1+删A1缓存时间)稍微长一些

  • 设置缓存时增加分布式锁来保证串行操作

1.2 Read/Write Through(读穿 / 写穿)

核心原则是用户只与缓存打交道,由缓存和数据库通信,写入或者读取数据。这就好比你在汇报工作的时候只对你的直接上级汇报,再由你的直接上级汇报给他的上级,你是不能越级汇报的。

Write Through: 先查询要写入的数据在缓存中是否已经存在,如果已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,如果缓存中数据不存在,我们把这种情况叫做“Write Miss(写失效)” 择两种“Write Miss”方式:一个是“Write Allocate(按写分配)”,做法是写入缓存相应位置,再由缓存组件同步更新到数据库中;另一个是“No-write allocate(不按写分配)”,做法是不写入缓存中,而是直接更新到数据库中。

Read Through : 先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库中同步加载数据。

应用场景:Guava Cache 中的 Loading Cache 就有 Read Through 策略的影子

1.3 Write Back(写回)

核心思想是在写入数据时只写入缓存,并且把缓存块儿标记为“脏”的。而脏块儿只有被再次使用时才会将其中的数据写入到后端存储中 读策略示意图

高并发系统-缓存(二)-如何使用缓存 写策略示意图

高并发系统-缓存(二)-如何使用缓存

主要应用于操作系统层面的 Page Cache、日志的异步刷盘、消息队列中消息的异步写入磁盘

2. 缓存高可用

2.1 客户端方案

客户端配置多个缓存的节点,通过缓存写入和读取算法策略来实现分布式,从而提高缓存的可用性。

  • 写入数据时,需要把被写入缓存的数据分散到多个节点中,即进行数据分片(一致性hash
  • 读数据时,可以利用多组的缓存来做容错,提升缓存系统的可用性。关于读数据,这里可以使用主从多副本两种策略。

2.2 中间代理层方案

虽然客户端方案已经能解决大部分的问题,但是只能在单一语言系统之间复用。例如微博使用 Java 语言实现了这么一套逻辑,我使用 PHP 就难以复用,需要重新写一套,很麻烦。而中间代理层的方案就可以解决这个问题。 你可以将客户端解决方案的经验移植到代理层中,通过通用的协议(如 Redis 协议)来实现在其他语言中的复用。

如果你来自研缓存代理层,你就可以将客户端方案中的高可用逻辑封装在代理层代码里面,这样用户在使用你的代理层的时候就不需要关心缓存的高可用是如何做的,只需要依赖你的代理层就好了。

除此以外,业界也有很多中间代理层方案,比如 Facebook 的Mcrouter,Twitter 的Twemproxy,豌豆荚的Codis

2.3 服务端方案

针对REDIS Redis哨兵和集群方案是服务端的高可用方案

3. 缓存穿透处理策略

缓存穿透其实是指从缓存中没有查到数据,而不得不从后端系统(比如数据库)中查询的情况

解决措施:

  • 返回空值
  • 使用布隆过滤器过滤,将不在数据库中的key直接使用布隆过滤器返回