Lewati ke konten

Reproduksi race di psql

Di tutorial ini kamu sengaja membuat PostgreSQL kehilangan update, dengan dua terminal sebagai dua order. Lalu kamu menghentikannya dengan cara yang dipakai warehouse, dan terakhir membatasi berapa lama order kedua boleh menunggu. Penjelasan kenapa tiap langkah berperilaku seperti itu ada di Dua pembeli, satu barang; di sini kamu cukup melihatnya terjadi.

Kamu butuh Docker. Setiap perintah dan setiap output di bawah dijalankan terhadap postgres:16-alpine.

  1. Jalankan PostgreSQL 16 dalam container yang terhapus sendiri saat dihentikan:

    Terminal window
    docker run --rm --name kinetix-race -e POSTGRES_PASSWORD=race -d postgres:16-alpine
  2. Buka satu terminal dan sambungkan. Ini Terminal A, order pertama:

    Terminal window
    docker exec -it kinetix-race psql -U postgres
  3. Di Terminal A, buat bin berisi tiga unit SKU-C. Check constraint-nya sengaja dipasang: perhatikan apakah ia menyelamatkanmu.

    Terminal A
    CREATE TABLE bin_inventories (
    id bigint PRIMARY KEY,
    sku text NOT NULL,
    quantity integer NOT NULL,
    reserved_quantity integer NOT NULL DEFAULT 0,
    CHECK (reserved_quantity <= quantity)
    );
    INSERT INTO bin_inventories (id, sku, quantity) VALUES (1, 'SKU-C', 3);
  4. Buka terminal kedua dan sambungkan dengan cara yang sama. Ini Terminal B, order kedua:

    Terminal window
    docker exec -it kinetix-race psql -U postgres

Kedua order ingin ketiga unit. Masing-masing membaca, memutuskan, lalu menulis.

  1. Di Terminal A, mulai transaksi dan baca berapa yang tersedia:

    Terminal A
    BEGIN;
    SELECT quantity - reserved_quantity AS available FROM bin_inventories WHERE id = 1;
    Output
    BEGIN
    available
    -----------
    3
    (1 row)
  2. Di Terminal B, lakukan hal yang persis sama. B juga melihat tiga unit, karena A belum menulis apa pun.

    Terminal B
    BEGIN;
    SELECT quantity - reserved_quantity AS available FROM bin_inventories WHERE id = 1;
  3. Di Terminal A, reserve ketiganya. Nilainya adalah hasil hitungan A dari bacaannya: 0 + 3.

    Terminal A
    UPDATE bin_inventories SET reserved_quantity = 3 WHERE id = 1;
    Output
    UPDATE 1
  4. Di Terminal B, reserve ketiganya juga:

    Terminal B
    UPDATE bin_inventories SET reserved_quantity = 3 WHERE id = 1;

    Terminal B tidak menjawab. PostgreSQL membuat tulisan kedua menunggu row lock yang diambil A lewat UPDATE-nya. Kalau kamu membuka terminal ketiga, kamu bisa melihatnya menunggu (pid-mu akan berbeda):

    Terminal C
    SELECT pid, wait_event_type, wait_event, left(query, 50) AS query
    FROM pg_stat_activity WHERE wait_event_type = 'Lock';
    Output
    pid | wait_event_type | wait_event | query
    -----+-----------------+---------------+----------------------------------------------------
    91 | Lock | transactionid | UPDATE bin_inventories SET reserved_quantity = 3 W
    (1 row)
  5. Di Terminal A, commit. Terminal B langsung bangun dan mencetak UPDATE 1.

    Terminal A
    COMMIT;
  6. Di Terminal B, commit juga:

    Terminal B
    COMMIT;
  7. Lihat barisnya dari terminal mana saja:

    Terminal A
    SELECT quantity, reserved_quantity FROM bin_inventories WHERE id = 1;
    Output
    quantity | reserved_quantity
    ----------+-------------------
    3 | 3
    (1 row)

Kedua transaksi commit dan kedua order diberi tahu mereka memegang tiga unit: enam dijanjikan, tiga di rak. Baris mencatat tiga ter-reserve. Check constraint tidak pernah bereaksi, karena tidak ada tulisan yang menyimpan nilai ilegal — B hanya menimpa A.

Sekarang bacaannya sendiri yang mengambil row lock, dengan FOR UPDATE, persis seperti warehouse.

  1. Di Terminal A, kembalikan rak seperti semula, lalu mulai lagi — kali ini dengan locking read:

    Terminal A
    UPDATE bin_inventories SET reserved_quantity = 0 WHERE id = 1;
    BEGIN;
    SELECT quantity - reserved_quantity AS available FROM bin_inventories WHERE id = 1 FOR UPDATE;
    Output
    UPDATE 1
    BEGIN
    available
    -----------
    3
    (1 row)
  2. Di Terminal B, minta baris yang sama dengan cara yang sama:

    Terminal B
    BEGIN;
    SELECT quantity - reserved_quantity AS available FROM bin_inventories WHERE id = 1 FOR UPDATE;

    Kali ini B menunggu saat membaca. B belum melihat angka apa pun, jadi tidak ada data basi yang bisa ia pakai.

  3. Di Terminal A, reserve lalu commit:

    Terminal A
    UPDATE bin_inventories SET reserved_quantity = 3 WHERE id = 1;
    COMMIT;
    Output
    UPDATE 1
    COMMIT
  4. Bacaan Terminal B selesai begitu A commit, dan mengembalikan baris sebagaimana A meninggalkannya:

    Output di Terminal B
    available
    -----------
    0
    (1 row)

    Tidak ada yang tersedia, jadi B mundur. Di warehouse, di sinilah panggilannya ditolak dengan INSUFFICIENT_STOCK:

    Terminal B
    ROLLBACK;

Lock mengubah balapan menjadi antrean. Tanpa batas, satu transaksi yang lambat menahan semua order untuk produk yang sama. Warehouse menetapkan lock_timeout hanya untuk transaksinya sendiri; ini hal yang sama secara manual.

  1. Di Terminal A, ambil lock dan tahan:

    Terminal A
    BEGIN;
    SELECT quantity - reserved_quantity AS available FROM bin_inventories WHERE id = 1 FOR UPDATE;
  2. Di Terminal B, izinkan penantian paling lama dua detik, lalu minta barisnya:

    Terminal B
    BEGIN;
    SET LOCAL lock_timeout = '2s';
    SELECT quantity - reserved_quantity AS available FROM bin_inventories WHERE id = 1 FOR UPDATE;

    Setelah dua detik:

    Output di Terminal B
    ERROR: canceling statement due to lock timeout
    CONTEXT: while locking tuple (0,5) in relation "bin_inventories"

    Posisi tuple di baris CONTEXT akan berbeda di mesinmu. SET LOCAL membatasi pengaturan itu ke transaksi ini, itulah sebabnya warehouse memakai bentuk yang sama.

  3. Akhiri kedua transaksi:

    Terminal B
    ROLLBACK;
    Terminal A
    ROLLBACK;

Keluar dari kedua sesi psql dengan \q, lalu hentikan container-nya. Container itu dijalankan dengan --rm, jadi menghentikannya sekaligus menghapusnya.

Terminal window
docker stop kinetix-race