以PostgreSQL为例详解数据库事务与并发控制

Transaction

Transaction是数据库系统的基本概念,表示原子的执行若干步操作,即全部执行成功或全部执行失败,执行中的状态对其他的Transaction不可见。

一个简单的例子,Alice向Bob转账100元。

UPDATE accounts SET balance = balance - 100.00 WHERE name = 'Alice';
UPDATE accounts SET balance = balance + 100.00 WHERE name = 'Bob';
复制代码

我们需要保证:

  1. 这些更新要么全部发生,要么全部没有发生。
  2. 如果中途出错,这之前的更新都不会生效。

将这些更新做成一个Transaction可以实现这两点。

BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE name = 'Alice';
UPDATE accounts SET balance = balance + 100.00 WHERE name = 'Bob';
COMMIT;
复制代码

除此之外:

  1. 一旦Transaction完成,即便不久后数据库崩溃,更新的数据也不会丢失。
  2. 当多个Transaction并发执行时,每个Transaction都不应该看到其他Transaction的不完整的更改。

Concurrency Control

PostgreSQL通过MVCC做并发控制,并发控制即对每个Session进行隔离,防止因为其他并发事务的更新导致当前事务观察到不一致的数据。

并发控制常见方法有:PCC、OCC、MVCC,各自又都有多种实现方式。

PCC、OCC只能做到读读互不阻塞,而MVCC可以做到读写互不阻塞,因此MVCC的性能更好,除了PostgreSQL外,MySQL也使用MVCC做并发控制,不过二者实现MVCC的方式不同。

在保证基于MVCC的读写互不阻塞的前提下,PostgreSQL引入了SSI隔离级别,提供最严格的事务隔离。

你也可以选择不用MVCC,而是通过PostgreSQL提供的表锁和行锁,自己控制并发事务的冲突,但性能是不如MVCC的。此外,PostgreSQL还提供了application-defined advisory locks,即不与事务绑定的锁。

Transaction Isolation

多个事务并发执行可能会产生如下这些现象。

现象 解释
Dirty Read 当前事务读取到了并发事务未提交的数据
Nonrepeatable Read 对于某一行数据,两次读取结果不一致
Phantom Read 对于满足某一条件的若干行数据,两次读取结果不一致
Serialization Anomaly 并发执行事务的结果,与以某种顺序串行执行这一组事务的结果不一致

SQL标准定义了4种隔离级别,对于每种隔离级别,都禁止了一些现象的产生。

Isolation Level Dirty Read Nonrepeatable Read Phantom Read Serialization Anomaly
Read Uncommitted
Read Committed ×
Repeatabled Read × ×
Serializable × × × ×

当然,上面是SQL标准规定的,但PostgreSQL有自己的想法。

Isolation Level Dirty Read Nonrepeatable Read Phantom Read Serialization Anomaly
Read Uncommitted ×
Read Committed ×
Repeatabled Read × × ×
Serializable × × × ×

你应该注意到RU和RC的表现是一样的,PostgreSQL实际上只有3种隔离级别。

你还应该注意到RR中不允许Phantom Read,但这是符合SQL标准的,因为SQL标准只规定了RR中不允许产生Dirty Read and Nonrepeatable Read

通过SET TRANSACTION命令可以设置隔离级别。

注意:PostgreSQL中,JSON Types and Sequence Manipulation Functions并不遵循标准的事务行为。

Read Committed

PostgreSQL中,默认的隔离级别就是RC,在RC Transaction中,每一条命令开始时都会获取一次快照,并基于这个快照做相应操作。

快照包括已提交的事务的结果,和当前事务未提交的结果,快照是一个逻辑概念,表示一个可见范围,由每行数据的某个版本组成。

SELECT

先获取快照,接着查询并返回结果集。

UPDATE, DELETE, SELECT FOR UPDATE, SELECT FOR SHARE

先获取快照,计算WHERE找到对应行。指定行可能已经被另一个并发事务(TA)更新/删除/锁定了,这时候需要等待TA提交或回滚。

如果TA回滚了,则对指定行进行更新/删除/锁定。

如果TA提交了,如果是DELETE,则什么都不做,如果不是DELETE,则再执行一次当前命令。

INSERT

先获取快照,计算是否可以插入,可以就插入,但由于并发事务的结果对INSERT是不可见的,因此可能插入失败。

INSERT CONFLICT DO UPDATE

先获取快照,计算是否可以插入,如果不可以插入或可以插入但插入失败,则执行UPDATE。

Repeatable Read

在RR Transaction中,只在事务开始时获取一次快照,每一条命令都基于这个快照做相应操作,因此连续Select得到的结果是相同的。

UPDATE, DELETE, SELECT FOR UPDATE, SELECT FOR SHARE

先去快照上搜索目标行,目标行可能已经被另一个并发事务(TA)更新/删除/锁定了,这时候需要等待TA提交或回滚。

如果TA回滚了,则对指定行进行更新/删除/锁定。

如果TA提交了并且更新/删除了指定行,会回滚事务并报序列化冲突的错误。

ERROR: could not serialize access due to concurrent update
复制代码

这是因为RR Transaction中,不能更新/删除/锁定其他并发事务的更改。收到这个错误后,应用程序应当重试这个事务。

注意到RR是没有避免Serialization Anomaly的,比如说顺序执行的Select会看到更新,但是并发执行的Select可能看不到更新。

Serializable

现在有这样一张表,和两个并发执行的事务,假设在RR隔离级别下。

class value
1 10
1 20
2 100
2 200
SELECT SUM(value) FROM mytab WHERE class = 1;
INSERT INTO mytab VALUES (2, 30);
复制代码

查询得到的结果为30。

SELECT SUM(value) FROM mytab WHERE class = 2;
INSERT INTO mytab VALUES (1, 300);
复制代码

查询得到的结果为300。

从并发执行的角度来看,这个结果是合理的,那么是否能以某种顺序串行执行,且得到的结果相同呢?

答案是不可以,这里也印证了RR是不能避免Serialization Anomaly的,那如果是Serializable隔离级别下,会是什么结果呢?

答案是其中一个事务会提交,而另一个事务会回滚并报序列化冲突的错误。

ERROR: could not serialize access due to read/write dependencies among transactions
复制代码

Serializable原理和RR一样,但是Serializable会监测并发事务能否序列化,就像上面的例子一样。当使用Serializable Transaction时,应该意识到事务中Select的结果在事务提交前都不该视其为有效的数据。

PostgreSQL使用了谓词锁,来判断序列化冲突。

什么是谓词?这可真是个糟糕的名字,本例中WHERE class = 1就是一个谓词。Select WHERE class = 1即持有该谓词的锁,当其他事务INSERT class = 1时,触发序列化冲突错误,因此这里虽然用了锁,但是不会阻塞。

还要注意的是,即便是可序列化的并发事务,依旧可能因为唯一约束而导致冲突。

Ending

按经典八股来说,提到事务先来一套ACID,AID在第一节提到了,那么C呢?事实上C是AID的结果,如果做到了AID,也就做到了C,当然应用层程序也得正确才是。本文中将的MVCC是保证了AI,至于D本文没讲到,是WAL保证的。

本文大部分参考自PostgreSQL官方文档,若有错误万望指出,不胜感激。

猜你喜欢

转载自juejin.im/post/7048303102033657892