Aby lista artykułów i ich liczba były spójne, wykonaj oba odczyty w jednej transakcji REPEATABLE READ READ ONLY na tym samym kliencie node-postgres. Ten samouczek pokazuje ten wzorzec oraz kontrolę z użyciem dwóch sesji, którą możesz przeprowadzić na Macu.

Dlaczego lista artykułów może nie zgadzać się z ich liczbą?

Załóżmy, że skrypt na komputerze pobiera opublikowane artykuły z tematu craft, a następnie zlicza pasujące artykuły. Między tymi odczytami inna sesja zmienia temat artykułu. Zwrócona lista może zawierać dwa artykuły, podczas gdy liczba wskazuje jeden.

Domyślny poziom izolacji PostgreSQL, READ COMMITTED, przypisuje każdemu zwykłemu SELECT migawkę z chwili rozpoczęcia tej instrukcji. Kolejne odczyty mogą więc widzieć różne zatwierdzone dane, nawet w ramach jednej transakcji. PostgreSQL: Izolacja transakcji.

W tym zadaniu oczekiwanym wynikiem jest jeden spójny obraz wierszy artykułów. Późniejsza edycja może pojawić się przy następnym uruchomieniu. PostgreSQL REPEATABLE READ utrzymuje kolejne odczyty w ramach migawki ustalonej przez pierwszą instrukcję transakcji, która nie steruje transakcją; samo wykonanie BEGIN nie ustanawia tej migawki. PostgreSQL: Izolacja transakcji.

Implementacja ma dwa wymagania: wybierz poziom izolacji przed odczytem i wykonuj wszystkie powiązane zapytania na tym samym kliencie. Dokumentacja node-postgres wyraźnie wymaga jednego klienta dla transakcji i ostrzega przed używaniem pool.query() do jej instrukcji. node-postgres: Transakcje.

Przygotuj niewielki zestaw przykładowych artykułów na Macu

Użyj działającego lokalnego serwera PostgreSQL, polecenia psql oraz obsługiwanej instalacji Node.js z npm. Poniższe polecenia zakładają, że istniejące lokalne połączenie działa z bazą danych postgres. Jeśli używana baza ma inną nazwę, zastąp ją konsekwentnie w każdym miejscu. Instalacja PostgreSQL i konfiguracja połączenia to wymagania wstępne.

Otwórz Terminal i utwórz folder projektu. Udokumentowany pakiet node-postgres to pg. node-postgres: Wprowadzenie.

mkdir article-snapshot-demo
cd article-snapshot-demo
npm init -y
npm install pg
export PGDATABASE=postgres
psql -d postgres

W wierszu poleceń psql uruchom tę konfigurację jeden raz. Utworzy ona dedykowaną tabelę zawierającą wyłącznie syntetyczne wiersze artykułów. Jawne identyfikatory ułatwią też prześledzenie dalszego ćwiczenia.

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);

Jeśli tabela już istnieje, zatrzymaj się i sprawdź poprzedni zestaw danych zamiast automatycznie go usuwać lub zastępować. Przykłady zakładają dokładnie te trzy początkowe wiersze. Przed uruchomieniem skryptu Node.js pozostaw psql z \q.

Odczytaj listę i liczbę za pomocą jednego pobranego klienta

Zapisz poniższy kompletny program w folderze projektu pod nazwą read-articles.mjs. new Pool() korzysta z udokumentowanych zmiennych środowiskowych połączenia z PostgreSQL, w tym z wartości PGDATABASE wyeksportowanej wcześniej. node-postgres: Łączenie.

Program pobiera jednego klienta, rozpoczyna transakcję z określonymi z góry jej właściwościami, wykonuje oba odczyty i zatwierdza transakcję, zanim zwróci złożony wynik. PostgreSQL obsługuje te właściwości bezpośrednio w BEGIN; transakcja kończy się przez COMMIT lub 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();
}

Uruchom program w tej samej sesji Terminal:

node read-articles.mjs

Przy niezmienionym zestawie danych oczekiwany wynik zawiera artykuły 1 i 2, a total ma wartość tekstową \"2\". Liczba jest jawnie zwracana jako tekst, więc ten przykład nie wymaga konwersji na liczbę JavaScript. Oba zapytania używają tego samego tematu i filtra publikacji; izolacja nie skoryguje niezgodnych warunków filtrowania.

Czyszczenie zasobów jest częścią użytecznego wzorca. Nieudany odczyt powoduje wycofanie transakcji, a jeśli wycofanie się nie powiedzie, klient zostaje odrzucony. Dokumentacja node-postgres opisuje client.release(true) do niszczenia pobranego klienta oraz pool.end() do zamykania puli skryptu. node-postgres: API puli.

To samodzielny skrypt, więc po jednym uruchomieniu zamyka swoją pulę. W istniejącej aplikacji zachowaj cykl życia jej puli i umieść w nim funkcję transakcyjną. Każda funkcja pomocnicza uczestnicząca w tych odczytach musi otrzymać pobrany client; funkcja, która po cichu wywołuje pool.query(), łamie tę granicę. node-postgres: Transakcje.

Sprawdź migawkę w dwóch sesjach Terminala

Szybki skrypt może zakończyć działanie, zanim inna sesja cokolwiek zmieni. Wykonaj poniższe ćwiczenie w ściśle określonej kolejności, aby wyraźnie pokazać istotną zależność czasową. Są to instrukcje SQL wykonywane w konkretnych sesjach, które korzystają z już utworzonej tabeli, a nie niezależne programy konfigurujące środowisko. Oczekiwane wyniki zapytań wynikają z udokumentowanego zachowania poziomów izolacji; nie są przedstawiane jako wyniki wykonanego testu PostgreSQL.

Otwórz dwa okna Terminal i w każdym uruchom psql -d postgres. Przed rozpoczęciem odczytu przywróć temat zestawu syntetycznych danych w sesji B. Zaczekaj, aż instrukcja się zakończy:

-- Session B: restore the fixture before beginning the exercise.
UPDATE public.article_snapshot_demo
SET topic = 'craft'
WHERE id = 2;

W sesji A rozpocznij transakcję odczytu i pobierz listę. Po zakończeniu zapytania wstrzymaj działanie:

-- 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;

Lista powinna zawierać artykuły Paper Shapes i Color Studies. Teraz przejdź do sesji B i zatwierdź zmianę tematu za pomocą samodzielnej instrukcji. Poza jawnym blokiem transakcji PostgreSQL automatycznie zatwierdza pomyślnie wykonaną instrukcję. PostgreSQL: BEGIN.

-- Session B: run while session A remains open.
UPDATE public.article_snapshot_demo
SET topic = 'design'
WHERE id = 2;

Wróć do sesji A i wykonaj powiązany odczyt:

-- Session A: finish the original reader transaction.
SELECT count(*)::text AS total
FROM public.article_snapshot_demo
WHERE topic = 'craft' AND published = true;

COMMIT;

Oczekiwana liczba to 2, zgodna z wcześniejszą listą z sesji A. Po zakończeniu świeże uruchomienie skryptu Node.js powinno zwrócić tylko Paper Shapes i łączną liczbę \"1\". Stary i nowy wynik przedstawiają spójne obrazy.

Dla kontrolowanego porównania powtórz całe ćwiczenie, łącznie z początkowym przywróceniem danych przez sesję B, ale rozpocznij w sesji A od BEGIN ISOLATION LEVEL READ COMMITTED READ ONLY;. Zachowaj tę samą kolejność przerw i aktualizacji. Oczekiwana pierwsza lista zawiera dwa artykuły, a późniejsza liczba to 1. W tym porównaniu zmienionym warunkiem jest wyłącznie ustawienie transakcji.

Wybierz właściwą granicę dla powiązanych odczytów

Skorzystaj z tej wskazówki przy dostosowywaniu przykładu. Najpierw ustal, które wyniki muszą być zgodne, a następnie wybierz najmniejszą operację bazy danych, która je zapewni.

Zadanie odczytu Odpowiednie podejście Warunek, który należy zachować
Zwrócenie jednej listy artykułów z jednego zwykłego zapytania Jedno zapytanie może używać READ COMMITTED Żadne późniejsze zapytanie nie musi współdzielić jego migawki.
Zwrócenie listy i osobno odpytywanej liczby Jedna transakcja REPEATABLE READ READ ONLY Użyj jednego klienta i zgodnych filtrów.
Pobranie wielu partii dla jednego spójnego wyniku Wykonuj odczyty w ramach tej samej transakcji Nowa transakcja ustanawia nowy obraz danych.
Koordynowanie odczytów w oddzielnych sesjach bazy danych Jawny eksport i import migawki Niezależne transakcje nie współdzielą migawki automatycznie.

W przypadku wielu sesji PostgreSQL udostępnia pg_export_snapshot() i SET TRANSACTION SNAPSHOT. Eksportująca transakcja musi pozostać otwarta, dopóki pozostałe transakcje nie zaimportują jej migawki. Transakcja importująca musi używać REPEATABLE READ lub SERIALIZABLE i zaimportować migawkę przed pierwszym zapytaniem albo instrukcją modyfikującą dane. Import w trybie Serializable podlega dodatkowym ograniczeniom opisanym w dokumentacji poleceń. PostgreSQL: Funkcje synchronizacji migawek, PostgreSQL: SET TRANSACTION.

W tym zadaniu z listą i liczbą na komputerze zachowanie jednego klienta pozwala uniknąć dodatkowej koordynacji. Zakończ odczyty z bazy danych i zamknij transakcję, zanim zaczniesz czekać, aż ktoś wybierze kolejną czynność.

Czego ten poziom izolacji nie rozwiązuje?

Poziom REPEATABLE READ w PostgreSQL zapobiega odczytom fantomowym, ale nadal mogą występować anomalie serializacji. Stabilna migawka nie gwarantuje więc, że współbieżne przepływy pracy zachowały się tak, jakby były wykonywane pojedynczo. Nie zamraża też wartości sekwencji; sekwencje podlegają szczególnym zasadom transakcyjnym. PostgreSQL: Izolacja transakcji.

READ ONLY zabrania modyfikowania zwykłych tabel, z udokumentowanymi wyjątkami, takimi jak niektóre operacje na tabelach tymczasowych. Samo to ustawienie nie wybiera stabilnej migawki. Ustaw obie właściwości przed odczytem: PostgreSQL nie pozwoli zmienić poziomu izolacji po pierwszym zapytaniu lub instrukcji modyfikującej dane w transakcji. PostgreSQL: SET TRANSACTION.

Gdy raport wymaga silniejszych zasad serializowalności, PostgreSQL obsługuje SERIALIZABLE READ ONLY DEFERRABLE. Podczas uzyskiwania migawki może zaczekać, a następnie działa bez anulowania z powodu błędu serializacji. Ten kompromis dotyczy innego problemu niż zwykłe odczyty artykułów przedstawione tutaj. PostgreSQL: SET TRANSACTION.

W przypadku listy artykułów i ich liczby zachowaj wyraźną granicę implementacji: pobierz jednego klienta, rozpocznij transakcję z wymaganymi właściwościami, odczytaj wszystkie powiązane wyniki, zatwierdź transakcję i zwolnij klienta. Używaj tych samych filtrów i udostępniaj złożony wynik dopiero po zakończeniu transakcji. Dzięki temu kolejna czynność na komputerze będzie mogła korzystać ze spójnego zestawu danych o artykułach.

Źródła