分布式事务的实现与演进

互联网应用的规模化,微服务和服务化成为了多数企业的首选,分布式集群从一定程度上解决了大规模运算以及性能负载问题,而伴随而来的数据一致性也成为了企业不得不面对的问题。

过去的应用,采用单数据库架构,数据的一致性由数据库事务管理,所以不存在什么问题。而今天的应用采用分布式数据库系统,数据被垂直或水平地分布在不同的数据库中,甚至分布在不同服务器上,这就导致数据关系的管理,脱离了数据库事务的约束,也就会出现数据关系不一致的情况。如图:

如上图所示,假设业务中有两个业务系统 -- 钱包和金币,用户可以通过钱包 + 金币抵扣的方式进行合并支付,例如:某用户购买的商品共计100元。其中用钱包支付90元,金币抵扣10元。正常情况下,上层的业务系统依次分别调用钱包系统和金币系统的扣除相应的金额,然后向用户返回支付成功的信息。不过这里可能存在几种异常的情况,比如用户的金币余额不足。由于钱包数据和金币数据分别存放在不同的数据库中,脱离了数据库事务的管理,因此出现钱包被正常扣除,而金币由于余额不足没有被扣除的情况,造成了数据不一致的情况。

目前解决分布式应用数据一致性问题的思路有几种,利用分布式事务(二阶段提交、三阶段提交)可以解决。不过由于分布式事务的性能问题,一般对性能有要求的平台都会尽量避免使用。这里我们介绍一种相对更轻量的思路,也是互联网应用中使用较多的一种解决方案。

1. 业务系统控制事务

继续我们上面的例子。最为简单的方法是钱包系统和金币系统为上层业务系统提供回滚事务的接口,当业务调用链中任何一个支撑系统发生错误时,立即调用已经成功的系统的回滚接口将数据会滚即可。具体流程如下:

  1. 上层业务系统调用钱包系统扣除用户余额;
  2. 扣除余额成功后,上层业务系统调用金币系统扣除余额;
  3. 金币系统扣除失败时,上层业务系统调用钱包的回滚接口将用户余额恢复。

这里比较关键的是钱包系统需要创建一张流水表,用来记录每笔订单的流水。回滚时,只要通过流水表的记录更新额度表即可。

这种方式实现起来比较简单,不过不足之处也很明显:

  1. 业务系统需要编写相同的事务处理代码;
  2. 调用链串行处理,效率低下;
  3. 系统相互调用,耦合度高;
  4. 下级的支撑系统越多,复杂度越高;
  5. 任何系统的变动都会影响业务的正常运行;
  6. 要为不同的支撑系统编写不同的处理代码;
  7. 业务处理过程中由于某种原因业务系统自身出现异常,仍有可能出现数据不一致情况;
  8. 调用过程中由于支撑系统并发压力等情况可能导致数据丢失或大量数据不一致情况发生。

2. 演进-1

可通过增加用来控制分布式事务的中间服务,解决耦合度高、复用、复杂度等问题。如图:

在业务系统与支撑系统之间增加事务服务系统,由业务系统告诉事务服务需要调用的支撑系统及相关信息,由事务服务统一控制分布式事务

该方案解决了以下几个问题:

  1. 业务系统需要编写相同的事务处理代码;
  2. 通过在事务服务中增加异步调用操作来解决调用链串行导致效率低的问题。

仍然存在的问题:

  1. 系统相互调用,耦合度高;
  2. 下级的支撑系统越多,复杂度越高;
  3. 任何系统的变动都会影响业务的正常运行;
  4. 要为不同的支撑系统编写不同的处理代码;
  5. 业务处理过程中由于某种原因业务系统自身出现异常,仍有可能出现数据不一致情况;
  6. 调用过程中由于支撑系统并发压力等情况可能导致数据丢失或大量数据不一致情况发生。

演进-2

事务服务与支撑系统之间引入 MQ 用于解耦依赖,并确保不会因为支撑系统的压力导致数据丢失及数据不一致情况。如图:

MQ的引入保证了消息的正确到达,同时解耦了事务服务与支撑系统的耦合。具体处理流程如下:

  1. 业务系统调用事务服务,并向告知其需要的支撑系统及业务信息;
  2. 事务服务对信息进行封装后置入 MQ,同时将需处理的支撑系统信息置入本地内存,用于保证事务的完整性(若事务服务是分布式多机部署,则将需处理的支撑系统信息置入远程缓存,如:Redis);
  3. 各支撑系统订阅统一的 Topic 用于接收事务服务的处理请求;接收到消息后对消息进行校验,并判断是否需要处理;
  4. 若需要处理,则向 MQ 置入正在处理状态,并进行业务处理;
  5. 处理完成后,则向 MQ 置入处理完成状态,并提交事务;
  6. 若处理失败,则向 MQ 置入处理失败状态,并回滚事务;
  7. 事务服务订阅支撑系统的状态 Topic,用于监控各系统的业务处理情况;
  8. 当收到处理任何一个失败状态时,则向 MQ 置入回滚消息,并向业务方返回处理失败;
  9. 直到收到所有支撑系统的处理完成状态后,向业务方返回处理成功;

异常情况: 事务服务预先设定超时时间,在超时时间内支撑系统没有返回状态或没有处理完成,则根据业务的需求有以下几种处理方法:

  1. 发送回滚消息并告知业务系统处理失败;
  2. 向业务方返回处理中,并继续等待直到处理结束后,异步通知业务方处理结果;
  3. 继续等待直到处理结束后,异步通知业务方处理结果;

其它说明:

  1. 由于 MQ 存在消息重复投递的情况,可能导致同一订单被处理了两次的情况。因此我们需要对 MQ 消息进行校验处理。支撑系统收到消息后,都去流水表中进行校验,如果数据存在,则不做处理,如数据不存在,则保存流水并进行处理。
  2. 由于是异步操作,很可能出现多个支撑系统处理失败的情况,会导致回滚消息重复发送的问题。因此事务服务要针对这种情况进行校验,确保是只发送一次回滚消息。同时支撑系统也要对回滚消息进行二次校验。
  3. 对于事务服务异步通知业务方的处理,要采用补偿机制,即调用业务方回调接口时,当且仅当业务方返回成功状态码才停止调用,否则每隔一段时间就调用一次。

4. 演进3

以上方案仍然存在业务系统和支撑系统的大量重复编码工作。因此可以通过封状 SDK 的方式将事务处理的相关细节进行封装。如果利用 Spring 等框架的系统,封装 SDK 时可以充分利用其 AOP 特性做到无侵入性。

另一方面,受网络、服务器环境等不安定因素的影响,以上方案仍然不能确保数据一致性。因此我们需要在每一个处理环节中输出具体日志(可打入本地文件并用 flume 收集入库,或直接入 kafka 等中间件),定时对日志进行分析整理入库,若发现异常则自动进行补偿或向相关人员发送报警信息,由人为介入审查和处理。