什么是分布式事务

熟悉计算机的朋友肯定都知道事务。事务是一个逻辑执行单元,单元内的操作要么全部执行,要么全部失败,并且,事务具有具有原子性(Atomicity),一致性(Consistency),隔离性(Isolation)和持久性(Durability)这4个特性。上述我们提到的一般称之为本地事务,而分布式事务又是指什么呢?分布式事务通常指的是同时包含多个本地事务的事务,而不同的不同的本地事务一般又由不同的主机或服务进行管理。

分布式事务与本地事务的异同点

分布式事务由多个本地事务组成,每个本地事务的提交或回滚都由各自的服务或主机控制,为了使各个本地事务之间行为一致.由此便引入了事务 manager 的角色,由 manager 协调各个本地事务的提交与回滚.
根据 CAP 理论, 在多个本地事务间维护 ACID 的成本极高,所以在分布式事务场景下,提出了 BASE:

  • Basically Available: 基本可用,系统能够一直运行,提供服务
  • Eventual consistency: 最终一致性,即系统在之后的某个时间点达到一致性要求
  • Soft-state:软状态, 系统不要求一直保持强一致状态。

分布式协议

为了维护各节点之间的一致性状态,于是就诞生了各种各样的分布式协议.常见的包括 Paxos, Raft 以及两阶段提交等等.根据协议对一致性的要求不同,可以将他们分之为强一致性协议和弱一致性协议.
其中强弱分别是指对节点之间不一致状态的约束强度.强一致性协议要求相同请求对应的不同节点的返回都必须相同,而这一般意味着较大的通信成本.而弱一致性则允许节点之间在短时间内返回不一致的响应,由此来缩短响应延时.我们前面提到的 Paxos , Raft以及两阶段提交协议其实都是属于强一致性协议.简单来说,强一致性中的冗余复制是同步的,而弱一致性是异步的.

Saga

Saga 全称为 Long Lived Transaction, 简称为长活事务。Saga 主要通过错误重试以及补偿事务(回滚)两种途径来保证整个分布式事务的一致性
假设整个分布式事务由 A1, A2, A3..An 这些本地事务组成,当 An 执行失败,此时可以选择多次重试,若多次重试仍然失败,则执行 A1, A2, A3..An-1 的逆向操作。
设想我们有订单,账户和仓储三个微服务,用户下单之后订单服务需要创建订单, 然后账户服务从用户钱包进行扣款,最终仓储服务需要扣减库存。如果库存不足我们要求账户服务退回扣款金额,订单服务也需要取消订单,同样,如果用户钱包余额不足我们也要求取消订单。其中,取消订单和退回扣款金额我们将其看作创建订单和扣款的补偿事务,通过执行补偿事务来保证整体一致性。

type Wallet interface {
    Charge(amount int) error
    Refund(amount int) error
}
type walletImpl struct {
    balance int
}
func (w *walletImpl) Charge(amount int) error {
   if amount > w.balance {
        // publish charge failed message
        return errors.New("charge failed: no enough balance")
    } 
   // publish charge success message
    w.balance -= amount
    return nil
} 
func (w *walletImpl) Refund(amount int) error {
    // publish refund success message
    w.balance += amount
}

Saga 一般通过事件发布的方式进行通信,多个本地事务通过彼此发布订阅操作执行成功或失败的消息来决定继续执行还是回滚。比如,账户服务订阅了创建订单的消息,当订单创建完成后,账户服务执行Charge,同时发布扣费成功的消息通知,此时仓储服务消费扣费成功的消息,但是库存余额不足,扣减库存失败,此时发布扣减库存失败的消息,由账户服务消费该条消息并执行Refund,同时发布退款成功的消息,订单服务也随之取消订单。

图1.协同式 saga

根据 Saga 的执行过程中是否有编排器的参与又可以分为编排式的 Saga 和协同式的 Saga。所谓编排式的 Saga 是指将决策和执行顺序逻辑放在一个 Saga 编排器中,而协同式 Saga 的决策以及执行顺序逻辑则分布在每一个 Saga 的参与方中。上面我们所提到的便是协同式 saga.

图2.编排式 saga

Saga 的优缺点

由上所述,我们不难看出 saga 是一种弱一致性协议.这意味着它允许整个分布式事务的执行过程中各个本地事务暂时处于不一致的状态,也正因如此,这也让它相较于很多强一致性协议的实现以及通信成本低得多.
优点:

  • 无锁,并发性好,吞吐量大
  • 基于事件发布的异步机制,可用性高
  • 简单,补偿事务易于实现

缺点:

  • 隔离性较差
  • 补偿事务具有业务侵入性

如何解决隔离性问题

Saga 的一大硬伤是隔离性较差.Saga 的某个本地事务一旦提交,就可以被其他的 Saga 事务看到和使用,在某些场景下甚至可能会导致不一致状态的产生.设想 A 用户账户中有 5 元余额, x 商品售价 5 元,此时库存为0.当 A 下单购买并迅速取消的情况下, A 用户账户可能会凭空多出 5 元.

  时序1   创建订单   用户取消订单  
  时序2   扣款 -5(余额0)      
  时序3     补款 +5 (余额5)    
  时序4     取消成功    
  时序5   扣减库存失败      
  时序6   补款 +5 (余额10)      
  时序7   取消失败      

为了解决隔离性问题,你可以采用分布式锁或状态机等方式.例如,当你使用选择使用状态机时,并且结合编排式 saga,订单的创建,扣款和扣减库存都对应编排器中状态机的一个状态,取消订单只能对状态为扣减库存过的订单执行。这样就能避因为隔离性而带来的数据不一致的的问题。同时,你也可以采用分布式锁,确保一个订单同时只有 1 个 saga 正在执行。