Menghindari Duplikasi dan Race Condition Saat Batch Import di Aplikasi WMS
Saat kita bekerja dengan sistem Warehouse Management System (WMS), salah satu tantangan yang sering muncul adalah bagaimana menjaga agar proses import data secara bersamaan oleh banyak user tidak menyebabkan duplikasi data di database. Apalagi kalau kita dealing dengan sesuatu yang kritikal seperti box code, yang harus benar-benar unik dan tidak boleh ganda.
Studi Kasus: Import XLSX Inbound
Misalnya, kita punya sebuah fitur import file .xlsx
yang berisi banyak box, produk, dan serial number. Bisa dibilang ini adalah proses batch import yang dilakukan oleh user.
Contoh data yang di-import:
CSVboxCode,productCode,serialNumber BOX001,P001,SN001 BOX001,P002,SN002 BOX002,P003,SN003
Masalah muncul ketika:
- Dua user atau dua thread mencoba meng-import data dengan
boxCode
yang sama. - Proses ini bisa berjalan bersamaan.
- Kalau tidak ada proteksi, maka dua-duanya bisa sukses menyimpan
BOX001
, dan kita punya duplikasi.
Solusi: Redis Lock per Box
Untuk menyelesaikan masalah ini, kita menggunakan Redis distributed lock (via Redisson). Idenya adalah:
- Ambil semua boxCode dari file import.
- Kunci masing-masing boxCode di Redis.
- Kalau gagal mengunci (karena sedang dikunci oleh proses lain), langsung throw error.
- Setelah proses selesai, unlock semua lock.
Implementasi di Spring Boot
JAVA@Transactional public List<Inbound> importXlsx(InboundBulkDTO body, User user) throws IOException { List<InboundImportXLSX> inboundImportXLSXList = this.extractXlsx(body); Set<String> boxCodes = new HashSet<>(inboundImportXLSXList.stream() .map(InboundImportXLSX::getBoxCode) .distinct() .toList()); Map<String, RLock> boxLocks = new HashMap<>(); try { for(String boxCode : boxCodes) { RLock boxLock = redissonClient.getLock( String.format("lock:box:%s:%s", body.getWarehouseId(), boxCode)); boolean isLocked = boxLock.tryLock(0, 30, TimeUnit.SECONDS); if(!isLocked) throw new BadRequestException(String.format("Box %s already on process!", boxCode)); boxLocks.put(boxCode, boxLock); } List<Inbound> inbounds = this.processImport(inboundImportXLSXList, user, body.getWarehouseId()); return inbounds; } catch (InterruptedException e) { throw new RuntimeException(e); } finally { boxLocks.values().forEach(boxLock -> { if (boxLock.isHeldByCurrentThread()) boxLock.unlock(); }); } }
Keuntungan Pendekatan Ini
✅ Thread-safe: Proses import oleh banyak user tetap aman.
✅ Atomic: Kalau gagal lock salah satu box, proses tidak dilanjutkan.
✅ Transactional: Dengan bantuan anotasi @Transactional
, proses rollback terjadi otomatis jika ada error.
✅ Reusable: Kunci berbasis Redis bisa dipakai untuk proses lain seperti inbound/outbound/pickpack, dll.
Catatan Tambahan
- Pastikan
tryLock
diberikan timeout supaya tidak deadlock. - Redis lock akan expired otomatis setelah waktu tertentu jika thread mati.
- Gunakan prefix kunci Redis yang jelas dan spesifik (
lock:box:warehouseId:boxCode
).
Penutup
Dengan pendekatan ini, proses import batch kita jadi jauh lebih solid. Redis di sini jadi seperti satpam virtual yang jaga supaya gak ada box yang diinput dua kali oleh user berbeda.
Kalau kamu kerja di sistem distribusi besar seperti WMS, trik ini wajib banget dikuasai!
"Better safe than sorry. Lock it before you drop it." 🚀