缓存与CQRS

缓存

经常遇到的场景就是双写两份数据源,要保证两份数据源的数据一致性。以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
    评论
第1章我们的领域: 会议管理系统1 1.1Contoso公司简介1 1.2谁与我们同行2 1.3Contoso会议管理系统3 1.3.1系统概览3 1.3.2非功能性需求4 1.4开始我们的旅程5 1.5更多信息5 第2章领域分解——站点规划6 2.1本章术语定义6 2.2会议管理系统里面的有界上下文7 2.2.1订单和注册有界上下文7 2.2.2会议管理有界上下文7 2.2.3支付有界上下文8 2.2.4不包括在内的有界上下文8 2.2.5Contoso会议管理系统的上下文路线图9 2.3为什么选择这些有界上下文10 2.4更多信息11 第3章订单和注册有界上下文12 3.1订单和注册有界上下文简介12 3.2本章术语定义13 3.3领域定义(普适语言)14 3.4订单创建的需求分析16 3.5系统架构17探索CQRS和事件源目录3.6模式和概念17 3.6.1系统验证21 3.6.2交易边界22 3.6.3并发处理22 3.6.4Aggregates和Aggregate Roots22 3.7实现细节23 3.7.1高层架构23 3.7.2写者模型28 3.7.3使用Windows Azure服务总线37 3.8对测试的影响44 3.9本章小结47 3.10更多信息47 第4章扩展和改进订单和注册有界上下文48 4.1修改有界上下文48 4.1.1本章术语定义49 4.1.2用户需求49 4.1.3系统架构49 4.2模式和概念51 4.2.1记录定位器51 4.2.2读者端查询51 4.2.3向读者端提供部分履行的订单信息54 4.2.4CQRS命令验证55 4.2.5倒计时定时器和读者模型56 4.3实现细节56 4.3.1订单访问码(记录定位器)57 4.3.2倒计时定时器58 4.3.3使用ASP.NET MVC验证60 4.3.4将改动推送到读者端62 4.3.5重构SeatsAvailability aggregate66 4.4对测试的影响68 4.4.1接受测试和领域专家68 4.4.2使用SpecFlow功能来定义接受测试68 4.4.3通过测试来帮助开发人员理解消息流75 4.5代码理解的旅程: 痛苦、释放和学习的故事77 4.5.1测试很重要77 4.5.2领域测试78 4.5.3硬币的另外一面80 4.6本章小结83 4.7更多信息84 第5章准备V1发布85 5.1Contoso会议管理系统的V1发布版85 5.1.1本章术语定义85 5.1.2用户需求86 5.1.3系统架构87 5.2模式和概念91 5.2.1事件源91 5.2.2基于任务的用户界面92 5.2.3有界上下文之间的集成95 5.2.4分布式交易和事件源98 5.2.5自治与集权99 5.2.6读者端的实现方法100 5.2.7最终一致性100 5.3实现细节101 5.3.1会议管理有界上下文101 5.3.2支付有界上下文102 5.3.3事件源105 5.3.4基于Windows Azure表格的事件库111 5.3.5订单总价计算114 5.4对测试的影响114 5.4.1时序问题114 5.4.2引入领域专家115 5.5本章小结115 5.6更多信息115 第6章系统版本控制116 6.1本章术语定义116 6.1.1用户需求116 6.1.2系统架构117 6.2模式和概念118 6.2.1修改事件定义118 6.2.2确保消息的自洽性119 6.2.3集成事件的保存121 6.2.4消息排序122 6.3实现细节123 6.3.1对零成本订单的支持123 6.3.2显示剩余座位数127 6.3.3删除重复命令130 6.3.4确保消息排序131 6.3.5保存会议管理有界上下文的事件135 6.3.6从V1版本迁移到V2版本139 6.4对测试的影响140 6.4.1重访SpecFlow140 6.4.2在迁移过程中发现错误143 6.5本章小结143 6.6更多信息144 第7章加入弹性和优化性能145 7.1本章术语定义145 7.2系统架构145 7.3加入弹性147 7.3.1增加事件重复处理时的弹性148 7.3.2确保命令的发送148 7.4优化性能148 7.4.1优化前的用户界面流程149 7.4.2用户界面优化150 7.4.3基础设施优化151 7.5无停机迁移158 7.6实现细节159 7.6.1改进RegistrationProcessManager类160 7.6.2用户界面流程优化165 7.6.3消息的异步接收、处理和发送170 7.6.4在流程内部对命令进行同步处理171 7.6.5使用备忘录模式来实现快照173 7.6.6对事件进行并行发布175 7.6.7在订购服务里面对消息进行过滤176 7.6.8为SeatsAvailability aggregate创建专门的SessionSubscriptionReceiver 实例177 7.6.9缓存读者模型数据179 7.6.10使用多个议题来划分服务总线180 7.6.11其他的优化和强化措施181 7.7对测试的影响184 7.7.1集成测试185 7.7.2用户界面测试185 7.8本章小结185 7.9更多信息185 第8章尾声: 经验教训186 8.1我们学到了什么186 8.1.1性能很重要186 8.1.2实现消息驱动并不简单187 8.1.3云平台的挑战187 8.1.4不同的CQRS188 8.1.5事件源和交易日志记录190 8.1.6引入领域专家190 8.1.7什么时候该使用CQRS190 8.2如果重新来过,我们会做的有什么不同191 8.2.1以牢靠的消息和保存基础设施为起点191 8.2.2更好地利用基础设施的能力191 8.2.3采纳更加系统化的方法来实现流程管理器192 8.2.4对应用程序实施不同的划分192 8.2.5以不同方式组织项目团队192 8.2.6对领域和有界上下文的CQRS适用性进行评估192 8.2.7为性能进行规划192 8.2.8重新考虑用户界面193 8.2.9探索事件源的其他用处193 8.2.10探索有界上下文的集成问题193 8.3更多信息194
当使用CQRS(Command Query Responsibility Segregation)模式时,可以封装 HttpClient 来执行 HTTP 请求。下面是一个示例代码,演示了如何在 CQRS 命令处理程序中封装 HttpClient: 首先,创建一个名为 HttpClientService 的类来封装 HttpClient 的使用: ```csharp using System.Net.Http; using System.Threading.Tasks; public class HttpClientService { private readonly HttpClient _httpClient; public HttpClientService(HttpClient httpClient) { _httpClient = httpClient; } public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request) { return await _httpClient.SendAsync(request); } } ``` 然后,在 CQRS 命令处理程序中使用 HttpClientService 类: ```csharp using System.Net.Http; using System.Threading.Tasks; public class MyCommandHandler { private readonly HttpClientService _httpClientService; public MyCommandHandler(HttpClientService httpClientService) { _httpClientService = httpClientService; } public async Task Handle(MyCommand command) { // 创建一个 HttpClient 请求 var request = new HttpRequestMessage(HttpMethod.Post, "https://api.example.com/resource"); // 设置请求的内容等等 // 调用 HttpClientService 的 SendAsync 方法发送请求 var response = await _httpClientService.SendAsync(request); // 处理响应等等 } } ``` 在上述示例中,我们将 HttpClient 的具体使用细节封装在 HttpClientService 类中,这样可以将 HTTP 客户端的配置和处理逻辑与 CQRS 命令处理程序解耦。通过使用依赖注入,我们可以将 HttpClientService 实例注入到命令处理程序中,以便在需要发送 HTTP 请求时使用它。 这种封装方法使得代码更易于测试和维护,并且可以在不影响命令处理程序的情况下更改 HttpClient 的实现细节。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值