阿里巴巴于2019年1月入驻springcloud的孵化器后,也相继开源了自己的微服务事务管理工具Seata,对于中大厂的系统,Seata提供了全局的事务处理方案,Seata可以说是现阶段特别优秀的一款事务处理架构啦。 Seata官网
Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT(案例也是以AT模式驱动,默认的事务模式)、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。有些专业术语去官网查看,这里只帮助大家做一个快速开发案例,以及我们需要了解的原理。
维护全局和分支事务的状态,驱动全局事务提交或回滚(可以简单的理解为Seata服务器,来负责全局的管控,包括事务的提交和回滚,XID为协调识别信息)
定义全局事务的范围:开始全局事务、提交或回滚全局事务。(我们后期在处理业务的时候会在业务方法上标注@GlobalTranscational,它即代表着事务的发起方,就是整个分布式事务的的一个管理者。)
管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。(在微服务架构中,我们会在不同的服务中调用接口,然后每一个服务都在全局事务中充当一个分支事务,分支事务会提交事务的提交和回滚报告。)
流程图讲解:
TM开启分布式事务《TM向TC注册全局事务记录》
RM向TC汇报资源准备状态
TM结束分布式事务,事务一阶段结束《TM通知TC提交/回滚事务》
TC汇总事务信息,决定分布式事务还是提交还是回滚
TC通知所有RM提交/回滚资源,事务二阶段结束
第一阶段:
Seata会拦截 “业务SQL”,解析SQL语义,在执行SQL前将元数据 保存成“before images” 内,然后执行业务数据更新,然后保存成“afterimage”,最后生成行锁,保证操作的acid特性。这样两份镜像就有了可提交,可回滚的控制条件。
第二阶段:提交/回滚
TC通知所有的TM执行提交还是回滚操作。如果是提交:将before image,after image ,行锁删除既可。如果是回滚:Seata就需要回滚到第一阶段已经执行的“业务SQL”,还原业务数据。回滚的方式是用“before image” 还原业务数据;但是还要首先校验脏读,如果元数据一致,可还原,进行反向补偿,否则人工处理。
接下来就是案例,带大家快速走进Seata的世界
官网下载Seata服务器 链接: 下载
这里推荐使用1.0版本或之后的。
service {
#transaction service group mapping
#将测试案例的自定义事务组名改成我们自己的 example_seata
vgroup_mapping.my_test_tx_group = "example_seata"
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091"
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
## transaction log store
store {
## store mode: file、db
#将默认的 存储模式file修改成数据库存储db
mode = "db"
## file store property
file {
## store location dir
dir = "sessionStore"
# branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions
max-branch-session-size = 16384
# globe session size , if exceeded throws exceptions
max-global-session-size = 512
# file buffer size , if exceeded allocate new buffer
file-write-buffer-cache-size = 16384
# when recover batch read size
session.reload.read_size = 100
# async, sync
flush-disk-mode = async
}
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc.
datasource = "dbcp"
## mysql/oracle/h2/oceanbase etc.
db-type = "mysql"
#修改数据库连接信息
driver-class-name = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata"
user = "public"
password = "public"
min-conn = 1
max-conn = 10
global.table = "global_table"
branch.table = "branch_table"
lock-table = "lock_table"
query-limit = 100
}
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = ""
cluster = "default"
}
branch_table,global_table,lock_table创建数据库表的sql,系统有子带,也可以官网下载。
我们就采用官网的,storage,order,account 的处理流程来创建案例,参考图二执行流程。
这里我们创建三个微服务:订单微服务,库存微服务,账户微服务。
业务逻辑:用户下单 ---->订单系统创建订单,远程调用库存服务减少库存,账户扣钱(openFegin客户端调用)---->订单状态修改。
案例操作跨域了三个数据库,两个远程调用,已经明显存在分布式事务的问题。
具体的建表语句自行创建,非常简单的字段数据。
undo_log.table
工程名:Seata_order_8001
pom.xml:
com.alibaba.cloud
spring-cloud-starter-alibaba-nacos-discovery
com.alibaba.cloud
spring-cloud-starter-alibaba-seata
seata-all
io.seata
io.seata
seata-all
0.9.0
org.springframework.cloud
spring-cloud-starter-openfeign
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.0.0
mysql
mysql-connector-java
5.1.37
com.alibaba
druid-spring-boot-starter
1.1.10
org.projectlombok
lombok
true
application.yml:
server:
port: 8001
spring:
application:
name: seata-order-service #服务名
cloud:
alibaba:
seata:
tx-service-group: fsp_tx_group #全局事务组名称
nacos:
discovery:
server-addr: localhost:8848 #服务注册中心
datasource: #数据源配置
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/seata_account
username: public
password: public
feign: #openfegin 打开
hystrix:
enabled: false
logging:
level:
io:
seata: info
mybatis: #mapper扫描地址
mapperLocations: classpath:mapper/*.xml
file.conf ,registry.conf 分别拷贝到resource下,seata加载要使用到。
主启动类:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class) //排除自带数据源
@EnableDiscoveryClient //服务发现
@EnableFeignClients // fegin接口调用
public class SeataOrderMainApp8001
{
public static void main(String[] args)
{
SpringApplication.run(SeataOrderMainApp8001.class, args);
}
}
Storage远程接口:
@FeignClient(value = "seata-storage-service")
public interface StorageService
{
@PostMapping(value = "/storage/decrease")
CommonResult decrease(@RequestParam("productId") Long productId, @RequestParam("count") Integer count);
}
DataSourceProxyConfig
@Configuration
public class DataSourceProxyConfig {
@Value("${mybatis.mapperLocations}")
private String mapperLocations;
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
mybatis 扫描入口
@Configuration
@MapperScan({"com.atguigu.springcloud.alibaba.dao"})
public class MyBatisConfig {
}
业务类:
@Service
@Slf4j
public class OrderServiceImpl implements OrderService
{
@Resource
private OrderDao orderDao;
@Resource
private StorageService storageService;
@Resource
private AccountService accountService;
/**
* 创建订单->调用库存服务扣减库存->调用账户服务扣减账户余额->修改订单状态
* 简单说:下订单->扣库存->减余额->改状态
*/
@Override
@GlobalTransactional(name = "example-create-order",rollbackFor = Exception.class)
public void create(Order order)
{
log.info("----->开始新建订单");
//1 新建订单
orderDao.create(order);
//2 扣减库存
log.info("----->订单微服务开始调用库存,做扣减Count");
storageService.decrease(order.getProductId(),order.getCount());
log.info("----->订单微服务开始调用库存,做扣减end");
//3 扣减账户
log.info("----->订单微服务开始调用账户,做扣减Money");
accountService.decrease(order.getUserId(),order.getMoney());
log.info("----->订单微服务开始调用账户,做扣减end");
//4 修改订单状态,从零到1,1代表已经完成
log.info("----->修改订单状态开始");
orderDao.update(order.getUserId(),0);
log.info("----->修改订单状态结束");
log.info("----->下订单结束了,O(∩_∩)O哈哈~");
}
}
这个注解开启,说明我们将当前的service方案已经递交给了TM进行管理啦,后续的所有接口调用,设计到SQL操作都是一个分支事务RM要处理的。
我们可以在其他系统中故意加入异常,或者调用延迟信息。系统就会触发回滚。如果没有异常那么一次调用正常走完,库信息响应修改。