Artikel ini ditujukan bagi mereka yang menggunakan cache yang efektif dalam aplikasi mereka dan ingin menambahkan stabilitas tidak hanya untuk aplikasi, tetapi juga untuk seluruh lingkungan hanya dengan menambahkan 1 kelas ke proyek.
Jika Anda mengenali diri sendiri, baca terus.
Apa itu Pemutus Sirkuit?

Tema ini basi seperti dunia dan saya tidak akan membuat Anda bosan, meningkatkan entropi dan mengulangi hal yang sama. Dari sudut pandang saya, Martin Fowler berbicara paling baik di
sini , tetapi saya akan mencoba mencocokkan definisi menjadi satu kalimat:
fungsionalitas yang mencegah permintaan dengan sengaja terhadap layanan yang tidak tersedia, memungkinkannya untuk “bangun dari lututnya” dan melanjutkan operasi normal .
Idealnya, mencegah permintaan yang gagal, Circuit Breaker (selanjutnya disebut CB) tidak boleh merusak aplikasi Anda. Sebagai gantinya, adalah praktik yang baik untuk mengembalikan, jika bukan data terkini, tetapi masih relevan ("tidak busuk"), atau, jika ini tidak mungkin, beberapa nilai default.
Tujuan
Kami memilih hal utama:
- Hal ini diperlukan untuk memungkinkan sumber data pulih, menghentikan permintaan untuk sementara waktu
- Dalam hal menghentikan permintaan ke layanan target, Anda harus memberikan, jika bukan yang terbaru, tetapi masih data yang relevan
- Jika layanan target tidak tersedia dan tidak ada data yang relevan, berikan strategi perilaku (mengembalikan nilai default atau strategi lain yang cocok untuk kasus tertentu)
Mekanisme implementasi
Kasus: layanan tersedia (permintaan pertama)
- Ayo pergi ke cache. Dengan kunci (CRT lihat di bawah). Kami melihat bahwa tidak ada apa pun di cache
- Kami pergi ke layanan target. Kami mendapatkan nilainya
- Kami menyimpan nilai dalam cache, mengaturnya ke TTL yang akan mencakup waktu maksimum yang mungkin bahwa layanan target tidak tersedia, tetapi pada saat yang sama tidak boleh melebihi periode relevansi data yang siap Anda berikan kepada klien jika terjadi kehilangan koneksi dengan layanan target
- Waktu Refresh Cache (CRT) disimpan dalam cache untuk nilai dari klausa 3 - waktu setelah itu Anda perlu mencoba untuk pergi ke layanan target dan memperbarui nilai.
- Kembalikan nilai dari item 2 ke pengguna
Kasus: CRT tidak kedaluwarsa
- Ayo pergi ke cache. Kami menemukan CRT. Kami melihat bahwa itu relevan
- Dapatkan nilai untuk itu dari cache.
- Kembalikan nilai ke pengguna.
Kasus: CRT kedaluwarsa, layanan target tersedia
- Ayo pergi ke cache. Kami menemukan CRT. Kami melihat bahwa itu tidak relevan
- Kami pergi ke layanan target. Kami mendapatkan nilainya
- Memperbarui nilai dalam cache dan TTL-nya
- Perbarui CRT untuknya dengan menambahkan Cache Refresh Periode (CRP) - ini adalah nilai yang perlu ditambahkan ke CRT untuk mendapatkan CRT berikutnya
- Kembalikan nilai ke pengguna.
Kasus: CRT kedaluwarsa, layanan target tidak tersedia
- Ayo pergi ke cache. Kami menemukan CRT. Kami melihat bahwa itu tidak relevan
- Kami pergi ke layanan target. Dia tidak tersedia
- Dapatkan nilai dari cache. Bukan yang terbaru (dengan CRT busuk), tetapi masih relevan, karena TTL-nya belum kedaluwarsa
- Kami mengembalikannya ke pengguna
Kasus: CRT kedaluwarsa, layanan target tidak tersedia, tidak ada dalam cache
- Ayo pergi ke cache. Kami menemukan CRT. Kami melihat bahwa itu tidak relevan
- Kami pergi ke layanan target. Dia tidak tersedia
- Dapatkan nilai dari cache. Bukan dia
- Kami mencoba menerapkan strategi khusus untuk kasus-kasus seperti itu. Misalnya, mengembalikan nilai default untuk bidang yang ditentukan, atau nilai khusus dari jenis "Informasi ini saat ini tidak tersedia". Secara umum, jika ini memungkinkan, lebih baik mengembalikan sesuatu dan tidak merusak aplikasi. Jika ini tidak memungkinkan, maka Anda perlu menerapkan strategi melempar pengecualian dan respons cepat ke pengguna pengecualian.
Apa yang akan kita gunakan
Saya menggunakan Spring Boot 1.5 dalam proyek saya, masih belum menemukan waktu untuk meningkatkan ke versi kedua.
Bahwa artikelnya tidak berubah 2 kali lebih lama, saya akan menggunakan Lombok.
Sebagai penyimpanan Key-Value (selanjutnya hanya disebut sebagai KV) saya menggunakan Redis 5.0.3, tetapi saya yakin bahwa Hazelcast atau analog akan melakukannya. Yang utama adalah bahwa ada implementasi antarmuka CacheManager. Dalam kasus saya, ini adalah RedisCacheManager dari spring-boot-starter-data-redis.
Implementasi
Di atas, di bagian "Mekanisme Implementasi", dua definisi penting dibuat: CRT dan CRP. Saya akan menulisnya lagi dengan lebih detail, karena mereka sangat penting untuk memahami kode berikut:
Cache Refresh Time (
CRT ) adalah entri terpisah di KV (kunci + postfix "_crt"), yang menunjukkan waktu kapan waktunya untuk pergi ke layanan target untuk data baru. Tidak seperti TTL, permulaan CRT tidak berarti bahwa data Anda "busuk", tetapi hanya bahwa itu kemungkinan akan lebih baru dalam layanan target. Menjadi segar - yah, jika tidak, dan arus akan turun.
Periode Refresh Cache (
CRP ) adalah nilai yang ditambahkan ke CRT setelah polling layanan target (tidak masalah apakah itu berhasil atau tidak). Berkat dia, layanan jarak jauh memiliki kemampuan untuk "menarik napas" dan mengembalikan pekerjaannya jika jatuh.
Jadi, secara tradisional, kita mulai dengan mendesain antarmuka utama. Melalui itu Anda perlu bekerja dengan cache jika Anda membutuhkan logika CB. Seharusnya sesederhana mungkin:
public interface CircuitBreakerService { <T> T getStableValue(StableValueParameter parameter); void evictValue(EvictValueParameter parameter); }
Parameter antarmuka:
@Getter @AllArgsConstructor public class StableValueParameter<T> { private String cachePrefix;
@Getter @AllArgsConstructor public class EvictValueParameter { private String cachePrefix; private String objectCacheKey; }
Inilah cara kami akan menggunakannya:
public AccountDataResponse findAccount(String accountId) { final StableValueParameter<?> parameter = new StableValueParameter<>( ACCOUNT_CACHE_PREFIX, accountId, properties.getCrpInSeconds(), () -> bankClient.findById(accountId) ); return circuitBreakerService.getStableValue(parameter); }
Jika Anda perlu menghapus cache, maka:
public void evictAccount(String accountId) { final EvictValueParameter parameter = new EvictValueParameter( ACCOUNT_CACHE_PREFIX, accountId ); circuitBreakerService.evictValue(parameter); }
Sekarang hal yang paling menarik adalah implementasinya (dijelaskan dalam komentar dalam kode):
@Override public <T> T getStableValue(StableValueParameter parameter) { final Cache cache = cacheManager.getCache(parameter.getCachePrefix()); if (cache == null) { return logAndThrowUnexpectedCacheMissing(parameter.getCachePrefix(), parameter.getObjectCacheKey()); }
Jika layanan target tidak tersedia, cobalah untuk mendapatkan data yang masih relevan dari cache:
private <T> T getFromTargetServiceAndUpdateCache( StableValueParameter parameter, Cache cache, String crtKey, LocalDateTime crt ) { T result; try { result = getFromTargetService(parameter); } catch (WebServiceIOException ex) { log.warn( "[CircuitBreaker] Service responded with error: {}. Try get from cache {}: {}", ex.getMessage(), parameter.getCachePrefix(), parameter.getObjectCacheKey()); result = getFromCacheOrDisasterStrategy(parameter, cache); } cache.put(parameter.getObjectCacheKey(), result); cache.put(crtKey, crt.plusSeconds(parameter.getCrpInSeconds())); return result; } private static <T> T getFromTargetService(StableValueParameter parameter) { return (T) parameter.getTargetServiceAction().get(); }
Jika tidak ada data aktual dalam cache (mereka dihapus oleh TTL, dan layanan target masih tidak tersedia), maka kami menggunakan DisasterStrategy:
private <T> T getFromCacheOrDisasterStrategy(StableValueParameter parameter, Cache cache) { return (T) getFromCache(parameter, cache).orElseGet(() -> parameter.getDisasterStrategy().getValue()); }
Menghapus dari cache tidak ada yang menarik, saya akan berikan di sini hanya untuk kelengkapan:
private <T> T getFromCacheOrDisasterStrategy(StableValueParameter parameter, Cache cache) { return (T) getFromCache(parameter, cache).orElseGet(() -> parameter.getDisasterStrategy().getValue()); }
Menghapus dari cache tidak ada yang menarik, saya akan berikan di sini hanya untuk kelengkapan:
@Override public void evictValue(EvictValueParameter parameter) { final Cache cache = cacheManager.getCache(parameter.getCachePrefix()); if (cache == null) { logAndThrowUnexpectedCacheMissing(parameter.getCachePrefix(), parameter.getObjectCacheKey()); return; } final String crtKey = parameter.getObjectCacheKey() + CRT_CACHE_POSTFIX; cache.evict(crtKey); }
Strategi bencana

Ini, pada kenyataannya, adalah logika yang terjadi jika CRT kedaluwarsa, layanan target tidak tersedia, dan tidak ada dalam cache.
Saya ingin menggambarkan logika ini secara terpisah, karena banyak yang tidak mendapatkan tangan mereka untuk memikirkan bagaimana mengimplementasikannya. Tetapi inilah yang membuat sistem kami benar-benar stabil.
Tidakkah Anda ingin merasakan kebanggaan terhadap gagasan Anda ketika segala sesuatu yang hanya bisa gagal ditolak, dan sistem Anda masih berfungsi. Meskipun fakta bahwa, misalnya, bukan harga sebenarnya dari barang yang akan ditampilkan di bidang "harga", tetapi tulisan: "itu sedang ditentukan sekarang", tetapi seberapa jauh ini lebih baik daripada jawaban "500 layanan tidak tersedia". Lagi pula, misalnya, 10 bidang yang tersisa: deskripsi produk, dll. kamu kembali. Seberapa besar perubahan kualitas layanan seperti itu? .. Panggilan saya adalah untuk lebih memperhatikan detail, menjadikannya lebih baik.
Menyelesaikan penyimpangan lirik. Jadi, antarmuka strategi adalah sebagai berikut:
public interface DisasterStrategy<T> { T getValue(); }
Anda harus memilih implementasinya tergantung pada kasus spesifik. Misalnya, jika Anda dapat mengembalikan beberapa nilai default, maka Anda dapat melakukan sesuatu seperti ini:
public class DefaultValueDisasterStrategy implements DisasterStrategy<String> { @Override public String getValue() { return " "; } }
Atau, jika dalam kasus tertentu Anda tidak perlu mengembalikan apa pun, maka Anda dapat melemparkan pengecualian:
public class ThrowExceptionDisasterStrategy implements DisasterStrategy<Object> { @Override public Object getValue() { throw new CircuitBreakerNullValueException("Ops! Service is down and there's null value in cache"); } }
Dalam hal ini, CRT tidak akan bertambah dan permintaan berikutnya akan mengikuti layanan target.
Kesimpulan
Saya berpegang pada sudut pandang berikut - jika Anda memiliki kesempatan untuk menggunakan solusi yang sudah jadi, dan tidak membuat keributan, pada kenyataannya, meskipun sederhana, tetapi tetap bersepeda dalam artikel ini, jadi lakukanlah. Gunakan artikel ini untuk memahami cara kerjanya, dan bukan sebagai panduan untuk bertindak.
Ada begitu banyak solusi yang sudah jadi, terutama jika Anda menggunakan Spring Boot 2, seperti Hystrix.
Yang paling penting untuk dipahami adalah bahwa solusi ini didasarkan pada cache dan efektivitasnya sama dengan efektivitas cache. Jika cache tidak efektif (beberapa hit, banyak misses), maka Circuit Breaker ini akan sama-sama tidak efisien: setiap cache miss akan disertai dengan perjalanan ke layanan target, yang, mungkin, dalam kesakitan dan kesedihan pada saat ini, mencoba untuk bangkit.
Pastikan untuk mengukur efektivitas cache Anda sebelum menerapkan pendekatan ini. Ini dapat dilakukan dengan “Cache Hit Rate” = hits / (hits + misses), seharusnya cenderung ke 1, bukan 0.
Dan ya, tidak ada yang mengganggu Anda untuk menyimpan beberapa varietas CB dalam proyek Anda sekaligus, menggunakan salah satu yang paling baik memecahkan masalah spesifik.