Telling a programmer there's already a library to do Something is like telling a songwriter there's already a song about love.

-- Enix Jin

前两篇:

微服务,通过把系统解耦成一个个“微小”的服务解决了大型系统的复杂性问题。然而,在享受分布式系统的好处的同时,我们也会面临分布式系统的复杂性带来的问题。比如,如何跨多个微服务管理分布式事务?

第三步的代码在这里


什么是分布式事务?

当微服务架构将大型系统分解为自封装的小服务时,它同时也破坏了事务。

这意味着大型系统中的本地事务现在会被分布到按顺序调用的多个微服务中。

以下是使用本地事务的客户订单示例:

客户订单示例

在上面的客户订单示例中,如果用户将Put Order操作发送到系统,系统将创建一个本地数据库事务。 如果任何步骤失败,则事务回滚。ACID(A:原子性,C:一致性,I:隔离性,D:耐久性),由数据库系统来保证。

然而,当我们使用微服务架构时,我们创建了CustomerMicroservice和OrderMicroservice,它们都有独立的数据库。客户订单就会变成这样:

当用户发送Put Order请求时,将分别调用两个微服务并存到他们自己的数据库中。由于事务现在跨多个数据库,因此它现在被视为分布式事务


有什么问题呢?

在单一系统中,ACID由数据库来保证,在微服务架构中,我们需要考虑以下问题:

我们如何保持事务的原子性?

在数据库系统中,原子性意味着一个事务中所有步骤要么都完成要么都没有完成。 默认情况下,基于微服务的系统没有全局事务协调器。 在上面的示例中,如果CreateOrder方法失败,我们如何回滚CustomerMicroservice应用的更改?

我们是否针对并发请求隔离用户操作?

如果一个Object正在由一个事务写入的,同时(在事务结束之前),如果有人访问改对象,系统应该返回旧数据还是将更新数据? 在上面的示例中,一旦UpdateCustomerFund成功但仍在等待CreateOrder的响应,当前客户资金信息请求是否会返回变更后的金额?


解决方案

上述问题对于基于微服务的系统很重要。否则,你无法判断事务是否已成功完成。

大致思考下,有以下两种模式可以解决问题:(我选择后者,原因后面会讲)

  • 两阶段提交
  • SAGA (来自于1987年Hector GM和Kenneth Salem论文)

两段提交

两段提交被广泛用于数据库系统。 在某些情况下,你可以使用两段提交进行微服务。但是,事实上,两段提交在微服务架构中被认为是不切实际的。

那什么是两阶段提交呢?

正如其名称提示,两段提交有两个阶段:准备阶段提交阶段

在准备阶段,将要求所有微服务准备一些可以原子方式完成的数据更改。一旦准备好所有微服务,提交阶段将要求所有微服务进行实际更改。

所以,两段提交通常需要有一个全局协调器(Coordinator)来维护事务的生命周期,协调器需要在准备和提交阶段调用微服务。

还是客户下单的例子,如果用两段提交:

在上面的示例中,当用户发送放置订单请求时,协调器将首先创建包含所有上下文信息的全局事务。然后它会告诉CustomerMicroservice准备用创建的交易更新客户资金。接着,CustomerMicroservice将检查客户是否有足够的资金来进行交易。一旦CustomerMicroservice可以执行更改,它将锁定对象的进一步更改并告诉协调器它已准备好。同时,在OrderMicroservice中创建订单时会发生同样的事情。一旦协调器确认所有微服务都准备好应用他们的更改,它就会要求他们通过请求提交事务来应用他们的更改。此时,所有对象都将被解锁。

如果在任何时候单个微服务无法准备,协调器将中止事务并开始回滚过程。比如以下的回滚图:

使用两段提交的好处

两段提交是一种非常强大的一致性协议。首先,准备和提交阶段保证事务是原子的。事务将以所有微服务成功返回或所有微服务没有任何改变而结束。其次,两段提交允许读写分离。 这意味着在协调器提交更改之前,字段上的更改不可见。

两段提交的缺点

虽然两段提交能解决事务问题,但对于微服务是不推荐的。原因?因为它是同步阻塞的。

协议需要在事务完成之前锁定更改对象。在上面的例子里,如果客户下订单,则将为客户锁定“资金”字段。 这会防止客户申请新订单。这是在两段提交里有道理的,因为如果“准备好的”对象在声称它已“准备好”之后发生了更改,那么提交阶段可能无法正常工作。

在数据库系统中,事务通常在50毫秒级别。但是,微服务在远程调用方面存在长时间延迟的可能性,尤其是需要与外部服务(比如支付服务)集成时。锁可能成为系统性能瓶颈。此外,如果一个事务请求锁定另一个事务需要的资源时,非常可能有两个事务相互锁定(死锁)。


SAGA

Saga模式是另一种广泛使用的分布式事务模式。与同步的两段提交不同,Saga模式是异步和响应式的。在Saga模式中,分布式事务由所有相关微服务上的异步本地事务完成。微服务通过事件总线(Event Bus)相互通信。

以下是客户订单示例的Saga模式图:

在上面的示例中,OrderMicroservice收到下订单的请求。它首先启动本地事务以创建订单,然后发出OrderCreated事件。CustomerMicroservice会在收听到此事件时尝试更新客户资金。如果从账户成功扣款,则会发出CustomerFundUpdated事件,表示交易结束。

如果某个微服务无法完成其本地事务,其他微服务将运行补偿事务(compensation transactions)以回滚更改。 以下是补偿交易的Saga模式图:

Saga模式的优点

Saga模式的一大优势是它很好的支持了耗时长的交易。因为每个微服务仅关注其自己的本地原子事务,所以即使某个微服务运行很长时间,也不会阻塞其他微服务。同时,Saga也允许事务等待期间用户的新订单。此外,由于所有本地事务都是并行发生的,因此任何对象都没有锁定。

Saga模式的缺点

Saga模式很难调试,特别是当事务涉及多个微服务时。此外,如果系统变得复杂,事件消息队列可能变得难以维护。Saga模式的另一个缺点是它没有读取隔离。例如,客户可以看到正在创建的订单成功了,但在下一秒,订单将因补偿交易而被删除。


代码(这里)的一些说明

  • 在第二步代码的基础上,增加了一个微服务:Order,账户系统就在原先的Auth上扩展(因为懒)
  • 引入了RabbitMQ作为事件消息队列

比如我们通过gateway,登录之后下一个订单:

order微服务收到消息之后会执行订单创建并往消息队列OrderCreated里发布一个orderid:

@Post({url: "/"})
async sendMessage(req) {
    let user = await encryption.getAuthentication(req);
    // 1.create order
    // 2.send to queue
    if (user) {
        await mqTools.sendToQueue("OrderCreated", {orderid: req.body.orderid});
        return {success: true, body: req.body, nodePort: global.config.servicePort};
    }
}

账户系统在听到这个消息后去操作账户:

//app.ts
mqTools.subscribe("OrderCreated").subscribe((msg) => {
  return customerService.handleOrderMessage(msg);
});

//customerService.ts
async handleOrderMessage(msg: string) {
    let message = JSON.parse(msg);
    logger.debug(`handle order id:${message.orderid}`);
    let success = false;
    if (success) {
        // success
        await this.updateCustomerFund();
        // current no one interested in this message
        await mqTools.sendToQueue("CustomerFundUpdated", {orderid: message.orderid});
    } else {
        // fail
        await mqTools.sendToQueue("CustomerFundFailed", {orderid: message.orderid});
    }
}

如果处理失败,order微服务会监听到CustomerFundFailed里的消息:

//orderService.ts
async handleCustomerFundMessage(msg: string) {
    let message = JSON.parse(msg);
    logger.debug(`handle order fail id:${message.orderid}`);
    await this.compensateOrder(message.orderid);
}

可以观察到两个微服务的日志:

Customer

Order