并发控制简介
PostgreSQL提供了多种方式以控制对数据的并发访问。在数据库内部,数据的一致性使用多版本模式(多版本并发控制(Multiversion Concurrency Control),即MVCC)维护。这意味着每个SQL语句查询到的数据,是查询开始时间节点的快照(一个数据版本),而与查询期间数据状态无关。此机制确保语句不会查询到由并发事务对同一行数据进行修改而产生的不一致数据,从而为每个数据库会话提供了事务隔离特性。MVCC通过避免传统数据库系统中的锁定方法,最大程度上减小了在多用户并发场景下的锁争用,从而提高了性能。
在PG中,mvvc只适用于读已提交、可重复度两个隔离级别。
事务隔离
四种现象
脏读
事务读取到另一并发事务未提交的数据。
不可重复读
同一事务内,读取之前读过的数据时,发现读取到了开始查询时间点之后其他事务修改后的数据。
幻读
同一事务,重复执行查询出来的符合条件行数,受另一最近提交的事务影响。
序列化异常
一次提交一组事务,与随机顺序单个事务运行的结果不同。
事务隔离级别
隔离级别 | 脏读 | 不可重复读 | 幻读 | 序列化异常 |
读未提交 | 允许,但PG中不允许 | 可能 | 可能 | 可能 |
读已提交 | 不可能 | 可能 | 可能 | 可能 |
可重复读 | 不可能 | 不可能 | 允许,但PG中不允许 | 可能 |
序列化(串行化) | 不可能 | 不可能 | 不可能 | 不可能 |
读未提交:一个事务可以读取另一个事务未提交的内容。pg中不存在该级别,pg的mvcc机制使读未提交和读已提交表现一致。
读已提交:一个事务可以读取另一个已提交事务的内容,这可能导致同一个事务中两次查询得到的结果不一致。
可重复读:一个事务可以读取另一个已提交事务的数据(列表层面)。
序列化:串行执行
PG中的锁
表级锁
以下列出了可用的锁模式,以及在何种场景下PostgreSQL会自动使用它们。也可以使用LOCK命令显式地获取这些锁。请谨记,所有这些锁模式均为表级锁,即使锁名中包含“行”(这种命名是历史遗留问题)。名称在某种程度上反应了每个锁模式的典型用法--但语义都相同。各种锁模式之间的唯一真正区别就是锁模式之间的冲突。两个事务不能同时在同一张表获取相互冲突的锁模式。(不过,事务本身无冲突,例如,对于同一张表,它可以先获得ACCESS EXCLUSIVE锁,随后又获得ACCESS SHARE锁。)多个事务可以并行获取不冲突的锁模式。请注意,有些锁模式,与自身会产生冲突(例如,同时仅可以有一个事务获得ACCESS EXCLUSIVE锁);有些却不会(例如,多个事务可同时获取ACCESS SHARE锁)。
表级锁模式
ACCESS SHARE(访问共享)
仅与ACCESS EXCLUSIVE锁模式冲突。
SELECT命令会在相关表上获取该锁。一般情况下,所有仅读取表而不更改表的查询均会获取该锁模式。
ROW SHARE(行共享)
与EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。
SELECT FOR UPDATE和SELECT FOR SHARE命令在目标表上获取该锁模式(在其他相关但未选择为FOR UPDATE/FOR SHARE的表上获取ACCESS SHARE锁)。
ROW EXCLUSIVE(行排他)
与SHARE,SHARE ROW EXCLUSIVE,EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。
UPDATE,DELETE和INSERT命令在目标表上获取此锁模式(在其他相关表上获取ACCESS SHARE锁)。一般情况下,所有更改表数据的命令均会获取该锁模式。
SHARE UPDATE EXCLUSIVE(共享更新排他)
与SHARE UPDATE EXCLUSIVE,SHARE,SHARE ROW EXCLUSIVE,EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。此模式可防止表发生并发模式(schema)更改和VACUUM。
由以下命令获取:VACUUM(无FULL)、ANALYZE、CREATE INDEX CONCURRENTYLY、REINDEX CONCURRENTLY、CREATE STATISTICS以及特殊的ALTER INDEX和ALTER TABLE变体(更多新详情请参见命令ALTER INDEX和ALTER TABLE)。
SHARE
与ROW EXCLUSIVE,SHARE UPDATE EXCLUSIVE,SHARE ROW EXCLUSIVE,EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。该模式可防止表发生并发数据变更。
由命令CREATE INDEX(无CONCURRENTLY)获取。
SHARE ROW EXCLUSIVE
与ROW EXCLUSIVE,SHARE UPDATE EXCLUSIVE,SHARE,SHARE ROW EXCLUSIVE,EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。该模式可防止表发生并发数据更改,且是自排他,所以可保证每次仅一个会话可持有该锁。
由命令CREATE TRIGGER和ALTER TABLE的某些格式获取。
EXCLUSIVE
与ROW SHARE,ROW EXCLUSIVE,SHARE UPDATE EXCLUSIVE,SHARE,SHARE ROW EXCLUSIVE,EXCLUSIVE和ACCESS EXCLUSIVE锁模式冲突。该模式仅允许并发的ACCESS SHARE锁,即,若事务持有该锁,那么仅可并行执行读取表数据的操作。
由REFRESH MATERIALIZED VIEW CONCURRENTLY命令获取。
ACCESS EXCLUSIVE
与所有锁模式冲突(ACCESS SHARE,ROW SHARE,ROW EXCLUSIVE,SHARE UPDATE EXCLUSIVE,SHARE,SHARE ROW EXCLUSIVE,EXCLUSIVE和ACCESS EXCLUSIVE)。该模式确保仅持有该锁的事务访问该目标表。
由命令DROP TABLE,TRUNCATE,REINDEX,CLUSTER,VACUUM FULL和REFRESH MATERIALIZED VIEW(无CONCURRENTLY)获取。ALTER INDEX和ALTER TABLE的一些模式同样获取此级别的锁模式。这也是LOCK TABLE语句的默认锁模式。
冲突模式
Requested Lock Mode | Current Lock Mode | |||||||
ACCESS SHARE | ROW SHARE | ROW EXCLUSIVE | SHARE UPDATE EXCLUSIVE | SHARE | SHARE ROW EXCLUSIVE | EXCLUSIVE | ACCESS EXCLUSIVE | |
ACCESS SHARE | X | |||||||
ROW SHARE | X | X | ||||||
ROW EXCLUSIVE | X | X | X | X | ||||
SHARE UPDATE EXCLUSIVE | X | X | X | X | X | |||
SHARE | X | X | X | X | X | |||
SHARE ROW EXCLUSIVE | X | X | X | X | X | X | ||
EXCLUSIVE | X | X | X | X | X | X | X | |
ACCESS EXCLUSIVE | X | X | X | X | X | X | X | X |
行级锁
除了表级锁,还有行级锁,以下列出了行级锁及在什么情况下PostgreSQL会自动的使用它们。请注意,在不同的子事务中,事务可以在同一行上获得冲突的锁;但除此之外,两个事务永远不能在同一行上获得冲突的锁。行级锁不会影响检索数据;它们仅会阻塞对同一行的写入及锁。如表级锁一样,行级锁仅会在事务结束或快照回滚时释放。
行级锁模式
FOR UPDATE
FOR UPDATE将SELECT语句返回的行锁定用于更新。这可以防止这些行在事务结束之前被其他事务锁定、修改或删除。也就是说,其他尝试针对这些行执行UPDATE,DELETE,SELECT FOR UPDATE,SELECT FOR NO KEY UPDATE,SELECT FOR SHARE或者SELECT FOR KEY SHARE的事务在当前事务结束之前会一直被阻塞;相反的,SELECT FOR UPDATE将会等待在同一行执行这些命令的并行事务,然后会返回更新后的行(如果这些行被删除了,那么不会返回行)。不过,在可重复读或序列化事务中,如果被锁定的行在事务开始后被修改了,那么会抛出错误。
DELETE行以及UPDATE某一列值,也会获取FOR UPDATE锁模式。当前,UPDATE情况下考虑的为可以在其上有唯一索引、可用于外键的列(所以不考虑部分索引和表达式索引),不过将来这种机制可能会改变。
FOR NO KEY UPDATE
行为与FOR UPDATE类似,不过锁级别低一些;该锁不会阻塞在同一行上的SELECT FOR KEY SHARE命令。那些不获取FOR UPDATE锁的UPDATE命令均获取该锁。
FOR SHARE
行为与FOR NO KEY UPDATE类似,不过它是在检索到的行上加共享锁而不是排它锁。共享锁会阻塞在相同行上执行的UPDATE,DELETE,SELECT FOR UPDATE或者SELECT FOR NO KEY UPDATE命令,但不会阻塞SELECT FOR SHARE或者SELECT FOR KEY SHARE命令。
FOR KEY SHARE
行为与FOR SHARE类似,但锁级别更低一些:会阻塞SELECT FOR UPDATE,但不会阻塞SELECT FOR NO KEY UPDATE。该锁会阻塞其他事务执行DELETE或那些改变键值的UPDATE操作,但不会阻塞其他UPDATE操作,也不会阻塞SELECT FOR NO KEY UPDATE,SELECT FOR SHARE或SELECT FOR KEY SHARE。
PostgreSQL并不会在内存中记录变更行的信息,所以对于同一时间锁定的行数没有限制。不过,锁定行可能会导致磁盘写,例如:SELECT FOR UPDATE修改选取的行以将其标记为锁定,从而会导致磁盘写。
行级锁冲突
quested Lock Mode | Current Lock Mode | |||
FOR KEY SHARE | FOR SHARE | FOR NO KEY UPDATE | FOR UPDATE | |
FOR KEY SHARE | X | |||
FOR SHARE | X | X | ||
FOR NO KEY UPDATE | X | X | X | |
FOR UPDATE | X | X | X | X |
页级锁
除了表级锁和行级锁,还有页级共享/排他锁用以控制对于共享缓冲池中表页的读/写。这些锁在行被获取或更新后立马释放。应用程序开发者一般无需关系此类锁。
咨询锁
一种显式可重入锁策略,完全由应用程序控制。
咨询锁函数
Name | Return Type | Description |
pg_advisory_lock(key bigint) | void | Obtain exclusive session level advisory lock |
pg_advisory_lock(key1 int, key2 int) | void | Obtain exclusive session level advisory lock |
pg_advisory_lock_shared(key bigint) | void | Obtain shared session level advisory lock |
pg_advisory_lock_shared(key1 int, key2 int) | void | Obtain shared session level advisory lock |
pg_advisory_unlock(key bigint) | boolean | Release an exclusive session level advisory lock |
pg_advisory_unlock(key1 int, key2 int) | boolean | Release an exclusive session level advisory lock |
pg_advisory_unlock_all() | void | Release all session level advisory locks held by the current session |
pg_advisory_unlock_shared(key bigint) | boolean | Release a shared session level advisory lock |
pg_advisory_unlock_shared(key1 int, key2 int) | boolean | Release a shared session level advisory lock |
pg_advisory_xact_lock(key bigint) | void | Obtain exclusive transaction level advisory lock |
pg_advisory_xact_lock(key1 int, key2 int) | void | Obtain exclusive transaction level advisory lock |
pg_advisory_xact_lock_shared(key bigint) | void | Obtain shared transaction level advisory lock |
pg_advisory_xact_lock_shared(key1 int, key2 int) | void | Obtain shared transaction level advisory lock |
pg_try_advisory_lock(key bigint) | boolean | Obtain exclusive session level advisory lock if available |
pg_try_advisory_lock(key1 int, key2 int) | boolean | Obtain exclusive session level advisory lock if available |
pg_try_advisory_lock_shared(key bigint) | boolean | Obtain shared session level advisory lock if available |
pg_try_advisory_lock_shared(key1 int, key2 int) | boolean | Obtain shared session level advisory lock if available |
pg_try_advisory_xact_lock(key bigint) | boolean | Obtain exclusive transaction level advisory lock if available |
pg_try_advisory_xact_lock(key1 int, key2 int) | boolean | Obtain exclusive transaction level advisory lock if available |
pg_try_advisory_xact_lock_shared(key bigint) | boolean | Obtain shared transaction level advisory lock if available |
pg_try_advisory_xact_lock_shared(key1 int, key2 int) | boolean | Obtain shared transaction level advisory lock if available |
咨询锁用法
库级:
select pg_advisory_lock(1) select pg_advisory_unlock(1)
表级:
select pg_advisory_lock(1) from users select pg_advisory_unlock(1) from users
行级:
select pg_advisory_lock(tableoid::int,id),tableoid,* from users where id = 1; select pg_advisory_unlock(tableoid::int,id),* from users where id = 1; select pg_advisory_lock(id) from users select pg_advisory_unlock(id) from users
MVCC
Multi-Version Concurrency Control 多版本并发控制,MVCC 是一种并发控制的方法,一般在数据库管理系统中,实现对数据库的并发访问;在编程语言中实现事务内存。(MVCC:百度百科)
在PG提供的三种隔离级别中,MVCC机制只工作在读已提交和可重复读两个隔离级别中,序列化使用锁控制事务串行化执行。
实例
测试表:
CREATE TABLE users ( id int4 NOT NULL, age int4 ); ALTER TABLE users ADD CONSTRAINT "user_pkey" PRIMARY KEY ("id");
MVCC验证
验证数据库默认事务隔离级别为提交读
show transaction_isolation; --result: read committed
事务1执行:
BEGIN; SELECT * from users;
id | age |
1 | 3 |
事务2执行:
BEGIN; UPDATE users set age = 4 where "id" = 1; SELECT * from users;
id | age |
1 | 4 |
在事务1中再次执行SELECT * from users
查询结果不变:
id | age |
1 | 3 |
可见,事务1和事务2的可见性不同,所以sqlSELECT * from users
查询结果也不同。证明多版本的存在。
脏读
PG中不存在未提交读隔离级别,显示使用未提交读隔离级别时,PG内部会使用为提交读隔离级别处理,脏读在PG中不会发生。
不可重复读现象验证
继续 MVCC验证 的测试。在事务2中执行COMMIT
提交事务。
事务1中再次执行SELECT * from users
查询结果:
id | age |
1 | 4 |
事务1的查询结果发生了改变,即发生了“不可重复读”现象,在同一个事务(事务1)中,两次查询返回的结果不一致。
消除不可重复读验证
解说上述所有事务。
事务1执行:
BEGIN; set default_transaction_isolation='repeatable read'; SELECT * from users;
id | age |
1 | 6 |
事务2执行:
BEGIN; set default_transaction_isolation='repeatable read'; UPDATE users set age = 7 where "id" = 1; SELECT * from users;
id | age |
1 | 7 |
执行COMMIT
提交事务2,并在事务1中再次查询SELECT * from users
查询结果:
id | age |
1 | 6 |
事务1没有发生不可重复读现象,age仍然是6。证明在repeatable read
事务隔离级别中解决了不可重复读现象。
mvvc解决不可重复读的方式:这个级别与Read Committed不同,可重复读事务中的查询在事务中第一个非事务控制语句开始时确定快照,而不是在事务中当前语句开始时确定快照。因此,单个事务中的连续SELECT命令会看到相同的数据,也就是说,它们不会看到在自己的事务启动后提交的其他事务所做的更改
幻读
PG的MVCC机制在可重复读隔离级别中解决了幻读问题。数据插入时或更新时,会在相应数据行中设置xmin,xmax,xmin,xmax,PG会根据这些辅助列控制事务的可见性,实现效果类似InnoDB的间隙锁。在提交读中,会发生幻读现象,在可重复读中,因为一直使用事务开始时数据库快照,所以消除了幻读。
隐藏列
PostgreSQL中,对于每一行数据(称为一个tuple),包含有多个隐藏字段。
查询命令:select * from pg_attribute where attrelid in (select oid from pg_class where relname = 'users');
attrelid | attname | atttypid | attstattarget | attlen | attnum | attndims | attcacheoff | atttypmod | attbyval | attstorage | attalign | attnotnull | atthasdef | atthasmissing | attidentity | attgenerated | attisdropped | attislocal | attinhcount | attcollation | attacl | attoptions | attfdwoptions | attmissingval | |
56034 | tableoid | 26 | 0 | 4 | -6 | 0 | -1 | -1 | t | p | i | t | f | f | f | t | 0 | 0 | |||||||
56034 | cmax | 29 | 0 | 4 | -5 | 0 | -1 | -1 | t | p | i | t | f | f | f | t | 0 | 0 | |||||||
56034 | xmax | 28 | 0 | 4 | -4 | 0 | -1 | -1 | t | p | i | t | f | f | f | t | 0 | 0 | |||||||
56034 | cmin | 29 | 0 | 4 | -3 | 0 | -1 | -1 | t | p | i | t | f | f | f | t | 0 | 0 | |||||||
56034 | xmin | 28 | 0 | 4 | -2 | 0 | -1 | -1 | t | p | i | t | f | f | f | t | 0 | 0 | |||||||
56034 | ctid | 27 | 0 | 6 | -1 | 0 | -1 | -1 | f | p | s | t | f | f | f | t | 0 | 0 | |||||||
56034 | id | 23 | -1 | 4 | 1 | 0 | -1 | -1 | t | p | i | t | f | f | f | t | 0 | 0 | |||||||
56034 | ........pg.dropped.2........ | 0 | 0 | -1 | 2 | 0 | -1 | 259 | f | x | i | f | f | f | t | t | 0 | 100 | |||||||
56034 | age | 23 | -1 | 4 | 3 | 0 | -1 | -1 | t | p | i | f | f | f | f | t | 0 | 0 |
xmin :在创建(insert)记录(tuple)时,记录此值为插入tuple的事务ID。
xmax :默认值为0.在删除tuple时,记录此值。
cmin和cmax :标识在同一个事务中多个语句命令的序列值,从0开始,用于同一个事务中实现版本可见性判断。
oid:行的对象标识符(对象 ID)。这个字段只有在创建表的时候使用了 WITH OIDS,或者是设置了配置参数 default_with_oids 时出现。 这个字段的类型是 oid(和字段同名); 参阅Section 8.12 获取有关这种类型的更多信息。
tableoid: 包含本行的表的 OID。这个字段对那些从继承层次中选取的查询特别有用(参阅 Section 5.8), 因为如果没有它的话,我们就很难说明一行来自哪个独立的表。 tableoid 可以和pg_class 的 oid 字段连接起来获取表名字。
ctid: 一个行版本在它所处的表内的物理位置。请注意,尽管 ctid 可以用于非常快速地定位行版本,但每次 VACUUM FULL 之后, 一个行的 ctid 都会被更新或者移动。 因此 ctid 是不能作为长期的行标识符的。 应该使用OID,或者更好是用户定义的序列号,来标识一个逻辑行。
MVCC与锁
MVCC主要是为解决读写并发加锁的问题,最终是为了减少锁的使用,提高查询效率。在消除不可重复读验证用例中,写对读没有影响。但是在一些必须的场景中,在查询时不允许变更,比如在盘点库存时,不允许出入库,此时可以显式使用for update加上更高级别的锁,从而阻止更新语句并发访问。
参见: