一、 事务
事务(Transaction),即满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。
ACID
-
A 原子性 Atomicity
事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。
回滚可以用回滚日志来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。 -
C 一致性 Consistency
数据库在事务执行前后都保持一致性状态。
在一致性状态下,所有事务对一个数据的读取结果都是相同的。 -
I 隔离性 Isolation
一个事务所做的修改在最终提交以前,对其它事务是不可见的。 -
D 持久性 Durability
一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
使用重做日志来保证持久性。
关系
原子性 + 隔离性 -> 一致性
原子性与隔离性共同在并发的情况下保证了一致性,即数据的正确性,最后正确的数据在数据库中持久化保存。
因此,一致性是核心。
一致性问题
串行的情况下,隔离性自然保证,保证了基本的原子性,就可以保证一致性。
并发的情况下,隔离性难以保证,由此引发了一系列不一致的问题:
-
写问题
-
更新丢失
- 第一类:A 的撤销覆盖了 B 的修改,B 的更新丢失
- 第二类:A 的修改覆盖了 B 的修改,B 的更新丢失
-
更新丢失
-
读问题:
-
读脏数据
B 读取 A 已修改但未提交的数据,但随后 A 再次进行了修改(或回滚),B 读到了脏数据(中间数据,垃圾数据)。 -
不可重复读
A 修改/删除了数据,B 在 A 修改/删除数据前后两次读取,B 读取的内容不一致。
与读脏数据的区别在于 A 是否已提交。 -
幻影读
A 新增了数据,B 在 A 新增数据前后两次读取,B 读取的内容不一致。
与不可重复读的区别在于是同一条数据的修改,还是读到了不同的数据。
-
解决这些一致性问题,重点在于解决隔离性问题,因此设置了 隔离级别。
二、 隔离级别
-
未提交读 READ_UNCOMMITTED:事务中的修改,即使没有提交,对其它事务也是可见的。
-
提交读 READ_COMMITTED:事务只能读取已经提交的事务所做的修改。
-
可重复读 REPEATABLE_READ:保证在同一个事务中多次读取同样数据的结果是一样的。
-
可串行化 SERIALIZABLE:强制事务串行执行。
四种级别,并发性依次下降,安全性依次上升。
对于 3 种读问题,4 种级别分别位于 4 个区间。即:
级别 | 读脏数据 | 不可重复读 | 幻影读 |
---|---|---|---|
未提交读 | 允许 | 允许 | 允许 |
提交读 | 禁止 | 允许 | 允许 |
可重复读 | 禁止 | 禁止 | 允许 |
可串行化 | 禁止 | 禁止 | 禁止 |
隔离级别的具体实现是通过 锁 来完成的。
三、 锁
读写锁
-
读锁:共享锁,S 锁(Shared)
加读锁后,其他事务只能进行读取,不能进行更新,即只能加读锁,不能加写锁。 -
写锁:排他锁,X 锁(Exclusive)
加写锁后,其他事务不能读取和更新,即不能加任何锁。
读写锁实现隔离级别
-
未提交读
- 读:不加锁
- 写:行级共享锁
B 可以读到 A 已修改未提交的数据。因此会造成读脏数据问题。
-
提交读
- 读:行级共享锁,读完该行立即释放
- 写:行级排他锁,事务结束才能释放
A 修改数据,会对该行加锁,直至事务结束,B 不会读到未提交的数据。因此可以解决脏读问题。
A 和 B 可以同时读取某行,保证该行不会被修改。但读取结束后,数据可以被修改。因此会造成可重复读问题。 -
可重复读
- 读:行级共享锁,事务结束才能释放
- 写:行级排他锁,事务结束才能释放
读锁写锁都是直到事务结束才能释放。因此可以解决不可重复读问题。
但无法解决幻影读问题:A B 1 select * from users where age < 20 2 insert into users(age) values(18) 3 select * from users where age < 20 (1) A 对 1 检索出的 n 条数据加共享锁,B 无法修改或删除这 n 条数据。
(2) B 新增 1 条数据。
(3) A 检索出 n + 1 条数据。A 两次读取得到的数据条数并不一致,出现了幻影读的现象。
-
可串行化
- 读:表级共享锁,事务结束才能释放
- 写:表级排他锁,事务结束才能释放
解决幻影读的问题。
但是效率极为低下。
意向锁
如果想为一张表加表锁,需要检查表的每一行是否有冲突的行锁,扫描全表消耗很大,为了解决这个问题,引入意向锁。
意向锁是表级锁,是一种虚锁,仅代表有加锁的意图,同样分为 IS 和 IX。
- 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
- 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。
乐观锁
乐观锁指 乐观 地认为操作不会导致冲突,在操作数据前不加锁,在修改后,再去判断是否冲突。
数据库本身不提供乐观锁,需要程序员自己实现,通常可以利用 CAS 的思想。
- 为表添加版本字段,每次操作数据时版本号 + 1
- 取出数据时,记录版本号
- 计算数据后,再次取出版本号,判断是否一致
- 一致:这段时间内,数据没有被修改过,将版本号 + 1 后放回数据
- 不同:这段时间内,数据被修改过,重新取出数据,即执行 2
与乐观锁相对应,悲观锁 悲观 地认为此次操作会出现数据冲突,所以在进行操作前就加锁,保证操作的正确性。
悲观锁直接由数据库提供,如共享锁,排他锁,都是悲观锁的实现。
四、 多版本并发控制
隔离级别可以使用锁来实现,但效率不高。
多版本并发控制( MVCC )用于实现提交读和可重复读这两种隔离级别。
未提交读隔离级别总是读取最新的数据行,无需使用MVCC。
可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。
MVCC是通过在每行记录后面保存两个隐藏的列来实现的。
- 行的创建时间
- 行的过期时间(或删除时间)
这两列用系统版本号表示。每开始一个新的事务, 系统版本号 都会自动递增。
事务开始时刻的系统版本号会作为 事务版本号,用来和查询到的每行记录的版本号进行比较。
实现可重复读
SELECT
- 只查找版本小于或等于当前事务版本的数据行:确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
- 只查找删除版本大于当前事务版本或未定义删除版本的数据行:确保事务读取到的行,在事务开始之前未被删除。
INSERT
新插入的每一行保存当前系统版本号作为行版本号。
DELETE
删除的每一行保存当前系统版本号作为行删除标识。
UPDATE
- 插入一行新记录,保存当前系统版本号作为行版本号。
- 同时保存当前系统版本号到原来的行作为行删除标识。
相当于 INSERT + DELETE。