相关概念整理
1 事务
1.1 ACID
ACID,指数据库事务正确执行的四个基本要素的缩写。包含:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。一个支持事务(Transaction)的数据库,必须要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易方的要求。
原子性(Atomicity)
整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
就像你买东西要么交钱收货一起都执行,要么要是发不出货,就退钱。
一致性(Consistency)
一个事务可以封装状态改变(除非它是一个只读的)。事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。
也就是说:如果事务是并发多个,系统也必须如同串行事务一样操作。其主要特征是保护性和不变性(Preserving an Invariant),以转账案例为例,假设有五个账户,每个账户余额是100元,那么五个账户总额是500元,如果在这个5个账户之间同时发生多个转账,无论并发多少个,比如在A与B账户之间转账5元,在C与D账户之间转账10元,在B与E之间转账15元,五个账户总额也应该还是500元,这就是保护性和不变性。
隔离性(Isolation)
隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。
打个比方,你买东西这个事情,是不影响其他人的。
持久性(Durability)
在事务完成以后,该事务对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。
打个比方,你买东西的时候需要记录在账本上,即使老板忘记了那也有据可查。
1.2 ACID的局限
数据库事务必须保证ACID,在2PC文章里,探讨了跨数据库事务是如何保证ACID的。
当数据量越来越大的时候,我们会对将大数据库拆分成若干小库,随着数据库数量越来越多,2PC(及XA)就显得有些捉襟见肘了:
- 性能低下,2PC协议是阻塞式的。当协调的数据库越来越多时,性能无法接受。
- 无法水平扩展以提升性能,只能靠垂直扩展(提升硬件)——更快的CPU、更快更大的硬盘、更大更快的内存——但是这样很贵,并且很容易遇到极限。
1.3 事务隔离
在提到隔离性的时候我们提到,在修改同一份数据的情况下,两个事务必须挨个执行以免出现冲突情况。而数据库有四种隔离级别(注意:不是所有数据库支持所有隔离级别)
Isolation Level | Dirty Reads | Non-Repeatable Reads | Phantom Reads |
---|---|---|---|
Read uncommitted | 允许 | 允许 | 允许 |
Read committed | 不允许 | 允许 | 允许 |
Repeatable reads | 不允许 | 不允许 | 允许 |
Serializable | 不允许 | 不允许 | 不允许 |
PS. 大多数数据库的默认隔离级别是Read committed。
来复习一下Dirty reads、Non-repeatable reads、Phantom reads的概念:
- Dirty reads:A事务可以读到B事务还未提交的数据
- Non-repeatable read:A事务读取一行数据,B事务后续修改了这行数据,A事务再次读取这行数据,结果得到的数据不同。
- Phantom reads:A事务通过
SELECT ... WHERE
得到一些行,B事务插入新行或者更新已有的行使得这些行满足A事务的WHERE
条件,A事务再次SELECT ... WHERE
结果比上一次多了一些行。
大多数数据库在实现以上事务隔离级别(Read uncommitted除外)时采用的机制是锁。这也就是为什么经常说当应用程序里大量使用事务或者高并发情况下会出现性能低下、死锁的问题。
2 分布式事务
2.1 CAP
CAP理论是Eric Brewer教授针对分布式数据库所提出的一套理论,他认为在实现分布式数据库,需要考虑3个需求:
- Consistency(一致性):当写发生时,每个节点的数据必须都被更新到。当查询发生时,如果还未全部更新到则返回error或timeout;如果全部更新到了,则直接返回结果。
- Availability(可用性):当查询发生时,不用考虑上一次写是否更新到了每个节点,直接提供当下的数据作为结果。
- 有限时间内:对于用户的一个操作请求,系统必须能够在指定的时间(响应时间)内返回对应的处理结果,如果超过了这个时间范围,那么系统就被认为是不可用的。即这个响应时间必须在一个合理的值内,不让用户感到失望。
- 返回正常结果:要求系统在完成对用户请求的处理后,返回一个正常的响应结果。正常的响应结果通常能够明确地反映出对请求的处理结果,即成功或失败,而不是一个让用户感到困惑的返回结果。比如返回一个系统错误如OutOfMemory,则认为系统是不可用的。
- Partition-tolerance(分区容错性):除非所有节点都挂,它都应该能继续提供服务。
CAP理论指出,当每个节点都工作正常的时候,C、A、P是可以都满足的,当出现节点故障时,我们只能3选2。
如果我们不想因为数据库的某个节点出现故障就让数据库停止服务,那么我们必定选择P,那么我们就只能在C和A之间做选择。如果选择C:后续的读都将失败。如果选择A:会读到的结果。
CAP | 说明 |
---|---|
放弃P | 如果希望能够避免系统出现分区容错性问题,一种较为简单的做法是将所有的数据(或者仅仅是哪些与事务相关的数据)都放在一个分布式节点上。这样做虽然无法100%保证系统不会出错,但至少不会碰到由于网络分区带来的负面影响。但同时需要注意的是,放弃P的同时也就意味着放弃了系统的可扩展性 |
放弃A | 一旦系统遇到网络分区或其他故障或为了保证一致性时,放弃可用性,那么受到影响的服务需要等待一定的时间,因此在等待期间系统无法对外提供正常的服务,即不可用 |
放弃C | 这里所说的放弃一致性,实际上指的是放弃数据的强一致性,而保留数据的最终一致性。这样的系统无法保证数据保持实时的一致性,但是能够承诺的是,数据最终会达到一个一致的状态。 |
需要明确的一点是:对于一个分布式系统而言,分区容错性可以说是一个最基本的要求。因为既然是一个分布式系统,那么分布式系统中的组件必然需要被部署到不同的节点,否则也就无所谓的分布式系统了,因此必然出现子网络。而对于分布式系统而言,网络问题又是一个必定会出现的异常情况,因此分区容错性也就成为了一个分布式系统必然需要面对和解决的问题。因此系统架构师往往需要把精力花在如何根据业务特点在C(一致性)和A(可用性)之间寻求平衡。
2.2 BASE
Basically Available, Soft state, Eventually consistent,简称BASE。
Basically Available(基本可用)
基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性,但不等于系统不可用。
- 响应时间上的损失:当出现故障时,响应时间增加
- 功能上的损失:当流量高峰期时,屏蔽一些功能的使用以保证系统稳定性(服务降级)
Soft state(软状态)
与硬状态相对,即是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
Eventually consistent(最终一致性)
强调系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。其本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
最终一致性可分为如下几种:
- 因果一致性(Causal consistency):即进程A在更新完数据后通知进程B,那么之后进程B对该项数据的范围都是进程A更新后的最新值。
- 读己之所写(Read your writes):进程A更新一项数据后,它自己总是能访问到自己更新过的最新值。
- 会话一致性(Session consistency):将数据一致性框定在会话当中,在一个会话当中实现读己之所写的一致性。即执行更新后,客户端在同一个会话中始终能读到该项数据的最新值
- 单调读一致性(Monotonic read consistency):如果一个进程从系统中读取出一个数据项的某个值后,那么系统对于该进程后续的任何数据访问都不应该返回更旧的值。
- 单调写一致性(Monotoic write consistency):一个系统需要保证来自同一个进程的写操作被顺序执行。
BASE定理是提出通过牺牲一致性来获得可用性,并允许数据在一段时间内是不一致的,但最终达到一致状态。
BASE和ACID相反,ACID是悲观的,它要求所有操作都必须保证一致性,而BASE是乐观的,它接受数据库的一致性在不断变化当中。同时,BASE对于CAP中的C做出了一定的妥协——接受临时的不一致,采用最终一致性。最终一致性,听上去怪怪的,一些开发人员觉得这是个坏东西。不过我们真的要时时刻刻保证一致性吗?BASE认为我们可以做一些妥协,因此如果我们按照BASE设计系统的话就能够保证:
- ACID
- ACID - A,不保证,一旦开始“写”则不可能回滚。
- ACID - C,保证最终一致性。
- ACID - I,不保证,是因为一个大事务是由多个小事务组成,每个小事务都会独立提交。
- ACID - D,保证,因为数据库保证D。
- CAP
- CAP - C,保证最终一致性。
- CAP - A,保证基本可用。
- CAP - P,保证。
举个例子,现在我们有3条insert要执行(至于是否是3个不同的表、数据库不重要),那么只要保证下面几点就能够满足BASE:
- 最终都能够执行成功
- 任何一条语句执行失败都会重试
- 任意一条语句重复执行结果都一样——幂等
正确地使用BASE模式也不是那么容易,比如刷卡消费业务,我们要保证“检查余额”和“扣款、记录消费日志”这两组动作不会产生交叉,否则就会因为高并发场景而发生透支,在这个例子里我们可以对“扣款、记录消费日志”做最终一致性,但是如何保证下达“扣款、记录消费日志”这两个指令肯定不会产生透支的情况则不是BASE解决的问题了。
所以总结一下BASE的特点就是:
- 解决的是提交的问题
- 2PC将提交动作放在数据库,而BASE将提交动作放在应用程序