欢迎加入.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 StoreService Client{ public static int Store{get;privateset;}=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(); //情况1 thrownew 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、3PC3PC,本质上就是为了解决上面浪费资源的问题 相比于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 intOrder{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; }}classStoreServiceClient{ public static int Store{get;privateset;}=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 { thrownew Exception("执行Commit存在有一方失败,成功一方进行回滚"); } } else { thrownew Exception("准备失败存在失败,不执行Commit"); } } else { thrownew 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、TCC2pc和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 intOrder {get;private set;}=10; private Action? _tran; public void CreateOrder(StoreServiceClient storeClient) { //订单服务开启事务-如果这里失败,直接拦截 _tran =AddOrderToDb(); try { //如果这里失败,catch进行回滚 storeClient.TryUpdateStore(); } catch(Exception e) { storeClient.Cancel(); //当前事务也取消,所有操作回滚 _tran =; return; } //如果没有任何异常,确认执行 try { storeClient.Confirm(); } catch(Exception e) { _tran =; return; } //上方如果库存服务执行成功,没报错,订单服务也提交 _tran.Invoke(); //订单提交事务 } //新增订单 Action AddOrderToDb() { return ()=> Order+=1; } }class StoreServiceClient{ publicstaticintStore{get;privateset;}=20; private Action? _tran; //新增订单 Action DecreaseStoreToDb() { return ()=>Store-=1; } public void TryUpdateStore() { //库存服务开启事务 _tran =DecreaseStoreToDb(); } public void Confirm() { _tran.Invoke(); } public void Cancel() { _tran =; }}上述代码我们2个服务交互,可以不需要中心服务共用,本质上原理是将自己服务的事务和另一个服务的事务绑定在一块,我们都先去尝试,有一方失败了,我们都去回滚。
5、消息队列另外,还有一种方案也很常见,大部分分布式中出问题,都是网络出的问题,导致多个请求响应不一致,那我们如何去避免这种问题?答案还是一个重试
前面的方案给的答案是算执行失败,全部回滚
如果业务允许,只是网络这种偶发性问题,我们通过将消息存放到消息队列中,反复重试,直到成功,能确保一定成功,确保最终一致性就不需要回滚操作了
还是前面的例子,我们即将开启双11的秒杀活动,先把库存总数缓存放到订单服务中,每次下单全无脑塞给消息队列,一个消息算一个任务,库存服务去消费,就算有网络中断等意外情况,那就重试,最后减少了库存再通知用户
这种方案,库存服务虽然存在短暂时间不一致,但能够确保最终一致性,就算是消费者出了问题,反正我们消息持久化了,自己搂出来,人工处理都行
(主要还有一点,简单~)
都说到这里了,不得不提我们.net一个著名的解决分布式事务问题的开源项目
CAP:https://github.com/dotnetcore/CAP
最后的最后 - 意.Net 小程序即将上线 啦!各位敬请期待!--爱你们的橙子老哥
.Net意社区,高频发布原创有深度的.Net相关知识内容
与你一起学习,一起进步