分布式事务-05-SAGA

SAGE核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作

  1. 如何实现补偿(提前准备回滚语句)
  2. dtm 的SAGA模式与Seata的SAGA在设计理念上是不一样的

流程

已跨行转账的业务为例,转出(TransOut)和转入(TransIn)分别在不同的微服务里,一个成功完成的SAGA事务典型的时序图如下

成功

saga_normal

失败

saga_rollback

代码如下:

1
2
3
4
5
req := &gin.H{"amount": 30} // 微服务的请求Body
saga := dtmcli.NewSaga(DtmServer, shortuuid.New()).
Add(qsBusi+"/TransOut", qsBusi+"/TransOutCompensate", req).
Add(qsBusi+"/TransIn", qsBusi+"/TransInCompensate", req)
err := saga.Submit()
  • 构建了事务的请求Body
  • 构建一个事务包含了
    • DtmServer为DTM服务的地址
    • shortuuid.New() 事务请求ID
    • 添加一个TransOut的子事务,每个事务都包含了正向操作补偿操作(逆向操作)
      • 正向操作为url: qsBusi+"/TransOut"
      • 逆向操作为url: qsBusi+"/TransOutCompensate"
    • 添加一个TransIn的子事务
      • 正向操作为url: qsBusi+"/TransIn"
      • 逆向操作为url: qsBusi+"/TransInCompensate"
  • 提交saga事务

问题1:是如何进行补偿的

问题2:补偿失败是如何处理的

答:在补偿操作遇见失败时,会不断进行重试,直到成功。(TM重启了怎么办)

问题3:sage事务是同步返回结果还是异步任务处理的

问题4:当RM1执行成功,RM2执行失败的同时RM1崩溃了,TM会如何处理

问题5:当服务崩溃大量事务堆积在TM上,TM如何支持短时间大量事务重试

补偿

补偿的情况有几种

  1. 第一种情况,子事务 A - B - C 中 C 失败,需要对 A - B 进行补偿操作,如何保存处理单个分布式事务的子事务顺序补偿问题 B 先回滚,然后是A
  2. 第二种情况,子事务 A - B - C 在执行过程中 A1 - B1 - C1 也在执行,A - B - C 中 C失败,A1 - B1 - C1 中 B1 失败,那么如何处理
  3. 单个服务的补偿分为 已执行 、未执行、执行中(结果未知),那么补偿又是如何处理的

补偿执行顺序

DTM 的SAGA事务在1.10.0及之前,补偿操作是并发执行的,1.10.1之后,是根据用户指定的分支顺序,进行回滚的。

  • 普通SAGA,未打开并发选项,那么SAGA事务的补偿分支是完全按照正向分支的反向顺序进行补偿的。

  • 并发SAGA,补偿分支也会并发执行,补偿分支的执行顺序与指定的正向分支顺序相反。假如并发SAGA指定A分支之后才能执行B,那么进行并发补偿时,DTM保证A的补偿操作在B的补偿操作之后执行

Demo

一个用户出行旅游的应用,收到一个用户出行计划,需要预定去三亚的机票,三亚的酒店,返程的机票。要求:

  1. 两张机票和酒店要么都预定成功,要么都回滚(酒店和航空公司提供了相关的回滚接口)
  2. 预订机票和酒店是并发的,避免串行的情况下,因为某一个预定最后确认时间晚,导致其他的预定错过时间
  3. 预定结果的确认时间可能从1分钟到1天不等

首先,根据要求1创建一个saga事务,这个saga包含三个分支,预定去三亚机票预定酒店预定返程机票

1
2
3
4
saga := dtmcli.NewSaga(DtmServer, gid).
Add(Busi+"/BookTicket", Busi+"/BookTicketRevert", bookTicketInfo1).
Add(Busi+"/BookHotel", Busi+"/BookHotelRevert", bookHotelInfo2).
Add(Busi+"/BookTicket", Busi+"/BookTicketRevert", bookTicketBackInfo3)

接着,根据要求2,让saga并发执行(默认是顺序执行)

1
saga.EnableConcurrent()

最后,根据要求3,由于不是即时响应,所以不能够让预定操作等待第三方的结果,而是提交预定请求后,就立即返回状态-进行中

1
2
3
4
5
6
7
8
9
10
11
12
saga.RetryInterval = 60
saga.Submit()
// ........
func bookTicket() string {
order := loadOrder()
if order == nil { // 尚未下单,进行第三方下单操作
order = submitTicketOrder()
order.save()
}
order.Query() // 查询第三方订单状态
return order.Status // 成功-SUCCESS 失败-FAILURE 进行中-ONGOING
}

分支事务未完成,dtm会重试我们的事务分支,把重试间隔指定为1分钟,这里订票结果不应当采用指数退避算法重试,否则最终用户不能及时收到通知。在bookTicket中,返回结果ONGOING,当dtm收到这个结果时,会采用固定间隔重试,这样能及时通知到用户

并发

并发SAGA通过EnableConcurrent()打开,当saga提交后,多个事务分支之间是并发执行。DTM也支持指定事务分支之间的依赖关系,可以指定特定任务A执行完成之后才能够执行任务B

并发SAGA如果出现回滚,那么所有回滚的补偿操作会全部并发执行,不再考虑前面的任务依赖。

由于并发SAGA的正向操作和补偿操作都是并发执行的,因此更容易出现空补偿和悬挂情况,需要参考DTM的子事务屏障环节妥善处理

部分无法回滚

1
2
3
4
5
6
7
8
saga := dtmcli.NewSaga(DtmServer, shortuuid.New()).
Add(Busi+"/CanRollback1", Busi+"/CanRollback1Revert", req).
Add(Busi+"/CanRollback2", Busi+"/CanRollback2Revert", req).
Add(Busi+"/UnRollback1", "", req).
Add(Busi+"/UnRollback2", "", req).
EnableConcurrent().
AddBranchOrder(2, []int{0, 1}). // 指定step 2,需要在0,1完成后执行
AddBranchOrder(3, []int{0, 1}) // 指定step 3,需要在0,1完成后执行

指定Step 2,3 中的 UnRollback 操作,必须在Step 0,1 完成后执行

这样也能处理 第一个事务输出是第二个事务的输入怎么办的问题

超时回滚

saga属于长事务,因此持续的时间跨度很大,可能是100ms到1天,因此saga没有默认的超时时间。

dtm支持saga事务单独指定超时时间,到了超时时间,全局事务就会回滚。

1
saga.TimeoutToFail = 1800

在saga事务中,设置超时时间一定要注意,这类事务里不能够包含无法回滚的事务分支,因为超时回滚时,已执行的无法回滚的分支,数据就是错的

参考文档

  1. https://dtm.pub/practice/saga.html
  2. https://dtm.pub/deploy/maintain.html
  3. https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf