php एक ही समय में एक ही डेटा को चलाने और संसाधित करने का कार्य संभालें




mysql concurrency (9)

माइक्रोटाइम विचार आपके कोड में जटिलता जोड़ देगा। $order->getAlreadyRefund() मेमोरी से मूल्य प्राप्त कर सकता है, इसलिए यह सच्चाई का एक विश्वसनीय स्रोत नहीं है।

हालाँकि आप किसी एक अद्यतन पर उन स्थितियों के साथ भरोसा कर सकते हैं जो केवल तभी अपडेट होती हैं यदि स्थिति अभी भी 'लंबित' है और पहले से ही_आर्थ्रंड अभी भी 0. है। आपके पास इस तरह एक SQL कथन होगा:

UPDATE
  sales_order
SET
  status = 'canceled',
  already_refund = %d
where
  order_id = 1
  and status = 'pending'
  and already_refund = 0;

आपको बस अपने मॉडल के लिए एक विधि लिखने की आवश्यकता है जो उपरोक्त SQL को setCancelRefund() निष्पादित करेगी और आपके पास कुछ इस तरह सरल हो सकता है:

<?php

function checkPendingOrders() {
   $orders = $this->orderCollection->filter(['status'=>'pending']);

   foreach($orders as $order) {
     //check if order is ready to be canceled
     $isCanceled = $this->isCanceled($order->getId());
     if ($isCanceled === false) {
        continue;
     }

     if ($order->getAlreadyRefund() == '0') { // check if already refund

        // Your new method should do the following
        // UPDATE sales_order SET status = 'canceled', already_refund = 1 where order_id = %d and status = 'pending' and already_refund = 0; 
        $affected_rows = $order->setCancelRefund();        

        if ($affected_rows == 0) {
            continue;
        }

        $this->refund($order->getId()); //refund the money to customer ewallet
     }

   }
}

मेरे पास एक php सिस्टम है जो ग्राहक को हमारे सिस्टम से ई-वॉलेट (स्टोर क्रेडिट) का उपयोग करके चीजें खरीदने (ऑर्डर देने) की अनुमति देता है।

यहाँ डेटाबेस उदाहरण है

**sales_order**
+--------+-------+----------+--------+--------------+-----------+
|order_id| price |product_id| status |already_refund|customer_id|
+--------+-------+----------+--------+--------------+-----------+
|   1    | 1000  |    1     |canceled|      1       |     2     |
|   2    | 2000  |    2     |pending |      0       |     2     |
|   3    | 3000  |    3     |complete|      0       |     1     | 
+--------+-------+----------+--------+--------------+-----------+

**ewallet**
+-----------+-------+
|customer_id|balance|
+-----------+-------+
|     1     | 43200 |
|     2     | 22500 |
|     3     | 78400 |
+-----------+-------+

टेबल सेल्स_ऑर्डर में वह ऑर्डर होता है जो ग्राहक ने बनाया था, कॉलम पहले से ही_रंड उस ध्वज के लिए है जिसे रद्द किया गया ऑर्डर पहले से ही वापस कर दिया गया है।

मैं जाँच करने के लिए हर 5 मिनट में एक क्रोन चला रहा हूँ कि क्या स्थिति लंबित होने के आदेश को रद्द किया जा सकता है और इसके बाद वह ग्राहक के ईवलेट को पैसा वापस कर सकता है

function checkPendingOrders(){
   $orders = $this->orderCollection->filter(['status'=>'pending']);
   foreach($orders as $order){
     //check if order is ready to be canceled
     $isCanceled = $this->isCanceled($order->getId());
     if($isCanceled === false) continue;
     if($order->getAlreadyRefund() == '0'){ // check if already refund
       $order->setAlredyRefund('1')->save();
       $this->refund($order->getId()); //refund the money to customer ewallet
     }
     $order->setStatus('canceled')->save();
   }
}

समस्या 2 विभिन्न क्रोन शेड्यूल इस फ़ंक्शन का उपयोग करके एक ही समय में एक ही डेटा को संसाधित कर सकती है और यह धनवापसी प्रक्रिया को दो बार कॉल कर सकती है, इसलिए ग्राहक को डबल रिफंड राशि प्राप्त होगी। जब एक ही डेटा को संसाधित करने के लिए एक ही समय पर चल रहे 2 समान फ़ंक्शन, मैं इस तरह की समस्या को कैसे संभाल सकता हूं? if क्लॉज जो मैंने बनाया है वह इस तरह के मुद्दे को संभाल नहीं सकता है

अद्यतन करें

मैंने सत्यापन के रूप में सत्र में माइक्रोटाइम का उपयोग करने की कोशिश की है और MySQL में तालिका पंक्ति को लॉक किया है, इसलिए शुरुआत में मैंने माइक्रोटाइम को शामिल करने के लिए चर सेट किया, जब मैं order_id द्वारा उत्पन्न एक अद्वितीय सत्र में संग्रहीत करता order_id , और फिर मैं एक शर्त जोड़ता हूं तालिका पंक्ति को लॉक करने से पहले सत्र के साथ सूक्ष्म मूल्य का मिलान करने के लिए और मेरी ईवालेट तालिका को अपडेट करें

function checkPendingOrders(){
   $orders = $this->orderCollection->filter(['status'=>'pending']);
   foreach($orders as $order){
     //assign unique microtime to session
     $mt = round(microtime(true) * 1000);
     if(!isset($_SESSION['cancel'.$order->getId()])) $_SESSION['cancel'.$order->getId()] = $mt;
     //check if order is ready to be canceled
     $isCanceled = $this->isCanceled($order->getId());
     if($isCanceled === false) continue;
     if($order->getAlreadyRefund() == '0'){ // check if already refund
       $order->setAlreadyRefund('1')->save();
       //check if microtime is the same as the first one that running
       if($_SESSION['cancel'.$order->getId()] == $mt){
        //update using lock row
        $this->_dbConnection->beginTransaction(); 
        $sqlRaws[] =  "SELECT * FROM ewallet WHERE customer_id = ".$order->getCustomerId()." FOR UPDATE;";
        $sqlRaws[] =  "UPDATE ewallet SET balance =(balance+".$order->getPrice().") WHERE customer_id = ".$order->getCustomerId().";";
        foreach ($sqlRaws as $sqlRaw) {
          $this->_dbConnection->query($sqlRaw);
        }
        $this->_dbConnection->commit(); 

       }
     }
     unset($_SESSION['cancel'.$order->getId()]);
     $order->setStatus('canceled')->save();
   }
}

लेकिन समस्या तब भी बनी रहती है जब मैं एक स्ट्रीम्स टेस्ट कर रहा होता हूं, क्योंकि एक ऐसा मामला होता है जब एक ही फ़ंक्शन एक ही माइक्रोटाइम पर एक ही डेटा को प्रोसेस करता है और एक ही सटीक समय पर mysql लेनदेन शुरू करता है


इसके लिए आपको mysql TransACT का उपयोग करना होगा और Select for UPDATE का उपयोग करना होगा।
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html

यदि आप पीडीओ का उपयोग कर रहे हैं, तो आपका फ़ंक्शन सेटएलेरेडिरेफंड () उस तरह दिख सकता है:

function setAlredyRefund($orderID){
    try{
        $pdo->beginTransaction();

        $sql = "SELECT * FROM sales_order WHERE order_id = :order_id AND already_refund = 0 FOR UPDATE";
        $stmt = $pdo->prepare($sql);
        $stmt->bindParam(":orderID", $orderID, PDO::PARAM_INT);
        $stmt->execute();       

        $sql = "UPDATE sales_order SET already_refund = 1";
        $stmt = $pdo->prepare($sql);
        $stmt->execute();       

        $pdo->commit();

    } 

    catch(Exception $e){    
        echo $e->getMessage();    
        $pdo->rollBack();
    }
}

यदि मैं समझता हूं, जब आप कहते हैं "2 अलग-अलग क्रोन शेड्यूल एक ही समय में एक ही डेटा को संसाधित कर सकते हैं", तो आपका कहना है कि स्क्रिप्ट के 2 उदाहरण एक ही समय में चल सकते हैं यदि पहला उदाहरण कार्य को पूरा करने में 5 मिनट से अधिक समय लेता है। ?

मुझे नहीं पता कि आपके कोड का कौन-सा भाग सबसे अधिक समय लेता है, लेकिन मुझे लगता है कि यह स्वयं की प्रक्रिया है। इस तरह से एक मामले में मैं क्या करूंगा:

  1. status = 'pending' साथ सीमित संख्या में आदेशों का चयन करें
  2. status='refunding' जैसी किसी चीज़ के लिए चुने गए सभी ऑर्डर को तुरंत अपडेट करें
  3. धनवापसी की प्रक्रिया करें और प्रत्येक धनवापसी के बाद संबंधित ऑर्डर को status='cancelled' करने के लिए अपडेट करें।

इस तरह अगर एक और क्रोन नौकरी शुरू की जाती है, तो यह प्रक्रिया के लिए लंबित आदेशों के एक पूरी तरह से अलग सेट का चयन करेगी।


रिक जेम्स के जवाब शो जैसे लेनदेन के अलावा।

आप विशिष्ट कार्य करने के लिए शेड्यूल नियमों का उपयोग कर सकते हैं केवल एक कार्यकर्ता द्वारा संसाधित किया जा सकता है।

उदाहरण के लिए 1 कार्य के लिए भी निर्धारित आईडी के साथ नौकरी, और विषम आईडी के साथ कार्य 2 के लिए निर्धारित है।


यदि मैं आप होते, तो मैं इसे दो चरणों वाली प्रक्रिया बना देता: एक कॉलम "पहले से ही_मुंड" के बजाय, मेरे पास एक कॉलम "रिफंड_स्टैटस" होता और क्रॉन जॉब पहले इस कॉलम को "to_refund" में बदल देता और फिर, अगले पर एक ही प्रकार की क्रोन जॉब या एक अलग क्रोन जॉब में, जब वास्तविक धनवापसी होती है, इसे फिर से "रिफंड" में बदल दें।

मुझे पता है कि शायद आप इसे एक ही समय में पूरा कर सकते हैं, लेकिन कई बार अधिक समझदार कोड / प्रक्रिया करना बेहतर होता है, भले ही इसमें थोड़ा अधिक समय लग सकता है। खासतौर पर तब जब आप पैसे का लेन-देन कर रहे हों ...


आप एक Pidfile का उपयोग करना चाह सकते हैं। एक Pidfile में किसी दिए गए प्रोग्राम की प्रोसेस आईडी होती है। दो जाँचें होंगी: पहला, यदि फ़ाइल स्वयं मौजूद है और दूसरी बात, यदि फ़ाइल में प्रक्रिया आईडी चल रही प्रक्रिया की है।

<?php

class Mutex {

    function lock() {

        /**
         * $_SERVER['PHP_SELF'] returns the current script being executed.
         * Ff your php file is located at http://www.yourserver.com/script.php,
         * PHP_SELF will contain script.php
         *
         * /!\ Do note that depending on the distribution, /tmp/ content might be cleared
         * periodically!
         */
        $pidfile = '/tmp/' . basename($_SERVER['PHP_SELF']) . '.pid';
        if (file_exists($pidfile)) {
            $pid = file_get_contents($pidfile);
            /**
             * Signal 0 is used to check whether a process exists or not
             */
            $running = posix_kill($pid, 0);
            if ($running) {
                /**
                 * Process already running
                 */
                exit("process running"); // terminates script
            } else {
                /**
                 * Pidfile contains a pid of a process that isn't running, remove the file
                 */
                unlink($pidfile);
            }
        }
        $handle = fopen($pidfile, 'x'); // stream
        if (!$handle) {
            exit("File already exists or was not able to create it");
        }
        $pid = getmypid();
        fwrite($handle, $pid); // write process id of current process

        register_shutdown_function(array($this, 'unlock')); // runs on exit or when the script terminates

        return true;
    }

    function unlock() {
        $pidfile = '/tmp/' . basename($_SERVER['PHP_SELF']) . '.pid';
        if (file_exists($pidfile)) {
            unlink($pidfile);
        }
    }
}

आप इसे इस तरह से उपयोग कर सकते हैं:

$mutex = new Mutex();
$mutex->lock();
// do something
$mutex->unlock();

इसलिए, अगर दो समवर्ती क्रोन प्रक्रियाएं हैं (यह एक ही फ़ाइल होनी चाहिए!), अगर एक ने ताला लगा लिया, तो दूसरा बंद हो जाएगा।


@ रिक जेम्स उत्तर हमेशा की तरह महान है, उसने अभी आपको यह नहीं बताया कि आपको किस डेटा को लॉक करने की आवश्यकता है।

पहले आपने जो कहा, उस पर टिप्पणी करूँ

लेकिन समस्या अभी भी बनी हुई है जब मैं एक स्ट्रीम्स टेस्ट कर रहा हूं,

कंज्यूरे-अवेयर एप्लिकेशन का परीक्षण केवल तनाव परीक्षणों द्वारा नहीं किया जाता है क्योंकि आप नियंत्रित नहीं करते हैं कि क्या होने जा रहा है और आप अशुभ हो सकते हैं और अच्छे परिणाम में परीक्षा परिणाम हो सकते हैं, जबकि आपके पास अभी भी आपके आवेदन में डरपोक बग है - और मुझ पर विश्वास करें कीड़े सबसे खराब हैं :( -

आपको 2 क्लाइंट (डीबी सत्र) खोलने और दौड़ की स्थिति को अपने हाथ से अनुकरण करने की आवश्यकता है, MySQL कार्यक्षेत्र में 2 कनेक्शन खोलने के लिए पर्याप्त है।

आइए इसे करते हैं, अपने क्लाइंट (MySQL Workbench या phpMyAdmin) में 2 कनेक्शन खोलें और इस क्रम में इन कथनों को निष्पादित करें, उन्हें उसी समय अपने PHP स्क्रिप्ट के रूप में सोचें।

**sales_order**
+--------+-------+----------+--------+--------------+-----------+
|order_id| price |product_id| status |already_refund|customer_id|
+--------+-------+----------+--------+--------------+-----------+
|   1    | 1000  |    1     |canceled|      1       |     2     |
|   2    | 2000  |    2     |pending |      0       |     2     |
|   3    | 3000  |    3     |complete|      0       |     1     | 
+--------+-------+----------+--------+--------------+-----------+


(SESSION 1) > select * from sales_order where status = 'pending';
-- result 1 row (order_id 2)
(SESSION 2) > select * from sales_order where status = 'pending';
-- result 1 row (order_id 2)
/*
 >> BUG: Both sessions are reading that order 2 is pending and already_refund is 0

 your session 1 script is going to see that this guy needs to cancel
 and his already_refund column is 0 so it will increase his wallet with 2000
*/
(SESSION 1) > update sales_order set  status = 'canceled' , already_refund = 1
              where  order_id = 2
(SESSION 1) > update ewallet set balance = balance + 2000 where customer_id = 2
/*
 same with your session 2 script : it is going to see that this guy needs
 to cancel and his already_refund column is 0 so it will increase his 
 wallet with 2000
*/
(SESSION 2) > update sales_order set  status = 'canceled' , already_refund = 1
              where  order_id = 2
(SESSION 2) > update ewallet set balance = balance + 2000 where customer_id = 2

अब ग्राहक 2 इस वजह से खुश होगा, और यह मामला वही है जो आपने के लिए सवाल पूछा था (कल्पना करें कि 5 सत्र पहले से ही आदेश पढ़ सकता है। यह उनके द्वारा 1 में अपडेट किया गया है, ग्राहक 2 सुपर खुश होगा क्योंकि वह 5 * 2000 मिल रहा है)

मुझे: अब अपना समय ले लो और इस परिदृश्य के बारे में सोचो, आपको कैसे लगता है कि आप खुद को इससे बचा सकते हैं? ..?

you: लॉकिंग @Rick के रूप में कहा

मैं: बिल्कुल!

you: ठीक है, अब मैं जाऊंगा और ewallet टेबल को लॉक ewallet

me: Noo, आपको sales_order को लॉक करने की आवश्यकता है इसलिए SESSION 2 डेटा को तब तक नहीं पढ़ सकता जब तक कि SESSION1 खत्म नहीं हो जाता है, अब चलो लॉक लगाकर परिदृश्य को बदलते हैं।

(SESSION 1) > START TRANSACTION;
-- MySQL > OK;
(SESSION 2) > START TRANSACTION;
-- MySQL > OK;
(SESSION 1) > select * from sales_order where status = 'pending' FOR UPDATE;
-- MySQL > OK result 1 row (order_id 2)
(SESSION 2) > select * from sales_order where status = 'pending' FOR UPDATE;
-- MySQL > WAAAAAAAAAAAAAAAIT ...... THE DATA IS LOCKED
/*
 now session 2 is waiting for the result of the select query .....

 and session 1 is going to see that this guy needs to cancel and his
 already_refund column is 0 so it will increase his  wallet with 2000
*/
(SESSION 1) > update sales_order set  status = 'canceled' , already_refund = 1
          where  order_id = 2
(SESSION 1) > update ewallet set balance = balance + 2000 where customer_id = 2;
(SESSION 2) >  :/  I am still waiting for the result of the select .....
(SESSION 1) > COMMIT;
-- MySQL > OK , now I will release the lock so any other session can read the data
-- MySQL > I will now execute the select statement of session 2
-- MySQL > the result of the select statement of session 2 is 0 rows
(SESSION 2) >  /* 0 rows ! no pending orders ! 
               Ok just end the transaction, there is nothing to do*/

अब आप खुश हैं ग्राहक 2 नहीं!

नोट 1:

SELECT * from sales_order where status = 'pending' FOR UPDATE इस कोड में लागू SELECT * from sales_order where status = 'pending' FOR UPDATE केवल pending ऑर्डर लॉक नहीं हो सकते क्‍योंकि यह status कॉलम पर खोज की स्थिति का उपयोग करता है और यूनिक इंडेक्स का उपयोग नहीं करता

MySQL manual कहा गया है

लॉकिंग रीड्स के लिए (सेलेक्ट फॉर UPDATE या SHARE), UPDATE और DELETE स्टेटमेंट्स के लिए लॉक किए गए लॉक इस बात पर निर्भर करते हैं कि स्टेटमेंट यूनिक इंडेक्स के साथ यूनिक सर्च कंडीशन या रेंज-टाइप सर्च कंडीशन का उपयोग करता है या नहीं।
.......

अन्य खोज स्थितियों के लिए, और गैर-अनूठे अनुक्रमितों के लिए, InnoDB सूचकांक श्रेणी को स्कैन करता है ...

(और यह उन सबसे अधिक चीजों में से एक है जिनसे मैं MySQL के बारे में नफरत करता हूं। मेरी इच्छा है कि मैं केवल चुनिंदा बयान द्वारा लौटी पंक्तियों को बंद कर दूं :(

नोट 2

मुझे आपके आवेदन के बारे में नहीं पता है, लेकिन अगर यह क्रोन मिशन केवल लंबित आदेशों को रद्द करने के लिए है, तो इससे छुटकारा पाएं और उपयोगकर्ता द्वारा अपने आदेश को रद्द करने पर बस रद्द करने की प्रक्रिया शुरू करें।

इसके अलावा अगर पहले से ही already_refund कॉलम कॉलम हमेशा 1 के साथ अपडेट किया जाता है, तो स्टेटस कॉलम को canceled करने के लिए अपडेट किया जाता है, तो "रद्द आदेश का अर्थ है कि उसे भी वापस कर दिया गया है" , और पहले से ही already_refund कॉलम से छुटकारा already_refund , अतिरिक्त डेटा = अतिरिक्त कार्य और अतिरिक्त समस्याएं

लॉकिंग रीड्स के MySQL डॉक्यूमेंटेशन उदाहरण "लॉकिंग रीड एग्जाम्स" पर स्क्रॉल करते हैं


यहाँ एक लॉक फ़ाइल के साथ सरल समाधान है:

<?php

// semaphore read lock status
$file_sem = fopen( "sem.txt", "r" );
$str = fgets( $file_sem );
fclose( $file_sem );
$secs_last_mod_file = time() - filemtime( "sem.txt" );

// if ( in file lock value ) and ( difference in time between current time and time of file modifcation less than 600 seconds ),
// then it means the same process running in another thread
if( ( $str == "2" ) && ( $secs_last_mod_file < 600 ) )
{
    die( "\n" . "----die can't put lock in file" . "\n" );
}
// semaphore open lock
$file_sem = fopen( "sem.txt", "w" );
fputs( $file_sem, "2" );
fflush( $file_sem );
fclose( $file_sem );


// Put your code here


// semaphore close lock
$file_sem = fopen( "sem.txt", "w" );
fputs( $file_sem, "1" );
fclose( $file_sem );

?>

मैं अपनी साइटों में इस समाधान का उपयोग करता हूं।


इस समस्या का एक सरल समाधान है। फॉर्म की एक क्वेरी का उपयोग करें UPDATE sales_order SET already_refund = 1 WHERE already_refund = 0 AND id = ? अपडेट के परिणाम में प्रभावित पंक्तियों की संख्या शामिल होनी चाहिए जो शून्य या एक होगी। यदि यह एक है, तो महान ईवालेट करें अन्यथा इसे किसी अन्य प्रक्रिया द्वारा अपडेट किया गया था।





race-condition