聊聊 Saga 方案
Saga 是 30 年前一篇数据库伦理提到的一个概念。其核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。
Saga 的组成如下:
- 每个 Saga 由一系列 sub-transaction Ti 组成
- 每个Ti 都有对应的补偿动作 Ci ,补偿动作用于撤销 Ti 造成的结果。这里的每个 T ,都是一个本地事务。
- 可以看到,和 TCC 相比,Saga 没有“预留 try”动作 ,它的 Ti 就是直接提交到库。
Saga的执行顺序有两种:
- 子事务序列 T1, T2, …, Tn得以完成 (最佳情况)。
- 或者序列 T1, T2, …, Tj, Cj, …, C2, C1, 0 < j < n, 得以完成。
Saga 定义了两种恢复策略:
向后恢复:补偿所有已完成的事务,如果任一子事务失败。
向后恢复,即上面提到的第二种执行顺序,其中 j 是发生错误的 sub-transaction ,这种做法的效果是撤销掉之前所有成功的 sub-transation ,使得整个 Saga 的执行结果撤销。
向前恢复:重试失败的事务,假设每个子事务最终都会成功。
显然,向前恢复没有必要提供补偿事务,如果你的业务中,子事务(最终)总会成功,或补偿事务难以定义或不可能,向前恢复更符合你的需求。理论上补偿事务永不失败,然而,在分布式世界中,服务器可能会宕机、网络可能会失败,甚至数据中心也可能会停电,这时需要提供故障恢复后回退的机制,比如人工干预。
🦅 如何解决没有 Prepare阶段可能带来的问题?
由于 Saga 模型中没有 Prepare 阶段,因此事务间不能保证隔离性,当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发。例如:
- 在应用层面加锁。
- 应用层面预先冻结资源。
还是拿 100 元买一瓶水的例子来说。
- 这里定义:
- T1=扣100元 T2=给用户加一瓶水 T3=减库存一瓶水
- C1=加100元 C2=给用户减一瓶水 C3=给库存加一瓶水
- 我们一次进行 T1,T2,T3。如果发生问题,就执行发生问题的 C 操作的反向。上面说到的隔离性的问题会出现在,如果执行到 T3 这个时候需要执行回滚,但是这个用户已经把水喝了(另外一个事务),回滚的时候就会发现,无法给用户减一瓶水了。这就是事务之间没有隔离性的问题。
艿艿:也就是说,给的太早,但是可以被取消!
可以看见 Saga 模式没有隔离性的影响还是较大,可以参照华为的解决方案:
- 从业务层面入手加入一 Session 以及锁的机制来保证能够串行化操作资源。
- 也可以在业务层面通过预先冻结资金的方式隔离这部分资源,最后在业务操作的过程中可以通过及时读取当前状态的方式获取到最新的更新。
🦅 解决方案
- Apache Service Comb 的 Saga 事务引擎
- Sharding Sphere 的 Saga 支持
实际是基于 Apache Service Comb 的 Saga 事务引擎之上进行开发。
本文由作者按照 CC BY 4.0 进行授权