ACID 理论是对事务特性的抽象和总结,方便我们实现事务。可以理解成:如果实现了操作的 ACID 特性,那么就实现了事务。
1. 事务是什么
事务可以看成是一个或者多个操作的组合操作,并且它对这个组合操作提供一个保证,如果这个组合操作之前的数据是一致的(即正确的),那么操作之后的数据也应该是一致的。不论这个组合操作执行的过程中,是否发生系统故障,还是在这个组合操作执行的过程中,是否与其他事务一起执行。
不论这个组合操作执行的过程中,是否发生系统故障,还是在这个组合操作执行的过程中,是否与其他事务一起执行。
2. 事务的四个特性
ACID简述:
- A:Atomicity,原子性。
- C:Consistency,一致性。
- I:Isolation,隔离性。
- D:Durability,持久性。
事务主要是通过ACID四个特性来实现的:
- 一致性(C):一个事务能够正确地将数据从一个一致性的状态,变换到另一个一致性的状态。
- 原子性(A):一个事务所有的操作,要么全部执行,要么就一个都不执行,即 all-or nothing。它可以让事务在出现故障等原因,导致不能全部执行成功时,将已经执行的部分操作,回滚到事务前的状态。
- 隔离性(I):如果多个事务并发执行,那么执行结果和一个一个串行执行是一样的。它可以使事务在执行时,不会受到其他事务的影响。不过在实践中,由于考虑到性能的问题,一般都使用较弱一点的保证,我们在后续的课程中会专门讨论。
- 持久性(D):如果一个事务已经提交,不论什么原因,它产生的结果都是永久存在的,它保证了事务的结果不会丢失。
在四个特性中,一致性是对事务执行最终结果正确性的保证,它需要依赖事务的其他特性来协助完成。
3. 一致性(C)
3.1 一致性
上文提到了一致性的定义,即一个事务能够正确地将数据从一个一致性的状态,变换到另一个一致性的状态。也就是在事务执行的过程中,不能出现任何不一致的问题,如果一个事务执行前的数据是正确的,那么执行后的数据也必须是正确的,所以,事务的一致性其实就是正确性。
事务的一致性需要保证一个事务在执行时,不论出现停电、宕机等任何问题,最终的执行结果都是正确的,这是一个非常高的要求,接下来分析一下,在高要求下事务是如何实现一致性的?
3.2 一致性是怎样实现的
从数据复制的角度来看,为了保障系统的高可用,每一份数据都复制了多个副本,事务执行后,这多个副本的数据需要完全一致,即数据的多副本必须通过强一致性的策略进行复制。
从事务的原子性、隔离性和持久性方面,来讨论事务的一致性是如何实现的。
3.2.1 原子性来保证
在事务的执行过程中,不能因为系统故障等原因,出现部分操作执行成功的情况,这个部分需要事务的原子性来保证。
3.2.2 隔离性保证
不能出现因为事务并发导致执行后状态错误的情况。为了让事务在执行时,不会受到其他事务的影响,需要要到事务的隔离性来保证。
3.2.2 持久性保证
在事务执行的过程中,也要考虑到因为数据丢失,导致执行后的结果错误的情况,这个部分需要事务的持久性来保证。
3.3 其他注意事项
虽然有了底层存储多副本数据强一致性的支持,以及事务三个特性的保驾护航,但我们还是要考虑事务执行的最终结果,是否满足数据库层以及业务层的约束规则,所以最后我们要做好约束检测。这里又分为如下两个层面来讨论。
3.3.1 数据库层面
第一个是数据库层面,数据库内部需要基于一些约束规则,来检测数据是否违反了一致性的约束,比如外键约束和唯一性约束等。
3.3.2 业务逻辑层面
应用层的业务逻辑,它需要结合业务场景做一些约束检测,这样做是为了保障数据的一致性,比如用户课程购买的场景,从用户账号扣掉的钱,应该和收款方的数目是相等的。如果应用层的处理逻辑出现 Bug,导致用户账号扣掉的钱比收款方的多,这样的一致性问题在数据库的事务层面是无法约束检测的,它需要应用层的业务逻辑来保证。
3.4 总结
事务一致性的实现需要多维度来保证,比如底层存储的多副本数据强一致性,事务原子性、隔离性和持久性的一起协作,以及数据库层和应用层的约束检测等各方面来保障,它不单单是事务层面的一致性问题。
这也是事务的一致性和其他三个特性不一样的地方,事务的原子性、隔离性和持久性这三个特性可以通过各自的实现机制来保障,而一致性则是应用层通过运用事务的原子性、隔离性和持久性的特性,加上数据库层的约束检测,并且在应用层开发中做好相关的约束检测才能达成,所以,我们说一致性是事务的集大成者。
4. 原子性
4.1 原子性定义
一般来说,我们在计算机领域第一次接触“原子”这一概念,都来源于操作系统的“原子操作”。在操作系统中,原子操作的定义是指,不可被中断的一个或者一系列操作,它包含了两个层面的意思。
4.1.1 整体的不可分割性
整体的不可分割性:一个原子操作的所有操作,要么全部执行,要么就一个都不执行,即 all-or-nothing 。
4.1.2 可串行化的隔离性
可串行化的隔离性,即线程安全。原子操作是在单核 CPU 时代定义的,由于原子操作是不可中断的,那么系统在执行原子操作的过程中,唯一的 CPU 就被占用了,这就确保了原子操作的临界区,不会出现竞争的情况。原子操作自带了线程安全的保证,即最严格的隔离级别的可串行化,所以我们在编程的时候,就不需要对原子操作加锁,来保护它的临界区了。
4.2 原子性两种隔离产生的原因
在前文中对事务中原子性的定义,一个事务所有的操作,要么全部执行,要么就一个都不执行。这个定义,只保留了原子操作的不可分割性,并没有关注可串行化的隔离性。
如果事务的原子性同时定义了不可分割性和可串行化的隔离性,那么对数据库性能的影响将会非常大,因为数据库需要频繁地操作,相对于内存来说非常慢的磁盘,而可串行化地去操作磁盘,在很多业务场景下的性能是我们不可以接受的。
因此,在事务的定义中,就将原子操作的不可分割性和隔离性,分别定义出了两个特性,即原子性和隔离性。其中隔离性为了在性能和正确性之间权衡,定义了多种隔离级别,我们可以依据自己的业务情况进行选择
4.3 怎样实现原子性
事务的原子性只关注整体的不可分割性,一个事务所有的操作,要么全部执行,要么就一个都不执行。那么我们应该如何实现事务的原子性呢?
从不可分割性的角度来思考,实现一个事务需要解决两个维度上的操作分割:
4.3.1 单节点事务
单节点事务,即单节点上操作的不可分割性。在单节点上,一个事务在执行的过程中出现崩溃等问题,它的一部分操作已经执行完成,而另一部分操作则无法继续执行,这时就会出现整个事务操作无法继续完成,仅部分操作完成的情况。
4.3.2 分布式事务
分布式事务,即多节点之间的操作不可分割性。在多节点上,一个事务操作需要在多个节点上运行,如果某些节点检测到违反约束、冲突、网络故障或者崩溃等问题,事务将无法继续执行,而其他节点的事务却已经顺利完成了,这时就会出现部分节点操作完成的情况。
4.4 单节点事务
对于单节点上运行的事务(单节点事务)来说,在执行过程中,不需要与其他的节点交互,也就不会出现部分节点失败导致的操作分割,我们只需要考虑当前节点整体失败导致的操作分割即可。对于单节点事务,一般是在存储引擎上,通过 Undo Log 、 Redo Log 和 Commit 记录来实现,具体流程如下图。
对于单节点事务来说,一个非常关键的顺序就是在磁盘上持久化数据的顺序:先写入 Undo Log 和 Redo Log ,然后再写入 Commit 记录。其中事务的提交或中止由 Commit 记录来决定,如果在写入 Commit 记录之前发生崩溃,那么事务就需要中止,通过 Undo Log 回滚已执行的操作;如果事务已经写入了 Commit 记录,就表明事务已经安全提交,后面发生了崩溃的话,就要等待系统重启后,通过 Redo Log 恢复事务,然后再提交。
Redo Log 保证了事务的持久性, Undo Log 保证了事务的原子性,而写入 Commit 记录了事务的提交点,它来决定事务是否应该安全提交。通过提交点,我们就可以将事务中多个操作的提交,绑定在一个提交点上,实现事务的原子提交。
4.5 分布式事务
对于多节点上运行的事务(分布式事务)来说,除了当前节点整体失败导致的操作分割之外,还存在部分节点失败导致的操作分割。当前节点整体失败导致的操作分割,可以按单节点事务来处理,而对于部分节点失败导致的操作分割,一个常见的思路是通过两阶段提交( 2PC )来解决,实现 2PC 的思路如下图所示。
4.5.1 两阶段提交过程
- 选择一个协调者,这个协调者可以是分布式事务的参与节点,也可以是一个单独的进程。
- 阶段 1
-
- 协调者发送事务请求(Prepare)到所有的参与节点,并询问它们是否可以提交。
-
- 如果所有的参与节点都回复“是”,那么接下来协调者在阶段 2 发出提交(Commit)请求。
-
- 如果任何的参与节点回复“否”,那么接下来协调者在阶段 2 发出放弃(Rollback)请求。
- 阶段 2
-
- 依据阶段 1 返回的结果,决定事务最终是提交(Commit)还是放弃(Rollback)。
4.5.2 2个关键点
关键点一是两个关键承诺:
- 第一个承诺在阶段 1 ,当事务的参与节点回复“是”的时候,对于当前事务,这个参与节点一定是能够安全提交的,它不仅要保障事务在提交时,不会出现冲突和约束违规之类的问题,还要保障即使出现系统崩溃、电源故障和磁盘空间不足等系统问题时,事务依然能够正常提交成功。
- 第二个承诺在阶段 2 ,当协调者基于参与者的投票,做出提交或者中止的决定后,这个决定是不可以撤销的。对于协调者来说,如果协调者通知参与者失败,那么协调者必须一直重试,直到所有的参与节点都通知成功为止;而对于参与者来说,不论协调者通知的结果是提交还是中止,参与者都必须严格执行,不能反悔。即使出现了故障,在故障恢复后,还是必须要执行,直到成功为止。
关键点二是2PC的提交点:
- 当协调者通过参与者的投票,做出提交或者中止事务的决定后,需要先将决定写入事务日志,然后再通知事务的参与者。如果协调者在事务执行过程中崩溃了,那么等到协调者恢复后,在事务日志中如果没有发现未解决的事务,就中止事务;反之,就会继续执行事务。
协调者将阶段 1 的决定写入事务日志就是 2PC 中事务的提交点,通过这个提交点,将多个节点的事务操作绑定在一个提交点上,然后像单节点事务一样,利用这个提交点来保证事务的原子性。
4.5.3 2PC 面临的问题
2PC 可以解决分布式事务的原子性问题,但是要正确使用 2PC,还需要了解以下几个方面的问题。
- 第一,2PC 是一个阻塞式协议。当 2PC 的一个参与者,在阶段 1 做出了“是”的回复后,参与者将不能单方面放弃,它必须等待协调者的决定,也就意味着参与者所有占用的资源都不能释放。如果协调者出现故障,不能将决定通知给参与者,那么这个参与者只能无限等待,直到协调者恢复后,成功收到协调者的决定为止。
- 第二, 2PC 是一个逆可用性协议。如果在阶段 1 ,任何一个参与者发生故障,使准备请求失败或者超时,协调者都将中止操作;如果在阶段 2 ,协调者发生故障,也会导致参与者只能等待,无法完成操作。
- 第三,虽然 2PC 能保证事务的原子性,即一个事务所有的操作,要么都成功,要么都失败,但是它并不能保证多个节点的事务操作会同时提交。如果没有同时提交,即一部分节点已经提交成功,而另一部分节点还没有提交的时候,就将使事务的可见性出现问题
虽然 2PC 在性能、可用性和可见性方面都存在问题,但是目前分布式事务中,使用最广泛的还是 2PC 。
5. 隔离性(I)
5.1 隔离性定义
通俗来讲,一个事务所做的修改在提交前,对其他事务是不可见的。
隔离性比想象的要复杂。后续还会单独再讲!
5.2 隔离级别
SQL-92 标准定义了 4 种事务的隔离级别:
- 读未提交(Read Uncommitted)
- 读已提交(Read Committed)
- 可重复读(Repeatable Read)
- 可串行化(Serializable)
在后面的发展过程中,又增加了:
- 快照隔离级别(Snapshot Isolation)。
5.2.1 读未提交(Read Uncommitted)
在READ UNCOMMITTED 级别,事务中的修改,即使没有提交,对其他事务也都是可见的。
事务可以读取未提交的数据,这也被称为脏读(Dirty Read)。这个级别会导致很多问题,从性能上来说,READ UNCOMMITTED不会比其他的级别好太多,但却缺乏其他级别的很多好处,除非真的有非常必要的理由,在实际应用中一般很少使用。
5.2.2 读已提交(Read Committed)
满足前面提到的隔离性的简单定义:一个事务开始时,只能“看见”已,经提交的事务所做的修改。
换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读(nonrepeatableread),因为两次执行同样的查询,可能会得到不一样的结果。
大多数数据库系统的默认隔离级别都是READ COMMITTED (但MySQL不是)。
5.2.3 可重复读(Repeatable Read)
REPEATABLE READ解决了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的。 但是理论上,可重复读隔离级别还是无法解决另外一个幻读(Phantom Read)的问题。
所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插人了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(Phantom Row). InnoDB和XtraDB存储引擎通过多版本并发控制(MVCC, Multiversion Concurrency Control)解决了幻读的问题。本章稍后会做进一步的讨论。
可重复读是MySQL的默认事务隔离级别。
5.2.4 可串行化(Serializable)
SERIALIZABLE是最高的隔离级别。它通过强制事务串行执行,避免了前面说的幻读的问题。
简单来说, SERIALIZABLE会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题。实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。
5.2.5 快照隔离级别(Snapshot Isolation)
快照隔离的核心思想是:
- 每个事务都从一个数据库的快照中读数据。
(即:事务看到的所有数据,都是在事务开始的时间点之前 committed 的数据。)
如果有些数据在当前事务开始之后,被其他事务改变了值,快照隔离能够保证当前事务无法看到这个新值。
对快照的理解后续再单独讲解。
5.3 事务的隔离级别与这些异常情况的关系
5.4 事务并发带来的异常情况
- 其一,脏写(Dirty Write),即有两个事务 T1 和 T2 , T1 更改了 x ,在 T1 提交之前, T2 随之也更改了 x ,这就是脏写,这时因为 T1 还没有提交,所以 T2 更改的就是 T1 的中间状态。假如现在 T2 提交了, T1 就要回滚,如果回滚到 T1 开始前的状态,已经提交的 T2 对 x 的操作就丢失了;假如不回滚到 T1 开始前的状态,已经 Roll Back 的 T1 的影响就还存在于数据库中。能够允许这种现象的数据库基本是不可用的,因为它已经不能完成事务的 Roll Back 了。
- 其二,脏读(Dirty Read),即有两个事务 T1 和 T2 , T1 更改了 x ,将 x 从 0 修改为 5 ,在 T1 提交之前, T2 对 x 进行了读取操作,读到 T1 的中间状态 x = 5 ,这就是脏读。假设最终 T1 Roll Back 了,而 T2 却根据 T1 的中间状态 x = 5 做了一些操作,那么最终就会出现不一致的结果。
- 其三,不可重复读(Nonrepeatable read)/ 读倾斜(Read Skew),即有两个事务 T1 和 T2 , T1 先读了 x = 0 ,然后 T2 更改了 x = 5 ,接着提交成功,这时如果 T1 再次读取 x = 5 ,就是不可重复读。不可重复读会出现在一个事务内,两次读同一个数据而结果不一样的情况。
- 其四,丢失更新(Loss of Update),即有两个事务 T1 和 T2 , T1 先读 x = 0 ,然后 T2 读 x = 0 ,接着 T1 将 x 加 3 后提交, T2 将 x 加 4 后提交,这时 x 的值最终为 4 , T1 的更新丢失了,如果 T1 和 T2 是串行的话,最终结果为 7 。
- 其五,幻读(Phantom Read),即有两个事务 T1 和 T2 , T1 根据条件 1 从表中查询满足条件的行,随后 T2 往这个表中插入满足条件 1 的行或者更新不满足条件 1 的行,使其满足条件 1 后提交,这时如果 T1 再次通过条件 1 查询,则会出现在一个事务内,两次按同一条件查询的结果却不一样的情况。
- 其六,写倾斜(Write Skew),即假如 x , y 需要满足约束 x + y >= 0 ,初始时 x = -3 , y = 5 ,事务 T1 先读 x 和 y ,然后事务 T2 读 x 和 y ,接着事务 T2 将 y 更新为 3 后提交,事务 T1 将 x 改为 -5 后提交,最终 x = -5 , y = 3 不满足约束 x + y >= 0 。
5.5 如何避免事务带来的异常
- 其一,对于脏写,几乎所有的数据库都可以防止异常的出现,并且我们可以理解为出现脏写的数据库是不可用的,所以这里就不讨论脏写的情况了。
- 其二,对于脏读,提供“读已提交”隔离级别及以上的数据库,都可以防止异常的出现,如果业务中不能接受脏读,那么隔离级别最少在“读已提交”隔离级别或者以上。
- 其三,对于不可重复读或读倾斜,“可重复读”隔离级别及以上的数据库都可以防止问题的出现,如果业务中不能接受不可重复读和读倾斜,那么隔离级别最少在“可重复读”隔离级别或者以上。
- 其四,对于丢失更新,如果数据库的隔离级别不能达到“可重复读”隔离级别或者以上,那么我们可以考虑以下的几种方法来避免。
-
- 首先,如果数据库提供了原子写操作,那么一定要避免在应用层代码中,进行“读-修改-写”操作,应该直接通过数据库的原子操作执行,避免更新丢失的问题。
-
- 其次,如果数据库不支持原子操作,或者在某些场景中,原子操作不能处理时,可以通过对查询结果显式加锁来解决。
6. 持久性
6.1 持久性的挑战
持久性在事务中的定义是,如果一个事务已经提交,不论什么原因,它产生的结果都是永久存在的,这保证了事务的结果不会丢失。
通过持久性的定义,我们会发现要保证事务的持久性,一个显而易见的思路就是,将事务的结果立即写入到非易失性存储设备中,比如 SSD 硬盘和 SATA 硬盘等,并且写入的副本数越多,持久性就越高。
将数据写入到硬盘中其实是非常消耗性能的。如果将每一个事务所有的操作结果,都实时写入到持久化的存储设备中,这样的数据库几乎就是不可用的,更不用说多副本的写入了。如何解决呢?
6.2 写入存储设备的挑战
关于这个问题,我们需要从磁盘设备的特性开始说起。对于 SATA 硬盘来说,可以将它简单理解为一个有很多同心圆的圆盘,在写入数据的时候,会经历以下几个步骤:
- 寻道,找到数据所在的同心圆,这个时间是毫秒级别的;
- 寻址,找到数据所在的同心圆的位置,这个时间也是毫秒级别的;
- 开始读写数据,每秒可以读写的数据量为 100M 级别的数据,这个是非常快的。
6.2.1 SATA 硬盘
如果没有寻道和寻址这两个步骤, SATA 硬盘的性能其实是非常不错的。那么如何避免寻道和寻址呢?
如果第一次寻道和寻址后,持续对数据进行大量的读写,即顺序读写,是可以忽略寻道和寻址的时间消耗的。
而对应顺序读写的是随机读写,它每一次读写的数据量很小,并且数据位置不相邻,都需要先寻道、寻址,然后才能进行数据读写,所以随机读写的性能是非常差的。
总结,怎样提高SATA硬盘性能:
- 增大顺序读写;
- 减少随机读写;
6.2.2 SSD 磁盘
对于 SSD 硬盘,寻址的情况则大大改观,不需要像 SATA 硬盘一样机械地寻道、寻址,它可以通过电路直接获得读写的地址。但是,SSD 硬盘与传统的 SATA 硬盘有一点不同,即它不能够覆盖写,所以对于已经存在数据的 SSD 磁盘来说,一次数据的写入需要分为 2 个步骤:
- 擦除 SSD 上已有的数据;
- 写入新的数据。
但是对于 SSD 来说,一般每次写入的最小单位为 Page ,一个 Page 的大小为 4KB,而每次擦除的大小单位为 Block , Block 通常由 64 或 128 个 Page 组成。
SSD 的写入与擦除的单位大小不匹配,那么如果仅仅是要修改一个 Page 的数据,在单个 Block 之中没有了空余的 Page 时,需要先读取 Block 的内容,然后擦除一个 Block 的数据,再将 Block 的内容和修改的内容进行合并,写入一个 Block 的数据。而这就会导致原本只需要写入 4KB 的数据,最终却写入了 64 倍甚至是 128 倍的数据,出现写放大的问题。
新的问题:对于 SSD 磁盘来说,写放大是无法避免的,相比于顺序写入,随机写入会大大加剧写放大的问题
6.2.3 小结
不论是 SATA 硬盘还是 SSD 硬盘,从硬盘自身的特点来说,顺序读写的性能都要远远高于随机读写。另外从系统的角度来看,顺序读写在预读和缓存命中率等方面也要大大优于随机读写。
6.3 如何解决写入存储设备带来的挑战
由于事务的持久性是必须的,如果一个事务已经提交,不论什么原因,它产生的结果都是永久存在的,所以对于单节点来说,我们可以先在内存中将事务的操作完成,然后将处理的结果顺序写入日志文件中,这就避免了事务操作结果随机写入存储的性能问题了。
然后我们再提交事务,这样一来,哪怕事务提交后,机器立即崩溃了,在机器故障恢复后,系统依然能通过日志文件,恢复已经提交的事务。
6.3.1 小结
所以,通过顺序写入日志的形式,避免了非易失性存储设备随机写入性能差的问题,达到了事务提交时,所有事务操作结果都写入存储设备的目的。在这个时候,即使系统崩溃,事务的持久性也是有保障的。我们把这种通过顺序写入日志的形式,称之为重做日志(RedoLog)或预写日志(Write Ahead Log)。
6.4 通过数据冗余保障持久性
虽然通过 Redo Log 或 WAL能保证系统在崩溃、重启等问题出现时的持久性,但是当存储设备出现了故障,比如数据都不可读的时候,还是会出现即使事务已经提交成功,但是事务结果却丢失的情况。如何解决?
6.4.1 磁盘阵列
思路一:通过磁盘阵列,从磁盘内部通过冗余数据来解决。比如 RAID 1 ,我们将多块硬盘组成一个磁盘阵列,磁盘阵列中每块磁盘都有一个或多个是副本磁盘。事务的每一次写入都同时写入所有的副本硬盘,这样只要不是所有的副本磁盘同时出现故障的情况,我们就都可以正常从磁盘上读到数据,不会影响事务的持久性。还有一种磁盘阵列的方式是 RAID 5 ,它是通过冗余校验数据的方法来保障持久性。
新的问题:由于磁盘阵列上多块硬盘的地理位置通常都是在一起的,这样如果出现地震、火灾和洪水等自然灾害时,可能会导致整个磁盘阵列上的硬盘都不可用,那么事务的持久性就不能被保证了。
6.4.1 增加副本并通过网络复制数据
思路二:通过增加副本,通过网络复制数据来解决。其实这个问题在“复制”系列课程中已经详细讨论过了,但是对于事务的场景来说,由于数据的复制必须是线性一致性的,所以我们只能采用同步的主从复制,但是这个方式在性能和可用性方面都存在问题
- 性能问题:一次写入必须等所有的节点都写入成功,整体的写入性能取决于最慢的节点的写入性能,并且网络的不确定性会加剧性能问题。
- 可用性问题:对于同步复制来说,如果一个节点出现故障,就会导致写入失败,非常影响系统整体的可用性。
对于事务场景,如果我们不采用同步的主从复制,是否有其他的办法来解决呢?
解决方法:通过 Raft 或者 Paxos 之类的共识算法来解决。后续在分布式算法中再详细说明。
7 总结
事务的四个特性的角度,总:
- 一致性(C):是指事务的正确性,需要底层数据的线性一致性,事务层的原子性、隔离性、持久性,以及数据库层面的约束检测和应用层的约束检测来保证。
- 原子性(A):是指事务操作的不可分割性,一般通过原子提交协议 2PC 或 3PC 来保障。
- 隔离性(I):是指事务操作在并发控制,一般数据库都提供弱隔离性,是数据库在性能和正确性之间的衡权,一般通过 MVCC 或者 2PL 来实现。
- 持久性(D):是指已提交的事务结果不可丢失,一般在单机上通过非易失性存储来保障,在分布式场景下,通过数据冗余来保障。
参考:
《极客时间——深入浅出分布式技术原理》
《高性能MySQL》
《分布式服务架构》