分布式事务最终一致性-CAP框架轻松搞定

前言

对于分布式事务,常用的解决方案根据一致性的程度可以进行如下划分:

  • 强一致性(2PC、3PC):数据库层面的实现,通过锁定资源,牺牲可用性,保证数据的强一致性,效率相对比较低。

  • 弱一致性(TCC):业务层面的实现,通过预留或锁定部分资源,最后通过确认或取消操作完成事务的处理。比如A向B转款500元,A账号会冻结500元,其他操作正常,B接收转款时,也不能直接入账,而是将500元放到预留空间,只有经过确认之后,A才正式扣钱,B才正式入账;如果取消把A的500块解冻,B也不会入账。

  • 最终一致性(本地消息表):不管经过多少个服务节点,最终数据一致就行。比如下单成功之后,需要库存服务扣减库存,如果库存扣减失败,不管是重试,还是最后人工处理,最后确保订单和库存数据能对上就行;为保证用户体验,及时通过中间状态的形式反馈给用户,比如常见的出票中、数据处理中等。

对于强一致性和弱一致性的解决方案一般针对数据一致性和时效性要求特别高的业务场景,通常会牺牲暂时的可用性来满足一致性的要求;由于为保证一致性,会锁定资源,在高并发的业务场景不是最佳选择,所以很多系统在业务需求允许的情况下,基本上都会采用最终一致性方案。

正文

1.1 最终一致性简述

顾名思义就是保证数据最后的一致性就行了。如果中间节点发生失败,系统为了减少代价,一般不会自动回滚,而是通过重试机制和人工参与的方式对失败数据进行处理,从而保证系统高并发场景下高可用数据一致性需求。

1.2 解决方案

目前用得最多的方案是结合本地消息表进行实现,再加上后台任务、消息队列中间件就可以更好的实现分布式事务的处理。

解决方案流程

本地消息表:就是在对应业务数据库中增加的一张消息表;这张表存储业务产生的消息,通过本地事务保证业务数据和消息数据的一致性。在消息表中通过一个状态来标识业务是否执行成功,如果失败,后台任务就进行重试。

1.2.2 CAP框架简介

CAP 是一个EventBus(事件总线),同时也是一个在微服务或者SOA系统中解决分布式事务问题的一个框架,基于CAP理论思想进行封装的。采用模块化设计,具有高度的可扩展性,可靠并且易于更改。

对于分布式事务的处理,CAP 框架采用的是“异步确保”这种方案,即本地消息表。官方支持的数据存储方式有SQL Server、MySQL、PostgreSql、MongoDB、In-Memory(内存),由于是开源项目,社区大佬也提供了其他数据存储支持,如:Oracle、SQLite、SmartSql等。

在分布式系统,各节点需要进行消息传输,CAP框架提供以下几种方式RabbitMQ、Kafka、Redis Streams(Redis 5.0支持)、Azure Service Bus、Amazon SQS、In-Memory Queue,使用方式都差不多。

CAP的架构图如下:

架构图

上图简要说明:

  • 有两个微服务,服务A和服务B;

  • 服务A中通过本地事务的方式,将事件消息和业务逻辑进行事务保存(事件消息保存在本地消息表中),保证业务逻辑和消息的一致性和可靠性;关于消息的处理和保存CAP已经封装在内部;

  • CAP内部定时调度任务将消息发布到消息队列中;

  • 服务B订阅到消息,将其保存到服务B的本地消息表中,CAP已经封装好,只需按照说明使用即可;

  • 如果业务处理失败,服务B中集成的CAP会根据配置的定时任务策略进行重试,直到处理成功为止;

主要的理论就说那么多,更多详细内容,请进下方传送门:

  • 官网:https://cap.dotnetcore.xyz/

  • github:https://github.com/dotnetcore/CAP/blob/master/README.zh-cn.md

接下来就到撸码时刻,CAP由于封装比较好,所以使用起来比较简单。

1.3 撸码实践

以下的业务场景是为了案例演示,目的是体现CAP的实践,所以业务逻辑都只是模拟,切勿当真。

1.3.1 环境准备

演示中要用到RabbitMQ,为了安装方便,这里使用Docker的方式,直接通过镜像运行,简单,快速方便。关于Docker的实践,后续会专门出系列文章。这里就先总结一下Docker的安装和RabbitMQ在Docker中的运行步骤,采用的主机环境是我之前买的阿里云服务器(CentOS 7);演示用的数据库是SqlServer。

  • Docker安装

    1、移除移动旧版本

    sudo yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-engine
    

    2、安装需要的依赖包

    sudo yum install -y yum-utils
    

    3、设置镜像仓库

    sudo yum-config-manager \--add-repo \https://download.docker.com/linux/centos/docker-ce.repo
    

    4、更新Yum软件包索引

    sudo yum makecache fast # 提高安装速度
    

    5、开始安装Docker

    sudo yum install docker-ce docker-ce-cli containerd.io
    

    6、启动Docker

    sudo systemctl start docker
    

    7、测试Docker

    sudo docker run hello-world # 运行Hello-world
    
    安装成功
  • RabbitMQ在Docker中安装和运行

    1、一行命令直接指定镜像运行,如果本地找不到镜像,会去远程仓储里去找。

    docker run -d --hostname my-rabbit --name cap-rabbit -p 8888:15672 -p 5672:5672 -p 5671:5671 -p 1883:1883 rabbitmq:3-management
    

    这里先不细说命令了,后续聊Docker的时候好好说说。命令需要注意的是主机端口和容器端口的映射。

    2、运行成功后就可以访问啦,默认用户名和密码:guest/guest;

    这里访问的地址端口是8888,那是在启动容器的时候将主机端口8888和容器端口15672进行了映射。

这就是选择Dokcer安装的原因,超级快;如果用传统的方式,还得安装语言环境,还得配置,最后才能安装;Docker通过镜像的方式直接运行即可。

如果小伙伴新增用户之后不能访问,或者程序连接报错,可以排查是否有权限访问,如下:

注:如果小伙伴用的是云服务器,需要配置安全组,允许端口访问;另外如果程序和RabbitMq所在的主机不是同一台机器,主机防火墙也需要放开对应的端口。

1.3.2 开始撸码
  • 项目准备

    这里模拟两个服务,一个是订单服务,一个是库存服务,两都用到EF(Code First),如果小伙伴对EF入门还不熟,<<跟我一起学.NetCore之EF Core 实战入门,一看就会>>这篇文章超详细,肯定能帮到你;所以接下来就上几张关键的图就行啦。

    项目结构:

    OrderDbContext:

    Startup中注册服务:

    库存服务的代码和这个类似。

    通过迁移并更新到数据库时,会生成如下数据库和表:

  • 集成CAP

    这里因为用的是RabbitMQ、SqlServer,所以需要引入以下几个包;如果用其他消息队列或数据库,可以引入对应的包。

    因为订单服务是在Respository层使用CAP,所以对应的包就在这层引用;

    库存服务是直接在Controller那层引用,这里就不重复截图啦。

    订单服务和库存服务都是在各自项目的Startup文件中注册CAP相关服务,并配置相关信息,如下图:

    集成完毕之后,启动项目(不需要手动自己迁移),在各自业务数据库中就自动生成两个消息表,用于后续消息的存储,如下:

  • 编写业务代码

    订单服务,在订单生成成功之后,向库存服务发送消息,业务逻辑如下:

    图中用到的_capPublisher是通过构造函数注入的。订单服务其他层的代码就不用截图了,就是简单调用,源码地址在文末。

    库存服务直接订阅就行,演示案例中是直接在StockController中进行订阅,如下:

    // 标记为不实Action
    [NonAction]
    // 订阅消息,参数和发布时指定的参数一致
    [CapSubscribe("Order.Create.Success")]
    public void UpdateStock(OrderEntity order)
    {//throw new Exception("扣减库存异常了~~~");// 为了测试,库存里面没有数据的话,先模拟一条数据bool bHaveData = _stockDbContext.Stock.Any();if(!bHaveData){StockEntity stock = new StockEntity{Id = Guid.NewGuid(),ProductNo = "Product001",StockCount = 100,UpdateDate = DateTime.Now};_stockDbContext.Stock.Add(stock);_stockDbContext.SaveChanges();}// 模拟扣减库存using var trans = _stockDbContext.Database.BeginTransaction(_capPublisher, autoCommit: false);try{// 根据产品编号找到产品var product = _stockDbContext.Stock.Where(s => s.ProductNo == order.ProductNo).FirstOrDefault();// 扣减库存之后保存product.StockCount = product.StockCount - order.Count;_stockDbContext.Update(product);_stockDbContext.SaveChanges();// 可以继续向下发布流程,比如库存扣减成功,下一步到物流服务进行相关处理,可以继续发布消息// _capPublisher.Publish();trans.Commit();Console.WriteLine(order.OrderNo);}catch (Exception ex){trans.Rollback();}
    }
    

    可以看到,订阅很简单,直接标上[CapSubscribe("Order.Create.Success")]这个Attribute就行了,如果消息状态为失败,后续CAP的定时任务会根据定时策略调用此方法。

1.3.3 运行看效果
  • 正常流程,下单成功,扣减库存成功

    将订单服务(端口5000)和库存服务(端口6000)都启动起来。

    订单服务中增加了OrderController,里面有一个GenerateOrder的接口,直接调用即可:

    这里使用Postman工具进行测试,如下:

    库存服务就会订阅到信息,如下:

    业务流程完成之后,订单和库存数据整体一致了,回过头来看看消息表,看看里面有什么消息,如下:

  • 异常流程模拟,下单成功,扣减库存失败

    在扣减服务逻辑方法中手动抛出异常,代码如下:

    然后启动项目重新测试,再下一个订单试试;操作后,先来看看消息表,如下:

    注:CAP在默认情况下,发送和消费消息的过程中失败会立即重试 3 次,在 3 次以后将进入重试轮询;重试将在发送和消费消息失败的 4分钟后 开始,这是为了避免设置消息状态延迟导致可能出现的问题;后续就会每隔1分钟之后重试一次,默认的最高重试次数为50次,当达到50次时,就不会重试了。

    现在知道问题了,优化代码,重新启动,即把抛异常的代码注释掉,看看会不会自动处理,如下:

    如上图,稍等一会,消息就自动处理了,业务数据符合预期,保证一致性。这个是CAP内部定时读取消息表,根据状态不断重试业务逻辑,直到成功为止。CAP的全自动是不是感觉比较便捷,写最少的代码,解决了最难搞的分布式事务。

  • 修改默认的配置

    在实际业务场景中,默认配置可能不太实用,可以在注册服务时进行默认配置更改,如下:

    配置修改之后的测试这里就不截图了,留给小伙伴们动手试试吧。

案例代码地址:https://gitee.com/CodeZoe/microservies-demo/tree/main/CapDemo

总结

关于分布式事务的实操,把最常用的最终一致性方案简单分享了一下,小伙伴可以根据自己的业务场景,赶紧动手试试吧;

其他方案会在后续的文章中加上,主要还是以实用为主,已经不咋用的就没必要再说啦。

文章中提及到Docker和RabbitMQ,我已经在着手准备这块的文章了,关注“Code综艺圈”,和我一起学习吧;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/299824.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Cus系统beta1.2发布

2019独角兽企业重金招聘Python工程师标准>>> 经过小伙伴的努力&#xff0c;Cus后台管理系统开发完成&#xff0c;完善了后台系统&#xff0c;权限控制&#xff0c;新闻发布&#xff0c;商务合作等等功能 主要功能包括&#xff1a; 后台系统截图 更多后台系统截图请点…

如何打开网页链接

System.Diagnostics.Process.Start("http://news.163.com") 转载于:https://www.cnblogs.com/habin/archive/2008/03/10/1099285.html

区区6位密码,凭什么守护我的百万家产?

全世界只有3.14 % 的人关注了青少年数学之旅今天超模君非常的开心你问为什么&#xff1f;当然是——又双叒可以买新的数学书好开心&#xff01;不过在这欢快的气氛中超模君却听见了小天的叹气声玩笑归玩笑但是银行的密码系统真很安全的吗&#xff1f;今天我们就来讨论下密码学的…

在ie6下实现position-fixed的效果--------续集---对联效果(02)

在ie6下实现position-fixed的效果&#xff0c;这回是实现的对联效果&#xff0c;没有JS在里面的对联效果&#xff0c;但是没有JS的动感。 这个的原理是和《在ie6下实现position-fixed的效果》的是一样的。CSS&#xff1a;1 <style type"text/css">2 *{margin:0…

狐狸和猫

一只猫在森林里遇到一只狐狸&#xff0c;心想&#xff1a;"他又聪明&#xff0c;经验又丰富&#xff0c;挺受人尊重的。"于是它很友好地和狐狸打招呼&#xff1a;"日安&#xff0c;尊敬的狐狸先生&#xff0c;您好吗&#xff1f;这些日子挺艰难的&#xff0c;您…

Java开发之上班摸鱼!写最少的代码!

I 前言 本次分享一下我所知道的如何写最少的代码的小技巧&#xff0c;如果你有更好的方案&#xff0c;欢迎在评论区留言&#xff0c;方案很棒的话&#xff0c;加我交流圈&#xff0c;为你送上冬天的一杯奶茶~ Java&#xff1a;我想返回多个返回值 秀一下Go的多返回值&#xf…

Windows 11 预览版 Build 22000.120 发布

微软现已发布第五个 Windows 11 预览版更新 KB5005188&#xff0c;版本号升级至 Build 22000.120。本次更新面向 Dev 频道和 Beta 频道的 Windows 预览体验成员推出。Windows 11 Insider Preview Build 22000.120 主要变化如下&#xff1a;1.全新的 Family Safety&#xff08;家…

经典SQL语句集锦

来自网络下列语句部分是MsSql语句&#xff0c;不可以在access中使用。SQL分类&#xff1a; DDL—数据定义语言(CREATE&#xff0c;ALTER&#xff0c;DROP&#xff0c;DECLARE) DML—数据操纵语言(SELECT&#xff0c;DELETE&#xff0c;UPDATE&#xff0c;INSERT) DCL—数据控制…

robocopy帮助

一 Robocopy简介 Robocopy 是一个功能超强的32位的文件复制工具&#xff0c;该工具来自windows资源包&#xff0c;可以直接在网上下载。 使用Robocopy你能够拷贝单个目录&#xff0c;或迭代的拷贝目录及其所有的子目录。该工具通过文件是否存在于源目录&#xff0c;目标目录&am…

有趣的灵魂连墓碑都很酷! | 今日趣图

全世界只有3.14 % 的人关注了青少年数学之旅&#xff08;图源别是个沙雕吧&#xff0c;侵权删&#xff09;

如何得到别人的上网帐号和密码

如何得到别人的上网帐号和密码&#xff0c;我在这里介绍几种弱智方法。方案一&#xff1a;利用特洛伊***得到帐号。必备工具&#xff1a;NTESPY、PWLVIEW。优点&#xff1a;只要对方运行了程序就有80%的成功率&#xff0c;而且不需要任何网络***知识&#xff0c;只要能熟悉的操…

Java 泛型 泛型的约束与局限性

不能用基本类型实例化类型参数 不能用类型参数代替基本类型&#xff1a;例如&#xff0c;没有Pair,只有Pair&#xff0c;其原因是类型擦除。擦除之后&#xff0c;Pair类含有Object类型的域&#xff0c;而Object不能存储double值。这体现了Java语言中基本类型的独立状态。 运行…

实施Exchange 2013中的 MailTip

实施Exchange 2013中的 MailTip邮件提示是用户撰写邮件时向其显示的提示性消息。Microsoft Exchange Server 2013 将分析邮件&#xff08;包括向其发送了邮件的收件人的列表&#xff09;&#xff0c;如果检测到潜在问题&#xff0c;它将使用邮件提示在邮件发送之前通知用户。借…

GitHub 的 Action 接入 Stryker.NET 进行自动化测试单元测试鲁棒性

假设有一个捣蛋的小伙伴加入了你的团队&#xff0c;这个捣蛋的小伙伴喜欢乱改代码&#xff0c;请问此时的单元测试能否拦住这些逗比行为&#xff1f;如果不能拦住逗比行为&#xff0c;是否代表着单元测试有所欠缺&#xff0c;或者有某些分支逻辑没有考虑到。本文将告诉大家的 S…

CN Erlounge IV 讲师名单公布及Call For Topic

近期将公布目前已经报名的讲师名单及Topic。那些希望Share Topic的朋友赶快了。 目前累计注册的讲师为12位&#xff08;不包括口头和我打过招呼但实际没有注册的讲师&#xff09;。 也提醒下那些已经注册但是Topic还是TODO的讲师赶紧提供下Topic。 :) 另外&#xff0c;借这个通…

防弹玻璃为啥会被钢球砸碎?这就是一道高中物理题!

全世界只有3.14 % 的人关注了青少年数学之旅马斯克&#xff0c;硅谷钢铁侠&#xff0c;全世界最具煽动力的企业家。旗下公司特斯拉最新电动皮卡&#xff0c;一经亮相就欢呼一片&#xff0c;传播到炸&#xff0c;看起来又要重新定义一个品类。然而也有网友“提醒”——如今的马斯…

八种ADSL接入情况中断流现象分析

转载自&#xff1a;网盟技术[url]http://technic.txwm.com[/url] 线路不稳定 如果住所离电信局太远(5公里以上)可以向电信部门申报。确保线路连接正确(不同的话音分离器的连接方法有所不同&#xff0c;请务必按照说明书指引正确连接)。同时确保线路通讯质量良好没有被干扰&…

Java之jdk与jre的区别

很多程序员已经干了一段时间java了依然不明白jdk与jre的区别。 JDK就是Java Development Kit.简单的说JDK是面向开发人员使用的SDK&#xff0c;它提供了Java的开发环境和运行环境。SDK是Software Development Kit 一般指软件开发包&#xff0c;可以包括函数库、编译程序等。 …

设计模式之组合

组合模式介绍一棵树结构组合模式是把相似对象或方法组合成一组可被调用的结构树对象的设计思路。组合模式不只是可以运用于规则决策树&#xff0c;还可以做服务包装将不同的接口进行组合配置&#xff0c;对外提供服务能力&#xff0c;减少开发成本。组合模式的主要解决的是一系…

实现 VC 最小化到 托盘

大家经常看到 程序 最小化到 托盘 但用C怎么实现呢 本人 试下哈 可以实现的 ~~ main.cpp 修改如下 #define WM_CLICKBIT (WM_USER 1) //定义消息 HINSTANCE hApp; NOTIFYICONDATA nid; BOOL WINAPI Main_Proc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { switch(u…