若要讓文章清單及其筆數保持一致,請在同一個 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。

若要取得文章清單和筆數,請明確界定實作範圍:取得一個用戶端、以所需特性開始交易、讀取所有相關結果、提交交易,並釋放用戶端。重複使用相同的篩選條件,並在交易完成後才提供組合好的結果。如此一來,下一個桌面操作就能使用一組一致的文章資料。

來源