هذه المقالة مخصصة لأولئك الذين يستخدمون ذاكرة تخزين مؤقت فعالة في تطبيقهم ويريدون إضافة الاستقرار ليس فقط إلى التطبيق ، ولكن إلى البيئة بأكملها ببساطة بإضافة فئة واحدة إلى المشروع.
إذا كنت تتعرف على نفسك ، فاقرأ.
ما هو قواطع دوائر؟

تم اختراق هذا الموضوع مثل العالم ولن أتعرض لك ، مما يزيد من الانتروبيا ويكرر نفس الشيء. من وجهة نظري ، تحدث مارتن فاولر أفضل ما في الأمر
هنا ، لكنني سأحاول وضع التعريف في جملة واحدة:
وظيفة تمنع الطلبات المحكوم عليها عن عمد إلى خدمة غير متوفرة ، مما يسمح لها "بالركوع على ركبتيها" ومواصلة التشغيل العادي .
من الناحية المثالية ، منع طلبات محكوم عليها ، لا ينبغي لكسارة الدارة (المشار إليها فيما يلي بـ CB) أن تكسر طلبك. بدلاً من ذلك ، من الممارسات الجيدة إرجاع ، إن لم يكن أحدث البيانات ، ولكن لا تزال ذات صلة ("ليست كريهة") ، أو ، إذا لم يكن ذلك ممكنًا ، بعض القيمة الافتراضية.
أهداف
نحن وحيد الشيء الرئيسي:
- من الضروري السماح لمصدر البيانات بالتعافي ، وإيقاف الاستعلامات لفترة من الوقت
- في حالة إيقاف الطلبات إلى الخدمة المستهدفة ، يلزمك تقديم ، إن لم يكن أحدث ، ولكن لا تزال البيانات ذات الصلة
- إذا كانت الخدمة المستهدفة غير متوفرة ولا توجد بيانات ذات صلة ، فقم بتقديم إستراتيجية سلوك (إرجاع القيمة الافتراضية أو إستراتيجية أخرى مناسبة لحالة معينة)
آلية التنفيذ
الحالة: الخدمة متاحة (الطلب الأول)
- دعنا نذهب إلى ذاكرة التخزين المؤقت. حسب المفتاح (CRT انظر أدناه). نرى أنه لا يوجد شيء في ذاكرة التخزين المؤقت
- نذهب إلى الخدمة المستهدفة. نحصل على القيمة
- نقوم بتخزين القيمة في ذاكرة التخزين المؤقت ، وقم بتعيينها على TTL بحيث تغطي الحد الأقصى للوقت المتاح لعدم توفر الخدمة المستهدفة ، ولكن في الوقت نفسه ، يجب ألا تتجاوز فترة صلاحية البيانات التي تكون مستعدًا لإعطاءها للعميل في حالة فقد الاتصال بالخدمة المستهدفة
- يتم تخزين وقت تحديث ذاكرة التخزين المؤقت (CRT) في ذاكرة التخزين المؤقت للقيمة من الفقرة 3 - الوقت الذي تحتاج بعده إلى محاولة الانتقال إلى الخدمة المستهدفة وتحديث القيمة
- إرجاع القيمة من العنصر 2 إلى المستخدم
الحالة: CRT لم تنتهي
- دعنا نذهب إلى ذاكرة التخزين المؤقت. بالمفتاح نجد CRT. نحن نرى أنه من المناسب
- الحصول على القيمة لذلك من ذاكرة التخزين المؤقت.
- إرجاع القيمة إلى المستخدم.
الحالة: CRT منتهية الصلاحية ، الخدمة المستهدفة متوفرة
- دعنا نذهب إلى ذاكرة التخزين المؤقت. بالمفتاح نجد CRT. نحن نرى أنه غير ذي صلة
- نذهب إلى الخدمة المستهدفة. نحصل على القيمة
- تحديث القيمة في ذاكرة التخزين المؤقت و TTL الخاص به
- قم بتحديث CRT لها ، مضيفا فترة تحديث ذاكرة التخزين المؤقت (CRP) - هذه هي القيمة التي يجب إضافتها إلى CRT للحصول على CRT التالي
- إرجاع القيمة إلى المستخدم.
الحالة: CRT منتهية الصلاحية ، الخدمة المستهدفة غير متوفرة
- دعنا نذهب إلى ذاكرة التخزين المؤقت. بالمفتاح نجد CRT. نحن نرى أنه غير ذي صلة
- نذهب إلى الخدمة المستهدفة. انه غير متوفر
- الحصول على القيمة من ذاكرة التخزين المؤقت. ليست الأحدث (مع CRT الفاسد) ، ولكن لا تزال ذات صلة ، لأن TTL لم تنته بعد
- نعيدها إلى المستخدم
الحالة: CRT منتهية الصلاحية ، الخدمة المستهدفة غير متوفرة ، لا شيء في ذاكرة التخزين المؤقت
- دعنا نذهب إلى ذاكرة التخزين المؤقت. بالمفتاح نجد CRT. نحن نرى أنه غير ذي صلة
- نذهب إلى الخدمة المستهدفة. انه غير متوفر
- الحصول على القيمة من ذاكرة التخزين المؤقت. هو ليس كذلك
- نحن نحاول تطبيق استراتيجية خاصة لمثل هذه الحالات. على سبيل المثال ، إرجاع قيمة افتراضية لحقل محدد ، أو قيمة خاصة من النوع "هذه المعلومات غير متوفرة حاليًا". بشكل عام ، إذا كان ذلك ممكنًا ، فمن الأفضل إرجاع شيء ما وعدم كسر التطبيق. إذا لم يكن ذلك ممكنًا ، فأنت بحاجة إلى تطبيق استراتيجية رمي الاستثناء والاستجابة السريعة للمستخدم الاستثناء.
ما سوف نستخدمه
أستخدم Spring Boot 1.5 في مشروعي ، ما زلت لم أجد الوقت للترقية إلى الإصدار الثاني.
أن المادة لم تتحول 2 مرات لفترة أطول ، وسوف تستخدم لومبوك.
كتخزين رئيسي القيمة (يشار إليه فيما يلي بكل بساطة KV) أستخدم Redis 5.0.3 ، لكنني متأكد من أن Hazelcast أو تناظري سيفعلانه. الشيء الرئيسي هو أن هناك تطبيق واجهة CacheManager. في حالتي ، هذا هو RedisCacheManager من spring-boot-starter-data-redis.
تطبيق
أعلاه ، في قسم "آلية التنفيذ" ، تم وضع تعريفين مهمين: CRT و CRP. سأكتب لهم مرة أخرى بمزيد من التفصيل ، لأنه أنها مهمة للغاية لفهم الكود التالي:
وقت تحديث ذاكرة التخزين المؤقت (
CRT ) هو إدخال منفصل في KV (مفتاح + postfix "_crt") ، والذي يوضح الوقت الذي حان الوقت للذهاب إلى الخدمة المستهدفة للحصول على بيانات جديدة. بخلاف TTL ، لا يعني ظهور CRT أن بياناتك "فاسدة" ، ولكن من المحتمل أن تصبح أكثر حداثة في الخدمة المستهدفة. حصلت جديدة - حسنا ، إن لم يكن ، وسوف ينخفض التيار.
فترة تحديث ذاكرة التخزين المؤقت (
CRP ) هي قيمة تضاف إلى CRT بعد استقصاء الخدمة المستهدفة (لا يهم إذا كانت ناجحة أم لا). بفضلها ، تتمتع الخدمة عن بعد بالقدرة على "التقاط أنفاسها" واستعادة عملها في حالة السقوط.
لذلك ، تقليديا ، نبدأ من خلال تصميم الواجهة الرئيسية. من خلاله ، ستحتاج إلى العمل مع ذاكرة التخزين المؤقت إذا كنت بحاجة إلى منطق CB. يجب أن تكون بسيطة قدر الإمكان:
public interface CircuitBreakerService { <T> T getStableValue(StableValueParameter parameter); void evictValue(EvictValueParameter parameter); }
معلمات واجهة:
@Getter @AllArgsConstructor public class StableValueParameter<T> { private String cachePrefix;
@Getter @AllArgsConstructor public class EvictValueParameter { private String cachePrefix; private String objectCacheKey; }
هذه هي الطريقة التي سوف نستخدمها:
public AccountDataResponse findAccount(String accountId) { final StableValueParameter<?> parameter = new StableValueParameter<>( ACCOUNT_CACHE_PREFIX, accountId, properties.getCrpInSeconds(), () -> bankClient.findById(accountId) ); return circuitBreakerService.getStableValue(parameter); }
إذا كنت بحاجة إلى مسح ذاكرة التخزين المؤقت ، فقم بما يلي:
public void evictAccount(String accountId) { final EvictValueParameter parameter = new EvictValueParameter( ACCOUNT_CACHE_PREFIX, accountId ); circuitBreakerService.evictValue(parameter); }
الآن الشيء الأكثر إثارة للاهتمام هو التنفيذ (الموضح في التعليقات في الكود):
@Override public <T> T getStableValue(StableValueParameter parameter) { final Cache cache = cacheManager.getCache(parameter.getCachePrefix()); if (cache == null) { return logAndThrowUnexpectedCacheMissing(parameter.getCachePrefix(), parameter.getObjectCacheKey()); }
إذا كانت الخدمة المستهدفة غير متوفرة ، فحاول الحصول على البيانات ذات الصلة من ذاكرة التخزين المؤقت:
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(); }
إذا لم تكن هناك بيانات فعلية في ذاكرة التخزين المؤقت (تم حذفها بواسطة TTL ، ولا تزال الخدمة المستهدفة غير متوفرة) ، فإننا نستخدم DisasterStrategy:
private <T> T getFromCacheOrDisasterStrategy(StableValueParameter parameter, Cache cache) { return (T) getFromCache(parameter, cache).orElseGet(() -> parameter.getDisasterStrategy().getValue()); }
لا يعد الإزالة من ذاكرة التخزين المؤقت أمرًا مثيرًا للاهتمام ، وسأقدمها هنا فقط للتأكد من اكتمالها:
private <T> T getFromCacheOrDisasterStrategy(StableValueParameter parameter, Cache cache) { return (T) getFromCache(parameter, cache).orElseGet(() -> parameter.getDisasterStrategy().getValue()); }
لا يعد الإزالة من ذاكرة التخزين المؤقت أمرًا مثيرًا للاهتمام ، وسأقدمها هنا فقط للتأكد من اكتمالها:
@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); }
استراتيجية الكوارث

هذا ، في الواقع ، هو المنطق الذي يحدث في حالة انتهاء صلاحية CRT ، وتكون الخدمة الهدف غير متوفرة ، ولا يوجد شيء في ذاكرة التخزين المؤقت.
أردت أن أصف هذا المنطق بشكل منفصل ، لأن كثيرون لا يحصلون على أيديهم للتفكير في كيفية تنفيذه. ولكن هذا ، في الواقع ، هو ما يجعل نظامنا مستقرًا حقًا.
لا تريد أن تشعر بهذا الشعور بالفخر في بنات أفكارك عندما يتم رفض كل شيء يمكن أن تفشل ، ولا يزال نظامك يعمل. على الرغم من حقيقة أنه ، على سبيل المثال ، في الحقل "السعر" ، لن يتم عرض التكلفة الفعلية للبضاعة ، ولكن النقش: "يتم تحديدها حاليًا" ، ولكن ما مدى أفضل من الإجابة "خدمة 500 غير متوفرة". بعد كل شيء ، على سبيل المثال ، الحقول العشرة المتبقية: وصف المنتج ، إلخ. لقد عدت. كم تتغير جودة مثل هذه الخدمة؟ .. دعوتي هي إيلاء المزيد من الاهتمام للتفاصيل ، وجعلها أفضل.
الانتهاء من الانحدار الغنائي. لذلك ، ستكون واجهة الإستراتيجية كما يلي:
public interface DisasterStrategy<T> { T getValue(); }
يجب عليك اختيار التنفيذ حسب الحالة المحددة. على سبيل المثال ، إذا كان يمكنك إرجاع بعض القيمة الافتراضية ، فيمكنك فعل شيء مثل هذا:
public class DefaultValueDisasterStrategy implements DisasterStrategy<String> { @Override public String getValue() { return " "; } }
أو إذا لم يكن عليك إرجاع أي شيء على الإطلاق في حالة معينة ، فيمكنك استثناء:
public class ThrowExceptionDisasterStrategy implements DisasterStrategy<Object> { @Override public Object getValue() { throw new CircuitBreakerNullValueException("Ops! Service is down and there's null value in cache"); } }
في هذه الحالة ، لن يتم زيادة CRT وسيتم متابعة الطلب التالي مرة أخرى إلى الخدمة المستهدفة.
استنتاج
ألتزم بوجهة النظر التالية - إذا كان لديك الفرصة لاستخدام حل جاهز ، وليس لإثارة ضجة ، في الواقع ، رغم أنه بسيط ، ولكن لا يزال في هذه المقالة ، فعل ذلك. استخدم هذه المقالة لفهم كيفية عملها ، وليس كدليل للعمل.
هناك العديد من الحلول الجاهزة ، خاصةً إذا كنت تستخدم Spring Boot 2 ، مثل Hystrix.
أهم شيء يجب فهمه هو أن هذا الحل يعتمد على ذاكرة التخزين المؤقت وأن فعاليته تساوي فعالية ذاكرة التخزين المؤقت. إذا كانت ذاكرة التخزين المؤقت غير فعالة (عدد قليل من الزيارات والعديد من الأخطاء) ، فعندئذٍ ستكون قواطع الدائرة الكهربائية غير فعالة بالمثل: ستصاحب كل ملكة جمال ذاكرة التخزين المؤقت رحلة إلى الخدمة المستهدفة ، التي ربما تكون في حالة من الألم والكرب في هذه اللحظة ، تحاول الارتفاع.
تأكد من قياس فعالية ذاكرة التخزين المؤقت الخاصة بك قبل تطبيق هذا النهج. يمكن القيام بذلك عن طريق "معدل ضرب ذاكرة التخزين المؤقت" = عدد مرات الدخول / (عدد مرات الدخول + يخطئ) ، يجب أن يميل إلى 1 وليس 0
ونعم ، لا أحد يزعجك للحفاظ على عدة أنواع من CB في مشروعك في وقت واحد ، وذلك باستخدام واحد الذي يحل أفضل مشكلة محددة.