事务
事务的作用是使用户的操作一口气成功,要么成功提交,要么都失败回滚
严格意义上的事务特性
为什么会出现,分布式事务是什么
传统事务是数据库层面的行为,它本身就是数据库提供的机制
单机项目只需要连接一个数据库,事务回滚只需要回滚自身
但当场景来到分布式系统,如下图
事务实际生命周期是从开始到结束,中间经历(假设说)3 个数据库实例,并不是单一数据库的行为
即使每个数据库都开启传统事务,那也是三个相互独立的事件,无法保证其同时同步
分布式场景中不仅可能涉及多个个数据库实例,还有可能涉及其他中间件、其他服务甚至其他系统
比如订单服务调用了库存服务,运输系统调用了短信通知系统等,此时更不可能通过传统事务控制
因此,分布式事务其实是一种业务上的概念,这是因为在分布式场景下,传统数据库事务是失效的
达成分布式事务的思路和方式
分布式事务整体上可以整理成两套大思路:强一致性和最终一致性
二者的本质区别是对事务原子性和一致性的执行力度,即实际执行中是否允许中间状态
但是,在各个技术实际实现时,出于对各种业务情况的妥协会出现不同方向的妥协
比如 2PC 最初是一个强一致性的思路,但 AT 在它的基础上允许它出现中间状态(写隔离)
因此,现在大部分场景下是按照开发手感划分的,即是主要依赖听口令提交还是事后补偿来进行区分
强一致性 | 最终一致性 | |
---|---|---|
允许中间状态 | × | √ |
关注焦点 | 事务是否可以执行,确认后才会提交 | 事务怎么执行,事务步骤最终可以都完成或都不完成 |
实现 | 2PC(AT / XA) / 3PC / TCC | 本地消息表 / 消息事务 / 最大努力通知 / SAGA |
技术依赖 | 依赖独立的分布式事务平台/很重的业务耦合 | 耦合于业务和消息中间件 |
事务层面 | 持久化层 / 业务(TCC) | 业务 |
开发复杂度 | 低 / 高(TCC) | 中 |
事务解决程度 | 不完善 / 完善(TCC) | 完善 |
高性能和简单易用的分布式事务服务
提供了多种模式:AT | XA | TCC | SAGA
术语
Seata 官网给出了三个概念的诠释和示意图,如下,
TC (Transaction Coordinator) - 事务协调者
维护全局和分支事务的状态,驱动全局事务提交或回滚
TM (Transaction Manager) - 事务管理器
定义全局事务的范围:开始全局事务、提交或回滚全局事务
RM (Resource Manager) - 资源管理器
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚
TC 和 TM 在诠释和示意图中非常容易混淆,可以使用下面两个视角对应上图
在服务调用视角,核心是 Service,是由 Service 开始的调用,逻辑上它直接引用了其他服务
在事务交互视角,核心是 TC,虽然事务依然由 TM(Service) 发起,但是是由 TC 进行实际驱动
模式
seata 提供了多种模式:AT | XA | TCC | SAGA,在使用和官网推荐上重点是 AT 模式
前置:原始的 2PC 事务流程
分布式事务的难点
原始的 2PC 事务二阶段的职责
原始的 2PC 事务的缺点
原始 2PC 事务最大的缺点是 阻塞 问题
即使事务参与者中的一部分已经处理完分布式事务中和自己相关的部分
依然不能释放本地事务中的锁资源,但实际上这属于无效的等待,等待的时间被浪费了
AT
AT(自动事务:Auto Transaction) 模式,是对 2PC(两阶段提交协议) 的演变:
AT 模式的流程
AT 模式提交回滚
依赖于对 sql 的解析,从原始 sql 中解析相关数据的前后镜像,并依赖它们完成回滚操作
这种方式可以看做对基于消息通知的分布式事务的 2PC 式改造,并由框架自动完成
AT 模式缺点
2PC 阻塞问题 是它为了实现强一致性做出的悲观让步,2PC 可以看做一个悲观锁,它认为在一个事务中,各个 RM 是很可能出问题的,所以它会一直让 RM 持有资源锁不释放,直到所有 RM 都确认正常的完成了。
而在 AT 模式,它相当于乐观锁,认为一个事务中,不出现问题一切正常才是大概率,所以 RM 应该执行完成就提交,然后去处理其他事务(注意,这个事务和上一个事务对应不同的全局锁,即对应不同的资源)。当其他 RM 异常后,本 RM 对于对应的分布式事务进行回滚(Undo)即可。如下图,
XA
XA 模式的流程
XA 模式提交回滚 依赖于对数据源和数据源连接的 XA 代理,其实现机理比 TA 更底层
最好的分布式事务其实是数据库天然支持分布式事务
XA 模式就是用代理数据源的形式变相实现这一效果
XA 模式的缺点
TCC
TCC 模式的流程
比如对于扣减库存,try 对应预占库存、comfirm 对应正式扣减库存、cancel 对应预占库存的释放
TCC 模式泛用性极高,但 try/comfirm/cancel 三个阶段都需要开发人员手撸代码,因此业务侵入极高
SAGA
saga 是一种长事务解决方案,在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现,如下图
这是一种按流程机制处理分布式事务的方式
各模式横向比较
AT | XA | TCC | SAGA | |
---|---|---|---|---|
一致性级别 | 最终一致性 | 强一致性 | 强一致性 | 最终一致性 |
侵入 | 少 | 少 | 多 | 中? |
保障 | redo/undo 日志补偿 | 数据源/数据源连接的 XA 代理 | 手撸补偿 | 流程机制 |
版本选择
不同版本的 Seata-server 配置使用上差别较大,这里使用 1.5.2(这组件简直反复横跳)
1.5.2 版本的配置都被合并到了一个 application.yml 中
同时对 nacos 作为配置中心的支持程度也已经很完善了
下载 server
从 官网 或 Github Release 页面 下载需要的版本
解压
cd /opt
tar -zxvf seata-server-1.5.2.tar.gz
目录结构
配置
application.yml,重点配置 seata 的注册中心、配置中心
cd /opt/seata/conf
vim application.yml
# 此文件可以只用于配置注册中心、配置中心、控制台等
# 一些 server 端的琐碎配置可以直接在 配置中心 中指定
server:
port: 7091
spring:
application:
name: seata-server
logging:
config: classpath:logback-spring.xml
file:
path: ${user.home}/logs/seata
# seata 控制台
console:
user:
username: seata
password: seata
seata:
# seata 的配置中心
config:
# support: nacos 、 consul 、 apollo 、 zk 、 etcd3
type: nacos
nacos:
server-addr: 192.168.3.10:8848
namespace:
group: SEATA_GROUP
username: nacos
password: nacos
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key: ""
#secret-key: ""
data-id: seataServer.properties
# seata 的注册中心
registry:
# support: nacos 、 eureka 、 redis 、 zk 、 consul 、 etcd3 、 sofa
type: nacos
preferred-networks: 30.240.*
nacos:
application: seata-server
server-addr: 192.168.3.10:8848
group: SEATA_GROUP
namespace:
cluster: default
username: nacos
password: nacos
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key: ""
#secret-key: ""
# 下面的配置可以放到 nacos 中去
# server:
# store:
# metrics:
# transport:
security:
secretKey: SeataSecretKey0c382ef121d778043159209298fd40bf3850a017
tokenValidityInMilliseconds: 1800000
nacos 中配置,重点配置 seata-server 参数(比如事务组)、存储模式等,官网上相关说明如下图
配置全文可以从如下地址获取
https://github.com/seata/seata/tree/develop/script/config-center
对应不需要的配置,比如设置 store.mode=db 后所有 store.file.* 的内容
#For details about configuration items, see https://seata.io/zh-cn/docs/user/configurations.html
#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.learning=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
#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=
#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
#These configurations are required if the `store mode` is `db`. If `store.mode,store.lock.mode,store.session.mode` are not equal to `db`, you can remove the configuration block.
store.db.datasource=druid
store.db.dbType=mysql
store.db.driverClassName=org.gjt.mm.mysql.Driver
store.db.url=jdbc:mysql://192.168.3.10:3306/seata?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
store.db.user=root
store.db.password=root
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
server.enableParallelRequestHandle=false
#Metrics configuration, only for the server
metrics.enabled=false
metrics.registryType=compact
metrics.exporterList=prometheus
metrics.exporterPrometheusPort=9898
准备 seata 库
官方为 seata 使用的数据库提供了初始化脚本,初始化脚本可以通过两个位置获取
https://github.com/seata/seata/blob/develop/script/server/db/mysql.sql
使用 url 时注意匹配 seata 的版本(切换对应分支),或 安装目录/script/server/db 中
CREATE DATABASE seata;
use seata;
-- -------------------------------- 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);
启动 seata-server
cd /opt/seata/bin
./seata-server.sh
验证
client 涉及各组件版本
下面示例使用 springcloud alibaba + nacos + mysql
版本说明页面参考 github wiki
组件 | 版本 | 选择原因 |
---|---|---|
Seata | 1.5.2 | 因为 seata 不同版本配置区别较大,所以尽量使用新版本 |
Spring Cloud Alibaba | 2.2.8.RELEASE | 由 seata 版本确定 |
Nacos | 2.1.0 | 由 seata 版本确定 |
Spring Cloud | Hoxton.SR12 | 由 springcloud-alibaba 版本确定 |
Spring Boot | 2.3.12.RELEASE | 由 springcloud-alibaba 版本确定 |
druid-spring-boot-starter | 1.2.11 | ide 自动推荐 |
注意:版本选择尤为关键,版本相互不兼容可能出现各种型号的启动异常、配置无提示、配置不生效等等问题
依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
Undo 表
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';
配置
seata:
enabled: true
application-id: seata-stock-service
#与 server 端 service.vgroupMapping.[learning]=default 括号中一致
#此配置用于定义事务组,约等于 GROUP 之于 nacos,常用于划分机房
tx-service-group: learning
# 定义 seata-client 的配置中心
config:
type: nacos
nacos:
namespace: "" #为空默认是public,更改其他值则使用对应名字的 nacos 命名空间
server-addr: 192.168.3.10:8848
group: SEATA_GROUP
username: "nacos"
password: "nacos"
##if use MSE Nacos with auth, mutex with username/password attribute
#access-key: ""
#secret-key: ""
data-id: seata-stock-service.yml #具体配置的位置
# 定义 seata-client 的注册中心,即 seata-server ,即 TC
registry:
type: nacos
nacos:
application: seata-server # 与 server 端 spring.application.name 一致
server-addr: 192.168.3.10:8848
group : "SEATA_GROUP"
namespace: ""
username: "nacos"
password: "nacos"
nacos
service.vgroupMapping.learning
用于指定 seata-server 的集群名
需与 seata-server 端配置 service.vgroupMapping.learning=default 保持一致,如下图
seata-stock-service.yml
用于配置 seata-client 的各种参数
名字与上面 yml 配置中 seata.config.nacos.data-id 保持一致
此配置文件中配置均有默认值
seata:
enabled: true
application-id: seata-so-service
tx-service-group: learning
enable-auto-data-source-proxy: true
data-source-proxy-mode: AT
use-jdk-proxy: false
client:
rm:
async-commit-buffer-limit: 10000
report-retry-count: 5
table-meta-check-enable: false
report-success-enable: false
saga-branch-register-enable: false
saga-json-parser: fastjson
saga-retry-persist-mode-update: false
saga-compensate-persist-mode-update: false
tcc-action-interceptor-order: -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000
sql-parser-type: druid
lock:
retry-interval: 10
retry-times: 30
retry-policy-branch-rollback-on-conflict: true
tm:
commit-retry-count: 5
rollback-retry-count: 5
default-global-transaction-timeout: 60000
degrade-check: false
degrade-check-period: 2000
degrade-check-allow-times: 10
interceptor-order: -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000
undo:
data-validation: true
log-serialization: jackson
log-table: undo_log
only-care-update-columns: true
compress:
enable: true
type: zip
threshold: 64k
load-balance:
type: RandomLoadBalance
virtual-nodes: 10
service:
vgroup-mapping:
learning: default
grouplist:
default: 192.168.3.10:8091
enable-degrade: false
disable-global-transaction: false
transport:
shutdown:
wait: 3
thread-factory:
boss-thread-prefix: NettyBoss
worker-thread-prefix: NettyServerNIOWorker
server-executor-thread-prefix: NettyServerBizHandler
share-boss-worker: false
client-selector-thread-prefix: NettyClientSelector
client-selector-thread-size: 1
client-worker-thread-prefix: NettyClientWorkerThread
worker-thread-size: default
boss-thread-size: 1
type: TCP
server: NIO
heartbeat: true
serialization: seata
compressor: none
enable-tm-client-batch-send-request: false
enable-rm-client-batch-send-request: true
rpc-rm-request-timeout: 30000
rpc-tm-request-timeout: 30000
log:
exception-rate: 100
tcc:
fence:
log-table-name: tcc_fence_log
clean-period: 1h
saga:
enabled: false
state-machine:
table-prefix: seata_
enable-async: false
async-thread-pool:
core-pool-size: 1
max-pool-size: 20
keep-alive-time: 60
trans-operation-timeout: 1800000
service-invoke-timeout: 300000
auto-register-resources: true
resources:
- classpath*:seata/saga/statelang/**/*.json
default-tenant-id: 000001
charset: UTF-8
seata.tx-service-group 和 seata.service.vgroup-mapping.learning 的关系
以使用 nacos 为例
seata 的 server 端启动后,会向 nacos 中组成服务,如下图
其详情如下图,其中
服务名即 server 端 yml 配置中 spring.application.name,client 端中 seata.registry.nacos.application 指向此服务名
seata.service.vgroup-mapping.learning 即 server 端配置的集群名
在 client 端中:
通过 seata.tx-service-group 配置指定事务组名
通过配置 seata.service.vgroup-mapping.事务组名 获取集群名,若无法获取则出现下面提示
can not get cluster name in registry config 'service.vgroupMapping.learning', please make sure registry config correct
最后获取名为 seata.registry.nacos.application 的 seata-server 服务中,对应集群名下的服务列表作为 TC 端
若上述配置没有对应上,会出现下面提
no available service found in cluster 'default', please make sure registry config correct and keep your seata server running
启动验证
client 启动正常后,除正常的启动信息外,可见下面日志信息
register RM success. client version:1.5.2, server version:1.5.2,channel:[id: 0xec773717, L:/192.168.3.7:53074 - R:/192.168.3.10:8091]
启动并注册两个服务
两个服务的依赖配置等参考上面
业务简述
so 服务中提供一个服务 /so,模拟下单,下单后先在 so 库中插入订单信息
随后 so 服务调用 stock 服务中 /stock 接口扣减库存,
若上面两步成功,则下单成功,否则回滚
so 代码
注意 @GlobalTransactional(name = “add-so”)
@RequestMapping(value = "", method = RequestMethod.POST)
@GlobalTransactional(name = "add-so")
@ResponseBody
public CommonResult<SoEntity> add(@RequestBody SoEntity entity){
entity = new SoEntity();
entity.setProductId(1L);
entity.setCount(1);
entity.setMoney(new BigDecimal(1));
entity.setUserId(1L);
entity.setStatus(0);
int count = soManager.add(entity);//1
stockOpenfeignService.stock(new StockEntity());//2
//int i = 1/0; //3
if (0!= count)
return new CommonResult<SoEntity>(200,"下单成功 for: " + this.serverPort ,entity);
else return new CommonResult<>(400,"下单失败 for: " + this.serverPort);
}
@FeignClient(value = "seata-stock-service")
public interface StockOpenfeignService {
@RequestMapping(value = "/stock", method = RequestMethod.POST)
CommonResult<StockEntity> stock(@RequestBody StockEntity entity);
}
stock 代码
@RequestMapping(value = "", method = RequestMethod.POST)
@ResponseBody
public CommonResult<StockEntity> add(@RequestBody StockEntity entity){
entity = new StockEntity();
entity.setId(1L);
entity.setProductId(1L);
entity.setCount(1);
int count = stockManager.stock(entity);
if (0!= count)
return new CommonResult<>(200,"扣减库存成功 for: " + this.serverPort ,entity);
else return new CommonResult<>(400,"扣减库存成功 for: " + this.serverPort);
}
效果
//1 执行后断点,可见 so 库中 so 表已有数据,undo_log 存在一条日志
//2 执行后断点,可见 storage 库中 storage 表已有数据,undo_log 存在一条日志
继续执行,业务执行成功,两个 undo_log 表中数据消失
undo_log 表中的数据是临时数据,用于回滚本地事务使用,当分布式事务完成时会自动删除
打开==//3==处的注释,并在这里断点,可见数据如下
继续执行,//3 处出现异常,数据库数据如下,实现全局回滚