1. 官网如下:
https://seata.io/zh-cn/docs/overview/what-is-seata.html
2. 问题 UNDO_LOG 表创建的位置是哪里?需要每一个微服务都创建一张表 UNDO_LOG(反向补偿表) 吗?
集成方法入如下:(参考官网来)
1.
1. 核心测试代码如下:
微信付款成功后,回调 支付服务,支付服务调用 订单服务修改订单状态
支付服务代码:
@GetMapping(value = "/notifyAfterPay")
public String notifyAfterPay(String orderId,Integer status) throws Exception {
return payLogService.updateOrderInfoAfterPay(orderId,status);
}
@Override
public String updateOrderInfoAfterPay(String orderId,Integer status) {
// 本地服务
//删除
payLogMapper.deleteById(orderId);
//增加
PayLog payLog = new PayLog();
payLog.setPayId(orderId);
payLog.setId(orderId);
payLog.setStatus(status);
payLog.setCreateTime(new Date());
payLogMapper.insert(payLog);
// 调用外部服务,更新订单状态
Integer i = orderService.updateOrderById(orderId, status);
if(i == 1){
return "success";
}else{
// 接入 seata 此处不需要 抛异常
return "false";
}
}
而 通过 feign 调用 的orderservice 代码如下:
@Override
public Integer updateOrderById(String id, Integer status) {
try {
log.info("to call order-service ' updateOrderStatus params : orderId {} ,status {}",id,status);
Integer result = orderFeignApi.updateOrderById(id, status);
log.info(" called order-service ' updateOrderById result : {} ",result);
return result;
} catch (Exception e) {
log.error("call order-service ' updateOrderById failed, cause : {}",e.getMessage());
}
// 返回 0 代表失败
return 0;
};
订单服务对应的controller:
@RequestMapping(value = "/updateOrderById")
public Integer updateOrderById(String id,Integer status) throws Exception {
return orderService.updateOrderById(id,status);
}
service:(此处模拟抛出一个异常,让订单服务系统异常)
@Override
//@GlobalTransactional
//@Transactional
public Integer updateOrderById(String id, Integer status) {
Oorder oorder = orderMapper.selectById(id);
oorder.setStatus(status);
int i = orderMapper.updateById(oorder);
log.info("update o_order result {}",i);
int a = 1/0;
return i;
}
先看下库里数据:
postMan测试:
测试结果:
订单服务抛异常了,但是 订单的数据还是更新了,原因就是没有加本地事务,
现在将两张表的status 都改成11 ,在订单服务的对应方法上加上 @Transactional 注解
@Override
//@GlobalTransactional
@Transactional
public Integer updateOrderById(String id, Integer status) {
Oorder oorder = orderMapper.selectById(id);
oorder.setStatus(status);
int i = orderMapper.updateById(oorder);
log.info("update o_order result {}",i);
int a = 1/0;
return i;
}
测试结果:
现在就导致了,支付服务和订单服务 对应订单 的状态不一致
能不能有一种方法让 支付服务 的方法 和 订单服务的方法都成功在提交各自的本地事务呢,只有有一个服务方法失败,其他的服务也都回滚: 有 Seata 组件
说明:
alibaba cloud ,springcloud 和 springboot 之间 是严格区分版本的,如果版本不匹配,项目启动会报错
Home · alibaba/spring-cloud-alibaba Wiki · GitHub
版本说明 · alibaba/spring-cloud-alibaba Wiki · GitHub
主要依赖:
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
2.1.3.RELEASE
fastjson
com.alibaba
error_prone_annotations
com.google.errorprone
guava
com.google.guava
点进去 2.1.3.RELEASE 查看:
发现是引擎里 有 seata 的最新依赖
pay-service 全部依赖如下:
4.0.0
org.springframework.boot
spring-boot-starter-parent
2.3.2.RELEASE
com.authresourcs
pay-service
0.0.1-SNAPSHOT
pay-service
Demo project for Spring Boot
1.8
Greenwich.SR4
org.springframework.boot
spring-boot-starter-web
com.github.wxpay
wxpay-sdk
3.0.9
org.apache.commons
commons-lang3
commons-codec
commons-codec
org.bouncycastle
bcprov-jdk15on
1.58
com.alibaba
fastjson
1.2.51
com.auth0
java-jwt
3.12.1
com.google.guava
guava
22.0
org.projectlombok
lombok
1.16.10
mysql
mysql-connector-java
8.0.18
com.baomidou
mybatis-plus-boot-starter
3.2.0
com.baomidou
mybatis-plus-annotation
3.2.0
compile
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
2.1.3.RELEASE
fastjson
com.alibaba
error_prone_annotations
com.google.errorprone
guava
com.google.guava
org.apache.rocketmq
rocketmq-spring-boot-starter
2.0.2
guava
com.google.guava
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
fastjson
com.alibaba
guava
com.google.guava
jsr305
com.google.code.findbugs
bcprov-jdk15on
org.bouncycastle
org.springframework.cloud
spring-cloud-starter-openfeign
2.1.1.RELEASE
jsr305
com.google.code.findbugs
guava
com.google.guava
bcprov-jdk15on
org.bouncycastle
org.springframework.cloud
spring-cloud-openfeign-core
io.github.openfeign
feign-core
org.springframework.cloud
spring-cloud-dependencies
Hoxton.SR9
pom
import
com.alibaba.cloud
spring-cloud-alibaba-dependencies
2.2.6.RELEASE
pom
import
配置文件:
seata.enabled=true
seata.applicationId=${spring.application.name}
# 事务组:value 是 file.conf 里 设置的
seata.tx-service-group=hxlh_tx_group
# 要和 seata 服务器里 file.conf 里 自定义 事务组名称 的 key value 一致
seata.service.vgroup-mapping.hxlh_tx_group=default
启动项目:
然后 对 order-service 也修改成一样的依赖(主要是springboot,springcloud和springcloudalibaba ,和seata 之间的 版本对应关系,否则启动会提示 找不到 类的问题),接着启动项目
发现启动成功
postMan测试:
发现报错:
需要引入 方向补偿表:
在 pay-service 里引入
说明:
其他我们业务使用的数据库中加入一个管理事务日志的表undo_log
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
接下来配置seata 服务器:
数据库:
创建三张表:
-- the table to store GlobalSession data
drop table if exists `global_table`;
create table `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_gmt_modified_status` (`gmt_modified`, `status`),
key `idx_transaction_id` (`transaction_id`)
);
-- the table to store BranchSession data
drop table if exists `branch_table`;
create table `branch_table` (
`branch_id` bigint not null,
`xid` varchar(128) not null,
`transaction_id` bigint ,
`resource_group_id` varchar(32),
`resource_id` varchar(256) ,
`lock_key` varchar(128) ,
`branch_type` varchar(8) ,
`status` tinyint,
`client_id` varchar(64),
`application_data` varchar(2000),
`gmt_create` datetime,
`gmt_modified` datetime,
primary key (`branch_id`),
key `idx_xid` (`xid`)
);
-- the table to store lock data
drop table if exists `lock_table`;
create table `lock_table` (
`row_key` varchar(128) not null,
`xid` varchar(96),
`transaction_id` long ,
`branch_id` long,
`resource_id` varchar(256) ,
`table_name` varchar(32) ,
`pk` varchar(36) ,
`gmt_create` datetime ,
`gmt_modified` datetime,
primary key(`row_key`)
);
修改seata 服务器配置:
修改 file.config
## transaction log store, only used in seata-server
service {
#vgroup->rgroup 这个很重要 客户端 的配置文件里要用的
vgroupMapping.hxlh_tx_group = "default" #修改自定义事务组名称 hxlh_tx_group
#only support single node
default.grouplist = "127.0.0.1:8091"
#degrade current not support
enableDegrade = false
#disable
disable = false
#unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent
max.commit.retry.timeout = "-1"
max.rollback.retry.timeout = "-1"
}
store {
## store mode: file、db、redis
mode = "db"
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
maxBranchSessionSize = 16384
# globe session size , if exceeded throws exceptions
maxGlobalSessionSize = 512
# file buffer size , if exceeded allocate new buffer
fileWriteBufferCacheSize = 16384
# when recover batch read size
sessionReloadReadSize = 100
# async, sync
flushDiskMode = async
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "druid"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "root"
password = "root"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
## redis store property
redis {
host = "127.0.0.1"
port = "6379"
password = ""
database = "0"
minConn = 1
maxConn = 10
maxTotal = 100
queryLimit = 100
}
}
修改registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
# 修改默认的 file 为 nacos
type = "nacos"
loadBalance = "RandomLoadBalance"
loadBalanceVirtualNodes = 10
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = ""
password = ""
}
eureka {
serviceUrl = "http://localhost:8761/eureka"
application = "default"
weight = "1"
}
redis {
serverAddr = "localhost:6379"
db = 0
password = ""
cluster = "default"
timeout = 0
}
zk {
cluster = "default"
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
consul {
cluster = "default"
serverAddr = "127.0.0.1:8500"
}
etcd3 {
cluster = "default"
serverAddr = "http://localhost:2379"
}
sofa {
serverAddr = "127.0.0.1:9603"
application = "default"
region = "DEFAULT_ZONE"
datacenter = "DefaultDataCenter"
cluster = "default"
group = "SEATA_GROUP"
addressWaitTime = "3000"
}
file {
name = "file.conf"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
#
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = ""
password = ""
}
consul {
serverAddr = "127.0.0.1:8500"
}
apollo {
appId = "seata-server"
apolloMeta = "http://192.168.1.204:8801"
namespace = "application"
apolloAccesskeySecret = ""
}
zk {
serverAddr = "127.0.0.1:2181"
sessionTimeout = 6000
connectTimeout = 2000
username = ""
password = ""
}
etcd3 {
serverAddr = "http://localhost:2379"
}
file {
name = "file.conf"
}
}
测试的 method 上加上注解:
pay-service 上业务代码:
@Override
@GlobalTransactional(name = "updateOrderInfoAfterPay",rollbackFor = Exception.class)
@Transactional
public String updateOrderInfoAfterPay(String orderId,Integer status) {
//删除
payLogMapper.deleteById(orderId);
//增加
PayLog payLog = new PayLog();
payLog.setPayId(orderId);
payLog.setId(orderId);
payLog.setStatus(status);
payLog.setCreateTime(new Date());
payLogMapper.insert(payLog);
// 调用外部服务,更新订单状态
Integer i = orderService.updateOrderById(orderId, status);
if(i == 1){
return "success";
}else{
// 接入 seata 此处不需要 抛异常
return "false";
}
}
orderService 里 调用的方法:
@Override
public Integer updateOrderById(String id, Integer status) {
try {
log.info("to call order-service ' updateOrderStatus params : orderId {} ,status {}",id,status);
Integer result = orderFeignApi.updateOrderById(id, status);
log.info(" called order-service ' updateOrderById result : {} ",result);
return result;
} catch (Exception e) {
log.error("call order-service ' updateOrderById failed, cause : {}",e.getMessage());
// 抛异常的目的是防止吃掉异常
throw new RuntimeException("修改订单状态异常");
}
};
order-service 上业务代码:
@GlobalTransactional(name = "updateOrderInfoAfterPay",rollbackFor = Exception.class)
//@Transactional
@Override
public Integer updateOrderById(String id, Integer status) {
Oorder oorder = orderMapper.selectById(id);
oorder.setStatus(status);
int i = orderMapper.updateById(oorder);
log.info("update o_order result {}",i);
//int a = 1/0;
return i;
}
postMan请求:
流程:
order-service
测试异常情况:
order-service 触发个异常
@GlobalTransactional(name = "updateOrderInfoAfterPay",rollbackFor = Exception.class)
//@Transactional
@Override
public Integer updateOrderById(String id, Integer status) {
Oorder oorder = orderMapper.selectById(id);
oorder.setStatus(status);
int i = orderMapper.updateById(oorder);
log.info("update o_order result {}",i);
int a = 1/0;
return i;
}
走PostMan 执行流程如下:
order-service
查看数据库也发现数据没有变,说明seata 生效了
说明:回滚日志,一个事务结束时会删除数据的
回滚日志里的 rollBackInfo 字段实例如下:(有更新前和更新后的 数据)
{
"@class": "io.seata.rm.datasource.undo.BranchUndoLog",
"xid": "192.168.60.84:8091:224970841895813120",
"branchId": 224970842080362496,
"sqlUndoLogs": ["java.util.ArrayList", [{
"@class": "io.seata.rm.datasource.undo.SQLUndoLog",
"sqlType": "UPDATE",
"tableName": "o_order",
"beforeImage": {
"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
"tableName": "o_order",
"rows": ["java.util.ArrayList", [{
"@class": "io.seata.rm.datasource.sql.struct.Row",
"fields": ["java.util.ArrayList", [{
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "id",
"keyType": "PRIMARY_KEY",
"type": 4,
"value": 1
}, {
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "create_time",
"keyType": "NULL",
"type": 93,
"value": ["java.sql.Timestamp", [1641998894000, 0]]
}, {
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "description",
"keyType": "NULL",
"type": 12,
"value": "测试订单1"
}, {
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "content",
"keyType": "NULL",
"type": 12,
"value": "测试订单1"
}, {
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "status",
"keyType": "NULL",
"type": 12,
"value": "11"
}]]
}]]
},
"afterImage": {
"@class": "io.seata.rm.datasource.sql.struct.TableRecords",
"tableName": "o_order",
"rows": ["java.util.ArrayList", [{
"@class": "io.seata.rm.datasource.sql.struct.Row",
"fields": ["java.util.ArrayList", [{
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "id",
"keyType": "PRIMARY_KEY",
"type": 4,
"value": 1
}, {
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "create_time",
"keyType": "NULL",
"type": 93,
"value": ["java.sql.Timestamp", [1641998894000, 0]]
}, {
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "description",
"keyType": "NULL",
"type": 12,
"value": "测试订单1"
}, {
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "content",
"keyType": "NULL",
"type": 12,
"value": "测试订单1"
}, {
"@class": "io.seata.rm.datasource.sql.struct.Field",
"name": "status",
"keyType": "NULL",
"type": 12,
"value": "12"
}]]
}]]
}
}]]
}