朝花夕拾

A Development Engineer, a Life Liver, a Hope Holder

应用场景

随着业务的增长,单个数据库已经无法完成业务需求。需要对业务进行拆分。不同的业务逻辑可能操作不同的数据库。这时,系统中的某些操作与其他操作有依赖关系。为了保证数据操作的业务完整性,我们要求某些操作与其关联系统要么全部操作成功,要么全部操作失败。这就要求我们要在整个系统层面实现事务控制。

分布式事务的常用解决方案

二阶段提交算法

在分布式系统中,虽然单独节点可以知道自己节点操作的成败,但是不能知道其他节点的成败状况。为了解决这个问题,二阶段提交算法在分布式系统中引入了一个中间协调者。分布式系统各节点与中间协调者通信,中间协调者感知各节点的操作状态,然后根据汇总结果协调其他节点的成败。

二阶段提交算法分为两个阶段,准备阶段,提交阶段

准备阶段操作过程为:
  1. 协调者向每一个参与节点发起事务询问,等待各个节点响应。
  2. 参与节点执行事务,不提交事务
  3. 并将执行结果反馈给协调者
提交阶段过程为:

对于准备阶段,一般情况会出现三种情况

  1. 所有节点全部正常执行了事务
  2. 一个或者多个节点执行事务出现异常
  3. 协调者等待节点响应超时

针对第一种情况,协调者会对所有参与节点发出commit请求,具体步骤如下:

  1. 协调者分别向参与节点发出commit请求
  2. 参与者执行commit,并释放事务资源
  3. 参与者向协调者返回事务执行结果

针对二三情况,协调者会向所有参与节点发出rollback请求,具体操作如下:

  1. 协调者分别向参与者发出rollback请求
  2. 参与者执行回滚,释放资源
  3. 参与者将回滚结果反馈给协调者
主要缺点
  1. 单点故障,在整个体系中,协调者处于比较核心的位置。在运行过程中,如果协调者出现了故障,将导致整个系统不可用。如果在提交阶段,协调者不能正常收发消息,那么整个系统中的参与者将处于阻塞状态。
  2. 效率问题,在系统事务同步的过程中,整个系统参与者都处于阻塞阶段。不能参与其他操作,这样会影响到系统的效率。
  3. 数据不一致问题,在提交阶段,如果协调者出现故障,使得一部分参与者接收到commit,另一部分接受者没有接收到commit。将导致参与节点的数据出现不一致问题。

三阶段提交算法

与二阶段提交算法相比,三阶段主要增加了与询问阶段,增加系统的可用性。
三阶段提交共分can-commit,pre-commit,do-commit

状态机方案:

假如有若干个服务顺序执行,并且每个服务都提供配套的回滚服务;后续服务如果执行失败,回调前续服务的回滚服务。对于超时情况,这个比较复杂,因为链接超时可能有两种情况

  1. 前置服务发送消息时出现超时,这种情况后续服务没有执行操作。
  2. 前置服务已经成功发送消息,并且后续服务也执行了请求。只是后续服务在返回时出现超时。这种情况下,后续服务其实已经执行了操作。
    为了解决这个问题,我们可以在后续服务中增加一些操作记录表,一旦后续服务执行了前置服务的请求,更新记录数据。这样如果出现请求超时,可以通过后续服务进行查询确认后续服务是否已经执行了请求操作。
主要缺点
  1. 请求链中,如果出现了crash,已经宕机的服务不能自己恢复。
  2. 这个方案要求前置设计提供回滚接口,一是增加开发量,二是不可能要求每一个服务开发者都能提供回滚接口。

消息驱动的一致性方案

方案1 消息数据与前置服务业务数据保存在一个数据库中

假如有若干服务进行顺序执行,在执行前置服务成功后,将消息以异步的方式写入消息数据表中。后置服务可以轮询查询消息更新状态,发现有新消息,则进行后续消息业务逻辑。如果消费失败,后续业务满足幂等,可以进行重试。增设后续服务操作记录日志,可以不要求后续服务幂等。假设有两个服务A,B

#服务A:
Begin tx;
服务A DML
push message //将消息数据保存到数据库
Commit/Rollback;
#服务B:
for eash message in queue
begin local trx:
服务B DML;
commit/rollback;
if trx.success:
pop message;
end if;
end for;
优点
  1. 能满足业务一致性,性能较好
  2. 将消息数据与业务数据保存到一个数据库中的好处是,插入消息和业务DML可以很方便的实现原子性
主要缺点
  1. 消息跟业务共用数据库资源。使得对业务进行拆分变得困难。
  2. 系统没有隔离性,消息一旦产生就必须被消费,如果消息无法消费,需要人工介入。系统维护成本较高。

方案2:将消息数据与业务数据解耦,分库

执行前置业务前先记录操作消息数据,这时可以标识消息数据的状态为不可消费。当前置业务操作完毕,业务消息将消息置为可消费状态。假设有两个服务A,B

#服务A:
push haf message //将消息数据保存到数据库
Begin tx;
服务A DML
Commit/Rollback;
If tx.success
Commit half message; //将消息置为有效 保证消息与业务数据的原子性
End if;

#服务B:
for eash message in queue
begin local trx:
服务B DML;
commit/rollback;
if trx.success:
pop message;
end if;
end for;
优点
  1. 能满足业务一致性,性能较好
  2. 消息与业务数据分开存储,实现消息业务的解耦。方便业务分层扩展。
主要缺点
  1. 一次操作,要将消息发送两次,一次插入消息,一次更新消息状态;增加了系统的运行成本。
  2. 系统没有隔离性,消息一旦产生就必须被消费,如果消息无法消费,需要人工介入。系统维护成本较高。