随着项目体量越来越大,分布式系统越来越收到大家的欢迎,可分布式系统也会带来两大问题,一是分布式锁,二是分布式事务。分布式锁可以使用redis解决,可分布式事务没有这么简单。Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。接下来本文以AT事务模式为例,详细介绍Seata。
两阶段提交协议的演变:
一阶段:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。
二阶段:
以一个示例来说明:
两个全局事务 tx1 和 tx2,分别对 a 表的 m 字段进行更新操作,m 的初始值 1000。
tx1 先开始,开启本地事务,拿到本地锁,更新操作 m = 1000 - 100 = 900。本地事务提交前,先拿到该记录的 全局锁 ,本地提交释放本地锁。 tx2 后开始,开启本地事务,拿到本地锁,更新操作 m = 900 - 100 = 800。本地事务提交前,尝试拿该记录的 全局锁 ,tx1 全局提交前,该记录的全局锁被 tx1 持有,tx2 需要重试等待 全局锁 。
tx1 二阶段全局提交,释放 全局锁 。tx2 拿到 全局锁 提交本地事务。
如果 tx1 的二阶段全局回滚,则 tx1 需要重新获取该数据的本地锁,进行反向补偿的更新操作,实现分支的回滚。
此时,如果 tx2 仍在等待该数据的 全局锁,同时持有本地锁,则 tx1 的分支回滚会失败。分支的回滚会一直重试,直到 tx2 的 全局锁 等锁超时,放弃 全局锁 并回滚本地事务释放本地锁,tx1 的分支回滚最终成功。
因为整个过程 全局锁 在 tx1 结束前一直是被 tx1 持有的,所以不会发生 脏写 的问题。
在数据库本地事务隔离级别 读已提交(Read Committed) 或以上的基础上,Seata(AT 模式)的默认全局隔离级别是 读未提交(Read Uncommitted) 。
如果应用在特定场景下,必需要求全局的 读已提交 ,目前 Seata 的方式是通过 SELECT FOR UPDATE 语句的代理。
SELECT FOR UPDATE 语句的执行会申请 全局锁 ,如果 全局锁 被其他事务持有,则释放本地锁(回滚 SELECT FOR UPDATE 语句的本地执行)并重试。这个过程中,查询是被 block 住的,直到 全局锁 拿到,即读取的相关数据是 已提交 的,才返回。
出于总体性能上的考虑,Seata 目前的方案并没有对所有 SELECT 语句都进行代理,仅针对 FOR UPDATE 的 SELECT 语句。
以一个示例来说明整个 AT 分支的工作过程。
业务表:product
Field | Type | Key |
---|---|---|
id | bigint(20) | PRI |
name | varchar(100) | |
since | varchar(100) |
AT 分支事务的业务逻辑:
update product set name = 'GTS' where name = 'TXC';
过程:
select id, name, since from product where name = 'TXC';
得到前镜像:
id | name | since |
---|---|---|
1 | TXC | 2014 |
select id, name, since from product where id = 1;
得到后镜像:
id | name | since |
---|---|---|
1 | GTS | 2014 |
{
"branchId": 641789253,
"undoItems": [{
"afterImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "GTS"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"beforeImage": {
"rows": [{
"fields": [{
"name": "id",
"type": 4,
"value": 1
}, {
"name": "name",
"type": 12,
"value": "TXC"
}, {
"name": "since",
"type": 12,
"value": "2014"
}]
}],
"tableName": "product"
},
"sqlType": "UPDATE"
}],
"xid": "xid:xxx"
}
update product set name = 'TXC' where id = 1;
创建事务组
加入事务组
通知事务组
优点
缺点
详细介绍见上文
优点
缺点
seata和lcn比较,有什么不一致?
优点
缺点
SpringCloud
Consul
Pgsql
Seata
从seata.io官网下载最新版seata-1.4.2压缩包并解压
修改registry.conf注册中心配置
consul {
cluster = "seata-server" //注册中心注册的服务名
serverAddr = "127.0.0.1:8500"
aclToken = ""
}
修改file.conf配置
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 = "postgresql"
driverClassName = "org.postgresql.Driver"
## if using mysql to store the data, recommend add rewriteBatchedStatements=true in jdbc connection param
url = "jdbc:postgresql://127.0.0.1:5433/seata"
user = "XXX"
password = "XXX"
minConn = 5
maxConn = 100
globalTable = "global_table"
branchTable = "branch_table"
lockTable = "lock_table"
queryLimit = 100
maxWait = 5000
}
执行SQL,创建seata-server所需要的表,SQL脚本可在官方GitHub中找到
启动服务
创建undo_log表,用于记录事务和回滚日志
CREATE TABLE IF NOT EXISTS public.undo_log
(
id SERIAL NOT NULL,
branch_id BIGINT NOT NULL,
xid VARCHAR(128) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info BYTEA NOT NULL,
log_status INT NOT NULL,
log_created TIMESTAMP(0) NOT NULL,
log_modified TIMESTAMP(0) NOT NULL,
CONSTRAINT pk_undo_log PRIMARY KEY (id),
CONSTRAINT ux_undo_log UNIQUE (xid, branch_id)
);
CREATE SEQUENCE IF NOT EXISTS undo_log_id_seq INCREMENT BY 1 MINVALUE 1 ;
seata对数据源做了代理的和接管,在每个参与到分布式事务的服务中,都需要做如下配置
@Configuration
public class DataSourceConfiguration {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource druidDataSource(){
DruidDataSource druidDataSource = new DruidDataSource();
return druidDataSource;
}
@Primary
@Bean("dataSource")
public DataSourceProxy dataSource(DataSource druidDataSource){
return new DataSourceProxy(druidDataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactory(DataSourceProxy dataSourceProxy)throws Exception{
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver()
.getResources("classpath*:/mapper/*.xml"));
sqlSessionFactoryBean.setTransactionFactory(new SpringManagedTransactionFactory());
return sqlSessionFactoryBean.getObject();
}
}
在项目resources中加入两个配置文件 file.conf和registry.conf
file.conf修改
service {
#transaction service group mapping
vgroupMapping.my_test_tx_group = "seata-server" //这个 seata-server 是seata服务端在注册中心的名称,my_test_tx_group 是自己自定义的,需和服务application.yml配置一致
#only support when registry.type=file, please don't set multiple addresses
default.grouplist = "127.0.0.1:8091" //这个是seata的服务地址
#degrade, current not support
enableDegrade = false
#disable seata
disableGlobalTransaction = false
}
registry.conf更改consul注册中心
consul {
cluster = "seata-server" //注册中心注册的服务名
serverAddr = "127.0.0.1:8500"
aclToken = ""
}
在发起全局事务的上游服务中加入@GlobalTransactional注解,即可开启全局事务
接下来,我们启动服务测试
Client 配置完成后启动应用并稍待片刻,出现以下后日志就表示 Seata 服务注册成功
register TM success. client version:1.4.2, server version:1.4.2,channel:[id: 0xa4675e28, L:/127.0.0.1:8238 - R:/127.0.0.1:8091]
register RM success. client version:1.4.2, server version:1.4.2,channel:[id: 0x408192d3, L:/127.0.0.1:8237 - R:/127.0.0.1:8091]
register success, cost 94 ms, version:1.4.2,role:RMROLE,channel:[id: 0x408192d3, L:/127.0.0.1:8237 - R:/127.0.0.1:8091]
register success, cost 94 ms, version:1.4.2,role:TMROLE,channel:[id: 0xa4675e28, L:/127.0.0.1:8238 - R:/127.0.0.1:8091]
我准备了order,account,storage三个服务演示seata
order服务
@Override
@GlobalTransactional(name = "create-order",rollbackFor = Exception.class)
public void create(Order order) {
String xid = RootContext.getXID();
log.info("------->交易开始");
orderDao.create(order);
//远程方法 扣除库存
storageApi.decrease(order.getProductId(), order.getCount());
//远程方法,扣除账户余额
accountApi.decrease(order.getUserId(), order.getMoney());
log.info("全局事务 xid:{}", xid);
log.info("操作结束---->end");
}
storage服务
@Override
public void decrease(Long productId, Integer count) {
String xid = RootContext.getXID();
log.info("全局事务 xid: {}", xid);
log.info("-------->扣减库存开始");
storageDao.decrease(productId, count);
log.info("-------->扣减库存结束");
}
account服务
@Override
public void decrease(Long userId, BigDecimal money) throws Exception {
String xid = RootContext.getXID();
log.info("全局事务 xid:{}", xid);
log.info("---->扣除账户开始account中");
Account account = accountDao.getByUserId(userId);
if (account.getResidue().compareTo(money) < 0) {
// 如果余额不足,则抛出异常,进行全局事务回滚操作
throw new Exception("账户余额不足");
}
accountDao.decrease(userId, money);
log.info("---->扣除账户结束account中");
//修改订单状态,此时调用会导致调用成环
log.info("修改订单状态开始 account中");
orderApi.update(userId, money, 0);
}
account表初始状态,余额为2
id | user_id | total | used | residue |
---|---|---|---|---|
1 | 1 | 100 | 98 | 2 |
storage表初始状态
id | product_id | total | used | residue |
---|---|---|---|---|
1 | 1 | 100 | 10 | 90 |
调用接口
POST http://localhost:8005/order/create
Content-Type: application/json
{
“userId”: 1,
“productId”: 1,
“count”: 1,
“money”: 10
}
account服务抛出异常
java.lang.Exception: 账户余额不足
at com.beast.account.service.impl.AccountServiceImpl.decrease(AccountServiceImpl.java:39) ~[classes/:na]
at com.beast.account.controller.AccountController.decrease(AccountController.java:26) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_151]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_151]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_151]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_151]
order,storage服务根据undo_log执行回滚
2022-03-22 16:15:35.713 INFO 19404 --- [ch_RMROLE_1_1_8] i.s.c.r.p.c.RmBranchRollbackProcessor : rm handle branch rollback process:xid=10.21.80.24:8091:2522259474975834267,branchId=2522259474975834272,branchType=AT,resourceId=jdbc:postgresql://10.41.173.228:5433/seata-storage,applicationData=null
2022-03-22 16:15:35.715 INFO 19404 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler : Branch Rollbacking: 10.21.80.24:8091:2522259474975834267 2522259474975834272 jdbc:postgresql://10.41.173.228:5433/seata-storage
2022-03-22 16:15:35.846 INFO 19404 --- [ch_RMROLE_1_1_8] i.s.r.d.undo.AbstractUndoLogManager : xid 10.21.80.24:8091:2522259474975834267 branch 2522259474975834272, undo_log deleted with GlobalFinished
2022-03-22 16:15:35.847 INFO 19404 --- [ch_RMROLE_1_1_8] io.seata.rm.AbstractRMHandler : Branch Rollbacked result: PhaseTwo_Rollbacked