精通java事务编程-弱隔离级别之快照隔离和可重复读(代码片段)

JavaEdge. JavaEdge.     2022-11-30     191

关键词:

表面看,RC已满足事务所需的一切特征:支持中止(原子性),防止读取不完整的事务结果,并防止并发写的混乱。这点很关键!为我们的开发省去一大堆麻烦。

但此隔离级别仍有很多地方可能产生并发错误。如图-6说明RC可能发生的问题。

Alice在银行有1000存款,分为两个账户,每个500。现有一笔转账交易从账户1转移100到账户2。若她在提交转账请求后、银行DB系统执行转账的过程中间,查看两个账户的余额,她可能看到账号2在收到转账前的余额(500),和账户1在完成转账之后的余额(400)。对Alice,貌似她的账户总共只有900,100消失!

这种异常就是不可重复读(nonrepeatable read)或读倾斜(read skew):若Alice在交易结束时再读取账户1的余额,将看到和她之前的查询看到的不同的值(600)。RC下,不可重复读被认为是可接受的:Alice 看到的帐户余额的确都是账户当时的最新值。

术语 倾斜(skew) 这词有些滥用:以前使用它是因为热点的不平衡工作量,而在此意味着异常时序。

Alice案例不是长期持续的问题,几s后当她刷新银行页面,可能就看到一致的帐户余额。但有的场景不能容忍这种暂时的不一致:

  • 备份

    备份需复制整个DB,大型DB可能需数h。备份进程运行时,DB仍会接受写。因此镜像备份里可能包含一些旧版本数据和一些新版本数据。从这样的备份中恢复,最终就会导致永久性的不一致(如那些消失的存款)

  • 分析查询和完整性检查

    有时查询会扫描几乎大半个DB。这类查询在分析中很常见,也可能是定期的数据完整性检查(监视数据损坏情况)。若这些查询在不同时间点观察DB,则可能会返回无意义的结果

【快照隔离】是这类问题最常见解决方案。每个事务都从DB的一致性快照(consistent snapshot)中读取,即事务一开始所看到是最近提交的数据。即使这些数据随后被另一个事务更改,每个事务也只能看到该特定时间点的旧数据。

快照隔离对长时间运行的只读查询(如备份和分析)很有用。若数据在查询执行的同时变化,则很难理解查询结果的物理含义。而若查询的是DB在某特定时间点冻结时的一致性快照,则查询结果含义明确。

快照隔离很流行:PostgreSQL、InnoDB引擎的MySQL、Oracle、SQL Server 等都支持。

实现快照隔离

类似RC,快照隔离的实现通常使用写锁防止脏写,正在进行写入的事务会阻止另一个事务修改同一个对象。但读取则不无需加锁。性能角度,快照隔离的关键点:读不会阻塞写,写不会阻塞读。这允许DB可在正常处理写入的同时,在一致性快照上执行长时间的只读查询,且两者之间没有任何锁竞争。

为实现快照隔离,DB用类似图-4防脏读但却更通用的机制。考虑到多个正在进行的事务可能在不同时间点查看数据库状态,所以DB保留对象的多个不同的提交版本,所以这种技术也称为多版本并发控制(MVCC, multi-version concurrency control)。

若只是为提供RC,而非完整的快照隔离,则只保留对象的两个版本即可:

  • 已提交的旧版本
  • 尚未提交的新版本

所以,支持快照隔离的存储引擎一般也直接使用MVCC实现RC。典型做法:

  • 在RC下,为每个不同的查询单独创建一个快照
  • 而快照隔离则是对整个事务使用相同的一个快照。

图-7说明如何在 PostgreSQL 中实现基于 MVCC 的快照隔离(其他实现基本类似)。当事务开始时,首先赋予一个唯一、单调递增 1 的事务ID(txid)。每当事务向DB写入新内容,所写入的数据都会被标记写入者的事务ID。

表中的每行都有个 created_by 字段,其中包含将该行插入到表中的的事务ID。都有个 deleted_by 字段,最初是空的。如某事务删除了一行,那么该行实际上并未从数据库中删除,而是通过将 deleted_by 字段设置为请求删除的事务的 ID 来标记为删除。稍后时间,当确定没有事务可以再访问已删除的数据时,数据库中的gc过程会将所有带有删除标记的行移除,并释放其空间。

这样的一笔UPDATE 操作在内部会被转换为一个 DELETE 和一个 INSERT 。图-7中,事务13从账户2扣100,将余额从 500改为400。account 表会出现两条账户2的记录:

  • 余额为500的行被标记为被事务13删除
  • 余额为400的行由事务13创建

一致性快照的可见性规则

当事务读DB时,通过事务ID可决定哪些对象可见,哪些不可见。要想对上层应用维护好快照的一致性,需仔细定义可见性规则:

  1. 每个事务开始时,DB列出当时所有当时还在进行中(即尚未提交或中止)的其它事务,然后忽略这些事务完成的部分写入(尽管之后可能会被提交),即不可见
  2. 所有中止事务所做的任何修改全部不可见
  3. 较晚事务ID(即晚于当前事务开始)所做的任何修改不可见,而不管这些事务是否已完成提交
  4. 此外的所有其他写入都对应用查询可见

以上规则适用于创建、删除操作。图-7中,当事务12从账户2读时,会看到500余额,因为500余额的删除是由事务13完成的(根据规则 3,事务12看不到事务13执行的删除),同理400美元记录的创建也不可见。

即若如下两个条件都成立,则该数据对象对事务可见:

  • 读事务开始的时刻,创建该对象的事务已完成提交
  • 对象未被标记为删除或即使被标记为删除了,但删除事务在当前读事务开始时还没有完成提交

长时间运行的事务可能会使用快照很长时间,其他事务角度,它可能在持续访问正在被覆盖或删除的内容。由于没有就地更新,而是每次修改总创建一个新版本,因此DB可以以较小运行代价来维护一致性快照。

索引和快照隔离

多版本DB如何支持索引?一种方案是索引直接指向对象所有版本,并且需要索引查询过滤掉对当前事务不可见的对象版本。当后台的GC进程决定删除某个事务不可见的旧对象版本时,相应索引条目也随之删除。

实践中,许多细节决定了多版本并发控制的性能,如:

  • 可将同一对象的不同版本放入同一内存页,PostgreSQL如此优化可避免更新索引
  • CouchDB、Datomic 和 LMDB使用另一种方案。虽然也使用B树,但采用追加/写时复制(append-only/copy-on-write),当需要更新时,不会修改现有的页,而总是创建一个新的修改副本,拷贝必要的内容,然后让父结点或递归向上直到树root都指向新创建的结点。那些不受更新影响的页面都无需复制,保持不变并被父结点所指向。

这种使用追加的B树,每个写入事务(或一批事务)都会创建一个新的B 树,当创建时,从该特定树根生长的树就是该时刻DB的一致性快照。这时就没必要根据事务ID再去过滤对象,每个写入都会修改现有的B树,因为之后的 询可以直接作用于特定快照B-tree(有利于查询性能)。采用这种方案依然需要后台进程来执行压缩和GC。

可重复读与命名混淆

快照隔离对只读事务特别有效。但DB实现用不同名字来称呼:

  • Oracle 中称为可串行化(Serializable)
  • PostgreSQL 和 MySQL 中称为可重复读(repeatable read)

命名混淆原因是SQL标准未定义快照隔离,而仍是基于System R 1975年定义的隔离级别,那时还没快照隔离。而定义了 可重复读,表面看起来接近快照隔离。 所以PostgreSQL 和 MySQL 称快照隔离级别为可重复读(repeatable read),这符合标准要求。

但SQL标准对隔离级别的定义存在缺陷的,模糊,不精确,做不到独立于实现。有几个DB实现了可重复读,但它们实际提供的保证差异很大。IBM DB2 使用 “可重复读” 实现可串行化级别的隔离。

所以导致结果,无人真正知道可重复读到底啥意思。


  1. 事务ID是32位整数,所以大约在40亿次事务后溢出。 PostgreSQL 的 Vacuum 过程会清理老旧的事务 ID,确保事务 ID 溢出(回卷)不会影响到数据。 ↩︎

精通java事务编程-可串行化隔离级别之可串行化的快照隔离(代码片段)

本系列文章描述了DB并发控制的黯淡:2PL虽保证了串行化,但性能和扩展不好性能良好的弱隔离级别,但易出现各种竞争条件(丢失更新,写倾斜,幻读串行化的隔离级别和高性能就是相互矛盾的吗?... 查看详情

精通java事务编程-弱隔离级别之防止更新丢失(代码片段)

RC和快照隔离级别主要都是为解决只读事务遇到并发写时可以看到什么(虽然中间也涉及脏写),还没触及另一种情况:两个写事务并发,而脏写只是写并发的特例。写事务并发带来最着名的问题就是丢失更新... 查看详情

精通java事务编程-可串行化隔离级别之真串行

RC和快照隔离级别可防止某些竞争条件,但并非全部。一些棘手案例,如写偏斜和幻读,会发现可悲情况:隔离级别难理解,且不同DB实现不一(如RR含义天差地别)若检查应用层代码很难判断特定隔离级别下是否安全,尤其是大... 查看详情

精通java事务编程-弱隔离级别之已提交读

若两个事务不触及相同数据,即无数据依赖关系,则它们能安全并行运行。只有当:某事务读取由另一个事务同时修改的数据时或两个事务同时修改相同数据才会出现并发问题。并发BUG很难通过测试找到,因为这样的错误只有在... 查看详情

精通java事务编程-弱隔离级别之已提交读(代码片段)

若两个事务不触及相同数据,即无数据依赖关系,则它们能安全并行运行。只有当:某事务读取由另一个事务同时修改的数据时或两个事务同时修改相同数据才会出现并发问题。并发BUG很难通过测试找到,因为这... 查看详情

精通java事务编程-弱隔离级别之写倾斜与幻读(代码片段)

多个事务并发写相同对象时,会出现脏写和更新丢失两种竞争条件。为避免数据不一致,可:借助DB内置机制或通过显式加锁、执行原子写操作但这还不算并发写可能引发的全部问题。为医院写一个值班管理程序。医... 查看详情

精通java事务编程-可串行化隔离级别之真串行

RC和快照隔离级别可防止某些竞争条件,但并非全部。一些棘手案例,如写偏斜和幻读,会发现可悲情况:隔离级别难理解,且不同DB实现不一(如RR含义天差地别)若检查应用层代码很难判断特定隔离... 查看详情

精通java事务编程-可串行化隔离级别之两阶段锁定(2pl,two-phaselocking)(代码片段)

近30年,DB只有一种广泛使用的串行化算法:两阶段加锁12PL不是2PC请注意,虽然两阶段锁定(2PL)听起来非常类似于两阶段提交(2PC),但是完全不同概念之前我们知道,加锁可防止脏写:... 查看详情

弱隔离级别&事务并发问题

介绍弱隔离级别为什么要有弱隔离级别如果两个事务操作的是不同的数据,即不存在数据依赖关系,则它们可以安全地并行执行。但是当出现某个事务修改数据而另一个事务同时要读取该数据,或者两个事务同时修改相同数据时... 查看详情

数据库的快照隔离级别(snapshotisolation)

隔离级别定义事务操作资源和更新数据的隔离程度,在SQLServer中,隔离级别只会影响读操作申请的共享锁,而不会影响写操作申请的互斥锁。隔离级别控制事务在执行读操作时:在读数据时是否使用共享锁,申请何种类型的隔离... 查看详情

mysql事务隔离级别的实现原理

文章目录一、什么是事务的隔离级别二、再看可重复读原理锁定读(当前读)一致性非锁定读(快照读)隐式锁定(两阶段锁)显式锁定三、总结一、什么是事务的隔离级别在数据库系统中,一个事务是指:由一系列数据库操作... 查看详情

mysql在可重复读事务隔离级别下怎么解决幻读的(代码片段)

目录前言并发事务产生的问题更新丢失回滚丢失覆盖丢失脏读不可重复读幻读快照读和当前读幻读验证快照读如何避免幻读当前读如何避免幻读可重复读隔离级别发生幻读情况小结前言Mysql在可重复读(REPEATABLEREAD)隔离级别下࿰... 查看详情

springboot系列教程之事务隔离级别知识点小结

SpringBoot系列教程之事务隔离级别知识点小结上一篇博文介绍了声明式事务@Transactional的简单使用姿势,最文章的最后给出了这个注解的多个属性,本文将着重放在事务隔离级别的知识点上,并通过实例演示不同的事务隔离级别下... 查看详情

第31讲:mysql事务的并发问题以及事务的隔离级别

文章目录1.事务的并发问题1.1.事务并发之脏读1.2.事务并发之不可重复读1.3.事务并发之幻读2.事务的隔离级别3.模拟事务并发问题的产生以及如何避免3.1.事务并发问题脏读的模拟以及避免3.1.1.模拟事务并发脏读的问题3.1.2.解决事... 查看详情

mysql基础篇之事务真的是隔离的吗?--08(代码片段)

Mysql基础篇之事务真的是隔离的吗?--08引言“快照”在MVCC里是怎么工作的?更新逻辑小结引言我在第3篇文章和你讲事务隔离级别的时候提到过,如果是可重复读隔离级别,事务T启动的时候会创建一个视图read-view&... 查看详情

事务隔离级别

事务隔离是数据库处理的基础之一,Isolation是ACID中I的缩写,当多个事务同时进行更改和执行查询时,隔离级别是微调性能和可靠性、一致性和结果再现性之间的平衡的设置MySQL支持以下几个隔离级别REPEATABLEREAD(innodb使用的默认级... 查看详情

数据库的快照隔离级别(snapshotisolation)(代码片段)

...bsp;转自:https://www.cnblogs.com/ljhdo/p/5037033.html隔离级别定义事务处理数据读取操作的隔离程度,在SQLServer中,隔离级别只会影响读操作申请的共享锁(SharedLock),而不会影响写操作申请的互斥锁(ExclusiveLock),隔离级别控制读操... 查看详情

程序员面试宝典之mysql数据库innodb引擎的4个隔离级别

...Readuncommitted(读未提交):,最低的隔离级别,可以一个事务读到其他事务没有提交的数据,也称脏读,这个隔离级别很少人用2.Readcommitted(读已提交):相比于读未提交,这个隔离级别只能读到其他事物已经提交了的数据,这... 查看详情