1. 为什么要使用分布式事务?
1.1. 问题重现
使用微服务架构,当账户余额为 0 时,还可以继续下单,而且扣减库存;或者当库存不足时,也可以下单继续扣减余额等问题,造成数据不一致。
1.2. 新的需求
下单逻辑需要保证数据一致性,当账户余额不够或者库存不足,该回滚库存回滚库存,该回滚账户回滚账户,让当前下单失败。
1.3. 解决方案
思考:使用 Spring 事务能解决问题吗? 不能;
使用分布式事务解决方案 Seata(官方推荐)
2. Seata 是什么?
官方文档:Seata 是什么? | Apache Seata
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。
首选 Seata AT 模式(官方推荐),可以做到业务无侵入。
3. Seata AT 模式的工作流程
3.1. 非常重要的三个概念
- TC(Transaction Coordinator):事务协调器,负责全局事务的管理,包括事务的开启、提交、回滚等操作,它会记录全局事务和分支事务的状态信息。
- TM(Transaction Manager):事务管理器,主要用于开启、提交或回滚全局事务。在应用代码中,开发人员通过 TM 来定义全局事务的边界。
- RM(Resource Manager):资源管理器,负责分支事务的管理,与 TC 交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
例如:当用户通过订单服务下单,调用库存服务扣减库存,调用账户服务扣减账户余额。
订单服务要接入 TM 组件,下单操作需要开启全局事务(向 TC 申请一个全局事务 XID),进入下单逻辑,如果下单正常,需要通知 TC 提交全局事务;如果下单出现异常,比如余额不够,需要通知 TC 回滚全局事务。
订单服务、库存服务、账户服务都需要接入 RM 组件,提交本地事务的同时,需要向 TC 注册分支事务信息,接受 TC 的通知,提交或回滚分支事务。
TC 是独立的服务,维护 TM 申请的全局事务信息和 RM 提交的分支事务信息,TM 通知 TC 全局事务提交或回滚的时候, TC 要通知 RM 分支事务提交或回滚。
3.2. AT 模式工作流程
问题:如何知道调用的是同一个分布式事务?
开启全局事务会分配一个全局事务 XID,微服务链路会传递 XID,注册每个分支事务都会带上 XID。
4. Seata Server(TC)安装部署
4.1. 注意版本
Seata Version:2.0.0
4.2. 下载地址
Seata Java Download | Apache Seata
4.3. 官网参考资料
4.3.1. Seata 新手部署指南
4.3.2. Seata 官网参数配置
4.4. TC 端存储模式
全局事务、分支事务信息存储到哪里了?
4.4.1. Seata 1.x 支持的模式
4.4.1.1. file:单机模式
全局事务、分支事务信息内存中读写并持久化本地文件 root.data,性能较高,但是只支持单机模式部署,生产环境不考虑。
4.4.1.2. db:高可用模式
全局事务、分支事务信息通过 db 共享,相应性能差点。
4.4.1.3. redis:1.3及以上版本支持,性能较高
存在事务信息丢失风险,请提前配置适合当前场景的 redis 持久化配置。
4.4.2. Seata2.x 新增的 Raft 模式
利用 Raft 算法实现多个 TC 之间数据的同步。Raft 模式是最理想的方案,但当前并不成熟,所以不考虑。
4.4.3. 从稳定性角度考虑,最终选择采用 db 模式
创建 Seata 数据库,sql 脚本在 seata 目录中
例如:seata-server-2.0.0\seata\script\server\db\mysql.sql
4.5. 思考: RM 和 TM 如何找到 TC 服务的?
可以将 TC 注册到 Nacos 注册中心,TM 和 RM 通过 Nacos 注册中心实现 TC 服务的发现。
注意:Seata 的注册中心是作用于 Seata 自身的,和微服务中的配置的注册中心无关,但可以共用注册中心。可以创建一个 Seata 的命名空间,区分 Seata 的 TC 服务和业务微服务。
4.6. 思考: TC 的配置是不是也可以交给 Nacos 配置中心配置?
可以。
4.7. 最终方案:db 存储模式 + Nacos(注册&配置中)方式部署
4.7.1. 前置环境准备
4.7.1.1. db 模式准备好 seata 的数据库
例如:seata-server-2.0.0\seata\script\server\db\mysql.sql
4.7.1.2. 准备好 Nacos 环境
Nacos 控制台中创建一个 seata 的命名空间
4.7.2. Seata 配置融合 Nacos 注册中心
配置将 Seata Server 注册到 Nacos 中。主要配置 seata 的配置文件(路径:seata-server-2.0.0\seata\conf\application.yml)
seata:
registry:
# support: nacos, eureka, redis, zk, consul, etcd3, sofa
type: nacos
nacos:
application: seata-server
server-addr: icoolkj-mall-nacos-server:8848
namespace: seata
group: SEATA_GROUP
cluster: default
注意:
- 这个 cluster 配置,默认 TC 是 default 集群,TM 和 RM 都要通过这个集群名找 TC 集群。
- 请确保client(RM 和 TM)与 server(TC)的注册出于同一个 namespace 和 group,不然会找不到 server(TC)服务。
4.8. Seata 配置融合 Nacos 配置中心
4.8.1.1. 配置 Seata 使用 Nacos 配置中心
主要配置 seata 的配置文件(路径:seata-server-2.0.0\seata\conf\application.yml)
seata:
config:
# support: nacos, consul, apollo, zk, etcd3
type: nacos
nacos:
server-addr: icoolkj-mall-nacos-server:8848
namespace: seata
group: SEATA_GROUP
data-id: seataServer.properties
4.8.1.2. 将 seata server 的配置上传至 Nacos 配置中心
-
- 获取 seata server 配置信息(文件路径:seata\script\config-center\config.txt)
- 修改为 db 存储模式,并修改 mysql 连接配置
#Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
store.mode=db
store.lock.mode=db
store.session.mode=db
#Used for password encryption
store.publicKey=#These configurations are required if the
store mode
isdb
. Ifstore.mode,store.lock.mode,store.session.mode
are not equal todb
, you can remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://icoolkj-mall-mysql:33060/seata2.0.0?useSSL=false&characterEncoding=utf8&useUnicode=true&rewriteBatchedStatements=true
store.db.user=root
store.db.password=icoolDP1988
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 -
- 配置事务分组,TC 要与 client(TM 和 RM)配置的事务分组一致
-
事务分组:seata 的资源逻辑,可以按微服务的需要,在应用程序(客户端)对自行定义事务分组,每组取一个名字。
-
集群:seata-server 服务端一个或多个节点组成的集群 cluster。 应用程序(客户端)使用时需要指定事务逻辑分组与 Seata 服务端集群的映射关系。
service.vgroupMapping.default_tx_group=default
注意:事务分组如何找到后端的 Seata 集群?
-
- 在 Nacos 控制台 seata 命名空间下新建 dataId 为 seataServer.properties 配置,配置内容为修改后的 config.txt 信息。
#Transport configuration, for client and server
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=falseclient.metadataMaxAgeMs=30000
#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=1hYou can choose from the following options: fastjson, jackson, gson
tcc.contextJsonParserType=fastjson
#Log rule configuration, for client and server
log.exceptionRate=100#Transaction storage configuration, only for the server. The file, db, and redis configuration values are optional.
store.mode=db
store.lock.mode=db
store.session.mode=db
#Used for password encryption
store.publicKey=#These configurations are required if the
store mode
isdb
. Ifstore.mode,store.lock.mode,store.session.mode
are not equal todb
, you can remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=com.mysql.cj.jdbc.Driver
store.db.url=jdbc:mysql://icoolkj-mall-mysql:3306/seata2.0.0?useUnicode=true&rewriteBatchedStatements=true&useSSL=false&characterEncoding=utf8&allowPublicKeyRetrieval=true
store.db.user=root
store.db.password=123456
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#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.session.branchAsyncQueueSize=5000
server.session.enableBranchAsyncRemove=false
server.enableParallelRequestHandle=true
server.enableParallelHandleBranch=false#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
注意:在 seata 命名空间下建立的 seataServer.properties 配置,要和 seata 的配置文件(seata-server-2.0.0\seata\conf\application.yml)的 config 配置要对应上,特别注意 seataServer.properties 是否是 SEATA_GROUP。
4.9. 启动 Seata Server
- windows 点击 bin 目录下 seata-server.bat 直接启动。
- 启动成功,查看控制台 http://localhost:7091,账号密码:seata。
- 在 Naocs 控制台,查看 seata-server 注册情况。
5. 微服务整合 Seata AT 模式实战
5.1. 业务场景
用户下单,订单服务调用库存服务扣减库存,调用账户服务扣减账户余额。
事务发起者:订单服务。
事务参与者:库存服务,账户服务,商品服务。
5.2. 订单服务(事务发起者)整合 Seata
5.2.1. 订单服务 pom.xml 引入 Seata 的依赖
<!-- seata 依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
5.2.2. 订单服务对应数据库中添加 undo_log 表(仅支持 AT 模式)
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';
ALTER TABLE `undo_log` ADD INDEX `ix_log_created` (`log_created`);
5.2.3. 订单服务 application.yml 中添加 seata 配置
seata:
# seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
tx-service-group: default_tx_group
registry:
# 指定nacos作为注册中心
type: nacos
nacos:
application: seata-server
server-addr: icoolkj-mall-nacos-server:8848
namespace: seata
group: SEATA_GROUP
config:
# 指定nacos作为配置中心
type: nacos
nacos:
server-addr: icoolkj-mall-nacos-server:8848
namespace: seata
group: SEATA_GROUP
data-id: seataServer.properties
优化:可以将 seata 配置移到 Nacos 配置中心。
-
- Nacos 控制台创建一个 dataId 为 seata-client.yml 的配置,配置内容如下:
seata:
seata 服务分组,要与服务端配置service.vgroup_mapping的后缀对应
tx-service-group: default_tx_group
registry:
# 指定nacos作为注册中心
type: nacos
nacos:
application: seata-server
server-addr: icoolkj-mall-nacos-server:8848
namespace: seata
group: SEATA_GROUPconfig:
# 指定nacos作为配置中心
type: nacos
nacos:
server-addr: icoolkj-mall-nacos-server:8848
namespace: seata
group: SEATA_GROUP
data-id: seataServer.properties
-
- 订单微服务的 application.yml 引入 seata-client.yml
spring:
config:
import:
- optional:nacos:${spring.application.name}.yml
- optional:nacos:db-mysql-common.yml # mysql数据库公共配置
- nacos:nacos-discovery.yml
- optional:nacos:seata-client.yml # Seata Client 配置
5.2.4. 订单服务作为全局事务发起者,在下单的方法上面添加 @GlobalTransactional 注解
@GlobalTransactional(name = "createOrder", rollbackFor = Exception.class)
//@Transactional
public Result<?> createOrder(Long userId, Long productId, Integer orderQuantity) {
5.3. 库存服务(事务参与者)整合 Seata
5.3.1. 和整合订单服务前三步一样
5.3.2. 库存服务只需要再扣减库存方法上添加 Spring 事务 @Transaction 注解
@Transactional
public void reduceInventory(Long productId, Integer inventoryQuantity) {
5.4. 账户服务(事务参与者)整合 Seata
5.4.1. 和整合订单服务前三步一样
5.4.2. 账户服务只需要再扣减余额方法上添加 Spring 事务 @Transaction 注解
@Transactional
public void reduceBalance(Long userId, BigDecimal orderCost) {
5.5. 商品服务(事务参与者)整合 Seata
5.5.1. 和整合订单服务前三步一样
5.5.2. 订单调用商品服务的查询商品价格,无需增加事务
5.6. Seata2.x 常见问题
5.6.1. 微服务启动报错
io.seata.conf.exception.ConfigNotFoundException:service.vgroupMapping.default_tx_group configuration itme is required
产生原因:无法拉取到 service.vgroupMapping.default_tx_proup=default 这个配置,也就是找不到集群名称为 default 的 seata server 服务。
解决方式:
思路1:检查下微服务 seata 配置是否未配置事务分组 seata.tx-service-group: default_tx_group
思路2:检查下 namespace 和 group 配置 Server 端和 Client 端是否对应,特别注意 seataSever.properties 是否是 SEATA_GROUP
6. 重启所有服务,测试分布式事务是否生效
6.1. 分布式事务成功场景:模拟正常下单,扣库存,扣余额
6.2. 分布式事务失败场景:模拟下单扣库存成功,扣余额失败,事务是否回滚
6.3. 目前 seata2.0.0 版本的中存在 bug
事务发生回滚,回滚成功了,但是最外层代码无法捕捉到原始的 BusinessException 异常,只能捕捉到 RuntimeException 运行时异常。
建议不要在生产上使用该版本。
7. 小结
通过 seata 可以解决微服务分布式事务的问题。