পাঠ ১৮ · ৫১-এর মধ্যে · মডিউল ৪
Home / Courses / System Design / টু-ফেজ কমিট

ডিস্ট্রিবিউটেড ট্রানজেকশন ও টু-ফেজ কমিট

Distributed transactions & two-phase commit
৯ মিনিট পড়া মধ্যম · Intermediate Python কোডসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • কেন একটি শার্ডেড/মাল্টি-ডেটাবেস সিস্টেমে একক ট্রানজেকশন এখন আর সহজ নয়
  • Two-Phase Commit-এর দুটি ফেজ — Prepare ও Commit/Abort — বিস্তারিত
  • Coordinator ও Participant-এর ভূমিকা এবং ভোটিং লজিক
  • 2PC-এর blocking দুর্বলতা এবং কেন এটি SAGA (L28)-এর পথ খুলে দেয়
  • Python দিয়ে ২টি participant-এর 2PC সিমুলেশন — একটি সফল, একটি ব্যর্থ কেস

১ · সমস্যা — একাধিক ডেটাবেস জুড়ে Atomicity

DBMS কোর্সে আমরা শিখেছি একটি একক ডেটাবেসের ট্রানজেকশন ACID-এর "A" (Atomicity) মেনে চলে — হয় পুরো ট্রানজেকশন সফল হয়, নয়তো পুরোটাই বাতিল হয়ে যায়, আংশিক অবস্থায় থামে না। কিন্তু L17-এ শার্ডিং শেখার পর প্রশ্ন ওঠে — যদি একটি ব্যবসায়িক অপারেশনের জন্য দুটি ভিন্ন শার্ড বা ডেটাবেসে লিখতে হয় (যেমন ব্যাংক ট্রান্সফার — একজনের অ্যাকাউন্ট থেকে টাকা কাটা এক শার্ডে, অন্যজনের অ্যাকাউন্টে টাকা যোগ করা অন্য শার্ডে) — তাহলে atomicity কীভাবে নিশ্চিত করা যায়? একটি ডেটাবেস সফল হলো, অন্যটি নেটওয়ার্ক সমস্যায় ব্যর্থ হলো — এমন অর্ধেক-সফল অবস্থা কখনোই গ্রহণযোগ্য নয়।

কেন এটি নতুন সমস্যা

একক ডেটাবেসে ট্রানজেকশন ম্যানেজার নিজেই লক ও রোলব্যাক নিয়ন্ত্রণ করে, সব একই মেশিনে। কিন্তু দুটি ভিন্ন ডেটাবেস সার্ভারের মধ্যে কোনো একক নিয়ন্ত্রক নেই — নেটওয়ার্ক যেকোনো মুহূর্তে ব্যর্থ হতে পারে, একটি সার্ভার ক্র্যাশ করতে পারে, বার্তা হারিয়ে যেতে পারে। Two-Phase Commit এই অনিশ্চিত পরিবেশেও সব অংশগ্রহণকারীকে একই সিদ্ধান্তে (সবাই কমিট, অথবা সবাই অ্যাবোর্ট) নিয়ে আসার একটি প্রোটোকল।

২ · Two-Phase Commit — দুটি ফেজ

2PC-তে দুই ধরনের ভূমিকা থাকে — CoordinatorCoordinatorযে নোডটি পুরো ডিস্ট্রিবিউটেড ট্রানজেকশন পরিচালনা করে — সব participant-কে ভোট চায় এবং চূড়ান্ত commit/abort সিদ্ধান্ত জানায়। (একজন, পুরো প্রক্রিয়া পরিচালনা করে) এবং একাধিক ParticipantParticipantএকটি ডেটাবেস/শার্ড যা ট্রানজেকশনের একটি অংশ সম্পন্ন করে — কোঅর্ডিনেটরের নির্দেশে রিসোর্স লক করে, ভোট দেয় এবং কমিট/অ্যাবোর্ট করে। (প্রতিটি ডেটাবেস/শার্ড যেখানে ট্রানজেকশনের একটি অংশ ঘটছে)।

Phase ১ · Prepare (Vote)
কোঅর্ডিনেটর প্রতিটি participant-কে জিজ্ঞেস করে "তুমি কি কমিট করতে পারবে?" প্রতিটি participant নিজের কাজটি সম্পন্ন করার প্রস্তুতি নেয়, প্রয়োজনীয় রিসোর্স লক করে, এবং হ্যাঁ (prepared) বা না (abort) ভোট দেয় — কিন্তু এখনও চূড়ান্তভাবে কমিট করে না।
Phase ২ · Commit / Abort
সব participant হ্যাঁ ভোট দিলে কোঅর্ডিনেটর সবাইকে "কমিট করো" নির্দেশ পাঠায়, প্রতিটি participant লক ছেড়ে স্থায়ীভাবে পরিবর্তন প্রয়োগ করে। যেকোনো একজন না ভোট দিলে কোঅর্ডিনেটর সবাইকে "অ্যাবোর্ট করো" পাঠায় — এমনকি যারা হ্যাঁ বলেছিল তারাও রোলব্যাক করে।
কোঅর্ডিনেটর Coordinator prepare? prepare? Participant ১ Bank-A Participant ২ Bank-B vote: yes/no vote: yes/no commit / abort
Phase ১-এ সবার ভোট সংগ্রহ করার পরই Phase ২-এ চূড়ান্ত কমিট/অ্যাবোর্ট নির্দেশ যায় — কোনো participant একা সিদ্ধান্ত নেয় না।

৩ · দুর্বলতা — Blocking Problem

2PC-এর সবচেয়ে বড় সমস্যা হলো এটি একটি ব্লকিং প্রোটোকল। ধরুন সব participant হ্যাঁ ভোট দিয়ে রিসোর্স লক করে রেখেছে (Phase ১ শেষ), কিন্তু ঠিক তখনই কোঅর্ডিনেটর ক্র্যাশ করল — Phase ২-এর চূড়ান্ত নির্দেশ কখনোই আসবে না। Participant-রা জানে না কমিট করবে না অ্যাবোর্ট করবে, তাই নিরাপদ থাকতে তারা লক ধরে রেখে অনির্দিষ্টকালের জন্য অপেক্ষা করে — এই সময় সেই রিসোর্সগুলো অন্য কোনো ট্রানজেকশনের জন্য ব্যবহারযোগ্য থাকে না।

কেন মাইক্রোসার্ভিস 2PC এড়িয়ে চলে

মাইক্রোসার্ভিস আর্কিটেকচারে (L25) সার্ভিসগুলো স্বাধীনভাবে স্কেল ও ডিপ্লয় হয় — একটি সার্ভিসের রিসোর্স আরেকটি সার্ভিসের ব্যর্থতার জন্য লক হয়ে থাকা এই দর্শনের সম্পূর্ণ বিপরীত। এই কারণেই বাস্তব মাইক্রোসার্ভিস সিস্টেমে 2PC-এর বদলে SAGA প্যাটার্ন (L28) বেশি ব্যবহৃত হয় — যেখানে স্ট্রিক্ট atomicity/isolation ছেড়ে দিয়ে, প্রতিটি ধাপের জন্য একটি compensating (ক্ষতিপূরণমূলক) অ্যাকশন রাখা হয়, লক না ধরে রেখেই।

৪ · Python-এ 2PC সিমুলেশন

নিচে দুটি participant নিয়ে 2PC সিমুলেট করা হয়েছে — একবার উভয়ে হ্যাঁ ভোট দেয় (সফল কমিট), আরেকবার একজন না ভোট দেয় (সম্পূর্ণ অ্যাবোর্ট) — লক্ষ্য করুন দ্বিতীয় কেসে যে participant হ্যাঁ ভোট দিয়েছিল সেও রোলব্যাক করে।

Python
class Participant:
    def __init__(self, name, will_vote_yes):
        self.name = name
        self.will_vote_yes = will_vote_yes
        self.state = "INIT"

    def prepare(self):
        # Phase 1: রিসোর্স লক করা ও ভোট দেওয়া
        if self.will_vote_yes:
            self.state = "PREPARED"
            return True
        self.state = "ABORTED"
        return False

    def commit(self):
        self.state = "COMMITTED"

    def abort(self):
        self.state = "ABORTED"


def two_phase_commit(participants):
    votes = {p.name: p.prepare() for p in participants}
    all_yes = all(votes.values())

    if all_yes:
        for p in participants:
            p.commit()
        outcome = "COMMIT"
    else:
        for p in participants:
            p.abort()
        outcome = "ABORT"

    return outcome, votes


print("=== কেস ১: উভয় participant হ্যাঁ ভোট দেয় ===")
p1 = Participant("Bank-A", True)
p2 = Participant("Bank-B", True)
outcome1, votes1 = two_phase_commit([p1, p2])
print(f"ভোট: {votes1}")
print(f"চূড়ান্ত ফলাফল: {outcome1}")
print(f"Bank-A state: {p1.state}, Bank-B state: {p2.state}")

print()
print("=== কেস ২: Bank-B না ভোট দেয় (রিসোর্স ব্যস্ত/ব্যর্থ) ===")
p3 = Participant("Bank-A", True)
p4 = Participant("Bank-B", False)
outcome2, votes2 = two_phase_commit([p3, p4])
print(f"ভোট: {votes2}")
print(f"চূড়ান্ত ফলাফল: {outcome2}")
print(f"Bank-A state: {p3.state}, Bank-B state: {p4.state}")

    
কেস ২-তে লক্ষ্য করুন — Bank-A হ্যাঁ ভোট দিয়েছিল এবং কমিট করার জন্য প্রস্তুত ছিল, তবুও Bank-B-এর "না" ভোটের কারণে Bank-A-কেও ABORTED অবস্থায় যেতে হয়েছে। এটাই 2PC-এর মূল নিয়ম — একজনও না বললে সবাই বাতিল, আংশিক কমিট কখনোই হয় না।
মূল কথা · Key takeaway

2PC ডিস্ট্রিবিউটেড atomicity-এর একটি প্রমাণিত সমাধান, কিন্তু এর ব্লকিং প্রকৃতির কারণে এটি availability-এর মূল্যে আসে (CAP থিওরেম, L04-এর প্রতিধ্বনি)। কখন strict atomicity (2PC) প্রয়োজন আর কখন eventual consistency + compensation (SAGA, L28) যথেষ্ট — এই সিদ্ধান্তই আধুনিক ডিস্ট্রিবিউটেড সিস্টেম ডিজাইনের একটি কেন্দ্রীয় ট্রেড-অফ।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ কোঅর্ডিনেটর Phase ১-এর পরে ক্র্যাশ করলে participant-রা কেন নিজে থেকে অ্যাবোর্ট করে এগিয়ে যেতে পারে না?

কারণ একটি participant জানে না বাকি participant-রা কী ভোট দিয়েছে। হতে পারে সবাই হ্যাঁ বলেছিল এবং কোঅর্ডিনেটর ক্র্যাশ করার ঠিক আগে কাউকে "commit" নির্দেশ পাঠিয়ে দিয়েছিল কিন্তু বাকিদের পাঠানোর আগেই ক্র্যাশ করল — এই অবস্থায় যদি একটি participant নিজে থেকে অ্যাবোর্ট করে ফেলে, অথচ অন্য একটি participant ইতিমধ্যে কমিট নির্দেশ পেয়ে কমিট করে ফেলেছে, তাহলে সিস্টেমে অসামঞ্জস্যপূর্ণ (কিছু কমিটেড, কিছু অ্যাবোর্টেড) অবস্থা তৈরি হবে — যা atomicity ভঙ্গ করে। তাই নিরাপদ থাকতে participant-রা লক ধরে রেখে কোঅর্ডিনেটরের পুনরুদ্ধারের (recovery) জন্য অপেক্ষা করে।

প্র ০২ 3টি participant থাকলে এবং একটি "না" ভোট দিলে, বাকি ২টি participant-এর জন্য চূড়ান্ত ফলাফল কী হবে?

তিনজনই ABORT পাবে — এমনকি যে দুইজন হ্যাঁ ভোট দিয়েছিল তারাও। 2PC-এর নিয়ম হলো all() — সব participant হ্যাঁ বললেই কেবল কমিট হবে, একজনও না বললে বাকি সবাইকেও রোলব্যাক করতে হবে, কারণ চূড়ান্ত সিদ্ধান্ত সবসময় সর্বসম্মত (unanimous) হতে হবে — আংশিক কমিট বলে কিছু নেই।

প্র ০৩ আধুনিক মাইক্রোসার্ভিস সিস্টেম কেন 2PC-এর বদলে প্রায়ই SAGA প্যাটার্ন বেছে নেয়?

কারণ 2PC-এর blocking প্রকৃতি মাইক্রোসার্ভিসের স্বাধীন-স্কেলিং দর্শনের বিপরীত — একটি সার্ভিসের সমস্যা অন্য সার্ভিসের রিসোর্স আটকে রাখতে পারে না। SAGA (L28) প্রতিটি ধাপকে একটি স্বতন্ত্র লোকাল ট্রানজেকশন হিসেবে চালায় (লক না ধরে রেখে) এবং ব্যর্থতার ক্ষেত্রে compensating অ্যাকশন দিয়ে আগের ধাপগুলো "undo" করে — এতে strict atomicity/isolation হারানো হয় (মাঝপথের অবস্থা সাময়িকভাবে দৃশ্যমান হতে পারে), কিন্তু বিনিময়ে অনেক বেশি availability ও scalability পাওয়া যায়।

অনুশীলন

  1. ট্রেস করুন: ৩টি participant নিয়ে ভাবুন যাদের ভোট যথাক্রমে হ্যাঁ, হ্যাঁ, না। প্রতিটির চূড়ান্ত state (COMMITTED/ABORTED) কী হবে তা কাগজে লিখুন, তারপর উপরের কোড সেলে একটি তৃতীয় Participant যোগ করে মিলিয়ে দেখুন।

    তিনজনেরই চূড়ান্ত state হবে ABORTED — all_yes মিথ্যা হওয়ায় (একজন না বলেছে) কোঅর্ডিনেটর সবাইকে অ্যাবোর্ট নির্দেশ দেবে, এমনকি যে দুইজন হ্যাঁ বলেছিল তারাও। কোড সেলে Participant("Service-C", False) যোগ করে two_phase_commit([p1, p2, p3]) চালালে এটাই নিশ্চিত হবে।

  2. বিশ্লেষণ করুন: কোন ধরনের অপারেশনে 2PC-এর ব্লকিং খরচ মেনে নেওয়া যুক্তিসঙ্গত (যেমন ব্যাংকিং কোর সিস্টেম), আর কোথায় SAGA-এর looser guarantee বেশি উপযুক্ত (যেমন ই-কমার্স অর্ডার প্রসেসিং)?

    কোর ব্যাংকিং লেজার-এর মতো সিস্টেমে, যেখানে সামান্যতম অসামঞ্জস্যও অগ্রহণযোগ্য এবং ট্রানজেকশনের পরিমাণ তুলনামূলক কম/নিয়ন্ত্রিত, সেখানে 2PC-এর strict atomicity মূল্যবান — সাময়িক ব্লকিং একটি গ্রহণযোগ্য খরচ। কিন্তু ই-কমার্স অর্ডার প্রসেসিং-এর মতো উচ্চ-থ্রুপুট, বহু-সার্ভিস সিস্টেমে, যেখানে সাময়িক অসামঞ্জস্যপূর্ণ অবস্থা (যেমন "অর্ডার প্লেসড কিন্তু পেমেন্ট এখনও প্রসেসিং") মেনে নেওয়া যায় এবং compensating action দিয়ে ঠিক করা যায়, সেখানে SAGA-এর availability ও non-blocking প্রকৃতি ব্যবসায়িকভাবে বেশি মূল্যবান।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

আগের পাঠ
L17 · শার্ডিং ও পার্টিশনিং