缓存与CQRS

本文探讨了缓存策略中的数据一致性问题,包括Cache Aside、Read/Write Through和Write Back模式,以及各自在并发场景下的挑战。介绍了缓存穿透、缓存击穿和缓存雪崩的解决方案。随后,文章引入CQRS(命令查询职责分离)模式,分析了同步和异步双写策略,以及如何处理数据一致性。最后,提出了应对ABA问题的最新读和乐观锁策略。
摘要由CSDN通过智能技术生成

缓存

经常遇到的场景就是双写两份数据源,要保证两份数据源的数据一致性。以mysql和kv为例,分析下缓存。

先从缓存角度出发聊一下数据读写一致性问题:
作为数据库缓存,缓存的存在是为了减少数据库压力,避免大量请求打入到数据库,提示系统的QPS。

  • 错误的更新方式:
    更新请求:
    1.先删除缓存,
    2.然后再更新数据库
    读请求:
    1. 读缓存,有直接返回,没有进入第二步
    2. 从数据库读数据
    3. 更新到缓存

该方案有个很大的问题,更新请求和读请求同时来的时候,更新请求把缓存删除,读请求读不到缓存,去请求数据库,并将老的数据写入到缓存,这个时候更新请求把数据库更新了,缓存和数据库的数据不一致了,缓存的数据都是老的。

三种缓存策略

  1. Cache Aside Pattern
    这个是最经典的模式了,看下基本的流程
    读请求:
    (1) 缓存读到数据,返回
    (2) 没有读到,从数据库读取,并写入到缓存
    更新请求:
    (1)更新数据库
    (2)删除缓存
    基本流程简单,实现起来并不复杂,然后看下上面的那个问题是否存在,答案是存在的,但是发生的概率是很低。这里模拟一下过程:
    (1)因为没有了先删除缓存的操作,所以这里先更新了数据库
    (2)读请求到来,读到缓存是老的数据
    (3)更新请求更新了缓存
    可以看到这个过程中,读请求虽然读到老的数据,但是后续请求都能够读到新的数据,不会产生上面逻辑性的错误。那么再看下,这种方式会不会产生上述逻辑性问题,缓存数据是老的,数据库是新的。答案是会有很小的概率发生。模拟一下过程:
    (1)读请求没有命中缓存,然后从数据库读数据
    (2)更新请求更新数据
    (3)更新请求删除缓存
    (4)读请求将老数据写入到缓存了
    可以看到这种情况是会导致上述的不一致,但是数据库的更新相比数据库的查询是更慢的,所以上述case发生概率很低。但是为了能够弥补上述逻辑性错误。在使用缓存策略的同时,通常要配合着缓存的过期时间,过期读请求就必须去数据库读区。
  2. Read/Write Through Pattern
    cache aside应用代码需要维护两个数据源,数据库和缓存。而该方案应用层只需要维护一个数据源。数据库的操作由缓存代理。
    (1)Read Through
    查询操作中更新缓存,相比cache aside由调用方把数据写入缓存,而read through则是缓存服务自己加载进来。
    (2)write Through
    Write Through在更新时发生,如果命中缓存直接更新缓存,然后缓存服务更新数据库,如果没有命中缓存,直接更新数据库。
  3. Write Behind Caching Pattern
    Write Back在更新数据的时候,只更新缓存,不更新数据库,而缓存服务会异步地批量更新数据库。

三种缓存更新机制简单回顾下,一般情况使用第一种方案就行。可以注意到整个过程并没有讨论 如果更新数据库成功,更新缓存失败类似的情况。这三种方式也是无法完全保证数据的一直性。所提到的缓存也会有经典的三个缓存问题:

  1. 缓存穿透
    缓存和数据库中都没有的数据,用户不断请求打到数据库中,导致压力过大深圳打垮服务
    解决方案:对于没有存在的数据在缓存中value设置成null并设置过期时间。复杂点的做法 增加布隆过滤器,过滤不存在的数据。
  2. 缓存击穿
    用户大量请求热点key,在热点key过期的同时,大量请求打到了数据库中,造成数据库压力过大
    解决方案:热点key不过期,或者 通过互斥锁,取数据的时候 只有一个线程可以获得这把锁,拿到锁的线程读取数据并设置缓存。
  3. 缓存雪崩
    大面积缓存同时过期,请求打到数据库,导致数据库服务不可用

缓存的部分聊完了,缓存的合理使用能够减轻数据库的读写压力,但是目前的这些策略并不能保证数据的一致性,同时缓存也不会全量保存数据。随着业务的发展,读多写少的情况越来越多,读请求的需要支持更为强大的读写能力,例如:批量查询,条件查询,模糊匹配等。这些普通的key-value缓存是很困难做到。面对大量请求时,mysql的qps也无法满足要求。因此提出了CQRS全新的模型

CQRS
CQRS命令指责分离模式,读写模型解耦。写入的数据源和读数据源分离,写入的数据源通常是mysql,mysql作为数据库代表稳定存储。而读数据源可以分为以下两类:
OLAP:也叫联机分析处理(Online Analytical Processing)表示读能力非常强的系统,评估系统的时候往往是磁盘系统的吞吐量。例如es,clickhouse。
OLTP:也叫联机事务处理(Online Transaction Processing)表示事务性非常高的系统,评估系统的时候按照TPS进行评估,例如美国的eBay。
使用CQRS通常是源系统的读能力无法满足要求了,需要新增数据源增强系统的读能力。这个时候两个数据源的关系就和缓存有本质的区别,两个数据源的数据需要具备一致性,两个数据源的数据都要可信。这里先从同步方案出发分析CQRS的实现要点:

同步方案
同步处理也就是所谓的双写,只有两份数据源都写成功才返回成功
问题:如果一个写成功一个写失败如何处理
解决方案:

  1. 强一致性方案:采用两阶段提交等保证数据的强一致性
    优点:强一致性
    缺点:性能差,业务侵入性强,需要有回滚操作
  2. 最终一致性方案:
    a. mysql写成功,并写入流水表,流水表记录写入kv的情况
    b. kv写失败,将b放入消息队列,返回用户成功结果
    c.消息队列重试写入b,写入成功,更新流水表
    该方案的关键点:为什么要流水表,因为无法保证将b能够成功放入到消息队列,mysql写成功和写入流水表可以放到同一个事务中进行处理,保证myslq写成功,流水表写成功。有了流水表,即使没有将b放入到消息队列,后续也可以通过流水表进行重试。
    优点:mysql作为写数据源,只要写成功对上层就意味着成功。后续的写入失败不会影响mysql的写入,不需要mysql进行回滚。
    缺点:相比两阶段提交,正常情况一致性没问题,失败情况一致性稍差,但是最终还是能够一致。同样的双写业务代码复杂,业务侵入性强。
    异步处理
    1. 消息队列,和上面的最终一致性方案相似,写入的时候,写入mysql,然后把写入读数据源的请求放到消息队列异步处理
    2. 订阅mysql binlog,系统监听binlog日志,通过消费队列同步到读数据源。
      缺点:两种方案都是异步处理,数据一致性要更差点,队列阻塞挤压都会导致大量请求无法处理,延迟可能打到分钟级
      优点:业务侵入性小,写入性能强

上面的几种方案,可以根据业务场景的诉求选择不同的方案实现,包括读数据源目前业界有多种,例如es,clickhouse等,根据诉求进行选择。最后上述方案除了两阶段提交的强一致方案,在数据更新的时候都会遇到经典ABA问题,例如,并发两个更新请求,请求a 修改了mysql ,请求b再修改mysql,请求b更新的读数据源,然后请求a更新读数据源,最终 mysql和读数据源数据不一致。本文提供两种常用的解决方案:

  1. 最新读,在写读数据源的时候重新读取同步的数据,这样就a请求的时候,就不会同步老数据,但是该方案也只是降低数据不一致发生的概率
  2. 乐观锁,为数据增加版本号,在数据更新的时候,如果版本老就无法更新。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值