Whoami:5年+金融、政府、医疗领域工作经验的DBACertificate:OCP、PCPSkill:Oracle、Mysql、PostgreSQLPlatform:CSDN、墨天伦、公众号(呆呆的私房菜)
阅读本文可以了解到什么是数据库锁、锁的类型数据库表锁、死锁、行锁等内容,读者亦可以通过相关的案例来辅助理解PostgreSQL锁机制。
锁是为了在高并发环境下保证数据库数据的一致性,在多个用户同时访问数据库的时候,如果不对并发操作加以控制,就有可能读取和存储不正确的数据,破坏数据库的一致性。
PostgreSQL实现并发控制的基本方法是使用锁来控制临界区互斥访问。后台进程访问磁盘文件时,需要获得锁,获得成功后才能进入临界区执行磁盘读写访问,访问完成后退出临界区并释放锁,否则进程睡眠之道别的后台进程唤醒。
PostgreSQL数据库定义了3种锁,分别是spinlock、lwlock和regularLock。
1. spinlock是最底层的锁,使用互斥信号量实现。特点是封锁时间短,没有等待队列和死锁检测机制,事务结束时不能自动释放。
2. lwlock主要提供共享存储器的互斥访问。lwlock有两种模式,一种排他模式,一种共享模式,它是利用spinlock实现的。特点是有等待队列、没有死锁检测和能自动释放锁。
3. regularlock指的是数据库管理中的锁,由lwlock实现。特点是有等待队列、有死锁检测和能自动释放锁。regularlock实际上又分为表级锁和行级锁。
一、 reguarlock表级锁模式有8种:1. 访问共享锁:内部锁模式,进行查询时在被查询的表上获得;2. 行共享锁:使用了select ... for update和for share命令;3. 行排他锁:使用update \ delete \ insert命令;4. 共享更新排他锁:使用vacuum \ analyze \ create index concurrently命令;5. 共享锁:使用不带concurrently选项的create index语句请求;6. 共享行排他锁:类似于排他锁;7. 排他锁:阻塞行共享和select ... for update;8. 访问排他锁:alter table \ drop table \ truncate \ reindex \ cluster \vacuum full;
排他模式的锁(ShareRowExclusiveLock、ExclusiveLock、AccessExclusiveLock)表示事务执行期间组是其他任何类型锁作用于这张表;
共享模式的锁(非排他模式的锁)表示允许其他用户同时共享此锁,但在事务执行期间阻止排他型锁的使用。
排他模式和共享模式上的锁等可以工作在下列授权级别上:Access表示锁定这个表模式,Rows表示仅锁定单独的元组。

二、 reguarlock行级锁模式有2种:1. 共享锁(读锁)2. 排他锁(写锁)由于多版本的实现,实际读取数据行的时候,并不会在行上执行任何锁;两个事务永远不可能在相同的行上持有冲突的锁;行级锁不影响数据查询,他们只阻塞同一行的写入者和加锁者。
下面我们通过一些案例来了解下PostgreSQL的锁。
# 创建表tt1create table t1 (id int, c1 char(10));# 场景1:事务执行查询时,会获得1个访问共享锁和1个意图读锁postgres=# begin;BEGINpostgres=*# select * from t1;id | c1----+----(0 rows)postgres=*# select locktype, database, relation, virtualtransaction, pid, mode, granted from pg_locks where relation = 't1'::regclass::oid;locktype | database | relation | virtualtransaction | pid | mode | granted----------+----------+----------+--------------------+-----+-----------------+---------relation | 13757 | 16384 | 3/31 | 733 | AccessShareLock | trelation | 13757 | 16384 | 3/31 | 733 | SIReadLock | t(2 rows)postgres=*# commit;COMMIT# 场景2: 事务执行select ... for update的时候,会获得1个行共享锁和意图读锁postgres=# begin;BEGINpostgres=*# select * from t1 for update;id | c1----+----(0 rows)postgres=*# select locktype, database, relation, virtualtransaction, pid, mode, granted from pg_locks where relation = 't1'::regclass::oid;locktype | database | relation | virtualtransaction | pid | mode | granted----------+----------+----------+--------------------+-----+--------------+---------relation | 13757 | 16384 | 3/33 | 733 | RowShareLock | trelation | 13757 | 16384 | 3/33 | 733 | SIReadLock | t(2 rows)postgres=*# rollback;ROLLBACK# 场景3: 事务执行insert\update\delelete时,会获得1个行独占锁postgres=# begin;BEGINpostgres=*# insert into t1 values (1, 't1');INSERT 0 1postgres=*# select locktype, database, relation, virtualtransaction, pid, mode, granted from pg_locks where relation = 't1'::regclass::oid;locktype | database | relation | virtualtransaction | pid | mode | granted----------+----------+----------+--------------------+-----+------------------+---------relation | 13757 | 16384 | 3/34 | 733 | RowExclusiveLock | t(1 row)postgres=*# commit;COMMIT# 场景4: 事务执行vacuum\analyze\create index concurrently时,会获得1个共享更新排他锁postgres=# begin;BEGINpostgres=*# analyze t1;ANALYZEpostgres=*# select locktype, database, relation, virtualtransaction, pid, mode, granted from pg_locks where relation = 't1'::regclass::oid;locktype | database | relation | virtualtransaction | pid | mode | granted----------+----------+----------+--------------------+-----+--------------------------+---------relation | 13757 | 16384 | 3/35 | 733 | ShareUpdateExclusiveLock | t(1 row)postgres=*# commit;COMMIT# 场景5: 事务执行create index时,会获得1个共享锁postgres=# begin;BEGINpostgres=*# create index idx_t1_c1 on t1 (c1);CREATE INDEXpostgres=*# select locktype, database, relation, virtualtransaction, pid, mode, granted from pg_locks where relation = 't1'::regclass::oid;locktype | database | relation | virtualtransaction | pid | mode | granted----------+----------+----------+--------------------+-----+-----------+---------relation | 13757 | 16384 | 3/36 | 733 | ShareLock | t(1 row)# 场景6:会话1执行for update产生排他锁,会话2执行for share产生共享锁,会获得2# 会话1:执行update操作postgres=# begin;BEGINpostgres=*# select * from t1 for update;id | c1----+-----1 | tt1(1 row)# 会话2:执行for share操作,会话hung住postgres=# begin;BEGINpostgres=*# select * from t1 where id = 1 for share;# 会话3:查看数据库锁postgres=# select locktype, database, relation, virtualtransaction, pid, mode, granted from pg_locks where relation = 't1'::regclass::oid;locktype | database | relation | virtualtransaction | pid | mode | granted----------+----------+----------+--------------------+-----+--------------+---------relation | 13757 | 16384 | 3/50 | 733 | RowShareLock | trelation | 13757 | 16384 | 4/271 | 179 | RowShareLock | ttuple | 13757 | 16384 | 4/271 | 179 | RowShareLock | trelation | 13757 | 16384 | 4/271 | 179 | SIReadLock | trelation | 13757 | 16384 | 3/50 | 733 | SIReadLock | t(5 rows)# 场景7:会话1和会话2同时update t1表中的记录# 会话1:postgres=# begin;BEGINpostgres=*# update t1 set c1 = 'tt1' where id = 1;UPDATE 1# 会话2:会话会hung住postgres=# begin;BEGINpostgres=*# update t1 set c1 = 'ttt1' where id = 1;# 会话3:查看锁情况postgres=# select locktype, database, relation, virtualtransaction, pid, mode, granted from pg_locks where relation = 't1'::regclass::oid;locktype | database | relation | virtualtransaction | pid | mode | granted----------+----------+----------+--------------------+-----+------------------+---------relation | 13757 | 16384 | 3/52 | 733 | RowExclusiveLock | trelation | 13757 | 16384 | 4/272 | 179 | RowExclusiveLock | ttuple | 13757 | 16384 | 4/272 | 179 | ExclusiveLock | trelation | 13757 | 16384 | 4/272 | 179 | SIReadLock | trelation | 13757 | 16384 | 3/52 | 733 | SIReadLock | t(5 rows)# 场景8:表t1获得访问排他锁,其他访问t1表的事务都会被阻塞。postgres=# begin;BEGINpostgres=*# alter table t1 add column d1 char(10);ALTER TABLEpostgres=*# select locktype, database, relation, virtualtransaction, pid, mode, granted from pg_locks where relation = 't1'::regclass::oid;locktype | database | relation | virtualtransaction | pid | mode | granted----------+----------+----------+--------------------+-----+---------------------+---------relation | 13757 | 16384 | 3/56 | 733 | AccessExclusiveLock | t(1 row)
postgres=# \h lockCommand: LOCKDescription: lock a tableSyntax:LOCK [ TABLE ] [ ONLY ] name [ * ] [, ...] [ IN lockmode MODE ] [ NOWAIT ]where lockmode is one of:ACCESS SHARE | ROW SHARE | ROW EXCLUSIVE | SHARE UPDATE EXCLUSIVE| SHARE | SHARE ROW EXCLUSIVE | EXCLUSIVE | ACCESS EXCLUSIVEURL: https://www.postgresql.org/docs/14/sql-lock.html注意:1. name 表示要锁定的现有表的锁名称(可选模式限定),如果在表名之前指定 only,则仅该表被锁定,如果未指定only,则表及所有后代表(如果有)被锁定;2. lock_mode 表示指定此锁与之冲突的锁,如果未指定锁模式,则使用最严格的access exclusive。3. nowait 表示lock table不等待任何锁冲突被释放。如果被指定的锁不能立即获得,那么事务就会终止。
postgres=# \d pg_locks;View "pg_catalog.pg_locks"Column | Type | Collation | Nullable | Default--------------------+--------------------------+-----------+----------+---------locktype | text | | |database | oid | | |relation | oid | | |page | integer | | |tuple | smallint | | |virtualxid | text | | |transactionid | xid | | |classid | oid | | |objid | oid | | |objsubid | smallint | | |virtualtransaction | text | | |pid | integer | | |mode | text | | |granted | boolean | | |fastpath | boolean | | |waitstart | timestamp with time zone | | |## 查询所有正在等待锁的会话以及持有这些锁的会话SELECTpg_stat_activity.pid,pg_stat_activity.query,pg_locks.locktype,pg_locks.mode,pg_locks.relation::regclass,pg_locks.transactionid,pg_locks.virtualxid,pg_locks.virtualtransaction,pg_locks.grantedFROM pg_stat_activityJOIN pg_locks ON pg_stat_activity.pid = pg_locks.pidWHERE pg_locks.granted = false;## 查看数据库锁源WITH sos AS (SELECT array_cat(array_agg(pid),array_agg((pg_blocking_pids(pid))[array_length(pg_blocking_pids(pid),1)])) pidsFROM pg_locksWHERE NOT granted)SELECT a.pid, a.usename, a.datname, a.state,a.wait_event_type || ': ' || a.wait_event AS wait_event,current_timestamp-a.state_change time_in_state,current_timestamp-a.xact_start time_in_xact,l.relation::regclass relname,l.locktype, l.mode, l.page, l.tuple,pg_blocking_pids(l.pid) blocking_pids,(pg_blocking_pids(l.pid))[array_length(pg_blocking_pids(l.pid),1)] last_session,coalesce((pg_blocking_pids(l.pid))[1]||'.'||coalesce(case when locktype='transactionid' then 1 else array_length(pg_blocking_pids(l.pid),1)+1 end,0),a.pid||'.0') lock_depth,a.queryFROM pg_stat_activity aJOIN sos s on (a.pid = any(s.pids))LEFT OUTER JOIN pg_locks l on (a.pid = l.pid and not l.granted)ORDER BY lock_depth;
死锁是两个或两个以上的事务在执行过程中相互持有对象期待的锁、导致事务都将无法进行。 PostgreSQL能够自动检测死锁,会退出其中一个事务,从而允许其他事务完成。 死锁发生的必要条件:
1. 互斥条件; 2. 请求保持条件; 3. 不剥夺条件 4. 环路等待条件。
# 1. 创建表a和表bpostgres=# create table a (id int);CREATE TABLEpostgres=# create table b (id int);CREATE TABLE# 2. 开启会话1:postgres=# begin;BEGINpostgres=*# lock table a in exclusive mode;LOCK TABLEpostgres=*# select pg_sleep(2);pg_sleep----------(1 row)# 3. 开启会话2:postgres=# begin;BEGINpostgres=*# lock table b in exclusive mode;LOCK TABLEpostgres=*# select pg_sleep(2);pg_sleep----------(1 row)# 4. 会话1申请表b的独占排他锁:postgres=*# lock table b in exclusive mode;# 此时发现语句执行失败,事务hung住。# 5. 我们可以通过如下语句查看数据库中的锁,我们可以看到此时会话1被会话2阻塞了postgres=# WITH sos AS (SELECT array_cat(array_agg(pid),array_agg((pg_blocking_pids(pid))[array_length(pg_blocking_pids(pid),1)])) pidsFROM pg_locksWHERE NOT granted)SELECT a.pid, a.usename, a.datname, a.state,a.wait_event_type || ': ' || a.wait_event AS wait_event,current_timestamp-a.state_change time_in_state,current_timestamp-a.xact_start time_in_xact,l.relation::regclass relname,l.locktype, l.mode, l.page, l.tuple,pg_blocking_pids(l.pid) blocking_pids,(pg_blocking_pids(l.pid))[array_length(pg_blocking_pids(l.pid),1)] last_session,coalesce((pg_blocking_pids(l.pid))[1]||'.'||coalesce(case when locktype='transactionid' then 1 else array_length(pg_blocking_pids(l.pid),1)+1 end,0),a.pid||'.0') lock_depth,a.queryFROM pg_stat_activity aJOIN sos s on (a.pid = any(s.pids))LEFT OUTER JOIN pg_locks l on (a.pid = l.pid and not l.granted)ORDER BY lock_depth;pid | usename | datname | state | wait_event | time_in_state | time_in_xact | relname | locktype | mode | page | tuple | blocking_pids | last_session | lock_depth | query-----+----------+----------+---------------------+--------------------+-----------------+-----------------+---------+----------+---------------+------+-------+---------------+--------------+------------+---------------------------------179 | postgres | postgres | idle in transaction | Client: ClientRead | 00:18:36.49402 | 00:18:52.443567 | | | | | | | | 179.0 | select pg_sleep(2);177 | postgres | postgres | active | Lock: relation | 00:18:23.443331 | 00:19:05.027391 | b | relation | ExclusiveLock | | | {179} | 179 | 179.2 | lock table b in exclusive mode;(2 rows)# 6. 这里我们制造死锁条件,让会话2申请表a的独占排他锁,postgres=*# lock table a in exclusive mode;ERROR: deadlock detectedDETAIL: Process 179 waits for ExclusiveLock on relation 57643 of database 13757; blocked by process 177.Process 177 waits for ExclusiveLock on relation 57646 of database 13757; blocked by process 179.HINT: See server log for query details.# 结论:PostgreSQL会自动进行死锁检测,出现死锁后,会话2的事务被abort掉,会话1成功获得表b的独占排他锁。
针对死锁问题,我们有什么好的方式去预防呢?
1. 设置限制条件,去破坏死锁的四个必要条件中的一个或多个,来预防发生死锁;
2. 数据库可以自动检测死锁,根据超时参数设定选择在何时回滚一个事务,解除死锁;
3. 开发测试环境中,需要关闭死锁超时,发现死锁,优化程序逻辑,从而避免死锁。
本文内容就到这啦,阅读完本篇,相信你也对PostgreSQL的锁机制有了更深的理解了吧!我们下篇再见!





