---
title: 微服务架构本地尝试(七)-分布式事务
---

> 来了解一下分布式事务,事务简单来说就是:"要么什么都不做,要么做全套"

## 本地事务

平常我们的事务都是在同一个数据库中执行.

举个栗子:当你发帖的时候,需要发表帖子存入数据库,然后再给这个发帖的用户增加积分.如果帖子存入数据库失败,那么就不再往下执行;但是你增加积分失败了,然而你的帖子已经发布出去了,总不可能再去删除刚刚发的那条帖子吧.如果你使用了事务的话,那么可以在一项失败的时候,回滚事务,一切就像没有发生一样.

### ACID 模型

说到事务就不得不说 A(原子性)C(一致性)I(隔离性)D(持久性),来贴一下百科说明.

- Atomicity（原子性）：一个事务（transaction）中的所有操作，或者全部完成，或者全部不完成，不会结束在中间某个环节。事务在执行过程中发生错误，会被恢复（Rollback）到事务开始前的状态，就像这个事务从来没有执行过一样。即，事务不可分割、不可约简。
- Consistency（一致性）：在事务开始之前和事务结束以后，数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
- Isolation（隔离性）：数据库允许多个并发事务同时对其数据进行读写和修改的能力，隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别，包括读未提交（Read uncommitted）、读提交（read committed）、可重复读（repeatable read）和串行化（Serializable）。
- Durability（持久性）：事务处理结束后，对数据的修改就是永久的，即便系统故障也不会丢失。

## 分布式事务

像上面一样的场景,但是这个时候我们是微服务架构啦.帖子数据库和积分数据库不在一起,他们分别在两个不同的数据库上面.

如果发帖失败,我们可以选择不再往下走,增加积分(我就当发帖先执行,积分增加后执行).但是如果发帖成功了,但是积分增加失败,这样就导致了数据的不一致.这种情况还算好,但是如果订单下成功了,钱没扣,造成的损失就大了不是.

于是我们需要通过一些手段,实现不同数据库之间的事务,这就是分布式事务了.

还有一些分库分表的应用也会用到分布式事务.

### CAP 定理

CAP 定理指的是在分布式**系统**中最多只能满足 C,A,P 中的两个需求.

#### 一致性(Consistency)

一致性指的在不同的服务器中,数据都是一致的.在分库分表的环境下,这个一致指的是 A 库中的 A 表和 B 库中的 A 表里面的数据是一模一样的;在我们上面所说的场景中指的是,发帖应得到的积分,和我们实际的积分数量应该是一致的.(为什么说这些,因为我感觉好多地方说的一致都是指的前者,我这个理解应该没错吧 hhh)

一致性也可以分为下面 3 类

- 强一致性:执行完成后,后面不管什么时候读取,数据都是更新完毕后的.比如发完贴之后,我立刻就可以看到我的积分增加.
- 弱一致性:执行完成后,不能保证数据会更新,多久会更新.比如发完贴之后,过了很久很久,我的积分都不一定变化了.(感觉是不是失败了...可能是某些场景下的吧)
- 最终一致性:执行完后,一段时间内,数据更新.比如发帖完之后,过了一两秒,我就能看到我的积分变化了.这个是弱一致性的一种特殊情况,大多数分布式系统也是采用这种模式.

#### 可用性(Availability)

每一次请求,都能得到服务器的非错响应.

可用性好理解,就是系统能否使用呗.能够访问帖子数据也能够访问积分数据库,这就算可用了,但是中间一项宕机那么这个系统就是不可用了(因为完成不了这个操作).

#### 分区容错性(Partition tolerance)

因为网络或者机器故障等影响,数据可能无法送达,还要保证系统能够继续正常运行,这时候我们需要在 CA 中做出选择.

又拿我上面的场景举个栗子:

我发帖服务器的数据无法传达给积分数据库.

如果我选择 A 可用性,那就是不理会积分数据库宕机,就会导致 C 不一致;如果我选择 C 一致性,那就回滚事务,不能够发帖成功,A 不可用.

如果是成年人我全都要

![](img/07-%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1.assets/bqb_4.gif)

我又要可用,又要一致.那我们基本就是是单机系统了,没有了 P,也就没有了网络因素,然后因为单机爆炸,一无所有.

分布式系统的话,因为无法 100%保证数据是否到达,必定会出现分区现象,当一旦出现故障,那么就必须选择 P,如果不选的话间接造成 CA 错误(数据不回滚,还特么报错),一无所有,所以按照 CAP 理论来说,分布式 P 是必选项.

### BASE 模型

BASE 模型是对 CAP 中 AP 的一个扩展.

- Basically Available 基本可用,出现故障时,允许损失部分可用的功能,保证核心功能可用.
- Soft state 软状态,允许系统中存在中间状态,这个状态不影响系统可用性,这里指的是 CAP 中的不一致.
- Eventually consistent 最终一致,一段时间后数据达到一致.

上一节中的 CQRS 和 EDA 就是属于这种模型,面向最终一致性.

## 分布式事务解决方案

大多数情况下,我们还是遵循不用分布式事务就不用分布式事务.这里是因为我们的微服务引出分布式事务.

下面列出一些分布式事务的解决方案.

### CQRS/EDA

先来说说我们上一节所用到的 CQRS,这种架构就是依赖 MQ 来保证的最终一致性

在本地开启一个事务,然后通过 MQ 发送一个消息,消息发送成功,完成事务,消息发送失败回滚事务.消息发送成功后就不用再考虑我们的发帖服务了,直接反馈给用户成功.

然后再通过 MQ 将消息发送到积分服务,如果这时候积分服务是不可用的,那么这条消息就会存储在 MQ 中,当积分服务复活时,再继续吧消息推送给积分服务,直到积分服务告诉 MQ 成功了为止,达到最终一致.

这个架构基本符合上面的 BASE 模型.

### 2PC(两阶段提交)

顾名思义就是两个阶段 0.0,感觉有点像五大流氓的投票,主流数据库也有相关的实现.

首先得有一个协调者(coordinator),若干参与者(participant)

#### 第一阶段

协调者:我们来商量一件事情

- 参与者 1:收到
- 参与者 2:收到
- 参与者 3:收到

如果有一个参与者没有收到,表示不能提交,不再继续执行.

#### 第二阶段

协调者:好了,都商量好了,执行吧

- 参与者 1:收到
- 参与者 2:收到
- 参与者 3:收到

如果有一个参与者,不干(执行失败之类的),那么协调者就会通知其他参与者算了不干了(回滚)

这个方案虽然简单,主流数据库也有相关实现,但是因为单个协调者,同步堵塞的问题不太适合高并发的场景.

### TCC(Try-Confirm-Cancel)

- Try:对所有业务进行检查,并预留必须业务资源
- Confirm:执行业务,不做用进行检查,只能使用 Try 阶段预留的业务资源.
- Cancel:取消业务执行,回滚事务.

Confirm 和 Cancel 的操作要求幂等(不管执行多少次,结果都是一样的)

假设 A 花 3 元买一瓶快乐水

Try 阶段:先检查 A 是否有足够的钱,水是否有足够的货;然后锁住 A 的金额和水的库存.如果全部成功进入 confirm 阶段,否则 cancel 阶段

Confirm 阶段:减去 A 中的钱和水的库存(只操作 Try 预留的业务资源);如果执行失败,继续进行 Confirm(所以要求幂等);直到执行成功为止.

Cancel 阶段:取消锁,或者回滚操作;如果执行失败也继续重试 Cancel,直到成功为止.

Confirm 和 Cancel,应该是可以异步的,适合于一些要求隔离性高,一致性强的业务.

### Saga

Saga 将各个事务拆分出来变成若干小事务$T_i$,每一个小事务都对应着一个回滚操作$C_i$.其中每一个操作也要求是幂等,而且$C_i$要求肯定是成功.

Saga 的执行顺序也很好理解:

成功顺序:$T_1>T_2>T_3>...T_n$

失败顺序:$T_1>T_2>...T_i>C_i>C_(i-1)...>C_2>C_1$

Saga 也可以不需要取消操作,但是要求 T 最终是成功的,失败重试,一直到成功为止.

对比 TCC 来说,Saga 的隔离性没有 TCC 的强,而且撤销操作每一步都需要一个 C(虽然不需要可以重试),感觉上是 TCC 更好,但是既然存在肯定是有它所使用的场景的.

如果再拿上面的场景举例子,就变成了下面这样:

成功:A 扣钱->水减货

失败:A 扣钱->水减货失败->回滚水减货->回滚 A 扣钱

感觉更适合有多步操作,而不需要立刻到达最终状态的场景

例如购买商品:

| 事务     | 取消     |
| -------- | -------- |
| 发起订单 | 取消订单 |
| 付款     | 退款     |
| 发货     | 回库     |
| 收货     | 退货     |
| 完成     | 打款     |
