likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 15

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 LevelDirty ReadNonrepeatable ReadPhantom ReadSerialization Anomaly
Read Uncommitted
Read Committed×
Repeatabled Read××
Serializable××××

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

Isolation LevelDirty ReadNonrepeatable ReadPhantom ReadSerialization 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隔离级别下。

classvalue
110
120
2100
2200
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官方文档,若有错误万望指出,不胜感激。

转载自:https://juejin.cn/post/7048303102033657892
评论
请登录