मैंने दौड़ की स्थिति की एक अच्छी परिभाषा की खोज की और मुझे यह सबसे अच्छी परिभाषा मिली:
दौड़ की स्थिति एक अप्रत्याशित व्यवहार है जो कई प्रक्रियाओं द्वारा साझा संसाधनों के साथ अपेक्षा से भिन्न क्रम में बातचीत करने के कारण होता है।
यह काफी मुंह की बात है और यह अभी भी बहुत स्पष्ट नहीं है कि रेल्स में दौड़ की स्थिति कैसी दिखती है।
रेल का उपयोग करते हुए, हम हमेशा कई प्रक्रियाओं के साथ काम कर रहे हैं - प्रत्येक अनुरोध या पृष्ठभूमि कार्य एक व्यक्तिगत प्रक्रिया है जो अन्य प्रक्रियाओं से अधिकतर स्वतंत्र रूप से काम कर सकती है।
हम हमेशा साझा संसाधनों के साथ भी काम कर रहे हैं। क्या एप्लिकेशन रिलेशनल डेटाबेस का उपयोग करता है? वह एक साझा संसाधन है. क्या एप्लिकेशन किसी प्रकार के कैशिंग सर्वर का उपयोग करता है? हाँ, यह एक साझा संसाधन है। क्या आप किसी प्रकार की बाहरी एपीआई का उपयोग करते हैं? आपने अनुमान लगाया - यह एक साझा संसाधन है।
दौड़ की स्थितियों की दो उदाहरण श्रेणियां हैं जिनके बारे में मैं बात करना चाहूंगा और फिर उनसे निपटने के तरीके पर बात करना चाहूंगा।
रीड-मॉडिफाई-राइट श्रेणी एक प्रकार की दौड़ की स्थिति है जहां एक प्रक्रिया साझा संसाधन से मूल्यों को पढ़ेगी, मेमोरी के भीतर मूल्य को संशोधित करेगी, और फिर इसे साझा संसाधन पर वापस लिखने का प्रयास करेगी। जब हम इसे एक प्रक्रिया के चश्मे से देखते हैं तो यह बहुत सीधा लगता है। लेकिन जब कोई दूसरी प्रक्रिया सामने आती है, तो इसके परिणामस्वरूप कुछ अप्रत्याशित व्यवहार हो सकता है।
ऐसे कोड पर विचार करें जो इस तरह दिखता है:
class IdeasController < ActionController::Base def vote @idea = Idea.find(params[:id]) @idea.votes += 1 @idea.save! end end
यहां हम ( Idea.find(params[:id])
, संशोधित कर रहे हैं ( @idea.votes += 1
), फिर लिख रहे हैं ( @idea.save!
)।
हम देख सकते हैं कि इससे किसी विचार पर वोटों की संख्या एक से बढ़ जाएगी। यदि कोई विचार शून्य वोट वाला होता तो वह एक वोट पर ही ख़त्म हो जाता। हालाँकि, यदि कोई दूसरा अनुरोध आता है और डेटाबेस से विचार पढ़ता है जबकि उसके पास अभी भी शून्य वोट हैं और स्मृति में उस मूल्य को बढ़ाता है, तो हमारे पास ऐसी स्थिति हो सकती है जहां दो वोट एक साथ आते हैं - फिर भी अंतिम परिणाम यह है कि वोटों की संख्या डेटाबेस में केवल एक है.
इसे लॉस्ट अपडेट रेस स्थिति भी कहा जाता है।
चेक-तब-एक्ट श्रेणी एक प्रकार की दौड़ की स्थिति है जहां डेटा एक साझा संसाधन से लोड किया जाता है, और मौजूद मूल्य के आधार पर, हम निर्धारित करते हैं कि क्या कोई कार्रवाई करने की आवश्यकता है।
यह कैसे दिखाई देता है इसका एक उत्कृष्ट उदाहरण रेल्स में validates_uniqueness_of
सत्यापन में इस प्रकार है:
class User < ActiveRecord::Base validates_uniqueness_of :email end
ऐसे कोड पर विचार करें जो इस तरह दिखता है:
User.create(email: "[email protected]")
सत्यापन के साथ, रेल्स जाँच करेगा कि क्या उस ईमेल के पास कोई मौजूदा उपयोगकर्ता है। यदि कोई अन्य नहीं है, तो यह उपयोगकर्ता को डेटाबेस में बनाए रखकर कार्य करेगा। हालाँकि, यदि दूसरा अनुरोध एक ही समय में समान कोड निष्पादित कर रहा हो तो क्या होगा? हम ऐसी स्थिति में पहुँच सकते हैं जहाँ दोनों अनुरोध यह निर्धारित करने के लिए जाँच करते हैं कि क्या डुप्लिकेट डेटा है (और कोई नहीं है) - फिर वे दोनों डेटा को सहेजकर कार्य करेंगे, जिसके परिणामस्वरूप डेटाबेस में एक डुप्लिकेट उपयोगकर्ता होगा।
दौड़ की स्थितियों को ठीक करने के लिए कोई चांदी की गोली नहीं है, लेकिन कुछ रणनीतियाँ हैं जिनका उपयोग किसी विशेष समस्या के लिए किया जा सकता है। दौड़ की शर्तों को हटाने के लिए तीन मुख्य श्रेणियां हैं:
हालाँकि इसे आपत्तिजनक कोड को हटाने के रूप में देखा जा सकता है, कभी-कभी आप कोड को दोबारा तैयार कर सकते हैं ताकि यह दौड़ की स्थितियों के प्रति संवेदनशील न हो। अन्य समय में, आप परमाणु संचालन पर गौर कर सकते हैं।
एक परमाणु ऑपरेशन वह है जहां कोई अन्य प्रक्रिया ऑपरेशन को बाधित नहीं कर सकती है, इसलिए आप जानते हैं कि यह हमेशा एक इकाई के रूप में निष्पादित होगा।
पढ़ने-संशोधित-लिखने के उदाहरण के लिए, स्मृति में विचार वोटों को बढ़ाने के बजाय, उन्हें डेटाबेस में बढ़ाया जा सकता है:
@ideas.increment!(:votes)
वह एसक्यूएल निष्पादित करेगा जो इस तरह दिखता है:
UPDATE "ideas" SET "votes" = COALESCE("votes", 0) + 1 WHERE "ideas"."id" = 123
इसका उपयोग समान दौड़ शर्तों के अधीन नहीं होगा।
चेक-तब-एक्ट उदाहरण के लिए, रेल को मॉडल को मान्य करने की अनुमति देने के बजाय, हम रिकॉर्ड को सीधे डेटाबेस में एक अप्सर्ट के साथ सम्मिलित कर सकते हैं:
User.where(email: "[email protected]").upsert({}, unique_by: :email)
वह रिकॉर्ड को डेटाबेस में डाल देगा। यदि ईमेल पर कोई विरोध है (जिसके लिए ईमेल पर एक अद्वितीय अनुक्रमणिका की आवश्यकता होगी) तो यह केवल सम्मिलन को अनदेखा कर देगा।
कभी-कभी आप महत्वपूर्ण अनुभाग को नहीं हटा सकते. यह संभव है कि कोई परमाणु क्रिया हो, लेकिन यह उस तरीके से काम नहीं करता है जिसकी कोड को आवश्यकता होती है। उन स्थितियों में, आप पता लगाने और पुनर्प्राप्त करने का तरीका आज़मा सकते हैं। इस दृष्टिकोण के साथ, सुरक्षा उपाय स्थापित किए जाते हैं जो दौड़ की स्थिति होने पर आपको सूचित करेंगे। आप या तो शालीनतापूर्वक ऑपरेशन को निरस्त कर सकते हैं या पुनः प्रयास कर सकते हैं।
पढ़ने-संशोधित-लिखने के उदाहरण के लिए, यह आशावादी लॉकिंग के साथ किया जा सकता है। आशावादी लॉकिंग को रेल्स में बनाया गया है और यह पता लगाने की अनुमति दे सकता है कि एक ही समय में एक ही रिकॉर्ड पर कई प्रक्रियाएं कब चल रही हैं। आशावादी लॉकिंग को सक्षम करने के लिए, आपको केवल अपनी तालिका में एक lock_version
कॉलम जोड़ना होगा और रेल स्वचालित रूप से इसे सक्षम कर देगी।
change_table :ideas do |t| t.integer :lock_version, default: 0 end
फिर जब आप किसी रिकॉर्ड को अपडेट करने का प्रयास करते हैं, तो रेल्स इसे केवल तभी अपडेट करेगा यदि lock_version
वही संस्करण है जो यह मेमोरी में था। यदि ऐसा नहीं है, तो यह एक ActiveRecord::StaleObjectError
अपवाद उठाएगा, जिसे इसे संभालने के लिए बचाया जा सकता है। इसे संभालना एक retry
हो सकता है या यह उपयोगकर्ता को वापस रिपोर्ट किया गया एक त्रुटि संदेश हो सकता है।
def vote @idea = Idea.find(params[:id]) @idea.votes += 1 @idea.save! rescue ActiveRecord::StaleObjectError retry end
चेक-तब-एक्ट उदाहरण के लिए, यह कॉलम पर एक अद्वितीय इंडेक्स के साथ किया जा सकता है, फिर डेटा को जारी रखते समय अपवाद को बचाया जा सकता है।
add_index :users, [:email], unique: true
एक अद्वितीय इंडेक्स के साथ, यदि उस email
के साथ डेटाबेस में डेटा पहले से मौजूद है, तो रेल एक ActiveRecord::RecordNotUnique
त्रुटि उत्पन्न करेगी और उसे बचाया जा सकता है और उचित रूप से प्रबंधित किया जा सकता है।
begin user = User.create(email: "[email protected]") rescue ActiveRecord::RecordNotUnique user = User.find_by(email: "[email protected]") end
कार्रवाइयों को पुनः प्रयास करने के लिए, यह महत्वपूर्ण है कि संपूर्ण ऑपरेशन निष्क्रिय हो। इसका मतलब यह है कि यदि कोई ऑपरेशन कई बार किया जाता है, तो परिणाम वैसा ही होगा जैसे कि इसे केवल एक बार लागू किया गया हो।
उदाहरण के लिए, कल्पना करें कि यदि किसी नौकरी ने एक ईमेल भेजा हो और जब भी किसी विचार के वोट बदले गए हों तो उसे निष्पादित किया जाए। यह सचमुच बहुत बुरा होगा यदि प्रत्येक पुनः प्रयास के लिए एक ईमेल भेजा जाए। ऑपरेशन को निष्प्रभावी बनाने के लिए, आप संपूर्ण वोटिंग ऑपरेशन पूरा होने तक ईमेल भेजना बंद कर सकते हैं। वैकल्पिक रूप से, आप उस प्रक्रिया के कार्यान्वयन को अपडेट कर सकते हैं जो ईमेल भेजती है ताकि ईमेल केवल तभी भेजा जा सके जब वोट पिछली बार भेजे जाने के बाद बदल गए हों। यदि दौड़ की स्थिति उत्पन्न होती है और आपको पुनः प्रयास करने की आवश्यकता होती है, तो ईमेल भेजने के पहले प्रयास के परिणामस्वरूप नो-ऑप हो सकता है और इसे फिर से ट्रिगर करना सुरक्षित है।
कई ऑपरेशन निष्प्रभावी नहीं हो सकते हैं - जैसे पृष्ठभूमि कार्य को सूचीबद्ध करना, ईमेल भेजना, या तृतीय-पक्ष एपीआई को कॉल करना।
यदि आप पता नहीं लगा सकते और पुनर्प्राप्त नहीं कर सकते, तो आप कोड को सुरक्षित रखने का प्रयास कर सकते हैं। यहां लक्ष्य एक ऐसा अनुबंध बनाना है जहां एक समय में केवल एक ही प्रक्रिया साझा संसाधन तक पहुंच सके। प्रभावी रूप से, आप समवर्तीता को हटा रहे हैं - चूंकि केवल एक प्रक्रिया ही साझा संसाधन तक पहुंच सकती है, हम अधिकांश दौड़ स्थितियों से बच सकते हैं। हालाँकि, व्यापार यह है कि जितनी अधिक समवर्तीता हटा दी जाएगी, आवेदन उतना ही धीमा हो सकता है क्योंकि अन्य प्रक्रियाएँ तब तक प्रतीक्षा करेंगी जब तक उन्हें पहुँच की अनुमति नहीं मिल जाती।
इसे रेल्स के साथ निर्मित निराशावादी लॉकिंग का उपयोग करके नियंत्रित किया जा सकता है। निराशावादी लॉकिंग का उपयोग करने के लिए, आप बनाई जा रही क्वेरी में lock
जोड़ सकते हैं, और रेल डेटाबेस को उन रिकॉर्ड्स पर एक पंक्ति लॉक रखने के लिए कहेगी। डेटाबेस तब तक किसी अन्य प्रक्रिया को लॉक प्राप्त करने से रोक देगा जब तक कि यह पूरा न हो जाए। transaction
में कोड को लपेटना सुनिश्चित करें ताकि डेटाबेस को पता चले कि लॉक कब जारी करना है।
Idea.transaction do @idea = Idea.lock.find(params[:id]) @idea.votes += 1 @idea.save! end
यदि पंक्ति-स्तरीय लॉकिंग संभव नहीं है, तो Redlock या with_advisory_lock जैसे अन्य उपकरण हैं जिनका उपयोग किया जा सकता है। ये कोड के एक मनमाने ब्लॉक को लॉक करने की अनुमति देंगे। इसका उपयोग करना कुछ इस तरह सरल हो सकता है:
email = "[email protected]" User.with_advisory_lock("user_uniqueness_#{email}"} do User.find_or_create_by(email: email) end
ये रणनीतियाँ लॉक प्राप्त होने तक प्रक्रियाओं को प्रतीक्षा करने का कारण बनेंगी। इसलिए, किसी प्रक्रिया को हमेशा के लिए प्रतीक्षा करने से रोकने के लिए वे कुछ प्रकार का टाइमआउट भी चाहेंगे - साथ ही टाइमआउट की स्थिति में क्या करना है इसके लिए कुछ प्रबंधन भी चाहेंगे।
हालाँकि दौड़ की स्थितियों को ठीक करने का कोई रामबाण इलाज नहीं है, लेकिन इन रणनीतियों के माध्यम से दौड़ की कई स्थितियों को ठीक किया जा सकता है। हालाँकि, प्रत्येक समस्या थोड़ी अलग है, इसलिए समाधान का विवरण भिन्न हो सकता है। आप RailsConf 2023 से मेरी बातचीत पर एक नज़र डाल सकते हैं जो दौड़ की स्थितियों के बारे में अधिक विस्तार से बताती है।
काइल डी'ओलिवेरा
काइल को अमूर्त विचारों को सॉफ्टवेयर के कामकाजी टुकड़ों में बदलने का शौक है। वह अहा में एक प्रमुख सॉफ्टवेयर इंजीनियर हैं! - दुनिया का #1 उत्पाद विकास सॉफ्टवेयर । जब काइल विकास नहीं कर रहा होता है, तो वह कनाडा के वैंकूवर में अपने घर के पास अद्भुत भोजन और शिल्प ब्रुअरीज का आनंद लेता है।