本地事务如何实现隔离性?

文章介绍了数据库如何通过锁机制实现事务的隔离性,包括写锁、读锁和范围锁。详细阐述了四种隔离级别——可串行化、可重复读、读已提交和读未提交,以及它们各自可能出现的问题,如幻读、不可重复读和脏读。文章强调了隔离级别与并发性能之间的权衡,并以MySQL的InnoDB为例说明其在幻读问题上的处理方式。
摘要由CSDN通过智能技术生成

你好。我是周志明。

今天我们接着上一节课的话题,继续来探讨数据库如何实现隔离性。

隔离性保证了每个事务各自读、写的数据互相独立,不会彼此影响。只从定义上,我们就能感觉到隔离性肯定与并发密切相关。如果没有并发,所有事务全都是串行的,那就不需要任何隔离,或者说这样的访问具备了天然的隔离性。

但在现实情况中不可能没有并发,要在并发下实现串行的数据访问,该怎样做?几乎所有程序员都会回答到:加锁同步呀!现代数据库都提供了以下三种锁:

写锁(Write Lock,也叫做排他锁 eXclusive Lock,简写为 X-Lock):只有持有写锁的事务才能对数据进行写入操作,数据加持着写锁时,其他事务不能写入数据,也不能施加读锁。

读锁(Read Lock,也叫做共享锁 Shared Lock,简写为 S-Lock):多个事务可以对同一个数据添加多个读锁,数据被加上读锁后就不能再被加上写锁,所以其他事务不能对该数据进行写入,但仍然可以读取。对于持有读锁的事务,如果该数据只有一个事务加了读锁,那可以直接将其升级为写锁,然后写入数据。

范围锁(Range Lock):对于某个范围直接加排他锁,在这个范围内的数据不能被读取,也不能被写入。如下语句是典型的加范围锁的例子:

SELECT * FROM books WHERE price < 100 FOR UPDATE;

请注意“范围不能写入”与“一批数据不能写入”的差别,也就是我们不要把范围锁理解成一组排他锁的集合。加了范围锁后,不仅无法修改该范围内已有的数据,也不能在该范围内新增或删除任何数据,这是一组排他锁的集合无法做到的。

本地事务的四种隔离级别

了解了这三种锁的概念之后,如果我们要继续探讨数据库是如何实现隔离性的,那就得先理解事务的隔离级别。接下来,我就按照隔离强度从高到低来给你一一介绍。

可串行化

串行化访问提供了强度最高的隔离性,ANSI/ISO SQL-92 中定义的最高等级的隔离级别便是可串行化(Serializable)。

可串行化比较符合普通程序员对数据竞争加锁的理解,如果不考虑性能优化的话,对事务所有读、写的数据全都加上读锁、写锁和范围锁即可(这种可串行化的实现方案称为 Two-Phase Lock)。

但数据库不考虑性能肯定是不行的,并发控制理论(Concurrency Control)决定了隔离程度与并发能力是相互抵触的,隔离程度越高,并发访问时的吞吐量就越低。现代数据库一定会提供除可串行化以外的其他隔离级别供用户使用,让用户调节隔离级别的选项,这样做的根本目的是让用户可以调节数据库的加锁方式,取得隔离性与吞吐量之间的平衡。

可重复读(其他事务提交了也影响不了我的读)

可串行化的下一个隔离级别是可重复读(Repeatable Read)。可重复读的意思就是对事务所涉及到的数据加读锁和写锁,并且一直持续到事务结束,但不再加范围锁。

可重复读比可串行化弱化的地方在于幻读问题(Phantom Reads),它是指在事务执行的过程中,两个完全相同的范围查询得到了不同的结果集。比如我现在准备统计一下 Fenix's Bookstore 中售价小于 100 元的书有多少本,就可以执行以下第一条 SQL 语句:


SELECT count(1) FROM books WHERE price < 100 /* 时间顺序:1,事务: T1 */

INSERT INTO books(name,price) VALUES ('深入理解Java虚拟机',90) /* 时间顺序:2,事务: T2 */

SELECT count(1) FROM books WHERE price < 100 /* 时间顺序:3,事务: T1 */

那么,根据前面对范围锁、读锁和写锁的定义,我们可以知道,假如这条 SQL 语句在同一个事务中重复执行了两次,并且这两次执行之间,恰好有另外一个事务在数据库中插入了一本小于 100 元的书籍(这是当前隔离级别允许的操作),那这两次相同的查询就会得到不一样的结果。原因就是,可重复读没有范围锁来禁止在该范围内插入新的数据。

这就是一个事务遭到其他事务影响,隔离性被破坏的表现。

这里我要提醒你注意一个地方,我这里的介绍实际上是以 ARIES 理论作为讨论目标的,而具体的数据库并不一定要完全遵照着这个理论去实现。

我给你举个例子。MySQL/InnoDB 的默认隔离级别是可重复读,但它在只读事务中就可以完全避免幻读问题。

比如在前面这个例子中,事务 T1 只有查询语句,它是一个只读事务,所以这个例子里出现的幻读问题在 MySQL 中并不会出现。

但在读写事务中,MySQL 仍然会出现幻读问题,比如例子中的事务 T1,如果在其他事务插入新书后,不是重新查询一次数量,而是要把所有小于 100 元的书全部改名,那就依然会受到新插入书籍的影响。

读已提交(其他书屋提交了才影响我的读)

可重复读的下一个隔离级别是读已提交(Read Committed)。读已提交对事务涉及到的数据加的写锁,会一直持续到事务结束,但加的读锁在查询操作完成后就马上会释放。

读已提交比可重复读弱化的地方在于不可重复读问题(Non-Repeatable Reads),它是指在事务执行过程中,对同一行数据的两次查询得到了不同的结果。

所以到这里,你其实也会发现,如果隔离级别是读已提交,那么这两次重复执行的查询结果也会不一样。原因是读已提交的隔离级别缺乏贯穿整个事务周期的读锁,无法禁止读取过的数据发生变化。而此时,事务 T2 中的更新语句可以马上提供成功,这也是一个事务遭到其他事务影响,隔离性被破坏的表现。

不过,假如隔离级别是可重复读的话,由于数据已被事务 T1 施加了读锁,并且读取后不会马上释放,所以事务 T2 无法获取到写锁,更新就会被阻塞,直至事务 T1 被提交或回滚后才能提交。

读未提交(其他事务还没提交就影响我的读了)

读已提交的下一个级别是读未提交(Read Uncommitted)。读未提交对事务涉及到的数据只加写锁,这会一直持续到事务结束,但完全不加读锁。

读未提交比读已提交弱化的地方在于脏读问题(Dirty Reads),它是指在事务执行的过程中,一个事务读取到了另一个事务未提交的数据。

 

比如说,我觉得《深入理解 Java 虚拟机》从 90 元涨价到 110 元是损害消费者利益的行为,又执行了一条更新语句,把价格改回了 90 元。而在我提交事务之前,同事过来告诉我,这并不是随便涨价的,而是印刷成本上升导致的,按 90 元卖要亏本,于是我随即回滚了事务。那么在这个场景下,程序执行的 SQL 语句是这样的:

SELECT * FROM books WHERE id = 1; /* 时间顺序:1,事务: T1 */

/* 注意没有COMMIT */

UPDATE books SET price = 90 WHERE ID = 1; /* 时间顺序:2,事务: T2 */

/* 这条SELECT模拟购书的操作的逻辑 */

SELECT * FROM books WHERE id = 1; /* 时间顺序:3,事务: T1 */

ROLLBACK; /* 时间顺序:4,事务: T2 */

不过,在我修改完价格之后,事务 T1 已经按 90 元的价格卖出了几本。出现这个问题的原因就在于,读未提交在数据上完全不加读锁,这反而令它能读到其他事务加了写锁的数据,也就是我前面所说的,事务 T1 中两条查询语句得到的结果并不相同。

这里,你可能会有点疑问,“为什么完全不加读锁,反而令它能读到其他事务加了写锁的数据”,这句话中的“反而”代表的是什么意思呢?不理解也没关系,我们再来重新读一遍写锁的定义:写锁禁止其他事务施加读锁,而不是禁止事务读取数据。

所以说,如果事务 T1 读取数据时,根本就不用去加读锁的话,就会导致事务 T2 未提交的数据也能马上就被事务 T1 所读到。这同样是一个事务遭到其他事务影响,隔离性被破坏的表现。

那么,这里我们假设隔离级别是读已提交的话,由于事务 T2 持有数据的写锁,所以事务 T1 的第二次查询就无法获得读锁。而读已提交级别是要求先加读锁后读数据的,所以 T1 中的查询就会被阻塞,直到事务 T2 被提交或者回滚后才能得到结果。

理论上还有更低的隔离级别,就是“完全不隔离”,即读、写锁都不加。读未提交会有脏读问题,但不会有脏写问题(Dirty Write,即一个事务没提交之前的修改可以被另外一个事务的修改覆盖掉),脏写已经不单纯是隔离性上的问题了,它会导致事务的原子性都无法实现,所以一般隔离级别不会包括它,会把读未提交看作是最低级的隔离级别。

这四种隔离级别属于数据库的基础知识,多数大学的计算机课程应该都会讲到,但不少教材、资料都把它们当作数据库的某种固有设定来进行讲解,导致很多人只能对这些现象死记硬背。其实,不同隔离级别以及幻读、脏读等问题都只是表面现象,它们是各种锁在不同加锁时间上组合应用所产生的结果,锁才是根本的原因。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值