Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata的AT模式基于支持ACID事务的关系型数据库,是2PC(两阶段提交协议)的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
- 提交异步化,非常快速地完成。
- 回滚通过一阶段的回滚日志进行反向补偿。
写隔离
一阶段本地事务提交前,需要确保先拿到 全局锁 。拿不到 全局锁 ,不能提交本地事务。拿 全局锁 的尝试被限制在一定范围内,超出范围将放弃,并回滚本地事务,释放本地锁。
读隔离
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
使用示例
由于Seata在Github提供的Demo包含了多个Moudle,接下来对其中的Seata-JPA模块抽取,方便以后的学习和使用。
Github的代码地址:https://github.com/zuotaorui/seata-jpa
启动Seata-server
Seata-Server在Seata Framework中担任Transaction Coordinator(TC 事务协调者)的角色,负责维护全局和分支事务的状态,驱动全局事务提交或回滚。
采用docker命令启动Seata-Server,并将Seata-Server的配置文件挂载到宿主机
docker run --name seata-server -p 8091:8091 -e SEATA_CONFIG_NAME=file:/root/seata-config/registry -v /Users/albert/Docker/seata:/root/seata-config fancyfong/seata:1.4.1_arm64
配置文件registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "consul"
#注册中心采用Consul 实现Seata-Server的高可用
consul {
cluster = "seata-server"
serverAddr = "192.168.0.107:8500"
aclToken = ""
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "file"
# 配置中心
file {
name = "file:/root/seata-config/file.conf"
}
}
配置文件file.conf
## transaction log store, only used in seata-server
store {
## store mode: file、db、redis
mode = "db"
## rsa decryption public key
publicKey = ""
## database store property
db {
## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc.
datasource = "hikari"
## mysql/oracle/postgresql/h2/oceanbase etc.
dbType = "mysql"
driverClassName = "com.mysql.jdbc.Driver"
## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
url = "jdbc:mysql://192.168.0.107:3306/seata?rewriteBatchedStatements=true&autoReconnect=true"
user = "root"
password = "123456"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
}
示例项目
示例项目采用Spring Boot ,包含Business-Service 、Order-Service、Account-Service、Stock-Service,用来模拟电商系统的下单流程,保证订单创建、账户余额扣减、库存更新等操作的原子性。
引入Seata依赖
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<version>2.2.1.RELEASE</version>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
添加Seata配置
示例项目只使用了简单的配置,具体的配置参数可查看官方文档:docs/user/configurations.html
spring.application.name=order-service
#数据库访问采用spring-data-jpa
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/db_order?useSSL=false&useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.hikari.maximum-pool-size=50
spring.datasource.hikari.minimum-idle=20
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.show-sql=true
logging.level.org.hibernate.type.descriptor.sql.BasicBinder=trace
#seata事务分组
#1.首先应用程序(客户端)中配置了事务分组(GlobalTransactionScanner构造方法的txServiceGroup参数)
#2.应用程序(客户端)会通过用户配置的配置中心去寻找service.vgroupMapping.[事务分组配置项],
# 取得配置项的值就是TC集群的名称
#3.拿到集群名称程序通过一定的前后缀+集群名称去构造服务名,各配置中心的服务名实现不同(前提是Seata-Server
# 已经完成服务注册,且Seata-Server向注册中心报告cluster名与应用程序(客户端)配置的集群名称一致)
#4.拿到服务名去相应的注册中心去拉取相应服务名的服务列表,获得后端真实的TC服务列表(即Seata-Server集群节点列表)
seata.tx-service-group=my_test_tx_group
#尝试本地采用consul配置中心后,由于采用docker部署,导致获取到的Seata-Server的IP地址不正确
#目前采用的注册类型为file,未实现高可用
seata.registry.type=file
seata.service.vgroup-mapping.my_test_tx_group=default
seata.service.grouplist.default=127.0.0.1:8091
代理DataSource
Seata 通过代理数据源实现分支事务,如果没有注入,事务无法成功回滚
@Configuration
public class DataSourceConfig {
protected static <T> T createDataSource(DataSourceProperties properties,
Class<? extends DataSource> type) {
return (T) properties.initializeDataSourceBuilder().type(type).build();
}
/**
** SpringBoot 2.0以后,默认的数据库连接池Hikari
**/
@Bean
@ConfigurationProperties(prefix = "spring.datasource.hikari")
public HikariDataSource dataSource(DataSourceProperties properties) {
HikariDataSource dataSource = createDataSource(properties,
HikariDataSource.class);
if (StringUtils.hasText(properties.getName())) {
dataSource.setPoolName(properties.getName());
}
return dataSource;
}
/**
** 使用Seata代理DataSource,使用@Primary赋予bean更高的优先级
**/
@Primary
@Bean("dataSource")
public DataSource dataSource(HikariDataSource hikariDataSource) {
return new DataSourceProxy(hikariDataSource);
}
}
开启分布式事务
分布式事务的发起者在Seata中担任Transaction Manager ( TM 事务管理器) 的角色,定义全局事务的范围,开始全局事务、提交或回滚全局事务,只需在方法上面加上注解@GlobalTransactional。
@Service
public class BusinessService {
@Autowired
private StockFeignClient stockFeignClient;
@Autowired
private OrderFeignClient orderFeignClient;
/**
* 减库存,下订单
*
* @param userId
* @param commodityCode
* @param orderCount
*/
@GlobalTransactional
public void purchase(String userId, String commodityCode, int orderCount) {
stockFeignClient.deduct(commodityCode, orderCount);
orderFeignClient.create(userId, commodityCode, orderCount);
}
}
其他分布式事务的参与者在Seata中担任Resource Manager ( RM 资源管理器) 的角色,管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚,只需在方法上面加上注解@Transactional。
效果显示
调用business-service的 localhost:9000/purchase/rollback 的API之后,触发在Account-Service中模拟的异常。
@Transactional(rollbackFor = Exception.class)
public void debit(String userId, BigDecimal num) {
Account account = accountDAO.findByUserId(userId);
account.setMoney(account.getMoney().subtract(num));
accountDAO.save(account);
if (ERROR_USER_ID.equals(userId)) {
throw new RuntimeException("account branch exception");
}
}
在account-service中可以看到库存更新操作的回滚(分布式事务实现2PC阶段的第二阶段 )
示例项目的代码已上传至GitHub,有兴趣的同学可以下载下来自己验证。