跳到主要内容

事务

1. 为什么要有事务

为什么需要有事务

事务的存在是为了保证数据的一致性与完整性。

数据库作为保存数据的组建,很重要的要求,就是要在数据变动的过程中,保证数据的一致性完整性

  1. 一致性:数据变动前后,数据的状态要满足业务逻辑的要求,如银行转账时,账户 A 向 B 转账,A 减少的余额应该与 B 增加的余额相同,且不能大于 A 现有的余额。
  2. 完整性:数据变动前后,数据的状态要满足预定的约束条件,如主键约束、外键约束等。

为了保证这两个特性,数据库引入了事务的概念。

2. 事务的定义

事务(Transaction)是包含一系列操作的数据库执行单元,这些操作要么全部执行成功,要么全部执行失败。不存在中间状态。

还是转账的例子,假设账户 A 向账户 B 转账,需要执行以下操作:

  1. 检查账户 A 的余额是否足够
  2. 如果足够,从账户 A 中减去相应的金额
  3. 将相应的金额加到账户 B 中

在执行实际的数据变动时,这些操作就应该放到同一个事务中执行。

3. 事务的特性 - ACID

事务如何保证数据的一致性与完整性

事务具备了 ACID 的特性,用来保证数据的一致性与完整性。这四个特性分别是:

  1. Atomicity 原子性:事务中的所有操作要么全部执行成功,要么全部执行失败。
  2. Consistency 一致性:事务在执行前后,数据始终保持在正确的状态,既要满足业务逻辑,也需要满足数据库的约束条件。
  3. Isolation 隔离性:事务在执行过程中,其他事务的执行不会影响当前事务。
  4. Durability 持久性:事务执行完成后,对数据库会产生永久的改变,数据的状态会持久化到磁盘上。

事务有四个重要的特性,用以保障数据的一致性与完整性,

  1. 原子性(Atomicity)
  2. 一致性(Consistency)
  3. 隔离性(Isolation)
  4. 持久性(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 同时对其进行操作,可能面临以下情况:

  1. 事务 A 更新 r1 但是尚未提交,事务 B 查询到了事务 A 尚未提交的修改。然后事务 A 回滚或者再一次更新了 r1,此时事务 B 查询到的就是一个事务 A 执行过程中的「脏数据」。这种情况被称为脏读

  2. 事务 B 先查询了记录 r1。在事务 B 执行的过程中,事务 A 更新了 r1 的值并提交结束。此时事务 B 又一次查询了记录 r1,由于此时 r1 已经被事务 A 更新,导致事务 B 两次重复的查询的结果不一致,这种情况被称为不可重复读

  3. 第三种情况类似上一种情况,只不过事务 B 进行的是一个范围查询,查询过程中事务 A 插入了一个新的记录 r2,导致事务 B 第二次查询时多了一条新的记录,就像是出现了幻觉一样,这种情况被称为幻读

4.2. 隔离级别

隔离级别有哪些,解决了什么问题

隔离级别规定了并发场景下,对于同一个记录进行操作时,一个事务对于另一个事务的可见性。

隔离级别从弱到强依次为:读未提交、读已提交、可重复读、串行化。

  • 读未提交:一个事务可以读取另一个事务尚未提交的修改。
  • 读已提交:一个事务可以读取另一个事务已经提交的修改。
  • 可重复读:一个事务中对于同一个记录的多次读取结果是一致的。
  • 串行化:所有的事务串行执行。

为了解决上述问题,数据库引入了隔离级别的概念。

隔离级别定义了并发场景下,一个事务的修改对于另一个事务的可见性。

隔离级别由弱到强,依次为:

  1. 读未提交(Read Uncommitted)
  2. 读已提交(Read Committed)
  3. 可重复读(Repeatable Read)
  4. 串行化(Serializable)

下面来分别介绍一下这四种隔离级别。

4.3. 读未提交 Read Uncommitted

读未提交」,顾名思义,事务 B 可以读取事务 A 尚未提交的修改。

这是一种最弱的隔离级别,无法解决「脏读」、「不可重复读」、「幻读」任何一个问题,在实际中很少使用。

也正是因为没有做任何一致性处理,所以性能是最好的。

4.4. 读已提交 Read Committed

读已提交」指的是事务 B 只能读取事务 A 已经提交的修改。

因为只能读取已经提交的修改,所以解决了「脏读」问题。但是依然没有解决「不可重复读」、「幻读」问题。

「读已提交」是 PostgreSQL 的默认隔离级别。

4.5. 可重复读 Repeatable Read

可重复读」指的是在同一事务中,对于同一条记录的多次读取结果是一致的。

一个常见的实现方式是使用快照,在事务开始时,记录需要读取的记录的快照,在事务中的查询都会使用快照进行查询,取保在事务执行过程中,读取到的数据是一致的。

「可重复读」是 MySQL 的默认隔离级别。

「可重复读」与「读已提交」是相对常用的隔离级别,在保证了一定的数据一致性的同时,又不会对性能产生太大的影响。

4.6. 串行化 Serializable

串行化」是最强的隔离级别,它要求事务串行执行,即一个事务执行过程中,其他事务必须等待,直到该事务执行完成。

「串行化」可以解决「脏读」、「不可重复读」、「幻读」所有问题,但是代价是数据库失去了并执行事务的能力,导致了最差的性能。

「串行化」的隔离级别在对数据一致性有最严苛要求的时候才会使用。

5. 扩展阅读

  1. MySQL 事务隔离级别
  2. PostgreSQL 事务隔离级别