需要注意的是标题中的CAP不是指的CAP理论,而是园区大神杨晓东实现的框架,CAP框架基于本地消息表用最终一致性实现分布式事务。
本地消息表
首先我们考虑一个场景,在将用户信息更改后,需要发送一条消息到消息队列、缓存或是写入到其他库中。这个过程涉及到一个本地库与MQ、本地库与Cache或是本地库与其他库两者之间的事务问题,不能用简单的数据库事务控制了。
这种分布式事务下,常用的解决方案有2PC、3PC等强一致性保证的,也有TCC、Sagas模型、本地消息表、内嵌本地消息表的MQ等最终一致性保证的。
而在很多异步场景下,允许系统存在短暂的不一致,只需达到最终一致,比起强一致性那种刚性事务,采用柔性事务,在很多场景下更有利于我们去实现。
执行过程
在使用CAP框架前,先熟悉下作为分布式事务解决方案之一的本地消息表工作过程。
- 消息发起方(如图左侧部分)和消息接收方(如图右侧部分),先额外建一套消息表,用来记录及跟踪消息内容及状态。
- 当有请求到消息发起方时,处理完业务逻辑发布消息将业务数据和消息数据一同提交到本地表中,此时为本地事务。
- 本地事务没有问题后,将消息发送到MQ传递给消息消费方。如果消息发送失败,会进行重试发送。
- 消息消费方,接收并处理消息,完成自己的业务逻辑,此时为消息消费方本地事务,如果本地事务完成,则更改接收消息的状态,更改本地,如果处理失败,那么可再次重试执行。
- 最终,左侧事务与右侧事务达到最终一致。
CAP框架
CAP是一个在分布式系统中(SOA,MicroService)实现事件总线及最终一致性(分布式事务)的一个开源的 C# 库,具有轻量级,高性能,易使用等特点。
- 具有 Event Bus 的所有功能,提供了更加简化的方式来处理EventBus中的发布/订阅。
- 具有消息持久化的功能,当服务进行重启或者宕机时,可以保证消息的可靠性。
- 基于本地消息表实现了分布式事务中的最终一致性。
- 集成了可视化页面方便观察消息状态。
- 提供了一系列Nuget包以选择需要的工具接入。
- 第一个包DotNetCore.CAP为必须要安装的。
- 可以依据消息队列的不同选择用RabbitMQ、Kafka或是AzureServiceBus等。
- 根据服务使用的数据库情况选择需要将本地消息表落库,可以选择SqlServer、MySql、PostgreSql、MongoDB等,或是直接使用内存存储,方便快速实践。
场景案例
依照EShopOnContainers中的一张图来实现一个例子,用户更新用户信息,将更新的部分通过事件发送到消息队列中,下游的购物车和订单服务侦听到消息,更改买家信息。
在此基础上行,设计三个上下文,并分别集成CAP,借助RabbitMQ作为消息队列,对于UserService、BasketService和OrderService,都直接使用了数据库(当然可以不仅限于数据库)。
服务建立
项目创建
开始建立几个服务,新建空白解决方案,依次建立三个WebApi项目,并移除默认的控制器。
简单设计下,在三个服务中创建三个DbContext,对应三个独立的数据库。
- UserService中创建UserInfo实体及UserDbContext
- BasketService中创建Basket实体及BasketDbContext
- OrderService中创建Order实体及OrderDbContext
安装Nuget包
在三个服务中均安装完如下选中的包,此次Demo中为方便快速实践,选择RabbitMQ作为消息队列,MySql作为数据库存储。
对于EFCore及MySql包,安装了如下几个包
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.Design
Microsoft.EntityFrameworkCore.Tools
Pomelo.EntityFrameworkCore.MySql
注意此处EFCore中MySql版本和CAP中MySql版本两者间依赖的MySqlConnector不一致会有点问题
配置服务
需要对CAP进行设置,比如使用的是什么数据库、什么消息队列及配置下消息队列参数,这一系列初始化设置在Startup.cs中配置好。
- ConfigureService中配置DbContext和CAP服务
- Configure中CAP的引入中间件
- 利用EFCore的迁移命令生成下数据库迁移脚本,将DbContext内实体生成到数据库中
- 单个服务启动后,CAP组件会将内置表创建到数据库中。
- 服务全部启动后,RabbitMQ Client会自动注册到RabbitMQ Server中同时创建好给定的Exchange(不给定则使用默认值),存在订阅的服务则注册队列绑定到给定的Exchange下。
发布事件
在 UserService中UserController 中注入ICapPublisher,使用Patch接口更新一个Address,然后使用ICapPublisher发布一条消息。
- 更新本地User表内信息。
- 借助_capPublisher发布事件,先将事件信息记录到本地MqPublish表。
- 前两步都是针对本地表操作,一个事务保证,写入MqPublish成功后再由CAP将记录发送到RabbitMQ中。
订阅事件
在BasketService和OrderService中完成事件的订阅。各自新建了一个Handler来处理消息。在Handler中对处理的方法加上CapSubscribe特性,其中监听的是发布事件时发送的事件名或消息名。
- BasketService收到RabbitMQ中的消息,CAP将消息写入到MqReceive中。
- 调用相应的Handler处理事件。
- 更新Basket本地表,本地事务完成被提交。
- CAP组件将本地的MqReceive相关记录更改状态到完成,如本地事务提交失败,则再次重试。
总结
抛弃强一致性想法借助最终一致性完成,将分布式事务拆分成多个本地事务进行处理。采用最终一致性来使得所有本地事务完成,即使部分出现失败,也可重试,如重试机制无效最终借助人力完成。
在异步场景下,CAP及其方便了我们去处理分布式事务的过程。
当前RabbitMQ场景下,当某个服务做多个部署时,同一个队列仍能保证一个消费者消费。这也避免了有些场景下,需要对资源加锁来防止同时消费场景。
参考
2021-04-28,望技术有成后能回来看见自己的脚步