2009-07-28 16 views
21

SQL डेटाबेस में समवर्ती अद्यतनों से निपटने का सामान्य तरीका क्या है?डेटाबेस में समवर्ती अद्यतनों से कैसे निपटें?

एक सरल एसक्यूएल स्कीमा (की कमी और चूक नहीं दिखाया ..) की तरह

create table credits (
    int id, 
    int creds, 
    int user_id 
); 

आशय एक उपयोगकर्ता है, उदा के लिए क्रेडिट किसी तरह का स्टोर करने के लिए है पर विचार करें Stackoverflow की प्रतिष्ठा की तरह कुछ।

उस तालिका के समवर्ती अद्यतनों से कैसे निपटें? कुछ विकल्प:

  • update credits set creds= 150 where userid = 1;

    इस मामले में आवेदन वर्तमान मान पुनर्प्राप्त, नया मान (150) गणना की और एक अद्यतन प्रदर्शन किया। यदि कोई और एक ही समय में एक ही करता है तो आपदा का क्या कारण होता है। मैं अनुमान लगा रहा हूं कि वर्तमान मूल्य के प्रतिरक्षा को लपेटने और लेनदेन में अपडेट करने से यह हल हो जाएगा, उदा। Begin; select creds from credits where userid=1; do application logic to calculate new value, update credits set credits = 160 where userid = 1; end; इस मामले में आप जांच सकते हैं कि नया क्रेडिट < 0 होगा और नकारात्मक क्रेडिट को कोई समझ नहीं होने पर इसे 0 पर छोटा कर दें।

  • update credits set creds = creds - 150 where userid=1;

    इस मामले में समवर्ती अपडेट के बारे में चिंता करने की डीबी स्थिरता समस्या का ख्याल रखता है, लेकिन कमी थी जिसके कारण creds खुशी से नकारात्मक बन जाएगा, जो कुछ अनुप्रयोगों के लिए कोई औचित्य नहीं है के रूप में की जरूरत नहीं होगी ।

तो बस, ऊपर उल्लिखित (काफी सरल) समस्या से निपटने के लिए स्वीकार्य विधि क्या है, क्या होगा यदि डीबी एक त्रुटि फेंकता है?

+0

यदि आप अपने कॉलम पर बाधाओं का उल्लंघन करने के बारे में चिंतित हैं, तो डेटाबेस में कन्स्ट्रेंट को परिभाषित करें। – jva

उत्तर

25

उपयोग लेन-देन:

BEGIN WORK; 
SELECT creds FROM credits WHERE userid = 1; 
-- do your work 
UPDATE credits SET creds = 150 WHERE userid = 1; 
COMMIT; 

कुछ महत्वपूर्ण सूचनाएं:

  • नहीं सभी डेटाबेस प्रकार के लेन-देन का समर्थन है। विशेष रूप से, MySQL का डिफ़ॉल्ट डेटाबेस प्रकार, MyISAM, नहीं करता है। यदि आप mysql पर हैं तो InnoDB का उपयोग करें।
  • लेनदेन आपके नियंत्रण से परे कारणों से निरस्त हो सकते हैं। यदि ऐसा होता है, तो आपका काम BEGIN WORK से फिर से शुरू करने के लिए तैयार होना चाहिए।
  • आपको अलगाव स्तर को अलगाव के लिए सेट करने की आवश्यकता होगी, अन्यथा पहला चयन डेटा को पढ़ सकता है जो अन्य लेनदेन अभी तक नहीं किए गए हैं (लेन-देन प्रोग्रामिंग भाषाओं में म्यूटेक्स की तरह नहीं हैं)। कुछ डेटाबेस एक त्रुटि फेंक देंगे यदि समवर्ती चालू सर्जिकल लेनदेन है, और आपको लेनदेन को पुनरारंभ करना होगा।
  • कुछ डीबीएमएस चयन प्रदान करते हैं .. अद्यतन के लिए, जो लेनदेन समाप्त होने तक चयन द्वारा पुनर्प्राप्त पंक्तियों को लॉक कर देगा।

एसक्यूएल संग्रहीत प्रक्रियाओं के साथ लेनदेन को जोड़ना बाद के हिस्से को सौदा करने में आसान बना सकता है; एप्लिकेशन केवल एक लेनदेन में एक ही संग्रहीत प्रक्रिया को कॉल करेगा, और लेनदेन के बंद होने पर इसे फिर से कॉल करेगा।

+0

क्या आपको इस मामले में "अद्यतन के लिए चयन" की आवश्यकता नहीं होगी, या कम से कम एक अलौकिक अलगाव स्तर की आवश्यकता नहीं होगी? – nos

+2

@ नहीं, यह डेटाबेस पर निर्भर करता है। वास्तविक लेनदेन समर्थन वाले डेटाबेस को लेनदेन के साथ एक सतत स्नैपशॉट प्रदान करना चाहिए, हालांकि संभवतः डिफ़ॉल्ट रूप से नहीं। Innodb के मामले में, डेटाबेस स्थिति का एक स्नैपशॉट जैसे ही आप किसी भी innodb तालिका पर चयन करते हैं। – bdonlan

+0

आपको वास्तव में अलगाव स्तर सेट करने की आवश्यकता हो सकती है: http://dev.mysql.com/doc/refman/5.0/en/set-transaction.html – bdonlan

2

पहले परिदृश्य के लिए आप यह सुनिश्चित करने के लिए कहां से एक और शर्त जोड़ सकते हैं कि आप एक समवर्ती उपयोगकर्ता द्वारा किए गए परिवर्तनों को ओवरराइट नहीं करेंगे। जैसे

update credits set creds= 150 where userid = 1 AND creds = 0; 
0

यदि आप मूल्य के साथ अंतिम अपडेट टाइमस्टैम्प स्टोर करते हैं, तो जब आप मान पढ़ते हैं, तो टाइमस्टैम्प भी पढ़ें। जब आप रिकॉर्ड अपडेट करने के लिए जाते हैं, तो टाइमस्टैम्प मैचों को सुनिश्चित करने के लिए जांचें। अगर कोई आपके पीछे आया और आपके सामने अपडेट किया गया, तो टाइमस्टैम्प मेल नहीं खाएगा।

+2

यदि आप एक ही मिलीसेकंड के भीतर होते हैं (या जो भी आपके डीबी टाइममेम्प के लिए संकल्प होता है) में आपको परेशानी होती है। निश्चित रूप से कभी नहीं होता है, एक दिन तक यह अचानक होता है और पतली हवा में खोए गए क्रेडिट (या धन) के साथ आपके खराब उपयोगकर्ता को छोड़ देता है। – nos

1

आप एक क्यूइंग तंत्र स्थापित कर सकते हैं जहां रैंक प्रकार मान से जोड़ या घटाव कुछ नौकरी द्वारा आवधिक एलआईएफओ प्रसंस्करण के लिए कतारबद्ध हो जाएगा। यदि रैंक की "बैलेंस" पर वास्तविक समय की जानकारी आवश्यक है तो यह फिट नहीं होगा क्योंकि शेष बकाया कतार प्रविष्टियों को सुलझाने तक शेष गणना नहीं की जाएगी, लेकिन अगर ऐसा कुछ है जिसके लिए तत्काल सुलह की आवश्यकता नहीं है तो यह सेवा दे सकता है।

ऐसा लगता है कि कम से कम बाहर की तरफ देखकर, पुरानी पेंजर जनरल श्रृंखला जैसे गेम व्यक्तिगत चाल को कैसे संभालेंगे। एक खिलाड़ी की बारी आती है, और वे अपनी चाल घोषित करते हैं। बदले में प्रत्येक कदम अनुक्रम में संसाधित होता है, और कोई संघर्ष नहीं होता है क्योंकि प्रत्येक चाल की कतार में इसकी जगह होती है।

+1

व्यक्तिगत रिकॉर्ड स्टोर करना हमेशा एक अच्छा विचार है। यह भी उपयोगी है अगर आपके कोड में बग हैं और आपको स्रोत रिकॉर्ड से शेष राशि को फिर से बनाने की आवश्यकता है। यह भी उपयोगी है क्योंकि अब आपके पास एक सारांश लेखक है जो आपकी सारांश तालिका में वापस आता है, जो विवाद, अवरोध और जटिलता को कम करता है, और उपयोगकर्ता अनुभव को अधिक असमान बनाता है। – Doug

2

एक नए timestamp कॉलम का उपयोग कर आशावादी लॉकिंग इस समवर्ती मुद्दे को हल कर सकती है।

UPDATE credits SET creds = 150 WHERE userid = 1 and modified_data = old_modified_date 
12

MySQL InnoDB तालिकाओं के लिए, यह वास्तव में आपके द्वारा सेट अलगाव स्तर पर निर्भर करता है।

यदि आप डिफ़ॉल्ट स्तर 3 (दोहराए गए पढ़ने) का उपयोग कर रहे हैं, तो आपको किसी भी पंक्ति को लॉक करना होगा जो बाद के लिखने को प्रभावित करता है, भले ही आप लेनदेन में हों।

SELECT FOR UPDATE creds FROM credits WHERE userid = 1; 
-- calculate -- 
UPDATE credits SET creds = 150 WHERE userid = 1; 

आप स्तर 4 (serializable) का उपयोग कर रहे हैं, तो एक सरल अद्यतन के बाद चयन के लिए पर्याप्त है: अपने उदाहरण में आप की आवश्यकता होगी। इनो डीबी में लेवल 4 को आपके द्वारा पढ़ी जाने वाली प्रत्येक पंक्ति को रीड-लॉक करके कार्यान्वित किया जाता है।

SELECT creds FROM credits WHERE userid = 1; 
-- calculate -- 
UPDATE credits SET creds = 150 WHERE userid = 1; 
इस विशिष्ट उदाहरण में

हालांकि, बाद से गणना (क्रेडिट जोड़ने) काफी सरल एसक्यूएल में किया जा रहा है, एक सरल:

UPDATE credits set creds = creds - 150 where userid=1; 

अपडेट के लिए कोई चयन अद्यतन के बाद के बराबर होगी ।

+0

इस स्पष्ट टूटने के लिए धन्यवाद। –

0

तालिका को नीचे के रूप में संशोधित किया जा सकता है, आशावादी लॉकिंग को संभालने के लिए नया फ़ील्ड संस्करण प्रस्तुत करें। यह और अधिक लागत प्रभावी और कुशल तरीके से बेहतर परिणाम हासिल करने के लिए के बजाय डेटाबेस स्तर पर ताले का उपयोग कर तालिका क्रेडिट ( पूर्णांक आईडी, पूर्णांक creds, पूर्णांक user_id, पूर्णांक संस्करण ) बना है,

क्रेडिट, user_id, क्रेडिट से संस्करण का चयन करें जहां user_id = 1;

इस creds = 100 और संस्करण रिटर्न मान = 1

अद्यतन क्रेडिट creds सेट = creds * 10, संस्करण = संस्करण + 1 जहां user_id = 1 और संस्करण = 1;

हमेशा इस सुनिश्चित करना है कि नवीनतम जो कोई भी हो रही है संस्करण संख्या केवल अद्यतन करता है यह कर सकते हैं इस रिकॉर्ड और गंदे राईट अनुमति नहीं दी जाएगी

6

एक सौदे यह नहीं परवाह किए बिना अलगाव स्तर आपके द्वारा निर्धारित कुछ मामलों में काफी है अंदर कोड रैपिंग।

1) open a transaction 
2) fetch the data (SELECT creds FROM credits WHERE userid = 1;) 
3) do your work (credits + amount) 
4) update the data (UPDATE credits SET creds = ? WHERE userid = 1;) 
5) commit 

और यह समय रेखा:

मान लीजिए कि आप इन चरणों का और 2 संगामिति धागे करते

Time = 0; creds = 100 
Time = 1; ThreadA executes (1) and creates Txn1 
Time = 2; ThreadB executes (1) and creates Txn2 
Time = 3; ThreadA executes (2) and fetches 100 
Time = 4; ThreadB executes (2) and fetches 100 
Time = 5; ThreadA executes (3) and adds 100 + 50 
Time = 6; ThreadB executes (3) and adds 100 + 50 
Time = 7; ThreadA executes (4) and updates creds to 150 
Time = 8; ThreadB tries to executes (4) but in the best scenario the transaction 
      (depending of isolation level) won't allow it and you get an error 

लेन-देन एक गलत मान के साथ creds मान को ओवरराइड करने के लिए आप से बचाता है, लेकिन यह नहीं है पर्याप्त क्योंकि मैं किसी भी त्रुटि को विफल नहीं करना चाहता हूं।

मैं बजाय धीमी प्रक्रिया को पसंद करता हूं जो कभी विफल नहीं होता है और जब मैं डेटा (चरण 2) लाने के पल में "डेटाबेस पंक्ति लॉक" के साथ समस्या हल करता हूं जो अन्य थ्रेड को तब तक पढ़ता है जब तक कि मैं पूरा नहीं कर लेता इसके साथ।

वहाँ एसक्यूएल सर्वर में क्या करना कुछ तरीके हैं और यह उनमें से एक है:

Time = 0; creds = 100 
Time = 1; ThreadA executes (1) and creates Txn1 
Time = 2; ThreadB executes (1) and creates Txn2 
Time = 3; ThreadA executes (2) with lock and fetches 100 
Time = 4; ThreadB tries executes (2) but the row is locked and 
        it's has to wait... 

Time = 5; ThreadA executes (3) and adds 100 + 50 
Time = 6; ThreadA executes (4) and updates creds to 150 
Time = 7; ThreadA executes (5) and commits the Txn1 

Time = 8; ThreadB was waiting up to this point and now is able to execute (2) 
        with lock and fetches 150 
Time = 9; ThreadB executes (3) and adds 150 + 50 
Time = 10; ThreadB executes (4) and updates creds to 200 
Time = 11; ThreadB executes (5) and commits the Txn2 
1

:

SELECT creds FROM credits WITH (UPDLOCK) WHERE userid = 1; 

अगर मैं इस सुधार के साथ पिछले समय रेखा को पुन: आप कुछ इस तरह मिल आपके मामले में एक महत्वपूर्ण बिंदु है जब आप अनुरोधित राशि द्वारा उपयोगकर्ता के वर्तमान क्रेडिट फ़ील्ड को कम करते हैं और यदि यह सफलतापूर्वक घट गया है तो आप अन्य परिचालन करते हैं और समस्या सिद्धांत में है ऑपरेशन घटाने के लिए कई समानांतर अनुरोध हो सकते हैं, उदाहरण के लिए उपयोगकर्ता के पास शेष राशि पर 1 क्रेडिट होता है और 5 समांतर 1 क्रेडिट चार्ज अनुरोधों के साथ वह 5 चीजें खरीद सकता है यदि अनुरोध एक ही समय पर भेजा जाएगा और आप -4 क्रेडिट के साथ समाप्त हो जाएंगे उपयोगकर्ता की शेष राशि

से बचने के लिए इस आप भी जहां अगर वर्तमान मूल्य शून्य से अनुरोध राशि अधिक या शून्य के बराबर है में जाँच अनुरोध राशि (हमारे उदाहरण 1 क्रेडिट में) और के साथ मौजूदा क्रेडिट मूल्य में कमी करनी चाहिए:

अद्यतन क्रेडिट creds सेट = creds -1 कहां creds -1> = 0 और उपयोगकर्ता id = 1

हो जाएगा ताकि गारंटी है कि उपयोगकर्ता कुछ क्रेडिट के तहत बहुत सी बातें की खरीद कभी नहीं होगा अगर वह डॉस होगा आपके सिस्टम।

इस क्वेरी आप ROW_COUNT() जो बताता है कि वर्तमान उपयोगकर्ता क्रेडिट मापदंड से मुलाकात की और पंक्ति अद्यतन किया गया था चलाना चाहिए के बाद:

mysqli_query ("UPDATE credits SET creds = creds-$amount WHERE creds-$amount>=0 and userid = $user"); 
if (mysqli_affected_rows()) 
{ 
    \\do good things here 
} 

:

UPDATE credits SET creds = creds-1 WHERE creds-1>=0 and userid = 1 
IF (ROW_COUNT()>0) THEN 
    --IF WE ARE HERE MEANS USER HAD SURELY ENOUGH CREDITS TO PURCHASE THINGS  
END IF; 

एक PHP में इसी प्रकार के बात की तरह किया जा सकता है यहां हमने उपयोग किया और न ही चयन ... अद्यतन करने के लिए न तो लेनदेन लेकिन यदि आप इस कोड को लेनदेन के अंदर डालते हैं तो सुनिश्चित करें कि लेनदेन स्तर हमेशा पंक्ति से सबसे हालिया डेटा प्रदान करता है (जिसमें पहले से किए गए अन्य लेनदेन भी शामिल हैं)। आप भी कर सकते हैं उपयोगकर्ता ROLLBACK अगर ROW_COUNT() = 0

नकारात्मक पहलू के WHERE क्रेडिट $ राशि> = 0 पंक्ति ताला बिना कर रहे हैं:

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

सावधानी:

लेन-देन के स्तर का जो हाल ही में पंक्ति डेटा प्रदान नहीं करता है के अंदर इस रणनीति का प्रयोग न करें।

यदि आप जानना चाहते हैं कि अपडेट से पहले और बाद में मूल्य क्या था, तो इस रणनीति का उपयोग न करें।

बस इस तथ्य पर भरोसा करने का प्रयास करें कि शून्य से नीचे बिना क्रेडिट सफलतापूर्वक चार्ज किया गया था।

संबंधित मुद्दे