若要讓文章清單及其筆數保持一致,請在同一個 node-postgres 用戶端上,於單一 REPEATABLE READ READ ONLY 交易中執行這兩次讀取。本教學會說明這種模式,以及如何在 Mac 上透過兩個工作階段進行檢查。
為什麼文章清單可能與其筆數不一致?
假設某個桌面指令碼先取得 craft 主題中已發布的文章,接著再計算符合條件的文章數。在這兩次讀取之間,另一個工作階段變更了一篇文章的主題。傳回的清單可能包含兩篇文章,但筆數卻是 1。
PostgreSQL 的預設隔離等級 READ COMMITTED 會在每個一般 SELECT 陳述式開始時提供一個快照。因此,即使在同一個交易中,連續讀取也可能看到不同的已提交資料。PostgreSQL:交易隔離。
就這項工作而言,我們要的是對文章資料列的一致檢視。後續編輯可以在下次執行時顯示。PostgreSQL 的 REPEATABLE READ 會讓連續讀取使用交易中第一個非交易控制陳述式所建立的快照;單純執行 BEGIN 並不會建立該快照。PostgreSQL:交易隔離。
這項實作有兩個要求:在讀取前選定隔離等級,並讓所有相關查詢都使用同一個用戶端。node-postgres 明確要求交易使用單一用戶端,並警告不要用 pool.query() 執行交易陳述式。node-postgres:交易。
在 Mac 上準備少量文章測試資料
請使用執行中的本機 PostgreSQL 伺服器、psql 指令,以及已安裝 npm 的受支援 Node.js 版本。以下指令假設現有本機連線可搭配 postgres 資料庫使用。如果你的工作資料庫名稱不同,請一致地替換名稱。安裝 PostgreSQL 並設定連線是必要的先決條件。
開啟 Terminal 並建立專案資料夾。node-postgres 的套件名稱為 pg。node-postgres:歡迎。
mkdir article-snapshot-demo
cd article-snapshot-demo
npm init -y
npm install pg
export PGDATABASE=postgres
psql -d postgres
在 psql 提示字元下執行一次以下設定。這會建立一個專用資料表,其中只包含合成的文章資料列。明確指定識別碼也能讓後續操作更容易理解。
CREATE TABLE public.article_snapshot_demo (
id integer PRIMARY KEY,
title text NOT NULL,
topic text NOT NULL,
published boolean NOT NULL
);
INSERT INTO public.article_snapshot_demo
(id, title, topic, published)
VALUES
(1, 'Paper Shapes', 'craft', true),
(2, 'Color Studies', 'craft', true),
(3, 'Desk Layouts', 'design', true);
如果資料表已存在,請先停止並檢查先前的測試資料,不要自動刪除或取代。以下範例假設起始時恰有這三筆資料列。在執行 Node.js 指令碼前,請確認 psql 為 \q。
透過同一個已取出的用戶端讀取清單和筆數
將以下完整程式儲存為專案資料夾中的 read-articles.mjs。new Pool() 會使用文件所述的 PostgreSQL 連線環境變數,包括上方匯出的 PGDATABASE 值。node-postgres:連線。
程式會取得一個用戶端,以預先指定交易特性的方式開始交易,執行兩次讀取,並在傳回組合後的結果前提交交易。PostgreSQL 支援直接在 BEGIN 中指定這些特性;交易會以 COMMIT 或 ROLLBACK 結束。PostgreSQL:BEGIN。
import pg from 'pg';
const { Pool } = pg;
const pool = new Pool({ connectionTimeoutMillis: 5000 });
pool.on('error', (error) => {
console.error('Idle database client error:', error.message);
process.exitCode = 1;
});
async function readArticles(topic) {
const client = await pool.connect();
let began = false;
let discardClient = false;
try {
await client.query(
'BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY'
);
began = true;
const articles = await client.query(
`SELECT id, title, topic
FROM public.article_snapshot_demo
WHERE topic = $1 AND published = true
ORDER BY id`,
[topic]
);
const count = await client.query(
`SELECT count(*)::text AS total
FROM public.article_snapshot_demo
WHERE topic = $1 AND published = true`,
[topic]
);
await client.query('COMMIT');
began = false;
return {
topic,
articles: articles.rows,
total: count.rows[0].total
};
} catch (error) {
if (began) {
try {
await client.query('ROLLBACK');
} catch (rollbackError) {
discardClient = true;
throw new AggregateError(
[error, rollbackError],
'Article read failed; rollback also failed'
);
}
} else {
discardClient = true;
}
throw error;
} finally {
client.release(discardClient);
}
}
try {
const result = await readArticles('craft');
console.log(JSON.stringify(result, null, 2));
} catch (error) {
console.error(error);
process.exitCode = 1;
} finally {
await pool.end();
}
在同一個 Terminal 工作階段中執行程式:
node read-articles.mjs
在測試資料未變更的情況下,預期結果會包含文章 1 和 2,且 total 的值為字串 \"2\"。筆數明確以文字傳回,因此此範例不需要轉換成 JavaScript 數字。兩個查詢使用相同的主題和發布條件;隔離無法修正不相符的篩選條件。
清理流程也是可用模式的一部分。讀取失敗時會觸發復原;如果復原失敗,則會捨棄該用戶端。node-postgres 文件說明,使用 client.release(true) 銷毀已取出的用戶端,並使用 pool.end() 關閉指令碼的連線池。node-postgres:連線池 API。
這是獨立指令碼,因此每次執行後都會關閉連線池。在既有應用程式中,請沿用應用程式的連線池生命週期,並在其中呼叫交易函式。所有參與這些讀取的輔助函式都必須接收已取出的 client;若某個輔助函式暗中呼叫 pool.query(),就會破壞這項界線。node-postgres:交易。
使用兩個 Terminal 工作階段驗證快照
快速執行的指令碼可能會在另一個工作階段變更資料之前就結束。請依照以下刻意安排的步驟操作,明確呈現相關時序。這些是使用已建立資料表、依工作階段分別執行的 SQL 步驟,不是彼此獨立的設定程式。預期的資料庫結果依據文件所述的隔離行為推論而來,並非宣稱實際執行過 PostgreSQL 測試。
開啟兩個 Terminal 視窗,並在各自執行 psql -d postgres。開始讀取前,先在工作階段 B 還原合成測試資料的主題。等待此陳述式執行完成:
-- Session B: restore the fixture before beginning the exercise.
UPDATE public.article_snapshot_demo
SET topic = 'craft'
WHERE id = 2;
在工作階段 A 中啟動讀取交易並取得清單。查詢完成後暫停:
-- Session A: run these statements, then pause.
BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY;
SELECT id, title
FROM public.article_snapshot_demo
WHERE topic = 'craft' AND published = true
ORDER BY id;
清單應包含 Paper Shapes 和 Color Studies。接著切換到工作階段 B,使用以下獨立陳述式提交主題變更。在明確交易區塊外,PostgreSQL 會自動提交成功的陳述式。PostgreSQL:BEGIN。
-- Session B: run while session A remains open.
UPDATE public.article_snapshot_demo
SET topic = 'design'
WHERE id = 2;
回到工作階段 A,完成相關讀取:
-- Session A: finish the original reader transaction.
SELECT count(*)::text AS total
FROM public.article_snapshot_demo
WHERE topic = 'craft' AND published = true;
COMMIT;
預期筆數為 2,與工作階段 A 先前取得的清單相符。之後重新執行 Node.js 指令碼,應只傳回 Paper Shapes,總數為 \"1\"。新舊結果各自都代表一致的檢視。
若要進行受控比較,請重做整個操作,包括先在工作階段 B 還原資料,並在工作階段 A 中使用 BEGIN ISOLATION LEVEL READ COMMITTED READ ONLY; 開始交易。維持相同的暫停和更新順序。預期第一次取得的清單會包含兩篇文章,而稍後的筆數為 1。如此比較可將交易設定作為唯一變更條件。
為相關讀取選擇合適的界線
調整範例時可參考以下判斷方式。先決定哪些輸出必須一致,再選擇能提供這些輸出的最小資料庫操作。
| 讀取者的工作 | 合適的做法 | 必須維持的條件 |
|---|---|---|
| 透過單一一般查詢傳回一份文章清單 | 單一查詢可使用 READ COMMITTED |
後續查詢不需要共用該快照。 |
| 傳回清單及透過另一個查詢取得的筆數 | 單一 REPEATABLE READ READ ONLY 交易 |
使用同一個用戶端和相符的篩選條件。 |
| 取得多個批次以組成一致的結果 | 在同一個交易中進行讀取 | 新交易會建立新的檢視。 |
| 協調不同資料庫工作階段之間的讀取 | 明確匯出及匯入快照 | 不同交易不會自動共用快照。 |
若要跨多個工作階段操作,PostgreSQL 提供 pg_export_snapshot() 和 SET TRANSACTION SNAPSHOT。在其他交易匯入快照前,匯出者必須保持開啟。匯入交易必須使用 REPEATABLE READ 或 SERIALIZABLE,並在第一次查詢或資料修改陳述式前匯入。命令參考文件說明了序列化匯入的其他限制。PostgreSQL:快照同步函式、PostgreSQL:SET TRANSACTION。
對於這項桌面清單和筆數工作,保留同一個用戶端即可避免額外協調。完成資料庫讀取並關閉交易後,再等待使用者選擇其他操作。
此隔離等級無法解決哪些問題?
PostgreSQL 的 REPEATABLE READ 可避免幻讀,但仍可能發生序列化異常。因此,穩定的快照無法確保並行工作流程的行為等同於逐一執行。它也不會凍結序列值;序列具有特殊的交易行為。PostgreSQL:交易隔離。
READ ONLY 禁止變更一般資料表,但文件列出的特定例外操作(例如某些暫存資料表操作)除外。它本身不會選擇穩定快照。請在讀取前設定這兩項特性:交易的第一個查詢或資料修改陳述式執行後,PostgreSQL 不會允許你變更隔離等級。PostgreSQL:SET TRANSACTION。
如果報表需要更強的序列化語意,PostgreSQL 支援 SERIALIZABLE READ ONLY DEFERRABLE。取得快照時可能需要等待,之後執行時則不會因序列化失敗而取消。這項取捨與此處示範的一般文章讀取需求不同。PostgreSQL:SET TRANSACTION。
若要取得文章清單和筆數,請明確界定實作範圍:取得一個用戶端、以所需特性開始交易、讀取所有相關結果、提交交易,並釋放用戶端。重複使用相同的篩選條件,並在交易完成後才提供組合好的結果。如此一來,下一個桌面操作就能使用一組一致的文章資料。
來源
- 交易隔離 — PostgreSQL Global Development Group。
- BEGIN — PostgreSQL Global Development Group。
- SET TRANSACTION — PostgreSQL Global Development Group。
- 系統管理函式:快照同步函式 — PostgreSQL Global Development Group。
- 交易 — node-postgres。
- 連線池 API — node-postgres。
- 連線 — node-postgres。
- 歡迎 — node-postgres。