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.
Persiapan
Section titled “Persiapan”-
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 -
Buka satu terminal dan sambungkan. Ini Terminal A, order pertama:
Terminal window docker exec -it kinetix-race psql -U postgres -
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); -
Buka terminal kedua dan sambungkan dengan cara yang sama. Ini Terminal B, order kedua:
Terminal window docker exec -it kinetix-race psql -U postgres
Bagian 1 — kehilangan update
Section titled “Bagian 1 — kehilangan update”Kedua order ingin ketiga unit. Masing-masing membaca, memutuskan, lalu menulis.
-
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 BEGINavailable-----------3(1 row) -
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; -
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 -
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 queryFROM 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) -
Di Terminal A, commit. Terminal B langsung bangun dan mencetak
UPDATE 1.Terminal A COMMIT; -
Di Terminal B, commit juga:
Terminal B COMMIT; -
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.
Bagian 2 — ambil lock saat membaca
Section titled “Bagian 2 — ambil lock saat membaca”Sekarang bacaannya sendiri yang mengambil row lock, dengan FOR UPDATE, persis seperti warehouse.
-
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 1BEGINavailable-----------3(1 row) -
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.
-
Di Terminal A, reserve lalu commit:
Terminal A UPDATE bin_inventories SET reserved_quantity = 3 WHERE id = 1;COMMIT;Output UPDATE 1COMMIT -
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;
Bagian 3 — batasi penantiannya
Section titled “Bagian 3 — batasi penantiannya”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.
-
Di Terminal A, ambil lock dan tahan:
Terminal A BEGIN;SELECT quantity - reserved_quantity AS available FROM bin_inventories WHERE id = 1 FOR UPDATE; -
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 timeoutCONTEXT: while locking tuple (0,5) in relation "bin_inventories"Posisi tuple di baris
CONTEXTakan berbeda di mesinmu.SET LOCALmembatasi pengaturan itu ke transaksi ini, itulah sebabnya warehouse memakai bentuk yang sama. -
Akhiri kedua transaksi:
Terminal B ROLLBACK;Terminal A ROLLBACK;
Bersih-bersih
Section titled “Bersih-bersih”Keluar dari kedua sesi psql dengan \q, lalu hentikan container-nya. Container itu dijalankan
dengan --rm, jadi menghentikannya sekaligus menghapusnya.
docker stop kinetix-race