第十二章 RocketMQ消息队列

352 阅读27分钟

在微服务架构中分布式事务是我们绕不开问题,比如电商项目下单业务中,先要调用库存微服务扣减库存,然后调用订单微服务下单,在订单微服务中又通过Feign调用用户微服务,增加用户积分。最终才能完成一个下单业务,而在不同微服务中可能又要操作不同的数据库,如何保证这种业务的事务一致性,是本章讨论的问题。

11.1分布式事务简介

分布式事务是指在微服务架构下保证事务的特性,包括原子性、一致性、隔离性和持久性。

11.1.1 事务介绍

数据库事务(简称:事务,Transaction)是指数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。如转账业务:一方扣款,一方增加金额。

事务的四个特性如下。

  • 原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
  • 一致性(Consistency):指的是操作前后,总数据保持保持不变。(-100元与+100元)。
  • 隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行,张三取钱不会影响李四取钱。
  • 持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中,在事务结束时,此操作将不可逆转。

11.1.2 分布式事务介绍

简单讲,在分布式系统中进行的事务就是分布式事务。意思就是说,在分布式系统中,也要保证事务的四个特性。分布式事务有下面几种模式。

  • 单个服务多数据源分布式事务。
  • 多服务分布式事务。
  • 多服务多数据源分布式事务(微服务中的典型模型)。
  1. 单一服务分布式事务

单一服务的分布式事务,是指单体架构下,一个服务操作,不涉及服务间的相互调用,但是这个服务会涉及多个数据库资源的访问,单一服务分布式事务模型,如图5-1所示。

图11-1 单一服务分布式事务模型

  1. 多服务分布式事务

上面的分布式事务架构,虽然设计多个不同的数据库资源,但是整个事务还是控制在单一服务的内部实现,如果一个服务业务逻辑,需要调用另外一个服务,比如下单服务,需要调用库存服务扣减库存,这时事务就需要阔多个服务了,在这种情况下,起始于某个服务的事务在调用另一个服务的时候需要以一定的机制流转到另外一个服务,从而是被调用的服务访问的数据库资源也能纳入到该事务的管理中。这种架构,就成为多服务分布式事务。如图11-2所示。

图11-2  多服务分布式事务

  1. 多服务多数据源分布式事务

如果将上面两种场景结合,服务可以调用多个数据库资源,而一个服务又可以调用其他服务,完成一个业务操作。
比如下单业务需要调用库存微服务扣减库存,库存微服务操作商品库,下单业务有需要调用用户微服务,实现增加用户积分操作,用户微服务又调用用户库,这样一个下单业务,就需要调用多个微服务,访问多个数据库。
多个微服务操作不同的数据库,整个分布式事务的参与者将会组成一个树形的拓扑结构,我们要实现的分布式事务就是这种结构,在一个跨服务的分布式事务中,事务的发起者和提交者均系同一个,他可以是整个调用的客户端,也可以是客户端最先调用的那个服务。多服务多数据源分布式事务模型,如图11-3所示。

图11-3  多服务多数据源分布式事务模型

较之基于单一数据库资源访问的本地事务,分布式事务的应用架构更为复杂。在不同的分布式应用架构下,实现一个分布式事务要考虑的问题并不完全一样,比如对多资源的协调、事务的跨服务传播等,实现机制也是复杂多变。分布式事务的应用架构如图11-4所示。

5-4 分布式事务的应用架构\

11.2 分布式事务解决方案

在分布式系统中,要实现分布式事务,无外乎那几种解决方案,2PC、TCC、本地消息表、事务消息。\

11.2.1 两阶段提交(2PC)

2PC 即两阶段提交协议,是将整个事务流程分为两个阶段,准备阶段(Prepare phase)、提交阶段(commit phase)。部分关系数据库如 Oracle、MySQL都支持两阶段提交协议。

  • 准备阶段(Prepare phase):事务管理器给每个参与者发送 Prepare 消息,每个数据库参与者在本地执行事务,并写本地的 Undo/Redo 日志,此时事务没有提交。(Undo 日志是记录修改前的数据,用于数据库回滚,Redo 日志是记录修改后的数据,用于提交事务后写入数据文件)。
  • 提交阶段(commit phase):如果事务管理器收到了参与者的执行失败或者超时消息时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据事务管理器的指令执行提交或者回滚操作,并释放事务处理过程中使用的锁资源。注意:必须在最后阶段释放锁资源。
    分析两阶段提交的分布式事务又分为两种情况。
  1. 当所有参与者均反馈yes
    当所有参与者均反馈yes,这时提交事务,步骤,如图11-5所示。
  • 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。
  • 参与者执行 commit 请求,并释放整个事务期间占用的资源。
  • 各参与者向协调者反馈 ack(应答)完成的消息。
  • 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。

图11-5  两阶段事务正常提交\

  1. 一个参与者反馈no
    当阶段1的一个参与者反馈no,此时中断事务,步骤如下,如图11-6所示。
  • 协调者向所有参与者发出回滚请求(即 rollback 请求)。
  • 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。
  • 各参与者向协调者反馈 ack 完成的消息。
  • 协调者收到所有参与者反馈的 ack 消息后,即完成事务中断。

图11-6 二阶段事务中断

2PC方案总结如下。

  • 性能问题:所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。
  • 可靠性问题:如果协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态。
  • 数据一致性问题:在阶段 2 中,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。

11.2.2 补偿事务(TCC)


TCC 就是采用的补偿机制,其核心思想是:针对每个操作,都要编写一个与其对应的确认和补偿(撤销)操作逻辑。它分为三个阶段。

  • Try 阶段主要是对业务系统做检测及资源预留。
  • Confirm 阶段主要是对业务系统做确认提交,Try阶段执行成功并开始执行 Confirm阶段时,默认 Confirm阶段是不会出错的。即:只要Try成功,Confirm一定成功。
  • Cancel 阶段主要是在业务执行错误,需要回滚的状态下执行的业务取消,预留资源释放。

\

举个例子,假设张三要向李四转账,思路大概是。
我们有一个本地方法,里面依次调用。

  • 首先在 Try 阶段,要先调用远程接口把 张三和李四的钱给冻结起来。
  • 在 Confirm 阶段,执行远程调用的转账的操作,转账成功进行解冻。
  • 如果第2步执行成功,那么转账成功,如果第二步执行失败,则调用远程冻结接口对应的解冻方法 (Cancel)。

TCC事务总结,优点如下。

  • 性能提升:具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。
  • 跟2PC比起来,实现以及流程相对简单了一些。
  • 可靠性:解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。\

TCC缺点还是比较明显的,在2,3步中都有可能失败。TCC属于应用层的一种补偿方式,所以需要程序员在实现的时候多写很多补偿的代码,在一些场景中,一些业务流程可能用TCC不太好定义及处理。\

11.2.3 本地消息表


本地消息表的方案最初是由 eBay 提出,核心思路是将分布式事务拆分成本地事务进行处理。如图11-7所示。

图11-7 本地消息表方案

消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务方法多次执行失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。
这种方案遵循BASE理论,采用的是最终一致性,笔者认为是这几种方案里面比较适合实际业务场景的,即不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或者回滚不了的情况。
本地消息表事务优点:这种方案是一种非常经典的实现,避免了分布式事务,实现了最终一致性。
本地消息表事务缺点:与具体的业务场景绑定,耦合性强,消息数据与业务数据同库,占用业务系统资源。业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。

11.2.4 MQ 事务消息


RocketMQ是支持事务消息的,事务消息的方式类似于采用的二阶段提交。
业务方法内要想消息队列提交两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。

RocketMQ的事务消息分为两个流程,其一是正常事务消息的发送及提交,其二事务消息的补偿流程。

  1. 正常事务消息的发送提交

事务消息正常情况下发送放向MQ Server进行二次确认,步骤如下,如图11-8所示。

  • 步骤1发送方向 MQ 服务端(MQ Server)发送 half 消息。
  • 步骤2 MQ Server 将消息持久化成功之后,向发送方 ack 确认消息已经发送成功。
  • 步骤3发送方开始执行本地事务逻辑。
  • 步骤4发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。
  • 步骤5 MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。

图11-8 正常情况-事务主动方发消息

  1. 事务消息补偿流程

图11-9中步骤4提交的二次确认超时未能到达 MQ Server,此时MQ Server发起消息回查,此时处理步骤如下,如图5-9所示。

图11-9 异常情况

  • 步骤5 MQ Server 对该消息发起消息回查。
     步骤6发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
     步骤7 发送方根据检查得到的本地事务的最终状态再次提交二次确认。
     步骤8 MQ Server基于 commit/rollback 对消息进行投递或者删除。

MQ事务消息优点,实现了最终一致性,不需要依赖本地数据库事务。
MQ事务消息缺点,实现难度大,主流MQ不支持,比如RabbitMQ和Kafka不支持事务消息。

11.3 Seata四种模式

\

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

11.3.1 AT模式


这是Seata的一大特色,AT对业务代码完全无侵入性,使用非常简单,改造成本低。我们只需要关注自己的业务SQL,Seata会通过分析我们业务SQL,反向生成回滚数据,在 AT 模式下,用户只需关心自己的 “业务SQL” AT 模式分为两个阶段,如图5-10所示。

  • 一阶段,所有参与事务的分支,本地事务Commit 业务数据和写入回滚日志(undoLog)。
  • 二阶段,事务协调者根据所有分支的情况,决定本次全局事务是Commit 还是 Rollback。

图11-10 AT 模式的两个阶段
第一阶段Seata 的 JDBC 数据源代理通过对业务 SQL 的解析,把业务数据在更新前后的数据镜像组织成回滚日志,利用 本地事务 的 ACID 特性,将业务数据的更新和回滚日志的写入在同一个 本地事务 中提交。这样,可以保证:任何提交的业务数据的更新一定有相应的回滚日志存在,AT 模式第一阶段如图11-11所示。

图5-11  AT 模式第一阶段
基于这样的机制,分支的本地事务便可以在全局事务的第一阶段提交,并马上释放本地事务锁定的资源。
这也是Seata和XA事务的不同之处,两阶段提交往往对资源的锁定需要持续到第二阶段实际的提交或者回滚操作,而有了回滚日志之后,可以在第一阶段释放对资源的锁定,降低了锁范围,提高效率,即使第二阶段发生异常需要回滚,只需找对undolog中对应数据并反解析成SQL来达到回滚目的。
同时Seata通过代理数据源将业务SQL的执行解析成undolog来与业务数据的更新同时入库,达到了对业务无侵入的效果。
第二阶段如果决议是全局提交,此时分支事务此时已经完成提交,不需要同步协调处理(只需要异步清理回滚日志),Phase2 可以非常快速地完成。
如果决议是全局回滚,RM 收到协调器发来的回滚请求,通过 XID 和 Branch ID 找到相应的回滚日志记录,通过回滚记录生成反向的更新 SQL 并执行,以完成分支的回滚。
AT 模式的一阶段、二阶段提交和回滚均由 Seata 框架自动生成,用户只需编写“业务 SQL”,便能轻松接入分布式事务,AT 模式是一种对业务无任何侵入的分布式事务解决方案。

11.3.2 XA模式

\

XA 模式是 Seata 另一种无侵入的分布式事务解决方案,XA模式在 Seata 定义的分布式事务框架内,利用事务资源(数据库、消息服务等)对 XA 协议的支持,以 XA 协议的机制来管理分支事务的一种事务模式, XA要求数据库本身提供对规范和协议的支持。
从编程模型上,XA 模式与 AT 模式保持完全一致。只需要修改数据源代理,即可实现 XA 模式与 AT 模式之间的切换。代码如下所示。

1.	@Bean("dataSource")  
2.	    public DataSource dataSource(DruidDataSource druidDataSource) {  
3.	        // DataSourceProxy for AT mode  
4.	        // return new DataSourceProxy(druidDataSource);  
5.	  
6.	        // DataSourceProxyXA for XA mode  
7.	        return new DataSourceProxyXA(druidDataSource);  
8.	    }  

11.3.3 TCC模式


Seata中的TCC 模式同样包含三个阶段。如图11-12所示。

  • Try 阶段 :所有参与分布式事务的分支,对业务资源进行检查和预留。
  • Confirm阶段:所有分支的Try全部成功后,执行业务提交。
  • Cancel阶段:取消Try阶段预留的业务资源。

图11-12 TCC模式
对比AT或者XA模式来说,TCC模式需要我们自己抽象并实现Try,Confirm,Cancel三个接口,编码量会大一些,但是由于事务的每一个阶段都由开发人员自行实现。而且相较于AT模式来说,减少了SQL解析的过程,也没有全局锁的限制,所以TCC模式的性能是优于AT 、XA模式。但是带来的是工作量的增加,简单和高效果然难以共存。

11.3.4 Sage模式


Saga 是长事务解决方案,每个参与者需要实现事务的正向操作和补偿操作。当参与者正向操作执行失败时,回滚本地事务的同时,会调用上一阶段的补偿操作,在业务失败时最终会使事务回到初始状态。如图11-13所示。

图11-13 Sage模式

Saga与TCC类似,同样没有全局锁。由于相比缺少锁定资源这一步,在某些适合的场景,Saga要比TCC实现起来更简单。
Saga 模式是长事务解决方案,适用于业务流程长且需要保证事务最终一致性的业务系统,Saga 模式一阶段就会提交本地事务,无锁,长流程情况下可以保证性能,多用于渠道层、集成层业务系统。事务参与者可能是其它公司的服务或者是遗留系统的服务,无法进行改造和提供 TCC 要求的接口,也可以使用 Saga 模式。

11.4 Seata最佳实践

用户下单操作,往往会设计多少个微服务调用,和多个数据库访问,下面在订单业务中具体实现Seata的AT模式的分布式事务。

11.4.1 需求介绍

一个下单业务,需要调用库存微服务扣减库存,也需要调用用户微服务扣除会员余额,具体步骤分析如下,如图11-14所示。

  • 用户请求下单业务微服务(Bussiness),请求下单。
  • Bussiness通过Open Feign调用库存微服务(Storage),扣减库存。
  • Bussiness通过Open Feign调用订单微服务(Order),创建订单。
  • 订单微服务(Order),通过Open Feign调用用户微服务(Account),执行扣款操作。

图11-14 用户下单

订单业务事务管理项目目录结构如图11-15所示。

图11-15  订单业务事务管理项目目录截图\

11.4.2 数据库介绍

\

案例中用到的数据库分析如图11-16所示。

图11-16 事务管理表结构

下单业务设计有4个表,内容如下。

  • Account_tbl:下单后,由订单微服务调用用户微服务,执行扣款操作,account_tbl表中的会员余额扣减。
  • Storage_tbl:下单后,有bussiness微服务调用storage微服务,执行storate_tbl表中库存扣减。
  • Order_tbl:bussiness微服务调用订单微服务,创建订单。
  • Undo_log:Seata,处理分布式事务过程中存储日志的表。

11.4.3 Seata Server

seata-server是seata中的事务协调器,从官网下载seater-server 1.4 解压安装。该项目由两个主要的配置文件 registy.conf和file.conf,文件位置如图11-17所示。

图11-17 seata-server配置

\

  1. registry.conf

默认情况下,seata-server的配置模式是file模式,由registy.conf的registy.type和config.type属性确定,该模式下seata-server的配置都是走配置文件,该配置文件的名称在registry.file.name和config.file.name属性中确定,默认都是file.conf,因此,该项目在不修改配置文件的情况下也可以正常启动,走默认配置。
当然也支持Nacos 、Eureka、Redis、ZK、Consul、Etcd3、Sofa等多种配置方式,我们使用Nacos模式,删除其他无用的配置方式后,registry.conf的结构精简如下:

	registry {  
	  # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa  
	  type = " nacos"  
	  
	  file {  
	    name = "file.conf"  
	  }  
	}  
	  
	config {  
	  # file、nacos 、apollo、zk、consul、etcd3  
	  type = "file"  
	  file {  
	    name = "file.conf"  
	  }  
	}
  1. file.conf文件


file文件主要配置seata-server的各种属性,也可以完全不修改,使用默认配置。
seata-server的存储模式有file和db两种,可以通过store.mode属性配置,默认的存储方式是file。
file模式下,seata的事务相关信息会存储在内存,并持久化到root.data文件中,这种模式性能较高。
db模式是一种高可用的模式,seata的全局事务,分支事务和锁都在数据库中存储。
如果是db模式,找到db模块 修改数据库配置信息,根据自己的数据库,修改数据库IP,端口号,用户名和密码,具体如下。

	## transaction log store  
	store {  
	  ## store mode: file、db  
	  mode = "file"  
	  
	  ## file store  
	  file {  
	    dir = "sessionStore"  
	  
	    # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions  
	    max-branch-session-size = 16384  
	    # globe session size , if exceeded throws exceptions  
	    max-global-session-size = 512  
	    # file buffer size , if exceeded allocate new buffer  
	    file-write-buffer-cache-size = 16384  
	    # when recover batch read size  
	    session.reload.read_size = 100  
	    # async, sync  
	    flush-disk-mode = async  
	  }  
	  
	  ## database store  
	  db {  
	    ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.  
	    datasource = "dbcp"  
	    ## mysql/oracle/h2/oceanbase etc.  
	    db-type = "mysql"  
	    driver-class-name = "com.mysql.jdbc.Driver"  
	    url = "jdbc:mysql://***:3306/seata"  
	    user = "***"  
	    password = "***"  
	    min-conn = 1  
	    max-conn = 3  
	    global.table = "global_table"  
	    branch.table = "branch_table"  
	    lock-table = "lock_table"  
	    query-limit = 100  
	  }  
	}  

11.4.4 父工程

\

使用maven聚合父工程,统一管理Spring Boot 、Spring Cloud、Spring Cloud Alibaba版本号,具体pom.xml代码如下。

  • Spring Boot 2.4.9
  • Spring Cloud 2020.0.3
  • Spring Cloud Alibaba 2021.1
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.lxs.demo</groupId>
    <artifactId>springcloud-seata</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>storage</module>
        <module>account</module>
        <module>order</module>
        <module>bussiness</module>
    </modules>


    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.9</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <java.version>1.8</java.version>
        <alibaba-cloud.version>2021.1</alibaba-cloud.version>
        <springcloud.version>2020.0.3</springcloud.version>
    </properties>


    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${springcloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>${alibaba-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.11</version>
            </dependency>
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                <version>1.1.10</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.9</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

11.4.5 库存微服务

案例中,库存微服务由下单微服务调用,使用JDBC完成下单后减扣库存功能。

  1. 导入依赖

在pom.xml中导入Seata相关的依赖组件,代码如下。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>springcloud-seata</artifactId>
        <groupId>com.lxs.demo</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>storage</artifactId>

    <dependencies>

        <!--sentinel-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>
        <!--nacos config-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
        </dependency>
        <!--nacos-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

        <!--open feign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>

        <!-- SpringBoot整合Web组件 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
        </dependency>

    </dependencies>


</project>

\

  1. 配置类

在配置类中,配置使用Seata的AT模式,需要配置“io.seata.rm.datasource.DataSourceProxy”数据源代理对象,其中@Primary表示优先使用此对象,作为系统中的数据源对象。代码如下。

@Configuration
public class DataSourceConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }

    /**
     * 配置的seata代理数据源
     * @param dataSource
     * @return
     */
    @Primary
    @Bean
    public DataSourceProxy dataSourceProxy(DataSource dataSource) {
        return new DataSourceProxy(dataSource);
    }

    @Bean("jdbcTemplate")
    @ConditionalOnBean(DataSourceProxy.class)
    public JdbcTemplate jdbcTemplate(DataSourceProxy dataSourceProxy) {
        return new JdbcTemplate(dataSourceProxy);
    }

}

\

  1. 控制层

控制组件StorageController,调用StorageService组件,完成库存扣减工作。代码如下。

@RestController
public class StorageController {

    @Autowired
    private StorageService storageService;

    @RequestMapping(value = "/deduct", produces = "application/json")
    public Boolean deduct(String commodityCode, Integer count) {
        storageService.deduct(commodityCode, count);
        return true;
    }
}

11.4.6 配置文件详解

微服务工程中的Seata配置文件,如图11-18所示。

图11-18配置文件
file.conf中service配置如下。

service {
  #transaction service group mapping,配置Seata Server在注册中心注册的服务名
  vgroupMapping.my_test_tx_group = "default"
  #配置Client连接Seata Server的地址
  default.grouplist = "127.0.0.1:8091"
  #degrade, current not support
  enableDegrade = false
  #disable seata
  disableGlobalTransaction = false
}

application.properties配置文件,需要保证spring.cloud.alibaba.seata.tx-service-group配置的事务组和file.conf中的service配置一致,配置关系如图11-19所示。

图11-19 application.properties配置
完整application.properties代码如下。

spring.application.name=storage-service
server.port=8081
#spring.datasource.url=jdbc:mysql://192.168.220.110:3306/fescar?useSSL=false&serverTimezone=UTC
spring.datasource.url=jdbc:mysql://localhost:3306/fescar?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.cloud.alibaba.seata.tx-service-group=my_test_tx_group
logging.level.org.springframework.cloud.alibaba.seata.web=debug
logging.level.io.seata=debug

spring.cloud.nacos.discovery.server-addr = localhost:8848
spring.cloud.sentinel.transport.dashboard = localhost:8080
spring.cloud.sentinel.transport.port = localhost:8719
feign.sentinel.enabled=  true


spring.main.allow-bean-definition-overriding=true

其他工程都是用上述配置文件相似。修改响应的微服务的配置端口号即可。这里就不在其他微服务工程中重复阐述了。

11.4.7 用户微服务

创建用户微服务,用户微服务,由订单微服务调用,完成下单扣减用户余额逻辑。

  1. 引入依赖
    用户微服务工程pom.xml跟库存相似,参考库存pom.xml,相应修改。

 

  1. 业务层

用户微服务业务AccountService.java类,使用JDBC完成用户扣减余额功能。代码如下。

@Service
public class AccountService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void reduce(String userId, int money) {
        jdbcTemplate.update("update account_tbl set money = money - ? where user_id = ?", new Object[] {money, userId});
    }
}
  1. 控制层

控制层AccountController类调用accountService完成用户扣减余额功能,代码如下。

@RestController
public class AccountController {

    @Autowired
    private AccountService accountService;

    @RequestMapping(value = "/reduce", produces = "application/json")
    public Boolean debit(String userId, int money) {
        accountService.reduce(userId, money);
        return true;
    }
}

11.4.8 订单微服务


创建订单微服务order,订单微服务有业务微服务bussiness微服务调用,同时又通过Feign调用用户微服务,完成下单业务。

  1. 引入依赖


订单微服务工程pom.xml跟库存相似,参考库存pom.xml,相应修改。

  1. UserFeign


在订单微服务中通过Open Feign调用用户微服务实现扣款功能,UserFeignClient代码如下。

@FeignClient(name = "account-service", url = "127.0.0.1:8083")
public interface UserFeignClient {

    @GetMapping("/reduce")
    Boolean reduce(@RequestParam("userId") String userId, @RequestParam("money") int money);
}
  1. 业务层

业务层类OrderService,使用JdbcTemlate对象,调用jdbc完成创建订单工作,同时使用UserFeignClient对象,调用用户微服务完成,用户余额扣款功能,代码如下。

@Service
public class OrderService {

    @Autowired
    private UserFeignClient userFeignClient;

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void create(String userId, String commodityCode, Integer count) {

        int orderMoney = count * 100;
        jdbcTemplate.update("insert order_tbl(user_id,commodity_code,count,money) values(?,?,?,?)",
                new Object[] {userId, commodityCode, count, orderMoney});

        userFeignClient.reduce(userId, orderMoney);

    }
}

\

  1. 控制层

控制层,通过调用OrderService,完成下单和扣除用户余额功能,代码如下。

@RestController
public class OrderController {

    @Autowired
    private OrderService orderService;

    @GetMapping(value = "/create", produces = "application/json")
    public Boolean create(String userId, String commodityCode, Integer count) {

        orderService.create(userId, commodityCode, count);
        return true;
    }

}

11.4.9 业务微服务

创建bussiness业务微服务,通过Feign调用库存微服务,和订单微服务,实现下单业务,分布式事务,也在此微服务进行控制。

  1. 引入依赖\

订单微服务工程pom.xml跟库存相似,参考库存pom.xml,相应修改。

\

  1. 配置类

在业务微服务中,只是执行基本的数据库操作,不涉及分布式事务,所以这里直接使用普通DataSource即可,代码如下。

@Configuration
public class DataSourceConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        DruidDataSource druidDataSource = new DruidDataSource();
        return druidDataSource;
    }

    @Bean("jdbcTemplate")
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
        return new JdbcTemplate(dataSource);
    }

}
  1. FeignClient


通过OrderFeignClient调用订单微服务实现下单,订单微服务又通过Feign调用用户微服务实现扣款,代码如下。

@FeignClient(name = "order-service", url = "127.0.0.1:8082")
public interface OrderFeignClient {

    @GetMapping("/create")
    void create(@RequestParam("userId") String userId,
                @RequestParam("commodityCode") String commodityCode,
                @RequestParam("count") Integer count);

}

通过StorageFeignClient调用库存微服务,实现消减库存操作,代码如下。

@FeignClient(name = "storage-service", url = "127.0.0.1:8081")
public interface StorageFeignClient {

    @GetMapping("/deduct")
    void deduct(@RequestParam("commodityCode") String commodityCode,
                @RequestParam("count") Integer count);

}
  1. 业务层

业务类中purchase方法通过storageFeignClient调用库存微服务,实现库存扣减功能,通过orderFeignClient,实现创建订单功能,订单微服务又通过userFeignClient调用用户微服务,扣减用户余额。最后在purchase方法中,通过validData方法,查询用户余额和库存数,如果余额和库存不足,则抛出RuntimeException运行期异常,回滚分布式事务。
purchase方法使用@GlobalTransactional注解控制分布式事务能力。我们可以看到Seata的只需要一个注解即可以在复杂的微服务架构下,完成分布式事务。

@PostConstruct修饰的initData方法会在服务器容器加载的时候运行,并且只会被服务器执行一次,此处initData方法,在服务重启时,初始化数据库数据,代码如下。

\

5.控制层

控制器组件提供2个方法,具体如下。

  • purchaseCommit:事务正常提交方法,购买数量30小于库存数量200,分布式事务正常提交。订单表、库存表和用户表进行相应的修改。
  • purchaseRollback:事务异常回滚方法,购买数量9999大于库存数量200,service中抛出运行期异常,事务回滚。执行后可以看到,库存=200,用户余额=10000,数据已回滚。
    BussinessController控制器代码如下。
@RestController
public class BusinessController {

    @Autowired
    private BusinessService businessService;

    /**
     * 购买下单,模拟全局事务提交
     *
     * @return
     */
    @RequestMapping(value = "/purchase/commit", produces = "application/json")
    public String purchaseCommit() {
        try {
            businessService.purchase("U100000", "C100000", 30);
        } catch (Exception exx) {
            return exx.getMessage();
        }
        return "全局事务提交";
    }

    /**
     * 购买下单,模拟全局事务回滚
     * 账户或库存不足
     *
     * @return
     */
    @RequestMapping("/purchase/rollback")
    public String purchaseRollback() {
        try {
            businessService.purchase("U100000", "C100000", 99999);
        } catch (Exception exx) {
            return exx.getMessage();
        }
        return "全局事务提交";
    }
}

11.4.10 启动并测试


浏览器访问分布式事务回滚方法“/purchase/ commit”因为此方法购买商品数量30,小于库存数200,所以分布式事务正常执行提交。

重启项目,数据初始化后,浏览器访问分布式事务回滚方法“/purchase/rollback”因为此方法,购买商品数量99999,大于库存数。所以分布式事务回滚,执行后查看数据,已回滚。
重启项目,数据初始化后,浏览器访问分布式事务回滚方法“/purchase/rollback”,在bussinessService类43行处,设置断点如图5-20所示。

图11-20 businessSerivce断点调试
进入断点后,观察数据库中数据变化发现,余额已经扣减,如图11-21所示。

图11-21:库存余额扣减
同时undo_log中存在Seata执行过程中的相应日志数据,如图11-22所示。

图11-22 Seata执行产生数据
继续执行,因为抛出运行期异常,分布式事务回滚, 观察到订单表,用户表,库存表,根据undo_log中的日志,回滚到初始状态。