Hari ini saya menyarankan Anda untuk menganalisis satu tugas praktis tentang ras permintaan klien yang saya temui di MaximTelecom ketika mengembangkan back-end untuk aplikasi seluler kami MT_FREE.
Saat startup, aplikasi klien secara tidak sinkron mengirim "paket" permintaan ke API. Aplikasi memiliki pengidentifikasi clientId, yang memungkinkan untuk membedakan permintaan dari satu klien dengan yang lain. Untuk setiap permintaan di server, kode formulir dijalankan:
di mana entitas Klien memiliki bidang clientId, yang harus unik dan memiliki batasan unik dalam database untuk ini. Karena di Musim Semi setiap permintaan akan mengeksekusi kode ini di utas terpisah, bahkan jika ini adalah permintaan dari aplikasi klien yang sama, kesalahan formulir akan muncul:
pelanggaran integritas integritas: kendala unik atau pelanggaran indeks; Tabel UK_BFJDOY2DPUSSYLQ7G1S3S1TN8: CLIENT
Kesalahan terjadi karena alasan yang jelas: 2 atau lebih utas dengan clientId yang sama menerima klien == entitas nol dan mulai membuatnya, setelah itu mereka mendapatkan kesalahan saat melakukan.
Tantangan:
Adalah perlu untuk menyinkronkan permintaan dari satu clientId sehingga hanya permintaan pertama yang menyelesaikan pembuatan entitas Klien, dan sisanya akan diblokir pada saat pembuatan dan menerima objek yang sudah dibuat.
Solusi 1
Solusi ini berfungsi, tetapi sangat mahal, karena semua permintaan (utas) yang perlu dibuat diblokir, bahkan jika mereka menciptakan Klien dengan clientId yang berbeda dan tidak saling bersaing.
Harap perhatikan bahwa kombinasi sinkronisasi dengan anotasi @Transaksional
@Transactional public synchronized Client getOrCreateUser(String clientId){
kesalahan yang sama akan terjadi lagi. Alasannya adalah monitor (yang disinkronkan) dibebaskan terlebih dahulu dan utas berikutnya memasuki area yang disinkronkan, dan hanya setelah itu transaksi dilakukan oleh utas pertama pada objek proxy. Untuk mengatasi masalah ini sederhana - Anda perlu monitor dilepaskan setelah komit, oleh karena itu, disinkronkan harus disebut di atas:
synchronized (this){ client = clientService.getOrCreateUser(clientId); }
Keputusan 2
Saya benar-benar ingin menggunakan desain formulir:
synchronized (clientId)
tetapi masalahnya adalah objek clientId baru akan dibuat untuk setiap permintaan, bahkan jika nilainya setara, oleh karena itu, sinkronisasi tidak dapat dilakukan dengan cara ini. Untuk menyelesaikan masalah dengan objek clientId yang berbeda, Anda perlu menggunakan kumpulan:
Client client = clientRepository.findByClientId(clientId);
Solusi ini menggunakan kumpulan string java, masing-masing, permintaan dengan clientId yang setara, dengan memanggil clientId.intern (), akan menerima objek yang sama. Sayangnya, dalam praktiknya, solusi ini tidak berlaku, karena tidak mungkin untuk mengelola "membusuk" clientId, yang cepat atau lambat akan mengarah ke OutOfMemory.
Keputusan 3
Untuk menggunakan ReentrantLock, Anda memerlukan kumpulan formulir:
private final ConcurrentMap<String, ReentrantLock> locks;
dan kemudian:
Client client = clientRepository.findByClientId(clientId);
Satu-satunya masalah adalah manajemen "basi" clientId, dapat diselesaikan dengan menggunakan implementasi ConcurrentMap yang tidak standar, yang sudah mendukung kedaluwarsa, misalnya, ambil jambu Cache:
locks = CacheBuilder.newBuilder() .concurrencyLevel(4) .expireAfterWrite(Duration.ofMinutes(1)) .<String, ReentrantLock>build().asMap();
Keputusan 4
Solusi di atas menyinkronkan permintaan dalam satu instance. Apa yang harus dilakukan jika layanan Anda berputar pada N node dan permintaan dapat langsung berbeda? Untuk situasi ini, menggunakan perpustakaan
Redisson sempurna sebagai solusi:
Client client = clientRepository.findByClientId(clientId);
Perpustakaan memecahkan masalah kunci terdistribusi menggunakan redis sebagai repositori.
Kesimpulan
Keputusan mana untuk menerapkan tentu tergantung pada skala masalah: solusi 1-3 cukup cocok untuk layanan kecil satu contoh, solusi 4 ditujukan untuk layanan terdistribusi. Perlu juga dicatat secara terpisah bahwa penyelesaian masalah ini menggunakan Redisson atau analog (misalnya, Zookeeper klasik), tentu saja, merupakan kasus khusus, karena mereka dirancang untuk berbagai tugas yang jauh lebih besar untuk sistem terdistribusi.
Dalam kasus kami, kami memilih solusi 4, karena layanan kami didistribusikan dan integrasi Redisson adalah yang paling mudah dibandingkan dengan analog.
Teman, sarankan di komentar opsi Anda untuk memecahkan masalah ini, saya akan sangat senang!
Kode sumber untuk contoh tersedia di
GitHub .
Omong-omong, kami terus memperluas staf pengembangan, lowongan yang relevan dapat ditemukan di
halaman karir kami.
UPD 1. Solusi dari pembaca 1
Solusi ini mengusulkan untuk tidak menyinkronkan permintaan, tetapi jika terjadi kesalahan pada formulir:
pelanggaran integritas integritas: kendala unik atau pelanggaran indeks; Tabel UK_BFJDOY2DPUSSYLQ7G1S3S1TN8: CLIENT
harus diproses dan dipanggil kembali
client = clientRepository.findByClientId(clientId);
atau melakukannya melalui pegas coba:
@Retryable(value = { SQLException.class }, maxAttempts = 3, backoff = @Backoff(delay = 1000)) @Transactional public Client getOrCreateUser(String clientId)
(terima kasih kepada
Throwable untuk
contoh )
Dalam hal ini, akan ada pertanyaan "ekstra" ke basis data, tetapi dalam praktiknya penciptaan entitas Klien tidak akan sering terjadi, dan jika sinkronisasi hanya diperlukan untuk menyelesaikan masalah memasukkan ke dalam basis data, maka solusi ini dapat ditiadakan.
UPD 2. Solusi dari pembaca 2
Solusi ini mengusulkan untuk melakukan sinkronisasi melalui sesi:
HttpSession session = request.getSession(false); if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { ... } }
Solusi ini akan berfungsi untuk layanan satu contoh, tetapi akan perlu untuk menyelesaikan masalah sehingga semua permintaan dari satu klien ke API dilakukan dalam sesi yang sama.