Ketika satu berkas hanya disentuh oleh satu proses, urutan baca-tulis selalu tertata rapi. Masalah muncul begitu beberapa proses menulis ke berkas yang sama pada waktu yang hampir bersamaan: dua worker membaca nilai counter yang identik, dua-duanya menambah satu, lalu dua-duanya menulis hasil yang sama — satu pembaruan hilang tanpa jejak. Chapter append dan file locking
pernah menyinggung flock() sebagai solusi singkat; pada chapter ini kita membedahnya tuntas: mode-mode kunci, perilaku blocking, dan — sama pentingnya — batas perlindungannya.
Cara Kerja flock #
flock() memasang kunci yang melekat pada handle berkas terbuka, bukan pada nama berkas di disk. Sintaksnya adalah flock(resource $stream, int $operation, int &$wouldBlock = null): bool, dengan $operation dibentuk dari kombinasi konstanta berikut:
| Konstanta | Peran |
|---|---|
LOCK_SH | Kunci bersama (shared) — untuk membaca; boleh dipegang banyak handle sekaligus |
LOCK_EX | Kunci eksklusif (exclusive) — untuk menulis; hanya boleh satu pemegang |
LOCK_UN | Melepas kunci yang sedang dipegang |
LOCK_NB | Pengubah yang di-OR-kan — jangan menunggu, langsung gagal bila kunci diperebutkan |
Dua aturan dasar mengatur masa hidup kunci: kunci otomatis dilepas saat handle ditutup dengan fclose(), dan kunci ikut hilang saat proses berakhir. Anda tetap disarankan melepas kunci secara eksplisit dengan LOCK_UN segera setelah selesai menulis agar proses lain tidak menunggu lebih lama dari yang diperlukan.
Pola Dasar Tulis dengan LOCK_EX #
Pola paling dasar dari file locking terdiri dari empat langkah: buka berkas, kunci eksklusif, tulis, lalu lepas kunci dan tutup handle. Contoh berikut menjalankan pola tersebut utuh:
<?php
$file = sys_get_temp_dir() . '/kursus-php-lock-demo.txt';
@unlink($file); // mulai dari kondisi bersih
$handle = fopen($file, 'c');
if (!$handle) {
exit("Gagal membuka berkas\n");
}
if (flock($handle, LOCK_EX)) {
fwrite($handle, "Data penting dari proses pertama\n");
fflush($handle);
flock($handle, LOCK_UN);
echo "Penulisan selesai\n";
} else {
echo "Gagal mengunci berkas\n";
}
fclose($handle);
echo "Isi akhir : " . trim(file_get_contents($file)) . "\n";
unlink($file);Output yang Dihasilkan:
Penulisan selesai
Isi akhir : Data penting dari proses pertamaMari kita bedah baris-baris kuncinya:
@unlink($file)menghapus sisa eksekusi sebelumnya sehingga contoh selalu mulai dari keadaan kosong.- Mode
'c'membuka berkas untuk ditulis dan membuatnya bila belum ada, tanpa memotong isi yang sudah ada — mode yang pas untuk siklus kunci-tulis-lepas. flock($handle, LOCK_EX)menunggu sampai tidak ada proses lain yang memegang kunci apa pun pada berkas itu, lalu memasang kunci eksklusif milik kita.fflush($handle)memaksa isi buffer tulis PHP benar-benar sampai ke disk sebelum kunci dilepas — tanpa ini, proses lain bisa memperoleh kunci dan membaca data yang sebenarnya belum selesai ditulis.flock($handle, LOCK_UN)lalufclose($handle)melepas kunci secara eksplisit dan menutup handle.
Kunci Bersama vs Kunci Eksklusif #
Hubungan kedua jenis kunci dirangkum pada tabel berikut. Baris adalah kunci yang sedang dipegang seseorang, kolom adalah permintaan baru yang datang:
| Kunci yang dipegang | Permintaan LOCK_SH baru | Permintaan LOCK_EX baru |
|---|---|---|
| Tidak ada | langsung berhasil | langsung berhasil |
LOCK_SH oleh satu atau lebih pembaca | langsung berhasil | menunggu semua pembaca selesai |
LOCK_EX oleh satu penulis | menunggu | menunggu |
Aturan singkatnya: pembaca boleh beramai-ramai, penulis harus sendirian. Contoh berikut membuktikan tabel itu di dalam satu skrip — kita membuka dua handle berbeda pada berkas yang sama, sehingga keduanya berperilaku seperti dua proses terpisah yang memperebutkan kunci:
<?php
$file = sys_get_temp_dir() . '/kursus-php-lock-nb.txt';
file_put_contents($file, "isi demo\n");
$pembaca = fopen($file, 'r');
flock($pembaca, LOCK_SH); // pembaca memegang kunci bersama
$penulis = fopen($file, 'c');
$wouldBlock = 0;
$berhasil = flock($penulis, LOCK_EX | LOCK_NB, $wouldBlock);
echo "LOCK_EX saat ada pembaca : " . var_export($berhasil, true) . "\n";
echo "Parameter wouldBlock : $wouldBlock\n";
fclose($pembaca); // otomatis melepas kunci baca
$berhasil = flock($penulis, LOCK_EX | LOCK_NB);
echo "LOCK_EX setelah pembaca lepas : " . var_export($berhasil, true) . "\n";
flock($penulis, LOCK_UN);
fclose($penulis);
unlink($file);Output yang Dihasilkan:
LOCK_EX saat ada pembaca : false
Parameter wouldBlock : 1
LOCK_EX setelah pembaca lepas : trueAlur skripnya: pertama, LOCK_EX | LOCK_NB pada handle $penulis gagal (false) karena handle $pembaca masih memegang LOCK_SH, dan parameter ketiga $wouldBlock terisi 1 yang menandakan kegagalan disebabkan kunci yang sedang diperebutkan — bukan oleh error lain. Setelah $pembaca ditutup, permintaan kunci yang sama langsung berhasil. Perhatikan juga bahwa memanggil flock() dengan LOCK_NB saat kunci diperebutkan tidak memicu warning; nilai kembali false memang jalur normal untuk kondisi ini.
Blocking vs Non-Blocking #
Tanpa LOCK_NB, pemanggilan flock() bersifat blocking: skrip berhenti di baris itu sampai kunci tersedia. Untuk worker CLI yang memang harus mengantri menulis log atau berkas data, blocking justru perilaku yang diinginkan — kodenya sederhana dan urutan penulisan terjaga.
Di lingkungan web, blocking bisa berbahaya. Ketika satu request memegang kunci lama, request lain menumpuk menunggu dan menghabiskan slot proses server. Dengan LOCK_NB, pemanggilan langsung kembali dengan false sehingga kode bisa memilih jalur alternatif: menampilkan pesan “coba lagi”, atau melewati penulisan sama sekali — pola coba-kunci-lalu-minggir inilah yang diperagakan pada contoh sebelumnya.
Batas Perlindungan flock #
flock() adalah kunci advisory: sistem operasi hanya mencegat proses yang juga memanggil flock() pada berkas yang sama. Kode yang membaca atau menulis tanpa mengunci — misalnya file_get_contents() polos, editor teks, atau skrip pihak ketiga — berjalan bebas tanpa peduli kunci kita. Karena itu, kontrak locking harus diikuti oleh setiap pihak yang menyentuh berkas tersebut.
Yang lebih mengejutkan bagi pemula: flock() juga tidak melindungi berkas dari unlink() dan rename(), karena kunci melekat pada handle yang terbuka, bukan pada nama berkas:
<?php
$file = sys_get_temp_dir() . '/kursus-php-lock-unlink.txt';
file_put_contents($file, 'isi lama');
$handle = fopen($file, 'r');
flock($handle, LOCK_EX);
echo 'unlink saat terkunci : ' . var_export(unlink($file), true) . "\n";
echo 'handle masih terbaca : ' . var_export(fread($handle, 8), true) . "\n";
flock($handle, LOCK_UN);
fclose($handle);Output yang Dihasilkan:
unlink saat terkunci : true
handle masih terbaca : 'isi lama'Meskipun $handle memegang LOCK_EX, unlink() tetap berhasil menghapus nama berkas dari direktori — sementara handle yang sudah terbuka masih bisa membaca isi lamanya sampai ditutup. Pola seperti ini (buka, kunci, lalu hapus) justru dipakai secara sengaja pada berkas sementara, tetapi kalau dilakukan proses lain di luar koordinasi, penulis yang berpegang pada nama berkas bisa tiba-tiba menulis ke berkas baru yang berbeda dari yang dikuncinya. Untuk berkas yang diperebutkan, hindari mengganti namanya selama siklus kunci berlangsung.
Pola Praktis Counter File Atomik #
Sebagai penutup, berikut pola yang menyatukan seluruh materi chapter ini: counter file yang aman diperebutkan. Seluruh siklus baca-tulisnya berada di bawah satu kunci eksklusif, sehingga dua proses yang memanggilnya bersamaan tidak akan kehilangan pembaruan:
<?php
function tambahCounter(string $file): int
{
$handle = fopen($file, 'c+');
flock($handle, LOCK_EX);
$nilai = (int) stream_get_contents($handle);
$nilai++;
ftruncate($handle, 0);
rewind($handle);
fwrite($handle, (string) $nilai);
fflush($handle);
flock($handle, LOCK_UN);
fclose($handle);
return $nilai;
}
$file = sys_get_temp_dir() . '/kursus-php-counter.txt';
@unlink($file);
for ($i = 1; $i <= 3; $i++) {
echo "Setelah pemanggilan ke-$i: " . tambahCounter($file) . "\n";
}
unlink($file);Output yang Dihasilkan:
Setelah pemanggilan ke-1: 1
Setelah pemanggilan ke-2: 2
Setelah pemanggilan ke-3: 3Bagian terpentingnya adalah urutan operasi setelah kunci terpasang: stream_get_contents() membaca nilai lama langsung dari handle yang sama, ftruncate() mengosongkan berkas, rewind() memundarkan pointer ke awal sebelum menulis nilai baru, dan fflush() memastikan data sampai ke disk sebelum kunci dilepas. Pada aplikasi nyata, tiga pemanggilan itu bisa datang dari tiga request web atau tiga worker yang berbeda — pola kuncinya tetap sama, dan itulah yang menjadikan penghitung ini atomik.
Ringkasan #
flock() mengoordinasikan akses antar proses lewat kunci advisory yang melekat pada handle: LOCK_SH untuk pembaca yang berbagi, LOCK_EX untuk penulis yang harus sendirian, LOCK_NB untuk menolak menunggu, dan flock() tidak melindungi apa pun dari kode yang tidak ikut mengunci maupun dari unlink() dan rename(). Kunci menjaga integritas data dari tabrakan antar proses — tetapi ia sama sekali tidak melindungi dari masukan jahat. Chapter berikutnya beralih ke sisi itu: keamanan filesystem, mulai dari serangan path traversal hingga penyimpanan upload yang aman.