【橙子老哥】C# 实操分布式事务解决方案

news/2024/10/24 2:41:15/

hello,大家好,今天来到橙子老哥的分享时间,希望大家一起学习,一起进步。

欢迎加入.net意社区,第一时间了解我们的动态,文章第一时间分享至社区

社区官方地址:https://ccnetcore.com (上千.neter聚集地)

官方微信公众号:搜索 意.Net

添加橙子老哥微信加入官方微信群:chengzilaoge520

上一篇我们实操了高并发分布式缓存的解决方案, 这篇我们接着分布式的话题,使用c#去实操了一下分布式事务问题的解决方案

相信很多人已经对分布式事务这种面试八股文很熟悉了,说个七七八八不成问题,网上也有很多教程,但是多偏向于理论,没有实操,今天橙子老哥使用c#,带大家把整个流程落地一遍

希望下次遇到这个问题,能回想到橙子老哥的这篇文章,就是这篇文章的意义了

1、事务-ACID

长话短说,理论知识不能少:一个事务有四个基本特性,也就是我们常说的(ACID)。

  1. Atomicity(原子性) :事务是一个不可分割的整体,事务内所有操作要么全做成功,要么全失败。
  2. Consistency(一致性) :务执行前后,数据从一个状态到另一个状态必须是一致的(A向B转账,不能出现A扣了钱,B却没收到)。
  3. Isolation(隔离性):多个并发事务之间相互隔离,不能互相干扰。
  4. Durablity(持久性) :事务完成后,对数据库的更改是永久保存的,不能回滚。

以上这些特征相信大家在使用数据库的时候,已经了如指掌了,这里也不再过多赘述。

2、不处理分布式事务

通常的,如果是在单体架构中,为了保持数据的一致性,只需要在批量执行数据库操作的时候,开启事务,在最终完成操作的时候,再提交事务即可

但是如果各各操作是分布在不同的程序/数据库/服务器上,我们还按照原先的方式会怎么样呢?

废话少说,我们直接实操,准备代码:

这里我们模拟一个经典场景,订单服务库存服务,用户创建订单,订单数+1,库存数量-1,像这种场景,我们必须要确保数据的一致性,如果出现了订单加的多了,库存减的少了,那不就产生了超卖的严重生产事故?

//情况1,无分布式事务处理//订单服务客户端
var orderServiceClient = new OrderServiceClient();//库存服务客户端
var storeServiceClient = new StoreServiceClient();//模拟执行10次下单
var i = 10;
while (i>0)
{try{//入口,用户进行创建订单orderServiceClient.CreateOrder(storeServiceClient);}catch (Exception e){Console.WriteLine(e.Message);}finally{	//打印数据库订单和库存数量Console.WriteLine($"当前订单数量:{OrderServiceClient.Order},库存数量:{StoreServiceClient.Store}");i--;}}
Console.WriteLine("完成");class OrderServiceClient
{public static int Order { get; private set; } = 10;private Action? _tran;//新增订单到数据库(不会真正执行,返回委托,事务预处理,执行委托就是提交事务)Action AddOrderToDb(){return () => Order += 1;}//业务代码public void CreateOrder(StoreServiceClient storeClient){//订单服务开启事务_tran = AddOrderToDb();storeClient.UpdateStore();//订单提交事务_tran.Invoke();}
}//同理
class StoreServiceClient
{public static int Store { get; private set; } = 20;private Action? _tran;//新增订单Action DecreaseStoreToDb(){return () => Store -= 1;}public void UpdateStore(){//库存服务开启事务_tran = DecreaseStoreToDb();_tran.Invoke();}
}

在上面的例子中,我们在订单服务,调用了自己的数据库,同时又远程调用了库存服务,双方各自执行事务操作

当没有一方出现错误、网络完美、服务器稳定、内存够用,好像怎么执行也不会有任何问题

CAP:我又来了,分布式中要满足分区容错,一致性和可用性就不能同时抓

上面执行中,很明显有个地方容易出问题,如果在库存服务事务已经提交,返回的时候,网络波动订单服务没有收到结果,订单报错了,取消事务,库存执行完了,新增库存,导致数据不一致

//业务代码public void CreateOrder(StoreServiceClient storeClient){//订单服务开启事务_tran = AddOrderToDb();storeClient.UpdateStore();//情况1throw new Exception("订单服务网络波动,无法收到库存服务的回应,或者收到回应,但是事务没有提交宕机");//订单提交事务_tran.Invoke();}
//初始数据
当前订单数量:10,库存数量:20//结果
当前订单数量:10,库存数量:10

为了解决这种分布式事务问题,行业内提出了非常多的方案,比较经典常用的是以下4个

  1. 2PC (悲观锁)
  2. 3PC (悲观锁)
  3. TCC (乐观锁)
  4. 消息队列 (异步)

其中,3PC是对2PC的补充,2PC和3PC是更针对与资源(多数据库)事务的情况,TCC增对应用接口

接下来,我们实操一下,这几个到底是个啥

3、2PC

想到分布式的一致性,那肯定离不开中心化,我们是否可以将多个服务的事务通过一个中心化的事务协调器 进行统一管理?

每个执行操作,都先问问这个事务协调器 ,所有人说可以,我们就执行,有人执行失败了,其他成功的全部回滚,简单好记,无脑粗暴

那就整一个中心的事务协调器,然后执行的业务的时候,先让各各服务预执行事务,都没问题,再让他们都提交事务即可

流程图:
借图
分为两步:Prepare预执行Commit提交

我们代码实现下:

情况2,2pc  悲观并发控制var orderServiceClient = new OrderServiceClient();var storeServiceClient = new StoreServiceClient();//我们引入一个新的客户端,专门来协调各各服务之间的事务var tranServiceClient = new TranServiceClient();var i = 10;while (i>0){try{//用户触发事务动作,通过事务协调者统一调度tranServiceClient.CreateOrder(orderServiceClient, storeServiceClient);}catch (Exception e){Console.WriteLine(e.Message);}finally{Console.WriteLine($"当前订单数量:{OrderServiceClient.Order},库存数量:{StoreServiceClient.Store}");i--;}}Console.WriteLine("完成");class OrderServiceClient{public static int Order { get; private set; } = 10;private Action? _tran;//订单服务预执行方法public bool Prepare(){_tran = AddOrderToDb();return true;}//订单服务提交方法public bool Commit(){_tran.Invoke();return true;}//落库Action AddOrderToDb(){return () => Order += 1;}}//同理class StoreServiceClient{public static int Store { get; private set; } = 20;private Action? _tran;public bool Prepare(){_tran = DecreaseStoreToDb();return true;}public bool Commit(){_tran.Invoke();return true;}//新增订单Action DecreaseStoreToDb(){return () => Store -= 1;}}//事务协调者class TranServiceClient{public void CreateOrder(OrderServiceClient client1, StoreServiceClient client2){//调用两个服务接口,预执行,只有都成功才走下一步if (Prepare(client1, client2)){	 //预执行都成功了,再全部统一提交if (Commit(client1,client2)){Console.WriteLine("事务全部完成提交");}else{throw new Exception("执行Commit存在有一方失败,成功一方进行回滚");}}else{throw new Exception("准备失败存在失败,不执行Commit");}}//预先执行事务内容bool Prepare(OrderServiceClient client1, StoreServiceClient client2){var res1 = client1.Prepare();var res2 = client2.Prepare();return res1 && res2;}//真正提交事务bool Commit(OrderServiceClient client1, StoreServiceClient client2){var res1 = client1.Commit();var res2 = client2.Commit();return res1 && res2;}}

这里,我们探究下原理,是靠什么保持的一致性?在事务协调者分别去调用两个服务的方法,只有等2个服务都返回了结果,才能进入下一个阶段!

答案,就在等字,意味着事务协调者认为任何请求都可能失败,不相信他们会成功,只有锁住了,都返回了结果,才允许走下一步,这就是悲观锁,虽然能保持一致性,但会一定程度降低吞吐量

同时,上面也可以看出,太依赖了这个中心事务协调者,如果它蹦了,那全玩完了

另外,2pc还有个严重的问题,因为我们只有2个阶段,当我们预提交的时候,虽然没有实际提交,但是也很消耗资源

如果我们分布式的2个服务,库存其实早就已经没有了,每次都去预执行事务,最后又不提交回滚,对订单服务会有一个连带效应,它也要每次去预执行事务,特别是微服务中,拆的非常细,很浪费资源

4、3PC

3PC,本质上就是为了解决上面浪费资源的问题
相比于2pc,它多了一个步骤,先把各各服务询问一下,准备好了没有,这里只是做一个基础的校验,如果库存都没有,那后面事务也不需要去提交再回滚的操作

分为三步:CanCommit准备Prepare预执行Commit提交

情况3,3pcvar orderServiceClient = new OrderServiceClient();var storeServiceClient = new StoreServiceClient();var tranServiceClient = new TranServiceClient();var i = 10;while (i > 0){try{tranServiceClient.CreateOrder(orderServiceClient, storeServiceClient);}catch (Exception e){Console.WriteLine(e.Message);}finally{Console.WriteLine($"当前订单数量:{OrderServiceClient.Order},库存数量:{StoreServiceClient.Store}");i--;}}Console.WriteLine("完成");class OrderServiceClient{public static int Order { get; private set; } = 10;private Action? _tran;public bool CanCommit(){return true;}public bool Prepare(){_tran = AddOrderToDb();return true;}public bool Commit(){_tran.Invoke();return true;}//新增订单Action AddOrderToDb(){return () => Order += 1;}}class StoreServiceClient{public static int Store { get; private set; } = 20;private Action? _tran;public bool CanCommit(){return true;}public bool Prepare(){_tran = DecreaseStoreToDb();return true;}public bool Commit(){_tran.Invoke();return true;}//新增订单Action DecreaseStoreToDb(){return () => Store -= 1;}}class TranServiceClient{public void CreateOrder(OrderServiceClient client1, StoreServiceClient client2){   //多了一步是否能执行的步骤if (CanCommit(client1, client2)){//预执行事务if (Prepare(client1, client2)){//真正去执行事务if (Commit(client1, client2)){Console.WriteLine("事务全部完成提交");}else{throw new Exception("执行Commit存在有一方失败,成功一方进行回滚");}}else{throw new Exception("准备失败存在失败,不执行Commit");}}else{throw new Exception("是否能提交阶段存在失败,通知之后,啥也不做");}}bool CanCommit(OrderServiceClient client1, StoreServiceClient client2){var res1 = client1.CanCommit();var res2 = client2.CanCommit();return res1 && res2;}//预先执行事务内容bool Prepare(OrderServiceClient client1, StoreServiceClient client2){var res1 = client1.Prepare();var res2 = client2.Prepare();return res1 && res2;}//真正提交事务bool Commit(OrderServiceClient client1, StoreServiceClient client2){var res1 = client1.Commit();var res2 = client2.Commit();return res1 && res2;}}

上面的代码,和2pc区别不大,只是对了一个判断是否能执行的询问阶段

5、TCC

2pc和3pc都是悲观锁的实现,而TCC是乐观锁的实现,它的全称是Try-Confirm-Cancel,看到这个是否很熟悉?对,这个跟数据库的事务一样

TCC认为大部分请求都是ok的,直接通过,不进行等待,如果小部分请求出现问题,那我们去回滚取消它就好了

分为三步:Try尝试Confirm确认Cancel取消

//情况4,TCC  ,Try、Confirm、Cancelvar orderServiceClient = new OrderServiceClient();
var storeServiceClient = new StoreServiceClient();
var i = 10;
while (i>0)
{try{orderServiceClient.CreateOrder(storeServiceClient);}catch (Exception e){Console.WriteLine(e.Message);}finally{Console.WriteLine($"当前订单数量:{OrderServiceClient.Order},库存数量:{StoreServiceClient.Store}");i--;}}
Console.WriteLine("完成");class OrderServiceClient
{public static int Order { get; private set; } = 10;private Action? _tran;public void CreateOrder(StoreServiceClient storeClient){//订单服务开启事务-如果这里失败,直接拦截_tran = AddOrderToDb();try{//如果这里失败,catch进行回滚storeClient.TryUpdateStore();}catch (Exception e){storeClient.Cancel();//当前事务也取消,所有操作回滚_tran = null;return;}//如果没有任何异常,确认执行try{storeClient.Confirm();}catch (Exception e){_tran = null;return;}//上方如果库存服务执行成功,没报错,订单服务也提交_tran.Invoke();//订单提交事务}//新增订单Action AddOrderToDb(){return () => Order += 1;}}class StoreServiceClient
{public static int Store { get; private set; } = 20;private Action? _tran;//新增订单Action DecreaseStoreToDb(){return () => Store -= 1;}public void TryUpdateStore(){//库存服务开启事务_tran = DecreaseStoreToDb();}public void Confirm(){_tran.Invoke();}public void Cancel(){_tran = null;}}

上述代码我们2个服务交互,可以不需要中心服务共用,本质上原理是将自己服务的事务和另一个服务的事务绑定在一块,我们都先去尝试,有一方失败了,我们都去回滚

5、消息队列

另外,还有一种方案也很常见,大部分分布式中出问题,都是网络出的问题,导致多个请求响应不一致,那我们如何去避免这种问题?答案还是一个重试

前面的方案给的答案是算执行失败,全部回滚

如果业务允许,只是网络这种偶发性问题,我们通过将消息存放到消息队列中,反复重试,直到成功,能确保一定成功,确保最终一致性就不需要回滚操作了

还是前面的例子,我们即将开启双11的秒杀活动,先把库存总数缓存放到订单服务中,每次下单全无脑塞给消息队列,一个消息算一个任务,库存服务去消费,就算有网络中断等意外情况,那就重试,最后减少了库存再通知用户

这种方案,库存服务虽然存在短暂时间不一致,但能够确保最终一致性,就算是消费者出了问题,反正我们消息持久化了,自己搂出来,人工处理都行

(主要还有一点,简单~)

都说到这里了,不得不提我们.net一个著名的解决分布式事务问题的开源项目
CAP:https://github.com/dotnetcore/CAP

最后的最后 - 意.Net 小程序即将上线 啦!各位敬请期待!–爱你们的橙子老哥


http://www.ppmy.cn/news/1541496.html

相关文章

【Hive】8-Hive性能优化及Hive3新特性

Hive性能优化及Hive3新特性 Hive表设计优化 Hive查询基本原理 Hive的设计思想是通过元数据解析描述将HDFS上的文件映射成表 基本的查询原理是当用户通过HQL语句对Hive中的表进行复杂数据处理和计算时,默认将其转换为分布式计算 MapReduce程序对HDFS中的数据进行…

RabbitMQ进阶_可靠性

文章目录 一、 发送者的可靠性1.1、 生产者重试机制1.2、 生产者确认机制1.2.1、确认机制理论1.2.2、确认机制实现1.2.2.1、定义ReturnCallback1.2.2.2、定义ConfirmCallback 二、 MQ的可靠性2.1、 数据持久化2.1.1、 交换机持久化2.1.2、 队列持久化2.1.3、 消息持久化 2.2、 …

HTTP安全么?如何更好的保护您的网站

在互联网飞速发展的今天,网络安全问题日益严峻。HTTP作为最常见的网络通信协议,虽然在传输效率方面表现优异,但其安全性却常常令人担忧。许多企业和个人网站在使用HTTP进行数据传输时,可能忽视了其中潜在的风险。那么,…

kafka自定义配置信息踩坑

org.apache.kafka.common.config.ConfigException: Invalid value 0 for configuration acks: Expected value to be a string, but it was a java.lang.Integer 场景描述: 单个kafka使用springboot框架自带的 yml 配置完全OK(因为底层会帮我们处理好类…

探索 JavaScript 事件机制(一):从基础概念到实战应用

前言 在现代前端开发中,JavaScript 事件是实现用户交互的核心机制之一。无论是按钮点击、鼠标移动还是键盘输入,这些用户操作都会触发特定的事件,从而使网页变得更加动态和响应式。然而,许多初学者对事件的概念和使用方法感到困惑…

上拉电阻和下拉电阻在电路中的作用(一)

上拉电阻和下拉电阻在电路中的作用(一) 1.什么是上下拉电阻2.上下拉电阻的作用:2.1.维持输入引脚处于稳定状态。2.2.配合三极管和MOS进行电平转换电路设计2.3.OC、OD电路(Open Collector集电极开路、Open Drain漏电极开路&#xf…

基于SpringBoot+Vue+uniapp的电影信息推荐APP的详细设计和实现

详细视频演示 请联系我获取更详细的演示视频 项目运行截图 技术框架 后端采用SpringBoot框架 Spring Boot 是一个用于快速开发基于 Spring 框架的应用程序的开源框架。它采用约定大于配置的理念,提供了一套默认的配置,让开发者可以更专注于业务逻辑而不…

c++ STL标准模板库-算法

C Standard Template Library(STL)算法是一组泛型算法,它们可以在各种容器上操作。这些算法被设计为与容器无关,因此可以在任何提供必要迭代器接口的容器上使用。STL算法分为以下几个主要类别: 非修改算法Non-modifyi…