介绍弱隔离级别

为什么要有弱隔离级别

如果两个事务操作的是不同的数据, 即不存在数据依赖关系, 则它们可以安全地并行执行。但是当出现某个事务修改数据而另一个事务同时要读取该数据, 或者两个事务同时修改相同数据时, 就会出现并发问题。

在应用程序的开发中,我们通常会利用锁进行并发控制,确保临界区的资源不会出现多个线程同时进行读写的情况,这其实就对应了事务的最高隔离级别:可串行化。可串行化隔离意味着数据库保证事务的最终执行结果与串行 (即一次一个, 没有任何并发) 执行结果相同。


那么为什么应用程序中可以提供可串行化的隔离级别,而数据库却不能呢?其实根本原因就是应用程序对临界区大多是内存操作,而数据库要保证持久性(Durability),需要把临界区的数据持久化到磁盘,可是磁盘操作比内存操作要慢好几个数量级,一次随机访问内存、 固态硬盘 和 机械硬盘,对应的操作时间分别为几十纳秒、几十微秒和几十毫秒,这会导致持有锁的时间变长,对临界区资源的竞争将会变得异常激烈,数据库的性能则会大大降低。

所以,数据库的研究者就对事务定义了隔离级别这个概念,也就是在高性能与正确性之间做一个权衡,相当于明确地告诉使用者,我们提供了正确性差一点但是性能好一点的模式,以及正确性好一点但是性能差一点的模式,使用者可以根据自己的业务场景来选择一个合适的隔离级别。

弱隔离级别带来的风险

弱隔离级别就是非串行化隔离级别。

较弱的隔离级别, 它可以防止某些并发问题,但并非全部的并发问题。

使用这些弱隔离级别,事务并发执行时,可能会出现异常情况,带来一些难以捉摸的隐患,因此,我们需要了解弱隔离级别存在的并发问题以及如何防范存在的并发问题。 然后, 我们就可以使用所掌握的工具和方法来构建正确、 可靠的应用。

各种隔离级别

SQL-92 标准定义了 4 种事务的隔离级别:读未提交(Read Uncommitted)、读已提交(Read Committed)、可重复读(Repeatable Read)和可串行化(Serializable),在后面的发展过程中,又增加了快照隔离级别(Snapshot Isolation)。

不同的弱隔离级别解决了不同的并发问题(正确性问题),同时也存在一些并发问题。


下面是各种隔离级别及对应的并发问题:

  • ️代表该隔离级别已解决该并发问题;
  • 代表该隔离级别未解决该并发问题。
脏写 脏读 不可重复读 更新丢失 幻读 写倾斜
读未提交 ️
读已提交 ️ ️
可重复读 ️ ️ ️ ️
快照 ️ ️ ️ ️ ️
可串行化 ️ ️ ️ ️ ️ ️

SQL 标准对隔离级别的定义还是存在一些缺陷,某些定义模棱两可,不够精确,且不能做到与实现无关,所以上面的表格只是对常见的隔离级别并发问题的定义,你可以把它当成一个通用的标准参考。

当你使用某一个数据库时,需要读一下它的文档,确定好它的每一种隔离级别具体的并发问题。

  • MySQL 的默认隔离级别为:可重复读。
  • Oracle、PostgreSQL 的默认隔离级别为:读已提交

事务并发执行时,存在的并发问题

如果两个事务操作的是不同的数据, 即不存在数据依赖关系, 则它们可以安全地并行执行。但是当出现某个事务修改数据而另一个事务同时要读取该数据, 或者两个事务同时修改相同数据时, 就会出现并发问题。

并发问题总结:

  • 脏写:一个事务覆盖了其他事务尚未提交的写入。
  • 脏读:一个事务读到了其他事务尚未提交的写入。
  • 不可重复读:一个事务内,多次读取同一个记录的结果不一样。
  • 更新丢失:两个事务同时执行“读-修改-写回”操作序列,事务 A 覆盖了 事务 B 的写入,但又没有包含 事务 B 修改后的值,最终导致了部分更新数据发生了丢失。
  • 幻读:一个事务内,多次读取满足指定条件的数据,读出来的结果不一样。
  • 写倾斜:事务首先查询数据,根据返回的结果而作出某些决定,然后修改数据库。当事务提交时,支持决定的前提条件已不再成立。

脏写

一个事务覆盖了其他事务尚未提交的写入。

脏读

一个事务读到了其他事务尚未提交的写入。


举例说明脏读

事务 B 修改了 x,在事务 B 提交之前,事务 A 读到了 x 修改后的数据。这时事务 B 回滚了,相当于事务 A 读到了一个无效的数据(未实际提交到数据库中的数据),事务 A 的读就是脏读。

时间顺序 Session A Session B
1 begin; begin;
2 update t1 set c1 = 'B' where id = 1
3 select * from t1 where id = 1
4 commit;
5 rollback;

不可重复读

一个事务内,多次读取同一个记录的结果不一样。(一个事务能够读到另一个事务对同一个记录的修改)


举例说明不可重复读

事务 A 读取了 x,然后事务 B 修改了 x 并提交。这时事务 A 再次读取 x,发现两次读取同一个记录的结果不一样,这就是不可重复读。

时间顺序 Session A Session B
1 begin; 该事务设置自动提交
2 select * from t1 where id = 1(此时读到 A)
3 update t1 set c1 = 'B' where id = 1
4 select * from t1 where id = 1(此时读到 B)
update t1 set c1 = 'C' where id = 1
5 select * from t1 where id = 1(此时读到 C)

更新丢失

两个事务同时执行“读-修改-写回”操作序列,事务 A 覆盖了 事务 B 的写入,但又没有包含 事务 B 的修改,最终导致了部分更新数据发生了丢失。


举例说明更新丢失

事务 A 先读取某记录,然后事务 B 再读取某记录,事务 B 修改并写回,紧接着 事务 A 修改并写入。事务 A 覆盖了 事务 B 的写入,但又没有包含 事务 B 的修改,最终导致事务 B 的更新丢失了。

时间顺序 Session A Session B
1 begin; begin;
2 select * from t1 where id = 1;
3 select * from t1 where id = 1
4 update t1 set col1 = 2 where id = 1;
5 update t1 set col1 = 3 where id = 1;

幻读

一个事务内,多次读取满足指定条件的数据,读出来的结果不一样(一个事务能够读到另一个事务创建的满足条件的记录)


举例说明幻读

事务 A 读取一组满足条件 1 的数据,之后事务 B 创建了满足条件 1 的数据,使其满足条件 1 并提交,如果事务 A 用相同的 条件 1 再次读取,得到一组不同于第一次读取的数据。这就叫幻读。

时间顺序 Session A Session B
1 begin; 该事务设置自动提交
2 select * from t1 where id > 0
3 insert into t1 values(B)
4 select * from t1 where id > 0(能读到 B)

不可重复读和幻读都是一个事务内,多次执行相同的查询,结果不一样。那两者有什么区别呢?

  • 幻读 主要说的是,读到了另一个事务的 insert 或者 update 的满足条件的记录
  • 不可重复读 主要说的是,读到了另一个事务对同一个记录的 update

写倾斜

写倾斜就是:事务首先查询数据,根据返回的结果而作出某些决定,然后修改数据库。当事务提交时,支持决定的前提条件已不再成立。

如何防止并发问题

现在我们已经知道了每一个隔离级别可能会出现的并发问题,如果当前数据库使用了某一个隔离级别,我们也知道这个隔离级别存在的并发问题,是否有办法来避免并发问题呢?以及对于避免并发问题是如何实现的?

有些并发问题只能通过提升隔离级别来避免,接下来,我们就针对每一种并发问题一一讨论。

防止脏写

允许脏写这种并发问题出现的数据库基本上是不可用的。因此所有的隔离级别都不允许出现脏写这种并发问题。

防止“脏写”就意味着,写数据库时, 只会覆盖已成功提交的数据。

防止脏写通常的方式是推迟第二个写请求,直到前面的事务完成提交(或者中止)。


数据库通常采用行级锁来防止脏写:如果两个事务同时尝试写入同一个对象时 ,以加锁的方式来确保第二个写入等待前面事务完成(包括中止或提交)。

这种锁定是由处于读已提交模式 ( 或更强的隔离级别) 的数据库自动完成的。

防止脏读

防止 “脏读”就意味着,读数据库时, 只能看到已成功提交的数据。

如果业务中不能接受脏读,那么隔离级别要在“读已提交”隔离级别或者以上。

当有以下需求时,需要防止脏读:

  • 如果事务需要进行多个操作更新多个对象,我们需要保证另一个事务或者应用层要么看到所有操作执行前的状态,要么看到所有操作完成后的状态,而不能看到部分操作完成的中间状态。如果我们要提供这样的保证,那么就必须防止脏读。脏读意味着另一个事务可能会看到部分更新, 而非全部,观察到部分更新的数据可能会造成用户的困惑。
  • 如果事务发生中止,则所有写入操作都需要回滚,那么就必须防止脏读,避免用户观察到一些稍后被回滚的数据, 而这些数据实际并未实际提交到数据库中。

防止脏读的解决方案:

  • 两段锁协议;
  • 存储数据的旧版本和新版本。

一种选择是使用和防止脏写相同的锁,所有试图读取该对象的事务必须先申请锁,事务完成后释放锁,从而确保不会发生读取到一个脏的、 未提交的值。

然而, 加锁的方式在实际中并不可行, 因为运行时间较长的写事务会导致许多只读的事务等待太长时间, 这会严重影响只读事务的响应时间。应用程序任何局部的性能问题会扩散,进而影响整个应用,产生连锁反应。

因此, 大多数数据库采用了下面的方式来防止脏读:对于每个待更新的对象, 数据库都会维护对象的两个版本(其旧值 和 当前持锁事务将要设置的新值)。在事务提交之前, 其他事务的读操作都读取旧值;仅当写事务提交之后, 才会切换到读取新值。而 MySQL 使用了多版本并发控制来防止脏读,多版本比两个版本更加通用。

防止不可重复读

防止“不可重复读”就意味着,一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。

不能忍受不可重复读的场景:

  • 备份场景:备份任务要复制整个数据库,这可能需要花费几小时才能完成。在备份过程中,数据可以继续写入数据库。因此,备份里可能包含部分旧版本数据和部分新版本数据。 如果从这样的备份进行恢复,那么就导致了永久性的不一致。

如果业务中不能接受不可重复读,那么隔离级别要在“可重复读”隔离级别或者以上。

在 MySQL 种,可重复读隔离级别即快照级别隔离。快照级别隔离的总体想法是:每个事务总是在某个时间点的一致性快照中读取数据。

为了实现快照级别隔离, MySQL 数据库采用了一种被称为多版本并发控制(MultiVersion Concurrency Control,MVCC)的机制。

防止更新丢失

更新丢失可能发生在这样一个操作场景中:应用程序从数据库读取某些值,根据应用逻辑做出修改,然后写回新值 (read-midify-write 过程)。当有两个事务在同样的数据对象上执行类似操作时,后一个写操作并不包含前一个写操作的修改,最终导致前一个写操作的修改丢失。

更新丢失属于写事务并发冲突。

防止更新丢失,目前有多种可行的解决方案。

  • 原子更新操作:许多数据库提供了原子更新操作,以避免在应用层代码完成“读-修改-写回”操作序列,如果数据库支持原子更新操作的话,通常这就是防止更新丢失最好的解决方案。

    • 原子操作通常采用对读取对象加独占锁的方式来实现,这样在更新被提交之前其他事务不可以读取它。
    • 原子操作的另一种实现方式是:强制所有的原子操作都在单线程上执行。这也是 Redis 防止更新丢失的解决方案
  • 显式的加锁:既然原子操作采用对读取对象加独占锁的方式来实现,那么我们也可以显式的锁定待更新的对象,使“读-修改-写回”操作序列串行执行。例如使用 MySQL 的 select ...... for update;

原子更新操作和 显式的加锁 都是通过强制“读-修改-写回”操作序列串行执行来防止丢失更新。

  • 自动检测更新丢失:先让“读-修改-写回”操作序列并发执行,但如果事务管理器检测到了更新丢失风险,则会中止当前事务,并强制回退到安全的“读-修改-写回”方式。
  • 比较并设置:先让“读-修改-写回”操作序列并发执行,如果读取的内容已经发生了变化且值与“旧内容”不匹配,则更新失败,需要应用层再次检查并在必要时进行重试。例如 update t1 set col1 = '新内容' where id = 1 and col1 = '旧内容';

自动检测更新丢失

PostgreSQL 的可重复读, Oracle 的可串行化以及 SQL Server 的快照级别隔离等,都可以自动检测何时发生了更新丢失,然后会中止违规的那个事务。

但是, MySQL 中 InnoDB 存储引擎的可重复读却并不支持自动检测更新丢失。

防止幻读 & 写倾斜

防止幻读:

  • 使用 可串行化隔离级别
  • 在 MySQL 的 可重复读隔离级别下,使用 select ...... for update;

使用可串行化隔离级别可以防止幻读。

可串行化隔离通常被认为是最强的隔离级别。使用可串行化隔离级别可以防止所有可能的竞争条件。

可串行化隔离保证即使事务可能会并行执行,但最终的执行结果与每次执行一个事务(即串行执行)的结果相同。

可串行化隔离级别的实现有以下几种方式:

  • 实际串行执行:
  • 两段锁 + 索引区间锁:将两段锁与索引区间锁结合使用,实现可串行化隔离
  • 可串行化快照隔离:(这个暂时还没有了解)

MySQL 的可串行化隔离级别使用了第 2 种方法(两段锁 + 索引区间锁)


写倾斜就是:事务首先查询数据,根据返回的结果而作出某些决定,然后修改数据库。当事务提交时,支持决定的前提条件已不再成立。写倾斜可能发生在这样一个操作场景中:

  1. 第一步 select:应用程序从数据库读取一组满足条件 1 的数据
  2. 第二步 决定:根据查询的结果,应用层代码来决定下一步的操作(有可能继续,或者报告错误井中止)
  3. 第三步 写入:如果应用程序决定继续执行,它将发起数据库写入(insert,update 或 delete)并提交事务。

而第 3 步的这个写操作会改变第 2 步做出决定的前提条件,如果两个事务并发执行这样的“读取-决定-写入”操作序列,那么后一个写入改变了前一个写入执行的前提条件,导致出现意料之外的结果。


防止写倾斜

对于写倾斜问题,有几种可能的解决方案:

  • 只使用 可串行化隔离级别 即可避免写倾斜(使用索引区间锁,避免其他事务写入满足条件的行)
  • 更改“读取-决定-写入”操作序列的执行顺序 为 “写入-读取-决定”:先写入,然后 select 查询并加独占锁(select ...... for update),最后根据查询的结果来决定是否提交或者放弃。
  • 实体化冲突,也称物化冲突:有的业务场景 select 查询的是不满足给定搜索条件的行(例如 select * from t1 where id != 1)如果第 1 步的查询根本没有返回任何行,则 select ...... for update 也就无从加锁,只能考虑实体化冲突。

本质上这三种可能的解决方案都是对事务所依赖的行显式的加锁。

对于实体化冲突(物化冲突)的说明

如果问题的关键是查询结果中没有对象(空)可以加锁,或许可以人为引人一些可加锁的对象。这种方法称为实体化冲突(或物化冲突),它把幻读问题转变为针对数据库中一组具体行的锁冲突问题。

然而,弄清楚如何实现实体化往往也具有挑战性,实现过程也容易出错,这种把一个并发控制机制降级为数据模型的思路总是不够优雅。出于这些原因,除非万不得己,没有其他可选方案,不推荐采用实体化冲突。

参考资料

24|事务(三):隔离性,正确与性能之间权衡的艺术-极客时间 (geekbang.org)

《数据密集型应用系统设计》

弱隔离级别 & 事务并发问题的更多相关文章

  1. Mysql中的读锁,写锁,乐观锁及事务隔离级别和并发问题

    mysql读锁,写锁,乐观锁 读锁,也叫共享锁(shared lock) SELECT * FROM table_name  WHERE ...  LOCK IN SHARE MODE 写锁,也叫排他 ...

  2. 事物的隔离级别与并发完美体现了cap理论(确保数据完整、安全、一致性,在此基础上实现高性能访问(鱼和熊掌不可兼得)

    事物的隔离级别与并发完美体现了cap理论(确保数据完整.安全.一致性,在此基础上实现高性能访问(鱼和熊掌不可兼得)

  3. MySQL事务隔离级别 解决并发问题

    MySQL事务隔离级别 1. 脏读: 骗钱的手段, 两个窗口或线程分别调用数据库转账表,转账后未提交,对方查看到账后,rollback,实际钱没转. 演示方法: mysql默认的事务隔离级别为repe ...

  4. sqlserver默认隔离级别下并发批量update同一张表引起的死锁

    提到死锁,最最常规的场景之一是Session1 以排它锁的方式锁定A表,请求B表,session2以排它锁的方式锁定B表,请求A表之类的,访问顺序不一致导致死锁的情况本文通过简化,测试这样一种稍显特殊 ...

  5. mysql 5.6 read-committed隔离级别下并发插入唯一索引导致死锁一例

    今天,某个环境又发生了死锁,如下: *** (1) TRANSACTION:TRANSACTION 735307073, ACTIVE 0 sec insertingmysql tables in u ...

  6. mysql事务_事务隔离级别详解

    使用事务语法 1. 开启事务start transaction,可以简写为 begin 2. 然后记录之后需要执行的一组sql 3. 提交commit 4. 如果所有的sql都执行成功,则提交,将sq ...

  7. MySQL之事务隔离级别和MVCC

    事务隔离级别 事务并发可能出现的问题 脏写 事务之间对增删改互相影响 脏读 事务之间读取其他未提交事务的数据 不可重复读 一个事务在多次执行一个select读到的数据前后不相同.因为被别的未提交事务修 ...

  8. 数据库事务隔离级ORACLE数据库事务隔离级别介绍

    本文系转载,原文地址:http://singo107.iteye.com/blog/1175084 数据库事务的隔离级别有4个,由低到高依次为Read uncommitted.Read committ ...

  9. 事务与隔离级别------《Designing Data-Intensive Applications》读书笔记10

    和数据库打交道的程序员绕不开的话题就是:事务,作为一个简化访问数据库的应用程序的编程模型.通过使用事务,应用程序可以忽略某些潜在的错误场景和并发问题,由数据库负责处理它们.而并非每个应用程序都需要事务 ...

随机推荐

  1. 初识Java GUI

    1. 使用Java  Swing 显示的窗口如下 在原有代码基础上添加代码实现对窗口大小 标题等信息

  2. Tapdata “设擂招贤”携手 LeetCode 举办全球极客技术竞赛

      2021年11月28日 Tapdata 专场全球极客技术竞赛将在 LeetCode 平台开赛,面向程序员"设擂招贤",打擂成功的前50名挑战者将优先获得 Tapdata 高端技 ...

  3. antd vue 折叠面板 v-for 循环点击无效

    问题描述 实现一个折叠面板点击展开,但是必须点击两次才能展开,第一次无效 <a-collapse-panel v-for="(item, index) in dataMap" ...

  4. Spring知识点详解

    1.Spring 概述 1.1.Spring 的概念和特点 Spring 是一个轻量级的控制反转(IoC)和面向切面(AOP)的开源容器框架,它是由 Rod Johnson(音乐学博士)所创建,其核心 ...

  5. ooday06 内部类

    笔记: 成员内部类:应用率低,了解 类中套类,外面的称为外部类,里面的称为内部类 内部类通常只服务于外部类,对外不具备可见性 内部类对象只能在外部类中创建 内部类中可以直接访问外部类的成员(包括私有的 ...

  6. JavaScript数组方法总结及手写

    目录 手写数组衍生方法 1.检测是否为数组 2.类数组转化为数组 3.数组扁平化 4.数组去重 5.数组使用Math.max 手写数组内置方法 1. Array.prototype.filter 2. ...

  7. 流程控制语句break

    break语句 用于结束循环结构,通常与分支结构if一起使用 即非正常循环,在中间循环的时候直接退出 注意break打断的是循环语句,不是if语句 注意while循环中一般需要有改变变量这个操作,否则 ...

  8. Nginx 的基本概念

    Nginx 简介 什么是 Nginx Nginx 是一个高性能的 HTTP 和 反向代理 web服务器 占用内存少,并发能力强,高性能,热部署 但不支持 Java,Java 得配合 tomcat 使用 ...

  9. ubu18时间设置

    ubu18 日期设置 1.date date 命令修改系统时间,重启失效,需要写入硬件Bios #查看日期 date #修改日期 date -s "2022-01-14 09:32:00&q ...

  10. 【原创】医鹿APP九价HPV数据抓包分析

    本文所有教程及源码.软件仅为技术研究.不涉及计算机信息系统功能的删除.修改.增加.干扰,更不会影响计算机信息系统的正常运行.不得将代码用于非法用途,如侵立删! 医鹿APP九价HPV数据抓包分析 操作环 ...