前言
分布式事务在实际的开发实践中是一个极其重要的概念,需要我们充分的认识,才能在合适的场景下充分的利用分布式事务,同时避免因为不理解导致不必要的生产故障。
技术目标
数据的一致性,通过系统设计保证在任意环境下数据写入不出现不可预知的问题。具体要求如下:
-
数据准确性要求
- 不允许漏泄
- 不允许错写
- 有顺序要求的不允许顺序错乱
-
时效性要求
- 处理时间尽可能短
-
性能要求
- 数据库操作(增删改查)QPS不宜过高
如果达不到上述要求,可能要面临问题:
- 数据不可追溯
- 脏数据
- 严重影响运价准确性。
- 发现周期长
- 每小时会有一个任务需要处理,假设在8月1日13点处理有问题,但是并未被发现,到了一个星期后的8月8日才发现
- 数据刷新时间长
- 无法只处理其中一个批次就可以完成数据的清洗,需要将全部数据从一份全量重新计算才可以。
- 不可预知性
- 不知道系统是否准确,在演练期间,需要停机维护。
分布式事务
在讨论如何实现以上需求目标前,有必要首先定义分布式事务是什么,而在讨论分布式事务前首先需要明确的是事务是什么。
事务
事务将应用程序的多个读、写操作捆绑在一起成为一个逻辑操作单元。即事务中所有读写是一个执行的整体,整个事务要么成功(提交)、要么失败(中止或回滚)。如果失败,应用程序可以安全地重试。
基于事务,不再需要担心部分失败的情况,应用层的错误处理就变得简单很多。
对于分布式事务大体上有如下两种概念:
数据库内部的分布式事务
某些分布式数据库支持跨数据库节点的内部事务,同时,所有参与节点都运行着相同的数据库软件。
软件系统包括国内的TiDB,Oracle和MySQL也号称支持分布式事务。
异构分布式事务
在异构分布式事务中,存在两种或两种以上不同的参与者实现技术,例如来自不同的供应商数据库,又比如消息中间件等非数据库系统。
异构分布式事务要求即使是完全不同的系统,跨系统的分布式事务也必须确保原子提交。
经典理论
经典理论是后续实践的理论基础,可以更好的指导系统的设计工作。
经典理论也从一个侧面说明,各种系统设计都是在不断的在各个关键指标或关键维度上不断权衡得出的相对最有结果。
ACID
首先要介绍的最经典理论是在事务处理中被大家广泛熟悉的ACID理论,指数据库管理系统(DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:
- 原子性(atomicity,或称不可分割性)
- 一致性(consistency)
- 隔离性(isolation,又称独立性)
- 持久性(durability)
各种DBMS都在想尽一切办法去满足以上四个特征,但是在实际上,每种DB对于ACID的实现又都不尽相同,各有各的侧重点。
CAP
CAP最初是作为一个经验法则提出来的,目的是为了帮助大家在探讨数据库设计的权衡之道。
一般意义的CAP理论指的是分布式系统不可能同时满足CAP三个条件,往往在C和A之间进行取舍。
- 一致性(C:Consistency)
- 可用性(A:Availability)
- 分区容忍性(P:Partition Tolerance)
这里存在一个问题,为什么说往往在C和A之间取舍呢?
- 因为网络分区是一种故障,对于高可靠的网络会减少分区故障的发生概率,但无法做到彻底避免,所以绝大多数情况下无法逃避甚至选择分区的问题。
BASE
对于无法做到ACID标准或者说不符合ACID标准的系统,有时也会用BASE来说明。
具体含义包括以下三点:
- 基本可用(Basically Available)
- 软状态(Soft State)
- 最终一致性(Eventually Consistent)
BASE理论同时也是对CAP理论的延伸,核心思想是即使无法做到强一致性(Strong Consistency,CAP的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性(Eventual Consitency)。BASE支持的是大型分布式系统,提出通过牺牲强一致性获得高可用性。
ACID和BASE代表了两种截然相反的设计哲学,也并不存在谁优谁劣的问题。
可线性化
可线性化这个概念并不常被提及,但是在实践中却无处不在。
一致性算法
两阶段提交(2PC)
两阶段提交(Two-phase Commit,2PC)是一种在多节点之间实现事务原子性提交的算法,用来确保所有节点要么全部提交,要么全部中止。通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。
- 准备阶段,协调者询问参与者事务是否执行成功,参与者发回事务执行结果。询问可以看成一种投票,需要参与者都同意才能执行。
- 提交阶段,如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
具体执行过程如下图:
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。 两阶段提交可能会存在问题:同步阻塞,单点问题,数据不一致,容错不完善。
容错共识算法
首先,共识是让几个节点就某一项提议达成一致。通常意义的共识算法需要满足如下性质:
- 协商一致性(Uniform agreement),所有的节点都接受相同的决议
- 诚实性(Integrity),所有节点不能反悔,即对一项提议不能有两次决定。
- 合法性(Validity),如果决定了V,则V一定是由某个节点所提议的。
- 可终止性(Termination),节点如何不崩溃,则最终一定可以达成决议。
通常意义讲的分布式一致性算法大多特指的容错共识算法,比较著名的算法包括VSR、Paxos、Raft、Zab、Gossip等等。
- 以上算法除了共识算法的特质外,还叠加了全序关系广播算法,将消息按照相同的顺序发送到所有节点,有且只有一次。
- 同时基于以上算法也有很多开源的实现,最为人所熟知的应该就是ZooKeeper。etcd和Google的Chubby同样著名。
PS:关于各种算法的具体实现,如有兴趣可以点击连接详细了解。
实施方案
为实现业务目标和技术目标,需要借助于经典理论和一致性算法设计一套数据处理的方案,这个章节会进行详细介绍。
灾难场景
首先,讨论下为满足业务场景的同时,需要考虑或者说可能的灾难场景
- 进程崩溃
- 网络中断
- 网络延迟
- 硬盘故障
- 磁盘写满
- 数据库阻塞
- 数据不完整
更常见的是的灾难演练。
以上灾难场景都应该在系统设计时考量的切入点,其中某些场景同时也会被DBMS托管。
整体架构
可以通过将数据拆分到具体的可以保证事务的任务,已达到数据处理的事务一致性的目标。如果整体任务混为一谈,由于事务过大,极不利于事务的执行和维护。
单次提交数据级别
在以上的执行中最小单位是Task,每个Task需要考虑是否可以保证事务。
对于MySQL的innodb引擎,事务仅受redo-log-size限制,因此,如果提交非常大的事务,仅需要确保设置innodb_log_file_size即可。
LS是innodb_log_file_size的缩写,MySQL理论的ACID保证数据量为innodb_log_file_size。
X是本地消息表任务是否完成的标记
O(英文字母)表示最终任务是否完成,用于判定最终数据一致性
本地消息表
本方法最先在ebay中引入,通过在业务库中建议独立的消息表,充分利用数据库的ACID特性保证最终分布式事务的一致性。
- 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
- 之后将本地消息表中的消息转发到消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
- 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
顺序保证
在每个执行计划中的事务任务,任务之间可能是相互之间有先后依赖的串行关系,也可能是无任何关系的并行关系。
下图是一个简单的任务关系图演示任务之间的相互依赖关系,但是实际的任务关系远比这个图要复杂的多得多,任务的顺序保证在构建执行计划是通过拓扑排序校验决定是否可以实际执行。
最终一致性
由于任务之间的依赖关系以及每个节点的任务特点,无法保证每时每刻的任务都能够按照线性化执行,因此需要外部一个额外的管理系统可以保证数据的最终一致性。
最大努力通知
系统设计完成后,虽然考虑了各方面的可能情况,在实际的测试和演练中也都尽可能的验证了系统的可靠性,但是由于分布式事务的特点,仍然无法保证万无一失。
因此,最大努力通知的概念被提了出来,当系统发生不可预知的问题时,要求系统可以做到尽系统本身最大能力的将系统的问题通知到消费者和生产者,以便于能够即时的介入排查问题。
后记
对于数据操作服务来讲,复杂的设计通常很难让人理解,很多人会问,直接写入数据库然后直接读取也可以实现同样的业务目标,为什么要设计如此复杂的系统?
我的回答是,对于数据方面的无小事,任何一条数据的不一致都有可能造成大面积的生产故障,这个和最常见的查询服务有着巨大的差异,查询服务而言可能在极特殊的情况下才会影响到极少量的查询结果,但是数据服务一旦一条数据出现错误,它的影响面会在后续的应用中被无限放大,进而造成不可预估的灾难。因此,我们有必要在系统设计之初就考虑的尽可能多的灾难场景,并通过各种容错等技术手段尽可能避免数据的错误。