TCC模式
TCC(Try Confirm Cancel)同样也是两阶段提交:
- 一阶段prepare行为
-
两阶段commit或rollback行为
和AT模式不同之处在于解决了AT基于支持本地ACID事务的关系型数据库的缺点,因为真实业务中可能不使用mysql之类的ACID关系型数据库,就算使用也很可能使用缓存如redis。
TCC模式不依赖于底层数据资源的事务支持:
- 一阶段prepare行为:调用自定义的prepare逻辑。
- 二阶段commit行为:调用自定义的commit逻辑。
- 二阶段rollback行为:调用自定义的rollback逻辑。
注意
虽然说TCC也是二阶段提交,但是和AT的本质区别是AT在第一阶段就提交了,而TCC第一阶段是prepare,第二阶段是根据第一阶段的准备来决定是提交还是rollback。
具体选择还是得看应用,比如以下场景:
某个下单业务涉及生成订单、扣库存、扣余额等,如果使用AT用户可能会看到订单生成成功了后面因为扣库存或者扣余额失败了又变成扣款失败,这样显然不合适,此时就可以使用TCC模式
- 先创建订单(订单状态为待扣款)、冻结库存(需要当前读库存是否充足,否则就直接失败了)、冻结余额(同冻结库存)——此步为prepare
- 如果第一步成功了就实扣、订单状态改为成功——此步为commit
- 如果第一步失败了就回滚冻结、订单状态改为扣款失败——此步为rollback
Saga模式
适用场景/痛点
- 参与者包含其它公司或遗留系统服务,无法提供TCC模式要求的三个接口
无法提供TCC模式要求的三个接口,此时可以使用配置文件进行指定
- 业务流程长、业务流程多
如果不管流程多长、有多少分支都属于一个事务,只要一个执行失败都进行回滚那还好,但是如果有些分支属于弱相关业务不想影响主业务,那么就需要进行区分,此时可以使用配置文件进行指定
实现
基于状态机引擎来实现:
- 通过状态图来定义服务调用的流程并生成json状态语言定义文件
- 状态图中一个节点可以是调用一个服务,节点可以配置它的补偿节点
- 状态图json由状态机引擎驱动执行,当出现异常时状态引擎反向执行已成功节点对应的补偿节点将事务回滚
- 可以实现服务编排需求,支持单项选择、并发、子流程、参数转换、参数映射、服务执行状态判断、异常捕获等功能
例子
库存扣减或余额扣减:
public interface InventoryAction {
/**
* reduce
* @param businessKey
* @param amount
* @param params
* @return
*/
boolean reduce(String businessKey, BigDecimal amount, Map<String, Object> params);
/**
* compensateReduce
* @param businessKey
* @param params
* @return
*/
boolean compensateReduce(String businessKey, Map<String, Object> params);
}
public interface BalanceAction {
/**
* reduce
* @param businessKey
* @param amount
* @param params
* @return
*/
boolean reduce(String businessKey, BigDecimal amount, Map<String, Object> params);
/**
* compensateReduce
* @param businessKey
* @param params
* @return
*/
boolean compensateReduce(String businessKey, Map<String, Object> params);
}
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"
}
}
}
可以看到库存扣减或余额扣减都制定了补偿节点,当出现异常的时候就会进行回滚。
最佳实践
允许空补偿
空补偿:原服务未执行,补偿服务执行了 出现原因:原服务超时(丢包)
所以服务设计时需要允许补偿,即没有找到要补偿的业务主键时返回补偿成功并将原业务主键记录下来
补偿业务主键指的某个分支的主表记录主键,如果分支业务没执行就进行补偿需要把原业务主键记录下来,防止原服务后续又执行了,可以依据补偿记录进行拒绝。
防悬挂
悬挂:补偿服务比原服务先执行 出现原因:原服务超时(拥堵)
所以要检查当前业务主键是否已经在空补偿记录下来的业务主键中存在,如果存在则要拒绝服务执行
如果补偿执行了说明TC已经认为这个分支失败了,其他提交分支都已经回滚,所以不允许原服务继续执行。
幂等控制
原服务与补偿服务都需要保证幂等性, 由于网络可能超时, 可以设置重试策略,重试发生时要通过幂等控制避免业务数据重复更新
缺乏幂等性的应付
saga事务不保证幂等性,脏写无法完成回滚操作,比如分布式事务内B转钱给A,如果A收钱成功了,在事务提交以前,A用户把余额消费掉了,如果事务回滚了,就没办法补偿了。
应对办法:
- 业务流程设计时遵循“宁可长款, 不可短款”的原则, 长款意思是客户少了钱机构多了钱, 以机构信誉可以给客户退款, 反之则是短款, 少的钱可能追不回来了。所以在业务流程设计上一定是先扣款。
针对于上面的例子,可以控制事务中执行的顺序,让A收钱最后执行
- 有些业务场景可以允许让业务最终成功, 在回滚不了的情况下可以继续重试完成后面的流程, 所以状态机引擎除了提供“回滚”能力还需要提供“向前”恢复上下文继续执行的能力, 让业务最终执行成功, 达到最终一致性的目的。
XA模式
XA模式需要事务资源(数据库、消息服务等)支持XA协议,比如mysql把redolog分为两个阶段,kafka的事务消息分uncommit、commited两个状态,rocketmq的半消息。
执行阶段:
XA start/XA end/XA prepare+SQL+注册分支
- 可回滚:业务SQL操作放在XA分支中进行,由资源对XA协议的支持来保证可回滚
- 持久化:XA分支完成后,执行XA prepare,同样,由资源对XA协议的支持来保证持久化(即,之后任何意外都不会造成无法回滚的情况)
注册分支
XA start需要Xid参数,Xid需要和Seata全局事务的XID和BranchId关联起来,以便由TC驱动XA分支的提交或回滚
完成阶段: XA commit/XA rollback
- 分支提交:执行XA分支的commit
- 分支回滚:执行分支的rollback
数据源代理
-
要求开发者配置XADataSource,类比AT模式如下:
2. 根据开发者的普通DataSource来创建
第二种比较方便,但不保证兼容性。
XA 模式的使用
上层编程模型与 AT 模式完全相同。只需要修改数据源代理,即可实现 XA 模式与 AT 模式之间的切换。
@Bean("dataSource")
public DataSource dataSource(DruidDataSource druidDataSource){
// DataSourceProxy for AT mode
// return new DataSourceProxy(druidDataSource);
// DataSourceProxyXA for XA mode
return new DataSourceProxyXA(druidDataSource);
}