In the context of transaction processing, Isolation plays a significant role.
Isolation is the property that controls how and when changes are made and when they must be visible to each other, users, and systems.
PostgreSQL achieves Isolation by multi-version concurrency control architecture.
Multi-version concurrency control is a technique to allow multiple sessions to access the same record concurrently, i.e., while session A is updating a record, session B can still access the record.
But,
- what if both session A and session B wanted to update the same record at the same time?
- What happens if you try to truncate a table from session B while session A is accessing it?
- What happens if you decide to vacuum the table if there is an ongoing transaction going on in another session?
Here comes the concept of locking.
The following picture is a summary of what we are going to learn in this humongous post. 6000+ words are waiting for you, grab a cup of coffee and lockdown yourself with PostgreSQL Locks.
Table of Contents
PostgreSQL Locks
PostgreSQL Locks is one of the critical topics of PostgreSQL, especially for developers who code with databases.
PostgreSQL Locks help us with concurrent access or modifications of the database objects by issuing a lock as soon as the command is executed.
The lock type is dependent on the type of command executed.
PostgreSQL supports three mechanisms of locking
- Table Level Locks
- Row Level Locks
- Advisory Locks
Table level and Row-level locks can be explicit or implicit, whereas Advisory locks are explicit.
- Implicit locks mean the locks would go off by default when the transaction ends.
- Explicit locks once acquired may be held until explicitly released. We can acquire locks explicitly with the
WITH LOCK
statement.
PostgreSQL Locks: Table Level Locks
Table-level locks are acquired by built-in SQL commands(implicitly); also, they can be acquired explicitly with LOCK command. Available table-level locks in PostgreSQL are:
- ACCESS SHARE
- ROW SHARE
- ROW EXCLUSIVE
- SHARE UPDATE EXCLUSIVE
- SHARE
- SHARE ROW EXCLUSIVE
- EXCLUSIVE
- ACCESS EXCLUSIVE
Throughout the post, we will try to understand each locking mechanism with explicit locking and implicit locking.
PostgreSQL Access Share Lock
PostgreSQL Access Share Lock is acquired by queries that only read from a table but do not modify it. Typically, this is a select query.
Implicit locking example:
- select something from session 1 which acquires Access Share Lock
1 2 3 4 |
36:54 (SESSION 1)#begin; BEGIN 37:05 (SESSION 1)#select *, pg_sleep(300) from acl; ... |
2. Try to truncate the table from session 2
1 2 3 4 |
37:02 (SESSION 2)#begin; BEGIN 37:33 (SESSION 2)#truncate table acl; .. |
Session 1 acquired AccessShareLock to fetch the records.
To truncate a table my session 2 has to acquire a lock with a mode AccessExclusiveLock but is waiting to acquire it because of lock conflict.
ACCESS SHARE LOCK conflicts with the ACCESS EXCLUSIVE
lock mode.[you will understand more about lock conflicts later in this post, don’t worry].
When I checked for the locks, I got two locks. You can see AccessShareLock will granted as true and AccessExclusiveLock with granted as false.
1 2 3 4 5 6 7 |
postgres=# postgres=# \! psql -f implicit.sql pid | vxid | lock_type | lock_mode | granted | xid_lock | relname ------+------+-----------+---------------------+---------+----------+--------- 7244 | 6/9 | relation | AccessShareLock | t | | acl 7364 | 3/12 | relation | AccessExclusiveLock | f | | acl (2 rows) |
To find the locked and locker PID’s
1 2 3 4 5 |
postgres=# \! psql -f tablelock.sql locked_pid | locker_pid | locked_user | locker_user | virtualtransaction | relname ------------+------------+-------------+-------------+--------------------+--------------- 7364 | 7244 | postgres | postgres | 3/12 | acl (1 row) |
Explicit locking example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
postgres=# begin; BEGIN postgres=# LOCK TABLE emp IN ACCESS SHARE MODE; LOCK TABLE postgres=# \! psql -f implicit.sql pid | vxid | lock_type | lock_mode | granted | relname ------+------+-----------+-----------------+---------+------------------------------------ 6020 | 4/8 | relation | AccessShareLock | t | emp 6025 | 3/27 | relation | AccessShareLock | t | pg_class 6025 | 3/27 | relation | AccessShareLock | t | pg_class_oid_index 6025 | 3/27 | relation | AccessShareLock | t | pg_class_relname_nsp_index 6025 | 3/27 | relation | AccessShareLock | t | pg_class_tblspc_relfilenode_index 6025 | 3/27 | relation | AccessShareLock | t | pg_locks (6 rows) postgres=# |
PostgreSQL Row Share Lock
The SELECT FOR UPDATE
and SELECT FOR SHARE
commands acquire a lock of this mode on the target table(s)
Conflicts with the EXCLUSIVE
and ACCESS EXCLUSIVE
lock modes.
As SELECT FOR UPDATE
and SELECT FOR SHARE
are two of the four available row-level lock modes, we will discuss more Row Share Lock later in this post.
PostgreSQL Row Exclusive Lock
The commands UPDATE
, DELETE
, and INSERT
acquire this lock mode on the target table.
Conflicts with the SHARE
, SHARE ROW EXCLUSIVE
, EXCLUSIVE
, and ACCESS EXCLUSIVE
lock modes.[detailed the discussion later in this post]
Implicit locking example:
Example.,
1 2 3 4 5 6 7 8 9 10 11 12 13 |
30:42 (SESSION 1)#begin; BEGIN 30:45 (SESSION 1)#select * from acl; id | sno | name | sal | dept ----+-----+------+-----+------- 1 | 1 | A | 200 | IT 2 | 2 | B | 200 | IT 3 | 3 | C | 300 | SALES (3 rows) 31:07 (SESSION 1)#insert into acl values(4,4,'D',400,'IT'); INSERT 0 1 31:39 (SESSION 1)# |
Now, this session 1 acquired a Row Exclusive lock.
From session 2, try to alter the table.
1 2 3 |
31:54 (SESSION 2)#alter table acl drop dept; ... |
Session 2 will be waiting for session 1 to release the lock, because alter table drop column needs access exclusive which conflicts with row exclusive.
1 2 3 4 5 6 7 |
postgres=# \! psql -f implicit.sql pid | vxid | lock_type | lock_mode | granted | xid_lock | relname ------+--------+-----------+---------------------+---------+----------+--------- 8038 | 4/1364 | relation | AccessShareLock | t | | acl 8038 | 4/1364 | relation | RowExclusiveLock | t | | acl 8058 | 3/14 | relation | AccessExclusiveLock | f | | acl (3 rows) |
executing the pg_stat_activity to find the PID,
1 2 3 4 5 6 7 |
postgres=# select pid, wait_event_type, wait_event, query from pg_stat_activity; -[ RECORD 3 ]---+--------------------------------------------------------------- pid | 8058 wait_event_type | Lock wait_event | relation query | alter table acl drop dept; |
Now, with PID pg_locks output is
1 2 3 4 5 6 7 8 |
postgres=# select locktype, relation, virtualxid, transactionid, mode, granted from pg_locks where pid='8058'; locktype | relation | virtualxid | transactionid | mode | granted ---------------+----------+------------+---------------+---------------------+--------- virtualxid | | 3/14 | | ExclusiveLock | t transactionid | | | 671 | ExclusiveLock | t relation | 32803 | | | AccessExclusiveLock | f (3 rows) |
We will understand more about virtualxid and transactionid later in a separate post.
Explicit locking example:
1 2 3 4 5 6 7 8 9 |
23:06:48 (SESSION 1)#begin; BEGIN 23:06:50 (SESSION 1)#lock table acl IN ROW EXCLUSIVE MODE; LOCK TABLE 23:07:11 (SESSION 1)#\! psql -f /home/postgres/queries/implicit.sql pid | vxid | lock_type | lock_mode | granted | xid_lock | relname ------+--------+-----------+------------------+---------+----------+--------- 8509 | 4/1394 | relation | RowExclusiveLock | t | | acl (1 row) |
PostgreSQL Share Update Exclusive
Acquired by VACUUM
(without FULL
), ANALYZE
, CREATE INDEX CONCURRENTLY
, CREATE STATISTICS
and ALTER TABLE VALIDATE
and other few other ALTER TABLE commands.
Conflicts with the SHARE UPDATE EXCLUSIVE
, SHARE
, SHARE ROW EXCLUSIVE
, EXCLUSIVE
, and ACCESS EXCLUSIVE
lock modes. This mode protects a table against concurrent schema changes and VACUUM
runs.
Implicit locking example:
Example,
1 2 3 |
23:20:07 (SESSION 1)#vacuum pgbench_accounts; .. .. |
Acquired lock is
1 2 3 4 5 6 7 8 9 |
postgres=# SELECT locktype,transactionid,virtualtransaction,mode FROM pg_locks ; locktype | transactionid | virtualtransaction | mode ------------+---------------+--------------------+-------------------------- relation | | 3/180 | RowExclusiveLock virtualxid | | 3/180 | ExclusiveLock relation | | 5/379 | AccessShareLock virtualxid | | 5/379 | ExclusiveLock relation | | 3/180 | ShareUpdateExclusiveLock (5 rows) |
You can uselock_timeout
to avoid waiting for the lock.
lock_timeoutWill
willt for the specified time to get access to the object, if the timeout occurs the session waiting for lock disconnects.
Example
SET lock_timeout TO '2s'
Explicit locking example:
1 2 3 4 5 |
23:22:32 (SESSION 1)#begin; BEGIN 23:25:36 (SESSION 1)#LOCK TABLE acl IN SHARE UPDATE EXCLUSIVE MODE ; LOCK TABLE 23:26:01 (SESSION 1)# |
and the lock is
1 2 3 4 5 |
postgres=# \! psql -f implicit.sql pid | vxid | lock_type | lock_mode | granted | xid_lock | relname ------+-------+-----------+--------------------------+---------+----------+--------- 8719 | 3/185 | relation | ShareUpdateExclusiveLock | t | | acl (1 row) |
PostgreSQL Share Lock
Acquired by CREATE INDEX
(without CONCURRENTLY
).
The non-concurrent version of CREATE INDEX prevents table updates, e.g., DROP TABLE or INSERT or DELETE, with ShareLock.
Conflicts with the ROW EXCLUSIVE
, SHARE UPDATE EXCLUSIVE
, SHARE ROW EXCLUSIVE
, EXCLUSIVE
, and ACCESS EXCLUSIVE
lock modes. This mode protects a table against concurrent data changes.
Implicit locking example:
1 2 3 |
23:28:28 (SESSION 1)#create index abalance_ind on pgbench_accounts(balance); ... ... |
and the lock is
1 2 3 4 5 6 7 8 9 10 |
postgres=# SELECT locktype,transactionid,virtualtransaction,mode FROM pg_locks ; locktype | transactionid | virtualtransaction | mode ---------------+---------------+--------------------+--------------------- virtualxid | | 3/196 | ExclusiveLock relation | | 5/382 | AccessShareLock virtualxid | | 5/382 | ExclusiveLock relation | | 3/196 | ShareLock transactionid | 737 | 3/196 | ExclusiveLock relation | | 3/196 | AccessExclusiveLock (6 rows) |
Explicit locking example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
23:28:55 (SESSION 1)#begin; BEGIN 23:32:42 (SESSION 1)#SELECT * FROM acl FOR SHARE; id | sno | name | sal | dept ----+-----+------+-----+------- 1 | 1 | A | 200 | IT 2 | 2 | B | 200 | IT 3 | 3 | C | 300 | SALES 4 | 4 | D | 400 | IT (4 rows) 23:32:49 (SESSION 1)# postgres=# \! psql -f implicit.sql pid | vxid | lock_type | lock_mode | granted | xid_lock | relname ------+-------+-----------+--------------+---------+----------+--------- 8719 | 3/197 | relation | RowShareLock | t | | acl (1 row) |
PostgreSQL Share Row Exclusive
Acquired by CREATE COLLATION
, CREATE TRIGGER
, and many forms of ALTER TABLE
Conflicts with the ROW EXCLUSIVE
, SHARE UPDATE EXCLUSIVE
, SHARE
, SHARE ROW EXCLUSIVE
, EXCLUSIVE
, and ACCESS EXCLUSIVE
lock modes. This mode protects a table against concurrent data changes and is self-exclusive so that only one session can hold it at a time.
Explicit locking example:
1 2 3 4 5 6 7 8 9 10 11 |
23:40:43 (SESSION 1)#BEGIN ; BEGIN 23:40:50 (SESSION 1)#LOCK TABLE acl IN SHARE ROW EXCLUSIVE MODE; LOCK TABLE 23:40:57 (SESSION 1)# postgres=# \! psql -f implicit.sql pid | vxid | lock_type | lock_mode | granted | xid_lock | relname ------+-------+-----------+-----------------------+---------+----------+--------- 8719 | 3/198 | relation | ShareRowExclusiveLock | t | | acl (1 row) |
PostgreSQL Exclusive Lock
Acquired by REFRESH MATERIALIZED VIEW CONCURRENTLY
.
Conflicts with the ROW SHARE
, ROW EXCLUSIVE
, SHARE UPDATE EXCLUSIVE
, SHARE
, SHARE ROW EXCLUSIVE
, EXCLUSIVE
, and ACCESS EXCLUSIVE
lock modes. This mode allows only concurrent ACCESS SHARE
locks, i.e., only reads from the table, can proceed in parallel with a transaction holding this lock mode.
Implicit locking example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
postgres=# REFRESH MATERIALIZED VIEW CONCURRENTLY pgbench_accounts_mv WITH DATA; .. .. postgres=# SELECT locktype,transactionid,virtualtransaction,mode FROM pg_locks ; locktype | transactionid | virtualtransaction | mode ---------------+---------------+--------------------+--------------------- relation | | 4/18 | AccessShareLock relation | | 4/18 | AccessShareLock virtualxid | | 4/18 | ExclusiveLock relation | | 3/10 | AccessShareLock virtualxid | | 3/10 | ExclusiveLock relation | | 4/18 | AccessExclusiveLock transactionid | 747 | 4/18 | ExclusiveLock relation | | 4/18 | AccessShareLock relation | | 4/18 | ExclusiveLock (9 rows) |
Explicit locking example
1 2 3 4 5 6 7 8 9 10 11 12 |
postgres=# begin; BEGIN postgres=# LOCK TABLE acl IN EXCLUSIVE MODE; LOCK TABLE postgres=# postgres=# \! psql -f implicit.sql pid | vxid | lock_type | lock_mode | granted | xid_lock | relname ------+------+-----------+---------------+---------+----------+--------- 2611 | 4/20 | relation | ExclusiveLock | t | | acl (1 row) postgres=# |
PostgreSQL Access Exclusive Locking
Acquired by the DROP TABLE
, TRUNCATE
, REINDEX
, CLUSTER
, VACUUM FULL
, and REFRESH MATERIALIZED VIEW
(without CONCURRENTLY
) commands. Many forms of ALTER TABLE
also, acquire a lock at this level. This is also the default lock mode for LOCK TABLE
statements that do not specify a mode explicitly
Conflicts with locks of all modes (ACCESS SHARE
, ROW SHARE
, ROW EXCLUSIVE
, SHARE UPDATE EXCLUSIVE
, SHARE
, SHARE ROW EXCLUSIVE
, EXCLUSIVE
, and ACCESS EXCLUSIVE
). This mode guarantees that the holder is the only transaction accessing the table in any way.
Implicit locking example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
postgres=# vacuum full pgbench_accounts; .. .. postgres=# SELECT locktype,transactionid,virtualtransaction,mode FROM pg_locks ; locktype | transactionid | virtualtransaction | mode ---------------+---------------+--------------------+--------------------- virtualxid | | 4/22 | ExclusiveLock relation | | 3/12 | AccessShareLock virtualxid | | 3/12 | ExclusiveLock relation | | 4/22 | AccessExclusiveLock transactionid | 749 | 4/22 | ExclusiveLock relation | | 4/22 | AccessExclusiveLock (6 rows) postgres=# |
Explicit locking example:
1 2 3 4 5 6 7 8 9 10 |
postgres=# begin; BEGIN postgres=# LOCK TABLE acl IN ACCESS EXCLUSIVE MODE; LOCK TABLE postgres=# \! psql -f implicit.sql pid | vxid | lock_type | lock_mode | granted | xid_lock | relname ------+------+-----------+---------------------+---------+----------+--------- 2611 | 4/19 | relation | AccessExclusiveLock | t | | acl (1 row) |
The following diagram depicts the lock modes’ conflicts.
conclusions can be drawn from the above picture are
- Two transactions cannot hold locks of conflicting modes on the same table at the same time.
Example., If there is an ongoing access share lock, then another session can’t acquire access exclusive.
- Non-conflicting lock modes can be held concurrently by many transactions.
Example., as per the above diagram Row Share lock is non conflicting with Row Exclusive lock so that they can be held at a time by multiple transactions/sessions.
- Some lock modes are self-conflicting
Example, an ACCESS EXCLUSIVE
lock cannot be held by more than one transaction at a time
- While some lock modes, not self-conflicting
Example, an ACCESS SHARE
lock can be held by multiple transactions.
PostgreSQL Locks: Row Level Locks
Row level locks in PostgreSQL are classified into four types
- FOR UPDATE
- FOR NO KEY UPDATE
- FOR SHARE
- FOR KEY SHARE
PostgreSQL for update lock
There are certain situations where your PostgreSQL has to obtain a lock to process the data correctly.
For example.,
1 2 3 4 5 |
select * from my_table where my_condition; --do some other stuff update my_table set my_column = my_column where my_condition; |
Here, if my requirement is to fetch the data with my_condition and do some stuff and update the table for the same condition.
Here, as select is an access share lock (as we learned above), there are chances that the other sessions can modify the same records while my session is processing the selects.
To avoid this situation, we use select for update clause
A select ... for update
will (as it implies) select certain rows but also lock them as if they have already been updated by the current transaction (or as if the identity update had been performed). This allows you to update them again in the current transaction and then commit, without another transaction being able to modify these rows in any way.
select * from my_table where my_condition;
1 |
select * from my_table where my_condition for update; |
Since the rows affected by my_condition
are locked, no other transaction can modify them in any way, and hence, transaction isolation level makes no difference here.
Select for update blocks the following actions by other sessions
- UPDATE,
- DELETE,
- SELECT FOR UPDATE,
- SELECT FOR NO KEY UPDATE,
- SELECT FOR SHARE or
- SELECT FOR KEY SHARE
PostgreSQL For No Key Update Lock
The select for no key updates
behaves similarly to the select for update
locking clause but it does not block the SELECT FOR KEY SHARE
. It is ideal if you are performing processing on the rows but don’t want to block the creation of child records.
PostgreSQL For Share Lock
Behaves similarly to FOR NO KEY UPDATE
, except that it acquires a shared lock rather than exclusive lock on each retrieved row.
A shared lock blocks other transactions from performing UPDATE
, DELETE
, SELECT FOR UPDATE
or SELECT FOR NO KEY UPDATE
on these rows, but it does not prevent them from performing SELECT FOR SHARE
or SELECT FOR KEY SHARE
.
select ... for update nowait
The statement helps us to get to know that the resource is blocked by some other session.
The Effect of Select For Update on Foreign Keys
Consider below tables
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
postgres=# \d emp Table "public.emp" Column | Type | Collation | Nullable | Default --------+------------------------+-----------+----------+--------- id | integer | | not null | sno | integer | | | name | character varying(255) | | | sal | character varying(100) | | | dept | character varying(10) | | | Indexes: "emp_pkey" PRIMARY KEY, btree (id) Foreign-key constraints: "emp_fkey" FOREIGN KEY (dept) REFERENCES dept(name) postgres=# \d dept Table "public.dept" Column | Type | Collation | Nullable | Default -------------+-----------------------+-----------+----------+--------- name | character varying(10) | | not null | address | character varying(10) | | | dept_status | character varying(10) | | | Indexes: "dept_pkey" PRIMARY KEY, btree (name) Referenced by: TABLE "emp" CONSTRAINT "emp_fkey" FOREIGN KEY (dept) REFERENCES dept(name) postgres=# |
Select for update has an evil impact of all the child tables or foreign key tables if not handled correctly. For example,
When selecting data from the emp table with select for update
, dept will be locked as well. This is necessary because otherwise there is a chance of breaking the foreign-key constraint.
1 2 3 4 5 6 7 8 9 10 11 |
53:24 (SESSION 1)#begin; BEGIN 18:54:10 (SESSION 1)#select * from emp for update; id | sno | name | sal | dept ----+-----+------+-----+------- 1 | 1 | A | 200 | IT 2 | 2 | B | 200 | IT 3 | 3 | C | 300 | SALES (3 rows) 18:54:11 (SESSION 1)# |
While on session 2
1 2 3 4 5 6 |
18:54:18 (SESSION 2)#begin; BEGIN 18:54:23 (SESSION 2)#update dept set name='NIT' where name='IT'; ... ... |
Lock query output
1 2 3 4 5 6 7 8 9 10 11 12 |
postgres=# \! psql -f implicit.sql pid | vxid | lock_type | lock_mode | granted | xid_lock | relname ------+-------+-----------+------------------+---------+----------+----------- 2700 | 4/31 | relation | AccessShareLock | t | | emp_pkey 2700 | 4/31 | relation | RowShareLock | t | | emp 3021 | 3/641 | relation | AccessShareLock | t | | dept_pkey 3021 | 3/641 | relation | AccessShareLock | t | | emp_pkey 3021 | 3/641 | relation | RowExclusiveLock | t | | dept 3021 | 3/641 | relation | RowExclusiveLock | t | | dept_pkey 3021 | 3/641 | relation | RowShareLock | t | | dept 3021 | 3/641 | relation | RowShareLock | t | | emp 3021 | 3/641 | tuple | AccessShareLock | t | | emp (9 rows) |
We can avoid this situation with For Share Lock
session 1 with For Share Lock
1 2 3 4 5 6 7 8 9 10 11 |
58:27 (SESSION 1)#begin; BEGIN 18:58:36 (SESSION 1)#select * from emp for share; id | sno | name | sal | dept ----+-----+------+-----+------- 1 | 1 | A | 200 | IT 2 | 2 | B | 200 | IT 3 | 3 | C | 300 | SALES (3 rows) 18:58:41 (SESSION 1)# |
Session 2 with the same query
1 2 3 4 5 |
18:59:00 (SESSION 2)#update dept set name='NIT' where name='IT'; ERROR: update or delete on table "dept" violates foreign key constraint "emp_fkey" on table "emp" DETAIL: Key (name)=(IT) is still referenced from table "emp". 18:59:05 (SESSION 2)# |
Lock query result
1 2 3 4 5 6 |
postgres=# \! psql -f implicit.sql pid | vxid | lock_type | lock_mode | granted | xid_lock | relname ------+------+-----------+-----------------+---------+----------+---------- 2700 | 4/32 | relation | AccessShareLock | t | | emp_pkey 2700 | 4/32 | relation | RowShareLock | t | | emp (2 rows) |
PostgreSQL For key share
Behaves similarly to FOR SHARE
, except that the lock is weaker: SELECT FOR UPDATE
is blocked, but not SELECT FOR NO KEY UPDATE
. A key-shared lock blocks other transactions from performing DELETE
or any UPDATE
that changes the key values, but not other UPDATE
, and neither does it prevent SELECT FOR NO KEY UPDATE
, SELECT FOR SHARE
, or SELECT FOR KEY SHARE
.
Conflicting Row Level Locks
Requested LockMode | 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 |
Now that we have completed understanding locks at the table level and row level.
The last type of locking mechanism is advisory locks.
PostgreSQL Locks: Advisory Locks
The default locks that we discussed so far are sufficiently enough for your application. But there are certain situations where you need an additional layer of locking mechanism from the application end.
PostgreSQL advisory locks solve such application related problems.
PostgreSQL advisory locks can be usable for the following scenarios:
- When your application needs to talk to multiple services when making an API call, for example, microservices architecture.
- we want to calculate and send a report to some of our users, but we must guarantee that background workers don’t start the calculation concurrently
- a multi-node task scheduler can use advisory locks to coordinate task distribution to workers or in PostgreSQL sharding.
The below code snippet helps you understand how explicit advisory lock is made on PostgreSQL through JAVA
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package org.killbill.commons.locker.postgresql; import org.killbill.commons.locker.GlobalLockDao; // Note: the lock is connection specific (closing the connection releases the lock) public class PostgreSQLGlobalLockDao implements GlobalLockDao { @Override public boolean lock(final Connection connection, final String lockName, final long timeout, final TimeUnit timeUnit) throws SQLException { final String sql = String.format("SELECT pg_try_advisory_lock(%s);", lockName); return executeLockQuery(connection, sql); } |
The following table depicts the advisory locks
Each lock type can be 32 bit or 64 bit.
32 bit functions:
Lock Mode | 32 Bit |
Acquire | pg_advisory_lock(key bigint) |
Release | pg_advisory_unlock(key bigint) |
Try | pg_try_advisory_lock(key bigint) |
Acquire | pg_advisory_lock_shared(key bigint) |
Release | pg_advisory_unlock_shared(key bigint) |
Try | pg_try_advisory_lock_shared(key bigint) |
Acquire | pg_advisory_xact_lock(key bigint) |
Release | lock is automatically released at the end of the current transaction |
Try | pg_try_advisory_xact_lock(key bigint) |
Acquire | pg_advisory_xact_lock_shared(key bigint) |
Release | lock is automatically released at the end of the current transaction |
Try | pg_try_advisory_xact_lock_shared(key bigint) |
64 Bit functions
Lock Mode | 64 Bit |
Acquire | pg_advisory_lock(key1 int, key2 int) |
Release | pg_advisory_unlock(key1 int, key2 int) |
Try | pg_try_advisory_lock(key1 int, key2 int) |
Acquire | pg_advisory_lock_shared(key1 int, key2 int) |
Release | pg_advisory_unlock_shared(key1 int, key2 int) |
Try | pg_try_advisory_lock_shared(key1 int, key2 int) |
Acquire | pg_advisory_xact_lock(key1 int, key2 int) |
Release | lock is automatically released at the end of the current transaction |
Try | pg_try_advisory_xact_lock(key1 int, key2 int) |
Acquire | pg_advisory_xact_lock_shared(key1 int, key2 int) |
Release | lock is automatically released at the end of the current transaction |
Try | pg_try_advisory_xact_lock_shared(key1 int, key2 int) |
How can I use these advisory locks?
Before we understand how to use these locks, let’s learn some concepts about them.
- Every lock is associated with an identifier, which can be a 32-bits integer or a 64-bits bigint.
- The Session-level locks are not bound to any database transaction and, once acquired, they need to be explicitly released by the application developer.
- Transaction-level advisory locks are bound to the currently executing transaction, and so the locks are released when the transaction ends, either with a commit or a rollback.
- An exclusive advisory lock will block any exclusive or shared advisory lock on the same lock key.
- A shared advisory lock will block any exclusive advisory lock on the same lock key while still allowing other shared advisory locks to be acquired for the same lock key.
- The
try_
variants return right away, and you can use theboolean
result value to verify if the lock has been successfully acquired. - If the same resource is locked three times, it must then be unlocked three times to be released for other sessions’ use.
Examples
Every lock is associated with an identifier, which can be a 32-bits integer or a 64-bits.
For a 64 bit value, you can pass one argument to function pg_advisory_lock(key)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
22:11:21 (SESSION 1)#select pg_advisory_lock(1); pg_advisory_lock ------------------ (1 row) 22:11:31 (SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory'; locktype | classid | objid | pid | mode | granted ----------+---------+-------+------+---------------+--------- advisory | 0 | 1 | 5758 | ExclusiveLock | t (1 row) 22:11:50 (SESSION 1)#select pg_advisory_unlock(1); pg_advisory_unlock -------------------- t (1 row) |
For a 32 bit value, you can pass two arguments to function pg_advisory_lock(key, key)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
22:11:57 (SESSION 1)#select pg_advisory_lock(1,3); pg_advisory_lock ------------------ (1 row) 22:12:06 (SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory'; locktype | classid | objid | pid | mode | granted ----------+---------+-------+------+---------------+--------- advisory | 1 | 3 | 5758 | ExclusiveLock | t (1 row) 22:12:13 (SESSION 1)#select pg_advisory_unlock(1,3); pg_advisory_unlock -------------------- t (1 row) 22:21:02 (SESSION 1)# |
The Session-level locks are not bound to any database transaction and, once acquired, they need to be explicitly released by the application developer.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
22:22:20 (SESSION 1)#begin; BEGIN 22:22:24 (SESSION 1)#select pg_advisory_lock(1,3); pg_advisory_lock ------------------ (1 row) 22:22:28 (SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory'; locktype | classid | objid | pid | mode | granted ----------+---------+-------+------+---------------+--------- advisory | 1 | 3 | 5911 | ExclusiveLock | t (1 row) 22:22:32 (SESSION 1)#commit; COMMIT 22:22:35 (SESSION 1)#end; WARNING: there is no transaction in progress COMMIT 22:22:38 (SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory'; locktype | classid | objid | pid | mode | granted ----------+---------+-------+------+---------------+--------- advisory | 1 | 3 | 5911 | ExclusiveLock | t (1 row) 22:22:42 (SESSION 1)# |
The lock is still there even I committed the transaction.
Transaction-level advisory locks are bound to the currently executing transaction, and so the locks are released when the transaction ends
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
22:24:52 (SESSION 1)#begin; BEGIN 22:24:55 (SESSION 1)#select pg_advisory_xact_lock(1); pg_advisory_xact_lock ----------------------- (1 row) 22:24:58 (SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory'; locktype | classid | objid | pid | mode | granted ----------+---------+-------+------+---------------+--------- advisory | 0 | 1 | 5911 | ExclusiveLock | t (1 row) 22:25:02 (SESSION 1)#commit; COMMIT 22:25:06 (SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory'; locktype | classid | objid | pid | mode | granted ----------+---------+-------+-----+------+--------- (0 rows) |
An exclusive advisory lock will block any exclusive or shared advisory lock on the same lock key.
Session 1: Acquired exclusive lock with lock key 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
22:27:04 (SESSION 1)#begin; BEGIN 22:27:05 (SESSION 1)#select pg_advisory_lock(1), * from emp; pg_advisory_lock | id | sno | name | sal | dept ------------------+----+-----+------+-----+------- | 2 | 2 | B | 200 | IT | 3 | 3 | C | 300 | SALES | 1 | 1 | A | 300 | IT (3 rows) 22:27:26 (SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory'; locktype | classid | objid | pid | mode | granted ----------+---------+-------+------+---------------+--------- advisory | 0 | 1 | 5911 | ExclusiveLock | t (1 row) 22:27:31 (SESSION 1)# |
Session 2: Try to acquire the exclusive lock with lock key 1
1 2 3 4 5 6 |
22:26:26 (SESSION 2)#begin; BEGIN 22:26:28 (SESSION 2)#select pg_advisory_lock(1), * from emp; .. .. |
Session 2 is hanged and can run only if session 1 releases lock
The error message that I got in alert log file is
1 2 3 4 5 6 |
2020-04-13 22:37:07.829 IST [5975] LOG: process 5975 still waiting for ExclusiveLock on advisory lock [13451,0,1,1] after 1000.565 ms 2020-04-13 22:37:07.829 IST [5975] DETAIL: Process holding the lock: 5911. Wait queue: 5975. 2020-04-13 22:37:07.829 IST [5975] STATEMENT: select pg_advisory_lock(1), * from emp; |
A shared advisory lock will block any exclusive advisory lock on the same lock key while still allowing other shared advisory locks to be acquired for the same lock key.
Session 1: Acquired shared lock with lock key 1
1 2 3 4 5 6 7 |
22:41:11 (SESSION 1)#select pg_advisory_lock_shared(1), * from emp; pg_advisory_lock_shared | id | sno | name | sal | dept -------------------------+----+-----+------+-----+------- | 2 | 2 | B | 200 | IT | 3 | 3 | C | 300 | SALES | 1 | 1 | A | 300 | IT (3 rows) |
Session 2: Still acquired shared lock with lock key 1
1 2 3 4 5 6 7 8 9 |
22:41:39 (SESSION 2)#select pg_advisory_lock_shared(1), * from emp; pg_advisory_lock_shared | id | sno | name | sal | dept -------------------------+----+-----+------+-----+------- | 2 | 2 | B | 200 | IT | 3 | 3 | C | 300 | SALES | 1 | 1 | A | 300 | IT (3 rows) 22:41:48 (SESSION 2)# |
The try_
variants return right away, and you can use the boolean
result value to verify if the lock has been successfully acquired.
Session 1: get a lock
1 2 3 4 5 |
22:46:25 (SESSION 1)#select pg_advisory_lock(1); pg_advisory_lock ------------------ (1 row) |
Session 2 with try variant, it immediately returned false without waiting
1 2 3 4 5 6 7 |
22:46:24 (SESSION 2)#select pg_try_advisory_lock(1); pg_try_advisory_lock ---------------------- f (1 row) 22:46:50 (SESSION 2)# |
If the same resource is locked three times, it must then be unlocked three times to be released for other sessions’ use.
1 2 3 4 5 6 7 |
22:49:09 (SESSION 1)#select locktype, classid, objid, pid, mode, granted from pg_locks where locktype = 'advisory'; locktype | classid | objid | pid | mode | granted ----------+---------+-------+------+---------------+--------- advisory | 0 | 2 | 6184 | ExclusiveLock | t advisory | 0 | 1 | 6184 | ExclusiveLock | t (2 rows) |
Here, I acquired two locks, so I need to run two unlocks.
Memory for Locks / Lock Space
This memory component is to store all the types of locks used by the PostgreSQL instance. These locks are shared across all the background server and user processes connecting to the database. A non-default larger setting of two database parameters, namely max_locks_per_transaction and max_pred_locks_per_transaction, in a way influences the size of this memory component.
Default values for PostgreSQL Lock space in postgresql.conf file.
- #deadlock_timeout = 1s
- #max_locks_per_transaction = 64
- #max_pred_locks_per_transaction = 64
- #max_pred_locks_per_relation = -2
- #max_pred_locks_per_page = 2
1 2 3 4 |
postgres=# select pg_advisory_lock(v) from generate_series(1,10000) v; ERROR: out of shared memory HINT: You might need to increase max_locks_per_transaction. postgres=# |
PostgreSQL Locks: Best practices
Let us recap what we understood so far on PostgreSQL locks with the below picture.
We shall now proceed with the best practices.
Lock queues:
Consider below example.,
1 2 3 4 5 6 7 8 |
02:39:12 (SESSION 1)#select *, pg_sleep(10000) from dept; .. 02:39:11 (SESSION 2)#alter table dept drop add1; .. 02:39:10 (SESSION 3)# alter table dept add add2 varchar(10); .. |
Now what locks are there on my database
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
postgres=# \! psql -f implicit.sql pid | vxid | lock_type | lock_mode | granted | xid_lock | relname ------+-------+-----------+---------------------+---------+----------+----------- 8464 | 6/5 | relation | AccessExclusiveLock | f | | dept 8473 | 3/446 | relation | AccessShareLock | t | | dept 8473 | 3/446 | relation | AccessShareLock | t | | dept_pkey 8477 | 5/118 | relation | AccessExclusiveLock | f | | dept (4 rows) postgres=# select pid, postgres-# usename, postgres-# pg_blocking_pids(pid) as blocked_by, postgres-# query as blocked_query postgres-# from pg_stat_activity postgres-# where cardinality(pg_blocking_pids(pid)) > 0; pid | usename | blocked_by | blocked_query ------+----------+-------------+---------------------------------------- 8477 | postgres | {8473} | alter table dept drop add1; 8464 | postgres | {8473,8477} | alter table dept add add2 varchar(10); (2 rows) |
- Session 1 was holding, AccessShareLock while season 2 and session 3 were waiting to get AccessExclusiveLock . Note that the locks are in the queue, i.e., session 2 has to wait for session 1 to release the lock, but session 3 has to wait for both session 1 and session 2 to release the lock.
- pg_dump also holds AccessShareLock
so, if you have any long-running queries or queries which require resource blocked by AccessShareLock, you can implement one of the following for subsequent queries.
- SET statement_timeout = 40;
- SET lock_timeout TO ’10s’
Making use of concurrency
- Indexes: As mentioned in the above picture, create index concurrently will come under the 4th category, which doesn’t block category 3, which means create index concurrently does not block INSERTS, UPDATES, AND DELETES on a table which is very much important in OLTP environment.
- Materialized views: As mentioned in the above picture, concurrent materialized view creation doesn’t block selects, which is important in the reporting environment.
Rewrite your query whenever required
For example, if you want to replace the contents of a table completely, do it in the test table then rename the test table with the main table with alter commands so that you can avoid long-running import/export on the main table.
Primary keys and Foreign Keys
I need to have primary keys on tables in my environment to have a logical replication to work. If not, I will end up with the following error in my alert log
1 2 3 4 5 6 7 |
18892 2020-04-12 17:27:24.267 IST [2709] DEBUG: unregistering background worker "logical replication worker for subscription 16458" 18893 2020-04-12 17:27:24.267 IST [2709] LOG: worker process: logical replication worker for subscription 16458 (PID 3888) exited with exit code 1 18894 2020-04-12 17:27:29.135 IST [2774] ERROR: there is no unique constraint matching given keys for referenced table "dept" |
Here, Instead of creating a primary key with alter table command, which needs to acquire lock 7, create a unique index concurrently which requires 5 and then make it as a primary key constraint.
Finally, keep an eye on point 7 things mentioned in the above picture, and if you think it’s going to take time, try to avoid doing it during business hours, for example, VACUUM FULL.
This post would not have been possible without taking references from prominent database administrators and architects. I conclude the post with a small token of gratitude to Vlad Mihalcea, Igor Šarčević, Marco slot, Bruce Momjian.
Help me understand that you liked the post by clicking on the like button on postgreshelp Facebook page.
Want to get trained from PostgreSQL expert? Click here
Did I miss anything?
Please let me know in the comments, and I will be happy to share my thoughts.
This would be more useful if you included the source for these two things: postgres=# ! psql -f implicit.sql postgres=# ! psql -f tablelock.sql