1.5、分布式之弹性:补偿事务

一、简介

微服务架构中,业务的实现基本上都是有多个服务共同组合的逻辑。依赖多个服务,对于需要保证一致性的业务就变得很难处理。解决方案基本就是以下两种

  • 其中一个服务失败,将其他服务回滚到之前的状态。
  • 不断重试,直到处理成功(这种要保证下游服务是幂等性的)

如果为了保证业强一致性,还需要2PC等操作。更详细的可以自行google

二、事务

2.1、ACID

事务四大特性ACID:原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)

  • 原子性:事务是由多个执行单元组成,原子性保证事务被处理为一个”单位”,要么全部成功,要么全部失败。如果其中构成事务的执行单元没有成功执行,则整个事务失败。
  • 一致性:确保事务只能将数据从一个有效状态转换为另一个有效状态。
  • 隔离性:一个事务的处理,在提交之前对于其他事务来说是互不干扰的。
  • 持久性:事务一旦被提交,对数据的更改将被持久保存,不会被回滚。

事务的ACID保证了数据的一致性,经典例子就是银行转账:

假如一个银行的数据库有两张表:支票表(checking)和储蓄表(savings)。现在要从用户Jane的支票账户转移200美元到她的储蓄账户,那么至少需要三个步骤:

1、检查支票账户的余额高于或者等于200美元。

2、从支票账户余额中减去200美元。

3、在储蓄帐户余额中增加200美元。

上述三个步骤的操作必须打包在一个事务中,任何一个步骤失败,则必须回滚所有的步骤。ACID重点实现的是CAP中C(一致性)

ACID在实际的业务场景中经典的就是电商系统中的购物场景,大家一起购买同一件商品对于每一个用户的请求,我们先锁库存,减完库存后释放锁,锁被释放后,其他用户继续发起购买请求。这种设计注定了无法同时并发的购买商品。因为订单处理排队的原因,系统的性能也就被限制死了。

有没有其他方案可以优化一下呢?

2.2、BASE

相对于ACID,为了提高性能,演变出了BASE

  • Basically Availble:基本可用,允许系统短时间出现不可用的情况,提高系统恢复能力。
  • Soft-state:软状态/柔性事务,就是将服务介于有状态和无状态之间,服务自身可以保存一些状态信息,这些状态不要求
  • Eventual Consistency:最终一致性,在一段时间内可能不一致,但是最终整个系统会保持一致性。

其实在很多业务场景中我们并非一定要做到强一致性的ACID,BASE可以让系统的容错性更好,在微服务系统中,故障不能避免,作为开发者可以做到的就是快速处理故障。使用BASE要确认我们当前的业务允许出现短时间的不一致,但是最终会保持一致性。

允许短时间出现不一致,那么就可以使用异步处理的方案,使用异步处理自然就提高了系统的性能。eBay基于BASE的规则设计的最终一致性解决方案使用消息队列来实现事务流程的控制,核心就是通过消息队列来异步处理任务,如果事务失败则重新消费,最终借助对账流程保证最终一致性的正确性。

2.2.1、方案模拟

模拟设计一个业务场景:现在很多忽悠学生进群,然后日赚100元的工作。进群就会发现其实就基本上就是到了群里去领取任务,这些任务都是先去他们的CMS平台注册手机设备,去应用商店下载某个App然后登入App。完成任务后得到对应的5元奖励。俗称刷榜!

我作为广告主去他们平台刷一些真实的下载量需要为他们平台提供以下接口。

  • 设备注册接口(A)
  • 设备激活接口(B)
  • 设备查询接口(C)

其中,A接口是提供外部使用,B接口是我们内部使用,C则是内部外部都使用。流程就是,他们使用我们的注册接口注册设备信息,用C接口查询该设备是否之前已经在我们App登入过,没有激活过则允许注册,此时广告平台会提供一个回调接口给我们,如果该设备激活过则拒绝注册。注册成功后用户登入app,我们发现成功激活则使用之前广告平台提供的callback请求一次即可,这就完成了一次刷单,提供了一个有效的下载量。

激活涉及以下数据修改:

1
2
3
1、查询改设备是否注册过
2、如果未注册,则查询该设备是否激活过
3、如果未激活,调用callback,修改该设备存储信息为已激活,在全量的设备表中添加该设备,防止二次注册。

激活流程基于BASE的准则该如何设计?

如上图,保证消息发送一致性的一般流程如下:

  • Producer确认登入信息,发送信息到消息队列。
  • 消息队列接收信息后,向订阅者推送该消息。
  • 消费者监控并接收该消息执行对应逻辑。
  • 消费者如果消费成功了(所有流程),则确认该消息已消费,如果该消息没有成功消费,可能是上面1,2,3中任何一个发生了错误。则通知消息服务这次消费失败了。

一般消息服务收到该消息失败的反馈,会重新发布该消息,消费者重新进行消费,直到消费成功。如果连续几次消费失败,这个时候可能就需要启动熔断机制,拒绝消费,人工介入去寻找原因,恢复后继续消费。

最后再利用一个对账服务照看当前已经注册的设备并且已经激活的设备是否在我们全量激活设备中。如果不在则修复一下,或者记录下该异常流水。

注意:该设计一定要保证接口是幂等的,尤其是callback回调。保证多次回调的结果相同。如果回调一次,认为激活了一次,那广告主可要赔死了。

2.3 业务补偿

为了提高系统性能,使用BASE标准,就需要实现补偿逻辑。做好补偿机制需要对业务流程有很好的认知,清楚哪些状态需要需要补偿,哪些可以不用管。

  • 努力做到服务成功执行一个流程
  • 流程执行中出现错误,启动补偿机制,回滚或者重试。

设计重点如下:

  • 流程中的服务要设计成幂等性的,因为可能不断重试。
  • 如果允许丢失部分消息,则可以使用消息队列的形式实现异步处理,当然可以通过多订阅一些消息,来保证消息不丢失。
  • 补偿业务很难做成通用,是跟业务强相关的。在设计前就需要考虑好。当然如果所有接口符合幂等的,并且允许最终一致性,那就可以使用消息队列不停重复消费知道消费成功。
  • 为了保障,再设计一个对账服务,保证最终一致性,这种对账我认为也是补偿设计中的一种。在对账服务中,针对不同的状态,做出不同的修复方案。

三、总结

设计服务之前,确认好是否需要保证数据的一致性,如果需要则考虑业务需要强一致性还是最终一致性。使用ACID还是BASE,使用BASE需要设计补偿事务逻辑,采用重试,还是针对各种失败状态给出不同的补偿方案,达到最终一致性。如果使用重试,一定要保证接口的幂等性,为了保证最终一致性,最好再设计一个对账服务。