事务与分布式事务

事务

事务是数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。数据库需要确保事务操作的原子性:当事务成功时,意味着事务的所有操作全部都执行完成;但事务失败时,数据库将所有执行过的SQL操作回滚。

数据库单机事务主要拥有四个特性:

  • 原子性,事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行
  • 一致性,事务应确保数据库的状态从一个一致状态转变为另一个一致状态,一致状态的含义是数据库中的数据应满足完整性约束
  • 隔离性,多个事务并发执行时,一个事务的执行不应影响其他事务的执行
  • 持久性,已被提交的事务对数据库的修改应该永久保存在数据库中

分布式事务

分布式事务是指事务的参与者支持事务的服务器资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。

随着微服务架构的普及,大型业务域往往包含很多服务,一个业务流程需要由多个服务共同参与完成。在特定的业务场景中,需要保障多个服务之间的数据一致性。

例如在大型电商系统中,下单接口通常会扣减库存、减去优惠、生成订单 id, 而订单服务与库存、优惠、订单 id 都是不同的服务,下单接口的成功与否,不仅取决于本地的 db 操作,而且依赖第三方系统的结果,这时候分布式事务就保证这些操作要么全部成功,要么全部失败。

所以本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

使用场景

在电商系统中,典型的使用场景有:

  • 下单扣减库存

    下单时,需要的操作有生成订单记录,扣减商品库存等操作。两者同上是独立的微服务,所以严格来说,需要分布式事务保证下单操作的原子性。

  • 第三方支付

    微服务架构下,支付与订单都是独立的服务。订单的支付状态依赖于财经服务的通知。财经服务又依赖于第三方支付服务的通知。

    一个经典的场景如图:

    https://xiaomi-info.github.io/2020/01/02/distributed-transaction/notify-message.png

从图中可以看出有两次调用,第三方支付调用支付服务,以及支付服务调用订单服务,这两步调用都可能出现调用超时的情况,此处如果没有分布式事务的保证,就会出现用户订单实际支付情况与最终用户看到的订单支付情况不一致的情况。

实现方案

两阶段提交

https://i.loli.net/2021/05/19/MfWzxseBFKaAnhk.png

一次事务的提交主要分为两个阶段:

  1. 准备阶段:

    • TM开始事务,记录事务开始的日志,并向参与事务的RM询问是否能够执行提交操作,并等待RM响应
    • RM执行本地事务,记录Redo/Undo日志,向TM返回执行结果,但不提交事务
  2. 提交/回滚阶段

    ( 1 )如果所有参与的RM执行成功,进入提交阶段

    • TM记录事务提交日志,并向所有RM发送提交事务指令
    • RM收到指令后,提交本地事务,向TM返回执行结果
    • TM记录事务结束

    ( 2 )如果准备或提交任一RM执行失败或者超时

    • TM记录回滚记录,并向所有RM发送回滚指令
    • RM回滚本地事务,向TM返回结果
    • TM记录事务结束

    特性

    • 原子性:支持
    • 一致性:强一致
    • 隔离性:支持
    • 持久性:支持

    弊端

    • 同步阻塞:当参与事务者存在占用公共资源的情况,其中一个占用了资源,其他事务参与者就只能阻塞等待资源释放,处于阻塞状态。
    • 单点故障:一旦事务管理器出现故障,整个系统不可用
    • 数据不一致:如果事务管理器只发送了部分 commit 消息,此时网络发生异常,那么只有部分参与者接收到 commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
    • 不确定性:当事务管理器发送 commit 之后,并且此时只有一个参与者收到了 commit,那么当该参与者与事务管理器同时宕机之后,重新选举的事务管理器无法确定该条消息是否提交成功。

    本地消息表

    事务发起方维护一张本地消息表,业务表与本地消息表的操作处于同一个本地事务内,通过异步的定时任务扫描消息表并投递到下游。

    广义的本地消息表方案中,下游通知方式并不局限于消息投递,也可以通过RPC调用等方式通知。

    https://i.loli.net/2021/05/19/tmNeiALsdof24PW.png

    1. 事务发起者执行本地事务,同时操作业务表和本地消息表
    2. 定时任务定时扫描待发送的本地消息(本地消息表中),将其发送到消息队列
      • 发送成功,则将本地消息标记为已发送
      • 发送失败,则重试直至成功
    3. 消息队列投递消息至下游
    4. 下游事务参与者收到消息后,执行本地事务
      • 本地事务执行失败,不返回ACK,消息队列重复投递消息
      • 本地事务执行成功,则向消息队列返回ACK,全局事务结束
      • 消息或ACK丢失,消息队列重复投递消息

    异常场景

    • 消息发送丢失,通过定时任务重复发送解决
    • 投递到下游的消息丢失,通过重复投递机制解决,需保障下游操作幂等
    • 下游回复的ACK丢失,通过重复投递机制解决,需保障下游操作幂等

    优点与问题

    优点:

    • 系统吞吐量高,通过消息中间件解耦,下游事务异步化
    • 业务侵入度适中,需要实现本地消息表和定时任务

    问题:

    • 事务支持不完备,不接受下游分支事务的回滚,只能重试

    特性

    • 原子性:支持
    • 一致性:最终一致
    • 隔离性:不支持(分支事务提交之后对其它事务可见)
    • 持久性:支持

    尽最大努力通知

    最大努力通知是最简单的一种柔性事务,适用于一些最终一致性时间敏感度低的业务,且被动方处理结果 不影响主动方的处理结果。

    这个方案的大致意思就是:

    1. 系统 A 本地事务执行完之后,发送个消息到 MQ;
    2. 这里会有个专门消费 MQ 的服务,这个服务会消费 MQ 并调用系统 B 的接口;
    3. 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B, 反复 N 次,最后还是不行就放弃。

    优点与问题

    优点:

    • 实现简单

    问题:

    • 无补偿机制,不保证送达
    • 幂等要求,需要提供接口保证一致性与原子性,系统无法保证

    特性

    • 原子性:不支持(需要额外接口保证)
    • 一致性:不支持(需要额外接口保证)
    • 隔离性:不支持(分支事务提交之后对其它事务可见)
    • 持久性:支持

    经典场景

    支付回调:

    支付服务收到第三方服务支付成功通知后,先更新自己库中订单支付状态,然后同步通知订单服务支付成功。如果此次同步通知失败,会通过异步脚步不断重试地调用订单服务的接口。

    https://xiaomi-info.github.io/2020/01/02/distributed-transaction/try-best-notify.jpg

参考

分布式事务,这一篇就够了