ग्राहक अनुरोध वसंत में सिंक्रनाइज़ेशन

आज मैं आपको सुझाव देता हूं कि हमारे मोबाइल एप्लिकेशन MT_FREE के लिए बैक-एंड का विकास करते समय मुझे मैक्सिमटेलकॉम में क्लाइंट अनुरोधों की दौड़ के बारे में एक व्यावहारिक कार्य का विश्लेषण करना चाहिए।

स्टार्टअप पर, क्लाइंट एप्लिकेशन एसिंक्रोनसली एपीआई के लिए अनुरोधों का एक "पैकेट" भेजता है। एप्लिकेशन में क्लाइंटआईड पहचानकर्ता है, जिसके आधार पर एक क्लाइंट से दूसरे क्लाइंट के बीच अंतर करना संभव है। सर्वर पर प्रत्येक अनुरोध के लिए, फ़ॉर्म का एक कोड निष्पादित किया जाता है:

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

जहां क्लाइंट इकाई के पास एक क्लाइंटआईड फ़ील्ड है, जो अद्वितीय होना चाहिए और इसके लिए डेटाबेस में अद्वितीय बाधा होनी चाहिए। चूंकि वसंत में प्रत्येक अनुरोध इस कोड को एक अलग थ्रेड में निष्पादित करेगा, भले ही ये एक ही क्लाइंट एप्लिकेशन से अनुरोध हों, फॉर्म की एक त्रुटि दिखाई देगी:
अखंडता बाधा उल्लंघन: अद्वितीय बाधा या सूचकांक उल्लंघन; UK_BFJDOY2DPUSSYLQ7G1S3S1TN8 तालिका: CLIENT

त्रुटि एक स्पष्ट कारण के लिए होती है: एक ही क्लाइंट के साथ 2 या अधिक थ्रेड्स क्लाइंट == शून्य इकाई को प्राप्त करते हैं और इसे बनाना शुरू करते हैं, जिसके बाद उन्हें कमिट करते समय एक त्रुटि मिलती है।

उद्देश्य:


एक क्लाइंटआईड से अनुरोधों को सिंक्रनाइज़ करना आवश्यक है ताकि केवल पहला अनुरोध क्लाइंट इकाई के निर्माण को पूरा करे, और बाकी निर्माण के समय अवरुद्ध हो जाए और उस ऑब्जेक्ट को प्राप्त कर ले जो इसे पहले ही बनाया गया था।

समाधान 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); } 

निर्णय २


मैं वास्तव में फ़ॉर्म के डिज़ाइन का उपयोग करना चाहूंगा:

 synchronized (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)); } } } 

यह समाधान क्रमशः जावा स्ट्रिंग पूल का उपयोग करता है, समतुल्य क्लाइंटआईड के साथ अनुरोध करता है, क्लाइंटआईड.इन () को कॉल करके, समान ऑब्जेक्ट प्राप्त करेगा। दुर्भाग्य से, व्यवहार में, यह समाधान लागू नहीं है, क्योंकि "सड़" क्लाइंटआईड को प्रबंधित करना असंभव है, जो कि जल्द या बाद में आउटऑफमेरी को बढ़ावा देगा।

निर्णय ३


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(); } } 

एकमात्र समस्या "बासी" क्लाइंटआईड का प्रबंधन है, यह समवर्ती नक्शे के गैर-मानक कार्यान्वयन का उपयोग करके हल किया जा सकता है, जो पहले से ही समाप्ति का समर्थन करता है, उदाहरण के लिए, अमरूद कैश लें:

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

निर्णय ४


उपरोक्त समाधान एक उदाहरण के भीतर अनुरोधों को सिंक्रनाइज़ करते हैं। यदि आपकी सेवा एन नोड्स पर घूम रही है और एक ही समय में अनुरोध अलग-अलग जा सकते हैं तो क्या करें? इस स्थिति के लिए, 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(); } } 

पुस्तकालय रेडिस का भंडार के रूप में उपयोग करके वितरित ताले की समस्या को हल करता है।

निष्कर्ष


निश्चित रूप से लागू करने का निर्णय समस्या के पैमाने पर निर्भर करता है: 1-3 समाधान छोटे एकल-उदाहरण सेवाओं के लिए काफी उपयुक्त हैं, समाधान 4 वितरित सेवाओं के उद्देश्य से है। यह भी अलग से ध्यान देने योग्य है कि रेडिसन या एनालॉग्स (उदाहरण के लिए, क्लासिक ज़ुकाइपर) का उपयोग करके इस समस्या को हल करना, निश्चित रूप से, एक विशेष मामला है, क्योंकि वे वितरित सिस्टम के लिए कार्यों की एक बड़ी रेंज के लिए डिज़ाइन किए गए हैं।

हमारे मामले में, हम समाधान 4 पर बसे, क्योंकि हमारी सेवा वितरित की गई है और एनालॉग्स की तुलना में रेडिसन एकीकरण सबसे आसान था।

दोस्तों, टिप्पणियों में सुझाव दें कि इस समस्या को हल करने के लिए आपके विकल्प, मुझे बहुत खुशी होगी!
उदाहरणों के लिए स्रोत कोड GitHub पर उपलब्ध है

वैसे, हम लगातार विकास कर्मचारियों का विस्तार कर रहे हैं, प्रासंगिक रिक्तियां हमारे कैरियर पृष्ठ पर पाई जा सकती हैं।

UPD 1. पाठकों से समाधान 1


यह समाधान अनुरोधों को सिंक्रनाइज़ नहीं करने का प्रस्ताव करता है, लेकिन फॉर्म की त्रुटि के मामले में:
अखंडता बाधा उल्लंघन: अद्वितीय बाधा या सूचकांक उल्लंघन; UK_BFJDOY2DPUSSYLQ7G1S3S1TN8 तालिका: CLIENT

संसाधित और वापस बुलाया जाना चाहिए
 client = clientRepository.findByClientId(clientId); 

या वसंत-रिट्री के माध्यम से करते हैं:
 @Retryable(value = { SQLException.class }, maxAttempts = 3, backoff = @Backoff(delay = 1000)) @Transactional public Client getOrCreateUser(String clientId) 

( उदाहरण के लिए थ्रोबेबल के लिए धन्यवाद)
इस मामले में, डेटाबेस के लिए "अतिरिक्त" प्रश्न होंगे, लेकिन व्यवहार में ग्राहक इकाई का निर्माण अक्सर नहीं होगा, और यदि डेटाबेस में डालने की समस्या को हल करने के लिए केवल सिंक्रनाइज़ेशन की आवश्यकता होती है, तो इस समाधान के साथ तिरस्कृत किया जा सकता है।

UPD 2. पाठकों से समाधान 2


यह समाधान सत्र के माध्यम से सिंक्रनाइज़ेशन बनाने का प्रस्ताव करता है:
 HttpSession session = request.getSession(false); if (session != null) { Object mutex = WebUtils.getSessionMutex(session); synchronized (mutex) { ... } } 

यह समाधान एकल-आवृत्ति सेवाओं के लिए काम करेगा, लेकिन समस्या को हल करने के लिए आवश्यक होगा ताकि एक क्लाइंट से एपीआई तक सभी अनुरोध एक ही सत्र के भीतर किए जाएं।

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


All Articles