TCC(Try Confirm Cancel)同样也是两阶段提交:
两阶段commit或rollback行为

和AT模式不同之处在于解决了AT基于支持本地ACID事务的关系型数据库的缺点,因为真实业务中可能不使用mysql之类的ACID关系型数据库,就算使用也很可能使用缓存如redis。
TCC模式不依赖于底层数据资源的事务支持:
虽然说TCC也是二阶段提交,但是和AT的本质区别是AT在第一阶段就提交了,而TCC第一阶段是prepare,第二阶段是根据第一阶段的准备来决定是提交还是rollback。
具体选择还是得看应用,比如以下场景:
某个下单业务涉及生成订单、扣库存、扣余额等,如果使用AT用户可能会看到订单生成成功了后面因为扣库存或者扣余额失败了又变成扣款失败,这样显然不合适,此时就可以使用TCC模式
无法提供TCC模式要求的三个接口,此时可以使用配置文件进行指定
如果不管流程多长、有多少分支都属于一个事务,只要一个执行失败都进行回滚那还好,但是如果有些分支属于弱相关业务不想影响主业务,那么就需要进行区分,此时可以使用配置文件进行指定
基于状态机引擎来实现:
库存扣减或余额扣减:
- 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协议,比如mysql把redolog分为两个阶段,kafka的事务消息分uncommit、commited两个状态,rocketmq的半消息。

执行阶段:
XA start/XA end/XA prepare+SQL+注册分支
注册分支
XA start需要Xid参数,Xid需要和Seata全局事务的XID和BranchId关联起来,以便由TC驱动XA分支的提交或回滚
完成阶段: XA commit/XA rollback
要求开发者配置XADataSource,类比AT模式如下:

2. 根据开发者的普通DataSource来创建

第二种比较方便,但不保证兼容性。
上层编程模型与 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);
- }