logo
企业版

技术分享

DDIA精读|分布式系统中的事务之那些事务隔离级别

以下文章来源于木鸟杂记 ,作者穆尼奥

分布式系统

上文中,我们了解了事务的相关概念,在本篇文章中你将了解到事务那些隔离级别们。

目录

  • 读已提交

    • 无脏读

    • 无脏写

    • 实现

  • 快照隔离和重复读

    • 快照隔离的实现

    • 可见性规则

  • 索引和快照隔离

    • 可重复读和命名困惑
  • 防止更新丢失

    • 原子写

    • 显式上锁

    • 自动检测更新丢失

    • Compare-and-set

    • 多副本和冲突解决

  • 写偏序和幻读

    • 写偏序的特点

    • 其他写偏序例子

    • 幻读会导致写偏序

    • 物化冲突

如果两个事务修改的数据没有交集,则可以安全的并发;否则,就会出现竞态条件。一旦出现并发 BUG,通常很难复现和修复。单客户端的并发已经足够麻烦,多客户端并发访问更加剧了并发问题。

数据库试图通过事务隔离(transaction isolation)来给用户提供一种隔离保证,从而降低应用侧的编程复杂度。最强的隔离性,可串行化(Serializability),可以对用户提供一种保证:任意时刻,可以认为只有一个事务在运行。

初学者对几种隔离级别的递进关系通常难以理解,往往是找不到一个合适的角度。我的经验是,从实现的角度对几种隔离级别进行理解,会简单一些。如 ANSI SQL 定义的四种隔离级别:读未提交(Read Uncommited)、读已提交(Read Commited)、可重复读(Repeatable Read)和可串行化(Serializability),可以从使用锁实现事务的角度来理解。

最强的隔离性——可串行化,可以理解为全局一把大排它锁,每个事务在启动时获取,在提交、回滚或终止时释放,但无疑这种隔离级别性能最差。 而其他几种弱隔离级别,可以理解为是为了提高性能,缩小了加锁的粒度、减小了加锁的时间,从而牺牲部分一致性换取性能。从上锁的强弱考虑,我们有互斥锁(Mutex Lock,又称写锁)和共享锁(Shared Lock,又称读锁);从上锁的长短来考虑,我们有长时锁(Long Period Lock,事务开始获取锁,到事务结束时释放)和短时锁(Short Period Lock,访问时获取,用完旋即释放);从上锁的粗细来考虑,我们有对象锁(Row Lock,锁一行)和谓词锁(Predicate Lock,锁一个范围)。

但这没有覆盖到到另一个常见的隔离级别——快照隔离(Snapshot Isolation),因为它引出了另一种实现族——MVCC。由于属于不同的实现,快照隔离和可重复读在隔离级别的光谱上属于一个偏序关系,不能说谁强于谁。

接下来几个小节,将依次考察读已提交、快照隔离、可重复读三个隔离级别。以及隔离级别不够导致的几种现象——更新丢失(Lost Update)、写偏序(Write Skew)和幻读(Phantom Read)。

读已提交

性能最好的隔离级别就是不上任何锁,但会存在脏读和脏写的问题。为了避免脏写,可以给要更改的对象加长时写锁,但读数据时并不加锁,此时的隔离级别称为读未提交(RU,Read Uncommitted)。但此时仍然会有脏读,为了避免脏读,可以对要读取的对象加短时读锁,此时的隔离级别是读已提交(RC,Read Committed),他提供了两个保证:

  1. 从数据库读取时,只能读到已经提交的数据(即没有脏读,no dirty reads)

  2. 往数据库写入时,只能覆盖已经提交的数据(即没有脏写,no dirty writes)

无脏读

如果一个事务 A 能够读到另一个未提交事务 B 的中间状态,则称有脏读(dirty reads)。在读已提交的隔离级别的运行的事务,不会有脏读。举个例子:

无脏读

在用户 1 的事务提交前,用户 2 看到的 x 值一直是 2。

如果允许脏读会有什么问题?举两个例子:

  1. 一个事务如果更新多个对象,脏读则可能让另外的事务看到中间不一致的状态。如前文举的未读邮件数的例子。
  2. 如果事务终止,回滚所有操作,允许脏读会让另外的事务读取到被回滚的数据。

无脏写

如果两个事务并发更新相同对象,且事务 A 修改了一个对象,但尚未提交,此时如果另一事务 B 同样修改该对象,并且覆盖了 A 未提交的值,则称有脏写(dirty writes)。在读已提交隔离级别运行的事务,为了防止脏写,通常会推迟(重试或者加锁)后面修改同一对象的事务到前一个事务提交或终止。

通过禁止脏写,可以避免一些并发产生的不一致问题:

  1. 如果多个事务同时更新相交的多个对象,脏写可能会产生错误的结果。如下图二手车销售,购买汽车需要两个步骤:更新购买列表、将发票发给买家。如果 Alice 和 Bob 的购买事务允许脏写,则可能出现 Bob 买到了商品,但发票给了 Alice。

  2. 但读已提交并不能防止如图 7-1 中的计数器的竞态条件(是一种更新丢失)。两个事务都是读的已提交的数据(因此不是脏读),且写入时,另一个事务写入发生在前一个事务之后(因此不是脏写),但仍然不能避免写入丢失的问题(只增加了一次)。

无脏写

实现

读已提交是一个常见的隔离级别,是 Oracle 11g、PostgreSQL、SQL Server 2012、MemSQL 和其他许多数据库的默认设置。

那如何实现读已提交的隔离级别呢?

首先说脏写,最简单、常见的方法是使用行锁(起源于关系型数据库),即针对单条数据的长时写锁(Long Period Write Lock)。当事务想要修改某对象时,需要先获取该对象的锁,如果已被获取,则等待,如果成功获取,则可以写入数据,待事务提交时释放锁。

其次说脏读,也可以使用针对单条数据的短时读锁来解决脏读问题。读锁可以并发,但和上述写锁是互斥的。这可以确保有脏数据(未提交的更改)时,其他事务针对该对象的读取会被阻塞。但使用行锁的性能也并不是很好,因为一个长写事务,可能会把其他要读取该对象的读事务都“饿死”,损失性能和延迟。

因此,当今大多数据库会走另一条路子,即非锁的形式实现读已提交。如某种方式将旧值记住,在有针对某对象写的事务进行时,其他针对同一对象的事务中的读取都会拿到旧值。当更改事务提交时,其后事务才能看到该对象的新值。将其泛化一下,就是我们常说的 MVCC。

快照隔离和重复读

粗看读已提交已经能够满足事务的定义,比如能够终止事务、能够实现某种程度上的隔离,但仍然会产生一些并发问题。

如图,考察这样一种场景,Alice 分两个账户,各存了 500 块钱,但如果其两次分别查看两个账户期间,发生了一笔转账交易,则两次查看的余额加起来并不等于 1000。

快照隔离和重复读

这种异常被称为不可重复读(non-repeatable read)或者读倾斜(read skew,skew 有点被过度使用)。读已提交的隔离级别允许出现不可重复读问题,如上述例子,每次读取到的都是已提交的内容。

例子中的不一致情况,只是暂时的。但在某些情况下,这种暂时的不一致也是不可接受的:

  1. 备份。备份可能需要花费很长时间,而备份过程中可能会有读写存在,从而造成备份时的不一致。如果之后再利用此备份进行恢复,则会造成永久的不一致。

  2. 分析型查询和完整性检查。这个操作和备份一样,耗时都会比较长,如果中间有其他事务并发导致出现不一致的现象,就会导致返回的结果有问题。

快照隔离(snapshot isolation)级别能够解决上述问题,使用此级别,每个事务都可以取得一个某个时间点的一致性快照(consistent snapshot),在整个事务期间,读到的状态都是该时间点的快照。其他事务的修改并不会影响到该快照上。 快照隔离级别在数据库中很常用,PostgreSQL、使用 InnoDB 引擎的 MySQL、Oracle、SQL Server 等都支持。

快照隔离的实现

和读已提交一样,快照隔离也使用加锁的方式来防止脏写,但在进行读取不使用锁。快照隔离的一个关键原则是“读不阻塞写,写不阻塞读”,从而允许用户在进行长时间查询时,不影响新的写入。

为了实现快照隔离,保证读不阻塞写,且避免脏读,数据库需要对同一个对象保留多个已提交的版本,我们称之为多版本并发控制(MVCC,multi-version concurrency control)。

如果一个数据只需要实现到读已提交级别,那么保留两个版本就够了;但要实现快照隔离级别,一般使用 MVCC。相对于锁,MVCC 是另一个进行事务实现的流派,而且近些年来更受欢迎。当然,MVCC 是一种思想或者协议,具体到实现,有 MVTO(Timestamp Ordering)、MVOCC(Optimistic Currenccy Control)、MV2PL(2 Phrase Lock),即基于多版本,加上一种避免写写冲突的方式。

具体来说,使用 MVCC 流派,也可以实现读未提交、读已提交、快照隔离、可串行化等隔离级别。

  1. 读已提交在查询语句粒度使用单独的快照,快照粒度更小,因此性能更好。

  2. 快照隔离在事务粒度使用相同的快照(主要解决不可重复读问题)。

MVCC 的基本要点为:

  1. 每个事务开始时会获取一个自增的、唯一的事务 ID(txid),该 txid = max(existing tx id) + 1。

  2. 该事务在修改数据时,不会修改以前版本,而会新增一个具有 txid 版本的数据。

  3. 该事务只能访问到所有版本 ≤ txid 的数据。

  4. 在写入时,如果发现某个数据存在 > txid 的版本,则存在写写冲突。

下图是 PostgreSQL 中基于 MVCC 实现快照隔离的示意图,其场景仍是两个账户,每个账户各有 500 块钱。本例中是通过使用两个版本信息:created by 和 deleted by,来标记一个数据版本的生命周期。

快照隔离的实现

使用 delete by 进行标记删除的原因在于,可能还有正在进行的事务(txid < deleted by)可能会访问该对象。之后,会有专门进行 GC 进程对这些数据进行真正的回收,当然删除时需要确认所有正在进行的事务 txid > deleted by。

  • 个人认为不使用 delete by 也能达到标记删除的效果?新的版本数据存在后,自动就使得老版本不可见。之后,只要确定没有事务正在访问老版本数据,即可进行 gc。通过 min(current tx) > latest version 即可判定没有事务访问了。

可见性规则

在事务中进行读取时,对于每个对象来说,需要控制其版本对事务的可见性,以保证该事务能够看到一致性的视图。

使用 MVCC,每个对象都有多个版本。上一节粗略说到该事务只能访问到所有版本 ≤ txid 的数据。展开来讲:

  1. 事务开始时,所有正在进行(已经开始但未提交或中止)的事务,所做的任何写入都会被忽略。

  2. 被中止的事务,所做的任何写入都会被忽略。

  3. 具有较晚事务 ID 的事务所做的任何写入都会被忽略。

  4. 剩余其他的数据,对此事务都可见。

如果事务 txid 是严格自增的,则可以理解为,对于 txid = x 的事务来说:

  1. 对于所有 txid < x 的事务,如果已经中止或正在进行,则其所写数据不可见。

  2. 对于所有 txid > x 的事务,所写数据皆不可见。

从另外一个角度来讲,如果一个对象的版本:

  1. 在事务开始时,创建该版本的事务已经提交。

  2. 未被标记删除,或被标记删除的事务尚未提交。

则该对象版本对改事务可见。 长时间运行的事务,可能会导致某些标记删除的对象版本不能够真正的被回收。但如果此类事务不太多,则代价并不大,只是需要维护一些对象的多个版本。

索引和快照隔离

当数据有多个版本时,如何给数据建立索引?一个简单的方法是将索引指向对象的所有版本,然后在查询时使用再进行版本过滤。当某个对象的所有版本对任何事务都不再可见时,相应的索引条目也可以被同时删除。

在实践中,有很多优化。如 PostgreSQL 的一个优化是,如果某个对象更新前后的数据都在一个物理页中,则对应的索引指向可以不用更新。

CouchDB、Datomic 和 LMDB 中使用一种 仅追加 / 写时拷贝(append-only/copy-on-write)的 B 树变体,是一种多版本技术的变体。boltdb 参考了 LMDB,也可以归为此类,此类 B 族树每次修改,都会引起叶子节点(所有数据都会落到叶子节点)到根节点的一条路径的全部修改(叶子节点变了,其父节点内容——指针也要修改,从而引起级联修改),如果发生节点的分裂或合并,会引起更大范围的更新。

这种方式在更新时不会覆盖老的页,每个数据修改都会新生成一个树根,每个树根所代表的树可以视作一个版本的快照。使用某个树根就相当于使用某个版本快照,其所能访问到的数据都属于同一个版本,而无须再进行版本过滤。当然,这类系统也需要后台常驻的 compaction 和 GC。

可重复读和命名困惑

在 1975 年 System R 定义 ANSI SQL 标准的隔离级别时,只定义了 RU、RC、RR 和 Serializability。当时,快照隔离还没有被发明,但是上述四种级别汇总有一个和快照隔离类似的级别:RR、Repeatable Read、可重复读。

因此,虽然快照隔离级别很有用,尤其是只读事务,但很多数据库虽然实现了快照隔离,但却另有称谓。比如 Oracle 将 SI 称为 可串行化(Serializable),PostgreSQL 和 MySQL 将 SI 称为 可重复读(repeatable read)。因为这样可以符合 SQL 标准要求,以号称兼容 SQL 标准。

但严格来说,SQL 对隔离级别的定义是有问题的,比如标准依赖于实现、几个隔离级别不连续、模糊不精确。很多数据库都号称实现了可重复读级别,但它们提供的保证却存在着很大差异。虽然一些文献中有对可重复读进行了精确定义,但大部分实现并不严格满足此定义。到最后,没有人知道可重复读的真正含义。

防止更新丢失

读已提交和快照隔离,只是定义了从只读事务的视角,在有并发写入时,哪些数据是可见的,即解决了读写冲突。但我们忽略了包含并发写的多个事务的一些冲突情况。当然,产生脏写的写写冲突已经讨论过,但还有其他几类冲突,比较有名的是更新丢失(lost update),典型的例子如并发更新计数器。

更新丢失发生的关键在于,两个事务中都有读后写序列(读取 - 修改 - 写入序列,写偏序也是这个序列,但是针对多个对象),即写依赖于之前的读。如果读到的内容被其他事务修改,则本事务稍后的依赖于此读的写就会发生问题。如:

  1. 并发更新计数器和账户余额。

  2. 复合值的并发修改(如 json 文档中的列表字段,需要先读出,加一个字段后写回)。

  3. 两个用户同时修改 wiki 页面,并且都是修改后将页面完整覆写回。

可以看出,这是一个普遍存在的问题,因此也诞生了很多方案来解决此问题。

原子写

有些数据库提供原子的(针对单个对象的)read-modify-write 操作,因此,如果应用层逻辑能用这个原子操作表达,就可以避免更新丢失。如大多数关系型数据库都可以使用此种原子操作对计数器进行安全并发更新:

UPDATE counters SET value = value + 1 WHERE key = 'foo';

与关系数据库类似,

  1. 文档数据库如 MongoDB,提供对文档局部的原子更新操作。

  2. KV 存储如 Redis,支持对复合数据结构优先队列的原子更新。

原子操作的通常实现方式为,在读取某对象时,获取其互斥锁,从而阻止其他事务读取该对象。这种实现有时也被称为游标稳定性(cursor stability)。如下图,在 fetch a row 处,数据库会释放上一行的互斥锁,同时获取该行的互斥锁,以阻止其他事务对改行进行读取或者修改。如果此处只获取短时读锁,则会退化成读已提交级别。

set isolation to cursor stability
declare cursor for SELECT * FROM customer
open the cursor
while there are more rows
       fetch a row
   do work
end while
close the cursor

其他粗暴的实现包括,让所有针对同一个对象的操作都在一个线程上执行,从而将对任何单个对象的执行序列化。

另外,ORM 框架很容易不使用原子操作来执行 read-modify-write 序列,常常会产生隐含的 bug。

显式上锁

即应用在有针对单个对象的 read-modify-write 序列时,将是否上锁的决策交给应用层,通常的 SQL 语法是:

select xx where xx for update;

书中举了一个多人下棋游戏,有几个玩家可以同时移动相同的棋子,由于规则限制,单个原子操作是不够的。但此时,可以使用数据库提供的语法来显式上锁,从而防止两个玩家移动有交集的棋子集。

BEGIN TRANSACTION;
SELECT * FROM figures
WHERE name = 'robot' AND game_id = 222 FOR UPDATE;
-- Check whether move is valid, then update the position -- of the piece that was returned by the previous SELECT. UPDATE figures SET position = 'c4' WHERE id = 1234;
COMMIT;

但需要根据应用需求进行合理的加锁——不要过度、也不要忘记。

自动检测更新丢失

除了使用锁的(悲观)方式(在数据库层或应用层)强制 read-modify-write 原子的执行;还可以使用乐观方式,允许其并发执行,检测到更新丢失后进行重试。

在使用 SI 隔离级的基础上,可以高效的对更新丢失进行检测。事实上,PostgreSQL 的可重复读,Oracle 的可串行化和 SQL Server 的快照隔离级别,都能够自动检测更新丢失的冲突,并中止后面的事务。但 MySQL 的 InnoDB 的快照隔离级别并不检测是否有更新丢失,有些人认为,快照隔离级别需要检测更新丢失冲突,从这个角度来讲,MySQL 没有提供严格的快照隔离。

相对于应用层主动上锁来说,自动检测更新丢失可以减少很多心智负担,避免写出一些察觉不到的 bug。

Compare-and-set

在不提供事务的数据库中,有时候会支持 CAS 操作,前面单对象写入中提到了该概念。使用 CAS 操作也能避免更新丢失,保证 read-modify-write 的原子性。

例如,在文档数据库的维基百科场景中,可以使用数据库提供的 CAS 操作,来对 wiki 页面进行原子的更新,仅当发现内容没有被修改时,才写会修改后的内容。

UPDATE wiki_pages SET content = 'new content' WHERE id = 1234 AND content = 'old content';

对于上述语句,如果数据库支持从快照中读取数据,则仍然没有办法防止丢失更新。

多副本和冲突解决

在多副本数据库中,解决更新丢失问题会更难一些,尤其如果多个副本允许并发写入。

在多主和无主模型中,允许数据进行并发的写入和异步的同步,因此难以保证所有的数据即时收敛成一致。之前提到的锁和 CAS 操作都是针对单份数据,因此在此情况下都不适用。

但如之前章节提到,可以允许并发写入和异步更新,如果有冲突就用多版本来解决,最后使用用户代码或者特殊数据结构来合并冲突。

特殊的,当多个操作满足“交换律”时,原子操作可以在多副本数据中进行正常的工作,如计数器场景就满足交换律。Riak 2.0 之后就支持并发的更新,并且会自动合并结果,而不会有更新丢失。

另一方面,后者胜(LWW,last write win)的冲突解决策略是会造成更新丢失的。虽然,很多多副本数据库默认使用 LWW 进行冲突解决。

写偏序和幻读

当不同事务试图并发地更新单个对象时,就会出现前面小节已经讨论过的的脏写和更新丢失问题。为了保持数据一致性,既可以在数据库层面自动的解决,也可以通过在应用层面显式的使用原子操作或加锁来解决。

但除了上述并发写入问题,还有一些更奇妙的冲突例子,你没猜错,这里会涉及到多个对象的访问。

考察一个医生值班的场景,医院通常会要求几名医生同时值班,即使有特殊情况,也要保证有不少于一名医生值班。假设在某天,轮到 Alice 和 Bob 两人值班,不巧的是,他们都感觉身体不适,并且恰好同时发起请假。

写偏序和幻读

假定数据库运行在快照隔离级别下,Alice 和 Bob 同时查询了今天值班情况,发现有多于一人值班,然后先后提交了休假申请,并且都通过了。这并没有违反快照隔离级别,但确实造成了问题——今天没有人值班了。

写偏序的特点

上述异常称为写偏序(write skew),它显然不属脏写和更新丢失,因为这两个事务在更新不同的对象,这里的竞态条件稍微有点不明显,但 的确存在竞态条件,因为如果顺序执行,不可能出现没人值班的后果。另一个常见的例子是黑白棋翻转。

从单对象到多对象的角度来看,写偏序可以算作是更新丢失的一种泛化。写偏序本质也是 read-modify-write,虽然是涉及多个对象,但本质仍然是一个事务的写入会导致另外一个事务读取到的信息失效。补充一句,写偏序是由 MVCC 实现的快照隔离级别的特有的缺陷,它是由于读依赖同一个不变的快照引起的。

解决更新丢失的很多手段,都难以直接用到解决写偏序上:

  1. 由于涉及多个对象,针对单对象的原子操作不能使用。

  2. 在快照隔离中,想要真正避免写偏序需要真正的可串行化。

  3. 虽然有些数据库允许指定约束(constraints),但往往是单对象的简单约束,如唯一性、外键约束。当然,可以使用触发器来在应用层维护多对象约束,以解决上述问题。

  4. 如果没有办法使用可串行的化的隔离级别,还可以利用数据库提供的(for update)机制进行显式的加锁。

BEGIN TRANSACTION;

SELECT * FROM doctors
 WHERE on_call = true
 AND shift_id = 1234 FOR UPDATE;

UPDATE doctors
 SET on_call = false
 WHERE name = 'Alice'
 AND shift_id = 1234;

COMMIT;

写偏序的特点

写偏序初看起来不好理解,但只要把握住写偏序的特点:

  1. 涉及多个对象。

  2. 一个事务的写入会使得另外事务的读取失效,进而影响其写入决策。

就能发现很多写偏序的例子。

会议室预定系统

基本流程是先检查是否有冲突的预定(同一个会议室、同一个时间段),如果没有,则创建会议。语句如下:

BEGIN TRANSACTION;
-- Check for any existing bookings that overlap with the period of noon-1pm
SELECT COUNT(*) FROM bookings
 WHERE room_id = 123 AND
  end_time > '2015-01-01 12:00' AND start_time < '2015-01-01 13:00';

-- If the previous query returned zero:
INSERT INTO bookings
 (room_id, start_time, end_time, user_id)
 VALUES (123, '2015-01-01 12:00', '2015-01-01 13:00', 666);

COMMIT;

但在快照隔离级别下,使用上述语句,不能避免多个用户并发预定时,预定到同一个会议室的时段。为了避免此冲突,就需要上可串行化级别了。

多人棋类游戏

之前提到的多人棋类游戏,对棋子对象加锁,虽然可以防止两个玩家同时移动同一个棋子,却不能避免两个玩家将不同棋子移到一个位置。

抢注用户名

在每个用户具有唯一用户名的网站上,两个用户可能会并发的尝试创建具有相同名字的账户。如果使用检查是否存在该名字 → 没有则注册该名字流程,在快照隔离级别下,是没法避免两个用户注册到相同用户名的。当然,可以通过对用户名列加唯一性约束来保证该特性,这样,第二个事务在提交时会因为违反唯一性约束而终止。

防止一钱多花

允许用户花钱和点券的服务,通常会在用户消费时检查其没有透支,导致余额变为负数。可以通过在账户余额中插入一个临时项目来实现:列出用户中所有项目,并检查总和是否为正。但有写偏序时,可能会导致两个支出项目各自检查都合法,但加在一块就超支了。

幻读会导致写偏序

上述例子都可以归纳为以下模式:

  1. 通过 select 语句 + 条件过滤出符合条件的所有行。

  2. 依赖上述结果,应用侧代码决定是否继续。

  3. 如果应用侧决定继续,就执行更改(插入、更新或者删除),并提交事务。

步骤 3 会导致另一个事务的步骤 1 失效,即如果另一个事务此时重新执行 1 的 select 查询,会得到不同的结果,进而影响步骤 2 是否继续的决策。

当然,这些步骤可能以不同的顺序发生,如可以首先写入,然后进行 select 查询,根据查询结果决定事务是否提交。

对于医生值班的例子,我们可以通过 for update 语句来锁住步骤 1 中查询到的结果;但对于其他例子,步骤 1 查询结果集为空,则无法锁住任何东西。

这种一个事务的写入会改变另一个事务的查询结果的现象,称为幻读。快照隔离能够避免只读事务中的幻读,但对于读写事务,就很可能出现由幻读引起的写偏序问题。

物化冲突

幻读在步骤 1 读不到任何对象来进行加锁。那很自然的一个想法就是,能不能手动引入一些对象槽来代表不存在的对象,从而是的加锁成为可能。

在预定会议室的例子中,可以创建一个会议室号 + 时间段表,比如每 15 分钟一个时间段。可以在该表中插入未来几个月中所有可预订的会议室号 + 时间段。如果现在一个事务想要预定某个会议室的某个时间段,便可在该表中将对应对象都锁住,然后执行预定的操作。

需要强调的是,该表只用于防止同时预定同一个会议室的同一个时间段,并不用来存储预定相关信息,可以理解为是个锁表,每一行都是一把锁。

这种方法称为物化冲突(materializing conflicts),因为它将幻读转化为数据库物理中实实在在的表和行。但如何对冲突进行合理的物化,很难且易出错。并且,此方法会将解决冲突的细节暴露给了应用层(因为应用层需要感知物化出来的表)。因此,这是最不得以的一种方法,如果数据库本就支持可串行化,则大多数情况下,可以直接使用可串行隔离级别。


谢谢你读完本文(///▽///)

如果你想尝鲜图数据库 NebulaGraph,记得去 GitHub 下载、使用、(^з^)-☆ star 它 -> GitHub;如果你有更高的性能、易用性、运维实施等方面的需求,你也可以随时 联系我们,获取进一步的帮助哦~