深度剖析:PG实验中的PostgreSQL锁机制

发表时间: 2019-08-05 00:03

概述

前面已经介绍了PG锁机制的一些概念性内容,这里主要做一些相关实验,帮助大家理解下PG的锁机制。

以下基于PG10.9进行测试。


1、创建测试表

CREATE TABLE parent ( id serial NOT NULL PRIMARY KEY, name text NOT NULL); CREATE TABLE child ( id serial NOT NULL PRIMARY KEY, parent_id int4 NOT NULL, name text NOT NULL, CONSTRAINT child_parent_fk FOREIGN KEY (parent_id) REFERENCES parent(id));


2、简单事务测试

尝试一些简单的事务,看看锁是什么样的:

BEGIN;SELECT * FROM active_locks; -- There are no active locks yetINSERT INTO parent (name) VALUES ('Parent 1');SELECT * FROM active_locks;COMMIT;SELECT * FROM active_locks;

可以看到在 parent 表里插入一行后,获得了 parent 表上的行独占锁。parent_id_seq 是 parent 表的主键序列。由于这种关系被选中(如表),获得了访问共享锁。


3、插入数据

咱们试着往 child 表里插点东西;

BEGIN;INSERT INTO child (parent_id, name) VALUES (1, 'Child 1 Parent 1');COMMIT;

可以看到 parent 表上增加的行共享锁。我们看不到的是,这个插入同样获得了 parent 表上引用行的行级共享锁。并行执行两个事务我们就可以看到它了:

-- Transaction 1BEGIN;INSERT INTO child (parent_id, name) VALUES (1, 'Child 2 Parent 1');-- Transaction 2BEGIN;DELETE FROM parent WHERE id = 1;

现在开始第三个会话,看看锁怎么样了:

SELECT * FROM active_locks;

DELETE 查询被阻塞了,等待事务 1 完成。我们可以看到它在元组 1 上获得了一个锁。但是如果我们看到所有的锁都是准许的(granted=t),为什么 DELETE 查询被阻塞了?这两个事务在任何关系上的锁都没有同步过。事实上,如果一个事务在某些行上持有一个锁,第二个事务请求这个锁,第二个事务会尝试获取持有此锁的事务上的共享锁。当第一个事务完成时,第二个事务将继续。这是可能的,因为每个事务都持有它自身的排他锁。我们可以看看 pg_locks 视图,这是输出(只有部分是重要的):

可以看到事务 563(pid 5690)和事务564(pid 5790)拥有它们事务标识上的排他锁,事务564 获得了事务563 上的共享锁。


4、更新子表

可以玩玩更新子表但不实际改变任何父表与相关的东西(在这个案例中是parent_id列)。

BEGIN;UPDATE child SET name = 'My new name' WHERE id = 1;SELECT * FROM active_locks;UPDATE child SET name = 'My new name' WHERE id = 1;SELECT * FROM active_locks;COMMIT;

可以看到正在执行的 UPDATE 查询不会触及任何与父表相关的东西。第一次执行后,我们可以看到,只有 child 表包含表级锁。行级锁也是如此。只有 child 表的行有更新锁。这是 Postgres 中的优化。

如果锁管理器可以从第一个查询中发现外键没有改变(没有被更新查询提及或被设置为相同的值),它不会锁定父表。但在第二个查询它会像文档描述的那样处理(它将锁定 parent 表为行共享锁定模式和涉及的行为分享模式)。这是非常危险的,因为它会导致最危险的和最难找到的死锁。不过可以在事务开头使用显式锁定以避免它。

防止死锁的最好方式,是当我们意识到它们可能在两个事务之间发生时,去按一定顺序获取行级锁(例如主键排序)和首先获取最严格的锁。一旦在 Postgres 发生死锁,可以通过中止一个参与死锁的事务来消除。


总结

对 Postgres 中锁的工作机制铭记于心非常重要。在高并发环境中死锁可能无法避免,但重要的是要知道如何发现、监控并解决它们。即使所有事情都照书而行也不一定能解决所有潜在的锁问题,但会减少它们并使其易于解决。表级锁可以通过 pg_locks 系统视图查看,但行级锁不行,所以这让调试锁更为困难,不过Postgres 的未来版本中还是有可能实现的。后面会分享更多devops和DBA方面的内容,感兴趣的朋友可以关注一下~