soul极简入门

目录: 1. 概述 2. 单机部署 3. 接入 Dubbo 应用 4. 接入 Spring Boot 应用 5. 接入 Spring Cloud 应用 6. rateLimiter 插件 7. hystrix 插件 666. 彩蛋 作者:芋道源码 原文地址 大家好,我是艿艿,一个永远 18 岁的技术宅 1. 概述 Soul 是基于 WebFlux 实现的响应式的 API 网关,具有异步、高性能、跨语言等特点。 作者:我希望能够有一样东西像灵魂一样,保护您的微服务。在参考了 Kong、Spring Cloud Gateway 等优秀的网关后,站在巨人的肩膀上,Soul 由此诞生! 作者是艿艿的大表弟,胖友信么?! 目前 Soul 功能列表如下: 支持各种语言,无缝集成到 Dubbo、Spring Cloud、Spring Boot 中。 Soul 是极其少支持 Dubbo 的 API 网关,通过 Dubbo 泛化调用 实现。 支持各种语言(http协议),支持 dubbo,springcloud协议。 插件化设计思想,插件热插拔,易扩展。 灵活的流量筛选,能满足各种流量控制。 内置丰富的插件支持,鉴权,限流,熔断,防火墙等等。 流量配置动态化,性能极高,网关消耗在 1~2ms。 支持集群部署,支持 A/B Test, 蓝绿发布。 整体架构如下图所示: 是不是看着就贼酷炫,实际一脸懵逼。不要慌~我们先来搭建 Soul 网关。 2. 单机部署 本小节,我们来单机部署一个 Soul 服务,适合测试环境。如下图所示: 2.1 MySQL 安装 相信大家都会,艿艿就不瞎哔哔了。嘿嘿~注意,目前最好安装 5.X 版本,艿艿一开始用 8.X 存在报错的情况。 安装完成后,创建 soul 数据库。 2.2 Soul Admin 安装 Soul Admin 控制台,负责所有流量的管理、配置等等,并提供给 网关服务读取。 友情提示:后续推荐胖友阅读如下两篇文章,搞懂 Soul Admin 和 Soul Bootstrap 的同步的原理: 《Soul 文档 —— 数据配置流程》 《Soul 文档 —— 数据同步原理》 ① 从 https://yu199195.github.io/jar/soul-admin.jar 下载启动 jar 包。

  ② 通过 java -jar soul-admin.jar 命令启动 Soul Admin 控制台。完整命令如下:

  --spring.datasource.url 配置项,修改成胖友的数据库地址。 --spring.datasource.username 配置项,修改成胖友的数据库账号。 --spring.datasource.password 配置项,修改成胖友的数据库密码。 Soul Admin 会自动创建数据库,以及表结构,并初始化默认数据。如下图所示: 友情提示:具体的数据库设计,后续可以看看《Soul 文档 —— 数据库设计》。 ③ 启动完成后,我们可以通过日志看到 Soul Admin 启动在 9095 端口。使用浏览器,访问 http://127.0.0.1:9095/ 地址,进入登录页。 默认内置管理员账号「admin/123456」。输入账号密码,进入首页。 胖友可以自己随便点点,简单了解下有哪些功能。 搭建自己的网关(推荐) 首先你新建一个空的springboot项目,可以参考 soul-bootstrap. 也可以在spring官网:[https://spring.io/quickstart] 引入如下jar包:

  在你的 application.yaml 文件中加上如下配置:

  你的项目环境搭建完成,启动你的项目。 2.4 下一步 至此,我们已经完成了 Soul 服务的单机部署,是不是挺简单的。下面,胖友可以根据自己的需要,阅读如下小节: 「3. 接入 Dubbo 应用」 「4. 接入 Spring Boot 应用」 「5. 接入 Spring Cloud 应用」 3. 接入 Dubbo 应用 示例代码对应仓库:lab-60-soul-dubbo-demo-user-service 本小节,我们参考《Soul 文档 —— Dubbo 用户接入》文章,接入 Soul 服务网关。整个示例架构如下图所示: 下面,我们来开始正式接入 Dubbo 应用。 3.1 设置 dubbo 插件 先需要设置 dubbo 插件 的注册中心,因为 Soul Bootstrap 服务网关需要从注册中心获取到 Dubbo 服务的实例列表。 ① 使用浏览器,访问 http://127.0.0.1:9095/#/system/plugin 地址,进入「系统管理 -> 插件管理」菜单,可以看到 dubbo 插件。如下图所示: ② 点击 dubbo 插件的「编辑」按钮,设置注册中心的配置。如下图所示: 因为我们将使用 Nacos 作为 Dubbo 服务的注册中心,所以这里设置为 {"register":"nacos://localhost:8848"}。 如果胖友使用 Zookeeper 作为 Dubbo 服务的注册中心,所以这里设置为 {"register":"zookeeper://localhost:2181"}。 友情提示:注册中心的地址,记得要填写正确哈~ ③ 因为 Soul Bootstrap 和 Soul Admin 暂时不支持插件修改的自动加载,所以我们此时需要手动重启下。 3.2 在网关的pom.xml引入对 dubbo插件的依赖

  3.3 搭建 Dubbo 示例项目 先快速搭建一个 Dubbo 示例项目,暂未接入 Soul 网关。如下图所示: lab-60-soul-dubbo-demo-user-service-api lab-60-soul-dubbo-demo-user-service 友情提示:如果胖友对 Dubbo + Nacos 不了解的胖友,可以阅读《芋道 Spring Boot Dubbo 入门》文章。 下面,我们来将它改造接入 Soul 网关。 3.2.1 引入依赖 修改 pom.xml 文件,引入 soul-client-apache-dubbo 依赖,它是 Soul 对 Apache Dubbo 2.7.X 的集成支持。

  友情提示:如果胖友使用 Alibaba Dubbo 2.6.X 的话,依赖如下:

  3.2.2 配置文件 修改 application.yaml 配置文件,添加 Soul 配置项如下:

  soul.dubbo 配置项,Soul 对 Dubbo 的配置项,对应 DubboConfig 配置类。具体每个配置项的作用,胖友自己看配置项后的注释。 3.2.3 UserServiceImpl 需要在 Dubbo Service 实现类的方法上,添加 @SoulDubboClient 注解,用于设置每个 Dubbo 方法对应的请求路径。这里,我们修改 UserServiceImpl 类,添加该注解。代码如下:

  @SoulDubboClient 注解一共有三个属性: path:映射的 HTTP 接口的请求路径。 desc:接口的描述,便于知道其用途。 enable:是否开启,默认为 true 开启。 后续,在 Dubbo 服务启动时,SoulDubboClient 会自动解析 @SoulDubboClient 注解的 Dubbo 方法,写入方法的元数据到 Soul Admin 控制台,最终通知到 Soul Bootstrap 服务网关上。 3.3 简单测试 ① 执行 UserServiceApplication 启动 Dubbo 服务。在 IDEA 控制台可以看到如下日志,看到写入 Dubbo 方法的元数据到 Soul Admin 控制台。

 

③ 使用浏览器,访问 http://127.0.0.1:9095/#/plug/dubbo 地址,进入「插件列表 -> Dubbo」菜单,看到选择器和规则。如下图所示: 点击选择器 /user-api 的「编辑」按钮,查看该选择器的具体信息。如下图所示: 点击规则 /user-api/user/get 的「编辑」按钮,查看该规则的具体信息。如下图所示: ④ 使用 Postman 模拟请求 http://127.0.0.1:9195/user-api/user/get 地址,调用 UserServiceImpl#getUser(Integer id) 方法。如下图所示: 使用 Request Method 为 POST。 请求内容类型为 application/json。 因为 UserServiceImpl#getUser(Integer id) 方法是非 Bean 参数类型,所以直接在 Request Body 输入具体值即可。 ⑤ 使用 Postman 模拟请求 http://127.0.0.1:9195/user-api/user/create 地址,调用 UserServiceImpl#createUser(UserCreateDTO createDTO) 方法。如下图所示: 使用 Request Method 为 POST。 请求内容类型为 application/json。 因为 UserServiceImpl#createUser(UserCreateDTO createDTO) 方法是 Bean 参数类型,所以直接在 Request Body 输入 JSON 数据格式。 至此,我们已经完成了 Soul 网关接入 Dubbo 应用,并进行相应的而测试。 友情提示:实际上,我们也可以参考《Soul 文档 —— dubbo 插件》,手动在 Soul Admin 控制台配置 Dubbo 接口方法的元数据,以及进行 dubbo 插件的选择器和规则的设置,实现 Soul Bootstrap 服务网关转发请求到 Dubbo 服务,无需在 Dubbo 项目中引入 soul-client-apache-dubbo 依赖。 也就是说,引入 soul-spring-boot-starter-client-apache-dubbo 依赖的目的,是为了实现自动化,毕竟手工配置比较麻烦,并且容易出错。 4. 接入 Spring Boot 应用 在网关的pom.xml引入依赖

  示例代码对应仓库:lab-60-soul-spring-boot-demo 本小节,我们参考《Soul 文档 —— http 用户接入》文章,接入 Soul 服务网关。整个示例架构如下图所示: 下面,我们来开始正式接入 Spring Boot 应用。 4.1 设置 divide 插件 需要设置 divide 插件 为开启。该插件实现 HTTP 正向代理的功能,所有 HTTP 类型的请求你,都由它进行负载均衡的调用。 使用浏览器,访问 http://127.0.0.1:9095/#/system/plugin 地址,进入「系统管理 -> 插件管理」菜单,可以看到 divide 插件。如下图所示: 默认情况下,divide 插件已经是开启状态,所以无需开启。 4.2 搭建 Spring Boot 示例项目 先快速搭建一个 Spring Boot 示例项目,暂未接入 Soul 网关。如下图所示: lab-60-soul-spring-boot-demo 4.3.1 引入依赖 修改 pom.xml 文件,引入 soul-client-springmvc 依赖,它是 Soul 对 SpringMVC 的集成支持。

  4.3.2 配置文件 修改 application.yaml 配置文件,添加 Soul 配置项如下:

  soul.http 配置项,Soul 对 SpringMVC 的配置项,对应 SoulSpringMvcConfig 配置类。具体每个配置项的作用,胖友自己看配置项后的注释。 4.3.3 UserController 需要在 Controller 的 HTTP API 方法上,添加 @SoulSpringMvcClient 注解,用于设置每个 API 方法对应的请求路径。这里,我们修改 UserController 类,添加该注解。代码如下:

  @SoulSpringMvcClient 注解一共有三个属性: path:映射的 HTTP 接口的请求路径。 desc:接口的描述,便于知道其用途。 enable:是否开启,默认为 true 开启。 后续,在 Spring Boot 应用启动时,SoulSpringMvcClient 会自动解析 @SoulSpringMvcClien 注解的 API 方法,写入方法的元数据到 Soul Admin 控制台,最终通知到 Soul Bootstrap 服务网关上。 4.4 简单测试 ① 执行 DemoApplication 启动 Spring Boot 应用。在 IDEA 控制台可以看到如下日志,看到写入 HTTP API 方法的元数据到 Soul Admin 控制台。 ③ 使用浏览器,访问 http://127.0.0.1:9095/#/plug/divide 地址,进入「插件列表 -> Divide」菜单,看到选择器和规则。如下图所示: 点击选择器 /sb-demo-api 的「编辑」按钮,查看该选择器的具体信息。如下图所示: 点击规则 /sb-demo-api/user/get 的「编辑」按钮,查看该规则的具体信息。如下图所示: ④ 使用 Postman 模拟请求 http://127.0.0.1:9195/sb-demo-api/user/get?id=1 地址,转发到 GET /user/get 接口上。如下图所示: 使用 […]

TCC和两阶段提交

经常在网络上看见有人介绍TCC时,都提一句,”TCC是两阶段提交的一种”。其理由是TCC将业务逻辑分成try、confirm/cancel在两个不同的阶段中执行。其实这个说法,是不正确的。可能是因为既不太了解两阶段提交机制、也不太了解TCC机制的缘故,于是将两阶段提交机制的prepare、commit两个事务提交阶段和TCC机制的try、confirm/cancel两个业务执行阶段互相混淆,才有了这种说法。 两阶段提交(Two Phase Commit,下文简称2PC),简单的说,是将事务的提交操作分成了prepare、commit两个阶段。其事务处理方式为: 1、 在全局事务决定提交时,a)逐个向RM发送prepare请求;b)若所有RM都返回OK,则逐个发送commit请求最终提交事务;否则,逐个发送rollback请求来回滚事务; 2、 在全局事务决定回滚时,直接逐个发送rollback请求即可,不必分阶段。 * 需要注意的是:2PC机制需要RM提供底层支持(一般是兼容XA),而TCC机制则不需要。 TCC(Try-Confirm-Cancel),则是将业务逻辑分成try、confirm/cancel两个阶段执行,具体介绍见TCC事务机制简介。其事务处理方式为: 1、 在全局事务决定提交时,调用与try业务逻辑相对应的confirm业务逻辑; 2、 在全局事务决定回滚时,调用与try业务逻辑相对应的cancel业务逻辑。 可见,TCC在事务处理方式上,是很简单的:要么调用confirm业务逻辑,要么调用cancel逻辑。这里为什么没有提到try业务逻辑呢?因为try逻辑与全局事务处理无关。 当讨论2PC时,我们只专注于事务处理阶段,因而只讨论prepare和commit,所以,可能很多人都忘了,使用2PC事务管理机制时也是有业务逻辑阶段的。正是因为业务逻辑的执行,发起了全局事务,这才有其后的事务处理阶段。实际上,使用2PC机制时————以提交为例————一个完整的事务生命周期是:begin -> 业务逻辑 -> prepare -> commit。 再看TCC,也不外乎如此。我们要发起全局事务,同样也必须通过执行一段业务逻辑来实现。该业务逻辑一来通过执行触发TCC全局事务的创建;二来也需要执行部分数据写操作;此外,还要通过执行来向TCC全局事务注册自己,以便后续TCC全局事务commit/rollback时回调其相应的confirm/cancel业务逻辑。所以,使用TCC机制时————以提交为例————一个完整的事务生命周期是:begin -> 业务逻辑(try业务) -> commit(comfirm业务)。 综上,我们可以从执行的阶段上将二者一一对应起来: 1、 2PC机制的业务阶段 等价于 TCC机制的try业务阶段; 2、 2PC机制的提交阶段(prepare & commit) 等价于 TCC机制的提交阶段(confirm); 3、 2PC机制的回滚阶段(rollback) 等价于 TCC机制的回滚阶段(cancel)。 因此,可以看出,虽然TCC机制中有两个阶段都存在业务逻辑的执行,但其中try业务阶段其实是与全局事务处理无关的。认清了这一点,当我们再比较TCC和2PC时,就会很容易地发现,TCC不是两阶段提交,而只是它对事务的提交/回滚是通过执行一段confirm/cancel业务逻辑来实现,仅此而已。     参考博客: 1. https://blog.csdn.net/Paranoia_ZK/article/details/79481976#commentsedit  TCC和两阶段分布式事务处理的区别 2. https://blog.csdn.net/Saintyyu/article/details/100822735 X/Open DTP模型与XA协议之我见 3、https://blog.csdn.net/Saintyyu/article/details/101054542 分布式事务的七种实现方案汇总分析   from:https://blog.csdn.net/Saintyyu/article/details/100862449

终于有人把“TCC分布式事务”实现原理讲明白了!

之前网上看到很多写分布式事务的文章,不过大多都是将分布式事务各种技术方案简单介绍一下。很多朋友看了还是不知道分布式事务到底怎么回事,在项目里到底如何使用。 所以这篇文章,就用大白话+手工绘图,并结合一个电商系统的案例实践,来给大家讲清楚到底什么是 TCC 分布式事务。 首先说一下,这里可能会牵扯到一些 Spring Cloud 的原理,如果有不太清楚的同学,可以参考之前的文章:《拜托,面试请不要再问我Spring Cloud底层原理!》。 业务场景介绍 咱们先来看看业务场景,假设你现在有一个电商系统,里面有一个支付订单的场景。 那对一个订单支付之后,我们需要做下面的步骤: 更改订单的状态为“已支付” 扣减商品库存 给会员增加积分 创建销售出库单通知仓库发货 这是一系列比较真实的步骤,无论大家有没有做过电商系统,应该都能理解。 进一步思考 好,业务场景有了,现在我们要更进一步,实现一个 TCC 分布式事务的效果。 什么意思呢?也就是说,[1] 订单服务-修改订单状态,[2] 库存服务-扣减库存,[3] 积分服务-增加积分,[4] 仓储服务-创建销售出库单。 上述这几个步骤,要么一起成功,要么一起失败,必须是一个整体性的事务。 举个例子,现在订单的状态都修改为“已支付”了,结果库存服务扣减库存失败。那个商品的库存原来是 100 件,现在卖掉了 2 件,本来应该是 98 件了。 结果呢?由于库存服务操作数据库异常,导致库存数量还是 100。这不是在坑人么,当然不能允许这种情况发生了! 但是如果你不用 TCC 分布式事务方案的话,就用个 Spring Cloud 开发这么一个微服务系统,很有可能会干出这种事儿来。 我们来看看下面的这个图,直观的表达了上述的过程: 所以说,我们有必要使用 TCC 分布式事务机制来保证各个服务形成一个整体性的事务。 上面那几个步骤,要么全部成功,如果任何一个服务的操作失败了,就全部一起回滚,撤销已经完成的操作。 比如说库存服务要是扣减库存失败了,那么订单服务就得撤销那个修改订单状态的操作,然后得停止执行增加积分和通知出库两个操作。 说了那么多,老规矩,给大家上一张图,大伙儿顺着图来直观的感受一下: 落地实现 TCC 分布式事务 那么现在到底要如何来实现一个 TCC 分布式事务,使得各个服务,要么一起成功?要么一起失败呢? 大家稍安勿躁,我们这就来一步一步的分析一下。咱们就以一个 Spring Cloud 开发系统作为背景来解释。 TCC 实现阶段一:Try 首先,订单服务那儿,它的代码大致来说应该是这样子的:

如果你之前看过 Spring Cloud 架构原理那篇文章,同时对 Spring Cloud 有一定的了解的话,应该是可以理解上面那段代码的。 其实就是订单服务完成本地数据库操作之后,通过 Spring Cloud 的 Feign 来调用其他的各个服务罢了。 但是光是凭借这段代码,是不足以实现 TCC 分布式事务的啊?!兄弟们,别着急,我们对这个订单服务修改点儿代码好不好。 首先,上面那个订单服务先把自己的状态修改为:OrderStatus.UPDATING。 这是啥意思呢?也就是说,在 pay() 那个方法里,你别直接把订单状态修改为已支付啊!你先把订单状态修改为 UPDATING,也就是修改中的意思。 这个状态是个没有任何含义的这么一个状态,代表有人正在修改这个状态罢了。 然后呢,库存服务直接提供的那个 reduceStock() 接口里,也别直接扣减库存啊,你可以是冻结掉库存。 举个例子,本来你的库存数量是 100,你别直接 100 – 2 = 98,扣减这个库存! 你可以把可销售的库存:100 – 2 = 98,设置为 98 没问题,然后在一个单独的冻结库存的字段里,设置一个 2。也就是说,有 2 个库存是给冻结了。 积分服务的 addCredit() 接口也是同理,别直接给用户增加会员积分。你可以先在积分表里的一个预增加积分字段加入积分。 比如:用户积分原本是 1190,现在要增加 10 个积分,别直接 1190 + 10 = 1200 个积分啊! 你可以保持积分为 1190 不变,在一个预增加字段里,比如说 prepare_add_credit 字段,设置一个 10,表示有 10 个积分准备增加。 仓储服务的 saleDelivery() 接口也是同理啊,你可以先创建一个销售出库单,但是这个销售出库单的状态是“UNKNOWN”。 也就是说,刚刚创建这个销售出库单,此时还不确定它的状态是什么呢! 上面这套改造接口的过程,其实就是所谓的 TCC 分布式事务中的第一个 T 字母代表的阶段,也就是 Try 阶段。 总结上述过程,如果你要实现一个 TCC 分布式事务,首先你的业务的主流程以及各个接口提供的业务含义,不是说直接完成那个业务操作,而是完成一个 Try 的操作。 这个操作,一般都是锁定某个资源,设置一个预备类的状态,冻结部分数据,等等,大概都是这类操作。 咱们来一起看看下面这张图,结合上面的文字,再来捋一捋整个过程: TCC 实现阶段二:Confirm 然后就分成两种情况了,第一种情况是比较理想的,那就是各个服务执行自己的那个 Try 操作,都执行成功了,Bingo! 这个时候,就需要依靠 TCC 分布式事务框架来推动后续的执行了。这里简单提一句,如果你要玩儿 TCC 分布式事务,必须引入一款 TCC 分布式事务框架,比如国内开源的 ByteTCC、Himly、TCC-transaction。 否则的话,感知各个阶段的执行情况以及推进执行下一个阶段的这些事情,不太可能自己手写实现,太复杂了。 如果你在各个服务里引入了一个 TCC 分布式事务的框架,订单服务里内嵌的那个 TCC 分布式事务框架可以感知到,各个服务的 Try 操作都成功了。 此时,TCC 分布式事务框架会控制进入 TCC 下一个阶段,第一个 C 阶段,也就是 Confirm 阶段。 为了实现这个阶段,你需要在各个服务里再加入一些代码。比如说,订单服务里,你可以加入一个 Confirm 的逻辑,就是正式把订单的状态设置为“已支付”了,大概是类似下面这样子:

库存服务也是类似的,你可以有一个 InventoryServiceConfirm 类,里面提供一个 reduceStock() 接口的 Confirm 逻辑,这里就是将之前冻结库存字段的 2 个库存扣掉变为 0。 这样的话,可销售库存之前就已经变为 98 了,现在冻结的 2 个库存也没了,那就正式完成了库存的扣减。 积分服务也是类似的,可以在积分服务里提供一个 CreditServiceConfirm 类,里面有一个 addCredit() 接口的 Confirm 逻辑,就是将预增加字段的 10 个积分扣掉,然后加入实际的会员积分字段中,从 1190 变为 1120。 仓储服务也是类似,可以在仓储服务中提供一个 WmsServiceConfirm 类,提供一个 saleDelivery() 接口的 Confirm 逻辑,将销售出库单的状态正式修改为“已创建”,可以供仓储管理人员查看和使用,而不是停留在之前的中间状态“UNKNOWN”了。 好了,上面各种服务的 Confirm 的逻辑都实现好了,一旦订单服务里面的 TCC 分布式事务框架感知到各个服务的 Try 阶段都成功了以后,就会执行各个服务的 Confirm 逻辑。 订单服务内的 TCC 事务框架会负责跟其他各个服务内的 TCC 事务框架进行通信,依次调用各个服务的 Confirm 逻辑。然后,正式完成各个服务的所有业务逻辑的执行。 同样,给大家来一张图,顺着图一起来看看整个过程: TCC 实现阶段三:Cancel 好,这是比较正常的一种情况,那如果是异常的一种情况呢? 举个例子:在 Try 阶段,比如积分服务吧,它执行出错了,此时会怎么样? 那订单服务内的 TCC 事务框架是可以感知到的,然后它会决定对整个 TCC 分布式事务进行回滚。 也就是说,会执行各个服务的第二个 C 阶段,Cancel 阶段。同样,为了实现这个 Cancel 阶段,各个服务还得加一些代码。 首先订单服务,它得提供一个 OrderServiceCancel 的类,在里面有一个 pay() 接口的 Cancel 逻辑,就是可以将订单的状态设置为“CANCELED”,也就是这个订单的状态是已取消。 库存服务也是同理,可以提供 reduceStock() 的 Cancel 逻辑,就是将冻结库存扣减掉 2,加回到可销售库存里去,98 + 2 = 100。 积分服务也需要提供 addCredit() 接口的 Cancel 逻辑,将预增加积分字段的 10 个积分扣减掉。 仓储服务也需要提供一个 saleDelivery() 接口的 Cancel 逻辑,将销售出库单的状态修改为“CANCELED”设置为已取消。 然后这个时候,订单服务的 TCC 分布式事务框架只要感知到了任何一个服务的 Try 逻辑失败了,就会跟各个服务内的 TCC 分布式事务框架进行通信,然后调用各个服务的 Cancel 逻辑。 大家看看下面的图,直观的感受一下: 总结与思考 好了,兄弟们,聊到这儿,基本上大家应该都知道 TCC 分布式事务具体是怎么回事了! 总结一下,你要玩儿 TCC 分布式事务的话:首先需要选择某种 TCC 分布式事务框架,各个服务里就会有这个 TCC 分布式事务框架在运行。 然后你原本的一个接口,要改造为 3 个逻辑,Try-Confirm-Cancel: 先是服务调用链路依次执行 Try 逻辑。 如果都正常的话,TCC 分布式事务框架推进执行 Confirm 逻辑,完成整个事务。 如果某个服务的 Try 逻辑有问题,TCC 分布式事务框架感知到之后就会推进执行各个服务的 Cancel 逻辑,撤销之前执行的各种操作。 这就是所谓的 TCC 分布式事务。TCC 分布式事务的核心思想,说白了,就是当遇到下面这些情况时: 某个服务的数据库宕机了。 某个服务自己挂了。 那个服务的 Redis、Elasticsearch、MQ 等基础设施故障了。 某些资源不足了,比如说库存不够这些。 先来 Try 一下,不要把业务逻辑完成,先试试看,看各个服务能不能基本正常运转,能不能先冻结我需要的资源。 如果 Try 都 OK,也就是说,底层的数据库、Redis、Elasticsearch、MQ 都是可以写入数据的,并且你保留好了需要使用的一些资源(比如冻结了一部分库存)。 接着,再执行各个服务的 Confirm 逻辑,基本上 Confirm 就可以很大概率保证一个分布式事务的完成了。 那如果 Try 阶段某个服务就失败了,比如说底层的数据库挂了,或者 Redis 挂了,等等。 此时就自动执行各个服务的 Cancel 逻辑,把之前的 Try 逻辑都回滚,所有服务都不要执行任何设计的业务逻辑。保证大家要么一起成功,要么一起失败。 等一等,你有没有想到一个问题?如果有一些意外的情况发生了,比如说订单服务突然挂了,然后再次重启,TCC 分布式事务框架是如何保证之前没执行完的分布式事务继续执行的呢? 所以,TCC 事务框架都是要记录一些分布式事务的活动日志的,可以在磁盘上的日志文件里记录,也可以在数据库里记录。保存下来分布式事务运行的各个阶段和状态。 问题还没完,万一某个服务的 Cancel 或者 Confirm 逻辑执行一直失败怎么办呢? 那也很简单,TCC 事务框架会通过活动日志记录各个服务的状态。举个例子,比如发现某个服务的 Cancel 或者 Confirm 一直没成功,会不停的重试调用它的 Cancel 或者 Confirm 逻辑,务必要它成功! 当然了,如果你的代码没有写什么 Bug,有充足的测试,而且 Try 阶段都基本尝试了一下,那么其实一般 Confirm、Cancel 都是可以成功的! 最后,再给大家来一张图,来看看给我们的业务,加上分布式事务之后的整个执行流程: 不少大公司里,其实都是自己研发 TCC 分布式事务框架的,专门在公司内部使用,比如我们就是这样。 不过如果自己公司没有研发 TCC 分布式事务框架的话,那一般就会选用开源的框架。 这里笔者给大家推荐几个比较不错的框架,都是咱们国内自己开源出去的:ByteTCC,TCC-transaction,Himly。 大家有兴趣的可以去它们的 GitHub 地址,学习一下如何使用,以及如何跟 Spring Cloud、Dubbo 等服务框架整合使用。 只要把那些框架整合到你的系统里,很容易就可以实现上面那种奇妙的 TCC 分布式事务的效果了。 下面,我们来讲讲可靠消息最终一致性方案实现的分布式事务,同时聊聊在实际生产中遇到的运用该方案的高可用保障架构。 最终一致性分布式事务如何保障实际生产中 99.99% 高可用? 上面咱们聊了聊 TCC 分布式事务,对于常见的微服务系统,大部分接口调用是同步的,也就是一个服务直接调用另外一个服务的接口。 这个时候,用 TCC 分布式事务方案来保证各个接口的调用,要么一起成功,要么一起回滚,是比较合适的。 但是在实际系统的开发过程中,可能服务间的调用是异步的。也就是说,一个服务发送一个消息给 MQ,即消息中间件,比如 RocketMQ、RabbitMQ、Kafka、ActiveMQ 等等。 然后,另外一个服务从 MQ 消费到一条消息后进行处理。这就成了基于 MQ 的异步调用了。 那么针对这种基于 MQ 的异步调用,如何保证各个服务间的分布式事务呢?也就是说,我希望的是基于 MQ 实现异步调用的多个服务的业务逻辑,要么一起成功,要么一起失败。 这个时候,就要用上可靠消息最终一致性方案,来实现分布式事务。 大家看上图,如果不考虑各种高并发、高可用等技术挑战的话,单从“可靠消息”以及“最终一致性”两个角度来考虑,这种分布式事务方案还是比较简单的。 可靠消息最终一致性方案的核心流程 ①上游服务投递消息 如果要实现可靠消息最终一致性方案,一般你可以自己写一个可靠消息服务,实现一些业务逻辑。 首先,上游服务需要发送一条消息给可靠消息服务。这条消息说白了,你可以认为是对下游服务一个接口的调用,里面包含了对应的一些请求参数。 然后,可靠消息服务就得把这条消息存储到自己的数据库里去,状态为“待确认”。 接着,上游服务就可以执行自己本地的数据库操作,根据自己的执行结果,再次调用可靠消息服务的接口。 如果本地数据库操作执行成功了,那么就找可靠消息服务确认那条消息。如果本地数据库操作失败了,那么就找可靠消息服务删除那条消息。 此时如果是确认消息,那么可靠消息服务就把数据库里的消息状态更新为“已发送”,同时将消息发送给 MQ。 这里有一个很关键的点,就是更新数据库里的消息状态和投递消息到 MQ。这俩操作,你得放在一个方法里,而且得开启本地事务。 啥意思呢?如果数据库里更新消息的状态失败了,那么就抛异常退出了,就别投递到 MQ;如果投递 MQ 失败报错了,那么就要抛异常让本地数据库事务回滚。这俩操作必须得一起成功,或者一起失败。 如果上游服务是通知删除消息,那么可靠消息服务就得删除这条消息。 ②下游服务接收消息 下游服务就一直等着从 MQ 消费消息好了,如果消费到了消息,那么就操作自己本地数据库。 如果操作成功了,就反过来通知可靠消息服务,说自己处理成功了,然后可靠消息服务就会把消息的状态设置为“已完成”。 ③如何保证上游服务对消息的 100% 可靠投递? 上面的核心流程大家都看完:一个很大的问题就是,如果在上述投递消息的过程中各个环节出现了问题该怎么办? 我们如何保证消息 100% 的可靠投递,一定会从上游服务投递到下游服务?别着急,下面我们来逐一分析。 如果上游服务给可靠消息服务发送待确认消息的过程出错了,那没关系,上游服务可以感知到调用异常的,就不用执行下面的流程了,这是没问题的。 如果上游服务操作完本地数据库之后,通知可靠消息服务确认消息或者删除消息的时候,出现了问题。 比如:没通知成功,或者没执行成功,或者是可靠消息服务没成功的投递消息到 MQ。这一系列步骤出了问题怎么办? 其实也没关系,因为在这些情况下,那条消息在可靠消息服务的数据库里的状态会一直是“待确认”。 此时,我们在可靠消息服务里开发一个后台定时运行的线程,不停的检查各个消息的状态。 如果一直是“待确认”状态,就认为这个消息出了点什么问题。此时的话,就可以回调上游服务提供的一个接口,问问说,兄弟,这个消息对应的数据库操作,你执行成功了没啊? 如果上游服务答复说,我执行成功了,那么可靠消息服务将消息状态修改为“已发送”,同时投递消息到 MQ。 如果上游服务答复说,没执行成功,那么可靠消息服务将数据库中的消息删除即可。 通过这套机制,就可以保证,可靠消息服务一定会尝试完成消息到 MQ 的投递。 ④如何保证下游服务对消息的 100% 可靠接收? 那如果下游服务消费消息出了问题,没消费到?或者是下游服务对消息的处理失败了,怎么办? 其实也没关系,在可靠消息服务里开发一个后台线程,不断的检查消息状态。 如果消息状态一直是“已发送”,始终没有变成“已完成”,那么就说明下游服务始终没有处理成功。 此时可靠消息服务就可以再次尝试重新投递消息到 MQ,让下游服务来再次处理。 只要下游服务的接口逻辑实现幂等性,保证多次处理一个消息,不会插入重复数据即可。 ⑤如何基于 RocketMQ 来实现可靠消息最终一致性方案? 在上面的通用方案设计里,完全依赖可靠消息服务的各种自检机制来确保: 如果上游服务的数据库操作没成功,下游服务是不会收到任何通知。 如果上游服务的数据库操作成功了,可靠消息服务死活都会确保将一个调用消息投递给下游服务,而且一定会确保下游服务务必成功处理这条消息。 通过这套机制,保证了基于 MQ 的异步调用/通知的服务间的分布式事务保障。其实阿里开源的 RocketMQ,就实现了可靠消息服务的所有功能,核心思想跟上面类似。 只不过 RocketMQ 为了保证高并发、高可用、高性能,做了较为复杂的架构实现,非常的优秀。有兴趣的同学,自己可以去查阅 RocketMQ 对分布式事务的支持。 可靠消息最终一致性方案的高可用保障生产实践 背景引入 上面那套方案和思想,很多同学应该都知道是怎么回事儿,我们也主要就是铺垫一下这套理论思想。 在实际落地生产的时候,如果没有高并发场景的,完全可以参照上面的思路自己基于某个 MQ 中间件开发一个可靠消息服务。 如果有高并发场景的,可以用 RocketMQ 的分布式事务支持上面的那套流程都可以实现。 今天给大家分享的一个核心主题,就是这套方案如何保证 99.99% 的高可用。 大家应该发现了这套方案里保障高可用性最大的一个依赖点,就是 MQ 的高可用性。 任何一种 MQ 中间件都有一整套的高可用保障机制,无论是 RabbitMQ、RocketMQ 还是 Kafka。 所以在大公司里使用可靠消息最终一致性方案的时候,我们通常对可用性的保障都是依赖于公司基础架构团队对 MQ […]