SAGA模式
Saga 模式是 Seata 提供的长事务解决方案,在 Saga 模式中,业务流程中每个参与者都提交本地事务,当某个参与者出现失败,则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都有业务开发实现。
适用场景
业务流程长、业务流程多。
参与者包含其它公司或者遗留系统服务,无法提供 TCC 模式要求的三个接口。
原理
SEATA提供的Saga模式是基于状态机引擎来实现的,机制是:
- 通过状态图来定义服务调用的流程并生成 json 状态语言定义文件
- 状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
- 状态图 json 由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚
注意: 异常发生时是否进行补偿也可由用户自定义决定
- 可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能
使用
@GetMapping("/test_saga")
public String testSaga() {
Map<String, Object> startParams = new HashMap<>(3);
String businessKey = String.valueOf(System.currentTimeMillis());
startParams.put("businessKey", businessKey);
startParams.put("count", 10);
startParams.put("amount", new BigDecimal("100"));
// 模拟compensate
//startParams.put("mockReduceBalanceFail", "true");
StateMachineInstance inst = stateMachineEngine.startWithBusinessKey("reduceInventoryAndBalance", null,
businessKey, startParams);
Assert.isTrue(ExecutionStatus.SU.equals(inst.getStatus()),
"saga transaction execute failed. XID: " + inst.getId());
System.out.println("saga transaction commit succeed. XID: " + inst.getId());
return "finished";
}
在 resources/statelang 目录下创建一个 reduce_inventory_and_balance.json
文件。
{
"Name": "reduceInventoryAndBalance",
"Comment": "reduce inventory then reduce balance in a transaction",
"StartState": "ReduceInventory",
"Version": "0.0.1",
"States": {
"ReduceInventory": {
"Type": "ServiceTask",
"ServiceName": "inventoryAction",
"ServiceMethod": "reduce",
"CompensateState": "CompensateReduceInventory",
"Next": "ChoiceState",
"Input": [
"$.[businessKey]",
"$.[count]"
],
"Output": {
"reduceInventoryResult": "$.#root"
},
"Status": {
"#root == true": "SU",
"#root == false": "FA",
"$Exception{java.lang.Throwable}": "UN"
}
},
"ChoiceState": {
"Type": "Choice",
"Choices": [
{
"Expression": "[reduceInventoryResult] == true",
"Next": "ReduceBalance"
}
],
"Default": "Fail"
},
"ReduceBalance": {
"Type": "ServiceTask",
"ServiceName": "balanceAction",
"ServiceMethod": "reduce",
"CompensateState": "CompensateReduceBalance",
"Input": [
"$.[businessKey]",
"$.[amount]",
{
"throwException": "$.[mockReduceBalanceFail]"
}
],
"Output": {
"compensateReduceBalanceResult": "$.#root"
},
"Status": {
"#root == true": "SU",
"#root == false": "FA",
"$Exception{java.lang.Throwable}": "UN"
},
"Catch": [
{
"Exceptions": [
"java.lang.Throwable"
],
"Next": "CompensationTrigger"
}
],
"Next": "Succeed"
},
"CompensateReduceInventory": {
"Type": "ServiceTask",
"ServiceName": "inventoryAction",
"ServiceMethod": "compensateReduce",
"Input": [
"$.[businessKey]"
]
},
"CompensateReduceBalance": {
"Type": "ServiceTask",
"ServiceName": "balanceAction",
"ServiceMethod": "compensateReduce",
"Input": [
"$.[businessKey]"
]
},
"CompensationTrigger": {
"Type": "CompensationTrigger",
"Next": "Fail"
},
"Succeed": {
"Type": "Succeed"
},
"Fail": {
"Type": "Fail",
"ErrorCode": "PURCHASE_FAILED",
"Message": "purchase failed"
}
}
}
BalanceAction
public interface BalanceAction {
boolean reduce(String businessKey, BigDecimal amount, Map<String, Object> params);
boolean compensateReduce(String businessKey, Map<String, Object> params);
}
BalanceActionImpl
@Service("balanceAction")
public class BalanceActionImpl implements BalanceAction {
private static final Logger LOGGER = LoggerFactory.getLogger(BalanceActionImpl.class);
@Override
public boolean reduce(String businessKey, BigDecimal amount, Map<String, Object> params) {
if(params != null) {
Object throwException = params.get("throwException");
if (throwException != null && "true".equals(throwException.toString())) {
throw new RuntimeException("reduce balance failed");
}
}
LOGGER.info("reduce balance succeed, amount: " + amount + ", businessKey:" + businessKey);
return true;
}
@Override
public boolean compensateReduce(String businessKey, Map<String, Object> params) {
if(params != null) {
Object throwException = params.get("throwException");
if (throwException != null && "true".equals(throwException.toString())) {
throw new RuntimeException("compensate reduce balance failed");
}
}
LOGGER.info("compensate reduce balance succeed, businessKey:" + businessKey);
return true;
}
}
InventoryAction
public interface InventoryAction {
boolean reduce(String businessKey, int count);
boolean compensateReduce(String businessKey);
}
InventoryActionImpl
@Service("inventoryAction")
public class InventoryActionImpl implements InventoryAction {
private static final Logger LOGGER = LoggerFactory.getLogger(InventoryActionImpl.class);
@Override
public boolean reduce(String businessKey, int count) {
LOGGER.info("reduce inventory succeed, count: " + count + ", businessKey:" + businessKey);
return true;
}
@Override
public boolean compensateReduce(String businessKey) {
LOGGER.info("compensate reduce inventory succeed, businessKey:" + businessKey);
return true;
}
}
ITAccountService
public interface ITAccountService extends IService<TAccount> {
ObjectResponse decreaseAccount(AccountDTO accountDTO);
void testGlobalLock();
}
TAccountServiceImpl
@Service
public class TAccountServiceImpl extends ServiceImpl<TAccountMapper, TAccount> implements ITAccountService {
@Override
public ObjectResponse decreaseAccount(AccountDTO accountDTO) {
int account = baseMapper.decreaseAccount(accountDTO.getUserId(), accountDTO.getAmount().doubleValue());
ObjectResponse<Object> response = new ObjectResponse<>();
if (account > 0) {
response.setStatus(RspStatusEnum.SUCCESS.getCode());
response.setMessage(RspStatusEnum.SUCCESS.getMessage());
return response;
}
response.setStatus(RspStatusEnum.FAIL.getCode());
response.setMessage(RspStatusEnum.FAIL.getMessage());
return response;
}
@Override
@GlobalLock
@Transactional(rollbackFor = {
Throwable.class})
public void testGlobalLock() {
baseMapper.testGlobalLock("1");
System.out.println("Hi, i got lock, i will do some thing with holding this lock.");
}
}