事务的隔离性
今天我们接着上一节课的话题,继续来探讨数据库如何实现隔离性。
只从定义上,我们就能感觉到隔离性肯定与并发密切相关。如果没有并发,所有事务全都是串行的,那就不需要任何隔离,或者说这样的访问具备了天然的隔离性。但在现实情况中不可能没有并发,要在并发下实现串行的数据访问,该怎样做?几乎所有程序员都会回答到:加锁同步呀!现代数据库都提供了以下三种锁:
写锁(Write Lock,也叫做排他锁 eXclusive Lock,简写为 X-Lock):只有持有写锁的事务才能对数据进行写入操作,数据加持着写锁时,其他事务不能写入数据,也不能施加读锁。
读锁(Read Lock,也叫做共享锁 Shared Lock,简写为 S-Lock):多个事务可以对同一个数据添加多个读锁,数据被加上读锁后就不能再被加上写锁,所以其他事务不能对该数据进行写入,但仍然可以读取。对于持有读锁的事务,如果该数据只有一个事务加了读锁,那可以直接将其升级为写锁,然后写入数据。
范围锁(Range Lock):对于某个范围直接加排他锁,在这个范围内的数据不能被读取,也不能被写入。如下语句是典型的加范围锁的例子:
代码语言:javascript复制SELECT * FROM books WHERE price < 100 FOR UPDATE;
请注意“范围不能写入”与“一批数据不能写入”的差别,也就是我们不要把范围锁理解成一组排他锁的集合。加了范围锁后,不仅无法修改该范围内已有的数据,也不能在该范围内新增或删除任何数据,这是一组排他锁的集合无法做到的。
隔离级别
本地事务的四种隔离级别了解了这三种锁的概念之后,如果我们要继续探讨数据库是如何实现隔离性的,那就得先理解事务的隔离级别。接下来,我就按照隔离强度从高到低来给你一一介绍。
可串行化——效率最低最安全
可重复读比可串行化弱化的地方在于幻读问题(Phantom Reads),它是指在事务执行的过程中,两个完全相同的范围查询得到了不同的结果集。
读已提交比可重复读弱化的地方在于不可重复读问题(Non-Repeatable Reads),它是指在事务执行过程中,对同一行数据的两次查询得到了不同的结果。
读未提交比读已提交弱化的地方在于脏读问题,它是指在事务执行的过程中,一个事务读取到了另一个事务未提交的数据。
理论上还有更低的隔离级别,就是“完全不隔离”,即读、写锁都不加。读未提交会有脏读问题,但不会有脏写问题(Dirty Write,即一个事务没提交之前的修改可以被另外一个事务的修改覆盖掉),脏写已经不单纯是隔离性上的问题了,它会导致事务的原子性都无法实现,所以一般隔离级别不会包括它,会把读未提交看作是最低级的隔离级别。
如果你也背了很多遍什么幻读脏读过两天就忘,那么忘记前面这一大段,只记住红色标注的这几种隔离级别,其实,不同隔离级别以及幻读、脏读等问题都只是表面现象,它们是各种锁在不同加锁时间上组合应用所产生的结果,锁才是根本的原因。
隔离级别的本质在于锁的运用,分别是 读锁 写锁 范围锁 读锁 写锁 写锁 读完就释放的读锁(写锁会一直持续到事务结束,但加的读锁在查询操作完成后就马上会释放) 只有写锁
写锁也不加
MVCC
“多版本并发控制”(Multi-Version Concurrency Control,MVCC)
MVCC 的基础原理MVCC 是一种读取优化策略,它的“无锁”是特指读取时不需要加锁。MVCC 的基本思路是对数据库的任何修改都不会直接覆盖之前的数据,而是产生一个新版副本与老版本共存,以此达到读取时可以完全不加锁的目的。这句话里的“版本”是个关键词,你不妨将其理解为数据库中每一行记录都存在两个看不见的字段:CREATE_VERSION 和 DELETE_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解决的是读 写的问题,所以第一个和第四个锁的组合不用考虑这个问题。