分布式事务解决方案 Seata - AT mode
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata
对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1
为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,未来 Seata 将以社区共建的形式帮助其技术更加可靠与完备。此前我们自己从头实现的分布式事务解决方案总是要耗费很多时间,seata的出现无疑给我们节省下很多时间,它的非侵入和侵入式的分布式事务解决方案很全面,但是在使用它的时候不要忽略seata的种种方案的理念要尽量做到知其所以然,这个对于搞技术的才是最重要的,毕竟没有什么工具是不可替代的。
下载安装Seata Server
Seata-Server-1.2.0.zip 下载(推荐 速度快)
AT模式下的 undo_log 建表语句
undo_log是AT mode 下的回滚事务表,其作用是在一阶段每个子事务提交时记录事务的的信息和回滚sql,如果事务回滚就会使用这条记录,这个事务回滚记录也会在事务完毕时被删除。
Demo
这个Demo项目使用seata的AT做一个分布式事务的实例,实例不使用任何服务注册中心如(nacos、zookppers、erurka…)
,只要有一个支持ACID的数据库就行,这里我们使用mysql数据库。
我们会创建两个项目:AccountService、BillService,分别连着不同的数据库(AT模式下每个数据库都要有undo_log表哦),两个项目没有什么实际意义就是做个Example。
AT 模式
前提
基于支持本地 ACID 事务的关系型数据库。
Java 应用,通过 JDBC 访问数据库。
整体机制
两阶段提交协议的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
写隔离
- 一阶段本地事务提交前,需要确保先拿到 全局锁 。
- 拿不到 全局锁 ,不能提交本地事务。
- 拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
读隔离
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
Demo环境
工具 | 版本 |
---|---|
java | 1.8 |
spring boot | 2.1.2 |
gradle | 4.8 |
seata | 1.2.0 |
windows | 10 |
CentOS | 7 |
Account服务
引入依赖
build.gradle
plugins {
id 'java'
}
apply plugin: 'idea'
group 'cn.springboot'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
maven { url 'https://maven.aliyun.com/repository/public/' }
mavenCentral()
}
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
// https://mvnrepository.com/artifact/org.springframework.cloud/spring-cloud-starter-oauth2
compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-oauth2', version: '2.1.2.RELEASE'
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web
compile group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: '2.1.2.RELEASE'
// https://mvnrepository.com/artifact/org.projectlombok/lombok
compile group: 'org.projectlombok', name: 'lombok', version: '1.18.12'
compileOnly 'org.projectlombok:lombok'
// developmentOnly 'org.springframework.boot:spring-boot-devtools'
annotationProcessor 'org.projectlombok:lombok'
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf
compile group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf', version: '2.1.2.RELEASE'
// https://mvnrepository.com/artifact/com.alibaba/fastjson
compile group: 'com.alibaba', name: 'fastjson', version: '1.2.56'
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator
compile group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: '2.1.2.RELEASE'
// https://mvnrepository.com/artifact/mysql/mysql-connector-java
compile group: 'mysql', name: 'mysql-connector-java', version: '5.1.47'
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-jpa
compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: '2.1.2.RELEASE'
// testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
// https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test
testCompile group: 'org.springframework.boot', name: 'spring-boot-starter-test', version: '2.1.2.RELEASE'
// -------------------- seata 相关 ----------------------------------
// https://mvnrepository.com/artifact/io.seata/seata-spring-boot-starter
compile group: 'io.seata', name: 'seata-spring-boot-starter', version: '1.2.0'
}
项目配置
特别提一句File注册配置下也是可以连公网的。
之前好像在哪看到人说File注册配置只可以本地这是不对的。
grouplist:
default: 117.23.23.1:8091
application.yml
spring:
jpa:
database: mysql
show-sql: true
hibernate:
ddl-auto: update
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
datasource:
url: jdbc:mysql://127.0.0.1:3306/exampledb
# jdbcUrl: jdbc:mysql://192.168.84.135:3306/demodb
password: 123456
username: root
driver-class-name: com.mysql.jdbc.Driver
server:
port: 8081
servlet:
context-path: /ResourceServer
seata:
enabled: true
application-id: applicationName
tx-service-group: my_test_tx_group
enable-auto-data-source-proxy: true
use-jdk-proxy: false
excludes-for-auto-proxying: firstClassNameForExclude,secondClassNameForExclude
client:
rm:
async-commit-buffer-limit: 1000
report-retry-count: 5
table-meta-check-enable: false
report-success-enable: false
saga-branch-register-enable: false
lock:
retry-interval: 10
retry-times: 30
retry-policy-branch-rollback-on-conflict: true
tm:
degrade-check: false
degrade-check-period: 2000
degrade-check-allow-times: 10
commit-retry-count: 5
rollback-retry-count: 5
undo:
data-validation: true
log-serialization: jackson
log-table: undo_log
only-care-update-columns: true
log:
exceptionRate: 100
service:
vgroup-mapping:
my_test_tx_group: default
grouplist:
default: 127.0.0.1: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-client-batch-send-request: true
config:
type: file
consul:
server-addr: 127.0.0.1:8500
apollo:
apollo-meta: http://192.168.1.204:8801
app-id: seata-server
namespace: application
etcd3:
server-addr: http://localhost:2379
nacos:
namespace:
serverAddr: 127.0.0.1:8848
group: SEATA_GROUP
userName: ""
password: ""
zk:
server-addr: 127.0.0.1:2181
session-timeout: 6000
connect-timeout: 2000
username: ""
password: ""
registry:
type: file
consul:
server-addr: 127.0.0.1:8500
etcd3:
serverAddr: http://localhost:2379
eureka:
weight: 1
service-url: http://localhost:8761/eureka
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
namespace:
userName: ""
password: ""
redis:
server-addr: localhost:6379
db: 0
password:
timeout: 0
sofa:
server-addr: 127.0.0.1:9603
region: DEFAULT_ZONE
datacenter: DefaultDataCenter
group: SEATA_GROUP
addressWaitTime: 3000
application: default
zk:
server-addr: 127.0.0.1:2181
session-timeout: 6000
connect-timeout: 2000
username: ""
password: ""
码代码
Controller
/**
* 测试用controller
*/
@RestController
public class HomeController {
@Autowired
private GlobalService globalService;
/**
* 分布式事务开始
* @param request
* @param a
* @param s
* @return
*/
@GetMapping("atstart")
public JSONObject ATStart(HttpServletRequest request ,Integer a ,String s) {
JSONObject result = new JSONObject();
globalService.procFirst(a,s);
result.put("status", "success");
return result;
}
}
Service
public interface GlobalService {
void procFirst(Integer a, String s);
void procSecond(Integer a, String s);
}
ServiceImpl
@Slf4j
@Service
public class GlobalServiceImpl implements GlobalService {
@Autowired
private AccountRepository accountRepository;
/**
* 应用seata分布式注解
* 事务的第一个动作
* @param a
* @param s
*/
@GlobalTransactional(name = "AT-First")
@Override
public void procFirst(Integer a, String s) {
RestTemplate template = new RestTemplate();
Account newAccount = new Account();
newAccount.setAname(""+a);
newAccount.setPassword(s);
accountRepository.save(newAccount);
try {
// 延迟5秒再去执行事务的下个工作,方便看到数据的变化
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
JSONObject jsonObject = template.getForObject("http://localhost:8083/ResourceServer/processContinue?a=" + a + "&s=" + s , JSONObject.class);
if ( jsonObject.size()>0 && jsonObject.getBoolean("status")) {
log.info("success");
log.info(jsonObject.toJSONString());
}else if(jsonObject != null){
log.info(jsonObject.toJSONString());
}
}
/**
* 事务的第二个动作
* 在第二个服务里实现
* @param a
* @param s
*/
@Override
public void procSecond(Integer a, String s) {
}
}
Bill服务
gradle依赖项与第一个项目一致
配置方面与第一个服务不同的地方有数据库连接和web服务端口号。
spring:
jpa:
database: mysql
show-sql: true
hibernate:
ddl-auto: update
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
datasource:
url: jdbc:mysql://127.0.0.1:3306/demodb
# jdbcUrl: jdbc:mysql://192.168.84.135:3306/demodb
password: 123456
username: root
driver-class-name: com.mysql.jdbc.Driver
server:
port: 8083
servlet:
context-path: /ResourceServer
码代码
Controller
/**
* 测试用controller
*/
@RestController
public class HomeController {
@Autowired
private GlobalService globalService;
/**
* 分布式事务第二个动作的入口
* @param request
* @param a
* @param s
* @return
*/
@GetMapping("processContinue")
public JSONObject processContinue(HttpServletRequest request,Integer a ,String s) {
JSONObject result = new JSONObject();
globalService.procSecond(a,s);
result.put("status", true);
return result;
}
}
Service
第二个服务的Service类与第一个项目一样
ServiceImpl
@Service
public class GlobalServiceImpl implements GlobalService {
@Autowired
private BillRepository billRepository;
/**
* 实现代码省略
* @param a
* @param s
*/
@GlobalTransactional(name = "AT-First")
@Override
public void procFirst(Integer a, String s) {
}
/**
* 分布式事务的第二个动作
* @param a
* @param s
*/
@GlobalTransactional(name = "AT-First")
@Override
public void procSecond(Integer a, String s) {
if (s.equals("break")) {
throw new RuntimeException("ready to rollback");
}
Bill newBill = new Bill();
newBill.setAmount(a);
newBill.setBillNo(s);
newBill.setNote(new Date().toLocaleString());
this.billRepository.save(newBill);
}
}
启动
在每个Service的数据库里建undo_log表(undo_log表是AT模式必需的)。
启动seata-server
Usage: sh seata-server.sh(for linux and mac) or cmd seata-server.bat(for windows) [options]
Options:
--host, -h
The host to bind.
Default: 0.0.0.0
--port, -p
The port to listen.
Default: 8091
--storeMode, -m
log store mode : file、db
Default: file
--help
e.g.
sh seata-server.sh -p 8091 -h 127.0.0.1 -m file
启动Account和Bill项目
查看项目和seata-server日志
Account服务-启动日志
在这里插入代码片
Global事务客户端项目启动简述
- Seata spring-boot自动配置
初始化Global事务客户端
这里可以看到我们设置的异步事务提交的上限配置为1000
RM初始化完成、Global事务客户端初始化完成。
AT代理目标为数据源
我们应用了@GlobalTransactional
注解的Service方法已经被seata拦截到了
TM也注册成功了
一般看到以上这些日志说明Global事务客户端启动正常
seata-server日志
2020-05-30 22:45:42.941 INFO [ServerHandlerThread_1_500]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegRmMessage:127 -RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://127.0.0.1:3306/demodb', applicationId='applicationName', transactionServiceGroup='my_test_tx_group'},channel:[id: 0x47e5634f, L:/127.0.0.1:8091 - R:/127.0.0.1:56683]
2020-05-30 22:46:41.449 INFO [NettyServerNIOWorker_1_16]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegTmMessage:153 -TM register success,message:RegisterTMRequest{applicationId='applicationName', transactionServiceGroup='my_test_tx_group'},channel:[id: 0x719e69f9, L:/127.0.0.1:8091 - R:/127.0.0.1:56700]
2020-05-30 22:48:14.114 INFO [ServerHandlerThread_1_500]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegRmMessage:127 -RM register success,message:RegisterRMRequest{resourceIds='jdbc:mysql://127.0.0.1:3306/exampledb', applicationId='applicationName', transactionServiceGroup='my_test_tx_group'},channel:[id: 0xaaadd3ae, L:/127.0.0.1:8091 - R:/127.0.0.1:56732]
2020-05-30 22:49:12.612 INFO [NettyServerNIOWorker_1_16]io.seata.core.rpc.DefaultServerMessageListenerImpl.onRegTmMessage:153 -TM register success,message:RegisterTMRequest{applicationId='applicationName', transactionServiceGroup='my_test_tx_group'},channel:[id: 0x32c93b17, L:/127.0.0.1:8091 - R:/127.0.0.1:56746]
执行事务测试
我们用3次http请求执行3个测试,其中两次为成功的事务,一个为失败的事务。
- success http://localhost:8081/ResourceServer/atstart?a=1&s=hello
- faill and rollback http://localhost:8081/ResourceServer/atstart?a=2&s=break
- success http://localhost:8081/ResourceServer/atstart?a=3&s=world
第一次请求完成,这里没啥好看的。
第二次请求,account写入一条记录(break)
bill遇到break,抛异常,事务回滚。
account还剩一条记录。
执行第三次请求,成功。
最后数据都是完整的,是不是很方便呢。
日志输出
一个有两个动作的分布式事务成功执行的seata日志
2020-05-30 22:11:28.107 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage timeout=60000,transactionName=AT-First
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:11:28.107 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCoordinator.doGlobalBegin:159 -Begin new global transaction applicationId: applicationName,transactionServiceGroup: my_test_tx_group, transactionName: AT-First,timeout:60000,xid:192.168.137.1:8091:2013030423
2020-05-30 22:11:28.117 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage xid=192.168.137.1:8091:2013030423,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/exampledb,lockKey=account:402809817265e788017265ec5af10004
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:11:28.117 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.AbstractCore.lambda$branchRegister$0:87 -Register branch successfully, xid = 192.168.137.1:8091:2013030423, branchId = 2013030424, resourceId = jdbc:mysql://127.0.0.1:3306/exampledb ,lockKeys = account:402809817265e788017265ec5af10004
2020-05-30 22:11:33.132 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage timeout=60000,transactionName=AT-First
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:11:33.132 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCoordinator.doGlobalBegin:159 -Begin new global transaction applicationId: applicationName,transactionServiceGroup: my_test_tx_group, transactionName: AT-First,timeout:60000,xid:192.168.137.1:8091:2013030425
2020-05-30 22:11:33.140 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage xid=192.168.137.1:8091:2013030425,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/demodb,lockKey=bill:break3
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:11:33.141 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.AbstractCore.lambda$branchRegister$0:87 -Register branch successfully, xid = 192.168.137.1:8091:2013030425, branchId = 2013030426, resourceId = jdbc:mysql://127.0.0.1:3306/demodb ,lockKeys = bill:break3
2020-05-30 22:11:33.147 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage xid=192.168.137.1:8091:2013030425,extraData=null
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:11:33.154 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage xid=192.168.137.1:8091:2013030423,extraData=null
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:11:33.852 INFO [AsyncCommitting_1]io.seata.server.coordinator.DefaultCore.doGlobalCommit:240 -Committing global transaction is successfully done, xid = 192.168.137.1:8091:2013030425.
2020-05-30 22:11:33.861 INFO [AsyncCommitting_1]io.seata.server.coordinator.DefaultCore.doGlobalCommit:240 -Committing global transaction is successfully done, xid = 192.168.137.1:8091:2013030423.
Committing global transaction is successfully done
是在整个事务完成之后输出的,有几个事务动作就是几条输出。
下面是事务中第2个动作失败然后整个事务回滚的输出日志
2020-05-30 22:17:12.913 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage timeout=60000,transactionName=AT-First
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:17:12.913 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCoordinator.doGlobalBegin:159 -Begin new global transaction applicationId: applicationName,transactionServiceGroup: my_test_tx_group, transactionName: AT-First,timeout:60000,xid:192.168.137.1:8091:2013030430
2020-05-30 22:17:12.921 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage xid=192.168.137.1:8091:2013030430,branchType=AT,resourceId=jdbc:mysql://127.0.0.1:3306/exampledb,lockKey=account:402809817265e788017265f19dd60006
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:17:12.921 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.AbstractCore.lambda$branchRegister$0:87 -Register branch successfully, xid = 192.168.137.1:8091:2013030430, branchId = 2013030431, resourceId = jdbc:mysql://127.0.0.1:3306/exampledb ,lockKeys = account:402809817265e788017265f19dd60006
2020-05-30 22:17:17.932 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage timeout=60000,transactionName=AT-First
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:17:17.932 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCoordinator.doGlobalBegin:159 -Begin new global transaction applicationId: applicationName,transactionServiceGroup: my_test_tx_group, transactionName: AT-First,timeout:60000,xid:192.168.137.1:8091:2013030432
2020-05-30 22:17:17.937 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCore.doGlobalRollback:334 -Rollback global transaction successfully, xid = 192.168.137.1:8091:2013030432.
2020-05-30 22:17:17.938 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage xid=192.168.137.1:8091:2013030432,extraData=null
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:17:17.945 INFO [batchLoggerPrint_1]io.seata.core.rpc.DefaultServerMessageListenerImpl.run:214 -SeataMergeMessage xid=192.168.137.1:8091:2013030430,extraData=null
,clientIp:127.0.0.1,vgroup:my_test_tx_group
2020-05-30 22:17:17.955 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCore.doGlobalRollback:290 -Rollback branch transaction successfully, xid = 192.168.137.1:8091:2013030430 branchId = 2013030431
2020-05-30 22:17:17.956 INFO [ServerHandlerThread_1_500]io.seata.server.coordinator.DefaultCore.doGlobalRollback:334 -Rollback global transaction successfully, xid = 192.168.137.1:8091:2013030430.
-Rollback global transaction successfully
相对于事务成功的输出日志,某个动作异常导致事务回滚的日志也是两个(跟事务的动作数量一致)。