今天,我们学习下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)形成死锁的主要原因和解决方法是:
- 事务之间互相等待对方锁定的表。解决方法是申请锁定表时不同事务之间的顺序也要一致。
- 事务之间对锁定了同一个表,但是某个先获取了锁的事务进行了锁升级,造成了死锁。解决方法是同一个事务种要使用不同级别的锁,要先申请最高级别的锁。