أقترح عليك اليوم تحليل مهمة عملية واحدة حول سباق طلبات العميل التي واجهتها في MaximTelecom عند تطوير الواجهة الخلفية لتطبيق الهاتف المحمول MT_FREE.
عند بدء التشغيل ، يرسل تطبيق العميل بشكل غير متزامن "حزمة" من الطلبات إلى API. يحتوي التطبيق على معرف العميل ، بناءً على أنه من الممكن التمييز بين الطلبات من عميل واحد من آخر. لكل طلب على الخادم ، يتم تنفيذ رمز النموذج:
حيث يحتوي كيان العميل على حقل clientId ، والذي يجب أن يكون فريدًا وقيدًا فريدًا في قاعدة البيانات لهذا الغرض. نظرًا لأن كل طلب في Spring سينفذ هذا الرمز في سلسلة رسائل منفصلة ، حتى إذا كانت هذه طلبات من نفس تطبيق العميل ، فسيظهر خطأ في النموذج:
انتهاك قيود السلامة: قيد فريد أو انتهاك فهرس؛ UK_BFJDOY2DPUSSYLQ7G1S3S1TN8 الجدول: العميل
يحدث الخطأ لسبب واضح: 2 سلاسل أو أكثر مع نفس clientId تلقي العميل == كيان فارغ والبدء في إنشائه ، وبعد ذلك يحصلون على خطأ عند ارتكاب.
التحدي:
من الضروري مزامنة الطلبات من عميل واحد حتى يكتمل الطلب الأول فقط إنشاء كيان العميل ، وسيتم حظر الباقي في وقت الإنشاء وتلقي الكائن الذي أنشأه بالفعل.
الحل 1
هذا الحل فعال ، ولكنه مكلف للغاية ، حيث يتم حظر جميع الطلبات (سلاسل الرسائل) التي تحتاج إلى إنشاء ، حتى لو قاموا بإنشاء عميل مع عميل مختلف ولا يتنافسون مع بعضهم البعض.
يرجى ملاحظة أن مزيج من التزامن مع التعليقات التوضيحية @
@Transactional public synchronized Client getOrCreateUser(String clientId){
سوف يحدث نفس الخطأ مرة أخرى. والسبب هو أن الشاشة (متزامنة) يتم تحريرها أولاً ويدخل الخيط التالي إلى المنطقة المتزامنة ، وفقط بعد ذلك يتم الالتزام بالمعاملة بواسطة الخيط الأول في الكائن الوكيل. لحل هذه المشكلة بسيط - تحتاج إلى إطلاق جهاز العرض بعد الالتزام ، لذلك ، يجب أن تتم المزامنة أعلاه:
synchronized (this){ client = clientService.getOrCreateUser(clientId); }
القرار 2
أود حقًا استخدام تصميم النموذج:
synchronized (clientId)
ولكن المشكلة هي أنه سيتم إنشاء كائن clientId جديد لكل طلب ، حتى إذا كانت قيمهم مكافئة ، لذلك ، لا يمكن إجراء التزامن بهذه الطريقة. لحل المشكلة مع كائنات clientId مختلفة ، تحتاج إلى استخدام التجمع:
Client client = clientRepository.findByClientId(clientId);
يستخدم هذا الحل تجمع سلسلة java ، على التوالي ، سوف تتلقى الطلبات ذات العميل المكافئ ، عن طريق استدعاء clientId.intern () ، نفس الكائن. لسوء الحظ ، في الممارسة العملية ، هذا الحل غير قابل للتطبيق ، لأنه من المستحيل إدارة العميل المتعفن ، والذي سيؤدي عاجلاً أم آجلاً إلى OutOfMemory.
القرار 3
من أجل استخدام ReentrantLock ، تحتاج إلى مجموعة من النموذج:
private final ConcurrentMap<String, ReentrantLock> locks;
ثم:
Client client = clientRepository.findByClientId(clientId);
المشكلة الوحيدة هي إدارة clientId "التي لا معنى لها" ، ويمكن حلها باستخدام التطبيق غير القياسي لـ ConcurrentMap ، والذي يدعم بالفعل انتهاء الصلاحية ، على سبيل المثال ، خذ الجوافة Cache:
locks = CacheBuilder.newBuilder() .concurrencyLevel(4) .expireAfterWrite(Duration.ofMinutes(1)) .<String, ReentrantLock>build().asMap();
القرار 4
تقوم الحلول المذكورة أعلاه بمزامنة الطلبات داخل مثيل واحد. ماذا تفعل إذا كانت خدمتك تدور على عقد N ويمكن أن تذهب الطلبات مختلفة في نفس الوقت؟ في هذا الموقف ، يعد استخدام مكتبة
Redisson مثاليًا كحل:
Client client = clientRepository.findByClientId(clientId);
تحل المكتبة مشكلة الأقفال الموزعة باستخدام redis كمستودع.
الخاتمة
يعتمد قرار التطبيق بالتأكيد على حجم المشكلة: الحلول 1-3 مناسبة تمامًا لخدمات المثيل الواحد الصغيرة ، والحل 4 يهدف إلى الخدمات الموزعة. تجدر الإشارة أيضًا بشكل منفصل إلى أن حل هذه المشكلة باستخدام Redisson أو نظائرها (على سبيل المثال ، Zookeeper الكلاسيكية) هو ، بطبيعة الحال ، حالة خاصة ، لأنها مصممة لمجموعة أكبر بكثير من المهام للأنظمة الموزعة.
في حالتنا ، استقرنا على الحل 4 ، نظرًا لتوزيع خدمتنا وكان تكامل Redisson هو الأسهل بالمقارنة مع نظائرها.
أيها الأصدقاء ، أقترح في التعليقات خياراتك لحل هذه المشكلة ، سأكون سعيدًا جدًا!
شفرة المصدر للأمثلة متاحة على
جيثب .
بالمناسبة ، نحن نعمل باستمرار على توسيع فريق التطوير ، ويمكن العثور على الوظائف الشاغرة ذات الصلة على صفحتنا
المهنية .
محدث 1. حل من القراء 1
يقترح هذا الحل عدم مزامنة الطلبات ، ولكن في حالة حدوث خطأ في النموذج:
انتهاك قيود السلامة: قيد فريد أو انتهاك فهرس؛ UK_BFJDOY2DPUSSYLQ7G1S3S1TN8 الجدول: العميل
يجب معالجتها واسترجاعها
client = clientRepository.findByClientId(clientId);
أو قم بذلك من خلال إعادة المحاولة في الربيع:
@Retryable(value = { SQLException.class }, maxAttempts = 3, backoff = @Backoff(delay = 1000)) @Transactional public Client getOrCreateUser(String clientId)
(بفضل
Throwable على سبيل المثال )
في هذه الحالة ، ستكون هناك استفسارات "إضافية" في قاعدة البيانات ، ولكن في الممارسة العملية لن يتم إنشاء كيان العميل في كثير من الأحيان ، وإذا كانت هناك حاجة إلى التزامن فقط لحل مشكلة الإدراج في قاعدة البيانات ، يمكن الاستغناء عن هذا الحل.
محدث 2. حل من القراء 2
يقترح هذا الحل إجراء التزامن خلال الجلسة:
HttpSession session = request.getSession(false); if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { ... } }
سيعمل هذا الحل على خدمات المثيل المفرد ، ولكن سيكون من الضروري حل المشكلة حتى يتم تنفيذ جميع الطلبات من عميل واحد إلى API خلال الجلسة نفسها.