事务
1. 为什么要有事务
事务的存在是为了保证数据的一致性与完整性。
数据库作为保存数据的组建,很重要的要求,就是要在数据变动的过程中,保证数据的一致性与完整性:
- 一致性:数据变动前后,数据的状态要满足业务逻辑的要求,如银行转账时,账户 A 向 B 转账,A 减少的余额应该与 B 增加的余额相同,且不能大于 A 现有的余额。
- 完整性:数据变动前后,数据的状态要满足预定的约束条件,如主键约束、外键约束等。
为了保证这两个特性,数据库引入了事务的概念。
2. 事务的定义
事务(Transaction)是包含一系列操作的数据库执行单元,这些操作要么全部执行成功,要么全部执行失败。不存在中间状态。
还是转账的例子,假设 账户 A 向账户 B 转账,需要执行以下操作:
- 检查账户 A 的余额是否足够
- 如果足够,从账户 A 中减去相应的金额
- 将相应的金额加到账户 B 中
在执行实际的数据变动时,这些操作就应该放到同一个事务中执行。
3. 事务的特性 - ACID
事务具备了 ACID 的特性,用来保证数据的一致性与完整性。这四个特性分别是:
- Atomicity 原子性:事务中的所有操作要么全部执行成功,要么全部执行失败。
- Consistency 一致性:事务在执行前后,数据始终保持在正确的状态,既要满足业务逻辑,也需要满足数据库的约束条件。
- Isolation 隔离性:事务在执行过程中,其他事务的执行不会影响当前事务。
- Durability 持久性:事务执行完成后,对数据库会产生永久的改变,数据的状态会持久化到磁盘上。
事务有四个重 要的特性,用以保障数据的一致性与完整性,
- 原子性(Atomicity)
- 一致性(Consistency)
- 隔离性(Isolation)
- 持久性(Durability)
下面来分别解释一下这四个特性。
3.1. 原子性 Atomicity
原子性要求事务中的所有操作要么全部执行成功,要么全部执行失败。
典型的案例就是转账操作,账户 A 向账户 B 转账,「A 的余额减少」与「B 的余额增加」是两个需要同时执行的操作,如果只执行了其中一个,那么余额就会出现混乱。
在实现上,通常通过日志的方式来实现。事务中的每一次操作都会记录到日志中,如果事务执行失败,可以通过日志来回滚。
3.2. 一致性 Consistency
一致性要求事务在执行前后,数据始终保持在正确的状态,既要满足业务逻辑,也需要满足数据库的约束条件。
3.3. 隔离性 Isolation
隔离性指事务在执行过程中,其他事务的执行不会影响当前事务。
在实际的生产环境中,对于数据库的读写通常是并发进行的,会存在多个事务对同一张数据表进行操作。
先考虑一个简单的情况,假设事务 A 正在更新记录 r1, 事务 B 在更新记录 r2,两个事务不操作同一条记录,理论上两个事务不会有任何影响。
接下来情况稍微复杂了一些,假设事务 A 更新记录 r1,同时事务 B 在查询记录 r1,那么如果事务 A 正在执行中,事务 B 查询到的结果应该是什么样的?
为了解决多个事务同时更新或者查询同一个记录的问题,隔离性又分为了不同的隔离级别,这个在后面会详细介绍。
3.4. 持久性 Durability
持久性指事务执行完成后,也可以说事务「提交」后,对数据库会产生永久的改变,数据的状态会持久化到磁盘上。
换个角度讲,数据库的每一次更新,背后对应的就是一个个事务的提交。
4. 事务的隔离级别
并发读写可能带来「脏读」、「不可重复读」、「幻读」问题。
- Dirty Read:一个事务读取到了另一个事务尚未提交的修改。
- Non-Repeatable Read:同一个事务中,多次查询同一个记录,查询的结果不一致
- Phantom Read:同一个事务中,范围查询时,多次查询同一个范围,靠后的查询查到了新插入的记录。
在 3.3 隔离性中提到,虽然事务的执行是互不影响的,但是在并发环境下还是存在多个事务同时修改、查询同一条记录的情况。
为了解决这个问题,数据库引入了隔离级别的概念。
4.1. 数据库并发读写面临的问题
在讨论隔离级别之前,我们先来思考一下数据库并发读写时可能会遇到的问题。
对于一条记录 r1,假设事务 A 与事务 B 同时对其进行操作,可能面临以下情况:
-
事务 A 更新 r1 但是尚未提交,事务 B 查询到了事务 A 尚未提交的修改。然后事务 A 回滚或者再一次更新了 r1,此时事务 B 查询到的就是一个事务 A 执行过程中的「脏数据」。这种情况被称为脏读。
-
事务 B 先查询了记录 r1。在事务 B 执行的过程中,事务 A 更新了 r1 的值并提交结束。此时事务 B 又一次查询了记录 r1,由于此时 r1 已经被事务 A 更新,导致事务 B 两次重复的查询的结果不一致,这种情况被称为不可重复读。
-
第三种情况类似上一种情况,只不过事务 B 进行的是一个范围查询,查询过程中事务 A 插入了一个新的记录 r2,导致事务 B 第二次查询时多了一条新的记录,就像是出现了幻觉一样,这种情况被称为幻读。
4.2. 隔离级别
隔离级别规定了并发场景下,对于同一个记录进行操作时,一个事务对于另一个事务的可见性。
隔离级别从弱到强依次为:读未提交、读已提交、可重复读、串行化。
- 读未提交:一个事务可以读取另一个事务尚未提交的修改。
- 读已提交:一个事务可以读取另一个事务已经提交的修改。
- 可重复读:一个事务中对于同一个记录的多次读取结果是一致的。
- 串行化:所有的事务串行执行。
为了解决上述问题,数据库引入了隔离级别的概念。
隔离级别定义了并发场景下,一个事务的修改对于另一个事务的可见性。
隔离级别由弱到强,依次为:
- 读未提交(Read Uncommitted)
- 读已提交(Read Committed)
- 可重复读(Repeatable Read)
- 串行化(Serializable)
下面来分别介绍一下这四种隔离级别。
4.3. 读未提交 Read Uncommitted
「读未提交」,顾名思义,事务 B 可以读取事务 A 尚未提交的修改。
这是一种最弱的隔离级别,无法解决「脏读」、「不可重复读」、「幻读」任何一个问题,在实际中很少使用。
也正是因为没有做任何一致性处理,所以性能是最好的。
4.4. 读已提交 Read Committed
「读已提交」指的是事务 B 只能读取事务 A 已经提交的修改。
因为只能读取已经提交的修改,所以解决了「脏读」问题。但是依然没有解决「不可重复读」、「幻读」问题。
「读已提交」是 PostgreSQL 的默认隔离级别。
4.5. 可重复读 Repeatable Read
「可重复读」指的是在同一事务中,对于同一条记录的多次读取结果是一致的。
一个常见的实现方 式是使用快照,在事务开始时,记录需要读取的记录的快照,在事务中的查询都会使用快照进行查询,取保在事务执行过程中,读取到的数据是一致的。
「可重复读」是 MySQL 的默认隔离级别。
「可重复读」与「读已提交」是相对常用的隔离级别,在保证了一定的数据一致性的同时,又不会对性能产生太大的影响。
4.6. 串行化 Serializable
「串行化」是最强的隔离级别,它要求事务串行执行,即一个事务执行过程中,其他事务必须等待,直到该事务执行完成。
「串行化」可以解决「脏读」、「不可重复读」、「幻读」所有问题,但是代价是数据库失去了并执行事务的能力,导致了最差的性能。
「串行化」的隔离级别在对数据一致性有最严苛要求的时候才会使用。