今天,我们学习下Postgresql中的锁机制。锁是数据库事务的基础,通过锁才能保证数据库在并发时能够保证数据的安全和一致,才能够达到事务的一致性和隔离性。但是任何事物都有它的两面性,引入锁同样会增加性能开销和可能的死锁问题,不过好在都存在解决方法。Postgresql和其它数据库一样,从锁作用的对象上来说可以分为表锁、行锁两种,从排他性来说可以大体分为共享锁和排他锁,但是由于Postgresql是通过多版本的方式对数据库进行更新,它也引入了Access锁。接下来我们就对这些内容分类进行学习。

一、Postgresql中的表锁

       Postgresql的表锁共有8个,而这8个锁又可以分成四个小类:普通锁、访问锁、意向锁和严格锁,本小节种,笔者会按照这四种分类对表锁分别介绍。

1.1 普通共享锁SHARE和普通排他锁EXCLUSIVE

  • SHARE

       SHARE是共享锁,也就是读锁,当它加到表上之后,整个表只允许读,不允许改,如果我们为一个表创建索引(不带CONCURRENTLY)时,会创建这种锁。它与ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE ROW EXCLUSIVE、EXCLUSIVE、ACCESS EXCLUSIVE冲突。接下来,我们通过实例进行验证。

       现在有一个表t1,t1的内容如下:

stock_analysis_data=# select * from t1;
 id | name  
----+-------
  1 | tom
  2 | json
  3 | hyman

       接下来,对表t1加上Share锁,由于通过创建索引的方式使数据库自动加Share锁,我们无法控制时间,现在选择直接使用lock命令加锁的方式:

stock_analysis_data=# BEGIN;
BEGIN
stock_analysis_data=# Lock table t1 in SHARE MODE NOWAIT;
LOCK TABLE

       先从pg_locks表里面查看加索的信息,在此之前,先看下本次会话所在的pid:

stock_analysis_data=# select pg_backend_pid();
 pg_backend_pid 
----------------
          26004

       查看锁状态:

postgres=# select locktype,database,relation,pid,mode from pg_locks where pid=26004;
  locktype  | database | relation |  pid  |     mode      
------------+----------+----------+-------+---------------
 virtualxid |          |          | 26004 | ExclusiveLock
 relation   |    16385 |    19608 | 26004 | ShareLock

       我们只需要关注上表中locktype为relation的锁即可,这是刚才我们加的Share锁,database字段是锁所在的数据库的oid,relation字段是表的oid,pid就是加锁的事务所在的进程id,mode表明锁的类型为Share。

       接下来,再去开启一个中断,对表进行查询,发现查询功能正常:

stock_analysis_data=# select * from t1;
 id | name  
----+-------
  1 | tom
  2 | json
  3 | hyman
(3 rows)

       但是我们尝试去改变表中的数据,比如删除id为3的数据行,则会阻塞,直到加锁的事务结束,释放掉锁为止。

  • EXCLUSIVE

       任何的Postgresql的命令都不会加EXCLUSIVE锁,不过在一些操作时,可能会在某些系统表加上此类锁。为了验证EXCLUSIVE锁的特性,我们还是先要对t1表加上改锁:

stock_analysis_data=# Lock table t1 in exclusive MODE NOWAIT;
LOCK TABLE

       查看pg_locks里面锁的状态:

stock_analysis_data=# select locktype,database,relation,pid,mode from pg_locks where pid=26004;
  locktype  | database | relation |  pid  |     mode      
------------+----------+----------+-------+---------------
 virtualxid |          |          | 26004 | ExclusiveLock
 relation   |    16385 |    19608 | 26004 | ExclusiveLock

      EXCLUSIVE锁和除了ACCESS SHARE外所有锁冲突,而ACCESS SHARE锁在执行select语句会加该锁,所以一旦表t1被加了EXCLUSIVE锁,对于另一个事务的效果也是只能select,不能修改表。

       select正常,updae、insert、和delete操作都会阻塞。

stock_analysis_data=# select * from t1;
 id | name 
----+------
  1 | tom
  2 | json
(2 rows)

1.2 访问共享锁ACCESS SHARE和访问排他锁ACCESS EXCLUSIVE

       Access锁是Postgresql特有的一种锁,主要是针对Postgresql多版本更新数据的方式而创建的锁。所谓的多版本更新数据,意思是Postgresql在更改某一行数据时,不是在该行直接修改数据,而是另外复制了一个新行,修改都在新行上进行。Access锁有两种:ACCESS SHARE和ACCESS EXCLUSIVE,下面分别进行介绍。

  • ACCESS SHARE

       select语句会在对应的表上加上ACCESS SHARE类型的锁,通常情况下,任何只读取表而不修改表的查询都会请求这种锁模式。加上了该锁之后,表明即使在修改数据的情况下也允许读数据。ACCESS SHARE锁只和ACCESS EXCLUSIVE锁冲突。

       下面,我们还是通过实例的方式来验证ACCESS SHARE锁的特点。首先,查询t1表,对t1表加上ACCESS SHARE类型的锁。

stock_analysis_data=# select * from t1;
 id | name 
----+------
  1 | tom
  2 | json
(2 rows)

       在另外一个终端中查询t1表的加锁信息:

stock_analysis_data=# select locktype,database,relation,pid,mode from pg_locks where pid=26004;
  locktype  | database | relation |  pid  |      mode       
------------+----------+----------+-------+-----------------
 relation   |    16385 |    19627 | 26004 | AccessShareLock
 relation   |    16385 |    19608 | 26004 | AccessShareLock
 virtualxid |          |          | 26004 | ExclusiveLock
(3 rows)

      可以看到,select语句对oid位19608和19627两个relation都加了AccessShare锁,我们知道19608是表t1的OID,那么19627是什么?不妨先查一下:

stock_analysis_data=# select relname,reltype,oid from pg_class where oid=19627;
  relname  | reltype |  oid  
-----------+---------+-------
 t1_id_idx |       0 | 19627
(1 row)

       t1_id_idx 是建立在t1表上的索引,因为select语句走了索引,所以需要在该索引上加上AccessShare锁。

       完成了对t1的加锁之后,尝试在另一个终端中创建ACCESS EXCLUSIVE锁和ROW EXCLUSIVE锁,来查看ACCESS SHARE锁的效果。

       首先尝试修改表t1,ALTER操作会对t1加上ACCESS EXCLUSIVE锁:

stock_analysis_data=# alter table t1 add column age int;

       我们尝试通过alter命令,对表t1增加一个新列age,但是由于t1上已经加了ACCESS SHARE锁,所以alter命令发生了阻塞。直到select的事务结束之后,阻塞状态才退出:

stock_analysis_data=# alter table t1 add column age int;

ALTER TABLE
  • ACCESS EXCLUSIVE

       ACCESS EXCLUSIVE模式与所有的模式的锁冲突,包括SHARE结尾和EXCLUSIVE结尾的锁,也就是说当一个表加上ACCESS EXCLUSIVE锁之后,该表会阻塞其它事务的任何操作。会对表加ACCESS EXCLUSIVE的操作有ALTER TABLE、DROP TABLE、TRUNCATE、REINDEX、CLUSTER、VACUUM FULL等等。

       上文中,我们已经使用Alter给表加过ACCESS EXCLUSIVE锁,这里不再赘述。

1.3 意向共享锁ROW SHARE和意向排他锁ROW EXCLUSIVE

       意向锁是我们要修改表中某一行的数据时,需要先在表上加的一种锁,表示即将要在表的部分行上加上共享锁或者排它锁。也就是说我们在为一个数据表的某些数据行加行锁时,实际上在该表上至少加了两种锁,一种是意向表锁,一种才是行锁。

  • ROW SHARE

       使用select from update 或者使用selet for share会加上此锁,它和EXCLUSIVE以及ACCESS EXCLUSIVE冲突。我们还是在t1表上执行selet for share语句,查看加锁情况:

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=#  select *  from t1 where id=1 for update; 
 id | name | age 
----+------+-----
  1 | tom  |    
(1 row)

       再次从pg_locks表中查看锁的状态:

   locktype    | database | relation |  pid  |      mode       
---------------+----------+----------+-------+-----------------
 relation      |    16385 |    19627 | 26004 | AccessShareLock
 relation      |    16385 |    19608 | 26004 | RowShareLock
 virtualxid    |          |          | 26004 | ExclusiveLock
 transactionid |          |          | 26004 | ExclusiveLock

       如上面第4行,发现这次对t1表加了RowShare锁。在另外一个终端中执行update语句修改id为1的数据行,你会发现发生了阻塞:

stock_analysis_data=# update t1 set age=18 where id=1;

       而修改id为2的数据行,却正常执行:

stock_analysis_data=# update t1 set age=18 where id=2;

UPDATE 1

       这里需要解释一下,所谓意向锁,就是打算去做什么,但是还没有真做,所以意向锁之间是不会出现冲突的。上面的例子,当updae id为1的数据行时,会给表t1加上ROW EXCLUSIVE锁,而这里出现阻塞并不是因为ROW SHARE和ROW EXCLUSIVE之间发生了冲突,而是在id为1的行上都加上了排它锁,出现了冲突。

  • ROW EXCLUSIVE

       在前面的例子中,我们已经通过update语句使用过了ROW EXCLUSIVE。该锁会在UPDATE、DELETE、INSERT命令执行时在相关表上自动创建,它和其它三种EXCLUSIVE的锁和SHARE锁冲突。

1.4 SHARE UPDATE EXCLUSIVE和SHARE ROW EXCLUSIVE

       SHARE UPDATE EXCLUSIVE和SHARE ROW EXCLUSIVE是针对意向锁不会发生冲突的特点,为了应对可能出现的更加严格的锁需求提出的。

  • SHARE ROW EXCLUSIVE

       SHARE ROW EXCLUSIVE与ROW EXCLUSIVE、SHARE UPDATE EXCLUSIVE、SHARE ROW EXCLUSIVE\EXCLUSIVE、ACCESS EXCLUSIVE冲突,目前任何的Postgresql命令都不会创建该锁,在这里我们就不再赘述。

  • SHARE UPDATE EXCLUSIVE

       VACUUM(不带FULL选项)、ANALYZE、CREATE INDEX CONCURRENTLY命令会创建该锁。它与SHARE UPDATE EXCLUSIVE、SHARE 、SHARE ROW EXCLUSIVE、EXCLUSIVE、ACCESS EXCLUSIVE锁模式冲突。

二、Postgresql中的行锁

       相对于表锁来说,行锁就是加到某一行上的锁。它的模式比较简单,只有共享和排它两种类型。但是在Postgresql中,实际上由于使用多版本的方式更新数据,实际上当update时创建的排它行锁,也不会影响对该行的读。为了验证这一问题,我们采用如下实例:

       首先,在第一个终端中,执行update语句,更新t1表中id为1的数据行:

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# update t1 set age=18 where id=1;
UPDATE 1

       由于行锁是不会写入到pg_locks视图中的,此时我们简单通过查询pg_locks视图无法确认加没加行锁,但是如果另外一个事务此时争抢行锁时,若发生阻塞,会写pg_locks视图,我们可以根据这点来确认行锁的存在。所以我们需要启用第二个终端,再次执行上面的update语句:

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# update t1 set age=18 where id=1;

       很明显,语句发生了阻塞,这是因为两个事务间行锁发生了冲突。接下来,我们再启用第三个终端,查看pg_locks视图:

stock_analysis_data=# select locktype,database,relation,pid,page,tuple,mode,granted from pg_locks;
   locktype    | database | relation |  pid  | page | tuple |       mode       | granted 
---------------+----------+----------+-------+------+-------+------------------+---------
 relation      |    16385 |    19627 | 30971 |      |       | RowExclusiveLock | t
 relation      |    16385 |    19608 | 30971 |      |       | RowExclusiveLock | t
 virtualxid    |          |          | 30971 |      |       | ExclusiveLock    | t
 relation      |    16385 |    19627 | 26004 |      |       | RowExclusiveLock | t
 relation      |    16385 |    19608 | 26004 |      |       | RowExclusiveLock | t
 virtualxid    |          |          | 26004 |      |       | ExclusiveLock    | t
 relation      |    16385 |    11645 | 31062 |      |       | AccessShareLock  | t
 virtualxid    |          |          | 31062 |      |       | ExclusiveLock    | t
 tuple         |    16385 |    19608 | 30971 |    0 |     1 | ExclusiveLock    | t
 transactionid |          |          | 30971 |      |       | ExclusiveLock    | t
 transactionid |          |          | 30971 |      |       | ShareLock        | f
 transactionid |          |          | 26004 |      |       | ExclusiveLock    | t
(12 rows)

       查看第12行,这就是在第二个终端等待获取的行锁。它的locktype是tuple,page和tuple代表等待叶编号为0,行编号为1的数据行(也就是ctid为01)释放锁,而其mode为Exclusive表示行排它。

      我们看到,此时再次对同一行update会发生锁冲突,从而阻塞命令。但是我们说过,由于Postgresql多版本更新的特点,行排它锁不会阻塞数据行的读取,若我们在第三个终端执行查询语句,会发现查询数据正常:

stock_analysis_data=# select * from t1 where id=1;
 id | name | age 
----+------+-----
  1 | tom  |    
(1 row)

三、死锁

       当两个以上的事务,比如事务A等待事务B中的资源,而事务B又在等待事务A中的资源,双方互相等待对方持有的资源,而又不释放自己的资源,就会导致死锁。数据库中,死锁主要会在两种场景中产生,下面我们将分别进行介绍。

3.1 互斥条件形成的死锁

       有这样一种场景:

(1)开启事务A,事务中首先我们要truncate t1(在t1上加上了EXCLUSIVE锁)

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# truncate table t1;
TRUNCATE TABLE

(2)开启事务B,事务中我们truncate t2(在t2上加上了EXCLUSIVE锁)

stock_analysis_data=# begin;
BEGIN
stock_analysis_data=# truncate table t2;
TRUNCATE TABLE

(3)我们再回到事务A,选择查询t2中的所有数据(尝试获取ACCESS SHARE锁),因为t2上已经加了EXCLUSIVE锁,所以select语句会发生阻塞:

stock_analysis_data=# select * from t2;

(4)回到事务B,查询t1表中的所有数据。此时,两个事务A、B就形成了死锁,只不过数据库可以侦测到此种类型的死锁,会自动处理掉。处理的方法就是回滚并结束事务B,如果不出意外,数据库会进行如下的提示:

stock_analysis_data=# select * from t1;
ERROR:  deadlock detected
LINE 1: select * from t1;
                      ^
DETAIL:  Process 31062 waits for AccessShareLock on relation 19608 of database 16385; blocked by process 26004.
Process 26004 waits for AccessShareLock on relation 19616 of database 16385; blocked by process 31062.
HINT:  See server log for query details.

       上述就是一个很典型的死锁出现的场景,不过因为它足够经典,所以现在数据库可以自行侦测和处理掉。不过我们在开发过程中也要主要,尽量避免这种交叉等待资源的情况的发生,最简单的方法就是不管在那个事务中,申请锁的顺序都要一致,比如事务A中申请锁的顺序是t1->t2->t3,那么在事务B中申请锁的顺序也要是t1->t2->t3,才能避免此类死锁的形成。

3.2 锁升级形成的死锁

       因为事务中锁升级造成的死锁,比较难以让人察觉。笔者在工作中就遇到过这样的情况,下面我们也模拟下这种死锁的形成:

(1)开启事务A,执行select for update,此时事务A将在t1表上加上ROW SHARE和行排它锁。

stock_analysis_data=# select * from t1 where id=1 for update;
 id | name | age 
----+------+-----
  1 | tom  |    
(1 row)

(2)开启事务B,执行update。由于update语句也要先获取对应行的行锁,所以这个语句会发生阻塞

stock_analysis_data=# update t1 set age=18 where id=1;

(3)回到事务A,此时执行truncate,将行级排它和ROW SHARE升级为ACCESS EXCLUSIVE,此时就会形成了死锁,数据库同样会自动检测和处理:

stock_analysis_data=# truncate table t1;
ERROR:  deadlock detected
DETAIL:  Process 26004 waits for AccessExclusiveLock on relation 19608 of database 16385; blocked by process 31062.
Process 31062 waits for ShareLock on transaction 353261; blocked by process 26004.
HINT:  See server log for query details.

       避免这种由锁升级造成的死锁的方法就是:在一个事务中,如果用到了不同级别的锁,要一开始就申请最高级别的锁。

四、总结

本文主要介绍了Postgresql中的锁的使用和死锁的原理以及预防,现总结如下:

(1)Postgresql中的锁分为表锁和行锁,其中表锁有8种,行锁有2种。

(2)select语句会加ACCESS SHARE锁,会和ALTER\DROP\TRUNCATE等操作使用的ACCESS EXCLUSIVE锁冲突

(3)CREATE INDEX(不使用CONCURRENTLY)操作会使用SHARE锁,不会和SELECT操作冲突,但是会和UPDATE\DELETE\INSERT操作使用的ROW EXCLUSIVE冲突。

(4)CREATE INDEX CONCURRENTLY 操作会使用SHARE UPDATE EXCLUSIVE锁,不会和SELECT以及UPDATE\DELETE\INSERT操作冲突。

(5)SELECT FRO UPDATE|SHARE会在对应的行加上行排它或者行共享,但是由于Postgresql采用多版本更新的模式,对行加上行排它时,实际上该行照样可以被读取。

(6)形成死锁的主要原因和解决方法是:

  • 事务之间互相等待对方锁定的表。解决方法是申请锁定表时不同事务之间的顺序也要一致。
  • 事务之间对锁定了同一个表,但是某个先获取了锁的事务进行了锁升级,造成了死锁。解决方法是同一个事务种要使用不同级别的锁,要先申请最高级别的锁。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐