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';
我们需要保证:
- 这些更新要么全部发生,要么全部没有发生。
- 如果中途出错,这之前的更新都不会生效。
将这些更新做成一个Transaction可以实现这两点。
BEGIN;
UPDATE accounts SET balance = balance - 100.00 WHERE name = 'Alice';
UPDATE accounts SET balance = balance + 100.00 WHERE name = 'Bob';
COMMIT;
除此之外:
- 一旦Transaction完成,即便不久后数据库崩溃,更新的数据也不会丢失。
- 当多个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官方文档,若有错误万望指出,不胜感激。