之前在学习InnoDB的事务隔离级别时,零星地了解了有哪些隔离级别,大致思路。
现在要系统、详细地阐述事务的隔离级别。

1. 什么是事务

In a database management system, a transaction is a single unit of logic or work, sometimes made up of multiple operations. Any logical calculation done in a consistent mode in a database is known as a transaction. One example is a transfer from one bank account to another: the complete transaction requires subtracting the amount to be transferred from one account and adding that same amount to the other.

在数据库管理系统中,事务是逻辑或工作的单个单元,有时由多个操作组成。在数据库中以一致模式进行的任何逻辑计算都称为事务。一个示例是从一个银行帐户到另一个银行帐户的转帐:完整的交易需要从一个帐户中减去要转帐的金额,然后将相同的金额加到另一个帐户中。

——维基百科

数据库对数据的操作无外乎CRUD,通常完成一个业务逻辑可能会涉及到不止一个操作,比如(将A的分数翻倍,那么要先查询分数、计算结果、更新数据)。也就是说,由这些逻辑上相关联的操作组成一个逻辑工作单元,就是事务。

2.事务状态

事务就像多线程(多进程)一样,会有个类似生命周期的状态转换机制。
参考文章:
https://www.gatevidyalay.com/acid-properties/

2.1事务的操作

  • 读操作
  • 写操作

读、写操作都是有个在内存中的缓冲区,数据库的设计是Buffered-Read/Write的形式

2.2事务的六种状态

image.png

1.活动状态(Active state)

  • 这是第一个生命周期
  • 在事务开始执行时,就会进入此状态
  • 所有的数据RW都会在缓冲区

    2.半提交状态(Partially committed state)

  • 事务最后一个操作执行后(未完成前),进入此状态

  • 进入此状态后,事务被认为是“半提交”的,这里可以理解为正在提交
  • 它不是提交状态,因为此时数据仍存在于内存缓冲区中

    3.已提交状态(Committed state)

  • 所有操作都已经同步到数据库中的持久化存储当中了以后,变为已提交状态,对应上一个状态的“完成时”

  • 现在,事务被认为是“已提交”的,即数据库可以保证数据持久化了。

ps:

  • 事务进入这个状态后,便再也无法回滚,因为事务是无法被撤销的,此时整个系统已经进入新的同一状态了。
  • 要想撤销只能通过提交已提交事务的反操作事务。

4.失败状态(Failed state)

活动状态或半提交状态遇到任何问题都会进入失败状态。

5.中止状态(Aborted state)

  • 失败的事务一定要进行回滚
  • 回滚完成以后进入此状态

    6.终止状态(Terminated state)

  • 谐音梗

  • 中止状态或已提交状态会进入此状态,表示事务执行结束

3.事务隔离

3.1为什么要隔离

其实,事务隔离和并发编程并没有什么区别。我们知道,并发编程当中,为了避免线程冲突,我们诞生了很多很多的从简到复杂的操作,比如原子操作、线程池等等。
同样的,数据库作为一个服务(进程),同样要面临“线程冲突”,正如上面所说的,一个事务是原子的,同样也说明在实际环境中会出现同一时间多个事务执行。在这里,可以稍微把“事务”代换为“线程”或“原子操作”。那么我们已经有多线程的各种机制了,为什么还要搞一套事务隔离的机制呢?因为数据库通常是一个服务或进程,多个其他进程去消费的,进程之间就没有线程管理的机制可用了,所以到这里,又可以将“事务”代换为“多进程(任务)”。

3.2事务的4个特征

参考文章:
https://www.gatevidyalay.com/acid-properties/
之前已经说了一个原子性,总共4个,分别如下:

1.原子性

你一定在多线程编程中听到过“原子性”这个词:如果一个线程执行一个原子操作,这意味着另一个线程无法看到该操作进行到一半的结果。另一个线程只能看到操作之前或操作之后的状态,而不是介于两者之间的状态。

事务的原子性和多线程很像,但还有些不同。_

而事务中原子性的意思是:中止出错的事务,并撤销该事务进行的所有变更。叫做可中止性更通俗易懂。

2.一致性

ACID中的一致性是什么意思呢?如果做一个财务系统,账户A转钱到账户B,那么账户A中减少的钱与账户B中增加的钱必须相等。这并不是数据库可以保证的事情,而是应用程序来保证的。

原子性,隔离性、持久性都是数据库的属性,而一致性是应用程序的属性。应用程序可以用原子性和隔离性来实现一致性,所以ACID中的C就是用来凑缩写单词的。

3.隔离性

多线程中的原子性,其实就是事务ACID中的隔离性——同时执行的事务不能相互冒犯。这意味着,每个事务都可以认为它是唯一在整个数据库上运行的事务。数据库确保多个事务提交后,结果与它们按顺序一个接一个地运行是一样的,尽管实际上它们可能是并发执行的。

4.持久性

持久性,就是安全地存储数据,而不用担心丢失。
一旦事务成功完成,即使发生硬件故障或数据库崩溃,写入的任何数据也不会丢失。数据库的预写日志或分布式系统的复制都可以为持久性做出承诺。

3.3数据库的读写问题

事务隔离是为了解决“线程冲突”的方式,那明白方式之前,要先明白问题是啥。
本节参考:
https://www.gatevidyalay.com/concurrency-problems-in-transaction/
image.png

1. 脏读(Dirty Read Problem)

读取由未提交的事务写入的数据被称为脏读取。

“脏”这个词在之前将Linux虚拟内存的文章里“脏内存页”提到过,它的概念是一个将原始数据更新或空数据写入后的数据,称之为“脏数据”,是等着要被更新到它该待着的地方,脏数据是放在一个临时的地方的。

此读取称为脏读取,因为 :

  • 未提交的交易始终总是有机会滚动。
  • 因此,未提交的事务可能会使其他事务读取甚至不存在的值。
  • 这导致数据库不一致。

    2.不可重复读(Unrepeatable Read Problem)

    在一个事务中,重复读取一个数据,但是在两次读数据之间,其他事务修改并提交了,这样的话,在这个事务当中两次(不一定连续两次,通常两次隔一定时间)读取数据会发现数据不同。

    3.丢失更新 (Lost Update Problem)

    多个事务并发更新数据,会使得数据更新冲突导致失败

    第一类事物丢失:(称为回滚丢失)

    对于第一类事物丢失,就是比如A和B同时在执行一个数据,然后B事物已经提交了,然后A事物回滚了,这样B事物的操作就因A事物回滚丢失了。
    举个例子,比如我有1000元。买一个东西,花了100元。然后我朋友给我转了1000元。理论上这两个事物正常的话,我应该还有1900元。
    但是比如现在两个A,B事物同时进行,第一步都先查询我余额还有1000元,然后B事物给我转了1000元,提交了,理论上我还有2000元。然后我买东西,100元的,买到一半,我事物回滚,就回滚成了1000元。(回滚丢失)如果我不回滚,也提交了,我就还剩900元(也就是下面介绍的第二类事物丢失,覆盖丢失)。

    第二类事物丢失:(提交覆盖丢失、脏写)

    对于第二类事物丢失,也称为覆盖丢失,就是A和B一起执行一个数据,两个同时取到一个数据,然后B事物首先提交,但是A事物加下来又提交,这样就覆盖了B事物,称为第二类事物丢失,覆盖丢失。
    • 每当有写入冲突时会发生此问题。
    • 在写入冲突中,在同一数据项上有两个交易中的两个写入,而没有任何中间的读取。

4.幻读(Phantom Read Problem)

前面的写都是更新数据,这里是新增或删除数据时发生。
CREATE:在事务A进行多次读(所有)数据时,另一个事务插入了一条新数据,A发现读的数据变多了
ps:此处和重复读很像,但是重复读是针对一条数据,幻读则是整个表的数据(总体数据增多)
DELETE:事务A多次重复读取一条数据,此时另一个事务删除了这条数据,然后A再次读取是发现数据不存在。

5.由于行更新导致读取缺失和重复读

这一个其实也算作幻读,也是表级别的冲突

  • 缺失一个更新行或多次看到某更新行

在低于快照隔离级别的隔离级别当中,锁是行锁,顺着索引扫描整张表时,如果其他事务将索引值改变了,就会触发两种情况:

  1. 如果将当前扫描的位置后面的索引值修改到了前面,就会发生读取缺失
  2. 如果当前扫描的位置前面的索引值修改到了后面,就会发生重复读取

3.4事务隔离级别

这个隔离界别可以和“锁”一起来理解。我们知道并发编程当中的锁有很多种,如果只用一种重量级锁把整个表锁上,确实所有问题都能解决,但是效率很慢,也失去了并发的作用,所以将锁由“轻”到“重”地划分为多种锁,根据实际环境选择,使得效率最大化。事务隔离也是这样,通过划分不同的隔离级别,由“轻”到“重”,使得数据库事务执行效率最大化。
参考:https://docs.microsoft.com/zh-cn/sql/relational-databases/sql-server-transaction-locking-and-row-versioning-guide?view=sql-server-ver15#locking-and-row-versioning-basics

隔离级别 脏读 不可重复读 幻读
未提交读
已提交读
可重复读
快照
可序列化

1. 未提交读(Read uncommitted)

在进行操作的时候会上行写锁,事务终止状态释放,禁止第一类丢失更新(回滚丢失),但是会出现所有其他数据并发问题。

2.提交读(Read committed)

在进行操作的时候在读未提交基础上,对操作行上读锁,操作完毕释放,禁止第一类丢失更新和脏读。

3.读提交快照隔离(Read Committed Snapshot)

不少DBMS已经将读提交快照隔离透明地替换了传统读提交。比如SQLServer可以用参数设定读提交采用传统锁的方式还是行版本控制(快照)的方式。
读取操作不需要页锁或行锁,为每个语句提供一个在事务上版本一致的数据快照,因为该数据在语句开始时就存在。 不使用锁来防止其他事务更新数据。
禁止第一类丢失更新和脏读。

4.可重复读(Read repeatable)

对于读操作读锁事务结束,其他写事务的UPDATE/DELETE只能等到事务结束之后进行。
对于写操作写锁事务结束。其他事务不能READ/UPDATE/DELETE。
和提交读的区别在于,提交读的读操作是加读锁到本次读操作结束,可重复读的锁粒度更大。禁止两类丢失更新,禁止脏读和不可 重复度,但是可能出现幻读.
一个事物读的时候,我们把两次读看成整体,在读的过程中,不允许写的操作,这样就可以禁止不可重复读。就是两次读操作不允许其他事物。可以使用排他写锁实现。

这是大部分关系数据库的默认 隔离级别。

5. 快照隔离(Snapshot)

相较于读提交快照隔离,提供的快照从之前的每个语句变成当前事务,为当前事务提供版本一致的快照,直到事务提交以前,快照都不会改变。
禁止了丢失更新、脏读、不可重复读。

6. 序列化(Serializable)

  • 最高隔离级别
  • 相当于超级重量锁
  • 所有事务均顺序(序列化)执行
  • 相当于串行执行,失去了并行的优点,效率低,但是是最安全的
  • 禁止了丢失更新、脏读、不可重复读、幻读。

    两种隔离方式

    快照隔离是Microsoft SQLServer提出的《行版本控制隔离》,更像是“乐观锁”,基于快照来操作数据,提升并行能力,当然也增加了快照创建和管理的消耗。
    其他隔离是《ISO标准》定义的传统隔离,采用加锁的方式,下面有DBMS的锁类型。

4.锁模式

锁模式 说明
共享 (S) 用于不更改或不更新数据的读取操作,如 SELECT 语句。
更新 (U) 用于可更新的资源中。 防止当多个会话在读取、锁定以及随后可能进行的资源更新时发生常见形式的死锁。
排他 (X) 用于数据修改操作,例如 INSERT、UPDATE 或 DELETE。 确保不会同时对同一资源进行多重更新。
意向 用于建立锁的层次结构。 意向锁包含三种类型:意向共享 (IS)、意向排他 (IX) 和意向排他共享 (SIX)。
架构 在执行依赖于表架构的操作时使用。 架构锁包含两种类型:架构修改 (Sch-M) 和架构稳定性 (Sch-S)。
大容量更新 (BU) 在向表进行大容量数据复制且指定了 TABLOCK 提示时使用。
键范围 当使用可序列化事务隔离级别时保护查询读取的行的范围。 确保再次运行查询时其他事务无法插入符合可序列化事务的查询的行。

4.1共享锁

S锁,也叫读锁,用于所有的只读数据操作。共享锁是非独占的,允许多个事务在封闭式并发控制下读取其锁定的资源。

  • 多个事务可封锁同一个共享单元;
  • 任何事务都不能修改该单元;
  • 通常是该单元被读取完毕,S锁立即被释放。除非是可重复读情况下,将会持续到事务结束。

    4.2更新锁

    U锁,在修改操作的初始化阶段用来锁定可能要被修改的资源,这样可以避免使用共享锁造成的死锁现象。

    在修改操作当中,分为两个步骤:先读再写,所以先获取共享锁,然后再升级成排它锁,此时若两个或多个事务同时申请共享锁,同时升级排它锁,A事务升级排它锁需要B事务释放共享锁,但B事务不会释放共享锁因为B事务也要升级排它锁,这样就造成了死锁。

而更新锁就出现了:

  • 用来预定要对此单元施加X锁,它允许其他事务读(即加S锁),但不允许再施加U锁或X锁;
  • 当被读取的页要被更新时,则升级为X锁;
  • U锁一直到事务结束时才能被释放。

    4.3排他锁

    X锁,也叫写锁,表示对数据进行写操作。如果一个事务对对象加了排他锁,其他事务就不能再给它加任何锁了。

  • 仅允许一个事务封锁此单元;

  • 其他任何事务必须等到X锁被释放才能对该单元进行访问;
  • X锁一直到事务结束才能被释放。

    4.4意向锁

    意向锁有两种用途:

  • 防止其他事务以会使较低级别的锁无效的方式修改较高级别资源。

    • 例如,A事务在行或页上加了排它锁,此时,事务B要在表上加锁,A事务导致B事务失效。
  • 提高数据库引擎在较高的粒度级别检测锁冲突的效率。
    • 如果,要防止上述情况发生,那么事务B需要逐行检测是否有锁,这样效率极低

所以意向锁就是在低级别锁(低级别指粒度级别低,比如行锁最低,页锁较表锁更低)时,同时为表(最高级别)上意向锁,表明表中有行或页上了某种锁,这样其他事务在上高级别锁时,可以马上知道能不能上锁。

可以理解为,行锁+意向锁,使得整张表在被上表锁时表具有行锁性质,但是若不是表锁,则不影响。

意向锁包括意向共享 (IS)、意向排他 (IX) 以及意向排他共享 (SIX)。

锁模式 说明
意向共享 (IS) 保护针对层次结构中某些(而并非所有)低层资源请求或获取的共享锁。
意向排他 (IX) 保护针对层次结构中某些(而并非所有)低层资源请求或获取的排他锁。 IX 是 IS 的超集,它也保护针对低层级别资源请求的共享锁。
意向排他共享 (SIX) 保护针对层次结构中某些(而并非所有)低层资源请求或获取的共享锁以及针对某些(而并非所有)低层资源请求或获取的意向排他锁。 顶级资源允许使用并发 IS 锁。 例如,获取表上的 SIX 锁也将获取正在修改的页上的意向排他锁以及修改的行上的排他锁。 虽然每个资源在一段时间内只能有一个 SIX 锁,以防止其他事务对资源进行更新,但是其他事务可以通过获取表级的 IS 锁来读取层次结构中的低层资源。
意向更新 (IU) 保护针对层次结构中所有低层资源请求或获取的更新锁。 仅在页资源上使用 IU 锁。 如果进行了更新操作,IU 锁将转换为 IX 锁。
共享意向更新 (SIU) S 锁和 IU 锁的组合,作为分别获取这些锁并且同时持有两种锁的结果。 例如,事务执行带有 PAGLOCK 提示的查询,然后执行更新操作。 带有 PAGLOCK 提示的查询将获取 S 锁,更新操作将获取 IU 锁。
更新意向排他 (UIX) U 锁和 IX 锁的组合,作为分别获取这些锁并且同时持有两种锁的结果。

4.5 计划锁

计划锁分两种:

  • Sch-M:在进行修改表(schema)操作时,会上这个锁,上这个锁时,任何Session不能连接该表,即不能执行任何事务。
  • Sch-S:在编译语句时,会上此锁,此锁不影响其他事务,但是会禁止修改表(schema)操作(即禁止Sch-M锁)。

另可参阅:https://docs.microsoft.com/zh-cn/sql/relational-databases/sql-server-transaction-locking-and-row-versioning-guide?view=sql-server-ver15#lock-modes

参考文献

Microsoft-事务锁定和行版本控制指南:https://docs.microsoft.com/zh-cn/sql/relational-databases/sql-server-transaction-locking-and-row-versioning-guide?view=sql-server-ver15

数据库锁机制:https://blog.csdn.net/samjustin1/article/details/52210125#reply