এই পর্বে যা শিখবেন
- কেন কয়েক সেবা জুড়ে একটি কাজে একটি ACID transaction পাওয়া যায় না
- 2PC-এর ধাপ, log record আর recovery — আর coordinator মরলে সবাই কেন আটকায়, তার ছোট প্রমাণ
- 2PC-এর latency, lock, hot সারির throughput আর availability নিজে হিসাব করা
- Saga নকশা: compensation, pivot, forward বনাম backward recovery, orchestration বনাম choreography আর isolation-এর ফাঁক
- Dual write-এর ফাঁদ, transactional outbox, relay আর CDC — simulator আর লাইভ SQL-এ
১ · একটি কাজ, তিনটি ডেটাবেস — যেখানে একটি ACID transaction আর নেই
ঈদের আগের রাত। একটি অনলাইন বাস-টিকিট সেবায় (কাল্পনিক) রুমানা ঢাকা–রাজশাহী রুটের ট্রিপ ৭-এর সিট A3 বেছে “৮৫০ টাকা পরিশোধ করুন” চাপলেন। তাঁর কাছে এটি একটি কাজ; সেবার ভেতরে তিন টুকরো, তিনটি সেবায়:
- সিট সার্ভিস — নিজের
seatsডেটাবেসে A3 আটকে রাখে। - পেমেন্ট সার্ভিস — নিজের
walletডেটাবেস থেকে ৮৫০ টাকা কাটে। - টিকিট সার্ভিস — বাস অপারেটরের সিস্টেম থেকে টিকিট নম্বর এনে নিজের
ticketsডেটাবেসে লেখে।
প্রতিটি সেবার নিজস্ব ডেটাবেস — database-per-service নীতি, যা microservices-এ প্রচলিত (পর্ব ৪৭)। কেউ অন্যের টেবিলে সরাসরি লেখে না, কথা হয় API বা বার্তায়; তাই প্রতিটি দল স্বাধীনভাবে বদলাতে আর scale করতে পারে। দামটা দেখা যায় ঠিক এখানে।
একটি কেনা, তিনটি commit
One purchase touches three services, each with its own database
- রুমানার app
কিনুন · ৮৫০ টাকাএকটি কাজ - সিট সার্ভিস
seats DBcommit ১ - পেমেন্ট সার্ভিস
wallet DBcommit ২ - টিকিট সার্ভিস
tickets DBcommit ৩ · বাইরের অপারেটর
একটি ডেটাবেস হলে কাজটি সহজ ছিল (পর্ব ২৪): BEGIN, তিনটি লেখা, COMMIT — transaction-এর ACID নিশ্চয়তায় তিনটিই হবে, নয়তো একটিও না, আর মাঝের অবস্থা কেউ দেখবে না। এখন তিনটি আলাদা commit; মাঝপথে থামলে:
| কোথায় থামল | ডেটার অবস্থা | ক্ষতি |
|---|---|---|
| সিট আটকানোর পর পেমেন্ট সার্ভিস বন্ধ | A3 আটকে, টাকা কাটেনি | ঈদের ভিড়ে একটি সিট অবিক্রীত পড়ে থাকে |
| টাকা কাটার পর টিকিট সার্ভিস বন্ধ | টাকা গেছে, টিকিট নেই | সবচেয়ে খারাপ — ক্ষোভ আর হিসাবের গরমিল |
| পেমেন্টের উত্তরে timeout | টাকা কেটেছে কি না অজানা | আবার চাইলে দুবার কাটার ঝুঁকি, না চাইলে টিকিট হারানোর |
শেষ দুটি সারি এই কোর্সের পরিচিত শত্রু partial failure: উত্তর না আসা দেখে বলা যায় না ওপারে কী ঘটেছে — timeout মানে “জানি না”, “হয়নি” নয়। একটি অনুরোধের জন্য সমাধান পর্ব ০৮-এ দেখেছেন (idempotency key); এবার কয়েকটি সেবা জুড়ে একটি পুরো কাজের পালা।
কেন একটি ACID transaction আর সম্ভব নয়
এক ডেটাবেসে atomicity আসে একটি সরল কৌশল থেকে: সব পরিবর্তন আগে write-ahead log-এ (WAL), শেষে একটি COMMIT record ডিস্কে fsync. ওই record টেকসই হওয়ার মুহূর্তটিই সিদ্ধান্ত; crash-এর পর log পড়ে সব আবার করা হয়, নয়তো সব বাতিল (পর্ব ১৯-এর storage engine, DBMS কোর্সের recovery পর্ব)। Isolation দেয় একটি lock manager বা MVCC (পর্ব ২৪)।
তিনটি ডেটাবেসে তিনটি log, তিনটি lock manager — এমন কোনো একক record নেই, যা তিনজনের ভাগ্য একসাথে ঠিক করে। সামনে চারটি পথ:
🧭 একটি সিদ্ধান্ত বানানো
তিনটি log-কে একটি প্রোটোকলে বেঁধে একটিই সিদ্ধান্ত — atomic commit; প্রচলিত রূপ two-phase commit (অংশ ২–৫)।
এক প্রতিষ্ঠান, ছোট লেনদেন🔗 ধাপে ধাপে, ফেরার পথসহ
প্রতিটি ধাপ নিজে commit; কোনো ধাপ ব্যর্থ হলে আগেরগুলোর উল্টো কাজ — saga (অংশ ৬–৭)।
সেবা পেরোনো দীর্ঘ কাজ📮 লেখা আর খবর একসাথে
ডেটাবেসে লেখা আর বার্তা পাঠানো একসাথে — transactional outbox (অংশ ৮)।
প্রতিটি event-driven সেবা🧩 সমস্যাটাই এড়ানো
যে ডেটা একসাথে বদলায়, তা এক ডেটাবেসে বা এক shard-এ — তখন local transaction-ই যথেষ্ট (অংশ ৯)।
সবচেয়ে সস্তা সমাধানবিয়ের আয়োজকের কথা ভাবুন — কমিউনিটি সেন্টার, বাবুর্চি আর ডেকোরেটর, তিনজনকেই লাগবে ১২ তারিখে। এক: তিনজনকে বলেন, “তারিখটা ধরে রাখুন, আমি নিশ্চিত করে জানাব”; সবাই রাজি হলে খাতায় “পাকা” লিখে জানান — এটিই 2PC. আয়োজক মাঝপথে উধাও হলে তিনজনই তারিখ ধরে বসে থাকেন। দুই: একে একে অগ্রিম দিয়ে বুক করেন; বাবুর্চি না পেলে সেন্টারের বুকিং বাতিল করে অগ্রিম ফেরত চান — এটিই saga. কেউ আটকে থাকেন না, কিন্তু মাঝে সেন্টারটি সত্যিই বুক ছিল, আর বাতিলের মাশুলও লাগতে পারে।
পরিভাষার দুই অর্থ: “distributed transaction” বলতে (ক) একটি distributed ডেটাবেসের ভেতরে কয়েক shard জুড়ে লেনদেন — ডেটাবেস নিজেই সামলায় (ভিত্তি DBMS কোর্সের distributed database পর্বে); আর (খ) আলাদা সেবা ও ডেটাবেস জুড়ে একটি ব্যবসায়িক কাজ, যা সামলায় আপনার নকশা। এই পর্ব দুটোই দেখে; system design-এ দ্বিতীয়টিই সাধারণত কঠিন।
২ · Two-phase commit — prepare, ভোট আর একটি commit point
Two-phase commit (2PC) একটি atomic commit প্রোটোকল: কয়েকটি স্বাধীন ডেটাবেস একটিই সিদ্ধান্ত নেয় — সবাই commit, নয়তো সবাই abort. চরিত্র দুটি: একজন coordinator (প্রায়ই transaction manager নামের লাইব্রেরি বা সেবা), আর কয়েকজন participant — এখানে seats, wallet আর tickets ডেটাবেস।
2PC-এর ধারণা ১৯৭০-এর দশকের শেষ দিকে কয়েকজন গবেষকের কাজে প্রায় একই সময়ে আসে; সবচেয়ে উদ্ধৃত বর্ণনাগুলোর একটি Jim Gray-এর ১৯৭৮ সালের “Notes on Data Base Operating Systems”. ১৯৮১ সালে Dale Skeen দেখান কোন অবস্থায় 2PC আটকে যায়, আর প্রস্তাব করেন three-phase commit. ১৯৯১ সালে X/Open প্রকাশ করে XA specification — 2PC চালানোর একটি মানক interface.
চারটি ধাপ
- কাজ (execution). লেনদেনের global id ধরা যাক T42. প্রতিটি participant নিজের অংশ করে — A3 বিক্রি, wallet −৮৫০, টিকিটের খসড়া — আর lock ধরে রাখে, commit করে না।
- ধাপ ১ — prepare (ভোট). Coordinator সবাইকে PREPARE পাঠায়: “নিশ্চিতভাবে commit পারবেন?” Participant নিয়ম যাচাই করে, পরিবর্তন আর একটি PREPARED record log-এ লিখে fsync করে — forced log write — তারপর YES. না পারলে NO বলে সঙ্গে সঙ্গে নিজে abort করে।
- সিদ্ধান্ত। সব YES এলে coordinator নিজের log-এ COMMIT record লিখে fsync করে; এটি টেকসই হওয়ার মুহূর্তটিই commit point. একটিও NO, বা সময়মতো ভোট না এলে, সিদ্ধান্ত ABORT.
- ধাপ ২ — commit (খবর). Coordinator সিদ্ধান্ত পাঠায়; participant নিজের COMMIT বা ABORT record force করে, lock ছাড়ে, ACK দেয়। সব ACK এলে coordinator force ছাড়াই END লেখে আর লেনদেনটি ভুলে যায়।
দুটি প্রতিশ্রুতিতেই প্রোটোকল দাঁড়িয়ে। Participant-এর: “YES মানে crash হলেও commit করতে পারব, নিজে abort করব না।” Coordinator-এর: “সিদ্ধান্ত আগে log-এ, তারপর জানানো।” YES থেকে সিদ্ধান্ত পর্যন্ত participant-এর অবস্থা prepared — commit-ও নয়, abort-ও নয়।
নিচে সময়সহ পুরো পথ। ধরা যাক coordinator আর প্রতিটি participant-এর RTT ১ ms (একই data centre, একমুখী পথ ০.৫ ms), একটি fsync ২ ms, আর তিন participant সমান্তরালে কাজ করে। “পরে” চাপুন:
- Coordinator
transaction manager - seats
participant - wallet
participant - tickets
participant
| সময় t | |
| মোট বার্তা | |
| মোট forced write | |
| রুমানার পর্দায় |
- t = ০ — PREPARE যাচ্ছে। তিন participant নিজের অংশ করে lock ধরে আছে, কিছুই commit হয়নি। Coordinator তিনজনকে PREPARE পাঠাল।
- t = ০.৫ থেকে ২.৫ ms — PREPARED force. PREPARE পৌঁছাল; প্রত্যেকে PREPARED record fsync করল, ২ ms. এখন থেকে তারা নিজে abort করতে পারবে না।
- t = ৩.০ ms — তিনটি YES. ভোট পৌঁছাল, মোট বার্তা ৬. Participant-রা অনিশ্চিত — সিদ্ধান্ত জানে না, তাই lock ছাড়তে পারে না।
- t = ৫.০ ms — commit point. Coordinator COMMIT record force করল (forced write ৪)। লেনদেন commit হয়ে গেছে; রুমানাকে “নিশ্চিত” বলা যায়, যদিও participant-রা এখনো জানে না। RTT + ২f = ১ + ৪ = ৫ ms.
- t = ৫.৫ থেকে ৭.৫ ms — lock মুক্তি। COMMIT পৌঁছাল (বার্তা ৯); প্রত্যেকে COMMITTED force করে lock ছাড়ল। PREPARE পাওয়া থেকে lock ধরা ছিল ৭ ms = RTT + ৩f; forced write ৭ = ২N + ১.
- t = ৮.০ ms — ACK আর END. মোট ১২টি বার্তা (৪N); coordinator END লিখে T42 ভুলে গেল। পুরো প্রোটোকল ২RTT + ৩f = ৮ ms; এক ডেটাবেসে লাগত একটি fsync.
কে কোন record লেখে, আর crash-এর পর কী করে
| কে | Record | কখন | Force? | কেন |
|---|---|---|---|---|
| Participant | PREPARED (সাথে redo/undo তথ্য) | YES-এর আগে | হ্যাঁ | crash-এর পরেও commit করতে পারা |
| Coordinator | COMMIT বা ABORT | কাউকে জানানোর আগে | হ্যাঁ | সিদ্ধান্ত মনে রাখা; এটিই commit point |
| Participant | COMMITTED বা ABORTED | সিদ্ধান্ত পেয়ে | হ্যাঁ (মৌলিক 2PC-তে) | restart-এ আবার জিজ্ঞেস না করা |
| Coordinator | END | সব ACK-এর পর | না | শুধু হিসাব মোছার সংকেত |
Restart-এর পর প্রত্যেকে log পড়ে ঠিক করে। Participant-এর log-এ PREPARED না থাকলে সে নিজে abort করে — তার YES ছাড়া কেউ commit করতে পারত না। PREPARED আছে কিন্তু সিদ্ধান্ত নেই, তাহলে লেনদেনটি in-doubt: সে lock আবার ধরে coordinator-কে জিজ্ঞেস করে অপেক্ষা করে। সিদ্ধান্ত থাকলে সেই অনুযায়ী redo বা undo. Coordinator-এর log-এ সিদ্ধান্ত থাকলে সে আবার পাঠায়, না থাকলে ABORT — কারণ COMMIT না লিখে সে কাউকে COMMIT বলে না।
বাস্তবে — XA আর ডেটাবেসের prepare কমান্ড
বাস্তবে 2PC প্রায়ই চলে XA interface দিয়ে: transaction manager coordinator, আর ডেটাবেস বা কিছু broker participant (“resource manager”). MySQL-এর InnoDB-তে XA কমান্ড আছে; PostgreSQL-এর নিজস্ব PREPARE TRANSACTION আর COMMIT PREPARED ডিফল্টে বন্ধ থাকে (max_prepared_transactions = 0; উদাহরণ DBMS কোর্সের distributed database পর্বে)। MySQL-এ একটি participant-এর ধাপ:
XA START 't42';
UPDATE seats SET state = 'SOLD', booking = 't42'
WHERE trip_id = 7 AND seat = 'A3';
XA END 't42';
XA PREPARE 't42'; -- vote YES: changes are durable, locks are still held
-- ... the coordinator decides ...
XA COMMIT 't42'; -- or: XA ROLLBACK 't42'
XA RECOVER; -- after a crash: list prepared (in-doubt) transactionsXA-র সীমা: প্রতিটি participant-কে “prepare” বুঝতে হয়। HTTP API, বাইরের payment gateway বা অপারেটরের সিস্টেম তা বোঝে না — কাজটি করে, নয়তো করে না; আর অন্য প্রতিষ্ঠান আপনার coordinator-এর জন্য lock ধরে বসে থাকবে না। তাই সেবা আর প্রতিষ্ঠানের সীমা পেরোলে 2PC কার্যত অচল — এখান থেকেই saga (অংশ ৬)।
৩ · Coordinator মরলে — in-doubt participant আর blocking
2PC-এর বিপদ ধাপ ১ আর ২-এর মাঝখানে। YES দিয়েছে বলে participant নিজে abort পারে না, সিদ্ধান্ত পায়নি বলে commit-ও না। এই সময়ে coordinator crash করলে বা network ভাগ হলে সে শুধু অপেক্ষা করতে পারে — lock ধরে। তাই 2PC একটি blocking protocol.
নিচে সবচেয়ে খারাপ মুহূর্তের crash: COMMIT লেখার ঠিক পরে, পাঠানোর আগে। ধরা যাক ঈদের ভিড়ে ট্রিপ ৭-এ সেকেন্ডে ২০টি বুকিংয়ের চেষ্টা আসে, আর সবাইকে ট্রিপের সিট-গণনার একই সারি বদলাতে হয় — যা এখন T42-এর lock-এ।
| সময় t | |
| Coordinator | |
| তিন participant | |
| সিট-গণনার সারি | |
| আটকে থাকা বুকিং |
- t = ৩.০ ms. তিনটি YES এসেছে; সবাই PREPARED, lock ধরা, কোনো বুকিং এখনো আটকে নেই।
- t = ৫.০ ms — commit point, তারপর অন্ধকার। COMMIT record ডিস্কে পৌঁছাল — লেনদেন commit হয়ে গেছে। ঠিক তখনই coordinator-এর সার্ভারে বিদ্যুৎ গেল; একটি COMMIT বার্তাও বের হয়নি।
- প্রায় ১ সেকেন্ড — জিজ্ঞাসাবাদ। Timeout বাজল। seats coordinator-কে জিজ্ঞেস করল — উত্তর নেই; তারপর wallet আর tickets-কে (cooperative termination): “সিদ্ধান্ত জানা আছে?” সবার উত্তর, “আমিও PREPARED, কিছু জানি না।” কেউ সিদ্ধান্ত পেয়ে থাকলে বা ভোট না দিয়ে থাকলে মীমাংসা হতো। প্রায় ২০টি বুকিং আটকে।
- প্রায় ১০ সেকেন্ড — blocked. তিনজনই in-doubt; সিট-গণনার সারি lock-এ, তাই ট্রিপ ৭-এর প্রতিটি নতুন বুকিং দাঁড়িয়ে — প্রায় ২০০টি অপেক্ষায় বা timeout-এ। একটি যন্ত্রের বিভ্রাট পুরো ট্রিপের বিক্রি থামিয়েছে।
- t = ৩০,০০৫ ms — coordinator ফিরল। Log-এ COMMIT T42 পেয়ে আবার পাঠাল। (সিদ্ধান্ত না থাকলে নিরাপদে ABORT পাঠাত।) ততক্ষণে প্রায় ৬০০টি বুকিং আটকে গেছে বা ফিরে গেছে।
- t = ৩০,০০৭.৫ ms — মুক্তি। তিনজনই COMMITTED force করে lock ছাড়ল — প্রায় ৩০ সেকেন্ড পরে, স্বাভাবিক ৭ ms-এর চার হাজার গুণেরও বেশি। জমে থাকা অনুরোধ এবার ধীরে সরবে (পর্ব ১৮-এর overload).
কেন participant-রা নিজেরা সিদ্ধান্ত নিতে পারে না — একটি ছোট প্রমাণ
মনে হতে পারে, “সবাই তো YES দিয়েছে; তাহলে commit করে ফেললেই হয়।” দুটি সম্ভাব্য ইতিহাস ভাবুন:
- ইতিহাস ক: সব YES পেয়ে coordinator COMMIT লিখল, তারপর crash করল।
- ইতিহাস খ: একটি YES পৌঁছাতে দেরি হওয়ায় coordinator-এর নিজের timeout বাজল; সে ABORT লিখল (যা নিয়মসম্মত), তারপর crash করল। অথবা ঠিক তখনই গ্রাহক নিজে “বাতিল” চাপলেন।
দুই ইতিহাসেই তিন participant-এর অবস্থা হুবহু এক — PREPARED, কোনো বার্তা নেই। তাই তাদের যেকোনো নিয়ম — “সবাই PREPARED হলে commit” বা “abort” — দুই ইতিহাসে একই কাজ করবে, আর একটিতে ভুল হবেই: ক-তে abort করলে coordinator ফিরে বলবে COMMIT, খ-তে commit করলে বলবে ABORT. Atomicity বাঁচানোর একমাত্র নিরাপদ পথ অপেক্ষা। এটি বাস্তবায়নের দুর্বলতা নয়, প্রোটোকলের গঠনের ফল: সিদ্ধান্ত একটি যন্ত্রের ডিস্কে, আর সেটি নাগালের বাইরে।
2PC নিরাপত্তা (safety) কখনো ভাঙে না — কেউ commit আর কেউ abort, এমন হয় না; কিন্তু অগ্রগতির (liveness) জন্য coordinator-কে ফিরতে হয়। “ভুল করব না” বনাম “থেমে থাকব না” — consensus-এ (পর্ব ৪২) এই প্রশ্ন আবার আসবে।
বাস্তবের ক্ষতি আর “heuristic” সিদ্ধান্ত
In-doubt লেনদেন restart-এর পরেও টিকে থাকে, কারণ YES-এর প্রতিশ্রুতি ডিস্কে লেখা; ভুলে যাওয়া prepared লেনদেন দিনের পর দিন lock ধরে রাখতে পারে, PostgreSQL-এ পুরোনো সারি পরিষ্কারের কাজও (vacuum) আটকায়। দীর্ঘ বিভ্রাটে অপারেটর হাতে commit বা rollback করতে পারেন — XA-র ভাষায় heuristic decision; lock মুক্ত হয়, কিন্তু সিদ্ধান্ত coordinator-এর উল্টো হলে atomicity ভাঙে। তাই prepared লেনদেনের সংখ্যা আর বয়স monitor করুন।
Simulator-এ চারটি পরিস্থিতি — সব YES, একজনের NO, সিদ্ধান্তের আগে আর পরে crash — মৌলিক 2PC-তে (সিদ্ধান্ত আর PREPARED দুটোই force, সবার ACK). একমুখী পথ ০.৫ ms, fsync ২ ms, বিভ্রাট ৩০ সেকেন্ড — সবই ধরে নেওয়া মান:
// Basic two-phase commit: coordinator C + 3 participants. Time in ms, no randomness.
const HOP = 0.5, FSYNC = 2, DOWN = 30000; // assumed: one-way delay, one forced log write, C outage
const P = ["seats", "wallet", "tickets"];
const ms = (x) => x.toFixed(1);
function run(title, votes, crash) { // crash: none | "before-decision" | "after-decision"
console.log("--- " + title);
const yes = votes.filter(Boolean).length;
let decision = yes === P.length ? "COMMIT" : "ABORT";
let tSend = HOP + FSYNC + HOP + FSYNC; // PREPARE out, vote forced, vote back, decision forced
if (crash === "before-decision") { decision = "ABORT"; tSend = 1 + DOWN + FSYNC; }
if (crash === "after-decision") tSend += DOWN; // COMMIT is on C's disk, but C is down
console.log("votes: " + P.map((p, i) => p + "=" + (votes[i] ? "YES" : "NO")).join(" ") +
(crash === "before-decision" ? " (C crashed at t=1.0, votes lost)" : ""));
votes.forEach((v, i) => { if (!v) console.log(P[i] + " voted NO: aborts at once, locks freed at t=0.5"); });
if (crash) {
console.log("t=10000.0 all participants: PREPARED, no decision, peers know nothing -> BLOCKED");
console.log("C back at t=" + ms(tSend - (crash === "after-decision" ? 0 : FSYNC)) + ", reads log: " +
(decision === "COMMIT" ? "COMMIT found" : "no decision -> ABORT (nobody can have committed)"));
}
const tFree = tSend + HOP + FSYNC; // decision arrives, participant forces it, frees locks
console.log("C sends " + decision + " at t=" + ms(tSend) + "; YES voters free locks at t=" +
ms(tFree) + " (held " + ms(tFree - HOP) + " ms after PREPARE)");
console.log("ACKs in at t=" + ms(tFree + HOP) + "; messages=" + (2 * P.length + 2 * yes) +
", forced log writes=" + (1 + 2 * yes));
return tFree - HOP;
}
const hold = run("A: all vote YES", [true, true, true]);
run("B: wallet votes NO (balance too low)", [true, false, true]);
run("C: C crashes after sending PREPARE", [true, true, true], "before-decision");
const frozen = run("D: C crashes right after forcing COMMIT", [true, true, true], "after-decision");
console.log("hot seat row: at most " + Math.floor(1000 / hold) + " commits/s with " + ms(hold) +
" ms locks; in D it is frozen for " + (frozen / 1000).toFixed(1) + " s");আউটপুট:
--- A: all vote YES
votes: seats=YES wallet=YES tickets=YES
C sends COMMIT at t=5.0; YES voters free locks at t=7.5 (held 7.0 ms after PREPARE)
ACKs in at t=8.0; messages=12, forced log writes=7
--- B: wallet votes NO (balance too low)
votes: seats=YES wallet=NO tickets=YES
wallet voted NO: aborts at once, locks freed at t=0.5
C sends ABORT at t=5.0; YES voters free locks at t=7.5 (held 7.0 ms after PREPARE)
ACKs in at t=8.0; messages=10, forced log writes=5
--- C: C crashes after sending PREPARE
votes: seats=YES wallet=YES tickets=YES (C crashed at t=1.0, votes lost)
t=10000.0 all participants: PREPARED, no decision, peers know nothing -> BLOCKED
C back at t=30001.0, reads log: no decision -> ABORT (nobody can have committed)
C sends ABORT at t=30003.0; YES voters free locks at t=30005.5 (held 30005.0 ms after PREPARE)
ACKs in at t=30006.0; messages=12, forced log writes=7
--- D: C crashes right after forcing COMMIT
votes: seats=YES wallet=YES tickets=YES
t=10000.0 all participants: PREPARED, no decision, peers know nothing -> BLOCKED
C back at t=30005.0, reads log: COMMIT found
C sends COMMIT at t=30005.0; YES voters free locks at t=30007.5 (held 30007.0 ms after PREPARE)
ACKs in at t=30008.0; messages=12, forced log writes=7
hot seat row: at most 142 commits/s with 7.0 ms locks; in D it is frozen for 30.0 s
A হুবহু stepper: ৫.০ ms-এ commit point, ৭.৫ ms-এ lock মুক্ত, ১২টি বার্তা, ৭টি forced write. B-তে wallet-এর NO: সে ০.৫ ms-এই abort করে lock ছাড়ে; ABORT যায় শুধু YES-দাতাদের কাছে — বার্তা ১০, forced write ৫ (বাস্তবের coordinator প্রথম NO পেয়েই ABORT পাঠাতে পারে)। C আর D-তে t=10000.0 লাইনটি অক্ষরে অক্ষরে এক — participant-দের চোখে দুই পরিস্থিতি অভিন্ন, অথচ সঠিক ফল একটিতে ABORT, অন্যটিতে COMMIT: উপরের প্রমাণটি চোখের সামনে। শেষ লাইনটি দাম: ৭ ms lock-এ hot সারিতে সেকেন্ডে বড়জোর ১৪২টি commit, আর D-তে সারিটি ৩০ সেকেন্ড জমাট।
৪ · 2PC-এর দাম — latency, lock, throughput আর availability-র হিসাব
Stepper-এর সংখ্যাগুলো এবার সূত্রে বাঁধা যাক। ধরা যাক coordinator থেকে প্রতিটি participant-এর round trip = r, একটি forced log write = f, participant N জন, আর তারা সমান্তরালে কাজ করে — তাই প্রতিটি ধাপে সবচেয়ে ধীরজনের সময়ই গোনা হয়।
2PC-এর সময়রেখা — কোন অংশে কত সময়
Message sequence with forced log writes (RTT r = 1 ms, fsync f = 2 ms)
- Commit point পর্যন্ত: PREPARE যাওয়া (r/২) + PREPARED force (f) + YES ফেরা (r/২) + সিদ্ধান্ত force (f) = r + ২f. গ্রাহককে “নিশ্চিত” বলা যায় এর পরেই।
- Participant-এর lock, PREPARE পাওয়া থেকে: f + r/২ + f + r/২ + f = r + ৩f. এর আগের execution-এর সময়ও lock ধরা থাকে, তাই এটি নিম্নসীমা।
- পুরো প্রোটোকল, END পর্যন্ত: ২r + ৩f.
- বার্তা আর forced write: প্রতিজনের সাথে PREPARE, ভোট, সিদ্ধান্ত আর ACK — ৪Nটি বার্তা; প্রতিজনের দুটি আর coordinator-এর একটি — ২N + ১টি forced write.
বার্তা = ৪N · forced write = ২N + ১ · এক ডেটাবেসে: একটি f
| হিসাব (N = ৩) | একই data centre | দুই মহাদেশ |
|---|---|---|
| ধরা যাক RTT r | ০.৫ ms | ১০০ ms |
| ধরা যাক fsync f | ১ ms | ১ ms |
| Commit point, r + ২f | ২.৫ ms | ১০২ ms |
| Lock, r + ৩f | ৩.৫ ms | ১০৩ ms |
| মোট, ২r + ৩f | ৪ ms | ২০৩ ms |
| একটি hot সারিতে সর্বোচ্চ লেনদেন, ১০০০ ÷ lock | সেকেন্ডে প্রায় ২৮৫ | সেকেন্ডে প্রায় ৯.৭ |
শেষ সারিটিই আসল শিক্ষা। Hot row — যে সারি প্রায় প্রতিটি লেনদেন বদলায়, যেমন ঈদের জনপ্রিয় ট্রিপের সিট-গণনা। Lock না ছাড়া পর্যন্ত পরের লেনদেন দাঁড়িয়ে থাকে, তাই সেখানে সেকেন্ডে বড়জোর ১০০০ ÷ (lock-এর ms)টি লেনদেন — যত সার্ভারই যোগ করুন। এই সীমা latency-র চেয়েও বড় দাম (Little-এর সূত্র, পর্ব ০৪); দুই region-এর আসল RTT-র হিসাব পর্ব ০৭-এ।
সবচেয়ে ধীরজন আর সবচেয়ে দুর্বলজন
সমান্তরাল হলেও coordinator সবার ভোটের অপেক্ষা করে, তাই 2PC-এর latency সবচেয়ে ধীর participant-এর latency — একটি fan-out, যেখানে কারও p99 সবার সাধারণ সময় হয়ে দাঁড়ায় (পর্ব ০৪)। Availability-তে: commit-এ coordinator আর সব participant-কে একসাথে সচল থাকতে হয়, অর্থাৎ এরা series-এ (পর্ব ০৫)। প্রত্যেকে ৯৯.৯% হলে চারজনের মিলিত availability ০.৯৯৯৪ ≈ ৯৯.৬০% — বছরে প্রায় ৩৫ ঘণ্টা commit বন্ধ, একটি যন্ত্রে যেখানে প্রায় ৮.৮ ঘণ্টা।
প্রচলিত optimization
- Presumed abort: log-এ কোনো লেনদেনের খবর না থাকলে ধরে নেওয়া হয় ABORT, তাই abort-এ force বা ACK লাগে না — IBM-এর R* প্রকল্পের ১৯৮৬ সালের গবেষণাপত্রে (Mohan, Lindsay ও Obermarck) বর্ণিত।
- Read-only আর one-phase: শুধু পড়েছে এমন participant ধাপ ২ থেকে বাদ পড়ে; লেখার participant একজন হলে 2PC-ই লাগে না।
- Group commit: অনেক লেনদেনের log একটি fsync-এ — f-এর গড় খরচ কমে, কিন্তু round trip আর lock-এর সময় কমে না।
৫ · Blocking এড়ানোর চেষ্টা — three-phase commit আর consensus-ভিত্তিক commit
Skeen-এর পর্যবেক্ষণ: 2PC-তে একটি participant এমন অবস্থায় থাকতে পারে (prepared), যেখান থেকে commit আর abort দুটোই তখনও সম্ভব — তাই সে আটকায়। Three-phase commit (3PC) মাঝে একটি ধাপ বসায়:
- CanCommit? → ভোট, 2PC-এর prepare-এর মতো।
- PreCommit: সব YES হলে coordinator সবাইকে জানায় “সবাই রাজি, সিদ্ধান্ত commit হবে”; participant ACK দেয়।
- DoCommit: তারপর আসল commit.
এখন অবস্থা দেখেই বোঝা যায় কী করতে হবে: কেউ PreCommit পেয়ে থাকলে সবাই YES দিয়েছিল, তাই commit নিরাপদ; কেউই না পেলে কেউ commit করেনি, তাই abort নিরাপদ। Coordinator মরলে বেঁচে থাকা participant-রা নতুন coordinator বেছে কাজ শেষ করতে পারে।
শর্তটা কঠিন: 3PC ধরে নেয়, বার্তা নির্দিষ্ট সময়ের মধ্যে পৌঁছায় আর timeout মানেই সত্যিকারের crash — network কখনো ভাগ হয় না। বাস্তবের network এমন নয় (পর্ব ৩৮)। ভাগ হলে এক দিকের participant-রা PreCommit দেখে commit করতে পারে, অন্য দিকের যারা পায়নি তারা abort — দুই দিকে দুই ফল, অনেকটা split-brain-এর মতো, যা 2PC কখনো করত না। সাথে একটি বাড়তি round trip. তাই 3PC তত্ত্বে গুরুত্বপূর্ণ, production-এ প্রায় অদৃশ্য।
এর পেছনে একটি গভীর ফল আছে। ১৯৮৫ সালে Fischer, Lynch ও Paterson প্রমাণ করেন (FLP impossibility): বার্তার সময়ের সীমা নেই এমন (asynchronous) ব্যবস্থায় একটি process-ও crash করতে পারলে কোনো deterministic প্রোটোকল সবসময় ঐকমত্যের নিশ্চয়তা দিতে পারে না; non-blocking atomic commit এই ঐকমত্যেরই আত্মীয়। বাস্তবের উত্তর: নিরাপত্তা সবসময়, আর অগ্রগতি তখন, যখন বেশিরভাগ যন্ত্র সচল আর যোগাযোগে।
Coordinator-কে একা না রাখা — Paxos আর Raft দিয়ে
2PC-এর আসল সমস্যা, সিদ্ধান্তটি একটি যন্ত্রের ডিস্কে। সমাধান: সিদ্ধান্ত আর PREPARED অবস্থা কয়েকটি replica-য় consensus দিয়ে লেখা — Paxos বা Raft-এ, যেখানে বেশিরভাগ replica লিখলে তবেই record টেকসই। Coordinator-এর যন্ত্র মরলে অন্য replica leader হয়ে log থেকে সিদ্ধান্ত পড়ে কাজ শেষ করে (Raft পর্ব ৪২-এ)।
- Paxos Commit: Jim Gray আর Leslie Lamport-এর “Consensus on Transaction Commit” (ACM TODS, ২০০৬) — Paxos Commit; ২F + ১টি acceptor F-টি ব্যর্থতা সহ্য করে, আর F = ০ হলে এটি আসলে 2PC.
- Spanner-এর ধাঁচ: Google-এর Spanner-এর ২০১২ সালের (OSDI) গবেষণাপত্র অনুযায়ী প্রতিটি participant একটি Paxos দল, একটি দলের leader coordinator, আর PREPARE ও COMMIT record Paxos-এ replicated — আটকায় কেবল কোনো দলের majority হারালে।
- দাম: প্রতিটি forced write এখন একটি consensus round — কয়েকটি replica-য় fsync আর অন্তত একটি round trip, প্রায়ই আলাদা zone-এ। Latency বাড়ে, blocking-এর ঝুঁকি কমে।
| 2PC | 3PC | Consensus-ভিত্তিক 2PC | |
|---|---|---|---|
| Coordinator মরলে | in-doubt, blocked | অন্যরা শেষ করতে পারে | নতুন leader শেষ করে |
| Network ভাগ হলে | নিরাপদ, কিন্তু blocked | অসংগতি সম্ভব | majority-র দিক এগোয়, অন্য দিক অপেক্ষা করে |
| বাস্তবে | XA, ডেটাবেসের ভেতরে | প্রায় নেই | distributed SQL ডেটাবেস |
আপনার নকশার জন্য অর্থ: এক distributed SQL ডেটাবেসের ভেতরে কয়েক shard জুড়ে লেনদেনে (পর্ব ২৩) ডেটাবেস নিজেই সাধারণত consensus-এর ওপর 2PC চালায় — আপনি BEGIN … COMMIT লেখেন আর latency-র দাম দেন। আলাদা সেবা আর প্রতিষ্ঠান জুড়ে এই ছাতা নেই; তখন পথ saga.
৬ · Saga — local transaction-এর শিকল আর compensation
১৯৮৭ সালে Hector Garcia-Molina আর Kenneth Salem SIGMOD-এ “Sagas” গবেষণাপত্রে দীর্ঘ লেনদেনের (long-lived transaction) ঘণ্টার পর ঘণ্টা lock-এর সমাধান দেন: লেনদেনটিকে ছোট local transaction T1 … Tn-এ ভাগ, প্রতিটি সঙ্গে সঙ্গে commit, আর প্রতিটির একটি compensating transaction C1 …, যা প্রভাবটি অর্থের দিক থেকে উল্টে দেয়। নিশ্চয়তা: হয় T1 … Tn সব, নয়তো T1 … Tj আর তারপর উল্টো ক্রমে Cj … C1. এক ডেটাবেসের জন্য লেখা এই ধারণাই আজ কয়েক সেবা জুড়ে কাজের প্রধান কৌশল — saga.
দুই দিকে মেরামত — backward আর forward recovery
- Backward recovery: আগের সফল ধাপগুলোর compensation, উল্টো ক্রমে — saga বাতিল।
- Forward recovery: ব্যর্থ ধাপটি আবার চেষ্টা করে এগোনো — যখন ব্যর্থতা ক্ষণিকের আর ধাপটি শেষ পর্যন্ত সফল হবেই; মূল গবেষণাপত্রেও save-point থেকে এগোনোর কথা আছে।
নিয়ম: ব্যবসায়িক “না” (ট্রিপ বাতিল, ব্যালান্স কম) হলে backward; ক্ষণিকের ত্রুটিতে একই idempotency key-সহ retry, exponential backoff ও jitter দিয়ে (পর্ব ৪৮); তারপরও না হলে backward, বা মানুষের হাতে।
Compensation নকশা — “undo” নয়, একটি নতুন কাজ
- অর্থের দিক থেকে উল্টানো: টাকা ফেরত মানে সারিকে আগের মানে ফেরানো নয়, ledger-এ একটি নতুন refund entry — মুদি দোকানের খাতায় যেমন ভুল কেটে না মুছে নিচে লেখা হয় “ফেরত ৮৫০”; ইতিহাস থাকে, হিসাব মেলে।
- শেষ পর্যন্ত সফল হতেই হবে, আর idempotent: নইলে saga আধাআধি পড়ে থাকে; তাই retry চলে idempotency key-সহ (যেমন
s42-ChargePayment), আর বারবার ব্যর্থ হলে alert ও মানুষের হাতে। - শর্তসাপেক্ষ: “A3 free” নয়, “A3 এখনো s42-এর HELD হলে তবেই free” — নইলে অন্য কাজের বদল মুছে যায় (অংশ ৭-এর লাইভ SQL).
- সব কাজ উল্টানো যায় না: পাঠানো SMS বা ছাপা টিকিটের “compensation” শুধু আরেকটি সংশোধনী কাজ — এদের saga-র শেষে রাখুন।
Pivot — যেখানে ফেরার পথ বন্ধ
Chris Richardson-এর Microservices Patterns (২০১৮) বইয়ে জনপ্রিয় হওয়া একটি শ্রেণিবিভাগে ধাপ তিন রকম: compensatable, একটি pivot transaction (সফল হলে saga শেষ পর্যন্ত যাবেই, ব্যর্থ হলে আগেরগুলো উল্টাতে হয়), আর pivot-এর পরের retriable ধাপ। বাস-টিকিটে: BookSeat (hold, উল্টানো প্রায় বিনামূল্যে) → ChargePayment (refund-এ উল্টানো যায়, খরচসহ) → IssueTicket (অপারেটর টিকিট দিলে আর ফেরা নেই — pivot) → SendSMS (retriable).
ক্রমের নিয়ম: যে ধাপ ব্যর্থ হওয়ার সম্ভাবনা বেশি আর উল্টানো সস্তা, সেটি আগে; উল্টানো যায় না এমন কাজ pivot-এর পরে। তাই সিট আগে hold, তারপর টাকা — hold ছাড়তে কিছু লাগে না, refund-এ সময় আর মাশুল লাগে।
এবার একটি saga ধাপে ধাপে। Orchestrator (Booking service) তিনটি ধাপ চালায়; তৃতীয় ধাপে অপারেটর জানায় ট্রিপটি বাতিল — ব্যবসায়িক “না”, তাই backward recovery. সাথে, refund-এর প্রথম উত্তরটি হারায়।
| সিট A3 | |
| Wallet | |
| টিকিট | |
| Saga |
- শুরু। Orchestrator নিজের ডেটাবেসে s42-এর সারি লিখল — STARTED. এই log-ই তার স্মৃতি; crash করে ফিরলে এখান থেকে বুঝবে কোথায় ছিল।
- T1 — BookSeat. সিট সার্ভিস A3-কে
HELD (pending)করে local commit করল — অপেক্ষমাণ অবস্থা, বিক্রি নয়; অন্য কেউ A3 কিনতে পারবেন না। - T2 — ChargePayment. ৮৫০ টাকা কাটা, wallet ৪,১৫০ — সঙ্গে সঙ্গে commit, তাই এখন কেউ wallet দেখলে কাটা টাকাই দেখবেন।
- T3 — IssueTicket ব্যর্থ। অপারেটর জানাল “trip cancelled” — pivot ব্যর্থ, retry অর্থহীন। Orchestrator COMPENSATING লিখে উল্টো পথ ধরল: আগে C2, তারপর C1.
- C2, চেষ্টা ১ — উত্তর হারাল। Refund হলো, wallet আবার ৫,০০০ — কিন্তু উত্তর পথে হারাল; orchestrator শুধু timeout দেখল।
- C2, চেষ্টা ২ — একই key. একই
s42-ChargePaymentkey-তে আবার চাওয়া; পেমেন্ট সার্ভিস key চিনে বলল “already refunded, no-op” — wallet ৫,০০০-ই, ৫,৮৫০ নয়। Key ছাড়া দুবার টাকা ফেরত যেত। - C1 আর শেষ। সিট সার্ভিস A3 ছেড়ে দিল (শর্ত: এখনো s42-এর hold). Saga COMPENSATED; রুমানাকে জানানো হলো, “ট্রিপ বাতিল, টাকা ফেরত।” ডেটা আবার সংগত — কিন্তু মাঝের কয়েক মুহূর্ত সংগত ছিল না।
একই ঘটনা simulator-এ। প্রতিটি সেবা একটি ছোট in-memory অংশ; FAIL_AT বলে কোন ধাপ স্থায়ীভাবে ব্যর্থ হবে, আর refund-এর প্রথম উত্তরটি সবসময় হারায়:
// Saga orchestrator: BookSeat -> ChargePayment -> IssueTicket. Deterministic, in-memory services.
const FAIL_AT = 3; // which forward step fails for good (1..3); 0 = none fails
const db = { seat: "free", wallet: 5000, ticket: "none", refundKeys: new Set() }; let refundCalls = 0;
const log = [], ev = (s) => log.push(String(log.length + 1).padStart(2, "0") + " " + s);
function refund(key) { // payment service: refund is idempotent on its key
refundCalls++;
if (db.refundKeys.has(key)) return "already refunded, no-op";
db.refundKeys.add(key); db.wallet += 850;
if (refundCalls === 1) throw new Error("timeout (refund applied, reply lost)");
return "refunded 850";
}
const steps = [
{ name: "BookSeat", why: "seat A3 already sold", run: () => { db.seat = "A3 HELD (pending)"; },
undo: () => { db.seat = "free"; return "seat released"; } },
{ name: "ChargePayment", why: "balance too low", run: () => { db.wallet -= 850; }, undo: refund },
{ name: "IssueTicket", why: "operator rejected: trip cancelled", // pivot: no undo
run: () => { db.seat = "A3 SOLD"; db.ticket = "T-501"; } },
];
const done = []; ev("SAGA s42 STARTED");
for (const [i, s] of steps.entries()) {
if (i + 1 === FAIL_AT) { ev(s.name + " FAILED: " + s.why); break; }
s.run(); done.push(s); ev(s.name + " OK");
}
if (done.length < steps.length) {
ev("COMPENSATING in reverse order");
for (const s of done.reverse())
for (let n = 1; ; n++) { // retry with the SAME key until it succeeds
try { ev("undo " + s.name + " try " + n + ": " + s.undo("s42-" + s.name)); break; }
catch (e) { ev("undo " + s.name + " try " + n + ": " + e.message + " -> retry"); }
}
ev("SAGA s42 COMPENSATED");
} else ev("SAGA s42 COMPLETED");
log.forEach((l) => console.log(l));
console.log("final: seat=" + db.seat + ", wallet=" + db.wallet + ", ticket=" + db.ticket +
", refund calls=" + refundCalls + ", refunds applied=" + db.refundKeys.size);আউটপুট:
01 SAGA s42 STARTED
02 BookSeat OK
03 ChargePayment OK
04 IssueTicket FAILED: operator rejected: trip cancelled
05 COMPENSATING in reverse order
06 undo ChargePayment try 1: timeout (refund applied, reply lost) -> retry
07 undo ChargePayment try 2: already refunded, no-op
08 undo BookSeat try 1: seat released
09 SAGA s42 COMPENSATED
final: seat=free, wallet=5000, ticket=none, refund calls=2, refunds applied=1
০২–০৩-এ দুটি local commit, ০৪-এ pivot ব্যর্থ; তারপর উল্টো ক্রম — আগে ChargePayment, পরে BookSeat; IssueTicket-এর কিছুই হয়নি, তাই তার compensation নেই। ০৬-এ refund আসলে হয়ে গেছে, orchestrator দেখল শুধু timeout; ০৭-এ একই key-তে দ্বিতীয় চেষ্টা কিছু বদলায়নি — refund calls=2, refunds applied=1. চূড়ান্ত অবস্থা শুরুর মতো: সিট free, wallet ৫,০০০, টিকিট নেই। বাস্তবে orchestrator-এর log থাকে ডেটাবেসে, আর command যায় বার্তায় (অংশ ৮)।
Orchestration বনাম choreography
উপরের saga-য় একটি কেন্দ্রীয় সেবা সব ধাপ চালায় — orchestration. বিকল্প choreography: কোনো কেন্দ্র নেই; SeatHeld শুনে পেমেন্ট টাকা কাটে, PaymentCharged শুনে টিকিট সার্ভিস কাজ ধরে, TicketRejected শুনে পেমেন্ট refund করে, আর PaymentRefunded শুনে সিট সার্ভিস hold ছাড়ে।
| Orchestration | Choreography | |
|---|---|---|
| Saga-র অবস্থা কোথায় | orchestrator-এর টেবিলে, এক জায়গায় | event-এর ধারায় ছড়ানো |
| ঝুঁকি | orchestrator-এ সব ব্যবসায়িক যুক্তি জমে মোটা “god service” | কে কাকে শুনছে বোঝা কঠিন, চক্রাকার নির্ভরতা |
| মানানসই | ৪–৫টির বেশি ধাপ, জটিল compensation | ২–৩টি ধাপ, সরল প্রবাহ |
বিস্তারিত তুলনা পর্ব ৩২-এ (pub/sub আর event-driven architecture). Orchestrator নিজেও crash করতে পারে, তাই তার অবস্থা ডেটাবেসে টেকসই রাখতে হয়; Temporal-এর মতো open-source workflow engine ধাপ, retry, timer আর অবস্থা নিজেই টেকসই রাখে (পর্ব ৩৭)।
৭ · Saga-য় isolation নেই — anomaly আর প্রতিকার
Saga দেয় ACID-এর A (শেষে সব, নয়তো compensation), C (প্রতিটি সেবার নিয়ম) আর D (প্রতিটি local commit টেকসই) — কিন্তু I নয়: প্রতিটি T নিজে commit করে, তাই মাঝের অবস্থা সবার কাছে দৃশ্যমান। পর্ব ২৪-এর anomaly-গুলো ফিরে আসে, এবার lock-এর পাহারা ছাড়াই:
| Anomaly | বাস-টিকিটে উদাহরণ | প্রতিকার |
|---|---|---|
| Dirty read — পরে উল্টে যাবে এমন ডেটা পড়ে কাজ | Loyalty সেবা PaymentCharged দেখে ৮৫ পয়েন্ট দিল; পরে refund হলো, পয়েন্ট রয়ে গেল | পয়েন্ট দেওয়া হোক saga COMPLETED হলে; নয়তো পয়েন্টেরও compensation |
| Lost update — এক কাজের লেখা আরেক কাজ নিঃশব্দে মুছে দেয় | s42 A3 hold করল, অপারেটর ট্রিপ বাতিল করল, তারপর s42-এর compensation A3-কে free করল — বাতিল ট্রিপে সিট বিক্রিযোগ্য! | শর্তসাপেক্ষ লেখা (state = 'HELD' AND hold_by = 's42'), version কলাম — optimistic concurrency |
| Fuzzy read — এক saga দুই ধাপে একই ডেটা দুই রকম পড়ে | Hold-এ দাম ৮৫০, টাকা কাটার সময় ৯০০ | দাম hold-এর সাথে বেঁধে পাঠানো, বা আবার পড়ে যাচাই |
প্রতিকারের পুরো তালিকা
- Semantic lock: compensatable ধাপ একটি অপেক্ষমাণ পতাকা বসায় (সিট
HELD, অর্ডারPENDING); pivot বা compensation সেটি সরায়, আর অন্যরা দেখে অপেক্ষা করে বা গ্রাহককে “অপেক্ষমাণ” দেখায়। এর একটি মেয়াদ (TTL) রাখুন — বাস কাউন্টারে যেমন ১০ মিনিট পরে hold ছেড়ে দেওয়া হয় — যাতে মরে যাওয়া saga চিরকাল সিট আটকে না রাখে। - Commutative update: “balance = ৪,১৫০” নয়, বরং “−৮৫০” আর “+৮৫০” — ক্রম যা-ই হোক, ফল এক।
- Pessimistic view: অন্যরা যে ডেটা পড়ে সিদ্ধান্ত নেয়, তা retriable ধাপে বদলানো।
- Reread value: লেখার আগে version কলাম দিয়ে আবার যাচাই, পর্ব ০৮-এর ETag-এর মতো।
- Version file: কাজের হিসাব রাখা, যাতে উল্টো ক্রমে আসা কাজ (বাতিল আগে, তৈরি পরে) চেনা যায়।
- By value: ঝুঁকি বুঝে কৌশল — বড় অঙ্কের কাজে কড়া পদ্ধতি, সাধারণ কাজে saga.
মূল কথা (Richardson-এর বইয়ে বিস্তারিত): অপেক্ষমাণ অবস্থাকে ডেটায় স্পষ্ট করুন, আর প্রতিটি লেখাকে শর্তসাপেক্ষ বা ক্রম-নিরপেক্ষ রাখুন। নিচের লাইভ SQL-এ উপরের lost update-টি, দুই রকম compensation দিয়ে; নমুনা ডেটাবেসে ট্রিপ ৭-এর A3 আর A4 free, B1 SOLD.
-- Saga s42, step 1: hold seat A3 (a semantic lock: HELD + who holds it).
UPDATE seats SET state = 'HELD', hold_by = 's42', version = version + 1
WHERE trip = 7 AND seat = 'A3' AND state = 'free';
-- Meanwhile the operator cancels trip 7: every seat becomes CANCELLED.
UPDATE seats SET state = 'CANCELLED', hold_by = NULL, version = version + 1
WHERE trip = 7;
-- s42 fails later and compensates. The guarded compensation touches A3
-- only if it is STILL held by s42:
UPDATE seats SET state = 'free', hold_by = NULL, version = version + 1
WHERE trip = 7 AND seat = 'A3' AND state = 'HELD' AND hold_by = 's42';
SELECT seat, state, hold_by, version FROM seats ORDER BY seat;
-- A naive compensation ("make A3 free") resurrects a seat on a cancelled trip:
UPDATE seats SET state = 'free', hold_by = NULL WHERE trip = 7 AND seat = 'A3';
SELECT seat, state FROM seats WHERE trip = 7 ORDER BY seat;প্রথম তিনটি লেখার ফল: ১টি সারি বদলেছে (hold), ৩টি সারি বদলেছে (ট্রিপ বাতিল), আর শর্তসাপেক্ষ compensation-এ ০টি সারি বদলেছে — A3 আর s42-এর HELD নেই। মাঝের SELECT-এ তিনটি সিটই CANCELLED, hold_by NULL; A3-এর version ৩, বাকিদের ২. শর্তহীন compensation-এ আবার ১টি সারি বদলেছে, আর শেষ SELECT: A3 free, A4 CANCELLED, B1 CANCELLED — বাতিল ট্রিপে একটি সিট বিক্রির জন্য খোলা। শূন্য সারি বদলানো ব্যর্থতা নয়, তথ্য: অন্য কাজ আগেই বদলেছে। এটি single-node SQLite ডেমো — শর্তের যুক্তি দেখায়; আলাদা ডেটাবেস বা সময়ের খেলা দেখায় না।
৮ · Dual write-এর ফাঁদ আর transactional outbox
Orchestration হোক বা choreography, প্রতিটি সেবাকে দুটি কাজ করতে হয়: নিজের ডেটাবেসে লেখা, আর অন্যদের জানাতে message broker-এ (Kafka বা RabbitMQ-র মতো; পর্ব ৩১, ৩৩) বার্তা পাঠানো। কোড প্রায়ই এমন দেখায়:
await db.transaction(async (tx) => {
await tx.insert("orders", order); // 1. commit the order
});
await broker.publish("OrderPlaced", event); // 2. then tell the others
// The process can die between 1 and 2, or publish can time out.একে বলে dual write — দুটি আলাদা ব্যবস্থায় দুটি লেখা, মাঝে কোনো যৌথ transaction নেই। লাইন ১ আর ২-এর মাঝে process মরতে পারে, broker বন্ধ থাকতে পারে, publish-এ timeout হলে বার্তা গেছে কি না অজানা। ক্রম উল্টালে উল্টো বিপদ: event গেল, অর্ডার commit হলো না। try/catch সারায় না — catch-এর আগেই process মরতে পারে। আর publish-এর timeout-এ “গেল কি না জানি না” — এই অনিশ্চয়তা অনেকটা দুই সেনাপতির সমস্যার মতো।
ক্ষতি কতটা? নিচের simulator-এ ১০,০০০টি অর্ডার, প্রতিটির জন্য তিন পদ্ধতিতে একই seeded দুর্ঘটনা: ১% ক্ষেত্রে ডেটাবেসের লেখা ব্যর্থ, ২% ক্ষেত্রে broker সাময়িক বন্ধ, ০.৫% ক্ষেত্রে দুই কাজের মাঝে crash — সবই ধরে নেওয়া হার:
// Dual write vs transactional outbox: 10,000 orders, the SAME seeded faults for every approach.
function mulberry32(a) {
return () => { a |= 0; a = (a + 0x6D2B79F5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a);
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
const N = 10000, DB_FAIL = 0.01, BROKER_FAIL = 0.02, CRASH = 0.005; // assumed fault rates
function simulate(mode) {
const r = mulberry32(1), orders = new Set(), events = []; let retries = 0;
for (let id = 1; id <= N; id++) {
const dbFails = r() < DB_FAIL, brokerFails = r() < BROKER_FAIL, crash = r() < CRASH;
if (mode === "db-then-publish") { // commit the order, then publish
if (dbFails) continue;
orders.add(id);
if (!crash && !brokerFails) events.push(id); // crash in between or broker down: lost
} else if (mode === "publish-then-db") { // publish first, then commit
if (brokerFails) continue;
events.push(id);
if (!crash && !dbFails) orders.add(id); // event is out, order is not: phantom
} else { // outbox: order + event row in ONE transaction
if (dbFails) continue; // both rows rolled back together
orders.add(id);
if (brokerFails) retries++; // relay keeps the row and tries again later
events.push(id);
if (crash) events.push(id); // relay died before marking it sent: sent twice
}
}
const uniq = new Set(events);
const lost = [...orders].filter((id) => !uniq.has(id)).length;
const phantom = [...uniq].filter((id) => !orders.has(id)).length;
console.log(mode.padEnd(16) + String(orders.size).padStart(7) + String(events.length).padStart(8) +
String(lost).padStart(6) + String(phantom).padStart(9) + String(events.length - uniq.size).padStart(6) +
(mode === "outbox" ? " relay retries=" + retries + ", deduped consumer applies " + uniq.size : ""));
}
console.log("approach orders events lost phantom dups");
["db-then-publish", "publish-then-db", "outbox"].forEach(simulate);আউটপুট:
approach orders events lost phantom dups
db-then-publish 9895 9653 242 0 0
publish-then-db 9653 9802 0 149 0
outbox 9895 9943 0 0 48 relay retries=194, deduped consumer applies 9895
আগে DB, পরে publish: ৯,৮৯৫টি অর্ডারের ২৪২টির খবর কেউ পায়নি। আগে publish: কিছু হারায়নি, কিন্তু ১৪৯টি ভূতুড়ে event — অর্ডার নেই, অথচ অন্যরা কাজ করছে। Outbox: lost আর phantom দুটোই ০; broker বন্ধের ১৯৪টি ঘটনায় relay পরে আবার চেষ্টা করেছে। দাম ৪৮টি duplicate — পাঠানোর পরে, চিহ্নের আগে relay মরলে। event_id দেখে duplicate বাদ দেওয়া consumer ঠিক ৯,৮৯৫টি effect করে। হারগুলো কাল্পনিক, আকৃতিটাই আসল: dual write নিঃশব্দে হারায়, outbox শব্দ করে duplicate দেয়।
Outbox — event-টিও ডেটাবেসের একটি সারি
Transactional outbox-এর কৌশল সরল: event সরাসরি broker-এ না পাঠিয়ে একই ডেটাবেসের outbox টেবিলে লিখুন — ব্যবসায়িক সারির সাথে একই local transaction-এ। আলাদা একটি relay process না-পাঠানো সারি পড়ে broker-এ পাঠায়, তারপর “পাঠানো হয়েছে” চিহ্ন দেয় বা সারিটি মোছে। ফল:
- সব, নয়তো কিছুই না: অর্ডার commit হলে event-ও আছে, না হলে কোনোটিই নেই।
- হারায় না: relay বা broker বন্ধ থাকলে সারিটি অপেক্ষা করে, পরে যায়।
- কিন্তু দুবার যেতে পারে: পাঠানো আর চিহ্নের মাঝে relay মরলে — তাই outbox দেয় at-least-once delivery, আর প্রতিটি consumer-কে হতে হয় idempotent consumer: event_id-এর একটি inbox টেবিল, effect-এর সাথে একই local transaction-এ (পর্ব ৩৪)।
Saga, প্রতিটি সেবার নিজের ডেটাবেস, outbox আর broker — পুরো ছবি একসাথে:
Saga orchestrator, তিনটি সেবা, outbox আর message broker
Orchestrator → outbox → relay → broker → services (each with its own DB, inbox and outbox)
এবার relay-র পথ ধাপে ধাপে — একটি crash-সহ, যাতে duplicate কোথা থেকে আসে আর কীভাবে থামে, দেখা যায়:
- Booking DB
orders + outbox - Relay
আলাদা process - Broker
topic: orders - SMS সার্ভিস
idempotent consumer
| Broker-এ কপি | |
| রুমানার SMS |
- এক local transaction. অর্ডার ১০০১ আর outbox-এর ev-1001 একসাথে commit; broker-এর অপেক্ষা নেই, রুমানাকে সঙ্গে সঙ্গে উত্তর।
- Relay খোঁজে। কয়েকশো মিলিসেকেন্ড পরপর না-পাঠানো সারি (
sent_at IS NULL) — ev-1001 পেল। - Publish. ev-1001 broker-এ গেল, seq ১; outbox-এ এখনো চিহ্ন নেই।
- Relay crash. চিহ্ন দেওয়ার আগেই relay মরল; ev-1001 এখনো “না-পাঠানো”. এর মধ্যে SMS সার্ভিস seq ১ পেয়ে inbox-এ ev-1001 লিখল আর SMS পাঠাল — একই local transaction-এ।
- Relay ফিরল। ev-1001 এখনো NULL, তাই আবার publish — seq ২, একই event; এবার
sent_atলিখল। Event হারায়নি, কিন্তু দুবার গেছে। - Duplicate বাদ। seq ২ এলে inbox-এ ev-1001 আগেই আছে — insert-এ ০টি সারি বদলাল, SMS গেল না। At-least-once delivery আর idempotent consumer মিলে প্রতিটি effect একবার।
একই ঘটনা লাইভ SQL-এ। নমুনা ডেটাবেসে orders; outbox (event_id অনন্য, sent_at NULL মানে না-পাঠানো, আর না-পাঠানো সারির একটি partial index); message broker-এর ভূমিকায় broker; আর consumer-এর inbox, যেখানে event_id primary key. ROLLBACK crash-এর ভূমিকায়; relay-র crash মানে চিহ্ন দেওয়ার UPDATE-টি বাদ:
-- 1) Place order 1001: the order row and its event row commit TOGETHER.
BEGIN;
INSERT INTO orders VALUES (1001, 'A3', 850, 'PENDING');
INSERT INTO outbox (event_id, type, payload) VALUES ('ev-1001', 'OrderPlaced', '{"order":1001,"seat":"A3"}');
COMMIT;
-- 2) Order 1002 crashes before COMMIT (ROLLBACK plays the crash): neither row survives.
BEGIN;
INSERT INTO orders VALUES (1002, 'A4', 850, 'PENDING');
INSERT INTO outbox (event_id, type, payload) VALUES ('ev-1002', 'OrderPlaced', '{"order":1002,"seat":"A4"}');
ROLLBACK;
SELECT (SELECT count(*) FROM orders) AS orders, (SELECT count(*) FROM outbox) AS outbox_rows;
-- 3) Relay, run 1: read unsent rows, publish them ... and crash before marking them sent.
SELECT id, event_id FROM outbox WHERE sent_at IS NULL ORDER BY id LIMIT 100;
INSERT INTO broker (event_id) SELECT event_id FROM outbox WHERE sent_at IS NULL ORDER BY id;
-- 4) Relay, run 2 (after restart): the row is still unsent, so it goes out AGAIN, then is marked.
INSERT INTO broker (event_id) SELECT event_id FROM outbox WHERE sent_at IS NULL ORDER BY id;
UPDATE outbox SET sent_at = '2026-10-28 10:00:05' WHERE sent_at IS NULL;
-- 5) Consumer: remember each event_id once; a second copy changes 0 rows, so no second SMS.
INSERT OR IGNORE INTO inbox SELECT event_id FROM broker WHERE seq = 1;
INSERT OR IGNORE INTO inbox SELECT event_id FROM broker WHERE seq = 2;
SELECT b.seq, b.event_id, o.sent_at, (SELECT count(*) FROM inbox) AS applied
FROM broker b JOIN outbox o USING (event_id) ORDER BY b.seq;প্রথম transaction-এর দুটি INSERT-এর প্রতিটিতে ১টি সারি বদলেছে, তারপর COMMIT; দ্বিতীয়টি ROLLBACK — তাই প্রথম SELECT-এ orders 1, outbox_rows 1: ১০০২-এর অর্ডার আর event একসাথে হারিয়েছে। Relay-র SELECT দিল 1 ev-1001; broker-এ দুই চালানোয় দুবার ১টি করে সারি, তারপর UPDATE-এ ১টি। Inbox-এ প্রথম INSERT OR IGNORE-এ ১টি সারি বদলেছে, দ্বিতীয়টিতে ০টি সারি বদলেছে — এই শূন্য দেখে consumer SMS পাঠায় না। শেষ SELECT: 1 ev-1001 2026-10-28 10:00:05 1 আর 2 ev-1001 2026-10-28 10:00:05 1 — broker-এ দুটি কপি, applied ১. SQLite-এর broker টেবিল আসল broker-এর network বা ack দেখায় না, শুধু outbox-এর যুক্তিটি দেখায়।
Relay বাস্তবে — polling, ক্রম আর CDC
- Polling relay: কয়েকশো মিলিসেকেন্ড পরপর
WHERE sent_at IS NULL; এই শর্তে একটি partial index (পর্ব ২১), আর পাঠানো সারি নিয়মিত মোছা। কয়েকটি worker চালাতে PostgreSQL আর MySQL 8-এFOR UPDATE SKIP LOCKEDপ্রচলিত (সংস্করণভেদে আলাদা)। - ক্রম: একই অর্ডারের event-এর ক্রম চাইলে order id-কে message key করুন, যাতে একই partition-এ যায় (পর্ব ৩৩); consumer sequence number দিয়ে যাচাই করতে পারে।
- Change data capture (CDC): polling-এর বদলে ডেটাবেসের transaction log (MySQL-এর binlog, PostgreSQL-এর logical decoding) পড়ে event বানানো; Debezium-এর মতো open-source টুল outbox-এর নতুন সারিগুলো log থেকেই তুলে পাঠাতে পারে। লাভ: polling নেই, commit-এর ক্রম রক্ষা; দাম: আরেকটি চালু ব্যবস্থা আর ডেটাবেস-নির্দিষ্ট সেটআপ।
- আরেকটি পথ: event-ই যদি আসল ডেটা হয় — event sourcing — তাহলে লেখা একটিই, dual write থাকে না (পর্ব ৩৫)।
৯ · কোনটি কখন — বাছাইয়ের ছক আর idempotency-র সুতো
সব কৌশল এক ছকে। প্রশ্ন সবসময় একই: রক্ষা করার নিয়মটি (invariant) কয়টি ডেটাবেস জুড়ে, কতক্ষণের, আর কার নিয়ন্ত্রণে?
| পরিস্থিতি | ভালো পছন্দ | কেন |
|---|---|---|
| একসাথে বদলাতে হয় এমন ডেটা এক ডেটাবেসে রাখা যায় (একই shard key) | একটি local ACID transaction | সবচেয়ে সস্তা আর নিরাপদ — আগে এটি চেষ্টা করুন (পর্ব ২৩) |
| একই distributed SQL ডেটাবেসের কয়েকটি shard | ডেটাবেসের নিজস্ব consensus-ভিত্তিক 2PC | ডেটাবেস সামলায়; আপনি দেন latency-র দাম |
| এক প্রতিষ্ঠান, সব participant XA বোঝে, লেনদেন মিলিসেকেন্ডের | 2PC/XA সম্ভব | blocking আর availability-র দাম মেনে, prepared লেনদেন monitor করে |
| কয়েকটি সেবা, আলাদা দল, বাইরের API, মিনিট বা ঘণ্টার কাজ | saga + outbox + idempotency key | দীর্ঘ lock নেই; isolation-এর প্রতিকার নিজে নকশা করতে হয় |
| টাকা-পয়সার মূল হিসাব | এক ledger ডেটাবেসে double-entry, তার বাইরে saga | “মোট টাকা অপরিবর্তিত” নিয়ম এক জায়গায় রক্ষা পায় (পর্ব ৬০) |
পুরো পথে idempotency — প্রতিটি কড়ায় একটি key
Saga আর outbox দুটোই retry আর duplicate-এর ওপর দাঁড়িয়ে, তাই শিকলের প্রতিটি কড়ায় একটি key:
- App → API:
Idempotency-Keyheader (পর্ব ০৮)। - Orchestrator → সেবা: saga id আর ধাপ, যেমন
s42-ChargePayment. - Compensation: নিজস্ব key — simulator-এর refund-এর মতো।
- Relay → broker: প্রতিটি event-এর event_id, সাধারণত UUID.
- Consumer: inbox-এ event_id, effect-এর সাথে একই local transaction-এ।
- পেমেন্ট → বাইরের gateway: একই বা তা থেকে বানানো key, যাতে আমাদের retry-তে দ্বিতীয় চার্জ না হয়।
আরও গভীরে: Pat Helland-এর “Life beyond Distributed Transactions: an Apostate’s Opinion” (CIDR ২০০৭) যুক্তি দেয়, বড় সিস্টেমে distributed transaction-এর ওপর ভরসা না করে প্রতিটি entity-কে নিজস্ব সীমানা আর idempotent বার্তা দিয়ে ভাবতে। Gray ও Lamport-এর Paxos Commit (২০০৬) আর Spanner-এর গবেষণাপত্র (২০১২) consensus-এর ওপর commit-এর মূল লেখা; Calvin (SIGMOD ২০১২) লেনদেনের ক্রম আগে ঠিক করে commit-এর সময়ের 2PC এড়ানোর পথ দেখায়।
পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা system design ইন্টারভিউর পরীক্ষায় এমন প্রশ্ন আসতে পারে:
- 2PC-এর commit point কোনটি? সব YES পেয়ে coordinator-এর COMMIT record ডিস্কে force হওয়ার মুহূর্ত; তার পরে কেউ abort করতে পারে না।
- RTT ২ ms, fsync ৫ ms, ৩ participant — commit point কত দেরিতে? r + ২f = ২ + ১০ = ১২ ms; পুরো প্রোটোকল ২r + ৩f = ১৯ ms; বার্তা ১২, forced write ৭.
- Outbox কেন exactly-once দেয় না? Publish আর “পাঠানো” চিহ্নের মাঝে relay মরলে event আবার যায়; তাই at-least-once, আর consumer-কে event_id দিয়ে duplicate বাদ দিতে হয়।
১০ · AI-এর যুগে distributed transaction-এর নকশা যাচাই
“Microservice-এ অর্ডার, পেমেন্ট আর inventory কীভাবে consistent রাখব?” — AI প্রায়ই বলে, “Saga ব্যবহার করুন, Kafka দিয়ে event পাঠান।” ভুল নয়, কিন্তু আসল কাজ — কোন ধাপের compensation কী, কোনটি pivot, কোথায় duplicate আসবে — বাদ পড়ে। চেনা ভুলগুলো:
| AI-এর ভুল | কেমন দেখায় | কীভাবে যাচাই করবেন |
|---|---|---|
| সাধারণ, ভারী উত্তর | “Kafka, Redis, saga আর microservices” — অথচ ডেটা এক ডেটাবেসেই রাখা যেত | আগে জিজ্ঞেস করুন: নিয়মটি কি এক local transaction-এ রাখা যায়? |
| Compensation নেই, বা “delete” দিয়ে compensation | SMS বা অপারেটরের টিকিট “undo” | প্রতিটি ধাপে: উল্টানো যায়? pivot কোনটি? দুবার চললে? |
| কোডে dual write | db.commit()-এর পরে producer.send() | দুই লাইনের মাঝে process মেরে দেখুন (অংশ ৮) |
| “exactly-once আছে, duplicate হবে না” | broker-এর নিশ্চয়তা ডেটাবেস আর বাইরের সেবা পর্যন্ত টানা | প্রতিটি consumer-এ event_id দিয়ে duplicate বাদ আছে কি না (পর্ব ৩৪) |
| ACID আর CAP-এর consistency গোলানো | “saga ACID দেয়”, “2PC মানে CAP-এর C” | ACID-এর C নিয়ম রক্ষা, CAP-এর C সর্বশেষ মান (পর্ব ২৪, ৪০); saga-য় I নেই |
| Latency-র হিসাবে ভুল | 2PC-কে “একটি round trip” ধরা, fsync বাদ | নিজে হিসাব: r + ২f, ২r + ৩f, ২N + ১ forced write |
| পণ্যের সীমা বা আচরণ বানানো | “সব ডেটাবেস XA বোঝে”, “prepared transaction ডিফল্টে চালু” | অফিশিয়াল ডকুমেন্টেশন — PostgreSQL-এ max_prepared_transactions ডিফল্টে ০ |
| Failure mode উপেক্ষা | coordinator, orchestrator বা relay crash-এর কথাই নেই | প্রতিটি অংশে জিজ্ঞেস করুন: ঠিক এখানে মরলে কী থাকে? |
AI-কে নকশাকার নয়, কড়া reviewer বানান। আগে নিজে ধাপগুলোর খসড়া করুন, তারপর এভাবে যাচাই করান:
AI-এর নকশা মানার আগে — চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১2PC কেন blocking — আর কোনো চতুর প্রোটোকল দিয়ে কি এটি পুরোপুরি এড়ানো যায়?
একটি atomic commit প্রোটোকলের কাছে তিনটি জিনিস চাওয়া হয়। Agreement: সবাই একই সিদ্ধান্তে পৌঁছাবে। Validity: কেউ NO বললে commit নয়; সবাই YES আর কোনো ব্যর্থতা না থাকলে commit. Termination: ব্যর্থতা থাকলেও বেঁচে থাকা প্রত্যেকে শেষে একটি সিদ্ধান্তে পৌঁছাবে। 2PC প্রথম দুটি সবসময় দেয়, তৃতীয়টি নয় — coordinator না ফেরা পর্যন্ত prepared participant আটকে থাকে, কারণ “সবাই PREPARED, কেউ কিছু জানে না” অবস্থাটি commit আর abort দুই ইতিহাসেই হুবহু এক।
১৯৮১ সালে Dale Skeen গঠনগত শর্তটি দেখান: কোনো participant-এর এমন অবস্থা থাকা চলবে না, যার পাশেই commit আর abort দুটোই সম্ভব। 3PC একটি PreCommit ধাপ বসিয়ে তা মেটায় — কিন্তু শুধু বার্তার দেরির সীমা আর ভাগ-না-হওয়া network-এ। ১৯৮৫ সালের FLP ফল দেখায়, সময়ের সীমাহীন network-এ একটি crash-ও সম্ভব হলে deterministic প্রোটোকল সবসময় ঐকমত্যের নিশ্চয়তা দিতে পারে না, আর non-blocking atomic commit তার ঘনিষ্ঠ আত্মীয়। দুই সেনাপতির সমস্যা আরও মৌলিক: হারাতে পারে এমন বার্তায় দুই পক্ষ কখনো নিশ্চিত হয় না যে শেষ বার্তাটি পৌঁছেছে। 2PC এর সমাধান করে না; শুধু সিদ্ধান্ত এক জায়গায় রাখে, যাতে ভুল না হয়।
তাহলে বাস্তবে? Blocking পুরো দূর করা যায় না, কিন্তু তার শর্ত বদলানো যায়: Paxos Commit (২০০৬) বা Spanner-এর ধাঁচে সিদ্ধান্ত কয়েকটি replica-য় consensus দিয়ে লেখা হয়। তখন “একটি যন্ত্র মরলে আটকায়” বদলে হয় “বেশিরভাগ replica একসাথে হারালে আটকায়” — অনেক বিরল ঘটনা; দাম প্রতিটি লেখায় বাড়তি round trip.
মূল উপলব্ধি: asynchronous network-এ “কখনো ভুল করব না” আর “কখনো থেমে থাকব না” একসাথে পাওয়া যায় না। ভালো নকশা ঠিক করে, কোথায় আর কতজনের ব্যর্থতায় অপেক্ষা মেনে নেওয়া হবে।
প্র ০২Microservices-এর জগতে 2PC আর XA প্রায় এড়িয়ে চলা হয়, অথচ Spanner-এর মতো ডেটাবেস ভেতরে 2PC চালায়। দুটি একসাথে সত্য কীভাবে?
কারণ 2PC-এর দামগুলো দুই জায়গায় দুই রকম। একটি ডেটাবেসের ভেতরে সব participant একই দলের লেখা, একই codebase-এর অংশ। লেনদেন মিলিসেকেন্ডের, network একই প্রতিষ্ঠানের নিয়ন্ত্রণে, আর প্রকাশিত Spanner গবেষণাপত্র অনুযায়ী coordinator আর participant দুটোই Paxos দল — তাই একটি যন্ত্র মরলেও কেউ in-doubt হয়ে আটকে থাকে না। Lock manager, recovery আর prepare-এর নিয়ম সবাই এক রকম বোঝে।
সেবা-জগতে ছবিটি উল্টো:
- ভিন্ন প্রযুক্তি: HTTP API, বাইরের payment gateway, অপারেটরের সিস্টেম “prepare” বোঝে না। XA-র জন্য প্রতিটি participant-কে resource manager হতে হয়, যা অনেক managed ডেটাবেস আর broker সীমিতভাবে দেয় বা দেয়ই না।
- ভিন্ন মালিকানা: আপনার coordinator মরলে অন্য দলের ডেটাবেসের সারি lock-এ আটকে থাকবে — এমন coupling-ই microservices এড়াতে চায় — নইলে এক দলের বিভ্রাট হয়ে দাঁড়ায় অন্য দলের বিভ্রাট।
- Availability-র গুণফল: অংশ ৪-এর হিসাবে প্রতিটি নতুন participant commit-এর availability কমায়। পাঁচটি সেবা, প্রতিটি ৯৯.৯% হলে coordinator-সহ ৯৯.৪%-এর কাছাকাছি।
- সময়ের মাপ: ব্যবসায়িক কাজে মানুষ থাকে — OTP দেওয়া, অপারেটরের সাড়া। মিনিটের পর মিনিট lock ধরা অসম্ভব।
Pat Helland তাঁর ২০০৭ সালের লেখায় এই কথাটিই জোর দিয়ে বলেন: বড় সিস্টেমে প্রতিটি entity-কে নিজের সীমানা ধরে নকশা করুন, আর entity-গুলোর মধ্যে কথা হোক idempotent বার্তায়, অপেক্ষমাণ অবস্থা স্পষ্ট রেখে। এর মানে 2PC খারাপ নয়; এর মানে 2PC একটি সুনির্দিষ্ট পরিসরের যন্ত্র। তাই একই প্রতিষ্ঠানের দলগুলো ডেটাবেসের ভেতরের 2PC নির্দ্বিধায় ব্যবহার করে, কিন্তু সেবার সীমানায় এসে saga আর outbox বেছে নেয়।
মূল উপলব্ধি: 2PC চলে একটি trust domain আর একটি failure domain-এর ভেতরে, ছোট লেনদেনে, replicated coordinator-সহ। সীমানা পেরোলে নকশা করুন eventual consistency, স্পষ্ট অবস্থা আর compensation দিয়ে।
প্র ০৩Compensation কি সত্যিই “undo”? ব্যাংক বা মোবাইল ওয়ালেটে ভুল লেনদেন মুছে না ফেলে উল্টো entry দেওয়া হয় কেন?
Database-এর rollback একটি physical undo: সারিগুলো যেন কখনো বদলায়ইনি, আর বাইরের কেউ মাঝের অবস্থা দেখেনি। Saga-র compensation আসে পরে, যখন মাঝের অবস্থা ইতিমধ্যে commit হয়ে গেছে আর অন্যরা হয়তো তা দেখে কাজও করেছে। তাই এটি semantic undo — একটি নতুন ব্যবসায়িক কাজ, যার নিজস্ব নিয়ম, সময় আর খরচ আছে।
হিসাবরক্ষণে এই নীতি কয়েকশো বছরের পুরোনো। Luca Pacioli ১৪৯৪ সালে double-entry হিসাবের যে পদ্ধতি বর্ণনা করেন, তার মূল কথাই হলো লেখা মোছা হয় না; ভুল শুধরাতে উল্টো entry লেখা হয়। কারণগুলো আজও প্রাসঙ্গিক:
- Audit: কী ঘটেছিল আর কে কখন শুধরেছে — দুটোই খাতায় থাকে। মোছা সারি কোনো প্রমাণ রাখে না।
- অন্যদের দেখা তথ্য: গ্রাহক SMS-এ “৮৫০ টাকা কাটা হয়েছে” দেখেছেন, দিনের শেষের রিপোর্টে লেনদেনটি গেছে। মুছে দিলে এদের সাথে খাতা আর মেলে না।
- সময়ের প্রভাব: refund-এ কয়েক দিন লাগতে পারে, মাশুল কাটতে পারে — “আগের অবস্থা” হুবহু আর ফেরে না।
আর কিছু কাজ আদৌ উল্টানো যায় না। পাঠানো SMS ফেরে না, শুধু নতুন বার্তায় সংশোধন করা যায়। Hold ছাড়ার পর সিট অন্য কেউ কিনে ফেললে “আবার বুক করা” compensation অসম্ভব। তাই নকশায় এগুলোকে pivot-এর পরে রাখা হয়, বাতিলের নীতি (মাশুল, সময়সীমা) ব্যবসার সাথে মিলিয়ে ঠিক করা হয়, আর বারবার ব্যর্থ compensation-এর জন্য মানুষের একটি কাজের তালিকা রাখা হয়, যাতে কোনো saga চুপচাপ আধাআধি পড়ে না থাকে।
মূল উপলব্ধি: compensation একটি ব্যবসায়িক প্রক্রিয়া, প্রযুক্তিগত rollback নয়। তাই এর নকশা প্রকৌশলী একা নয়, ব্যবসার মানুষের সাথে বসে করতে হয়।
প্র ০৪Dual write সারানোর তিন পথ — polling outbox, CDC, আর event sourcing. কোনটি কখন?
তিনটির পেছনে একই চিন্তা: একটিমাত্র লেখাকে সত্যের উৎস বানানো, আর বাকি সব সেখান থেকে বের করা। পার্থক্য কোন লেখাটি আর কীভাবে বের করা।
- Outbox আর polling relay: যেকোনো SQL ডেটাবেসে চলে, বোঝা সহজ, বাড়তি ব্যবস্থা লাগে না। দাম: polling-এর query-র চাপ, polling-এর ব্যবধান সমান দেরি, পুরোনো সারি মোছার কাজ, আর কয়েকটি worker-এ ক্রম রক্ষার ঝামেলা। ছোট থেকে মাঝারি সিস্টেমের প্রথম পছন্দ।
- Outbox আর CDC: relay ডেটাবেসের transaction log পড়ে, তাই polling নেই, দেরি কম, commit-এর ক্রম রক্ষা হয়। দাম: Debezium-এর মতো আরেকটি চালু ব্যবস্থা, ডেটাবেস-নির্দিষ্ট সেটআপ, আর নতুন ব্যর্থতার ধরন — যেমন PostgreSQL-এ log পড়ার slot পিছিয়ে পড়লে ডেটাবেস পুরোনো WAL ধরে রাখে, ফলে ডিস্ক ভরতে পারে। তাই monitoring জরুরি।
- ব্যবসায়িক টেবিলের সরাসরি CDC (outbox ছাড়া): লেখার খরচ সবচেয়ে কম, কিন্তু event হয় “সারি বদলেছে” ধরনের নিচু স্তরের তথ্য, আর ভেতরের schema বাইরের সেবাদের চুক্তি হয়ে যায় — একটি কলামের নাম বদলালেই অন্য দলের কোড ভাঙে।
- Event sourcing: event-ই আসল ডেটা, অবস্থা তা থেকে গোনা হয় (পর্ব ৩৫)। Dual write থাকেই না, পুরো ইতিহাস বিনামূল্যে। দাম: read model আলাদা বানাতে হয়, event-এর schema বিবর্তন কঠিন (পর্ব ১২), আর দলের পুরো চিন্তার ধরন বদলাতে হয়।
যেকোনো পথেই consumer-দের idempotent হতে হবে, কারণ তিনটিই বাস্তবে at-least-once; আর event-এর ক্রম নিয়েও consumer-কে সতর্ক থাকতে হয়।
মূল উপলব্ধি: প্রশ্নটি “কোন টুল” নয়, “সত্যের উৎস কোন লেখা, আর বাকিগুলো কীভাবে তার পেছনে পেছনে আসে”। সরল outbox দিয়ে শুরু করুন; দেরি বা চাপ মাপা সমস্যা হলে CDC-তে যান।
প্র ০৫“Exactly-once” কি সম্ভব? Outbox আর idempotent consumer মিলে আসলে কী পাওয়া যায়?
দুটি আলাদা জিনিস গুলিয়ে যায়: delivery আর effect. হারাতে পারে এমন network-এ একটি বার্তা “ঠিক একবার পৌঁছাবে” — এই নিশ্চয়তা দেওয়া যায় না; ack হারালে প্রেরক জানে না, তাই হয় আবার পাঠায় (at-least-once), নয়তো পাঠায় না (at-most-once). কিন্তু effect — টাকা কাটা, SMS, পয়েন্ট — ঠিক একবার ঘটানো যায়, যদি গ্রাহক event_id দেখে duplicate চেনে আর effect ও “দেখা হয়েছে” চিহ্ন একই local transaction-এ লেখে। চিহ্নটি আগে আলাদা করে লিখলে crash-এ effect হারায়, পরে আলাদা করে লিখলে effect দুবার হতে পারে — তাই দুটি একসাথে। একে অনেকে বলেন effectively-once: at-least-once delivery আর idempotent processing মিলে।
১৯৮৪ সালে Saltzer, Reed আর Clark-এর “End-to-End Arguments in System Design” এই কথাটির তাত্ত্বিক ভিত্তি দেয়: নির্ভরযোগ্যতার আসল নিশ্চয়তা যাচাই করতে হয় দুই প্রান্তে, মাঝের স্তরে নয়। Message broker নিজের ভেতরে duplicate থামালেও (Kafka-র মতো broker-এ লেনদেন আর idempotent producer-এর ব্যবস্থা আছে, লেখার সময়ে) সেই নিশ্চয়তা আপনার consumer-এর ডেটাবেস বা বাইরের SMS gateway পর্যন্ত যায় না।
সবচেয়ে কঠিন প্রান্ত বাইরের সেবা। SMS পাঠানো আর inbox-এ লেখা এক transaction-এ বাঁধা যায় না। SMS যাওয়ার পর, inbox-এর commit-এর আগে crash হলে আবার SMS যাবে। উপায়: বাইরের সেবা নিজে idempotency key নিলে সেটি দিন; না নিলে ঠিক করুন কোনটি সহনীয় — মাঝে মাঝে দুটি SMS, নাকি মাঝে মাঝে একটিও না। টাকা-পয়সায় সাধারণত দ্বিতীয়বার চার্জ অসহনীয়, তাই সেখানে key ছাড়া gateway ব্যবহারই করা হয় না। সিদ্ধান্তটি যা-ই হোক, নকশার নথিতে লিখে রাখুন।
মূল উপলব্ধি: exactly-once পাইপের গুণ নয়, পুরো পথের নকশার গুণ — প্রতিটি প্রান্তে একটি key, প্রতিটি retry-তে সেই একই key, আর effect-এর পাশে একটি “দেখা হয়েছে” খাতা।
অনুশীলন
আগে কাগজে নিজে করুন, তারপর উত্তর মিলিয়ে নিন।
-
চার ধাপের saga নকশা: একটি ভ্রমণ-প্যাকেজ সেবা (কাল্পনিক) কক্সবাজারের প্যাকেজ বেচে: T1 বাসের সিট hold, T2 অংশীদার হোটেলে রুম hold, T3 wallet থেকে টাকা কাটা, T4 বাস অপারেটর আর হোটেলের কাছে চূড়ান্ত confirm; শেষে SMS. প্রতিটি ধাপের compensation, ধরন (compensatable, pivot, retriable), idempotency key, অপেক্ষমাণ অবস্থা আর compensation ব্যর্থ হলে কী হবে — নকশা করুন; ক্রমে দুর্বলতা থাকলে বলুন।
ধাপ কাজ আর অপেক্ষমাণ অবস্থা Compensation ধরন · key T1 HoldSeat সিট HELD(s77), মেয়াদ ১৫ মিনিটReleaseSeat, শর্ত: এখনো s77-এর HELD compensatable · s77-HoldSeatT2 HoldRoom হোটেলে রুম hold, মেয়াদ ১৫ মিনিট ReleaseRoom (অংশীদারের cancel-hold API, শর্তসাপেক্ষ) compensatable · s77-HoldRoomT3 Charge ledger-এ −X, প্যাকেজ PAYMENT_TAKENRefund: নতুন ledger entry +X, মোছা নয় compensatable (খরচসহ) · s77-ChargeT4 Confirm অপারেটরের টিকিট আর হোটেলের voucher নেই pivot · s77-ConfirmT5 SMS গ্রাহককে বার্তা নেই, শুধু সংশোধনী বার্তা retriable · s77-SMSক্রমের দুর্বলতা: T4 আসলে দুটি বাইরের কাজ; বাস confirm হয়ে হোটেল ব্যর্থ হলে pivot আধাআধি, আর বাসের টিকিট উল্টানো যায় না। সমাধান: (ক) T4 ভাঙা — বাতিল সস্তা এমন অংশ (ধরা যাক হোটেল, বাতিলের নীতিসহ) আগে, মাশুলসহ compensatable হিসেবে, তারপর বাস pivot হিসেবে; (খ) আরও ভালো: অংশীদারের সাথে চুক্তি যে hold মানে মেয়াদের মধ্যে নিশ্চিত জায়গা — তখন দুটি confirm-ই retriable, আর T3 হয় pivot.
ব্যর্থতার নিয়ম: ব্যবসায়িক “না” হলে উল্টো ক্রমে Refund (T3 হয়ে থাকলে), ReleaseRoom, ReleaseSeat; ক্ষণিকের ত্রুটিতে একই key-তে backoff-সহ retry. Compensation বারবার ব্যর্থ হলে alert আর মানুষের হাতে, আর hold-এর মেয়াদ নিরাপত্তা জাল। পর্দায় পুরো সময় “বুকিং প্রক্রিয়াধীন”; loyalty পয়েন্ট শুধু COMPLETED হলে — dirty read এড়াতে।
-
ব্যাখ্যা করুন: 2PC-তে সব participant YES দিয়েছে, তারপর coordinator crash করেছে। Participant-রা কেন নিজেরা commit করে ফেলতে পারে না? কোন পরিস্থিতিতে তারা coordinator ছাড়াই মীমাংসা করতে পারে, আর বাস্তবে blocking-এর ঝুঁকি কীভাবে কমানো হয়?
কেন পারে না: YES-এর পর participant prepared; দুটি ইতিহাস তার চোখে অভিন্ন — (ক) coordinator COMMIT লিখে crash করেছে, (খ) নিজের timeout বা গ্রাহকের বাতিলে ABORT লিখে crash করেছে। যেকোনো একতরফা নিয়ম একটিতে ভুল হবে, আর তাতে কেউ commit, কেউ abort — atomicity ভাঙে। তাই একমাত্র নিরাপদ কাজ lock ধরে অপেক্ষা।
কখন মীমাংসা হয় (cooperative termination): অন্য কেউ সিদ্ধান্ত পেয়ে থাকলে তার কাছ থেকে জানা যায়; কেউ ভোট না দিয়ে বা NO দিয়ে থাকলে কেউ commit করতে পারেনি, তাই সবাই abort. আটকায় কেবল যখন নাগালের সবাই prepared আর কেউ সিদ্ধান্ত জানে না।
বাস্তবে ঝুঁকি কমানো: coordinator-এর সিদ্ধান্ত Paxos বা Raft দিয়ে replicated রাখা (Spanner-ধাঁচ), ছোট লেনদেন, prepared লেনদেনের monitoring; শেষ উপায় heuristic সিদ্ধান্ত, atomicity-র ঝুঁকি মেনে।
-
2PC-এর হিসাব: মৌলিক 2PC, ৩টি participant সমান্তরালে, coordinator থেকে প্রত্যেকের RTT ২ ms, প্রতিটি forced log write ৫ ms. বের করুন: (ক) commit point পর্যন্ত সময়; (খ) PREPARE পাওয়া থেকে participant-এর lock-এর সময়; (গ) পুরো প্রোটোকল; (ঘ) বার্তা আর forced write; (ঙ) একটি hot সারিতে সেকেন্ডে সর্বোচ্চ লেনদেন — আর এক ডেটাবেসে, যেখানে lock ধরা থাকে শুধু একটি commit fsync-এর সময়; (চ) RTT ৮০ ms হলে; (ছ) প্রতিটি অংশ ৯৯.৯৫% হলে coordinator-সহ commit-এর availability.
- (ক) r + ২f = ২ + ২ × ৫ = ১২ ms.
- (খ) r + ৩f = ২ + ১৫ = ১৭ ms (execution-এর সময় বাদে)।
- (গ) ২r + ৩f = ৪ + ১৫ = ১৯ ms.
- (ঘ) বার্তা ৪N = ১২; forced write ২N + ১ = ৭.
- (ঙ) ১০০০ ÷ ১৭ ≈ সেকেন্ডে ৫৮.৮টি। এক ডেটাবেসে lock ≈ f = ৫ ms, তাই ১০০০ ÷ ৫ = সেকেন্ডে প্রায় ২০০টি — 2PC একই সারিতে প্রায় সাড়ে তিন গুণ কম লেনদেন দেয়।
- (চ) commit point ৮০ + ১০ = ৯০ ms; lock ৮০ + ১৫ = ৯৫ ms; মোট ১৬০ + ১৫ = ১৭৫ ms; hot সারিতে ১০০০ ÷ ৯৫ ≈ সেকেন্ডে ১০.৫টি। এখানে fsync নয়, দূরত্বই খরচের প্রধান অংশ।
- (ছ) চারটি অংশ series-এ: ০.৯৯৯৫৪ ≈ ০.৯৯৮০০ → প্রায় ৯৯.৮০%; বছরে প্রায় ০.০০১৯৯৯ × ৮,৭৬০ ≈ ১৭.৫ ঘণ্টা commit করা যায় না, যেখানে একটি অংশের জন্য প্রায় ৪.৪ ঘণ্টা।
-
আউটপুট অনুমান করুন: অংশ ৬-এর
saga_orchestrator.js-এconst FAIL_AT = 3;লাইনটি বদলেconst FAIL_AT = 2;করা হলো। চালানোর আগে লিখুন, পুরো আউটপুট কী হবে — প্রতিটি log লাইন আর শেষেরfinal:লাইন। তারপর simulator-এ বদলে মিলিয়ে দেখুন।01 SAGA s42 STARTED 02 BookSeat OK 03 ChargePayment FAILED: balance too low 04 COMPENSATING in reverse order 05 undo BookSeat try 1: seat released 06 SAGA s42 COMPENSATED final: seat=free, wallet=5000, ticket=none, refund calls=0, refunds applied=0ধাপ ২-এ পেমেন্ট নিজেই ব্যর্থ, তাই কোনো টাকা কাটা হয়নি আর ChargePayment
doneতালিকায় ওঠেনি। Compensation শুধু BookSeat-এর — সিট ছেড়ে দেওয়া। Refund একবারও ডাকা হয়নি (refund calls=0), তাই “প্রথম উত্তর হারায়” ফাঁদটিও এবার কাজে আসেনি। IssueTicket চালানোই হয়নি। শিক্ষা: যে ধাপ ব্যর্থ হয়, তার compensation লাগে না — কারণ তার local transaction commit হয়নি; compensation লাগে শুধু আগের সফল ধাপগুলোর। আরFAIL_AT = 0দিলে চারটি log লাইনের পর05 SAGA s42 COMPLETED, আর শেষেfinal: seat=A3 SOLD, wallet=4150, ticket=T-501, refund calls=0, refunds applied=0.
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমTwo-phase commit-এর ঘটনাগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- প্রতিটি participant নিজের অংশ করে lock ধরে রাখে, commit করে না
- Coordinator সবাইকে PREPARE পাঠায়
- প্রতিটি participant PREPARED record force করে YES ভোট দেয়
- Coordinator COMMIT record force করে — commit point
- Participant-রা COMMIT পেয়ে নিজের record force করে, lock ছাড়ে, ACK দেয়
- Coordinator END লিখে লেনদেনটি ভুলে যায়
Two-phase commit-এ একজন participant YES ভোট দিয়েছে, তারপর coordinator-এর কোনো খবর পাচ্ছে না। নিয়ম অনুযায়ী সে কী করতে পারে?
- নিজে abort করে lock ছেড়ে দেবে
- নিজে commit করবে, কারণ সে তো YES দিয়েছে
- Lock ধরে অপেক্ষা করবে, আর coordinator বা অন্য participant-দের কাছে সিদ্ধান্ত জানতে চাইবে
- লেনদেনটি ভুলে যাবে, কারণ timeout হয়ে গেছে
মৌলিক 2PC-তে coordinator থেকে প্রতিটি participant-এর RTT ২ ms, প্রতিটি forced log write ৩ ms, participant ৪ জন, সবাই সমান্তরালে কাজ করে। PREPARE পাঠানো থেকে commit point (coordinator-এর COMMIT record টেকসই হওয়া) পর্যন্ত কত সময়?
- ৫ ms
- ৮ ms
- ১১ ms
- ১৩ ms
একটি saga-র ধাপ T1, T2, T3, T4; প্রতিটির compensation C1, C2, C3. T1 আর T2 commit হয়েছে, তারপর T3 একটি ব্যবসায়িক কারণে স্থায়ীভাবে ব্যর্থ। Backward recovery-তে কী চলবে?
- C3, তারপর C2, তারপর C1
- C2, তারপর C1
- C1, তারপর C2
- T4 চালিয়ে তারপর সব compensation
একটি সেবা প্রথমে নিজের ডেটাবেসে অর্ডার commit করে, তারপর message broker-এ OrderPlaced event পাঠায়। দুই কাজের মাঝখানে process crash করলে কী হয়?
- অর্ডার ডেটাবেসে আছে, কিন্তু event হারিয়ে গেছে
- Event চলে গেছে, কিন্তু অর্ডার নেই
- দুটোই একসাথে rollback হয়
- Broker নিজে থেকে হারানো event পুনরুদ্ধার করে
Transactional outbox-এর relay একটি event broker-এ পাঠানোর পর, কিন্তু outbox-এ “পাঠানো হয়েছে” চিহ্ন দেওয়ার আগে crash করল। Relay ফিরে আসার পর কী ঘটে, আর consumer-কে কী করতে হয়?
- Event আর কখনো যাবে না, তাই consumer-কে কিছু করতে হয় না
- Event আবার যাবে; consumer event_id দেখে duplicate বাদ দেবে
- Broker duplicate চিনে ফেলে দেবে, consumer-এর কিছু করার নেই
- Outbox-এর সারি নিজে থেকে rollback হয়ে যাবে
Three-phase commit (3PC) বাস্তবের সিস্টেমে প্রায় ব্যবহৃত হয় না কেন?
- এটি 2PC-এর চেয়ে কম বার্তা পাঠায় বলে নির্ভরযোগ্য নয়
- এটি বার্তার দেরির সীমা আর partition-হীন network ধরে নেয়; ভাগ হলে অসংগতি হতে পারে
- এটি শুধু একটি participant-এর জন্য কাজ করে
- এটি চালাতে coordinator-কে Paxos দিয়ে replicate করতে হয়
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- কয়েক সেবা আর ডেটাবেস জুড়ে কাজে একটি ACID transaction নেই — তিনটি log, তিনটি commit; timeout মানে “জানি না”.
- 2PC: PREPARED force → YES → coordinator-এর COMMIT force (commit point) → খবর। দাম: r + ২f, lock ≥ r + ৩f, ২N + ১ forced write, availability-র গুণফল; coordinator মরলে in-doubt আর blocked.
- 3PC-র শর্ত বাস্তবে মেলে না; blocking কমানোর বাস্তব পথ coordinator-এর সিদ্ধান্ত consensus দিয়ে replicated রাখা (Paxos Commit, Spanner-ধাঁচ)।
- Saga: local transaction আর compensation, উল্টো ক্রমে; pivot-এর আগে compensatable, পরে retriable; compensation শর্তসাপেক্ষ আর idempotent; isolation নেই, তাই semantic lock, মেয়াদ আর commutative update.
- Dual write নয়, outbox: event একই local transaction-এ; relay বা CDC পাঠায়; at-least-once, তাই প্রতিটি consumer event_id দিয়ে duplicate বাদ দেয় — আর সম্ভব হলে ডেটা এক ডেটাবেসে রেখে সমস্যাটাই এড়ান।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২৬ · Object storage আর distributed file systemপরবর্তী পর্বChunk, metadata আর erasure coding — বড় ফাইলের ভাণ্ডার।
- পর্ব ২৪ · Transaction আর isolation levelফিরে দেখুনএক ডেটাবেসের ভেতরে ACID, anomaly, locking আর MVCC.
- পর্ব ০৮ · REST API নকশা — idempotency keyফিরে দেখুনএকটি POST-এর retry নিরাপদ করার পুরো কৌশল।
- সিস্টেম ডিজাইন · কোর্স হোম৬৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- DBMS ও SQL · Distributed databaseগভীরে যানডেটাবেসের দিক থেকে 2PC আর saga, PostgreSQL-এর prepare কমান্ডসহ।
- DBMS ও SQL · Recoveryপাশের ট্র্যাকWAL আর log দিয়ে crash-এর পর ডেটাবেস কীভাবে ফেরে — 2PC-এর log record-এর ভিত্তি।