Dalam kerjaya anda, anda akan menghadapi masalah kucing Schrödinger, situasi yang kadangkala berkesan dan kadangkala tidak. Keadaan perlumbaan adalah salah satu daripada cabaran ini (ya, hanya satu!).
Sepanjang catatan blog ini, saya akan membentangkan contoh dunia sebenar, menunjukkan cara menghasilkan semula masalah dan membincangkan strategi untuk mengendalikan keadaan perlumbaan menggunakan pengasingan transaksi boleh bersiri dan kunci nasihat dengan PostgreSQL.
Diinspirasikan oleh "Merancang Aplikasi Intensif Data," Bab 7 - Transaksi "Tahap Pengasingan Lemah"
Repositori Github dengan Contoh Praktikal
Aplikasi ini menguruskan syif atas panggilan untuk doktor di hospital. Untuk memberi tumpuan kepada masalah keadaan perlumbaan, mari kita permudahkan senario kita. Apl kami menyelesaikan sekitar jadual tunggal ini:
CREATE TABLE shifts ( id SERIAL PRIMARY KEY, doctor_name TEXT NOT NULL, shift_id INTEGER NOT NULL, on_call BOOLEAN NOT NULL DEFAULT FALSE );
Kami mempunyai peraturan perniagaan yang kritikal:
Seperti yang anda duga, melaksanakan API naif boleh membawa kepada senario keadaan perlumbaan. Pertimbangkan situasi hipotesis ini:
Jack dan John kedua-duanya on-call di hospital semasa syif yang sama. Pada masa yang hampir sama, mereka memutuskan untuk meminta cuti. Satu berjaya, tetapi yang lain bergantung pada maklumat lapuk tentang bilangan doktor yang sedang bekerja. Akibatnya, kedua-duanya akhirnya meninggalkan syif mereka, melanggar peraturan perniagaan dan meninggalkan syif tertentu tanpa doktor yang bertugas:
John --BEGIN------doctors on call: 2-------leave on call-----COMMIT--------> (t) \ \ \ \ \ \ \ \ Database ------------------------------------------------------------------> (t) / / / / / / / / Jack ------BEGIN------doctors on call: 2-----leave on call----COMMIT-------> (t)
Aplikasi ini adalah API mudah yang dilaksanakan di Golang. Lihat repositori GitHub untuk mendapatkan arahan tentang cara menjalankan dan melaksanakan skrip untuk menghasilkan semula senario keadaan perlumbaan ini. Secara ringkasnya, anda perlu:
Ujian cuba untuk membatalkan dua doktor secara serentak, mencapai titik akhir dengan pendekatan berbeza: shiftId=1 menggunakan kunci nasihat, shiftId=2 menggunakan pengasingan transaksi boleh bersiri dan shiftId=3 ialah pelaksanaan naif tanpa kawalan serentak.
Hasil k6 akan mengeluarkan metrik tersuai untuk menunjukkan shiftId yang melanggar peraturan perniagaan:
✓ at least one doctor on call for shiftId=1 ✓ at least one doctor on call for shiftId=2 ✗ at least one doctor on call for shiftId=3 ↳ 36% — ✓ 123 / ✗ 217
Anda memerlukan alatan seperti Yarn, Go, K6 dan Docker, atau anda boleh menggunakan DevBox untuk persediaan kebergantungan repositori yang lebih mudah.
Masalah berlaku apabila aplikasi kami membuat keputusan berdasarkan data lapuk. Ini boleh berlaku jika dua urus niaga berjalan hampir serentak dan kedua-duanya cuba membatalkan doktor untuk pertukaran mereka. Satu transaksi berjaya seperti yang diharapkan, tetapi satu lagi, bergantung pada maklumat lapuk, juga berjaya dengan salah. Bagaimanakah kita boleh mengelakkan tingkah laku yang tidak diingini ini? Terdapat beberapa cara untuk mencapai ini, dan saya akan meneroka dua pilihan yang disokong oleh PostgreSQL, walaupun penyelesaian yang serupa boleh ditemui dalam sistem pengurusan pangkalan data lain.
Pengasingan Syot Kilat Boleh Bersiri secara automatik mengesan dan menghalang anomali seperti pencongan tulis yang ditunjukkan oleh aplikasi kami.
Saya tidak akan mendalami teori di sebalik pengasingan transaksi, tetapi ia adalah topik biasa dalam banyak sistem pengurusan pangkalan data yang popular. Anda boleh mencari bahan yang baik dengan mencari pengasingan syot kilat, seperti ini daripada dokumentasi rasmi PostgreSQL tentang pengasingan transaksi. Selain itu, berikut ialah kertas yang mencadangkan penyelesaian ini beberapa tahun yang lalu. Cakap murah, jadi jom tengok kod:
Mula-mula, mulakan transaksi dan tetapkan tahap pengasingan kepada Boleh Bersiri:
// Init transaction with serializable isolation level tx, err := db.BeginTxx(c.Request().Context(), &sql.TxOptions{ Isolation: sql.LevelSerializable, })
Kemudian, teruskan untuk melaksanakan operasi. Dalam kes kami, ia melaksanakan fungsi ini:
CREATE OR REPLACE FUNCTION update_on_call_status_with_serializable_isolation(shift_id_to_update INT, doctor_name_to_update TEXT, on_call_to_update BOOLEAN) RETURNS VOID AS $$ DECLARE on_call_count INT; BEGIN -- Check the current number of doctors on call for this shift SELECT COUNT(*) INTO on_call_count FROM shifts s WHERE s.shift_id = shift_id_to_update AND s.on_call = TRUE; IF on_call_to_update = FALSE AND on_call_count = 1 THEN RAISE EXCEPTION '[SerializableIsolation] Cannot set on_call to FALSE. At least one doctor must be on call for this shiftId: %', shift_id_to_update; ELSE UPDATE shifts s SET on_call = on_call_to_update WHERE s.shift_id = shift_id_to_update AND s.doctor_name = doctor_name_to_update; END IF; END; $$ LANGUAGE plpgsql;
Apabila senario yang tidak konsisten berlaku disebabkan pelaksanaan serentak, tahap pengasingan boleh bersiri akan membolehkan satu transaksi berjaya dan akan melancarkan yang lain secara automatik dengan mesej ini, jadi anda boleh mencuba semula dengan selamat:
ERROR: could not serialize access due to read/write dependencies among transactions
Cara lain untuk memastikan peraturan perniagaan kami dikuatkuasakan ialah dengan mengunci sumber secara eksplisit untuk anjakan tertentu. Kita boleh mencapai ini menggunakan Kunci Nasihat pada peringkat transaksi. Kunci jenis ini dikawal sepenuhnya oleh aplikasi. Anda boleh mendapatkan maklumat lanjut mengenainya di sini.
Adalah penting untuk ambil perhatian bahawa kunci boleh digunakan pada kedua-dua peringkat sesi dan transaksi. Anda boleh meneroka pelbagai fungsi yang terdapat di sini. Dalam kes kami, kami akan menggunakan pg_try_advisory_xact_lock(key bigint) → boolean, yang secara automatik melepaskan kunci selepas komit atau rollback:
BEGIN; -- Attempt to acquire advisory lock and handle failure with EXCEPTION IF NOT pg_try_advisory_xact_lock(shift_id_to_update) THEN RAISE EXCEPTION '[AdvisoryLock] Could not acquire advisory lock for shift_id: %', shift_id_to_update; END IF; -- Perform necessary operations -- Commit will automatically release the lock COMMIT;
Berikut ialah fungsi lengkap yang digunakan dalam aplikasi kami:
-- Function to Manage On Call Status with Advisory Locks, automatic release when the trx commits CREATE OR REPLACE FUNCTION update_on_call_status_with_advisory_lock(shift_id_to_update INT, doctor_name_to_update TEXT, on_call_to_update BOOLEAN) RETURNS VOID AS $$ DECLARE on_call_count INT; BEGIN -- Attempt to acquire advisory lock and handle failure with NOTICE IF NOT pg_try_advisory_xact_lock(shift_id_to_update) THEN RAISE EXCEPTION '[AdvisoryLock] Could not acquire advisory lock for shift_id: %', shift_id_to_update; END IF; -- Check the current number of doctors on call for this shift SELECT COUNT(*) INTO on_call_count FROM shifts s WHERE s.shift_id = shift_id_to_update AND s.on_call = TRUE; IF on_call_to_update = FALSE AND on_call_count = 1 THEN RAISE EXCEPTION '[AdvisoryLock] Cannot set on_call to FALSE. At least one doctor must be on call for this shiftId: %', shift_id_to_update; ELSE UPDATE shifts s SET on_call = on_call_to_update WHERE s.shift_id = shift_id_to_update AND s.doctor_name = doctor_name_to_update; END IF; END; $$ LANGUAGE plpgsql;
Dealing with race conditions, like the write skew scenario we talked about, can be pretty tricky. There's a ton of research and different ways to solve these problems, so definitely check out some papers and articles if you're curious.
These issues can pop up in real-life situations, like when multiple people try to book the same seat at an event or buy the same spot in a theater. They tend to appear randomly and can be hard to figure out, especially if it's your first time dealing with them.
When you run into race conditions, it's important to look into what solution works best for your specific situation. I might do a benchmark in the future to compare different approaches and give you more insights.
I hope this post has been helpful. Remember, there are tools out there to help with these problems, and you're not alone in facing them!
Atas ialah kandungan terperinci Menangani Keadaan Perlumbaan: Contoh Praktikal. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!