什么叫事务

1)本地事务

在一般小的项目中,都是单个服务操作单个数据库的情况下,为了保证事务的一致性,要嘛全部都成功要嘛全部都失败,就会用到 本地事务。

1.1 事务的四个特性

  • 原子性
    • 在整个事务中的所有操作,要嘛都成功,要嘛都失败,当在事务中产生错误时就会进行回滚(Rollback),回滚到事务开始之前
  • 一致性
    • 事务开始前和结束后的结果应当和客观的数据保持一致,如 A转B 100元,A需要扣除100元,而B需要增加100元。如果在转账的过程中A出错了,A就不可以扣除100元,如果B也不能增加100元
  • 隔离性
    • 并发执行的多个事务之间的操作互不影响
  • 持久性
    • 在事务完成后,该事务对数据库的所有操作都会被持久化

1.2 常见的本地事务案例

这里我们举个转账例子来理解什么是本地事务:

A有50元,B有50元,此时A给B转账50元,开启事务后,那么A应该 - 50,B + 50,但是如果转账的过程中出错了,事务进行回滚,事务结束后 A和B的余额回到事务开始之前。

本地事务图例

1.3 JDBC中如何实现本地事务

在JDBC编程中,我们通过java.sql.Connection对象来开启、关闭或者提交事务。代码如下所示:

1
2
3
4
5
6
7
8
9
10
Connection conn = ... //获取数据库连接
conn.setAutoCommit(false); //开启事务
try{
   //...执行增删改查sql
   conn.commit(); //提交事务
}catch (Exception e) {
  conn.rollback();//事务回滚
}finally{
   conn.close();//关闭链接
}

2)分布式事务

在微服务架构中要完成一个业务的功能往往会跨多个服务和数据库进行操作,这样就会产生分布式事务。

分布式事务是为了保证数据一致性,当应用需要对多个服务、数据库进行操作时,确保 要嘛全部都成功,要嘛全部都失败。

而分布式事务为了确保ACID特性,就必然会带来性能问题,这个需要根据实际的场景和架构决定是否使用分布式事务。

在生产环境中,能避免跨库产生的分布式事务问题,尽量避免分布式事务,实在避免不了在根据实际的业务去选择合适的中间件和协议。

2.1 分布式事务常见场景

跨数据库事务

跨库事务

跨数据库事务指的是:一个应用的业务逻辑需要同时操作多个数据库。

如果我们需要实现分布式事务,可以通过三方中间件,也可以自己想办法 创建2个连接(方法),判断2个连接的执行结果,如果有一个连接执行失败了,则进行手动回滚,如果都执行成功,就放行。

分库分表

分库分表

当一个数据库的数据量过于庞大时,我们会对其进行分库分表,但是分库分表后随之而来的就是分布式事务。

如:当我们插入一条SQL,分库分表中间件 肯定会对SQL的字段进行分片后分配到不同的数据库存储,这就无法必然分布式事务。

微服务架构

微服务架构

SA需要完成某个业务逻辑,如 下单后需要去 执行库存和订单的逻辑,需要调用其他微服务去操作对应的数据库,这就会出现 跨服务跨数据库的情况,那就必须保证 要嘛都成功,要嘛都失败,才能确保数据一致性。

3)两阶段提交协议(2PC)

两阶段提交 2PC

3.1 名词介绍

  • RM(Resource Manage):资源管理器或称 事务参与者
  • TM(Transaction Manager):事务管理器
  • 全局事务:一个全局事务包含了多个事务分支,多个事务分支组成一个 大的全局事务
  • 事务分支:每个RM都会有一个本地事务分支
  • 全局事务唯一标识 XID:全局事务一旦开启后,会获得一个唯一标识 XID,用于标识 全局事务
  • 分支事务唯一标识 BID:用于标识每个RM的分支事务,XID与BID会进行一对多的关联,就可以确定全局事务下有哪些 分支事务

3.2 两个阶段

两阶段顾名思义,就是整个分布式事务会分为2个阶段来执行:

第一阶段 预提交

TM会通知各个RM(事务参与者)准备提交事务分支,如果RM判断当前的分支可以提交,则会对事务的内容进行持久化并提交到TM。

第二阶段 提交或回滚

TM根据 第一阶段收到的RM prepare(准备)结果,决定是否提交事务还是回滚事务。

如果所有的RM都prepare成功,那么TM会通知所有RM 都提交,如果有任何一个RM的prepare失败,那么TM会通知所有RM都回滚自己的本地事务分支。

如果在第一阶段的时候操作数据库时就发生了问题,或者TM收不到某个数据库的正确响应,也会判定为事务失败,然后通知所有参与事务的RM会进行本地事务回滚。

关于全局事务和事务分支

image-20230502202805812

每个事务分支都有自己的一个本地事务,具备ACID特性,而多个事务分支组成一个 大的全局事务,全局事务也具备ACID特性。

一个全局事务包含了多个事务分支,全局事务里面的所有事务分支要嘛全部都成功,要嘛全部都失败。

3.3 两阶段的缺点

  • 同步阻塞问题
    • 2PC的参与者第一阶段时会获取数据库的连接,只有等到第二阶段提交或回滚后才会释放连接。这就会导致 连接被占用的性能问题,如果连接池的连接已经溢出了,就会出现阻塞的情况。
  • TM单点故障

    • TM起到了一个事务协调的作用,如果TM突然发生了故障,那么所有的参与者RM都会陷入事务锁定,无法继续完成事务操作。
  • 数据不一致

    • 如果RM在第二阶段提交commit时请求崩溃了,那么该RM会回滚本地事务。但全局事务中其他的RM已经commit成功了,就会出现数据不一致的情况。

      注意:TM协调全部回滚的情况只会建立在 第一阶段提交prepare后,TM判断不满足条件才会全部回滚,RM在第二阶段commit时出现了BUG,其他的RM并不会回滚。

Seata 分布式事务框架

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。AT模式是阿里首推的模式,阿里云上有商用版本的GTS(Global Transaction Service 全局事务服务)

官网:https://seata.io/zh-cn/index.html

源码: https://github.com/seata/seata

1)Seata的三大角色

在 Seata 的架构中,一共有三个角色:

  • TC (Transaction Coordinator) - 事务协调者

维护全局和分支事务的状态,驱动全局事务提交或回滚。谁作为事务发起者,他就是TM

  • TM (Transaction Manager) - 事务管理器

定义全局事务的范围:开始全局事务、提交或回滚全局事务。

  • RM (Resource Manager) - 资源管理器/事务参与者

管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

其中,TC 为单独部署的 Server 服务端,TM 和 RM 为嵌入到应用中的 Client 客户端。

2)Seata AT模式流程

AT模式

名词介绍

前置镜像:执行业务SQL之前的数据

后置镜像:执行业务SQL之后的数据

undoLog日志:通过XID和BID 记录前置镜像和后置镜像等数据,而后期回滚时可以通过XID和BID定位到相应的回滚日志

  1. TM通过RPC请求TC开启一个全局事务,TC会生成一个XID(全局事务唯一标识)返回给TM,并随着请求把XID传递给下游
  2. RM会请求TC 将本地事务注册为全局分支事务,并将XID与RM的BID进行关联
  3. RM如果业务逻辑处理没问题,会直接对本地事务commit 落盘,并且释放本地锁和连接资源(第一阶段就直接commit了)
    1. RM在提交之前会把业务数据、前置镜像、后置镜像存储到undolog表中,便于后期回滚数据
    2. 如果RM处理的时候出了异常,会把异常一层一层往上抛,最终抛到了顶部的TM服务上,TM会捕获异常后通知TC全局回滚
    3. 注意:如果微服务把异常给拦截了,如sentinel的降级策略,就会导致异常无法被TM捕获,TM无法知道是否出现了问题,就无法通知TC进行全局回滚
  4. TM请求TC,告知XID对应的全局事务是需要 commit还是rollback
  5. TC告知XID关联的所有RM 对本地事务进行commit或rollback
  6. RM收到TC的确认后,如果是commit后会对本地事务进程commit,如果是rollback就按照之前预先准备的回滚SQL 进行回滚
    • 回滚的时候,会拿 后置镜像和当前数据库的数据进行对比是否一致
      • 如果一致,就进行回滚,回滚成功后会删除 undoLog日志
      • 如果不一致就会出现 回滚失败的情况,整个事务都会挂住,我们可以通过实现seata的接口进行预警,进行人工干预

注意细节

1、如果RM出现了异常或库存扣减失败抛出异常了,该异常会层层往上抛,切记不可捕获该异常,如sentinel 会捕获异常进行降级处理,否则会导致TM无法捕获该异常,自然无法告知TC进行回滚

2、RM会在第一阶段就进行commit并且释放锁和连接资源,在提交之前会把业务数据和业务回滚SQL存储到undolog表中,如果后面出现了异常,就会按照业务回滚SQL进行回滚

2.2 电商例子

image-20230503172030350

我们以电商场景举例,A客户在电商系统上下了一个订单,订单模块会进行一些列操作,随后通过open feign调用支付模块以及库存模块进行相关操作。

事务流程

  1. 订单模块作为全局事务发起者,集成了RM和TM,TM告知TC 发起全局事务,TC返回XID
    • 订单模块通过XID进行关联后,进行对应的业务操作,commit之前把业务数据和前置镜像和后置镜像存储到undolog表中
    • commit 成功后释放本地锁和数据库连接,并告知TM commit成功,通过open feign调用下一个模块
  2. 订单模块和上面步骤一致,通过XID进行关联后,执行对应的业务操作后进行commit,commit成功的话就释放锁和连接并告知TM
  3. 库存模块执行发现异常后,就会把异常层层往上抛,最终抛到了TM 被TM捕获,TM告知TC全局事务结果
  4. TC收到结果后 告知全局事务下的所有RM进行本地事务回滚
  5. RM收到本地事务回滚的通知后,会根据之前commit时候提前准备好的反向回滚SQL进行本地事务回滚
    • 回滚的时候,会拿 后置镜像和当前数据库的数据进行对比是否一致
      • 如果一致,就进行回滚,回滚成功后会删除 undoLog日志
      • 如果不一致就会出现 回滚失败的情况,整个事务都会挂住,我们可以通过实现seata的接口进行预警,进行人工干预

3)Seata AT设计思路

Seata AT不会对代码有侵入,是一种改进后的两阶段提交方式,不同于原本的 2PC。

  • 一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源

  • 二阶段:

    • 异步删除undoLog日志
    • 回滚通过一阶段的回滚日志进行反向补偿

3.1 一阶段

一阶段

RM在一阶段的时候会做很多事情,流程如下:

  1. 解析SQL
  2. 查询前置镜像
    • 为了确保在二阶段进行回滚的时候,可以对比后置镜像是否和回滚前当前数据库的数据是否一致,需要保存前置镜像和后置镜像
    • 前置镜像:执行SQL之前的数据,如 执行SQL之前数据为100,修改之后为200
    • 后置镜像:执行SQL之后的数据
  3. 插入业务SQL
  4. 查询后置镜像
  5. 插入undoLog日志
    • 为了确保在二阶段完成回滚,就必须插入一个 undoLog日志,来记录 执行的业务SQL和前置镜像和后置镜像数据
  6. 本地事务commit提交
  7. 通知TC事务分支状态
  8. 释放本地锁和数据库连接资源

前置镜像和后置镜像的细节

image-20230517153227216

值得注意的是,由于AT在一阶段 执行业务SQL之前和之后都会查询并创建 前置镜像和后置镜像,并且查询的时候用到了for update。

我们在SQL后面加上 for update的话,在查询的过程中就会造成锁表的情况,至于是表锁还是行锁,取决于你的SQL有没有走索引,如果走的索引就是行锁,没走索引就是表锁。

如:select * from test for update

3.1 二阶段

分布式事务成功分布式事务成功

TM判定分布式事务成功后,TC会告知所有的RM 删除本地的undoLog日志。

RM收到通知后,会根据XID和BID去查询对应的undoLog日志并删除。

分布式事务失败

分布式事务失败

TM判定分布式事务失败后,TM向TC发送全局回滚请求,TC会告知所有RM通过XID和Branch ID找到相应的回滚日志记录,通过日志反向生成的回滚SQL进行本地事务回滚。

RM回滚本地事务流程

  1. RM通过XID和Branch ID找到相应的回滚日志记录
  2. 查询记录的后置镜像是否和当前数据库的值一致
    1. 如果一致的话,就进行回滚,回滚后删除 undoLog日志记录,然后commit提交到本地事务并告知TC回滚成功
    2. 如果不一致,就无法回滚,因为回滚了也会出现脏数据的情况,这时候就需要人工介入,该事务就会锁死

Seata AT模式实战

Seata分TC、TM和RM三个角色,TC(Server端)为单独服务端部署,TM和RM(Client端)由业务系统集成。

1)Seata Server(TC)环境搭建

Server端存储模式(store.mode)支持三种:

  • file:单机模式,全局事务会话信息内存中读写并持久化本地文件root.data,性能较高
  • db:高可用模式,全局事务会话信息通过db共享,相应性能差些
  • redis:1.3及以上版本支持,性能较高,存在事务信息丢失风险,请提前配置适合当前场景的redis持久化配置

如果要使用Seata Server集群的话,就必须采用db或redis的形式进行持久化存储,应当根据场景使用对应的持久化形式。

本次实战,将以DB和Nacos的形式进行部署,seata server保存在DB,而配置和注册则放在Nacos中。

1.1 下载安装包

官网github:https://github.com/seata/seata/releases

本次实战安装包:https://files.javaxing.com/seata/seata-server-1.5.1.tar.gz

1
2
3
4
5
// 下载安装包
[root@S1 ~]# wget https://files.javaxing.com/seata/seata-server-1.5.1.tar.gz

// 解压seata
[root@S1 ~]# tar -xvf seata-server-1.5.1.tar.gz

解压后目录

image-20230516150318390

1.2 创建seata server 数据库表

我们需要创建一个seata server数据库来存储BID和XID全局事务,本次实战用的是mysql,如果是其他的数据库的话,可以访问下面的github 地址 https://github.com/seata/seata/tree/v1.5.1/script/server/db 获取。

mysql sql

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
-- -------------------------------- The script used when storeMode is 'db' --------------------------------
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_status_gmt_modified` (`status` , `gmt_modified`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`status` TINYINT NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_status` (`status`),
KEY `idx_branch_id` (`branch_id`),
KEY `idx_xid_and_branch_id` (`xid` , `branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

CREATE TABLE IF NOT EXISTS `distributed_lock`
(
`lock_key` CHAR(20) NOT NULL,
`lock_value` VARCHAR(20) NOT NULL,
`expire` BIGINT,
primary key (`lock_key`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);

创建后

image-20230516144700091

1.3 配置Seata Nacos注册中心&Nacos配置中心

参考文章:《Nacos配置中心使用之Nacos 微服务注册中心搭建》

https://www.javaxing.com/2023/04/01/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84%E9%80%89%E5%9E%8B%E5%92%8CNacos%E9%9B%86%E7%BE%A4%E6%90%AD%E5%BB%BA/

Nacos和seata逻辑图

Nacos注册中心记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到注册中心,当服务需要调用其它服务时,就到注册中心找到服务的地址,进行调用。

如:我们将TC和TM分别注册到Nacos注册中心后,TC和TM之间就可以通过Nacos发现彼此并进行通信。

创建Nacos命名空间

image-20230516154308807

修改seata安装目录中的 conf/application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: 127.0.0.1:8848
namespace: e56de5d4-a37e-49c0-b613-f85a7f67cb36
group: SEATA_GROUP
data-id: seataServer.properties
username:
password:
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
group: SEATA_GROUP
namespace:
cluster: default
username:
password:

image-20230516152012129

注意:请确保client(微服务)与seata server的注册处于同一个namespace和group,不然会找不到服务。

image-20230516152847814

1.4 新增Nacos配置

image-20230516154359364

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
transport.type=TCP
transport.server=NIO
transport.heartbeat=true
transport.enableTmClientBatchSendRequest=false
transport.enableRmClientBatchSendRequest=true
transport.enableTcServerBatchSendResponse=false
transport.rpcRmRequestTimeout=30000
transport.rpcTmRequestTimeout=30000
transport.rpcTcRequestTimeout=30000
transport.threadFactory.bossThreadPrefix=NettyBoss
transport.threadFactory.workerThreadPrefix=NettyServerNIOWorker
transport.threadFactory.serverExecutorThreadPrefix=NettyServerBizHandler
transport.threadFactory.shareBossWorker=false
transport.threadFactory.clientSelectorThreadPrefix=NettyClientSelector
transport.threadFactory.clientSelectorThreadSize=1
transport.threadFactory.clientWorkerThreadPrefix=NettyClientWorkerThread
transport.threadFactory.bossThreadSize=1
transport.threadFactory.workerThreadSize=default
transport.shutdown.wait=3
transport.serialization=seata
transport.compressor=none

#Transaction routing rules configuration, only for the client
service.vgroupMapping.default_tx_group=default
#If you use a registry, you can ignore it
service.default.grouplist=127.0.0.1:8091
service.enableDegrade=false
service.disableGlobalTransaction=false

#Transaction rule configuration, only for the client
client.rm.asyncCommitBufferLimit=10000
client.rm.lock.retryInterval=10
client.rm.lock.retryTimes=30
client.rm.lock.retryPolicyBranchRollbackOnConflict=true
client.rm.reportRetryCount=5
client.rm.tableMetaCheckEnable=true
client.rm.tableMetaCheckerInterval=60000
client.rm.sqlParserType=druid
client.rm.reportSuccessEnable=false
client.rm.sagaBranchRegisterEnable=false
client.rm.sagaJsonParser=fastjson
client.rm.tccActionInterceptorOrder=-2147482648
client.tm.commitRetryCount=5
client.tm.rollbackRetryCount=5
client.tm.defaultGlobalTransactionTimeout=60000
client.tm.degradeCheck=false
client.tm.degradeCheckAllowTimes=10
client.tm.degradeCheckPeriod=2000
client.tm.interceptorOrder=-2147482648
client.undo.dataValidation=true
client.undo.logSerialization=jackson
client.undo.onlyCareUpdateColumns=true
server.undo.logSaveDays=7
server.undo.logDeletePeriod=86400000
client.undo.logTable=undo_log
client.undo.compress.enable=true
client.undo.compress.type=zip
client.undo.compress.threshold=64k
#For TCC transaction mode
tcc.fence.logTableName=tcc_fence_log
tcc.fence.cleanPeriod=1h

#Log rule configuration, for client and server
log.exceptionRate=100

# 存储模式为db
store.mode=db
store.lock.mode=db
store.session.mode=db
#Used for password encryption
store.publicKey=

#If `store.mode,store.lock.mode,store.session.mode` are not equal to `file`, you can remove the configuration block.
store.file.dir=file_store/data
store.file.maxBranchSessionSize=16384
store.file.maxGlobalSessionSize=512
store.file.fileWriteBufferCacheSize=16384
store.file.flushDiskMode=async
store.file.sessionReloadReadSize=100

# 设置MySQL数据库源信息
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.jdbc.Driver
store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=123123
store.db.minConn=5
store.db.maxConn=30
store.db.globalTable=global_table
store.db.branchTable=branch_table
store.db.distributedLockTable=distributed_lock
store.db.queryLimit=100
store.db.lockTable=lock_table
store.db.maxWait=5000

#These configurations are required if the `store mode` is `redis`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `redis`, you can remove the configuration block.
store.redis.mode=single
store.redis.single.host=127.0.0.1
store.redis.single.port=6379
store.redis.sentinel.masterName=
store.redis.sentinel.sentinelHosts=
store.redis.maxConn=10
store.redis.minConn=1
store.redis.maxTotal=100
store.redis.database=0
store.redis.password=
store.redis.queryLimit=100

#Transaction rule configuration, only for the server
server.recovery.committingRetryPeriod=1000
server.recovery.asynCommittingRetryPeriod=1000
server.recovery.rollbackingRetryPeriod=1000
server.recovery.timeoutRetryPeriod=1000
server.maxCommitRetryTimeout=-1
server.maxRollbackRetryTimeout=-1
server.rollbackRetryTimeoutUnlockEnable=false
server.distributedLockExpireTime=10000
server.xaerNotaRetryTimeout=60000
server.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false

#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898

以上配置,只需要修改store.mode=db并且配置相应的数据源就可以了。

store.mode=db

store.lock.mode=db

store.session.mode=db

数据源:

store.db.datasource=druid

store.db.dbType=mysql

store.db.driverClassName=com.mysql.jdbc.Driver

store.db.url=jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&rewriteBatchedStatements=true

store.db.user=root

store.db.password=123123

配置注意事项

1、关于rewriteBatchedStatements参数

由于seata是通过jdbc的executeBatch来批量插入全局锁的,rewriteBatchedStatements为true时,插入数据的性能是原来的10倍多,因此在数据源为MySQL时,建议把该参数设置为true。

2、配置中的事务分组必须 与client 配置的事务分组保持一致

image-20230516160113841

我们可以通过分组,来逻辑划分 seata server和seata client,不同的分组之间是无法进行分布式事务的。

此时我们的分组名称叫default_tx_group,那么对应的client (微服务端)的配置中,分组也要叫default_tx_group ,具体下面会描述。

1.5 启动seata server

1
[root@S1 bin]# ./seata-server.sh

启动成功,查看控制台,账号密码都是seata(在conf/application.yaml中可以配置)。http://localhost:7091/#/login

image-20230516163818167

这个seata控制台可以实时看到全局事务的处理情况,起到一个监控全局事务的作用,通过这里也可以看到一些脏数据导致回滚失败 不停回滚的事务,但是一般我们遇到这种情况,都是修改seata源码,让他直接发一个钉钉通知等。

Nacos注册中心也可以看到seata注册成功

image-20230516163900285

支持的启动参数

参数 全写 作用 备注
-h –host 指定在注册中心注册的 IP 不指定时获取当前的 IP,外部访问部署在云环境和容器中的 server 建议指定
-p –port 指定 server 启动的端口 默认为 8091
-m –storeMode 事务日志存储方式 支持file,db,redis,默认为 file 注:redis需seata-server 1.3版本及以上
-n –serverNode 用于指定seata-server节点ID 如 1,2,3…, 默认为 1
-e –seataEnv 指定 seata-server 运行环境 如 dev, test 等, 服务启动时会使用 registry-dev.conf 这样的配置

比如:

1
[root@S1 bin]# ./seata-server.sh -p 8091 -h 127.0.0.1 -m db

2)Spring Cloud Alibaba整合Seata AT模式

电商分布式逻辑

我们通过一个电商的场景来整合Seata分布式事务,该场景由 三个微服务完成,用户下单后,订单服务通过OpenFeign调用库存服务扣减库存后,调用用户服务扣减余额。

  • 订单服务:根据用户需求进行下单
  • 库存服务:根据下单的商品减少库存
  • 账户服务:根据用户ID扣除余额

我们要模拟3种情况:

  1. 订单服务成功扣除库存和余额,分布式事务完成
  2. 订单服务修改用户余额时,发现余额不足无法扣除,抛出异常 进行全局事务回滚
  3. 订单服务修改用户余额时,发现余额不足无法扣除,抛出异常 进行全局事务回滚,但是此时我们模拟脏数据,故意在回滚之前修改了数据,让他 后置数据和当前的数据 对应不上,然后就报错。

1.1 工程介绍

image-20230516183043144

项目源码地址:https://files.javaxing.com/seata/SpringCloudAlibabaSeata.zip

1.2 环境依赖

  • 父pom指定微服务版本
Spring Cloud Alibaba Version Spring Cloud Version Spring Boot Version Seata Version
2.2.8.RELEASE Spring Cloud Hoxton.SR12 2.3.12.RELEASE 1.5.1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<?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>

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

<groupId>com.javaxing</groupId>
<artifactId>SpringCloudAlibabaSeata</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>mall-order</module>
<module>mall-user</module>
<module>mall-stock</module>
</modules>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
<spring-cloud-alibaba.version>2.2.8.RELEASE</spring-cloud-alibaba.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1</version>
</dependency>
<!--mysql的依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
<!--springcloudAlibaba-->
<!-- 引入nacos依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- druid 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.8</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring-cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

1.3 启动seata server和nacos

seata

1
[root@S1 bin]# ./seata-server.sh

nacos

nacos怎么安装,这里可以看之前的文章《Nacos配置中心使用之Spring Cloud集成Nacos配置中心》

地址: https://www.javaxing.com/2023/04/01/%E5%BE%AE%E6%9C%8D%E5%8A%A1%E6%9E%B6%E6%9E%84%E9%80%89%E5%9E%8B%E5%92%8CNacos%E9%9B%86%E7%BE%A4%E6%90%AD%E5%BB%BA/

1
[root@S1 bin]# sh startup.sh -m standalone

1.4 构建订单、库存、用户微服务

这里需要构建三个微服务,并且分别创建对应的数据库和service、mapper、controller

订单数据库

image-20230516195917761

库存数据库

image-20230516195941843

用户数据库

image-20230516195948034

1.4.1 订单微服务

父类pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?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>
<parent>
<groupId>com.javaxing</groupId>
<artifactId>SpringCloudAlibabaSeata</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>mall-order</artifactId>
<packaging>pom</packaging>
<modules>
<module>mall-order-api</module>
<module>mall-order-biz</module>
</modules>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>



</project>

api pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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>
<parent>
<groupId>com.javaxing</groupId>
<artifactId>mall-order</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>mall-order-api</artifactId>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

</project>

biz pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.javaxing</groupId>
<artifactId>mall-order</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.javaxing</groupId>
<artifactId>mall-order-biz</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mall-order-biz</name>
<description>mall-order-biz</description>
<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<!-- openfeign 远程调用 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>


</dependencies>


<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>

</project>

bootstrap.yml

注意:不可用application.yaml,必须用bootstrap.yml,否则无法使用nacos的配置文件,因为bootstrap.yml优先级更高,可以在启动时候更早的读取配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 8082
spring:
application:
name: mall-order
cloud:
nacos:
discovery:
server-addr: 10.211.55.12:8848
config:
server-addr: 10.211.55.12:8848
file-extension: yaml


nacos配置文件

这里我们把数据源的配置放到配置中心,当然你怕麻烦,也可以和上面的bootstrap.yml放在一起

1
2
3
4
5
6
7
8
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123123
url: jdbc:mysql://10.211.55.12:3306/order?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=America/Los_Angeles&allowMultiQueries=true&allowPublicKeyRetrieval=true

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.javaxing.mallorder.controller;

import com.javaxing.mallorder.service.SysOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@RestController
@RequestMapping("/order")
public class OrderController {

@Autowired
private SysOrderService sysOrderService;

@GetMapping("/createOrder")
public void createOrder(Integer shopId,Integer count, BigDecimal price, String title, Integer userId){
sysOrderService.createOrder(shopId,count,price,title,userId);
}
}

service

1
void createOrder(Integer shopId,Integer count, BigDecimal price, String title, Integer userId);

serviceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

@Service
public class SysOrderServiceImpl implements SysOrderService{

@Resource
private SysOrderMapper sysOrderMapper;
@Resource
private StockFeignService stockFeignService;

@Resource
private UserFeignService userFeignService;

@Override
// 开启分布式全局事务,名称为 createOrder
@GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
public void createOrder(Integer shopId,Integer count, BigDecimal price, String title, Integer userId) {
// 创建订单
SysOrder order = new SysOrder();
order.setShopId(shopId);
order.setPrice(price);
order.setStatus(1);
order.setTitle(title);
order.setUserId(userId);
sysOrderMapper.insertSelective(order);

// 扣减库存
stockFeignService.reduceStock(shopId,count);

// 扣除余额
userFeignService.reduceBalance(userId,price);
}
}

mapper

1
2
3
4
@Mapper
public interface SysOrderMapper {

}

StockFeignService OpenFeign接口

1
2
3
4
5
6
7
8
9
10
11
12
package com.javaxing.mallorder.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

// value指的是 对方的服务名称,path 是对应RequestMapping的路径
@FeignClient(value = "mall-stock",path = "/stock")
public interface StockFeignService {
@GetMapping("/reduce")
public String reduceStock(@RequestParam("shopId") Integer shopId, @RequestParam("count") Integer count);
}

UserFeignService OpenFeign接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.javaxing.mallorder.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.math.BigDecimal;

// value指的是 对方的服务名称,path 是对应RequestMapping的路径
@FeignClient(value = "mall-user",path = "/user")
public interface UserFeignService {
@GetMapping("/reduceBalance")
public void reduceBalance (@RequestParam("userId") Integer userId,@RequestParam("balance") BigDecimal balance);
}

1.4.2 库存微服务

父类pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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>
<parent>
<groupId>com.javaxing</groupId>
<artifactId>SpringCloudAlibabaSeata</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>mall-stock</artifactId>
<packaging>pom</packaging>
<modules>
<module>mall-stock-api</module>
<module>mall-stock-biz</module>
</modules>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

</project>

api pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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>
<parent>
<groupId>com.javaxing</groupId>
<artifactId>mall-stock</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>mall-stock-api</artifactId>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

</project>

biz pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.javaxing</groupId>
<artifactId>mall-stock</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.javaxing</groupId>
<artifactId>mall-stock-biz</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mall-stock-biz</name>
<description>mall-stock-biz</description>
<properties>
<java.version>1.8</java.version>
</properties>


<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>

</project>

bootstrap.yml

注意:不可用application.yaml,必须用bootstrap.yml,否则无法使用nacos的配置文件,因为bootstrap.yml优先级更高,可以在启动时候更早的读取配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8081
spring:
application:
name: mall-stock
cloud:
nacos:
discovery:
server-addr: 10.211.55.12:8848
config:
server-addr: 10.211.55.12:8848
file-extension: yaml

nacos配置文件

这里我们把数据源的配置放到配置中心,当然你怕麻烦,也可以和上面的bootstrap.yml放在一起

1
2
3
4
5
6
7
8
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123123
url: jdbc:mysql://10.211.55.12:3306/stock?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=America/Los_Angeles&allowMultiQueries=true&allowPublicKeyRetrieval=true

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.javaxing.mallstockbiz.controller;

import com.javaxing.mallstockbiz.service.SysShopStockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/stock")
public class StockController {

@Autowired
private SysShopStockService sysShopStockService;

@GetMapping("/reduce")
public void reduceStock(@RequestParam("shopId") Integer shopId,@RequestParam("count") Integer count){
// 根据订单号减少库存
sysShopStockService.reduceStock(shopId,count);
}
}

service

1
2
3
4
5
6
7
8
package com.javaxing.mallstockbiz.service;

import com.javaxing.mallstockbiz.entity.SysShopStock;
public interface SysShopStockService{

void reduceStock(Integer shopId, Integer count);
}

serviceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.javaxing.mallstockbiz.service.impl;
import java.util.List;

import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import com.javaxing.mallstockbiz.mapper.SysShopStockMapper;
import com.javaxing.mallstockbiz.entity.SysShopStock;
import com.javaxing.mallstockbiz.service.SysShopStockService;
import org.springframework.transaction.annotation.Transactional;

@Service
public class SysShopStockServiceImpl implements SysShopStockService{
@Override
@Transactional
public void reduceStock(Integer shopId, Integer count) {
SysShopStock shopStock = sysShopStockMapper.findFirstByShopId(shopId);
if(shopStock == null){
throw new RuntimeException("商品不存在");
}
if(shopStock.getStock() < count){
// 库存不够,抛出异常
throw new RuntimeException("库存不足");
}

// 扣除库存
int i = sysShopStockMapper.reduceStock(shopId,count);
if(i==0){
// 更新失败,抛出异常
throw new RuntimeException("库存不足");
}
}

@Override
public SysShopStock findFirstByShopId(Integer shopId){
return sysShopStockMapper.findFirstByShopId(shopId);
}
}

mapper

1
2
3
4
5
6
7
8
9
10
package com.javaxing.mallstockbiz.mapper;

import com.javaxing.mallstockbiz.entity.SysShopStock;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface SysShopStockMapper {
int reduceStock(@Param("shopId") Integer shopId, @Param("count") Integer count);
}

mapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.javaxing.mallstockbiz.mapper.SysShopStockMapper">
<resultMap id="BaseResultMap" type="com.javaxing.mallstockbiz.entity.SysShopStock">
<!--@mbg.generated-->
<!--@Table sys_shop_stock-->
<id column="id" jdbcType="INTEGER" property="id" />
<result column="shop_id" jdbcType="INTEGER" property="shopId" />
<result column="stock" jdbcType="INTEGER" property="stock" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
id, shop_id, stock
</sql>
<update id="reduceStock">
update sys_shop_stock
set stock = stock - #{count,jdbcType=INTEGER}
where shop_id = #{shopId,jdbcType=INTEGER}
</update>

<!--creation by Eason on 2023-05-15-->
<select id="findFirstByShopId" resultMap="BaseResultMap">
select
<include refid="Base_Column_List"/>
from sys_shop_stock
where shop_id=#{shopId,jdbcType=INTEGER} limit 1
</select>
</mapper>

1.4.3 用户微服务

父类pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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>
<parent>
<groupId>com.javaxing</groupId>
<artifactId>SpringCloudAlibabaSeata</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>mall-user</artifactId>
<packaging>pom</packaging>
<modules>
<module>mall-user-api</module>
<module>mall-user-biz</module>
</modules>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

</project>

api pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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>
<parent>
<groupId>com.javaxing</groupId>
<artifactId>mall-user</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>mall-user-api</artifactId>

<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

</project>

biz pom依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.javaxing</groupId>
<artifactId>mall-user</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.javaxing</groupId>
<artifactId>mall-user-biz</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>mall-user-biz</name>
<description>mall-user-biz</description>
<properties>
<java.version>1.8</java.version>
</properties>


<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</pluginRepository>
</pluginRepositories>

</project>

bootstrap.yml

注意:不可用application.yaml,必须用bootstrap.yml,否则无法使用nacos的配置文件,因为bootstrap.yml优先级更高,可以在启动时候更早的读取配置文件。

1
2
3
4
5
6
7
8
9
10
11
12
server:
port: 8080
spring:
application:
name: mall-user
cloud:
nacos:
discovery:
server-addr: 10.211.55.12:8848
config:
server-addr: 10.211.55.12:8848
file-extension: yaml

nacos配置文件

这里我们把数据源的配置放到配置中心,当然你怕麻烦,也可以和上面的bootstrap.yml放在一起

1
2
3
4
5
6
7
8
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123123
url: jdbc:mysql://10.211.55.12:3306/user?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=America/Los_Angeles&allowMultiQueries=true&allowPublicKeyRetrieval=true

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.javaxing.malluserbiz.controller;

import com.javaxing.malluserbiz.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.math.BigDecimal;

@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private SysUserService sysUserService;

@GetMapping("/reduceBalance")
public void reduceBalance (@RequestParam("userId") Integer userId,@RequestParam("balance") BigDecimal balance){
sysUserService.reduceBalance(userId,balance);
}
}

service

1
2
3
4
5
6
7
package com.javaxing.malluserbiz.service;
import com.javaxing.malluserbiz.entity.SysUser;
import java.math.BigDecimal;

public interface SysUserService{
void reduceBalance(Integer userId, BigDecimal balance);
}

serviceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.javaxing.malluserbiz.service.impl;

import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import com.javaxing.malluserbiz.entity.SysUser;
import com.javaxing.malluserbiz.mapper.SysUserMapper;
import com.javaxing.malluserbiz.service.SysUserService;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Service
public class SysUserServiceImpl implements SysUserService{
@Override
@Transactional
public void reduceBalance(Integer userId, BigDecimal balance) {
// 检查用户余额是否足够
SysUser user = sysUserMapper.selectByPrimaryKey(userId);
if(user ==null){
throw new RuntimeException("用户不存在");
}
if(balance.compareTo(user.getBalance()) == 1){
throw new RuntimeException("余额不足");
}

// 扣除余额
int i = sysUserMapper.reduceBalance(user.getUserId(), balance);
if(i==0){
throw new RuntimeException("扣除失败");
}
}
}

mapper

1
2
3
4
5
6
7
8
9
10
11
12
package com.javaxing.malluserbiz.mapper;

import com.javaxing.malluserbiz.entity.SysUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.math.BigDecimal;

@Mapper
public interface SysUserMapper {
int reduceBalance(@Param("userId") Integer userId, @Param("balance") BigDecimal balance);
}

mapper.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.javaxing.malluserbiz.mapper.SysUserMapper">
<resultMap id="BaseResultMap" type="com.javaxing.malluserbiz.entity.SysUser">
<!--@mbg.generated-->
<!--@Table sys_user-->
<id column="user_id" jdbcType="INTEGER" property="userId" />
<result column="balance" jdbcType="DECIMAL" property="balance" />
</resultMap>
<sql id="Base_Column_List">
<!--@mbg.generated-->
user_id, balance
</sql>

<update id="reduceBalance">
update sys_user
set balance = balance - #{balance,jdbcType=DECIMAL}
where user_id = #{userId,jdbcType=INTEGER}
</update>
</mapper>

1.5 导入seata依赖

在父类项目导入seata依赖,这样3个服务都有了seata的依赖

1
2
3
4
5
<!-- seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

1.6 插入undo_log表

在3个微服务的数据库中添加对应的undo_log表。

1
2
3
4
5
6
7
8
9
10
11
12
13
CREATE TABLE IF NOT EXISTS `undo_log`
(
`branch_id` BIGINT NOT NULL COMMENT 'branch transaction id',
`xid` VARCHAR(128) NOT NULL COMMENT 'global transaction id',
`context` VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` LONGBLOB NOT NULL COMMENT 'rollback info',
`log_status` INT(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` DATETIME(6) NOT NULL COMMENT 'create datetime',
`log_modified` DATETIME(6) NOT NULL COMMENT 'modify datetime',
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';

image-20230516195129112

1.7 微服务bootstrap.yml添加seata配置

image-20230516195529207image-20230516195623441

注意:请确保client与server的注册中心和配置中心namespace和group一致

1.7.1 订单微服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
seata:
application-id: ${spring.application.name}
# seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
tx-service-group: default_tx_group
registry:
# 指定nacos作为注册中心
type: nacos
nacos:
application: seata-server
server-addr: 10.211.55.12:8848
namespace:
group: SEATA_GROUP

config:
# 指定nacos作为配置中心
type: nacos
nacos:
server-addr: 10.211.55.12:8848
namespace: e56de5d4-a37e-49c0-b613-f85a7f67cb36
group: SEATA_GROUP
data-id: seataServer.properties

1.7.2 库存微服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
seata:
application-id: ${spring.application.name}
# seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
tx-service-group: default_tx_group
registry:
# 指定nacos作为注册中心
type: nacos
nacos:
application: seata-server
server-addr: 10.211.55.12:8848
namespace:
group: SEATA_GROUP

config:
# 指定nacos作为配置中心
type: nacos
nacos:
server-addr: 10.211.55.12:8848
namespace: e56de5d4-a37e-49c0-b613-f85a7f67cb36
group: SEATA_GROUP
data-id: seataServer.properties

1.7.3 用户微服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

seata:
application-id: ${spring.application.name}
# seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
tx-service-group: default_tx_group
registry:
# 指定nacos作为注册中心
type: nacos
nacos:
application: seata-server
server-addr: 10.211.55.12:8848
namespace:
group: SEATA_GROUP

config:
# 指定nacos作为配置中心
type: nacos
nacos:
server-addr: 10.211.55.12:8848
namespace: e56de5d4-a37e-49c0-b613-f85a7f67cb36
group: SEATA_GROUP
data-id: seataServer.properties

1.8 在全局事务发起者中添加@GlobalTransactional注解

image-20230516195724780

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
// 开启分布式全局事务,名称为 createOrder
@GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
public void createOrder(Integer shopId,Integer count, BigDecimal price, String title, Integer userId) {
// 创建订单
SysOrder order = new SysOrder();
order.setShopId(shopId);
order.setPrice(price);
order.setStatus(1);
order.setTitle(title);
order.setUserId(userId);
sysOrderMapper.insertSelective(order);

// 扣减库存
stockFeignService.reduceStock(shopId,count);

// 扣除余额
userFeignService.reduceBalance(userId,price);
}

1.9 模拟测试事务成功

请求下单接口

image-20230516200147531

订单表

创建订单成功

image-20230516200211116

库存表

扣减库存1 成功

image-20230516200216697

用户表

扣减费用10元成功

image-20230516200225971

2.0 模拟测试事务失败全局回滚成功

image-20230516200733429

接下来我们模拟回滚失败的话,我们余额只有90,而我们去让他扣100的费用,这样我们的业务逻辑 判断余额不够扣,就抛出异常。

image-20230516200409033

seata控制台

image-20230516200542474

回滚之前的数据表

回滚之前,成功创建了订单并且扣了库存,按理说如果回滚成功后,订单会删除并恢复库存

回滚前的表

image-20230516200716955

回滚之后的数据表

订单表

订单创建成功后,又回滚删除了

image-20230516200818221

库存表

库存也回滚了

image-20230516200824710

用户表

image-20230516200832102

2.1 模拟测试事务失败脏数据导致回滚失败

此时如上面的实验一样,扣除100的余额,库存从100扣减1 成99 ,由于用户余额只有90,此时在扣除100的话,必然会抛出异常导致回滚,而这里我们打了断点,我们在全局事务回滚之前,手动把 库存99 改成1000,这样就造成了脏数据,回滚的时候会核对 当前的数据库的值和 undoLog的后置镜像 是否一致,不一致的话就会陷入无限回滚状态。

全局事务开始之前的库存表

2912989

扣减库存后回滚之前的表

此时因为用户余额不足,已经抛出异常了,需要回滚库存表和订单表。

19182989

手动修改库存

在回滚之前,我们模拟其他线程介入修改库存,导致脏数据

image-20230516201706990

image-20230516201542675

修改成功后,我们释放 断点。

image-20230516201907553

很明显,库存服务的本地事务一直报错,一直在不停的回滚,但是却回滚失败,这个全局事务就会陷入锁死的状态,需要人工干预。

此时我们只需要把库存(在回滚的过程中,修改到的数据)改成 回滚之前的数据就行了:

image-20230516202025541

当我们修改之后,RM会尝试的去回滚,回滚成功后告知TC,全局事务结束。

image-20230516202039227

回滚之后,库存回到了100,订单表 也回滚成之前的样子。

2912989

Seata XA模式

1)XA机制

XA机制

XA也有TM、TC、RM,TM作为事务管理器,TC为事务协调者,RM为事务参与者。

  1. TM发起全局事务告知TC,TC返回XID给TM
  2. RM将本地事务分支注册到TC,TC根据XID和BID进行绑定存到 seata 数据库中
  3. RM XA本地事务开启后,执行SQL后 进行 第一阶段预提交 Prepare 给TM
  4. TM等待都收到RM的Prepare后,判断是否要进行 全局事务回滚还是提交,然后把结果告知TC
  5. TC告知本次全局事务底下的所有RM进行本地事务回滚
  6. RM收到 二阶段通知后,进行本地事务回滚或提交,然后释放掉数据库连接

XA和AT大致一样,都是基于两阶段提交(2PC)的形式演变而来的,不过有几点不太一样:

1、使用XA模式的微服务数据库不需要undo_log表,undo_log表仅用于AT模式

2、AT模式在第一阶段就会释放数据库连接,而XA模式则和传统的2PC一样 在全局事务结束后才会释放连接

基于上述,在生产环境中能用AT就用AT,金融场景则用TCC,XA的场景非常少用,因为XA非常浪费数据库性能资源。

2)Spring Cloud Alibaba整合Seata XA实战

对比Seata AT模式配置,只需修改两个地方:

  • 微服务数据库不需要undo_log表,undo_log表仅用于AT模式
  • 修改数据源代码模式为XA模式
  • 事务发起者的微服务模块,也要开启本地事务 @Transactional

2.1 设置seata 模式为XA

1
2
3
seata:
# 数据源代理模式 默认AT
data-source-proxy-mode: XA

2.2 事务发起者(TM)开启本地事务

XA模式的话,除了要开启@GlobalTransactional 全局事务外,还要在事务发起者 开启@Transactional本地事务,否则TM无法正常回滚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
@Transactional
// 开启分布式全局事务,名称为 createOrder
@GlobalTransactional(name="createOrder",rollbackFor=Exception.class)
public void createOrder(Integer shopId,Integer count, BigDecimal price, String title, Integer userId) {
// 创建订单
SysOrder order = new SysOrder();
order.setShopId(shopId);
order.setPrice(price);
order.setStatus(1);
order.setTitle(title);
order.setUserId(userId);
sysOrderMapper.insertSelective(order);

// 扣减库存
stockFeignService.reduceStock(shopId,count);
// 扣除余额
userFeignService.reduceBalance(userId,price);
}