什么叫事务 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 { conn.commit(); }catch (Exception e) { conn.rollback(); }finally { conn.close(); }
2)分布式事务 在微服务架构中要完成一个业务的功能往往会跨多个服务和数据库进行操作,这样就会产生分布式事务。
分布式事务是为了保证数据一致性,当应用需要对多个服务、数据库进行操作时,确保 要嘛全部都成功,要嘛全部都失败。
而分布式事务为了确保ACID特性,就必然会带来性能问题,这个需要根据实际的场景和架构决定是否使用分布式事务。
在生产环境中,能避免跨库产生的分布式事务问题,尽量避免分布式事务,实在避免不了在根据实际的业务去选择合适的中间件和协议。
2.1 分布式事务常见场景 跨数据库事务
跨数据库事务指的是:一个应用的业务逻辑需要同时操作多个数据库。
如果我们需要实现分布式事务,可以通过三方中间件,也可以自己想办法 创建2个连接(方法),判断2个连接的执行结果,如果有一个连接执行失败了,则进行手动回滚,如果都执行成功,就放行。
分库分表
当一个数据库的数据量过于庞大时,我们会对其进行分库分表,但是分库分表后随之而来的就是分布式事务。
如:当我们插入一条SQL,分库分表中间件 肯定会对SQL的字段进行分片后分配到不同的数据库存储,这就无法必然分布式事务。
微服务架构
SA需要完成某个业务逻辑,如 下单后需要去 执行库存和订单的逻辑,需要调用其他微服务去操作对应的数据库,这就会出现 跨服务跨数据库的情况,那就必须保证 要嘛都成功,要嘛都失败,才能确保数据一致性。
3)两阶段提交协议(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会进行本地事务回滚。
关于全局事务和事务分支
每个事务分支都有自己的一个本地事务,具备ACID特性,而多个事务分支组成一个 大的全局事务,全局事务也具备ACID特性。
一个全局事务包含了多个事务分支,全局事务里面的所有事务分支要嘛全部都成功,要嘛全部都失败。
3.3 两阶段的缺点
同步阻塞问题
2PC的参与者第一阶段时会获取数据库的连接,只有等到第二阶段提交或回滚后才会释放连接。这就会导致 连接被占用的性能问题,如果连接池的连接已经溢出了,就会出现阻塞的情况。
TM单点故障
TM起到了一个事务协调的作用,如果TM突然发生了故障,那么所有的参与者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模式流程
名词介绍
前置镜像:执行业务SQL之前的数据
后置镜像:执行业务SQL之后的数据
undoLog日志:通过XID和BID 记录前置镜像和后置镜像等数据,而后期回滚时可以通过XID和BID定位到相应的回滚日志
TM通过RPC请求TC开启一个全局事务,TC会生成一个XID(全局事务唯一标识)返回给TM,并随着请求把XID传递给下游
RM会请求TC 将本地事务注册为全局分支事务,并将XID与RM的BID进行关联
RM如果业务逻辑处理没问题,会直接对本地事务commit 落盘,并且释放本地锁和连接资源(第一阶段就直接commit了)
RM在提交之前会把业务数据、前置镜像、后置镜像存储到undolog表中,便于后期回滚数据
如果RM处理的时候出了异常,会把异常一层一层往上抛,最终抛到了顶部的TM服务上,TM会捕获异常后通知TC全局回滚
注意:如果微服务把异常给拦截了,如sentinel的降级策略,就会导致异常无法被TM捕获,TM无法知道是否出现了问题,就无法通知TC进行全局回滚
TM请求TC,告知XID对应的全局事务是需要 commit还是rollback
TC告知XID关联的所有RM 对本地事务进行commit或rollback
RM收到TC的确认后,如果是commit后会对本地事务进程commit,如果是rollback就按照之前预先准备的回滚SQL 进行回滚
回滚的时候,会拿 后置镜像和当前数据库的数据进行对比是否一致
如果一致,就进行回滚,回滚成功后会删除 undoLog日志
如果不一致就会出现 回滚失败的情况,整个事务都会挂住,我们可以通过实现seata的接口进行预警,进行人工干预
注意细节
1、如果RM出现了异常或库存扣减失败抛出异常了,该异常会层层往上抛,切记不可捕获该异常,如sentinel 会捕获异常进行降级处理,否则会导致TM无法捕获该异常,自然无法告知TC进行回滚
2、RM会在第一阶段就进行commit并且释放锁和连接资源,在提交之前会把业务数据和业务回滚SQL存储到undolog表中,如果后面出现了异常,就会按照业务回滚SQL进行回滚
2.2 电商例子
我们以电商场景举例,A客户在电商系统上下了一个订单,订单模块会进行一些列操作,随后通过open feign调用支付模块以及库存模块进行相关操作。
事务流程
订单模块作为全局事务发起者,集成了RM和TM,TM告知TC 发起全局事务,TC返回XID
订单模块通过XID进行关联后,进行对应的业务操作,commit之前把业务数据和前置镜像和后置镜像存储到undolog表中
commit 成功后释放本地锁和数据库连接,并告知TM commit成功,通过open feign调用下一个模块
订单模块和上面步骤一致,通过XID进行关联后,执行对应的业务操作后进行commit,commit成功的话就释放锁和连接并告知TM
库存模块执行发现异常后,就会把异常层层往上抛,最终抛到了TM 被TM捕获,TM告知TC全局事务结果
TC收到结果后 告知全局事务下的所有RM进行本地事务回滚
RM收到本地事务回滚的通知后,会根据之前commit时候提前准备好的反向回滚SQL进行本地事务回滚
回滚的时候,会拿 后置镜像和当前数据库的数据进行对比是否一致
如果一致,就进行回滚,回滚成功后会删除 undoLog日志
如果不一致就会出现 回滚失败的情况,整个事务都会挂住,我们可以通过实现seata的接口进行预警,进行人工干预
3)Seata AT设计思路 Seata AT不会对代码有侵入,是一种改进后的两阶段提交方式,不同于原本的 2PC。
3.1 一阶段
RM在一阶段的时候会做很多事情,流程如下:
解析SQL
查询前置镜像
为了确保在二阶段进行回滚的时候,可以对比后置镜像是否和回滚前当前数据库的数据是否一致,需要保存前置镜像和后置镜像
前置镜像:执行SQL之前的数据,如 执行SQL之前数据为100,修改之后为200
后置镜像:执行SQL之后的数据
插入业务SQL
查询后置镜像
插入undoLog日志
为了确保在二阶段完成回滚,就必须插入一个 undoLog日志,来记录 执行的业务SQL和前置镜像和后置镜像数据
本地事务commit提交
通知TC事务分支状态
释放本地锁和数据库连接资源
前置镜像和后置镜像的细节
值得注意的是,由于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回滚本地事务流程
RM通过XID和Branch ID找到相应的回滚日志记录
查询记录的后置镜像是否和当前数据库的值一致
如果一致的话,就进行回滚,回滚后删除 undoLog日志记录,然后commit提交到本地事务并告知TC回滚成功
如果不一致,就无法回滚,因为回滚了也会出现脏数据的情况,这时候就需要人工介入,该事务就会锁死
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
解压后目录
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);
创建后
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注册中心记录了服务和服务地址的映射关系。在分布式架构中,服务会注册到注册中心,当服务需要调用其它服务时,就到注册中心找到服务的地址,进行调用。
如:我们将TC和TM分别注册到Nacos注册中心后,TC和TM之间就可以通过Nacos发现彼此并进行通信。
创建Nacos命名空间
修改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: 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: type: nacos nacos: application: seata-server server-addr: 127.0 .0 .1 :8848 group: SEATA_GROUP namespace: cluster: default username: password:
注意:请确保client(微服务)与seata server的注册处于同一个namespace和group,不然会找不到服务。
1.4 新增Nacos配置
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 service.vgroupMapping.default_tx_group =default service.default.grouplist =127.0.0.1:8091 service.enableDegrade =false service.disableGlobalTransaction =false 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 tcc.fence.logTableName =tcc_fence_log tcc.fence.cleanPeriod =1h log.exceptionRate =100 store.mode =db store.lock.mode =db store.session.mode =db store.publicKey =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 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 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 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.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 配置的事务分组保持一致
我们可以通过分组,来逻辑划分 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
这个seata控制台可以实时看到全局事务的处理情况,起到一个监控全局事务的作用,通过这里也可以看到一些脏数据导致回滚失败 不停回滚的事务,但是一般我们遇到这种情况,都是修改seata源码,让他直接发一个钉钉通知等。
Nacos注册中心也可以看到seata注册成功
支持的启动参数
参数
全写
作用
备注
-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.1 工程介绍
项目源码地址:https://files.javaxing.com/seata/SpringCloudAlibabaSeata.zip
1.2 环境依赖
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 /> </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 > <dependency > <groupId > mysql</groupId > <artifactId > mysql-connector-java</artifactId > <version > 8.0.29</version > </dependency > <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 > <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 > <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
订单数据库
库存数据库
用户数据库
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 > <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 @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;@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;@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" > <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" > 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 > <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" > <id column ="user_id" jdbcType ="INTEGER" property ="userId" /> <result column ="balance" jdbcType ="DECIMAL" property ="balance" /> </resultMap > <sql id ="Base_Column_List" > 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 <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';
1.7 微服务bootstrap.yml添加seata配置
注意:请确保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} tx-service-group: default_tx_group registry: type: nacos nacos: application: seata-server server-addr: 10.211 .55 .12 :8848 namespace: group: SEATA_GROUP config: 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} tx-service-group: default_tx_group registry: type: nacos nacos: application: seata-server server-addr: 10.211 .55 .12 :8848 namespace: group: SEATA_GROUP config: 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} tx-service-group: default_tx_group registry: type: nacos nacos: application: seata-server server-addr: 10.211 .55 .12 :8848 namespace: group: SEATA_GROUP config: 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注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @Override @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 模拟测试事务成功 请求下单接口
订单表
创建订单成功
库存表
扣减库存1 成功
用户表
扣减费用10元成功
2.0 模拟测试事务失败全局回滚成功
接下来我们模拟回滚失败的话,我们余额只有90,而我们去让他扣100的费用,这样我们的业务逻辑 判断余额不够扣,就抛出异常。
seata控制台
回滚之前的数据表
回滚之前,成功创建了订单并且扣了库存,按理说如果回滚成功后,订单会删除并恢复库存
回滚之后的数据表
订单表
订单创建成功后,又回滚删除了
库存表
库存也回滚了
用户表
2.1 模拟测试事务失败脏数据导致回滚失败 此时如上面的实验一样,扣除100的余额,库存从100扣减1 成99 ,由于用户余额只有90,此时在扣除100的话,必然会抛出异常导致回滚,而这里我们打了断点,我们在全局事务回滚之前,手动把 库存99 改成1000,这样就造成了脏数据,回滚的时候会核对 当前的数据库的值和 undoLog的后置镜像 是否一致,不一致的话就会陷入无限回滚状态。
全局事务开始之前的库存表
扣减库存后回滚之前的表
此时因为用户余额不足,已经抛出异常了,需要回滚库存表和订单表。
手动修改库存
在回滚之前,我们模拟其他线程介入修改库存,导致脏数据
修改成功后,我们释放 断点。
很明显,库存服务的本地事务一直报错,一直在不停的回滚,但是却回滚失败,这个全局事务就会陷入锁死的状态,需要人工干预。
此时我们只需要把库存(在回滚的过程中,修改到的数据)改成 回滚之前的数据就行了:
当我们修改之后,RM会尝试的去回滚,回滚成功后告知TC,全局事务结束。
回滚之后,库存回到了100,订单表 也回滚成之前的样子。
Seata XA模式 1)XA机制
XA也有TM、TC、RM,TM作为事务管理器,TC为事务协调者,RM为事务参与者。
TM发起全局事务告知TC,TC返回XID给TM
RM将本地事务分支注册到TC,TC根据XID和BID进行绑定存到 seata 数据库中
RM XA本地事务开启后,执行SQL后 进行 第一阶段预提交 Prepare 给TM
TM等待都收到RM的Prepare后,判断是否要进行 全局事务回滚还是提交,然后把结果告知TC
TC告知本次全局事务底下的所有RM进行本地事务回滚
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: 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 @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); }