നിങ്ങൾ ഓർഡർ സേവ് ചെയ്തു, പക്ഷേ ഓർഡർ ഐറ്റങ്ങൾ ഡാറ്റാബേസിൽ എത്തിയില്ല. അല്ലെങ്കിൽ സ്റ്റോക്ക് കൗണ്ടർ കുറഞ്ഞു, പേയ്‌മെന്റ് ഗേറ്റ്‌വേ ടൈമൗട്ട് ആയി, ഇപ്പോൾ ഉപഭോക്താവിന്റെ കാർഡിൽ പണം കുറഞ്ഞു പക്ഷേ ഓർഡർ റെക്കോർഡ് ഇല്ല. ഇത്തരം സാഹചര്യങ്ങൾ സാധാരണയായി സംഭവിക്കാത്ത കാര്യങ്ങളാണെന്ന് തോന്നാം, എന്നാൽ അവ സംഭവിക്കുമ്പോൾ വലിയ പ്രശ്നങ്ങളാകുന്നു. തിരക്കേറിയ ഒരു ആപ്ലിക്കേഷനിൽ ഇവ ദിവസേനയുള്ള തലവേദനയായി മാറും.

ഇതിന്റെ മൂലകാരണം മിക്കവാറും ഒന്നാണ്: ഡാറ്റാബേസ് എഴുത്തുകൾ (writes) വേറിട്ടതും സ്വതന്ത്രവുമായ സംഭവങ്ങളായി പരിഗണിക്കുന്നത്. ഒന്ന് പരാജയപ്പെട്ടാൽ മറ്റുള്ളവ ബാക്കിയാകുന്നു. ഇതിനുള്ള പരിഹാരം ഒരു ഡാറ്റാബേസ് ട്രാൻസാക്ഷൻ (database transaction) ആണ്, Laravel-ൽ ഇതിനായി DB::transaction() എന്ന ടൂൾ ഉപയോഗിക്കാം.

ഒരു ട്രാൻസാക്ഷൻ യഥാർത്ഥത്തിൽ എന്താണ് വാഗ്ദാനം ചെയ്യുന്നത്

ഒരു ഡാറ്റാബേസ് ട്രാൻസാക്ഷൻ ഒന്നിലധികം പ്രവർത്തനങ്ങളെ ഒരു ഏകകമായി (single unit of work) ബന്ധിപ്പിക്കുന്നു. ഇതിനുള്ളിലെ കാര്യങ്ങളെല്ലാം ഒന്നുകിൽ സ്ഥിരമായി സേവ് ചെയ്യപ്പെടും (commit), അല്ലെങ്കിൽ പൂർണ്ണമായും റദ്ദാക്കപ്പെടും (rollback). ഇതിനിടയിൽ ഒരു അവസ്ഥയുമില്ല.

ഒരു ബാങ്ക് ട്രാൻസ്ഫർ സങ്കൽപ്പിക്കുക. സിസ്റ്റം ഒരു അക്കൗണ്ടിൽ നിന്ന് പണം കുറയ്ക്കുകയും (debit) മറ്റൊന്നിലേക്ക് ചേർക്കുകയും (credit) വേണം. ഡെബിറ്റ് ചെയ്തതിന് ശേഷം ക്രെഡിറ്റ് പരാജയപ്പെട്ടാൽ, പണം വെറുതെ അപ്രത്യക്ഷമാകില്ല. ബാങ്ക് ആ ഡെബിറ്റ് തിരിച്ചുപിടിക്കുന്നു (reverse). ആ തിരിച്ചുപിടിക്കലാണ് റോളബാക്ക് (rollback). രണ്ട് ഘട്ടങ്ങളും വിജയിച്ചാൽ, ട്രാൻസ്ഫർ കമ്മറ്റ് (commit) ചെയ്യപ്പെടുന്നു, അതായത് പുതിയ ബാലൻസ് സുരക്ഷിതമായി സേവ് ചെയ്യപ്പെടുന്നു.

ഈ 'എല്ലാം അല്ലെങ്കിൽ ഒന്നുമില്ല' (all-or-nothing) എന്ന രീതിയാണ് നിങ്ങളുടെ ഡാറ്റാ കൺസിസ്റ്റൻസി (consistency) നിലനിർത്തുന്നത്. ഇതില്ലെങ്കിൽ, പകുതി മാത്രം പൂർത്തിയായ റെക്കോർഡുകൾ ടേബിളുകളിൽ ചിതറിക്കിടക്കും, അവ സുരക്ഷിതമായി നീക്കം ചെയ്യാൻ ഒരു ഓട്ടോമേറ്റഡ് പ്രോസസ്സും ഇല്ലാത്തതിനാൽ അവ ഡാറ്റാബേസിൽ വെറുതെ കിടക്കും.

Laravel ഇത് എങ്ങനെയാണ് കൈകാര്യം ചെയ്യുന്നത്

സാധാരണ SQL-ൽ, നിങ്ങൾ തന്നെ BEGIN, COMMIT, ROLLBACK എന്നിവ എഴുതേണ്ടി വരും, കൂടാതെ ഒരു ട്രാൻസാക്ഷനും പാതിവഴിയിൽ നിൽക്കാതിരിക്കാൻ ഓരോ പിശകും (error) ശ്രദ്ധിക്കണം. Laravel ഈ സങ്കീർണ്ണതകളെ ഒരു സിംഗിൾ മെത്തേഡിലേക്ക് ചുരുക്കുന്നു.

നിങ്ങൾ DB::transaction()-ലേക്ക് ഒരു closure പാസ് ചെയ്യുന്നു. Laravel ട്രാൻസാക്ഷൻ ആരംഭിക്കുകയും നിങ്ങളുടെ കോഡ് പ്രവർത്തിപ്പിക്കുകയും ചെയ്യുന്നു. ഒരു എക്സെപ്ഷൻ (exception) ഇല്ലാതെ closure പൂർത്തിയായാൽ, അത് സ്വയമേവ കമ്മറ്റ് ചെയ്യപ്പെടുന്നു. എന്തെങ്കിലും പരാജയപ്പെട്ടാൽ, Laravel ആ എക്സെപ്ഷൻ പിടിച്ചെടുക്കുകയും (catch), എല്ലാം റോളബാക്ക് ചെയ്യുകയും, നിങ്ങളുടെ ലോഗിംഗും എറർ ഹാൻഡ്‌ലിംഗും ശരിയായി പ്രവർത്തിക്കുന്നതിനായി ആ എറർ വീണ്ടും റിപ്പോർട്ട് ചെയ്യുകയും ചെയ്യുന്നു.

use Illuminate\Support\Facades\DB;

DB::transaction(function () {
    $order = Order::create([/* ... */]);
    
    foreach ($cart->items as $item) {
        OrderItem::create([
            'order_id' => $order->id,
            'product_id' => $item->product_id,
            'quantity' => $item->quantity,
        ]);
        
        Product::find($item->product_id)
               ->decrement('stock', $item->quantity);
    }
    
    Payment::create([
        'order_id' => $order->id,
        'amount' => $cart->total,
        'status' => 'completed',
    ]);
});

ഒരു ഫോറിൻ കീ (foreign key) ഇല്ലാത്തതുകൊണ്ടോ ഡാറ്റാബേസ് കണക്ഷൻ നഷ്ടപ്പെട്ടതുകൊണ്ടോ പേയ്‌മെന്റ് ഇൻസേർട്ട് പരാജയപ്പെട്ടാൽ, ഓർഡറും ഓർഡർ ഐറ്റങ്ങളും സ്റ്റോക്ക് മാറ്റങ്ങളും എല്ലാം റദ്ദാക്കപ്പെടും. കാരണങ്ങളില്ലാതെ സ്റ്റോക്ക് കുറയുന്നതോ, പണമടയ്ക്കാത്ത ഒരു ഓർഡർ ഷിപ്പ് ചെയ്യാൻ ആവശ്യപ്പെടുന്നതോ ആയ അവസ്ഥ നിങ്ങൾ നേരിടേണ്ടി വരില്ല.

ഇത് എവിടെയൊക്കെ നിങ്ങളെ സഹായിക്കുന്നു

ചില വർക്ക്ഫ്ലോകൾ പൂർണ്ണമായും ട്രാൻസാക്ഷണൽ സുരക്ഷയെ ആശ്രയിച്ചിരിക്കുന്നു. ചെക്കൗട്ട് ഉദാഹരണമാണ് ഏറ്റവും വ്യക്തമായത്, എന്നാൽ ഈ രീതി എല്ലായിടത്തും കാണാം.

യൂസർ രജിസ്ട്രേഷൻ. ഒരു യൂസർ റോ (row) സൃഷ്ടിക്കുക, തുടർന്ന് ഒരു പ്രൊഫൈൽ റോ, പിന്നെ ഡിഫോൾട്ട് സെറ്റിംഗുകൾ. ഒരു വാലിഡേഷൻ പ്രശ്നം കാരണം പ്രൊഫൈൽ ഇൻസേർട്ട് പരാജയപ്പെട്ടാൽ, പ്രൊഫൈലില്ലാത്ത ഒരു യൂസർ ഒരു 'ഗോസ്റ്റ് അക്കൗണ്ട്' ആയി മാറും. എല്ലാ യൂസർമാർക്കും പ്രൊഫൈൽ ഉണ്ടെന്ന് കരുതുന്ന ഏതൊരു പേജും ക്രാഷ് ആകുകയോ അല്ലെങ്കിൽ തകരാറുള്ള UI കാണിക്കുകയോ ചെയ്യും.

ബൾക്ക് ഇംപോർട്ടുകൾ. അമ്പത് റെക്കോർഡുകൾ ഇൻസേർട്ട് ചെയ്യുന്ന ഒരു CSV അപ്‌ലോഡ്, ഇരുപത്തിയാറാമത്തെ വരിയിലെ തീയതി തെറ്റായതുകൊണ്ട് ഇരുപത്തിയഞ്ച് റെക്കോർഡുകൾ മാത്രം ബാക്കി വെക്കരുത്. ഒരു ട്രാൻസാക്ഷനിൽ ഈ ബാച്ച് ഉൾപ്പെടുത്തുന്നത് വഴി മുഴുവൻ ഇംപോർട്ടും ഒരു യൂണിറ്റായി പരാജയപ്പെടാൻ അനുവദിക്കുന്നു. ഏത് വരികളാണ് പകുതി മാത്രം ഇൻസേർട്ട് ആയതെന്ന് മണിക്കൂറുകളോളം തിരയുന്നതിന് പകരം അഡ്മിന് ഫയൽ ശരിയാക്കി വീണ്ടും ശ്രമിക്കാം.

ഇൻവെന്ററിയും അക്കൗണ്ടിംഗും. ഒരു ടേബിൾ ഭൗതികമായ ഒരു റിസോഴ്സിനെയും (physical resource) മറ്റൊരു ടേബിൾ പണമോ ക്രെഡിറ്റോ ട്രാക്ക് ചെയ്യുമ്പോഴെല്ലാം, അവ രണ്ടും ഒരേപോലെ നീങ്ങണം. അവയെ വേർതിരിക്കുന്നത് കണക്കുകൾ ഒത്തുനോക്കാൻ കഴിയാത്ത അവസ്ഥയ്ക്കും തെറ്റായ റിപ്പോർട്ടുകൾക്കും കാരണമാകും.

എപ്പോഴാണ് മാനുവൽ ആയി നിയന്ത്രിക്കേണ്ടത്

Closure രീതി മിക്ക കേസുകളും പരിഹരിക്കും, എന്നാൽ ചിലപ്പോൾ നിങ്ങൾക്ക് കൂടുതൽ നിയന്ത്രണം ആവശ്യമായി വന്നേക്കാം. ഒരു സർവീസ് ക്ലാസിനുള്ളിലെ സങ്കീർണ്ണമായ കണ്ടീഷണൽ ലോജിക് അല്ലെങ്കിൽ റൺടൈമിൽ കമ്മറ്റ് ചെയ്യണോ വേണ്ടയോ എന്ന് തീരുമാനിക്കേണ്ട സാഹചര്യം എന്നിവ ഒരു സിംഗിൾ closure ഉപയോഗിക്കുന്നത് ബുദ്ധിമുട്ടാക്കാം. അത്തരം സന്ദർഭങ്ങളിൽ, നിങ്ങൾക്ക് ട്രാൻസാക്ഷൻ സ്വയം നിയന്ത്രിക്കാം:

DB::beginTransaction();

try {
    // Run your operations
    $order = Order::create([/* ... */]);
    // ... more work ...
    
    if ($someBusinessRulePasses) {
        DB::commit();
    } else {
        DB::rollBack();
    }
} catch (\Throwable $e) {
    DB::rollBack();
    throw $e;
}

catch ബ്ലോക്കിനുള്ളിലെ ക്രമം ശ്രദ്ധിക്കുക: ആദ്യം റോളബാക്ക് ചെയ്യുക, അതിനുശേഷം മാത്രം എറർ റിപ്പോർട്ട് ചെയ്യുക (throw). റോളബാക്ക് ചെയ്യുന്നതിന് മുമ്പ് നിങ്ങൾ എറർ റിപ്പോർട്ട് ചെയ്താൽ, ട്രാൻസാക്ഷൻ കണക്ഷനിൽ തുറന്നുതന്നെ കിടക്കും. ഇത് റോകളെ ലോക്ക് ചെയ്യാനോ (lock), മറ്റ് ക്വറികൾ ഡെഡ്‌ലോക്ക് (deadlock) ആക്കാനോ, അല്ലെങ്കിൽ നിങ്ങളുടെ കണക്ഷൻ പൂൾ (connection pool) തീർന്നുപോകാനോ കാരണമായേക്കാം. മാനുവൽ ട്രാൻസാക്ഷനുകൾ ശക്തമാണ്, പക്ഷേ അവയുടെ ഉത്തരവാദിത്തം നിങ്ങൾക്കുതന്നെയാണ്.

സൈഡ് ഇഫക്റ്റുകൾ (Side Effects) ട്രാൻസാക്ഷനിൽ നിന്ന് ഒഴിവാക്കുക

പ്രൊഡക്ഷനിൽ ടീമുകളെ കുടുക്കുന്ന നിയമമാണിത്. ഒരു ട്രാൻസാക്ഷന് ഡാറ്റാബേസ് ജോലികൾ മാത്രമേ റദ്ദാക്കാൻ (undo) കഴിയൂ. ഒരു ഇമെയിൽ അയച്ചതിനെ പിൻവലിക്കാനോ, ക്ലൗഡ് സ്റ്റോറേജിൽ നിന്ന് ഒരു ഫയൽ ഡിലീറ്റ് ചെയ്യാനോ, അല്ലെങ്കിൽ പേയ്‌മെന്റ് API വഴി ഒരു റീഫണ്ട് നടത്താനോ അതിന് കഴിയില്ല.

നിങ്ങൾ ട്രാൻസാക്ഷൻ closure-നുള്ളിൽ ഒരു Mail::send() കോൾ നൽകുകയും, രണ്ട് വരികൾക്ക് ശേഷം ഡാറ്റാബേസ് റോളബാക്ക് ചെയ്യപ്പെടുകയും ചെയ്താൽ, ആ ഇമെയിൽ ഉപഭോക്താവിന്റെ ഇൻബോക്സിൽ എത്തും. നിങ്ങളുടെ സിസ്റ്റത്തിൽ നിലവിലില്ലാത്ത ഒരു ഓർഡറിനായുള്ള ഇൻവോയ്സ് ഉപഭോക്താവിന് ലഭിക്കുന്നു. Slack നോട്ടിഫിക്കേഷനുകൾ, S3-ലേക്ക് ഫയൽ അപ്‌ലോഡ് ചെയ്യുക, അല്ലെങ്കിൽ webhook ഡിസ്പാച്ചുകൾ എന്നിവയ്ക്കും ഇത് ബാധകമാണ്.

ശരിയായ ക്രമം ഇതാണ്:

  1. ഇടപാട് (transaction) പൂർത്തിയാക്കി നിങ്ങൾക്ക് ആവശ്യമുള്ള ഐഡികളോ (IDs) ഫലങ്ങളോ ശേഖരിക്കുക.
  2. അതിനുശേഷം മാത്രം ബാഹ്യമായ സൈഡ് ഇഫക്റ്റുകൾ (external side effects) പ്രവർത്തിപ്പിക്കുക.

ഉദാഹരണത്തിന്, കൺഫർമേഷൻ ഇമെയിൽ കമിറ്റിന് (commit) ശേഷം ക്യൂ ചെയ്യുക, അതിനുള്ളിലല്ല:

$order = DB::transaction(function () {
    // database work only
    return Order::create([/* ... */]);
});

// Side effects happen after the database state is solid
OrderConfirmationJob::dispatch($order);

ബാഹ്യമായ കോളുകൾ (external calls) ട്രാൻസാക്ഷന് പുറത്ത് നിർത്താൻ രണ്ടാമതൊരു കാരണമുണ്ട്: സമയം. ഒരു ട്രാൻസാക്ഷൻ ലോക്കുകൾ (locks) നിലനിർത്തുകയും ഒരു ഡാറ്റാബേസ് കണക്ഷൻ തിരക്കിലാക്കി നിർത്തുകയും ചെയ്യുന്നു. ഒരു ട്രാൻസാക്ഷനുള്ളിൽ ഇരിക്കുമ്പോൾ Stripe API മറുപടിക്കായി മൂന്ന് സെക്കൻഡ് കാത്തുനിൽക്കുന്നത് എന്നത് മൂന്ന് സെക്കൻഡ് അനാവശ്യമായ ലോക്ക് സമയമാണ്. ട്രാൻസാക്ഷൻ ലളിതവും വേഗതയുള്ളതുമായി നിലനിർത്തുക.

ഈ ബഗ് എന്തുകൊണ്ട് ഒളിച്ചിരിക്കുന്നു

ഒരു സിംഗിൾ യൂസറും ലോക്കൽ ഡാറ്റാബേസുമുള്ള ഒരു ഡെവലപ്‌മെന്റ് മെഷീനിൽ, ബന്ധപ്പെട്ട ഇൻസർട്ടുകൾ (inserts) മിക്കവാറും എപ്പോഴും വിജയകരമാകും. നെറ്റ്‌വർക്ക് സ്ഥിരതയുള്ളതാണ്, ഡിസ്ക് ഒരിക്കലും നിറയുന്നില്ല, മറ്റ് ലോഡുകളും ഇല്ല. കോഡ് സാധാരണയായി പ്രവർത്തിക്കുന്നതുകൊണ്ട് അത് ശരിയാണെന്ന് തോന്നും.

പ്രൊഡക്ഷൻ (Production) സാഹചര്യം വ്യത്യസ്തമാണ്. രണ്ട് ഉപഭോക്താക്കൾ ഒരേ മില്ലിസെക്കൻഡിൽ ഓർഡറുകൾ സമർപ്പിക്കുന്നു. ഒരു ഡിപ്ലോയ്മെന്റ് സമയത്ത് ക്യൂ വർക്കർ (queue worker) പകുതി ജോലിയിൽ വെച്ച് റീസ്റ്റാർട്ട് ആകുന്നു. ഒരു പേയ്‌മെന്റ് പ്രൊവൈഡർ മുപ്പത് സെക്കൻഡ് ടൈം ഔട്ട് ആകുന്നു. ട്രാൻസാക്ഷനുകൾ ഇല്ലെങ്കിൽ, ഈ സംഭവങ്ങൾ ട്രാസ് ചെയ്യുക പ്രയാസകരമായ ഓർഫൻ റെക്കോർഡുകളും (orphan records) തെറ്റായ ടോട്ടലുകളും സൃഷ്ടിക്കും. ഏറ്റവും മോശമായ കാര്യം, സ്റ്റാൻഡേർഡ് ഫീച്ചർ ടെസ്റ്റുകൾ ഇവയെ അപൂർവ്വമായി മാത്രമേ കണ്ടെത്തുന്നുള്ളൂ എന്നതാണ്, കാരണം പരാജയപ്പെടുന്നത് ലളിതമായ ലോജിക് പിശകുകൾ കൊണ്ടല്ല, മറിച്ച് സമയത്തെയും ഇൻഫ്രാസ്ട്രക്ചറിനെയും ആശ്രയിച്ചാണ്.

ഇതൊരു ശീലമാക്കുക

DB::transaction() സങ്കീർണ്ണത കൂട്ടുന്നില്ല. മറിച്ച്, പകുതി പൂർത്തിയായ ഡാറ്റ പിന്നീട് വൃത്തിയാക്കാൻ ശ്രമിക്കുന്നതിലൂടെ ഉണ്ടാകുന്ന മറഞ്ഞിരിക്കുന്ന സങ്കീർണ്ണതയെ അത് ഒഴിവാക്കുന്നു. ഡാറ്റാബേസ് ഓപ്പറേഷനുകളുടെ ഒരു കൂട്ടം ഒന്നിച്ച് ചെയ്യേണ്ടവയാണെങ്കിൽ, തുടക്കം മുതൽ അവയെ അങ്ങനെ തന്നെ പരിഗണിക്കുക. ഇത് നിങ്ങളുടെ ആപ്ലിക്കേഷനെ ലോഡുകൾക്കിടയിലും കൃത്യതയുള്ളതാക്കും, എറർ ലോഗുകൾ വൃത്തിയുള്ളതായിരിക്കും, കൂടാതെ നിങ്ങളുടെ ഡാറ്റാബേസ് പകുതി പൂർത്തിയായ റെക്കോർഡുകളുടെ ശ്മശാനമായി മാറില്ല.