通过Dapr实现一个简单的基于.net的微服务电商系统(十九)——分布式事务之Saga模式

在之前的系列文章中聊过分布式事务的一种实现方案,即通过在集群中暴露actor服务来实现分布式事务的本地原子化。但是actor服务本身有其特殊性,场景上并不通用。所以今天来讲讲分布式事务实现方案之saga模式,并在文后附上代码供各位读者参考,评论。

目录:
一、通过Dapr实现一个简单的基于.net的微服务电商系统

二、通过Dapr实现一个简单的基于.net的微服务电商系统(二)——通讯框架讲解

三、通过Dapr实现一个简单的基于.net的微服务电商系统(三)——一步一步教你如何撸Dapr

四、通过Dapr实现一个简单的基于.net的微服务电商系统(四)——一步一步教你如何撸Dapr之订阅发布

五、通过Dapr实现一个简单的基于.net的微服务电商系统(五)——一步一步教你如何撸Dapr之状态管理

六、通过Dapr实现一个简单的基于.net的微服务电商系统(六)——一步一步教你如何撸Dapr之Actor服务

七、通过Dapr实现一个简单的基于.net的微服务电商系统(七)——一步一步教你如何撸Dapr之服务限流

八、通过Dapr实现一个简单的基于.net的微服务电商系统(八)——一步一步教你如何撸Dapr之链路追踪

九、通过Dapr实现一个简单的基于.net的微服务电商系统(九)——一步一步教你如何撸Dapr之OAuth2授权 && 百度版Oauth2

十、通过Dapr实现一个简单的基于.net的微服务电商系统(十)——一步一步教你如何撸Dapr之绑定

十一、通过Dapr实现一个简单的基于.net的微服务电商系统(十一)——一步一步教你如何撸Dapr之自动扩/缩容

十二、通过Dapr实现一个简单的基于.net的微服务电商系统(十二)——istio+dapr构建多运行时服务网格

十三、通过Dapr实现一个简单的基于.net的微服务电商系统(十三)——istio+dapr构建多运行时服务网格之生产环境部署

十四、通过Dapr实现一个简单的基于.net的微服务电商系统(十四)——开发环境容器调试小技巧

十五、通过Dapr实现一个简单的基于.net的微服务电商系统(十五)——集中式接口文档实现

十六、通过Dapr实现一个简单的基于.net的微服务电商系统(十六)——dapr+sentinel中间件实现服务保护

十七、通过Dapr实现一个简单的基于.net的微服务电商系统(十七)——服务保护之动态配置与热重载

十八、通过Dapr实现一个简单的基于.net的微服务电商系统(十八)——服务保护之多级缓存

十九、通过Dapr实现一个简单的基于.net的微服务电商系统(十九)——分布式事务之Saga模式

二十、通过Dapr实现一个简单的基于.net的微服务电商系统(二十)——Saga框架实现思路分享

附录:(如果你觉得对你有用,请给个star)
一、电商Demo地址

二、通讯框架地址

三、Saga框架地址

一、什么是Saga

        在领域驱动设计中,由于领域边界的存在,以往的分层设计中业务会按照其固有的领域知识被切分到不同的限界中,并且引入了领域事件这一概念来降低单个业务的复杂度,通过非耦合的事件驱动来完成复杂的业务。但是事件驱动带来了一些新的问题,由于以往一个原子性极强的逻辑被拆散到了一个一个小的领域中,原子性事务数据的强一致性无法被保证。为了解决这个问题,一般会采用事务补偿的方式来确保最终一致。

        当我们采用多个本地事务组合去进行业务处理时,由于业务其本身的复杂性,往往需要在多个事务中协调。而事件协调器(saga)就是一个专门降低其复杂度的设计,开发人员原则上只需要将事件和补偿按照一定顺序注册到协调处理器中,原则上协调器会按照注册的事件依次执行,若出现事件执行失败时,也会按照补偿列表进行相应的回滚。

        去年曾经在一篇文章里聊过Saga,链接在此。相比去年的那个小DEMO。我在此基础上基于Dapr的状态管理完成了另外一个开源项目Oxygen-Saga,地址:https://github.com/sd797994/Oxygen-Saga。当然这个项目既可以引入到dapr的环境中完成Saga事务,本身也可以独立的基于rabbitmq+redis来完成非Dapr环境下的Saga事务。今天主要讲解一下如何在Dapr环境下引入Saga来实现一个分布式事务。

  首先我们还是看看目前的一个订单下单逻辑,可以看到基于Actor服务,我们的请求是在订单服务里直连商品服务Actor完成库存减扣的,并通过Actor的周期性持久化机制完成事务落库,所以实际上是把分布式事务的复杂性通过Actor屏蔽掉了,并没有涉及到真正的分布式事务。

  从代码层面也可以看到,整个事务其实是在同一个方法里以同步调用Actor的方式完成的。

        #region 私有远程服务包装器方法
        async Task<List<OrderGoodsSnapshot>> GetGoodsListByIds(IEnumerable<Guid> input)
        {
            return (await goodsQueryService.GetGoodsListByIds(new GetGoodsListByIdsDto(input))).GetData<List<OrderGoodsSnapshot>>();
        }
        async Task<bool> DeductionGoodsStock(CreateOrderDeductionGoodsStockDto input)
        {
            var data = input.CopyTo<CreateOrderDeductionGoodsStockDto, DeductionStockDto>();
            data.ActorId = input.GoodsId.ToString();
            return (await goodsActorService.DeductionGoodsStock(data)).GetData<bool>();
        }
        async Task<bool> UnDeductionGoodsStock(CreateOrderDeductionGoodsStockDto input)
        {
            var data = input.CopyTo<CreateOrderDeductionGoodsStockDto, DeductionStockDto>();
            data.ActorId = input.GoodsId.ToString();
            return (await goodsActorService.UnDeductionGoodsStock(data)).GetData<bool>();
        }
        #endregion
        public async Task<ApiResult> CreateOrder(OrderCreateDto input)
        {
            var mockUser = (await accountQueryService.GetMockAccount()).GetData<CurrentUser>();
            //申明一个创建订单领域服务实例,将远程rpc调用作为匿名函数传递进去
            var createOrderService = new CreateOrderService(GetGoodsListByIds, DeductionGoodsStock, UnDeductionGoodsStock);
            return await ApiResult.Ok("订单创建成功!").RunAsync(async () =>
            {
                var order = await createOrderService.CreateOrder(mockUser.Id, mockUser.UserName, mockUser.Address, mockUser.Tel, input.Items.CopyTo<OrderCreateDto.OrderCreateItemDto, OrderItem>().ToList());//通过订单服务创建订单
                repository.Add(order);
                if (await new CheckOrderCanCreateSpecification(repository).IsSatisfiedBy(order))
                    await unitofWork.CommitAsync();
                await eventBus.SendEvent(EventTopicDictionary.Order.CreateOrderSucc, new OperateOrderSuccessEvent(order, mockUser.UserName));//发送订单创建成功事件
            },
            //失败回滚
            createOrderService.UnCreateOrder);
        }

  而在Saga方案里就变得不一样了,我们需要借助Saga的流程管理器在多个实例中流转我们的事务,通过事件订阅和发布来最终完成一个分布式事务。具体的调用流程如下:

可以看到整个流程完全依赖于SagaManger来提供对应的调用策略,而作为开发人员只需要为每个策略提供对应的委托函数,对应的具体流程如下:

  一、首先是订单服务和商品服务在系统初始化时需要注册对应的配置文件到本地Saga管理器,并且需要提供对应的流程处理委托(包括业务事件委托+补偿事件委托+异常处理委托)。

  二、在客户发起一个订单时,只需要创建一个Saga流程实例,剩下的就交给Saga,它会自动帮我们流转整个业务逻辑而无需人工插手。

二、talk is cheap, show me the code

首先我们需要为整个订单创建流程设计一组Topic:

接着我们创建一个Saga配置注册这组topic进去,流程比较简单,第一步是扣除库存,下一步是创建订单,补偿事件只有一个就是库存回滚:

再然后我们需要创建这些Topic对应的事件处理器:

订单服务:

商品服务: 

接着我们在各自的服务里去实现它们:

商品服务:

订单服务:

然后我们在订单用例里创建一个Action用于启动saga流程(注意是第二个方法):

 最后我们在商品和订单服务中引入saga组件并注册事件和异常回调委托(注册代码相似,此处仅展示其中一个服务的):

接着我们修改m站的创建订单接口,修改为新的Saga流程接口,然后编译整个项目,启动并测试一下:

 

 

 可以看到整个流程被顺利的通过Saga管理器流转完毕。接着我们尝试注入一个故障,看看能否正常的被异常订阅器捕获到:

 编译后再测试一下,我们可以看到事件异常订阅器里已经成功捕获到了这个异常:

 接着我们在创建订单注入一个异常,让商品库存进行回滚操作,这里由于下单后异常商品补偿所以在界面上是体现不出来的(当然在真实的业务场景中一般是下单5秒等待后查询订单创建情况,或发送一条站内信告知下单失败),这里我们通过打印控制台来演示:

接着我们运行并下单,追踪商品服务的日志:

可以看到由于订单创建失败,saga触发了补偿事件并成功执行了补偿。好了,关于saga的演示就到这里,可尝试拉取最新的商城源码执行即可看到效果。

posted @ 2022-02-21 09:56  a1010  阅读(1599)  评论(2编辑  收藏  举报