PostgreSQL 的 pg_advisory_lock 函數
pg_advisory_lock 是 PostgreSQL 提供的一種應用級鎖機制,它不鎖定具體的數據庫對象(如表或行),而是通過數字鍵值來協調應用間的并發控制。
鎖的基本概念
PostgreSQL 提供兩種咨詢鎖(advisory lock):
- 會話級咨詢鎖:鎖在會話結束時自動釋放
- 事務級咨詢鎖:鎖在事務結束時自動釋放
主要函數列表
函數 | 描述 |
---|---|
pg_advisory_lock(key) | 獲取會話級咨詢鎖(阻塞) |
pg_try_advisory_lock(key) | 嘗試獲取會話級咨詢鎖(非阻塞) |
pg_advisory_xact_lock(key) | 獲取事務級咨詢鎖(阻塞) |
pg_try_advisory_xact_lock(key) | 嘗試獲取事務級咨詢鎖(非阻塞) |
pg_advisory_unlock(key) | 釋放會話級咨詢鎖 |
pg_advisory_unlock_all() | 釋放當前會話持有的所有咨詢鎖 |
函數詳解
1 pg_advisory_lock(key bigint)
功能:獲取一個會話級別的咨詢鎖(如果鎖已被其他會話持有,則阻塞等待)
參數:
key
:64位整數鎖標識
示例:
SELECT pg_advisory_lock(123456);
-- 執行需要同步的操作
SELECT pg_advisory_unlock(123456);
2 pg_try_advisory_lock(key bigint)
功能:嘗試獲取會話級咨詢鎖(非阻塞,立即返回成功與否)
返回值:boolean(true表示獲取成功)
示例:
DO $$
BEGINIF pg_try_advisory_lock(123456) THENRAISE NOTICE 'Lock acquired, performing work...';-- 執行受保護的操作PERFORM pg_advisory_unlock(123456);ELSERAISE NOTICE 'Could not acquire lock, skipping...';END IF;
END $$;
3 pg_advisory_xact_lock(key bigint)
功能:獲取事務級咨詢鎖(鎖在事務結束時自動釋放)
示例:
BEGIN;
SELECT pg_advisory_xact_lock(123456);
-- 執行需要同步的操作
COMMIT; -- 鎖自動釋放
4 pg_try_advisory_xact_lock(key bigint)
功能:嘗試獲取事務級咨詢鎖(非阻塞)
示例:
BEGIN;
SELECT pg_try_advisory_xact_lock(123456);
-- 無論是否獲取成功都繼續執行
COMMIT;
鎖的鍵值設計
咨詢鎖使用64位整數作為鍵值,有兩種使用方式:
-
單鍵模式:使用一個64位整數
SELECT pg_advisory_lock(123456789);
-
雙鍵模式:使用兩個32位整數組合
SELECT pg_advisory_lock(123, 456);
實際應用場景
場景1:防止定時任務重復執行
-- 在定時任務開始時檢查鎖
DO $$
BEGINIF NOT pg_try_advisory_xact_lock(987654) THENRAISE NOTICE 'Task is already running in another process';RETURN;END IF;RAISE NOTICE 'Starting scheduled task...';-- 執行定時任務邏輯-- ...COMMIT; -- 鎖自動釋放
END $$;
場景2:應用級分布式鎖
-- 應用1獲取鎖
SELECT pg_advisory_lock(555555) FROM my_table WHERE id = 1;-- 應用2嘗試獲取同樣的鎖
SELECT pg_try_advisory_lock(555555); -- 返回false-- 應用1釋放鎖
SELECT pg_advisory_unlock(555555);
場景3:確保單實例初始化
-- 系統初始化時確保只執行一次
DO $$
BEGIN-- 嘗試獲取鎖,等待最多5秒FOR i IN 1..5 LOOPIF pg_try_advisory_lock(1357924680) THEN-- 檢查是否已經初始化IF NOT EXISTS (SELECT 1 FROM system_status WHERE initialized = true) THEN-- 執行初始化INSERT INTO system_status(initialized) VALUES (true);RAISE NOTICE 'System initialized successfully';ELSERAISE NOTICE 'System already initialized';END IF;-- 顯式釋放鎖(雖然會話結束會自動釋放)PERFORM pg_advisory_unlock(1357924680);RETURN;END IF;PERFORM pg_sleep(1); -- 等待1秒END LOOP;RAISE EXCEPTION 'Could not acquire initialization lock after 5 seconds';
END $$;
監控咨詢鎖
查看當前持有的咨詢鎖
SELECT locktype, classid, objid, objsubid, mode, granted
FROM pg_locks
WHERE locktype = 'advisory';
查看所有咨詢鎖(包括已授予和等待的)
SELECT pid, locktype, mode, granted, fastpath, virtualtransaction
FROM pg_locks
WHERE locktype = 'advisory';
注意事項
-
鎖釋放:
- 會話級鎖必須顯式釋放或會話結束自動釋放
- 事務級鎖在事務結束時自動釋放
-
死鎖風險:
- 按固定順序獲取多個咨詢鎖以避免死鎖
- 使用 pg_try_advisory_lock 可以降低死鎖風險
-
性能影響:
- 咨詢鎖比表鎖/行鎖更輕量級
- 大量使用仍可能影響性能
-
集群環境:
- 咨詢鎖只在單個PostgreSQL實例內有效
- 不適用于跨多個數據庫實例的協調
-
鎖標識管理:
- 建議在應用中集中管理鎖標識
- 使用有意義的常量而非魔法數字
高級用法
超時獲取鎖
DO $$
DECLARElock_acquired BOOLEAN := false;timeout INTERVAL := '5 seconds';start_time TIMESTAMP := clock_timestamp();
BEGINWHILE (clock_timestamp() - start_time) < timeout LOOPIF pg_try_advisory_lock(424242) THENlock_acquired := true;EXIT;END IF;PERFORM pg_sleep(0.1); -- 等待100msEND LOOP;IF lock_acquired THENRAISE NOTICE 'Lock acquired after %', clock_timestamp() - start_time;-- 執行受保護的操作PERFORM pg_advisory_unlock(424242);ELSERAISE EXCEPTION 'Could not acquire lock within timeout';END IF;
END $$;
使用咨詢鎖實現隊列
-- 生產者
SELECT pg_advisory_lock(987); -- 全局寫鎖-- 插入隊列項
INSERT INTO job_queue(job_data) VALUES ('some data');SELECT pg_advisory_unlock(987);-- 消費者
SELECT pg_advisory_lock(988); -- 全局讀鎖-- 獲取并鎖定一個作業
UPDATE job_queue
SET status = 'processing', worker_id = pg_backend_pid(),claimed_at = NOW()
WHERE id = (SELECT id FROM job_queue WHERE status = 'pending' ORDER BY created_at LIMIT 1
)
RETURNING *;SELECT pg_advisory_unlock(988);
pg_advisory_lock 是 PostgreSQL 強大的應用級同步機制,合理使用可以解決復雜的并發控制問題,但需要謹慎設計以避免死鎖和性能問題。