cflock dan keadaan perlumbaan dalam CFML: cara mengunci dengan betul
Dua permintaan menulis serentak boleh hilangkan ribuan kiraan tanpa ralat. Panduan ini menunjukkan demo di rig, kunci exclusive, readonly dan timeout cflock.

Kandungan
- cflock dan keadaan perlumbaan dalam CFML: cara mengunci dengan betul
- Apa itu keadaan perlumbaan dalam aplikasi CFML?
- Kenapa pembilang tanpa kunci hilang ribuan kiraan?
- Bagaimana cflock menutup jurang itu?
- Apa beza kunci exclusive dan readonly?
- Bila guna scope dan bila guna name?
- Bagaimana mengunci satu barang sahaja?
- Kenapa cfcatch type="LockTimeout" tidak menangkap ralat kunci?
- Bila cflock tidak mencukupi?
- Adakah cflock menggantikan transaksi pangkalan data?
- Bila anda tidak perlu mengunci?
- Jadual ringkasan
- Senarai semak sebelum menambah kunci
- Soalan Lazim (FAQ)
- Sumber
cflock dan keadaan perlumbaan dalam CFML: cara mengunci dengan betul
Jawapan pantas: Keadaan perlumbaan berlaku apabila dua permintaan membaca dan menulis satu nilai yang sama secara serentak, jadi satu tulisan menimpa tulisan yang lain. cflock menutup jurang itu dengan memberi satu permintaan akses eksklusif pada satu masa, manakala type="readonly" membenarkan pembaca berjalan serentak. Pada rig Lucee 7.1.1.17, pembilang tanpa kunci hilang antara 5,000 hingga 13,000 daripada 16,000 tambahan, dan jumlahnya tepat 16,000 apabila kunci dipasang.
Apa itu keadaan perlumbaan dalam aplikasi CFML?
Keadaan perlumbaan ialah pepijat masa, bukan pepijat logik: kodnya betul baris demi baris, tetapi hasilnya bergantung pada permintaan mana yang sampai dahulu. Dalam CFML setiap permintaan berjalan dalam utasnya sendiri di dalam satu JVM, dan skop application, server serta session dikongsi antara utas itu.
Pola yang paling kerap rosak ialah baca, ubah, tulis: stok berkurang, pembilang bertambah, atau nombor dokumen diambil daripada pembilang yang sama. Dua permintaan boleh membaca nilai 10 serentak dan kedua-duanya menulis 9. Tiada ralat dan tiada entri log, jadi ia baru kelihatan apabila trafik naik.
Kenapa pembilang tanpa kunci hilang ribuan kiraan?
Halaman di bawah menjalankan lapan utas, setiap satu menambah application.kira sebanyak 2,000 kali. Jumlah sepatutnya 16,000.
<cfscript>
if (!structKeyExists(application, "kira")) {
application.kira = 0;
}
application.kira = 0;
bilUtas = 8;
bilTambahan = 2000;
</cfscript>
<cfloop from="1" to="#bilUtas#" index="i">
<cfthread action="run" name="t#i#">
<cfloop from="1" to="#bilTambahan#" index="j">
<cfset application.kira = application.kira + 1>
</cfloop>
</cfthread>
</cfloop>
<cfthread action="join" name="t1,t2,t3,t4,t5,t6,t7,t8" timeout="120000" />
<cfset jangka = bilUtas * bilTambahan>
<cfoutput>
Dijangka: #jangka#
Sebenar: #application.kira#
Hilang: #jangka - application.kira#
</cfoutput>
Empat larian berturut-turut pada rig yang sama:
Sebenar: 2243 Hilang: 13757
Sebenar: 8056 Hilang: 7944
Sebenar: 6275 Hilang: 9725
Sebenar: 6757 Hilang: 9243
Nombornya berubah setiap larian kerana bergantung pada masa utas bertindih, tetapi coraknya kekal: ribuan tambahan hilang, halaman membalas 200, dan tiada ralat dicatat. Setiap kali dua utas membaca nilai yang sama sebelum salah satunya menulis, satu tambahan lenyap.
Bagaimana cflock menutup jurang itu?
cflock menjadikan blok di dalamnya dijalankan oleh satu utas pada satu masa untuk nama kunci yang sama. Utas lain menunggu sehingga giliran mereka, jadi baca, ubah dan tulis menjadi satu langkah yang tidak boleh dipotong.
<cfscript>
if (!structKeyExists(application, "kira2")) {
application.kira2 = 0;
}
application.kira2 = 0;
bilUtas = 8;
bilTambahan = 2000;
</cfscript>
<cfloop from="1" to="#bilUtas#" index="i">
<cfthread action="run" name="k#i#">
<cfloop from="1" to="#bilTambahan#" index="j">
<cflock name="kiraApl" type="exclusive" timeout="30">
<cfset application.kira2 = application.kira2 + 1>
</cflock>
</cfloop>
</cfthread>
</cfloop>
<cfthread action="join" name="k1,k2,k3,k4,k5,k6,k7,k8" timeout="120000" />
<cfset jangka = bilUtas * bilTambahan>
<cfoutput>
Dijangka: #jangka#
Sebenar: #application.kira2#
Hilang: #jangka - application.kira2#
</cfoutput>
Keputusan sebenar pada larian yang sama:
Dijangka: 16000
Sebenar: 16000
Hilang: 0
Harganya ialah masa: 0.040 saat tanpa kunci berbanding 0.198 saat berkunci, kira-kira lima kali lebih perlahan. Itu sebab untuk mengunci sesedikit dan sesingkat mungkin.
Apa beza kunci exclusive dan readonly?
type="exclusive" memberi akses tunggal dan itulah yang anda perlukan untuk sebarang tulisan. type="readonly" membenarkan beberapa pembaca masuk serentak, tetapi ia hanya berguna apabila setiap penulis juga meminta kunci exclusive pada nama yang sama.
Perbezaannya boleh diukur. Dua utas yang masing-masing memegang kunci dan tidur 1,500 milisaat memberi keputusan ini pada dua larian:
readonly serentak: 1502 ms | exclusive serentak: 3003 ms
Dengan readonly, dua utas itu berjalan serentak, jadi jumlah masa hampir 1,500 milisaat. Dengan exclusive, yang kedua menunggu, jadi jumlahnya 3,000 milisaat. Itu juga amaran: readonly pada kod yang menulis data tidak melindungi apa-apa.
| Jenis kunci | Siapa boleh masuk | Bila guna |
|---|---|---|
exclusive | Satu utas sahaja | Menulis nilai dikongsi: pembilang, stok, senarai dalam application |
readonly | Ramai pembaca serentak | Membaca struktur besar yang jarang berubah |
Bila guna scope dan bila guna name?
Dokumentasi Lucee menyatakan scope dan name saling eksklusif, jadi pilih satu sahaja. scope menerima Application, Server atau Session dan mengunci skop itu secara keseluruhan, manakala name memberi kunci bernama yang boleh dikongsi antara bahagian berbeza aplikasi.
Dokumentasi yang sama menambah: nama kunci global kepada satu pelayan, dikongsi antara aplikasi dan sesi pengguna, tetapi tidak merentasi pelayan dalam kluster. Untuk data aplikasi, name lebih baik kerana ia membolehkan anda mengunci hanya bahagian yang terlibat.
Bagaimana mengunci satu barang sahaja?
Kunci bernama membolehkan penguncian halus. Jika setiap barang mempunyai kuncinya sendiri, permintaan untuk barang berbeza tidak menunggu, sedangkan permintaan untuk barang yang sama tetap beratur. Contoh ini mengurangkan stok satu barang sebanyak 2,000 kali melalui empat utas.
<cfscript>
application.stok = { A1: 5000 };
jumlah = 4;
kali = 500;
for (i = 1; i <= jumlah; i++) {
thread action="run" name="s" & i {
for (j = 1; j <= kali; j++) {
lock name="stok-A1" type="exclusive" timeout="10" {
application.stok.A1 = application.stok.A1 - 1;
}
}
}
}
thread action="join" name="s1,s2,s3,s4" timeout="60000";
jangka = 5000 - (jumlah * kali);
writeOutput("Stok akhir: " & application.stok.A1 & " | dijangka: " & jangka);
</cfscript>
Stok akhir: 3000 | dijangka: 3000
Perhatikan cara nama dibina: name="s" & i dan name="stok-A1". Di dalam <cfscript> tanda pagar tidak diinterpolasi di dalam rentetan, jadi name="stok-#idBarang#" menjadi teks harfiah dan semua permintaan berkongsi satu kunci yang salah. Bina nama kunci dengan sambungan.
Kenapa cfcatch type="LockTimeout" tidak menangkap ralat kunci?
Kerana jenis tangkapan itu tidak wujud dalam Lucee. Apabila throwOnTimeout="true" dan masa menunggu habis, enjin membaling ralat dengan jenis lock. Menulis type="LockTimeout" menghasilkan blok cfcatch yang tidak pernah padan, jadi ralat itu naik ke halaman dan pelawat melihat ralat 500.
<cfscript>
application.kunciUji4 = 0;
</cfscript>
<cfthread action="run" name="pemegang4">
<cflock name="kunciLambat4" type="exclusive" timeout="10">
<cfset sleep(2500)>
</cflock>
</cfthread>
<cfset sleep(200)>
<cftry>
<cflock name="kunciLambat4" type="exclusive" timeout="1" throwOnTimeout="true">
<cfset application.kunciUji4 = 1>
</cflock>
<cfset mesej4 = "kunci diperoleh">
<cfcatch type="lock">
<cfset mesej4 = "tamat masa: " & cfcatch.message>
</cfcatch>
</cftry>
<cfthread action="join" name="pemegang4" timeout="20000" />
<cfoutput>#mesej4#</cfoutput>
Halaman itu memulangkan teks ini dengan kod HTTP 200:
tamat masa: a timeout occurred after 1 second trying to acquire a exclusive lock with name [kunciLambat4].
Masa menunggu juga bukan hiasan. Tanpa timeout, permintaan boleh menunggu sehingga had masa pelayan, dan itu memakan utas yang sepatutnya melayani permintaan lain.
Bila cflock tidak mencukupi?
Kunci Lucee hidup di dalam satu JVM, jadi ia tidak menyelaraskan dua pelayan yang berkongsi pangkalan data, dan ia tidak melindungi data daripada aplikasi lain yang menulis ke pangkalan data yang sama. Untuk kes itu, kunci mesti berada di tempat data itu sendiri:
- Transaksi pangkalan data untuk kerja yang melibatkan beberapa baris atau jadual.
- Kemas kini atom seperti
UPDATE produk SET stok = stok - 1 WHERE id = 1 AND stok > 0. - Kunci baris seperti
SELECT ... FOR UPDATEapabila anda perlu membaca nilai dahulu.
Bahagian pentingnya ialah memilih satu tempat untuk menyelaraskan, bukan mencampur dua: kunci aplikasi di atas transaksi selalunya menambah risiko.
Adakah cflock menggantikan transaksi pangkalan data?
Tidak. cflock menyelaraskan utas di dalam satu aplikasi, manakala transaksi menyelaraskan perubahan data dan membenarkan pembatalan jika satu langkah gagal. Pertanyaan yang menulis dua jadual masih perlukan transaksi walaupun dibalut cflock, dan transaksi tidak melindungi pemboleh ubah dalam skop application.
Mulakan dengan cftransaction: transaksi, commit dan rollback, kemudian tahap pengasingan MySQL dalam CFML.
Bila anda tidak perlu mengunci?
Kunci yang tidak perlu memperlahankan aplikasi tanpa memberi apa-apa. Tiga keadaan yang biasanya selamat tanpa cflock:
- Data hanya milik satu permintaan, jadi tiada utas lain boleh menyentuhnya.
- Pangkalan data sudah menyelesaikan urutan melalui satu
UPDATEatom atau kekanganUNIQUE. - Nilai tidak penting jika tersasar sedikit, contohnya pembilang paparan untuk statistik.
Yang perlu dikunci ialah data yang salahnya menyebabkan kerugian: stok, baki, dan nombor dokumen. Untuk kerja mahal yang jarang berubah, cache query dengan cachedWithin selalunya menyelesaikan beban tanpa menambah kunci.
Jadual ringkasan
| Keadaan | Punca | Pembetulan |
|---|---|---|
| Pembilang hilang kiraan | Baca dan tulis tidak atom | cflock jenis exclusive di sekeliling baca, ubah, tulis |
| Stok menjadi negatif | Dua permintaan lulus semakan yang sama | Kunci bernama per barang, atau UPDATE ... WHERE stok > 0 |
| Kunci tidak berkesan | Nama kunci berbeza setiap permintaan | Bina nama dengan sambungan, bukan tanda pagar dalam rentetan |
| Ralat 500 semasa menunggu kunci | cfcatch jenis salah | <cfcatch type="lock"> |
| Dua pelayan tidak selaras | Kunci hanya dalam satu JVM | Kunci pangkalan data atau transaksi |
Senarai semak sebelum menambah kunci
- Pastikan setiap penulis bagi data itu melalui kunci yang sama.
- Pilih nama kunci yang stabil dan halus, contohnya
stok-A1, bukan satu nama untuk seluruh aplikasi. - Tetapkan
timeoutdan tangani<cfcatch type="lock">. - Kekalkan blok berkunci sekecil mungkin, tanpa panggilan HTTP atau pertanyaan perlahan.
- Jika dua kunci diperlukan, ambil dalam susunan yang sama untuk mengelak kebuntuan.
- Uji dengan utas serentak, bukan dengan satu permintaan, kerana satu permintaan tidak akan menunjukkan pepijat ini. TestBox berguna untuk mengulang ujian itu dalam saluran integrasi. Untuk sesi pengguna, baca pengurusan sesi dalam Lucee dahulu.
Soalan Lazim (FAQ)
Adakah cflock memperlahankan setiap permintaan? Hanya permintaan yang melalui blok berkunci. Pada rig ini, 16,000 operasi berkunci mengambil 0.198 saat berbanding 0.040 saat tanpa kunci, jadi kosnya kecil selagi blok itu pendek.
Kenapa kiraan saya masih hilang walaupun sudah guna cflock? Biasanya kerana nama kunci berbeza antara permintaan, atau sebahagian penulis tidak melalui kunci itu.
Adakah type="readonly" cukup untuk data yang jarang berubah?
Cukup selagi penulis juga memakai exclusive pada nama yang sama. Jika ada penulis tanpa kunci, pembaca boleh melihat nilai separuh siap.
Bolehkah cflock melindungi data antara dua pelayan? Tidak. Kunci itu hidup dalam satu JVM, jadi dua pelayan tidak berkongsi kunci yang sama. Gunakan kunci pangkalan data atau transaksi.
Bila saya patut tinggalkan cflock dan guna pangkalan data sahaja?
Apabila satu arahan atom boleh menyelesaikan kerja itu, contohnya UPDATE produk SET stok = stok - 1 WHERE id = 1 AND stok > 0.
Sumber
- Dokumentasi tag
cflockLucee, termasuk kenyataan bahawa nama kunci global kepada satu pelayan dan tidak merentasi pelayan kluster: docs.lucee.org/reference/tags/lock.html - Dokumentasi tag
cfthreadLucee untukaction="run"danaction="join"yang digunakan dalam contoh: docs.lucee.org/reference/tags/thread.html - Semua angka diukur pada rig ujian Lucee 7.1.1.17 dengan halaman
.cfmyang dijalankan sendiri.