本篇文章会尽力讲清楚的问题有:
1.什么叫分布式系统,分布式系统和集群的区别,分布式系统的演变?
2.什么是分布式事务,为什么要分布式事务,什么场景需要分布式事务?
3.一致性模型有哪些类型?
4.如何根据业务选择分布式事务方案?
其余非核心问题:如何划分微服务?
1.分布式系统和集群
分布式系统是将单体系统,根据模块拆分开来,不同系统有不同的业务逻辑,在多台不同的服务器中部署不同的服务模块,通过远程调用协同工作,对外提供服务。
和分布式的反义词就是集中式,相当于单体项目,所有功能都在一个系统内。
集群:是指在多台不同的服务器中部署相同应用或服务模块,构成一个集群,通过负载均衡设备对外提供服务。
2.分布式事务
个人觉得知道为什么要用分布式事务比使用分布式事务更重要。
先看这个经典场景:
分布式电商系统中,有两个服务,分别是订单服务和库存服务。
用户下单流程可以分为:
用户发起订单接口–>
创建订单(A)[订单服务,订单数据库],
扣减库存(B)[库存服务,库存数据库],
更新订单状态(C)[订单服务,订单数据库]
–>用户客户端响应订单创建结果
这个过程后端分为了三个阶段。
每一个阶段都有可能失败,但是某些阶段失败,会让事情变得难以处理。
比如在A成功,B失败时,此处的失败可以是B系统返回的业务失败,也有可能是本身网络的失败。
假设网络原因,B调用超时,此时订单服务应该怎么处理?
- 继续更新订单状态,但是如果库存扣减失败了,此时就是超卖。
- 不更新订单状态,但是如果库存扣减成功,此时就是少卖。
有人说,再去查一遍库存不就好了,问题是此时再去查库存,这个数据不准确,因为库存系统可能还在处理中。
以上两个处理方式,无论是哪种处理,都是无法接受的。
又有人说,那复制一份库存到订单库中不就好了,当成本地事务去做,事务成功全部成功,事务失败全部失败。
没错,这样看似数据一致了,不会有超卖,少卖的情况,但是最新的数据依然在库存库中。
假设此时库存由于某些原因需要添加扣减库存,例如业务要求这个sku卖得很好,我仓库还有货,给我马上再加点库存,你怎么确保订单的库存修改成功呢?
同样的流程
- 库存服务创建本地事务
- 新增或者减少库存(A)[库存服务,库存数据库],
- 发送rpc请求同步到订单库中(B)[订单服务,订单数据库],
- 提交事务(C)[库存服务,库存数据库]
同理在上述阶段,
第三步失败,无论怎么处理,依然会造成少卖或者超卖现象。
由此可见,在分布式系统中,想要保证数据一致,需要另外的方法–也就是分布式事务。
3.一致性模型
强一致性:
每个客户端的读操作都返回最近一次写操作的结果。也就是说客户端再任意时刻,看到的数据都是一致的。
适合在金融交易系统,银行系统中处理,如银行账户余额、交易处理等,要求每个操作都必须看到最新的数据,确保数据准确性。
弱一致性:
允许不同的客户端存在一定程度的数据不一致。
适用场景:
- 缓存系统:例如 Memcached,允许数据在缓存和后端存储之间保持一定的延迟一致性。
- 日志系统:记录日志的系统可能允许日志在不同节点之间有一定的延迟。
开源中间件:
- Memcached:一个分布式缓存系统,主要关注性能,允许缓存和底层存储之间存在数据不一致。
- **Redis (使用集群模式)**:在 Redis 集群模式下,由于主从复制机制的延迟,可能会出现弱一致性问题。
最终一致性:
最终一致性是一种弱一致性模型的特殊形式,它保证在没有新的更新操作的情况下,系统中的所有副本最终将达成一致状态。
特点:
- 短期不一致:系统可能在短期内处于不一致状态,但最终会一致。
- 高可用性:允许系统在部分副本不可用时继续操作。
适用场景:
- 社交媒体:如点赞、评论等,系统允许短暂的不一致,但最终所有副本会同步一致。
- 分布式数据库:适合对最终一致性要求较高的系统,能够容忍一定的延迟。
开源中间件:
- Cassandra:一个分布式 NoSQL 数据库,设计为最终一致性,支持高可用性和扩展性。
- DynamoDB:Amazon 提供的分布式数据库,允许配置一致性模型,包括最终一致性。
- Riak:另一个分布式 NoSQL 数据库,采用最终一致性模型,优化了高可用性。
4.分布式事务的类别和选型
根据上述的一致性模型,我们可以选择对应的分布式事务类型,
- 强一致:2PC,3PC
- 最终一致:TCC,本地消息表,Saga
- 弱一致性:最大努力通知
2PC:
角色有协调者和参与者。
sequenceDiagram participant C as 协调者 participant P1 as 参与者1 participant P2 as 参与者2 C->>P1: 发送准备请求 C->>P2: 发送准备请求 P1-->>C: 回复准备就绪(本地事务写Undo/Redo日志)或中止 P2-->>C: 回复准备就绪(本地事务写Undo/Redo日志)或中止

流程:
- 准备阶段(Prepare Phase):事务管理器(TM)询问所有参与者(如数据库)是否准备好提交事务。参与者执行事务操作,写Undo/Redo日志,但不会立即提交。参与者回复yes or no。
- 提交阶段(Commit Phase):如果所有参与者都准备好了,事务管理器将通知它们提交事务;如果任何一个参与者未准备好,事务管理器将通知所有参与者回滚事务。在这个阶段,参与者会根据事务管理器的指令执行提交或回滚操作,并释放事务过程中使用的锁资源。
sequenceDiagram participant C as 协调者 participant P1 as 参与者1 participant P2 as 参与者2 alt 所有参与者准备就绪 C->>P1: 发送提交请求 C->>P2: 发送提交请求 P1-->>C: 确认提交完成,本地事务commit P2-->>C: 确认提交完成,本地事务commit else 任一参与者未准备就绪 C->>P1: 发送回滚请求 C->>P2: 发送回滚请求 P1-->>C: 确认回滚完成,本地事务执行undo log P2-->>C: 确认回滚完成, 本地事务执行undo log end

2PC比较明显的问题:
1.协调者单点问题:TM崩溃,整个事务无法运行下去。
2.阻塞问题:
- 参与者阻塞
- 参与者在prepare阶段会锁定资源,等二阶段才会释放,这过程中,其他事物无法获得锁定资源,导致阻塞问题在系统中传播,造成连锁阻塞。
- 并且由于参与者不知道协调者的决策,在没收到TM的消息前,会永久锁定资源。
- 此时也没办法自主回滚或者提交,否则会数据不一致,破坏了2PC的原子性。
- 协调者阻塞
3PC:
角色分为协调者和参与者。
%%{init: { "sequence": { "width": 300, "height": 400, "messageAlign": "center" } } }%% sequenceDiagram participant C as 协调者 participant P1 as 参与者1 participant P2 as 参与者2 Note over C,P2: 阶段1:Prepare C->>P1: Prepare? C->>P2: Prepare? P1-->>C: Yes P2-->>C: Yes Note over C,P2: 阶段2:PreCommit C->>P1: PreCommit C->>P2: PreCommit P1-->>C: ACK,本地事务记录undo log redo log P2-->>C: ACK,本地事务记录undo log redo log Note over C,P2: 阶段3:Commit C->>P1: Commit C->>P2: Commit P1-->>C: 已完成 P2-->>C: 已完成 Note over C,P2: 事务完成
流程:
由图可知三阶段其实就是多了一个预先提交阶段。
阶段1:Prepare
- 协调者向所有参与者发送 Prepare 请求,询问是否可以执行事务。
- 参与者检查自身状态,如果可以执行事务,回复 Yes;否则回复 No。
- 在这个例子中,两个参与者都回复了 Yes。
阶段2:PreCommit
- 如果所有参与者在第一阶段都回复 Yes,协调者发送 PreCommit 请求。
- 参与者收到 PreCommit 后,执行事务但不提交,将结果写入 undo 和 redo 日志。
- 参与者完成准备工作后,回复 ACK(确认)。
阶段3:Commit
- 协调者收到所有参与者的 ACK 后,发送 Commit 请求。
- 参与者收到 Commit 后,正式完成事务提交。
- 参与者完成提交后,向协调者发送”已完成”消息。
不同阶段网络分区情况:
sequenceDiagram participant C as 协调者 participant P1 as 参与者组1 participant P2 as 参与者组2 C->>P1: 发送Prepare C->>P2: 发送Prepare P1-->>C: 准备就绪 Note over C,P2: 网络分区发生 Note over C,P2: 情况1:如果有参与者处于准备阶段就回滚,否则无事发生
如何解决阻塞问题的:
协调者在Prepare阶段挂掉,重启后检查参与者状态,如果没有参与者处于Prepare阶段;那么无事发生。
如果至少一个参与者,收到了Prepare,处于准备阶段了,所有Prepare参与者会等待超时时间自动回滚。
若协调者在发送完pre-commit请求后发生故障,如果至少有一个参与者处于pre-commit状态,那么所有的参与者都会转到这个状态,然后提交事务。
如果没有参与者处于pre-commit状态,那么调用abort命令。
关键点就在于3pc中各个参与方约定好了在哪个阶段等待超时后做一致的操作,就像一阶段都abort,第二阶段只要有参与者处于pre-commit那么大家都提交,否则回滚。
当然此处各个节点之间依然会发生网络分区,假设a,b,c,d四个节点,a处于pre-commit阶段,启动b,c和a正常通信,知道至少一个处于pre-commit阶段,准备继续commit,但是d网络分区,此时d处于两难阶段。
所以3PC仅容忍单点故障,协议不允许协调者和参与者同时停机或者存在网络分区。
3PC的优缺点:
优点:
1.通过约定阶段执行一致的动作从而解决了协调者单点问题。
2.通过Prepare阶段,降低了资源争抢并互相阻塞的概率,提高了并发性。
缺点:
1.性能开销大,3PC增加了很多网络通信的步骤,流量消耗大。
2.仅容忍单点故障,协议不允许协调者和参与者同时停机或者存在网络分区
TCC:
TCC角色有TC事务协调者,TM事务管理者,RM资源管理者和参与分布式事务的业务服务。
1 | 事务管理者(TM): |
举例:
1 | 在订单,库存,优惠券三个服务中,用户下单,订单服务作为服务发起者, |
TCC一共两个阶段,第一阶段是Try,通常是锁定资源,第二阶段是cancel或者commit。
TCC通常分为三个类型,分别是通用型,异步确保型,补偿性TCC。
1.通用型

1 | 问题一:Try阶段,其中一个调用超时怎么处理? |
适用场景
适用于从业务服务属于同步调用,其结果会影响主业务服务的决策,并且执行时间确定且较短的业务。
如互联网金融企业最核心的三个服务:交易、支付、账务:

当用户发起一笔交易时,首先访问交易服务,创建交易订单;然后交易服务调用支付服务为该交易创建支付订单,执行收款动作,最后支付服务调用账务服务记录账户流水和记账。
为了保证三个服务一起完成一笔交易,要么同时成功,要么同时失败,可以使用通用型 TCC 解决方案,将这三个服务放在一个分布式事务中,交易作为主业务服务,支付作为从业务服务,账务作为支付服务的嵌套从业务服务,由 TCC 模型保证事务的原子性。
交易服务Try:
创建交易单,状态交易待完成。
支付服务Try:
创建支付单,状态待支付。
账户服务Try:
冻结账户对应交易金额。
提交账户本地事务。
提交支付本地事务。
提交交易本地事务。
在递归提交本地事务的过程,会有很短暂的不一致,这个影响吗?
二阶段TCC框架调服务的confirm接口:
交易服务confirm:
支付服务confirm:
账户服务confirm:
扣除买家冻结资金;增加卖家可用资金。
提交账户本地事务。
支付服务修改支付订单为完成状态,完成支付
提交支付本地事务。
提交交易本地事务。
整个分布式事务结束。
TCC优点:可以灵活选择业务资源的锁定粒度,减少资源锁持有时间,可扩展性好。
TCC 另一个作用就是把两阶段拆分成了两个独立的阶段,通过资源业务锁定的方式进行关联。资源业务锁定方式的好处在于,既不会阻塞其他事务在第一阶段对于相同资源的继续使用,也不会影响本事务第二阶段的正确执行,这个模型相比Seata的AT模式并发更高。

只有在prepare阶段会阻塞,其他阶段都是并发。
2.异步确保型

顾名思义,异步确保型就是有一方从业务是消息服务,利用消息的可靠性和异步性,保证订阅消息的从业务的二阶段的执行。
和通用型差不多–Try阶段,消息服务,创建在数据中创建一条待发送的消息记录,同样注意防悬挂和空回滚。
confirm或者cancel阶段,将记录状态设置为允许发送或者取消和删除记录。
消息服务中定时任务定期扫描没有发送完成或者取消的消息记录。从服务做好幂等即可。
优点:
1.从服务接入成本低,订阅消息记录,消息服务改造成本比从服务低。
2.消息数据独立存储,独立伸缩,降低从服务之间的耦合度。
缺点:
由于是异步,实时性相比通用性更差点。
适用场景
由于从服务执行时间不定,适合用最终一致性时间敏感度较低的,在服务处理结果不影响主服务决策的场景(只被动的接收主业务服务的决策结果)。
例如会员注册和赠送积分。
会员注册成功,一定赠送积分。
会员注册失败,一定不赠送积分。
积分只会被动接受会员服务的决策,且最终一致性时间敏感度不高。

3.补偿型

在补偿性的TCC事务中,从服务只需要提供两个接口,Do 和 Compensate。
Do 接口直接执行真正的完整业务逻辑,完成业务处理,业务执行结果外部可见;
Compensate 操作用于业务补偿,抵消或部分抵消正向业务操作的业务结果,Compensate操作需满足幂等性。
优点:
从服务改造成本低,在原有Do的业务逻辑上,单独写一个补偿接口。
缺点:
由于Do完成的是整个业务逻辑,做不到事务隔离。并且补偿接口也可能失败。超异常次数时需要人工介入。
适用场景:
适合那些无法做try,cancel,comfirm接口的服务或者系统和外部系统,且从服务的成功失败影响主服务的决策,且并发冲突小的场景,因为一旦发生并发,数据不一致的案例相比其他TCC会变多。
例子:
A机票订购服务,B航空公司网关,C航空公司网关。
A需要同时在B,C中购买联乘机票。单独其中一班意义不大。

- 本文作者: 宏
- 本文链接: http://sasuke.top/2024/09/10/分布式事务优缺点/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!