一 saga模式
1.1 saga模式实现原理
Saga模式是SEATA提供的长事务解决方案,seata-saga模式是通过状态机来实现的,它使用状态图定义服务调用流程并生成json状态语言定义文件,状态图的节点可以是一个服务,也可以是补偿节点。这个生成的json由状态机引擎来驱动执行,出现异常是状态机引擎对调用成功的服务从后往前补偿,而补偿的逻辑需要由服务自己来实现。
seata中的saga模式适用于长流程或者长事务的场景。而saga模式复杂的地方在于引入了状态机,需要定义状态机的流程,把定义好的流程用json文件引入工程中。同时saga模式需要开发者自己定义回滚事件,如果回滚失败,对整个事务的控制就非常复杂了。
在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。
分布式事务执行过程中,依次执行各参与者的正向操作,如果所有正向操作均执行成功,那么分布式事务提交。如果任何一个正向操作执行失败,那么分布式事务会去退回去执行前面各参与者的逆向回滚操作,回滚已提交的参与者,使分布式事务回到初始状态。
Saga也分为两个阶段:1阶段:直接提交本地事务;2阶段:成功则什么都不做;失败则通过编写补偿业务来回滚.。
代码地址:https://gitee.com/jurf-liu/springcloud-eureka-seata-saga.git
二 案例实操
2.1 案例总结
seata中的saga模式适用于长流程或者长事务的场景。而saga模式复杂的地方在于引入了状态机,需要定义状态机的流程,把定义好的流程用json文件引入工程中。同时saga模式需要开发者自己定义回滚事件,如果回滚失败,对整个事务的控制就非常复杂了。
2.2 案例结构和需求
模拟电商网站购买一件商品,首先会从订单服务下单,然后订单服务会调用账户服务扣减商品金额,如果成功,再调用库存服务扣减库存。如果其中某一步失败,则从后往前依次补偿,这个补偿事件由状态机触发。
springcloud+eureka整合阿里seata-saga模式,代码地址:
https://gitee.com/jurf-liu/springcloud-eureka-seata-saga.git
2.3 定义状态机
seata提供了下面地址可以绘制这个图,同时生成对应的json代码。本文的json代码是参考官方示例手工改写的,http://seata.io/saga_designer/index.html#/
对应json文件内容:
{
"Name": "buyGoodsOnline",
"Comment": "buy a goods on line, add order, deduct account, deduct storage ",
"StartState": "SaveOrder",
"Version": "0.0.1",
"States": {
"SaveOrder": {
"Type": "ServiceTask",
"ServiceName": "orderSave",
"ServiceMethod": "saveOrder",
"CompensateState": "DeleteOrder",
"Next": "ChoiceAccountState",
"Input": [
"$.[businessKey]",
"$.[order]"
],
"Output": {
"SaveOrderResult": "$.#root"
},
"Status": {
"#root == true": "SU",
"#root == false": "FA",
"$Exception{java.lang.Throwable}": "UN"
}
},
"ChoiceAccountState":{
"Type": "Choice",
"Choices":[
{
"Expression":"[SaveOrderResult] == true",
"Next":"ReduceAccount"
}
],
"Default":"Fail"
},
"ReduceAccount": {
"Type": "ServiceTask",
"ServiceName": "accountService",
"ServiceMethod": "decrease",
"CompensateState": "CompensateReduceAccount",
"Next": "ChoiceStorageState",
"Input": [
"$.[businessKey]",
"$.[userId]",
"$.[money]",
{
"throwException" : "$.[mockReduceAccountFail]"
}
],
"Output": {
"ReduceAccountResult": "$.#root"
},
"Status": {
"#root == true": "SU",
"#root == false": "FA",
"$Exception{java.lang.Throwable}": "UN"
},
"Catch": [
{
"Exceptions": [
"java.lang.Throwable"
],
"Next": "CompensationTrigger"
}
]
},
"ChoiceStorageState":{
"Type": "Choice",
"Choices":[
{
"Expression":"[ReduceAccountResult] == true",
"Next":"ReduceStorage"
}
],
"Default":"Fail"
},
"ReduceStorage": {
"Type": "ServiceTask",
"ServiceName": "storageService",
"ServiceMethod": "decrease",
"CompensateState": "CompensateReduceStorage",
"Input": [
"$.[businessKey]",
"$.[productId]",
"$.[count]",
{
"throwException" : "$.[mockReduceStorageFail]"
}
],
"Output": {
"ReduceStorageResult": "$.#root"
},
"Status": {
"#root == true": "SU",
"#root == false": "FA",
"$Exception{java.lang.Throwable}": "UN"
},
"Catch": [
{
"Exceptions": [
"java.lang.Throwable"
],
"Next": "CompensationTrigger"
}
],
"Next": "Succeed"
},
"DeleteOrder": {
"Type": "ServiceTask",
"ServiceName": "orderSave",
"ServiceMethod": "deleteOrder",
"Input": [
"$.[businessKey]",
"$.[order]"
]
},
"CompensateReduceAccount": {
"Type": "ServiceTask",
"ServiceName": "accountService",
"ServiceMethod": "compensateDecrease",
"Input": [
"$.[businessKey]",
"$.[userId]",
"$.[money]"
]
},
"CompensateReduceStorage": {
"Type": "ServiceTask",
"ServiceName": "storageService",
"ServiceMethod": "compensateDecrease",
"Input": [
"$.[businessKey]",
"$.[productId]",
"$.[count]"
]
},
"CompensationTrigger": {
"Type": "CompensationTrigger",
"Next": "Fail"
},
"Succeed": {
"Type":"Succeed"
},
"Fail": {
"Type":"Fail",
"ErrorCode": "PURCHASE_FAILED",
"Message": "purchase failed"
}
}
}
2.4 数据库附加
1.订单
数据结构
#########################seata_order库
use database seata_order;
CREATE TABLE `orders` (
`id` mediumint(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) DEFAULT NULL,
`product_id` int(11) DEFAULT NULL,
`COUNT` int(11) DEFAULT NULL COMMENT '数量',
`pay_amount` decimal(10,2) DEFAULT NULL,
`status` varchar(100) DEFAULT NULL,
`add_time` datetime DEFAULT CURRENT_TIMESTAMP,
`last_update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8
2.附加状态机的3张表
create table seata_state_machine_def
(
id varchar(32) not null comment 'id',
name varchar(128) not null comment 'name',
tenant_id varchar(32) not null comment 'tenant id',
app_name varchar(32) not null comment 'application name',
type varchar(20) comment 'state language type',
comment_ varchar(255) comment 'comment',
ver varchar(16) not null comment 'version',
gmt_create timestamp(3) not null comment 'create time',
status varchar(2) not null comment 'status(AC:active|IN:inactive)',
content longtext comment 'content',
recover_strategy varchar(16) comment 'transaction recover strategy(compensate|retry)',
primary key (id)
);
CREATE TABLE seata_state_machine_inst
(
id VARCHAR(128) NOT NULL COMMENT 'id',
machine_id VARCHAR(32) NOT NULL COMMENT 'state machine definition id',
tenant_id VARCHAR(32) NOT NULL COMMENT 'tenant id',
parent_id VARCHAR(128) COMMENT 'parent id',
gmt_started TIMESTAMP(3) NOT NULL COMMENT 'start time',
business_key VARCHAR(48) COMMENT 'business key',
start_params LONGTEXT COMMENT 'start parameters',
gmt_end TIMESTAMP(3) COMMENT 'end time',
excep BLOB COMMENT 'exception',
end_params LONGTEXT COMMENT 'end parameters',
STATUS VARCHAR(2) COMMENT 'status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
compensation_status VARCHAR(2) COMMENT 'compensation status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
is_running TINYINT(1) COMMENT 'is running(0 no|1 yes)',
gmt_updated TIMESTAMP(3) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unikey_buz_tenant (business_key, tenant_id)
);
CREATE TABLE seata_state_inst
(
id VARCHAR(48) NOT NULL COMMENT 'id',
machine_inst_id VARCHAR(128) NOT NULL COMMENT 'state machine instance id',
NAME VARCHAR(128) NOT NULL COMMENT 'state name',
TYPE VARCHAR(20) COMMENT 'state type',
service_name VARCHAR(128) COMMENT 'service name',
service_method VARCHAR(128) COMMENT 'method name',
service_type VARCHAR(16) COMMENT 'service type',
business_key VARCHAR(48) COMMENT 'business key',
state_id_compensated_for VARCHAR(50) COMMENT 'state compensated for',
state_id_retried_for VARCHAR(50) COMMENT 'state retried for',
gmt_started TIMESTAMP(3) NOT NULL COMMENT 'start time',
is_for_update TINYINT(1) COMMENT 'is service for update',
input_params LONGTEXT COMMENT 'input parameters',
output_params LONGTEXT COMMENT 'output parameters',
STATUS VARCHAR(2) NOT NULL COMMENT 'status(SU succeed|FA failed|UN unknown|SK skipped|RU running)',
excep BLOB COMMENT 'exception',
gmt_end TIMESTAMP(3) COMMENT 'end time',
PRIMARY KEY (id, machine_inst_id)
);
2.库存表
#########################seata_storage库
use database seata_storage;
CREATE TABLE `storage` (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存',
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `seata_storage`.`storage` (`id`, `product_id`, `total`, `used`, `residue`) VALUES ('1', '1', '100', '0', '100');
3.账户表
#########################seata_pay库
use database seata_pay;
DROP TABLE account;
CREATE TABLE `account` (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额',
`balance` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度',
`last_update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `seata_pay`.`account` (`id`, `user_id`, `total`, `used`, `balance`) VALUES ('1', '1', '1000', '0', '100');
2.5 eureka注册中心配置
这里的eureka的端口为:8889
2.6 seata的配置
1.registry配置文件:这里eureka的端口为8889
2.7 修改工程配置
1.order :这里eureka的端口为8889
2.account
这里eureka的端口为8889
3.storage
这里eureka的端口为8889
2.8 启动相应服务
1.启动eureka,
2.启动seata服务
3.启动相应order,account,storage服务
1.order
2.account
3.storage
2.9 测试
2.9.1 初始态
查看几张表的状态
1.order表
2.seata_state_inst表
3. seata_state_machine_def表
4.seata_state_machine_inst表
5.account表
6.storage表
2.9.2 请求访问
1.查看order表
2.account表
3.storage表
4.seata_state_inst表
5. 5.account表
6.seata_state_machine_inst表
2.9.3 触发事务访问
1.在库存服务的decrease方法改成如下
2.再次请求
这时发送购买商品请求后会抛出异常,然后3个服务的事务依次做交易补偿,所有表数据没有变。
1.查看order表
2.account表
3.storage表