మీరు ఆర్డర్ను సేవ్ చేశారు, కానీ ఆర్డర్ ఐటమ్స్ డేటాబేస్లోకి వెళ్లలేదు. లేదా బహుశా స్టాక్ కౌంటర్ తగ్గిపోయింది, పేమెంట్ గేట్వే టైమ్ అవుట్ అయ్యింది, మరియు ఇప్పుడు కస్టమర్ కార్డు నుండి డబ్బు కట్ అయ్యింది కానీ ఆర్డర్ రికార్డు లేదు. ఇవి కేవలం అరుదుగా జరిగే సంఘటనలు (edge cases) అనిపిస్తాయి, కానీ అవి అలా కాదు. ఒక బిజీ అప్లికేషన్లో, ఇవి రోజువారీ సమస్యలుగా మారుతాయి.
దీనికి మూల కారణం దాదాపు ఎప్పుడూ ఒకటే: విడివిడి సంఘటనలుగా పరిగణించబడిన డేటాబేస్ రైట్స్ (database writes) యొక్క క్రమం. ఒకటి విఫలమైనప్పుడు, మిగిలినవి వెనుకబడిపోతాయి. దీనికి పరిష్కారం డేటాబేస్ ట్రాన్సాక్షన్ (database transaction), మరియు Laravelలో, దీని కోసం DB::transaction() అనే టూల్ ఉంది.
ట్రాన్సాక్షన్ నిజంగా ఏమి హామీ ఇస్తుంది
డేటాబేస్ ట్రాన్సాక్షన్ బహుళ ఆపరేషన్లను ఒకే యూనిట్ ఆఫ్ వర్క్గా (unit of work) కలుపుతుంది. లోపల ఉన్నవన్నీ శాశ్వతంగా కమిట్ (commit) అవుతాయో లేదా పూర్తిగా రోల్ బ్యాక్ (rollback) అవుతాయో డేటాబేస్ ఇంజిన్ గ్యారెంటీ ఇస్తుంది. మధ్యస్థంగా ఏదీ ఉండదు.
ఒక బ్యాంక్ ట్రాన్స్ఫర్ను ఊహించుకోండి. సిస్టమ్ ఒక ఖాతా నుండి డబ్బును డెబిట్ చేసి, మరొక ఖాతాకు క్రెడిట్ చేయాలి. డెబిట్ అయిన తర్వాత క్రెడిట్ విఫలమైతే, ఆ డబ్బు మాయమైపోదు. బ్యాంక్ ఆ డెబిట్ను రివర్స్ చేస్తుంది. ఆ రివర్సల్ ప్రక్రియనే రోల్ బ్యాక్ అంటారు. రెండు దశలు విజయవంతమైతే, ట్రాన్స్ఫర్ కమిట్ చేయబడింది, అంటే కొత్త బ్యాలెన్స్లు శాశ్వతంగా సేవ్ చేయబడ్డాయి.
ఈ 'అన్నీ లేదా ఏమీ లేదు' (all-or-nothing) ప్రవర్తనే మీ డేటాను కన్సిస్టెంట్గా ఉంచుతుంది. ఇది లేకపోతే, పాక్షిక వైఫల్యాల వల్ల సగం రాసిన రికార్డులు మీ టేబుల్స్ అంతటా చెల్లాచెదురు అవుతాయి, మరియు ఆ రికార్డులు మీ డేటాబేస్లో పాడైపోతాయి ఎందుకంటే వాటిని సురక్షితంగా ఎలా క్లీన్ చేయాలో ఏ ఆటోమేటెడ్ ప్రాసెస్ కి తెలియదు.
Laravel దీనిని ఎలా నిర్వహిస్తుంది
సాధారణ SQLలో, మీరు స్వయంగా BEGIN, COMMIT, మరియు ROLLBACK అని రాయాల్సి ఉంటుంది, మరియు ట్రాన్సాక్షన్ మధ్యలో ఆగిపోకుండా ఉండటానికి ప్రతి సాధ్యమయ్యే ఎర్రర్ను క్యాచీ (catch) చేయడం మర్చిపోకూడదు. Laravel ఆ బోయిలర్ప్లేట్ (boilerplate) కోడ్ను ఒకే మెథడ్గా మార్చిస్తుంది.
మీరు DB::transaction() కి ఒక క్లోజర్ (closure) ను పంపిస్తారు. Laravel ట్రాన్సాక్షన్ను ప్రారంభిస్తుంది, మీ కోడ్ను రన్ చేస్తుంది, మరియు ఒకవేళ క్లోజర్ ఎటువంటి ఎక్సెప్షన్ (exception) లేకుండా పూర్తయితే, అది ఆటోమేటిక్గా కమిట్ అవుతుంది. ఏదైనా విఫలమైతే, Laravel ఆ ఎక్సెప్షన్ను క్యాచీ చేసి, అన్నింటినీ రోల్ బ్యాక్ చేస్తుంది, మరియు మీ లాగింగ్ మరియు ఎర్రర్ హ్యాండ్లింగ్ ఆశించిన విధంగా పనిచేయడానికి ఎర్రర్ను తిరిగి త్రో (rethrow) చేస్తుంది.
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) లేకపోవడం వల్ల లేదా డేటాబేస్ కనెక్షన్ తెగిపోవడం వల్ల పేమెంట్ ఇన్సర్ట్ విఫలమైతే, ఆర్డర్, ఆర్డర్ ఐటమ్స్ మరియు స్టాక్ మార్పులు అన్నీ రద్దవుతాయి. కారణం లేకుండా స్టాక్ తగ్గిపోవడం లేదా చెల్లింపు జరగని ఆర్డర్ను షిప్ చేయాల్సి రావడం వంటి సమస్యలు మీకు ఎదురవ్వవు.
ఇది మీకు ఎక్కడ ఉపయోగపడుతుంది
కొన్ని వర్క్ఫ్లోలు (workflows) పూర్తిగా ట్రాన్సాక్షనల్ సేఫ్టీపై ఆధారపడి ఉంటాయి. చెక్అవుట్ ఉదాహరణ అత్యంత స్పష్టమైనది, కానీ ఈ ప్యాటర్న్ ప్రతిచోటా కనిపిస్తుంది.
యూజర్ రిజిస్ట్రేషన్. ఒక యూజర్ రో (row) సృష్టించడం, తర్వాత ప్రొఫైల్ రో, ఆపై డిఫాల్ట్ సెట్టింగ్స్. ఒకవేళ వాలిడేషన్ సమస్య వల్ల ప్రొఫైల్ ఇన్సర్ట్ విఫలమైతే, ప్రొఫైల్ లేని యూజర్ ఒక 'ఘోస్ట్ అకౌంట్' (ghost account) గా మిగిలిపోతారు. ప్రతి యూజర్కు ప్రొఫైల్ ఉంటుందని భావించే ఏ పేజీ అయినా క్రాష్ అవ్వచ్చు లేదా బ్రోకెన్ UIని చూపించవచ్చు.
బల్క్ ఇంపోర్ట్స్. యాభై రికార్డులను ఇన్సర్ట్ చేసే CSV అప్లోడ్లో, ఇరవై ఆరో రికార్డులో డేట్ ఫార్మాట్ తప్పుగా ఉన్నందున మిగిలిన ఇరవై ఐదు రికార్డులు వెనుకబడి ఉండకూడదు. బ్యాచ్ మొత్తాన్ని ఒక ట్రాన్సాక్షన్లో ఉంచడం వల్ల మొత్తం ఇంపోర్ట్ ఒకే యూనిట్గా విఫలమవుతుంది. దీనివల్ల అడ్మిన్ ఏ రికార్డులు మిగిలిపోయాయో వెతకడానికి గంటల సమయం వృథా చేయకుండా, ఫైల్ను సరిచేసి మళ్ళీ ప్రయత్నించవచ్చు.
ఇన్వెంటరీ మరియు అకౌంటింగ్. ఒక టేబుల్ భౌతిక వనరులను (physical resource) ట్రాక్ చేస్తున్నప్పుడు మరియు మరొక టేబుల్ డబ్బు లేదా క్రెడిట్లను ట్రాక్ చేస్తున్నప్పుడు, ఆ రెండూ కలిసి కదలాలి. వాటిని విడదీయడం వల్ల అకౌంట్స్ సరిపోలని ఆడిట్లు మరియు తప్పుడు రిపోర్టులు వచ్చే అవకాశం ఉంది.
మీరు మాన్యువల్గా నిర్వహించాల్సినప్పుడు
క్లోజర్ విధానం చాలా సందర్భాలలో సరిపోతుంది, కానీ కొన్నిసార్లు మీకు మరింత నియంత్రణ అవసరం కావచ్చు. సర్వీస్ క్లాస్ లోపల సంక్లిష్టమైన కండిషనల్ లాజిక్ ఉండటం లేదా రన్టైమ్లో కమిట్ చేయాలా వద్దా అని నిర్ణయించాల్సి రావడం వల్ల సింగిల్ క్లోజర్ వాడటం ఇబ్బందిగా అనిపించవచ్చు. అటువంటి సమయాల్లో, మీరు ట్రాన్సాక్షన్ను స్వయంగా నిర్వహించవచ్చు:
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 block) లోని క్రమాన్ని గమనించండి: మొదట రోల్ బ్యాక్ చేయండి, ఆపై త్రో చేయండి. మీరు రోల్ బ్యాక్ చేయకముందే త్రో చేస్తే, ట్రాన్సాక్షన్ కనెక్షన్లో అలాగే ఉండిపోతుంది. ఇది రోస్ను లాక్ చేయవచ్చు, ఇతర క్వెరీలను డెడ్లాక్ (deadlock) చేయవచ్చు లేదా మీ కనెక్షన్ పూల్ను ఖాళీ చేయవచ్చు. మాన్యువల్ ట్రాన్సాక్షన్లు శక్తివంతమైనవి, కానీ వాటి క్లీనప్ బాధ్యత మీపైనే ఉంటుంది.
సైడ్ ఎఫెక్ట్స్ (Side Effects) ను ట్రాన్సాక్షన్ నుండి దూరంగా ఉంచండి
ప్రొడక్షన్లో టీమ్లకు ఇబ్బంది కలిగించే నియమం ఇదే. ఒక ట్రాన్సాక్షన్ కేవలం డేటాబేస్ పనులను మాత్రమే రోల్ బ్యాక్ చేయగలదు. అది పంపిన ఈమెయిల్ను వెనక్కి తీసుకోలేదు, క్లౌడ్ స్టోరేజ్ నుండి ఫైల్ను డిలీట్ చేయలేదు లేదా పేమెంట్ API ద్వారా రీఫండ్ చేయలేదు.
మీరు ట్రాన్సాక్షన్ క్లోజర్ లోపల Mail::send() కాల్ చేస్తే, మరియు రెండు లైన్ల తర్వాత డేటాబేస్ రోల్ బ్యాక్ అయితే, ఆ ఈమెయిల్ కస్టమర్ ఇన్బాక్స్కు చేరుతుంది. మీ సిస్టమ్లో లేని ఆర్డర్ కోసం రిసీవర్కు ఇన్వాయిస్ అందుతుంది. Slack నోటిఫికేషన్లు, S3కి ఫైల్ అప్లోడ్లు లేదా వెబ్హుక్ డిస్పాచ్ల విషయంలో కూడా ఇదే వర్తిస్తుంది.
సరైన క్రమం ఇది:
- ట్రాన్సాక్షన్ను పూర్తి చేయండి మరియు మీకు కావలసిన IDలు లేదా ఫలితాలను సేకరించండి.
- ఆ తర్వాతే బాహ్య సైడ్ ఎఫెక్ట్స్ను (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);
బాహ్య కాల్స్ను ట్రాన్సాక్షన్ బయట ఉంచడానికి రెండవ కారణం: సమయం. ఒక ట్రాన్సాక్షన్ లాక్లను (locks) కలిగి ఉంటుంది మరియు డేటాబేస్ కనెక్షన్ను బిజీగా ఉంచుతుంది. ట్రాన్సాక్షన్ లోపల ఉన్నప్పుడు Stripe API స్పందన కోసం మూడు సెకన్ల పాటు వేచి ఉండటం అంటే, మూడు సెకన్ల అనవసరమైన లాక్ సమయం అని అర్థం. ట్రాన్సాక్షన్ను తక్కువ సమయంలో మరియు వేగంగా పూర్తి అయ్యేలా ఉంచండి.
ఈ బగ్ ఎందుకు దాగి ఉంటుంది
ఒకే యూజర్ మరియు లోకల్ డేటాబేస్తో ఉన్న డెవలప్మెంట్ మెషీన్లో, సంబంధిత ఇన్సర్ట్లు (inserts) దాదాపు ఎప్పుడూ విజయవంతమవుతాయి. నెట్వర్క్ స్థిరంగా ఉంటుంది, డిస్క్ ఎప్పుడూ నిండదు మరియు ఎటువంటి పోటీ లోడ్ (competing load) ఉండదు. కోడ్ సాధారణంగా పనిచేస్తుంది కాబట్టి అది సరిగ్గా ఉన్నట్లు కనిపిస్తుంది.
ప్రొడక్షన్ (Production) వాతావరణం వేరుగా ఉంటుంది. ఇద్దరు కస్టమర్లు సరిగ్గా ఒకే మిల్లీసెకనులో ఆర్డర్లను సమర్పిస్తారు. డిప్లాయ్మెంట్ సమయంలో క్యూ వర్కర్ (queue worker) మధ్యలో రీస్టార్ట్ అవుతుంది. పేమెంట్ ప్రొవైడర్ ముప్పై సెకన్ల పాటు టైమ్ అవుట్ అవుతుంది. ట్రాన్సాక్షన్లు లేకపోతే, ఈ సంఘటనలు అనామక రికార్డులను (orphan records) మరియు సరిపోని మొత్తాలను (mismatched totals) సృష్టిస్తాయి, వీటిని గుర్తించడం చాలా కష్టం. అన్నిటికంటే దారుణమైన విషయం ఏమిటంటే, స్టాండర్డ్ ఫీచర్ టెస్ట్లు వీటిని అరుదుగా మాత్రమే గుర్తిస్తాయి, ఎందుకంటే ఈ వైఫల్యం సమయంపై ఆధారపడి ఉంటుంది మరియు ఇన్ఫ్రాస్ట్రక్చర్తో ముడిపడి ఉంటుంది, ఇది కేవలం లాజిక్ ఎర్రర్స్ కాదు.
దీని పరిష్కారం ఏదో వింతైనది కాదు. ఇది యాంత్రికమైనది. మీరు రెండు లేదా అంతకంటే ఎక్కువ సంబంధిత రైట్లను (writes) చూసినప్పుడు, వాటిని ఒక ట్రాన్సాక్షన్లో చుట్టండి (wrap). కాలక్రమేణా, ఇది ఒక రిక్వెస్ట్ను వాలిడేట్ చేయడం అంతా ఆటోమేటిక్గా మారిపోవాలి.
దీనిని ఒక అలవాటుగా మార్చుకోండి
DB::transaction() సంక్లిష్టతను పెంచదు. ఇది జరిగిన తర్వాత పాక్షిక డేటాను శుభ్రం చేయడానికి ప్రయత్నించే దాగి ఉన్న సంక్లిష్టతను తొలగిస్తుంది. డేటాబేస్ ఆపరేషన్ల సమూహం ఒకదానితో ఒకటి సంబంధం కలిగి ఉంటే, మొదటి నుండి వాటిని అలాగే పరిగణించండి. దీనివల్ల మీ అప్లికేషన్ లోడ్ ఉన్నప్పుడు కూడా ఖచ్చితంగా పనిచేస్తుంది, మీ ఎర్రర్ లాగ్లు క్లీన్గా ఉంటాయి మరియు మీ డేటాబేస్ సగం పూర్తయిన రికార్డుల స్మశానవాటికగా మారదు.
