《凤凰架构》读书笔记 —— 本地事务如何实现隔离性?

本文详细介绍了数据库事务的隔离性及其四种隔离级别:可串行化、可重复读、读已提交和读未提交,以及各自存在的问题。重点讲解了锁的类型,包括写锁、读锁和范围锁,并分析了不同隔离级别下可能出现的脏读、不可重复读和幻读现象。此外,还探讨了MVCC(多版本并发控制)作为一种读取优化策略,如何在不同隔离级别下工作。

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

现代数据库都提供了以下三种锁:

  • 写锁(排他锁):只有持有写锁的事务才能对数据进行写入操作,数据加持着写锁时,其他事务不能写入数据,也不能施加读锁。

    注意:写锁禁止其他事务施加读锁,而不是禁止事务读取数据

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

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

    SELECT * FROM books WHERE price < 100 FOR UPDATE;
    

    加了范围锁后,不仅无法修改该范围内已有的数据,也不能在该范围内新增或删除任何数据

本地事务的四种隔离级别

可串行化 Serializable

串行化访问提供了强度最高的隔离性。

在该隔离级别下,对事务的所有读、写操作全部加上读锁、写锁、范围锁

可重复读 Repeatable Read

​ 在该隔离级别下,对事务所涉及到的数据加读锁和写锁,并一直持续到事务结束,但不加范围锁

可重复读比可串行化弱化的地方在于脏读问题:两个完全相同的范围查询得到了不一样的结果集。

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 仍然会出现幻读问题

读已提交 Read Committed

在该隔离级别下,对事务的所涉及到的数据加写锁,一直持续到事务结束,但加的读锁在查询操作完成后会马上释放。

​ 读已提交比可重复读弱化的地方在于不可重复读问题:在事务执行过程中,对同一行数据的两次查询得到了不同的结果。

SELECT * FROM books WHERE id = 1;               		/* 时间顺序:1,事务: T1 */
UPDATE books SET price = 110 WHERE ID = 1; COMMIT;      /* 时间顺序:2,事务: T2 */
SELECT * FROM books WHERE id = 1; COMMIT;           	/* 时间顺序:3,事务: T1 */

在这里插入图片描述

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

读未提交 Read Uncommitted

在该隔离级别下,对事务的所涉及到的数据只加写锁,一直持续到事务结束。

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

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

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

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

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

在这里插入图片描述

Q: 为什么 T1 不加读锁,就可以读到 T2 加了写锁的数据?

A: 写锁禁止其他事务施加读锁,而不是禁止事务读取数据。T1 是在不加读锁的情况下读取数据的

其实,不同隔离级别以及幻读、脏读等问题都只是表面现象,它们是各种锁在不同加锁时间上组合应用所产生的结果,锁才是根本的原因。

MVCC

​ MVCC 是一种读取优化策略,它的“无锁”是特指读取时不需要加锁。MVCC 的基本思路是对数据库的任何修改都不会直接覆盖之前的数据,而是产生一个新版副本与老版本共存,以此达到读取时可以完全不加锁的目的。

​ 这句话里的“版本”是个关键词,你不妨将其理解为数据库中每一行记录都存在两个看不见的字段:CREATE_VERSIONDELETE_VERSION,这两个字段记录的值都是事务 ID(事务 ID 是一个全局严格递增的数值),然后:

  • 数据被插入时:CREATE_VERSION 记录插入数据的事务 ID,DELETE_VERSION 为空。
  • 数据被删除时:DELETE_VERSION 记录删除数据的事务 ID,CREATE_VERSION 为空。
  • 数据被修改时:将修改视为“删除旧数据,插入新数据”,即先将原有数据复制一份,原有数据的 DELETE_VERSION 记录修改数据的事务 ID,CREATE_VERSION 为空。复制出来的新数据的 CREATE_VERSION 记录修改数据的事务 ID,DELETE_VERSION 为空。

此时,当有另外一个事务要读取这些发生了变化的数据时,会根据隔离级别来决定到底应该读取哪个版本的数据:

  • 隔离级别是可重复读:总是读取 CREATE_VERSION 小于或等于当前事务 ID 的记录,在这个前提下,如果数据仍有多个版本,则取最新(事务 ID 最大)的。
  • 隔离级别是读已提交:总是取最新的版本即可,即最近被 Commit 的那个版本的数据记录。

​ 另外,两个隔离级别都没有必要用到 MVCC,读未提交直接修改原始数据即可,其他事务查看数据的时候立刻可以查看到,根本无需版本字段。可串行化本来的语义就是要阻塞其他事务的读取操作,而 MVCC 是做读取时无锁优化的,自然就不会放到一起用。

MVCC 是只针对“读 + 写”场景的优化,如果是两个事务同时修改数据,即“写 + 写”的情况,那就没有多少优化的空间了,加锁几乎是唯一可行的解决方案。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值