Postgres 的跨 client 通知機制:LISTEN / NOTIFY
會想要使用 LISTEN
和 NOTIFY
的情況是想要偵測某個 table 的欄位變動時,想要做一些與 Query 本身無關的事情,例如:
- 寄送通知信
- 重算報表
- 更新快取
雖然這些操作我們都可以在 CPP ( 或其他 DB Client ) 執行完 query 後手動處理,但是如果今天我們 DB 的結構比較複雜,有比較多的 trigger 或是 foreign key 的時候,就會比較難在 CPP 來處理所有需要通知的時機點,或是未來在調整程式時很容易漏掉
相反的如果我們在 postgres trigger 來做這些操作,不但會讓 query 執行變久,也要做額外的錯誤處理
這時候 LISTEN
/ NOTIFY
會是一個不錯的選項
語法
NOTIFY
NOTIFY channel [ , payload ]
channel
和 payload
都是字串,只有 channel
是一定要帶的參數
假設我們今天每次登入都會寫紀錄到 login_log
這張 table,當發生 login 失敗時我們要額外寄送通知信,那麼我們就可以直接在這張 table 綁上一個 trigger,判斷是登入失敗時,就直接呼叫 NOTIFY login_failed 'user@example.com'
如此一來我們就送出了一個登入失敗的通知,並且帶有失敗的 email (也就是 payload)
還有另一種方法是執行 SQL function pg_notify(text, text)
,一樣也可以送出 notify
NOTIFY 有以下的特性:
- 只有在 transaction 被 commit 後才會送出
- 一個 transaction 內重複的 channel, payload 組合只會送出一個 event
- 一個 transaction 內的 event 會依 NOTFIY 執行的順序送出
LISTEN
LISTEN channel
我們只要將想要監聽的事件名稱放在 LISTEN
後面即可
舉例來說,我們想要寄送登入失敗的通知信的話,可以在負責寄信的 process 執行 LISTEN login_failed
,如此一來就可以收到事件
LISTEN 有以下特性:
- 不會收到執行 LISTEN 之前送出的 event
- 如果在執行 transaction 時有人送出通知,要到 transaction 結束 (commit / abort) 後才收得到
如果你是用兩個 psql
來測試不同 connection 的行為的話,執行 NOTIFY 之後在 LISTEN 的 session 記得要輸入 ;
後按 ENTER 才會把收到的 event 印出來
在實作方面,如果是用 postgres 提供的 libpq
,並沒有很好的通知機制,我們要在執行 Query 之後用 PQnotifies()
來檢查是否有收到 event
假設沒有 query 需要執行的話,也可以執行 PQconsumeInput()
後再執行 PQnotifies()
PGnotify *PQnotifies(PGconn *conn);
typedef struct pgNotify
{
char *relname; /* notification channel name */
int be_pid; /* process ID of notifying server process */
char *extra; /* notification payload string */
} PGnotify;
回傳 nullptr
代表沒有 event 了,記得用完 PGnotify
的資訊後,要用 PQfreemem
來釋放記憶體空間,細節可以再看 官方文件
UNLISTEN
UNLISTEN { channel | * }
當我們不在需要監聽的時候,除了直接斷開 DB 連線之外,也可以手動取消 LISTEN。
如果要取消全部就執行 UNLISTEN *
,反之就要寫上 channel 名稱
參考資料
- https://www.postgresql.org/docs/16/sql-notify.html
- https://www.postgresql.org/docs/16/sql-listen.html
- https://www.postgresql.org/docs/16/libpq-notify.html
- https://www.postgresql.org/docs/16/sql-unlisten.html
覺得有用的話可以給我一個拍手
留言
張貼留言