后端技术扫盲篇一 分布式事务常见的一些处理方式

后端技术扫盲篇一 分布式事务常见的一些处理方式

做了两年多开发,写一个系列,记录一下后端开发上的一些常见概念,给自己扫盲

一些基本概念

1. 事务

事务具有四个基本特性:ACID

  • Atomic 原子性 一个事务内的所有复杂操作,要么全部成功,要么全部失败
  • Consistency 一致性 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。
  • isolation 隔离性 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(Read uncommitted)、提交读(read committed)、可重复读(repeatable read)和串行化(Serializable)。
  • durability 持久性 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

2. 分布式事务

分布式事务是指后端服务中,一个事务的参与者分布在分布式系统不同的节点上,一个操作涉及的资源不仅仅存在于本地的数据库中,还需要依赖第三方服务在其他数据库上的操作。我们希望分布式事务可以保障在不同数据库上操作的服务要么同时成功,要么同时失败。

3. 分布式系统的CAP定理

CAP原则是指在一个分布式系统里,数据如果要完全一致,有三个原则,分别是:

  • Consistency 一致性 所有节点上的服务可以访问同一份最新的值
  • avaliable 可用性 分布式系统中的一部分节点故障后,系统依然可以响应用户的请求(高可用性)
  • partition tolerance 分区容错性

一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中。这就叫分区。当你一个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这时分区就是无法容忍的。提高分区容忍性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里。容忍性就提高了。

CAP定理所描述的三个原则,在理想的情况下,我们没有办法保证它们完全被实现,但是不代表在现实的场景里,不会出现都满足CAP原则的情况。比如分布式系统因为网络问题出现分区不容错的情况比较少,那么在没有出现网络问题的时候,这个分布式系统就有可能是符合CAP原则的。只是无法应对一些极端情况。
所以不是说CAP原则我们只能保证两个,而是说,我们应该根据系统实际的特点,选择放宽某一类极端情况的处理,尽可能保证别的原则。这里可以理解这三个原则是范围属性,而不是布尔值属性。

一些取舍的场景:
CA without P:如果不要求P(不允许分区),则C(强一致性)和A(可用性)是可以保证的。但放弃P的同时也就意味着放弃了系统的扩展性,也就是分布式节点受限,没办法部署子节点,这是违背分布式系统设计的初衷的。传统的关系型数据库RDBMS:Oracle、MySQL就是CA。

CP without A:如果不要求A(可用),相当于每个请求都需要在服务器之间保持强一致,而P(分区)会导致同步时间无限延长(也就是等待数据同步完才能正常访问服务),一旦发生网络故障或者消息丢失等情况,就要牺牲用户的体验,等待所有数据全部一致了之后再让用户访问系统。设计成CP的系统其实不少,最典型的就是分布式数据库,如Redis、HBase等。对于这些分布式数据库来说,数据的一致性是最基本的要求,因为如果连这个标准都达不到,那么直接采用关系型数据库就好,没必要再浪费资源来部署分布式数据库。

AP wihtout C:要高可用并允许分区,则需放弃一致性。一旦分区发生,节点之间可能会失去联系,为了高可用,每个节点只能用本地数据提供服务,而这样会导致全局数据的不一致性。典型的应用就如某米的抢购手机场景,可能前几秒你浏览商品的时候页面提示是有库存的,当你选择完商品准备下单的时候,系统提示你下单失败,商品已售完。这其实就是先在 A(可用性)方面保证系统可以正常的服务,然后在数据的一致性方面做了些牺牲,虽然多少会影响一些用户体验,但也不至于造成用户购物流程的严重阻塞。

对于一个分布式系统,我们必须要允许可扩展性,所以只能在CP和AP里进行选择。

4. 分布式系统中的数据一致性

强一致性&弱一致性

数据一致性其实只有强一致性和弱一致性两个大类。
凡是不属于强一致性的,都是各种各样的弱一致性。
强一致性也叫做线性一致性,除此以外,所有其他的一致性都是弱一致性的特殊情况。所谓强一致性,即复制是同步的,弱一致性,即复制是异步的。

最终一致性

当用户从异步从库读取时,如果此异步从库落后,他可能会看到过时的信息。这种不一致只是一个暂时的状态——如果等待一段时间,从库最终会赶上并与主库保持一致。这称为最终一致性。

读写一致性

读写一致性,也称为读己之写一致性。它可以保证,如果用户刷新页面,他们总会看到自己刚提交的任何更新。它不会对其他用户的写入做出承诺,其他用户的更新可能稍等才会看到,但它保证用户自己提交的数据能马上被自己看到。
这种情况下 需要确保某些信息的读取必须走主库读取。

单调读

用户从某从库查询到了一条记录,再次刷新后发现此记录不见了,就像遇到时光倒流。如果用户从不同从库进行多次读取,就可能发生这种情况。

单调读可以保证这种异常不会发生。单调读意味着如果一个用户进行多次读取时,绝对不会遇到时光倒流,即如果先前读取到较新的数据,后续读取不会得到更旧的数据。单调读比强一致性更弱,比最终一致性更强。

实现单调读取的一种方式是确保每个用户总是从同一个节点进行读取(不同的用户可以从不同的节点读取),比如可以基于用户ID的哈希值来选择节点,而不是随机选择节点。

因果一致性

分区后,每个节点并不包含全部数据。不同的节点独立运行,因此不存在全局写入顺序。如果用户A提交一个问题,用户B提交了回答。问题写入了节点A,回答写入了节点B。因为同步延迟,发起查询的用户可能会先看到回答,再看到问题。

为了防止这种异常,需要另一种类型的保证:因果一致性。 即如果一系列写入按某个逻辑顺序发生,那么任何人读取这些写入时,会看见它们以正确的逻辑顺序出现。

5. Base理论

BASE理论是Basically Available(基本可用),Soft State(软状态)和Eventually Consistent(最终一致性)三个短语的缩写。

既然无法做到强一致性(Strong consistency),但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性(Eventual consistency)。

基本可用

Basically Available
可以理解就是服务降级。面对一些极端场景,降低一些用户体验,以保障服务的基本可用,比如增加响应的时长,比如取消一些次要服务入口,保证主要服务的可用性。

软状态

soft state
允许中间状态的存在,分布式系统中,某些节点在某些时刻,可以存在中间状态,并且不影响功能的正常使用。

最终一致性

eventually consistent
中间状态不会一直存在,在一定期限后,所有节点上的服务可以访问到最终的相同的期望状态。

分布式事务的常见处理方式

1. 两阶段提交

两阶段提交的思路是把一个事务的处理过程,拆成了两个阶段处理。
首先定义一个分布式系统中,存在两类角色:

  1. 全局事务管理器TM
  2. 局部资源管理器RM
    事务处理的两个阶段:
  3. 准备阶段 prepare: 全局事务管理器确认所有参与者都准备好后,准备就绪
  4. commit rollback阶段 全局事务管理器向所有参与者发布commit命令

如果中间某一个资源管理者失败了,过程会终止并回滚。
两阶段提交事务的特点:

  • 简单好理解,开发模式比较简单
  • 对资源进行长时间的锁定,不利于并发

2. SAGA

Saga是这一篇数据库论文saga提到的一个方案。其核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。

SAGA事务的特点

  • 并发度高
  • 开发量高,需要额外定义正常操作和补偿操作
  • 一致性弱
    SAGA适用的场景较多,适用长事务,对中间结果不敏感的业务场景适用。

两阶段提交和SAGA的实现,go语言可参考DTM,java语言可参考seata

3. TCC

关于 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。

TCC分为3个阶段

  • Try 阶段:尝试执行,完成所有业务检查(一致性), 预留必须业务资源(准隔离性)
  • Confirm 阶段:确认执行真正执行业务,不作任何业务检查,只使用 Try 阶段预留的业务资源,Confirm 操作要求具备幂等设计,Confirm 失败后需要进行重试。
  • Cancel 阶段:取消执行,释放 Try 阶段预留的业务资源。Cancel 阶段的异常和 Confirm 阶段异常处理方案基本上一致,要求满足幂等设计。

TCC特点如下:

  • 并发度较高,无长期资源锁定。
  • 开发量较大,需要提供Try/Confirm/Cancel接口。
  • 一致性较好,不会发生SAGA已扣款最后又转账失败的情况

TCC适用于订单类业务,对中间状态有约束的业务

4. 本地消息表

本地消息表这个方案最初是 ebay 架构师 Dan Pritchett 在 2008 年发表给 ACM 的文章。设计核心是将需要分布式处理的任务通过消息的方式来异步确保执行。

这种处理方式需要和业务层的设计相结合,最终实现的效果是【最终一致性】。
将一个长事务拆分为几个短事务进行处理,步骤:

  1. 业务层接受到请求后,某个服务用事务写本地消息表,期间可以进行一些状态修改操作
  2. 轮训脚本检测本地消息表,并根据表内数据,生产消息,存放到消息队列中
  3. 其他服务消费消息队列中的消息,处理各自的本地数据库,最终在一定的时间范围内,所有数据库的数据达到了最终一致性

需要注意:写本地消息和业务操作放在一个事务里,保证了业务和发消息的原子性,要么他们全都成功,要么全都失败。
容错机制:

  • 步骤1事务 失败时,事务直接回滚,无后续步骤
  • 步骤2轮询生产消息失败, 步骤3事务失败都会进行重试
    适用于可异步执行的业务,且后续操作无需回滚的业务。

总结

分布式系统内,我们讨论的一致性,通常是数据一致性,业务上,涉及到的一致性场景,需要首先辨别,是否能接受【最终一致性】的设定,能接受的话,优先选择【本地事务表】的设计范式。
如果强调同步&实时性,那我们需要进一步考虑选择哪种实现方式,原则上应当避免这种场景,如果实在遇到了,优先选择现有轮子进行处理。

发表评论