طلب التزامن في الربيع

أقترح عليك اليوم تحليل مهمة عملية واحدة حول سباق طلبات العميل التي واجهتها في MaximTelecom عند تطوير الواجهة الخلفية لتطبيق الهاتف المحمول MT_FREE.

عند بدء التشغيل ، يرسل تطبيق العميل بشكل غير متزامن "حزمة" من الطلبات إلى API. يحتوي التطبيق على معرف العميل ، بناءً على أنه من الممكن التمييز بين الطلبات من عميل واحد من آخر. لكل طلب على الخادم ، يتم تنفيذ رمز النموذج:

//      Client client = clientRepository.findByClientId(clientId); //      if(client == null){ client = clientRepository.save(new Client(clientId)); } //    

حيث يحتوي كيان العميل على حقل clientId ، والذي يجب أن يكون فريدًا وقيدًا فريدًا في قاعدة البيانات لهذا الغرض. نظرًا لأن كل طلب في Spring سينفذ هذا الرمز في سلسلة رسائل منفصلة ، حتى إذا كانت هذه طلبات من نفس تطبيق العميل ، فسيظهر خطأ في النموذج:
انتهاك قيود السلامة: قيد فريد أو انتهاك فهرس؛ UK_BFJDOY2DPUSSYLQ7G1S3S1TN8 الجدول: العميل

يحدث الخطأ لسبب واضح: 2 سلاسل أو أكثر مع نفس clientId تلقي العميل == كيان فارغ والبدء في إنشائه ، وبعد ذلك يحصلون على خطأ عند ارتكاب.

التحدي:


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

الحل 1


  //      if(client == null){ //   synchronized (this){ //    client = clientRepository.findByClientId(clientId); if(client == null){ client = clientRepository.save(new Client(clientId)); } } } 

هذا الحل فعال ، ولكنه مكلف للغاية ، حيث يتم حظر جميع الطلبات (سلاسل الرسائل) التي تحتاج إلى إنشاء ، حتى لو قاموا بإنشاء عميل مع عميل مختلف ولا يتنافسون مع بعضهم البعض.

يرجى ملاحظة أن مزيج من التزامن مع التعليقات التوضيحية @

 @Transactional public synchronized Client getOrCreateUser(String clientId){ //      Client client = clientRepository.findByClientId(clientId); //      if(client == null){ client = clientRepository.save(new Client(clientId)); } return client; } 

سوف يحدث نفس الخطأ مرة أخرى. والسبب هو أن الشاشة (متزامنة) يتم تحريرها أولاً ويدخل الخيط التالي إلى المنطقة المتزامنة ، وفقط بعد ذلك يتم الالتزام بالمعاملة بواسطة الخيط الأول في الكائن الوكيل. لحل هذه المشكلة بسيط - تحتاج إلى إطلاق جهاز العرض بعد الالتزام ، لذلك ، يجب أن تتم المزامنة أعلاه:

  synchronized (this){ client = clientService.getOrCreateUser(clientId); } 

القرار 2


أود حقًا استخدام تصميم النموذج:

 synchronized (clientId) 

ولكن المشكلة هي أنه سيتم إنشاء كائن clientId جديد لكل طلب ، حتى إذا كانت قيمهم مكافئة ، لذلك ، لا يمكن إجراء التزامن بهذه الطريقة. لحل المشكلة مع كائنات clientId مختلفة ، تحتاج إلى استخدام التجمع:

 Client client = clientRepository.findByClientId(clientId); //      if(client == null){ //   synchronized (clientId.intern()){ //    client = clientRepository.findByClientId(clientId); if(client == null){ client = clientRepository.save(new Client(clientId)); } } } 

يستخدم هذا الحل تجمع سلسلة java ، على التوالي ، سوف تتلقى الطلبات ذات العميل المكافئ ، عن طريق استدعاء clientId.intern () ، نفس الكائن. لسوء الحظ ، في الممارسة العملية ، هذا الحل غير قابل للتطبيق ، لأنه من المستحيل إدارة العميل المتعفن ، والذي سيؤدي عاجلاً أم آجلاً إلى OutOfMemory.

القرار 3


من أجل استخدام ReentrantLock ، تحتاج إلى مجموعة من النموذج:

 private final ConcurrentMap<String, ReentrantLock> locks; 

ثم:

 Client client = clientRepository.findByClientId(clientId); //      if(client == null){ //   ReentrantLock lock = locks.computeIfAbsent(clientId, (k) -> new ReentrantLock()); lock.lock(); try{ //    client = clientRepository.findByClientId(clientId); if(client == null){ client = clientRepository.save(new Client(clientId)); } } finally { //   lock.unlock(); } } 

المشكلة الوحيدة هي إدارة clientId "التي لا معنى لها" ، ويمكن حلها باستخدام التطبيق غير القياسي لـ ConcurrentMap ، والذي يدعم بالفعل انتهاء الصلاحية ، على سبيل المثال ، خذ الجوافة Cache:

 locks = CacheBuilder.newBuilder() .concurrencyLevel(4) .expireAfterWrite(Duration.ofMinutes(1)) .<String, ReentrantLock>build().asMap(); 

القرار 4


تقوم الحلول المذكورة أعلاه بمزامنة الطلبات داخل مثيل واحد. ماذا تفعل إذا كانت خدمتك تدور على عقد N ويمكن أن تذهب الطلبات مختلفة في نفس الوقت؟ في هذا الموقف ، يعد استخدام مكتبة Redisson مثاليًا كحل:

  Client client = clientRepository.findByClientId(clientId); //      if(client == null){ //   RLock lock = redissonClient.getFairLock(clientId); lock.lock(); try{ //    client = clientRepository.findByClientId(clientId); if(client == null){ client = clientRepository.save(new Client(clientId)); } } finally { //   lock.unlock(); } } 

تحل المكتبة مشكلة الأقفال الموزعة باستخدام 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 خلال الجلسة نفسها.

Source: https://habr.com/ru/post/ar434714/


All Articles