cftransaction: transaksi, commit dan rollback
Ketahui cara guna cftransaction dalam Lucee CFML untuk transaksi, commit dan rollback, dengan corak try/catch, savepoint, tahap pengasingan dan kod diuji.

Kandungan
- Apa itu transaksi dan kenapa ia penting
- Bentuk asas: begin, commit dan rollback
- Corak try dan catch yang betul
- Satu unit kerja, satu transaksi
- Transaksi bersarang dan savepoint
- Tahap pengasingan dan bacaan kotor
- Deadlock dan percubaan semula
- DDL dan commit tersirat
- Kerja panjang menyekat pengguna lain
- Jadual masalah transaksi dan penyelesaian
- Soalan Lazim
- Sumber
cftransaction mengelompokkan beberapa operasi pangkalan data menjadi satu unit kerja atomik: sama ada semuanya disimpan dengan commit, atau semuanya dibatalkan dengan rollback. Kebanyakan operasi perniagaan melibatkan lebih daripada satu cfquery, dan keadaan separuh berjaya separuh gagal ialah punca utama data rosak. Artikel ini menunjukkan bentuk tag dan skrip, corak try dan catch yang betul, savepoint untuk transaksi bersarang, tahap pengasingan, dan cara bertahan terhadap deadlock. Semua contoh kod telah dijalankan pada Lucee 7.1 dengan pangkalan H2 dan lulus.
Apa itu transaksi dan kenapa ia penting
Transaksi ialah sekumpulan pernyataan SQL yang dianggap satu unit, dijamin sifat ACID: atomic, consistent, isolated dan durable. Yang paling penting ialah atomic. Bayangkan pemindahan stok antara dua gudang: anda menolak stok dari gudang A, kemudian menambah ke gudang B. Jika pertanyaan kedua gagal selepas yang pertama berjaya, stok hilang kerana ia sudah ditolak dari A tetapi tidak sampai ke B.
Dengan cftransaction, kedua-dua cfquery dijalankan dalam satu transaksi. Jika salah satu gagal, keseluruhan transaksi dibatalkan dan pangkalan data kembali ke keadaan asal. Inilah nilai utama transaksi: ia menghalang data separuh siap daripada wujud.
Bentuk asas: begin, commit dan rollback
Dalam bentuk tag, cftransaction menggunakan action untuk mengawal kitaran hidup transaksi. action="begin" membuka transaksi, action="commit" menyimpan perubahan, dan action="rollback" membatalkannya. Contoh berikut membuktikan kedua-dua arah pada jadual produk:
<cftransaction action="begin">
<cfquery datasource="kedai">
INSERT INTO produk (id, nama, harga, stok) VALUES (931, 'Dikekalkan', 1.50, 1)
</cfquery>
<cftransaction action="commit">
</cftransaction>
<cfquery name="selepasCommit" datasource="kedai">
SELECT COUNT(*) AS n FROM produk WHERE id = 931
</cfquery>
<cftransaction action="begin">
<cfquery datasource="kedai">
INSERT INTO produk (id, nama, harga, stok) VALUES (932, 'Dibatalkan', 2.50, 1)
</cfquery>
<cftransaction action="rollback">
</cftransaction>
<cfquery name="selepasRollback" datasource="kedai">
SELECT COUNT(*) AS n FROM produk WHERE id = 932
</cfquery>
<cfoutput>Selepas commit: #selepasCommit.n# | Selepas rollback: #selepasRollback.n#</cfoutput>
Outputnya ialah Selepas commit: 1 | Selepas rollback: 0. Baris 931 kekal kerana dicommit, baris 932 hilang kerana dirollback. action="commit" dan action="rollback" ditulis sebagai tag tutup sendiri tanpa badan.
Corak try dan catch yang betul
Dalam kod sebenar, anda jarang menulis action="rollback" secara terbuka. Sebaliknya anda membungkus kerja dalam try dan catch, dan rollback di dalam catch. Bentuk skrip transaction { } membuka dan menutup transaksi secara automatik, dan transactionRollback() membatalkannya secara eksplisit.
try {
transaction {
queryExecute("INSERT INTO produk (id, nama, harga, stok) VALUES (933, 'Pesanan', 20.00, 2)", [], { datasource = "kedai" });
queryExecute("UPDATE produk SET stok = stok - 99 WHERE id = 1", [], { datasource = "kedai" });
stok = queryExecute("SELECT stok FROM produk WHERE id = 1", [], { datasource = "kedai" }).stok;
if (stok < 0) {
throw(message = "Stok tidak mencukupi");
}
}
writeOutput("Komited");
}
catch (any e) {
transactionRollback();
writeLog(text = e.message, type = "error");
writeOutput("Dibatalkan: " & e.message);
}
Contoh ini sengaja menolak 99 unit daripada stok yang hanya 10 unit, jadi pemeriksaan stok < 0 membaling ralat. Blok catch memanggil transactionRollback() dan merekod ralat. Selepas ia selesai, stok kembali kepada 10 dan baris 933 tidak wujud, membuktikan kedua-dua pertanyaan dibatalkan bersama.
Peraturannya ada dua. Pertama, rollback mesti berlaku di dalam catch, bukan selepasnya, kerana sebaik sahaja ralat melompat keluar dari blok transaction, keadaan transaksi menjadi tidak menentu. Kedua, jangan telan ralat: catch kosong yang menutup ralat tanpa merekod atau membaling semula menyembunyikan kegagalan sebenar. Paling kurang tulis ke log seperti di atas, atau guna rethrow jika pemanggil perlu tahu operasi itu gagal.
Satu unit kerja, satu transaksi
Kesilapan paling kerap ialah membungkus hanya sebahagian pertanyaan. Transaksi mesti membalut semua cfquery yang membentuk satu unit kerja. Jika anda menolak stok di dalam transaksi tetapi menulis rekod audit di luar, anda mencipta dua unit yang boleh menyimpang: transaksi berjaya dicommit manakala audit gagal, atau sebaliknya.
Peraturannya mudah: jika dua pertanyaan mesti berjaya atau gagal bersama, kedua-duanya berada dalam blok transaction yang sama. Pastikan juga setiap nilai daripada input pengguna melewati cfqueryparam, seperti dalam cfquery dan cfqueryparam untuk elak SQL injection.
Transaksi bersarang dan savepoint
Transaksi bersarang ialah transaksi di dalam transaksi. Dalam Lucee, transaksi bersarang tidak memberikan rollback dalaman yang bebas secara automatik: jika bahagian dalam membatalkan diri, keseluruhan transaksi ditandakan untuk dibatalkan. Untuk pembatalan separa, guna savepoint.
transaction {
queryExecute("INSERT INTO produk (id, nama, harga, stok) VALUES (934, 'Luar', 1.00, 1)", [], { datasource = "kedai" });
transactionSetSavepoint("sebelumDalam");
queryExecute("INSERT INTO produk (id, nama, harga, stok) VALUES (935, 'Dalam', 2.00, 1)", [], { datasource = "kedai" });
transactionRollback("sebelumDalam");
transactionCommit();
}
q = queryExecute("SELECT nama FROM produk WHERE id IN (934, 935) ORDER BY id", [], { datasource = "kedai" });
writeOutput("Kekal: " & valueList(q.nama));
transactionSetSavepoint("sebelumDalam") menandakan satu titik dalam transaksi, dan transactionRollback("sebelumDalam") membatalkan semua perubahan selepas titik itu sahaja. Output Kekal: Luar menunjukkan baris 934 dikekalkan manakala 935 dibatalkan, kemudian transactionCommit() menyimpan baris luar itu. Bentuk tag yang setara ialah action="setsavepoint" dengan atribut savepoint, dan action="rollback" dengan atribut savepoint yang sama.
Tahap pengasingan dan bacaan kotor
Tahap pengasingan menentukan sejauh mana satu transaksi melihat kerja transaksi lain yang belum dicommit. Lucee menerima empat nilai pada atribut isolation: read_uncommitted, read_committed, repeatable_read dan serializable.
Bacaan kotor berlaku apabila satu transaksi membaca data yang ditulis oleh transaksi lain yang kemudiannya dirollback. read_uncommitted membenarkan ini, read_committed menghalangnya. Nilai lalai kebanyakan pangkalan data, termasuk H2 dan MySQL, ialah read_committed. Naikkan kepada repeatable_read atau serializable hanya apabila logik memerlukan jaminan lebih kuat, contohnya laporan yang mesti melihat satu snapshot konsisten, melalui transaction isolation = "serializable" dalam skrip atau atribut isolation pada tag. Atribut ini disahkan pada pangkalan data: jika pemacu tidak menyokong tahap itu, enjin melaporkan ralat. Tahap lebih tinggi bermakna lebih banyak kunci dan persaingan, jadi jangan menaikkannya tanpa sebab. Untuk asas sumber data, lihat konfigurasi datasource dalam Lucee.
Deadlock dan percubaan semula
Deadlock berlaku apabila dua transaksi memegang kunci yang masing-masing perlukan dan kedua-duanya menunggu selama-lamanya. Pangkalan data menyelesaikannya dengan memilih satu mangsa dan membatalkan transaksinya. Ralat ini biasanya muncul semasa commit, bukan semasa pertanyaan individu. Cara bertahan yang betul ialah menangkap ralat, rollback, dan cuba semula selepas jeda singkat.
function dalamTransaksi(required function kerja, numeric percubaanMaks = 3) {
var cubaan = 0;
while (true) {
cubaan++;
try {
transaction { kerja(); }
return cubaan;
}
catch (any e) {
if (cubaan >= percubaanMaks || findNoCase("deadlock", e.message) == 0) {
rethrow;
}
sleep(150 * cubaan);
}
}
}
cubaanSemasa = 0;
hasil = dalamTransaksi(function() {
cubaanSemasa++;
if (cubaanSemasa < 3) {
throw(message = "deadlock dikesan");
}
queryExecute("INSERT INTO produk (id, nama, harga, stok) VALUES (936, 'Berjaya', 3.00, 1)", [], { datasource = "kedai" });
});
writeOutput("Berjaya pada cubaan " & hasil);
Fungsi ini mencuba sehingga tiga kali dengan jeda yang bertambah, dan hanya mencuba semula apabila mesej mengandungi deadlock. Ralat lain dibaling semula dengan rethrow. Dalam ujian, kerja yang mensimulasikan dua deadlock berjaya pada cubaan ketiga. Jeda itu penting kerana ia memberi masa kepada transaksi yang bertanding untuk selesai, dan ia berkait dengan konkurensi dengan cfthread.
DDL dan commit tersirat
Satu perangkap yang kerap memerangkap pembangun ialah DDL, iaitu pernyataan yang mengubah struktur seperti CREATE TABLE dan ALTER TABLE. Dalam MySQL, DDL menyebabkan commit tersirat: transaksi yang sedang berjalan dicommit secara automatik sebelum DDL dijalankan, tanpa mengira action="rollback" yang anda tulis selepasnya.
Ujian pada rig Lucee dengan H2 menunjukkan tingkah laku yang sama: satu transaksi yang memasukkan baris, menjalankan CREATE TABLE, kemudian rollback, tetap mengekalkan baris itu kerana CREATE TABLE menutup transaksi dahulu. Implikasinya jelas: jangan campurkan DDL dengan DML dalam satu transaksi jika anda menjangkakan rollback membatalkan semuanya. Buat perubahan struktur dalam langkah berasingan, dan jangan jadikan ia bergantung kepada rollback.
Kerja panjang menyekat pengguna lain
Transaksi memegang kunci pada baris dan jadual sehingga ia dicommit atau dirollback. Semakin lama transaksi berjalan, semakin lama kunci dipegang dan semakin ramai pengguna menunggu. Sebab itu kerja panjang tidak sepatutnya berada di dalam transaksi.
Jangan letakkan panggilan rangkaian, pengiraan berat, atau menunggu input pengguna di antara pertanyaan. Buat semua kerja perlahan itu sebelum atau selepas transaksi, dan biarkan dalam transaksi hanya pertanyaan pantas. Ini juga berkaitan dengan caching untuk prestasi: data yang boleh dibaca dari cache tidak perlu dibaca semula dalam transaksi. Prinsip yang sama terpakai pada titik akhir API yang menggabungkan beberapa pertanyaan untuk satu respons: bungkus semuanya dalam satu transaksi supaya respons tidak dibina daripada data separuh, seperti dalam membina REST API dalam Lucee.
Jadual masalah transaksi dan penyelesaian
| Masalah | Penyelesaian |
|---|---|
| Data separuh siap, sebahagian pertanyaan berjaya dan sebahagian gagal | Bungkus semua pertanyaan dalam satu cftransaction, dengan rollback dalam catch |
| Deadlock yang membatalkan transaksi secara rawak | Tangkap ralat, rollback, dan cuba semula selepas jeda singkat |
| Transaksi bersarang yang perlu membatalkan sebahagian sahaja | Guna savepoint dengan transactionSetSavepoint dan transactionRollback(nama) |
| Sambungan terputus di tengah transaksi | Biarkan transaksi gagal dan dibatalkan oleh enjin, kemudian cuba semula keseluruhan unit kerja |
Soalan Lazim
Apakah perbezaan antara commit dan rollback?
Commit menyimpan semua perubahan dalam transaksi secara kekal, manakala rollback membuang semuanya dan mengembalikan pangkalan data ke keadaan sebelum transaksi bermula.
Adakah saya perlu menulis action="rollback" secara manual setiap kali?
Tidak semestinya. Dalam bentuk skrip, blok transaction { } membatalkan diri secara automatik apabila ralat melompat keluar. Panggilan transactionRollback() dalam catch ialah pertahanan tambahan yang menjadikan niat anda jelas.
Bolehkah saya meletakkan kerja perlahan seperti panggilan HTTP di dalam transaksi?
Tidak digalakkan. Kerja perlahan memegang kunci pangkalan data lebih lama dan menyekat pengguna lain. Lakukan kerja itu sebelum atau selepas transaksi, dan biarkan dalam transaksi hanya pertanyaan pantas.
Kenapa CREATE TABLE saya membuat rollback gagal berfungsi?
DDL seperti CREATE TABLE menyebabkan commit tersirat dalam MySQL, dan H2 menunjukkan tingkah laku yang sama. Transaksi ditutup sebelum rollback dijalankan, jadi perubahan data sebelum DDL itu kekal. Asingkan perubahan struktur daripada transaksi data.
Bagaimana saya tahu sama ada ralat itu deadlock atau kegagalan biasa?
Semak mesej ralat untuk perkataan deadlock, seperti dalam fungsi dalamTransaksi. Ralat deadlock biasanya muncul semasa commit, bukan semasa pertanyaan individu, jadi periksa mesej sebelum menganggap setiap ralat boleh dicuba semula.