7 মিনিট

মাল্টি-স্টেপ ওয়ার্কফ্লোর জন্য Postgres ট্রানজেকশন: ব্যবহারিক প্যাটার্ন

মাল্টি-স্টেপ ওয়ার্কফ্লোর জন্য Postgres ট্রানজেকশন শিখুন: কিভাবে আপডেটগুলো নিরাপদে গ্রুপ করবেন, আংশিক রাইট প্রতিরোধ করবেন, রিট্রাই হ্যান্ডেল করবেন এবং ডেটা কনসিস্টেন্ট রাখবেন।

মাল্টি-স্টেপ ওয়ার্কফ্লোর জন্য Postgres ট্রানজেকশন: ব্যবহারিক প্যাটার্ন

কেন মাল্টি-স্টেপ আপডেটগুলো প্রায়ই অসামঞ্জস্যপূর্ণ হয়

বেশিরভাগ বাস্তব ফিচার একক ডাটাবেস আপডেট নয়। এগুলো সাধারণত একটি ছোট চেইন: একটি সারি ইনসার্ট করা, ব্যালান্স আপডেট করা, স্ট্যাটাস চিহ্নিত করা, একটি অডিট রেকর্ড লেখা, হয়তো একটি জব এনকিউ করা। আংশিক রাইট তখন ঘটে যখন উক্ত ধাপগুলোর মাত্র কিছু অংশই ডাটাবেসে পৌঁছে।

এটি তখন দেখা দেয় যখন কোন কিছু চেইনকে বাধা দেয়: সার্ভার এরর, আপনার অ্যাপ ও Postgres-এর মধ্যে টাইমআউট, ধাপ 2-এর পরে ক্র্যাশ, বা এমন একটি রিট্রাই যা ধাপ 1 আবার চালায়। প্রতিটি স্টেটমেন্ট নিজে থেকে ঠিক থাকে। সমস্যা তখন আসে যখন ওয়ার্কফ্লো মাঝপথে থেমে যায়।

সাধারণত আপনি এটি দ্রুত ধরতে পারবেন:

  • একটি সারি আছে, কিন্তু সম্পর্কিত সারি নেই (অর্ডার তৈরি হয়েছে, আইটেম নেই)
  • টাকা সরানো হয়েছে, কিন্তু স্ট্যাটাস বদলায়নি (পেড কিন্তু এখনও unpaid হিসেবে চিহ্নিত)
  • দুটি রেকর্ড যা একটাই হওয়া উচিত (রিট্রাইয়ের পরে ডুপ্লিকেট সাবস্ক্রিপশন)
  • ফ্ল্যাগগুলো অসঙ্গত (ব্যবহারকারী "active" কিন্তু কোন প্ল্যান নেই)
  • স্টেটগুলো যা শুধু লোড বা ব্যর্থতার সময়ই দেখা যায়

একটি স্পষ্ট উদাহরণ: একটি প্ল্যান আপগ্রেড গ্রাহকের প্ল্যান আপডেট করে, একটি পেমেন্ট রেকর্ড যোগ করে এবং উপলব্ধ ক্রেডিট বাড়ায়। যদি অ্যাপ পেমেন্ট সেভ করার পরে ক্র্যাশ করে কিন্তু ক্রেডিট যোগ করার আগে থেমে যায়, সাপোর্ট টেবিলগুলোর একটিতে "paid" দেখবে এবং অন্যটিতে "no credits"। ক্লায়েন্ট যদি রিট্রাই করে, আপনি এমনকি পেমেন্ট দু'বার রেকর্ডও করতে পারেন।

লক্ষ্য সহজ: ওয়ার্কফ্লোটাকে একটি একক সুইচ মনে করুন। সব ধাপ সফল হবে, অথবা কোনটিই হবে না, যাতে আপনি অর্ধ-সম্পন্ন কাজ কখনই সেভ না করেন।

ট্রানজেকশন, সাধারণ ভাষায়

ট্রানজেকশন হলো ডাটাবেসের উপায় বলার: এই ধাপগুলোকে একক ইউনিট হিসেবে বিবেচনা করো। বা সব পরিবর্তন ঘটবে, বা কিছুই হবে না। যখন আপনার ওয়ার্কফ্লো একাধিক আপডেট প্রয়োজন, যেমন একটি সারি তৈরি করা, ব্যালান্স আপডেট করা এবং একটি অডিট রেকর্ড লেখা—তখন এটি গুরুত্বপূর্ণ।

ধরা যাক দুই অ্যাকাউন্টের মধ্যে টাকা স্থানান্তর করা হচ্ছে। আপনাকে অ্যাকাউন্ট A থেকে প্লাস করে অ্যাকাউন্ট B-তে যোগ করতে হবে। যদি অ্যাপ প্রথম ধাপের পরে ক্র্যাশ করে, আপনি শুধু কেটে ফেলা অংশই সিস্টেমে "মনে রাখে" এটা চান না।

Commit বনাম rollback

যখন আপনি commit করেন, আপনি Postgres-কে বলছেন: এই ট্রানজেকশনের সবকিছু রাখো। সব পরিবর্তন স্থায়ী এবং অন্য সেশনগুলোর কাছে দৃশ্যমান হয়ে যায়।

যখন আপনি rollback করেন, আপনি Postgres-কে বলছেন: এই ট্রানজেকশনের সবকিছু ভুলে যাও। Postgres ঐ পরিবর্তনগুলোকে এমনভাবে উল্টে দেয় যেন ট্রানজেকশনটি কখনোই ঘটেনি।

Postgres কি গ্যারান্টি দেয় (এবং কি দেয় না)

একটি ট্রানজেকশনের ভিতরে, Postgres গ্যারান্টি দেয় আপনি কমিট করার আগে অন্য সেশনগুলোর কাছে অর্ধসম্পন্ন ফলাফল দেখাবেন না। যদি কিছু ব্যর্থ হয় এবং আপনি রোলব্যাক করেন, ডাটাবেস ঐ ট্রানজেকশনের লিখাগুলো পরিষ্কার করে।

ট্রানজেকশন খারাপ ওয়ার্কফ্লো ডিজাইনকে ঠিক করে না। যদি আপনি ভুল পরিমাণ কেটে ফেলেন, ভুল ইউজার আইডি ব্যবহার করেন, বা কোন প্রয়োজনীয় চেক এড়িয়ে যান, Postgres বিশ্বস্তভাবে ভুল ফলাফল কমিট করবে। ট্রানজেকশন স্বয়ংক্রিয়ভাবে প্রতিটি ব্যবসায়িক স্তরের সংঘাত (যেমন অতিরিক্ত বিক্রি প্রতিরোধ) আটকায় না—এটি সঠিক কনস্ট্রেইন্ট, লক, বা আইসোলেশন লেভেলের সাথে জোড়া না করলে।

কোন ওয়ার্কফ্লোগুলো গ্রুপ করা উচিত

যখনই আপনি একটি বাস্তব-জগত ক্রিয়া শেষ করতে একাধিক টেবিল (বা একাধিক সারি) আপডেট করেন, তখন আপনি একটি ট্রানজেকশনের প্রার্থী পান। মূল নীতি একই: সবকিছু করা হবে, অথবা কিছুই হবে না।

অর্ডার ফ্লো ক্লাসিক কেস। আপনি একটি অর্ডার সারি তৈরি করতে পারেন, স্টক রিজার্ভ করতে পারেন, পেমেন্ট নিতে পারেন, তারপর অর্ডারকে paid হিসেবে চিহ্নিত করতে পারেন। যদি পেমেন্ট সফল হয় কিন্তু স্ট্যাটাস আপডেট ব্যর্থ হয়, আপনি এমন পরিস্থিতি পাবেন যেখানে টাকা নেওয়া হয়েছে কিন্তু অর্ডার এখনও unpaid হিসাবে আছে। যদি অর্ডার সারি তৈরি হয় কিন্তু স্টক রিজার্ভ না হয়, আপনি তাদের বিক্রি করতে পারেন যে আসলে আপনার কাছে নেই।

ইউজার অনবোর্ডিং একইভাবে চুপিচুপি ভেঙে যায়। ইউজার তৈরি করা, প্রোফাইল রেকর্ড ইনসার্ট করা, রোল অ্যাসাইন করা এবং একটি ওয়েলকাম ইমেল পাঠানোর নির্দেশ সেভ করা—সব মিলিয়ে একটি যৌক্তিক ক্রিয়া। গ্রুপ না করলে আপনি এমন একজন ইউজার পেতে পারেন যিনি সাইন ইন করতে পারবেন কিন্তু কোন অনুমতি নেই, বা একটি প্রোফাইল আছে কিন্তু কোন ইউজার নেই।

ব্যাক-অফিস অ্যাকশনগুলো প্রায়ই সখত "পেপার ট্রেইল + স্টেট চেইনজ" আচরণ চায়। একটি অনুরোধ অনুমোদন করা, একটি অডিট এন্ট্রি লেখা, এবং ব্যালান্স আপডেট করা একসাথে সফল হওয়া উচিত। যদি ব্যালান্স বদলায় কিন্তু অডিট লগ মিসিং থাকে, আপনি হারান কে কি এবং কেন পরিবর্তন করেছে সেই প্রমাণ।

ব্যাকগ্রাউন্ড জবগুলোও উপকৃত হয়, বিশেষ করে যখন আপনি অনেক ধাপে একটি ওয়ার্ক আইটেম প্রসেস করেন: আইটেম ক্লেইম করা যাতে দুই কর্মী এক্টি না করে, ব্যবসায়িক আপডেট প্রয়োগ করা, রিপোর্টিং ও রিট্রাইয়ের জন্য ফলাফল রেকর্ড করা, তারপর আইটেমকে ডোন (বা নির্দিষ্ট কারণেFailed) হিসেবে চিহ্নিত করা। যদি এই ধাপগুলো বিচ্ছিন্ন হয়, রিট্রাই ও কনকারেন্সি বিশৃঙ্খলা সৃষ্টি করবে।

SQL লেখার আগে ওয়ার্কফ্লো ডিজাইন করুন

মাল্টি-স্টেপ ফিচারগুলো ভাঙে যখন আপনি সেগুলোকে স্বাধীন আপডেটের গুচ্ছ হিসেবে আচরণ করেন। ডাটাবেস ক্লায়েন্ট খুলবার আগে, ওয়ার্কফ্লোটিকে একটি ছোট গল্প হিসেবে লিখুন এক স্পষ্ট ফিনিশ লাইনের সাথে: ব্যবহারকারীর জন্য ঠিক কোনটা "ডান" হিসেবে গণ্য হবে?

প্রথমে ধাপগুলো সাধারণ ভাষায় তালিকাভুক্ত করুন, তারপর একটি একক সাফল্য শর্ত নির্ধারণ করুন। উদাহরণ: "অর্ডার তৈরি হয়েছে, ইনভেন্টরী রিজার্ভ করা হয়েছে, এবং ব্যবহারকারী একটি অর্ডার কনফার্মেশন নম্বর দেখেছে।" এই কন্ডিশনের কম নয় এমন কিছুই সফল নয়।

এরপর, ডাটাবেস কাজ এবং বাইরের কাজের মধ্যে একটি কঠিন রেখা আঁকুন। ডাটাবেস ধাপগুলোই সেগুলো যেগুলো ট্রানজেকশন দিয়ে সুরক্ষিত করা যায়। কার্ড পেমেন্ট, ইমেল পাঠানো, বা তৃতীয়-পক্ষ API কলের মতো বাইরের কলগুলো ধীর, অনিয়মিতভাবে ব্যর্থ হতে পারে এবং সাধারণত আপনি সেগুলো রোলব্যাক করতে পারবেন না।

একটি সহজ পরিকল্পনা পদ্ধতি: ধাপগুলোকে (1) অবশ্যই সব-বা-কিছুই হওয়া উচিত, (2) কমিটের পরে ঘটতে পারে—এভাবে আলাদা করে ফেলুন।

ট্রানজেকশনের ভিতরে কি থাকছে তা নির্ধারণ করুন

ট্রানজেকশনের ভিতরে কেবল সেই ধাপগুলো রাখুন যেগুলো একসাথে সঙ্গতিপূর্ণ থাকা জরুরি:

  • কোর সারি তৈরি বা আপডেট করা (order, invoice, account balance)
  • শেয়ার্ড রিসোর্স রিজার্ভ করা (inventory, seats, quota)
  • একটি স্থায়ী "পরবর্তী করবার কাজ" ইভেন্ট রেকর্ড করা (outbox টেবিলে)
  • কনস্ট্রেইন্ট দিয়ে রুল আরোপ করা (unique keys, foreign keys)

সাইড-ইফেক্টগুলো বাইরে সরান। উদাহরণ: প্রথমে অর্ডার কমিট করুন, তারপর আউটবক্স রেকর্ডের উপর ভিত্তি করে কনফার্মেশন ইমেল পাঠান।

প্রতিটি ধাপের জন্য রোলব্যাক প্রত্যাশা লিখুন

প্রতিটি ধাপের জন্য লিখুন পরবর্তী ধাপ ব্যর্থ হলে কি হওয়া উচিত। "রোলব্যাক" মানে ডাটাবেস রোলব্যাক হতে পারে, অথবা এক ধরণের প্রতিকারী অ্যাকশন।

উদাহরণ: পেমেন্ট সফল কিন্তু ইনভেন্টরি রিজার্ভ ব্যর্থ হলে, আগে থেকেই সিদ্ধান্ত নিন আপনি কি দ্রুত রিফান্ড দেবেন, না অর্ডারকে "পেমেন্ট ক্যাপচারড, স্টক অপেক্ষায়" হিসেবে চিহ্নিত করে অ্যাসিঙ্ক্রোনাসভাবে হ্যান্ডেল করবেন।

ধাপে ধাপে: কিভাবে একটি ওয়ার্কফ্লো ট্রানজেকশনে মোড়ানো যায়

ট্রানজেকশন Postgres-কে বলে: এই ধাপগুলোকে এক ইউনিট হিসেবে বিবেচনা করো। সবগুলো হবে, বা কিছুই হবে না। এটা আংশিক রাইট প্রতিরোধ করার সবচেয়ে সরল উপায়।

বেসিক ফ্লো

শুরু থেকে শেষ পর্যন্ত একটি ডাটাবেস কানেকশন (একই সেশন) ব্যবহার করুন। যদি আপনি বিভিন্ন কানেকশনে ধাপগুলো ছড়িয়ে দেন, Postgres সব-অথবা-কিছুই ফলাফল গ্যারান্টি করতে পারবে না।

ক্রমটি সরল: begin, প্রয়োজনীয় রিড ও রাইট করুন, সবকিছু সফল হলে commit, নতুবা rollback করুন এবং একটি স্পষ্ট এরর ফেরত দিন।

নীচে SQL-এ একটি ন্যূনতম উদাহরণ:

BEGIN;

-- reads that inform your decision
SELECT balance FROM accounts WHERE id = 42 FOR UPDATE;

-- writes that must stay together
UPDATE accounts SET balance = balance - 50 WHERE id = 42;
INSERT INTO ledger(account_id, amount, note) VALUES (42, -50, 'Purchase');

COMMIT;

-- on error (in code), run:
-- ROLLBACK;

সংক্ষিপ্ত (এবং ডিবাগযোগ্য) রাখুন

ট্রানজেকশন চলাকালীন লক ধরে রাখে। আপনি যতক্ষণ এগুলো খোলা রাখবেন, অন্য কাজগুলো তত বেশি ব্লক হবে এবং টাইমআউট বা ডেডলকের সম্ভাবনা বাড়বে। ট্রানজেকশনের ভিতরে জরুরি কাজগুলোই রাখুন, এবং ধীর কাজগুলো (ইমেল পাঠানো, পেমেন্ট প্রোভাইডার কল, PDF জেনারেট করা) বাইরে সরান।

কিছু ব্যর্থ হলে পুনরুৎপাদন করতে পর্যাপ্ত কনটেক্সট লগ করুন, তবে সংবেদনশীল ডেটা ফাঁস করবেন না: ওয়ার্কফ্লো নাম, order_id বা user_id, মূল প্যারামিটার (amount, currency), এবং Postgres এরর কোড। সম্পূর্ণ পে-লোড, কার্ড ডেটা বা ব্যক্তিগত বিবরণ লগ করা উচিত নয়।

কনকারেন্সি বেসিক: লক ও আইসোলেশন জটিলতা ছাড়াই

সম্পূর্ণ নিয়ন্ত্রণ রাখুন
ট্রানজ্যাকশন নিরাপত্তার জন্য আপনি যেটা পর্যালোচনা, কাস্টমাইজ ও অডিট করতে চান সেই সোর্স কোড পান।

কনকারেন্সি হলো কেবল একই সময়ে দুইটি জিনিস ঘটা। দুই গ্রাহক শো দেখছেন একটাই টিকিট বাকি। উভয় স্ক্রিনে "1 left" দেখায়, উভয় ক্লিক করে Pay, এখন আপনার অ্যাপকে সিদ্ধান্ত নিতে হবে কে পাবে।

প্রতিরক্ষা অব্যবহারে, দুটি রিকোয়েস্ট একই পুরনো ভ্যালু পড়তে পারে এবং উভয়ই আপডেট লিখতে পারে। এভাবেই নেতিবাচক ইনভেন্টরি, ডুপ্লিকেট রিজার্ভেশন, বা অর্ডার ছাড়া পেমেন্ট দেখা যায়।

রো লকগুলো সবচেয়ে সহজ গার্ডরেইল। আপনি যে নির্দিষ্ট সারিটি বদলাতে যাচ্ছেন সেটি লক করেন, চেক করেন, তারপর আপডেট করেন। একই সারি স্পর্শ করা অন্য ট্রানজেকশনগুলোকে আপনার কমিট বা রোলব্যাক পর্যন্ত অপেক্ষা করতে হয়, যা ডাবল আপডেট আটকায়।

একটি প্রচলিত প্যাটার্ন: একটি ট্রানজেকশন শুরু করুন, FOR UPDATE দিয়ে ইনভেন্টরি সারি সিলেক্ট করুন, স্টক আছে কি ভেরিফাই করুন, এটি হ্রাস করুন, তারপর অর্ডার ইনসার্ট করুন। এটাই দরজা ধরে রাখে যতক্ষণ আপনি জরুরি ধাপগুলো শেষ করেন।

আইসোলেশন লেভেলগুলো নিয়ন্ত্রণ করে কনকারেন্ট ট্রানজেকশনগুলো থেকে কতটা অদ্ভুত ফল আপনি অনুমোদন করেন। ট্রেড-অফ সাধারণত সেফটি বনাম গতি:

  • Read Committed (ডিফল্ট): দ্রুত, কিন্তু আপনি স্টেটমেন্টগুলোর মাঝে অন্যদের কমিট হওয়া পরিবর্তন দেখতে পারেন।
  • Repeatable Read: আপনার ট্রানজেকশন একটি স্থির স্ন্যাপশট দেখে, ধারাবাহিক রিডের জন্য ভালো, তবে রিট্রাই বেশি হতে পারে।
  • Serializable: সর্বশ্রেষ্ঠ সেফটি, Postgres একটি ট্রানজেকশনকে বাতিল করতে পারে যেন ফলাফলগুলো এক-এক করে চালানো হয়েছে বলে মনে হয়।

লকগুলো সংক্ষিপ্ত রাখুন। যদি একটি ট্রানজেকশন খোলা বসে থাকে যখন আপনি একটি বহির্গত API কল করেন বা ব্যবহারকারীর একশন অপেক্ষা করেন, আপনি দীর্ঘ অপেক্ষা ও টাইমআউট তৈরি করবেন। একটি পরিষ্কার ফেলিওর পথ পছন্দ করুন: একটি লক টাইমআউট সেট করুন, এরর ধরা হলে "কৃপয়া রিট্রাই করুন" ফেরত দিন, অনির্দিষ্টভাবে অনুরোধ লটকে রাখার বদলে।

বহির্গত কাজ (যেমন কার্ড চার্জ করা) দরকার হলে ওয়ার্কফ্লো ভাগ করুন: দ্রুত রিজার্ভ করুন, কমিট করুন, তারপর ধীর অংশটি করুন, এবং আরেকটি সংক্ষিপ্ত ট্রানজেকশনে ফাইনালাইজ করুন।

রিট্রাই যা ডুপ্লিকেট তৈরি করে না

রিট্রাইসমূহ Postgres-ভিত্তিক অ্যাপগুলিতে স্বাভাবিক। একটি অনুরোধ ব্যর্থ হতে পারে এমনকি আপনার কোড সঠিক থাকলেও: ডেডলক, স্টেটমেন্ট টাইমআউট, সংক্ষিপ্ত নেটওয়ার্ক ড্রপ, বা উচ্চ আইসোলেশন লেভেলে সেরিয়ালাইজেশন এরর। যদি আপনি কেবল একই হ্যান্ডলার আবার চালান, আপনি দ্বিতীয় অর্ডার তৈরি করা, দুবার চার্জ করা, বা ডুপ্লিকেট "ইভেন্ট" সারি ইনসার্ট করার ঝুঁকি নেবেন।

ফিক্স হলো আইডেম্পোটেন্সি: একই ইনপুটে অপারেশন দু'বার চালানো নিরাপদ হওয়া উচিত। ডাটাবেস জানতে পারবে "এটি একই রিকুয়েস্ট" এবং ধারাবাহিকভাবে প্রতিক্রিয়া দেবে।

একটি ব্যবহারিক প্যাটার্ন হলো প্রতিটি মাল্টি-স্টেপ ওয়ার্কফ্লোর সাথে একটি idempotency কী (সাধারণত ক্লায়েন্ট-জেনারেটেড request_id) যুক্ত করা এবং এটিকে মেইন রেকর্ডে স্টোর করা, পরে ঐ কী-তে ইউনিক কনস্ট্রেইন্ট যোগ করা।

উদাহরণ: চেকআউটে, ব্যবহারকারী Pay এ ক্লিক করলে request_id জেনারেট করুন, তারপর ঐ request_id সহ অর্ডার ইনসার্ট করুন। যদি রিট্রাই ঘটে, দ্বিতীয় চেষ্টা ইউনিক কনস্ট্রেইন্টে পড়ে এবং আপনি বিদ্যমান অর্ডার ফেরত দেবেন নতুনটি তৈরির বদলে।

সাধারণভাবে যা গুরুত্বপূর্ণ:

  • (request_id) বা (user_id, request_id) এর উপর ইউনিক কনস্ট্রেইন্ট ব্যবহার করে ডুপ্লিকেট ব্লক করুন।
  • যখন কনস্ট্রেইন্ট ট্রিগার হয়, বিদ্যমান সারিটি ফেচ করে একই ফলাফল ফেরত দিন।
  • সাইড ইফেক্টগুলোও একই নিয়ম অনুসরণ করুক: প্রতিটি অর্ডারের জন্য শুধু একটি পেমেন্ট intent, প্রতিটি অর্ডারের জন্য কেবল একটি "order confirmed" ইভেন্ট।
  • রিকোয়েস্ট_আইডি লগ করুন যাতে সাপোর্ট ট্রেস করতে পারে কি ঘটেছে।

রিট্রাই লুপকে ট্রানজেকশনের বাইরে রাখুন। প্রতিটি চেষ্টা একটি নতুন ট্রানজেকশন শুরু করা উচিৎ এবং উপরে থেকে পুরো ইউনিট অফ ওয়ার্ক আবার চালানো উচিৎ। একটি ব্যর্থ ট্রানজেকশনের ভিতরে রিট্রাই করা সাহায্য করে না কারণ Postgres সেটিকে aborted হিসেবে চিহ্নিত করে।

একটি ছোট উদাহরণ: আপনার অ্যাপ অর্ডার তৈরি করে এবং ইনভেন্টরি রিজার্ভ করার চেষ্টা করে, কিন্তু COMMIT-এর ঠিক পরে টাইমআউট হয়। ক্লায়েন্ট রিট্রাই করে। একটি idempotency কী থাকলে দ্বিতীয় অনুরোধ ইতোমধ্যেই তৈরি অর্ডার ফেরত দেবে এবং দ্বিতীয় রিজার্ভ প্রতিরোধ করবে, কাজ দ্বিগুণ করবে না।

নিয়ম প্রয়োগ করতে ডাটাবেস ব্যবহার করুন, শুধু কোড নয়

কনস্ট্রেইন্ট দিয়ে শুরু করুন
স্কিমা, কনস্ট্রেইন্ট এবং হ্যান্ডলার একসাথে জেনারেট করুন যাতে আংশিক রাইট তৈরি করা কঠিন হয়।

ট্রানজেকশনগুলো মাল্টি-স্টেপ ওয়ার্কফ্লো একসাথে রাখে, কিন্তু তারা স্বয়ংক্রিয়ভাবে ডেটা সঠিক করে না। আংশিক-রাইট ফলাফল এড়াতে একটি শক্ত উপায় হলো ডাটাবেসে "ভুল" স্টেটগুলি কঠিন বা অসম্ভব করে তোলা, এমনকি যদি অ্যাপ কোডে বাগ চলে আসে।

বেসিক সেফটি রেল দিয়ে শুরু করুন। ফোরেন কী নিশ্চিত করে রেফারেন্সগুলো বাস্তব (একটি order line নাও করে একটি অনুপস্থিত order কে)। NOT NULL অর্ধ-পূরণ সারিকে থামায়। CHECK কনস্ট্রেইন্ট দিয়ে এমন মান আটকান যা মানে পাঠায় না (উদাহরণ: quantity > 0, total_cents >= 0)। এই নিয়মগুলো প্রতিটি লিখায় চলবে, কোনো সার্ভিস বা স্ক্রিপ্ট ডাটাবেস স্পর্শ করুক না কেন।

দীর্ঘ ওয়ার্কফ্লো জন্য, স্টেট চেইঞ্জগুলো স্পষ্টভাবে মডেল করুন। অনেক বুলিয়ান ফ্ল্যাগের পরিবর্তে একটি একক status কলাম ব্যবহার করুন (pending, paid, shipped, canceled) এবং কেবল বৈধ ট্রানজিশনগুলোই অনুমোদন করুন। আপনি কনস্ট্রেইন্ট বা ট্রিগার দিয়ে এটি জোর করতে পারেন যাতে ডাটাবেস অবৈধ জাম্পগুলো (যেমন shipped -> pending) গ্রহণ না করে।

ইউনিকনেস আরেকটি ধরনের সঠিকতা। যেখানে ডুপ্লিকেট আপনার ওয়ার্কফ্লো ভেঙে দেবে সেখানে ইউনিক কনস্ট্রেইন্ট যোগ করুন: order_number, invoice_number, বা রিট্রাই কিলের জন্য ব্যবহার করা idempotency_key। এরপর, যদি আপনার অ্যাপ একই রিকুয়েস্ট রিট্রাই করে, Postgres দ্বিতীয় ইনসার্ট ব্লক করবে এবং আপনি নিরাপদে "already processed" ফিরিয়ে দিতে পারবেন বরং দ্বিতীয় অর্ডার তৈরি করার বদলে।

ট্রেসেবিলিটির প্রয়োজন হলে, এটি স্পষ্টভাবে স্টোর করুন। একটি অডিট টেবিল (বা হিস্টোরি টেবিল) যারা কি পরিবর্তন করেছে ও কখন তা রেকর্ড করলে "রহস্যময় আপডেট"গুলিকে ইনসিডেন্টে কোয়েরি করার মত বাস্তব ইভেন্টে পরিণত করে।

সাধারণ ভুলগুলো যা আংশিক রাইট ঘটে

বেশিরভাগ আংশিক রাইট "খারাপ SQL" থেকে আসে না। এগুলো আসে ওয়ার্কফ্লো সিদ্ধান্ত থেকে যা এটি সহজ করে দেয় যে আপনি গল্পের শুধু অর্ধেকটাই কমিট করবেন।

প্রকৃত অ্যাপে দেখা যায় এমন ফাঁদগুলো

  • ট্রানজেকশন খোলা রেখে ধীর বাইরের কাজ করা। পেমেন্ট প্রোভাইডার কল করা, ইমেল পাঠানো, বা ফাইল আপলোড ট্রানজেকশনের ভিতরে করে গেলে লকগুলি বেশি সময় ধরে রাখা হয়। API ধীর বা টাইমআউট হলে অন্য ইউজারগুলো আপনার ওপেন ট্রানজেকশনের পিছনে কিউ হয়ে যাবে।
  • ট্রানজেকশনের বাইরে পড়া, পরে লেখা। উদাহরণ: আপনি একটি ইউজারের ব্যালান্স ফেচ করেন, স্ক্রিনে দেখান, তারপর পরে সেই পুরোনো ভ্যালু ভিত্তিতে কেটে দেন। অন্য একটি সেশন মাঝে ব্যালান্স পরিবর্তন করতে পারে।
  • এরর ধরার পরে তবুও কিছু কমিট করা। একটি সাধারণ প্যাটার্ন হলো "চেষ্টা ধাপ 1, চেষ্টা ধাপ 2, এরর লগ করো, তবুও সাফল্য রিটার্ন করো।" যদি কোড কোনো ব্যর্থতার পরে COMMIT-এ পৌঁছায়, আপনি ইচ্ছাকৃতভাবে ডাটাবেসকে অসঙ্গত করেছেন।
  • কোড পাথ অনুযায়ী টেবিলগুলো বিভিন্ন ক্রমে আপডেট করা। যদি একটি রিকুয়েস্ট accounts তারপর orders আপডেট করে, কিন্তু অন্যটি orders তারপর accounts করে, লোডের সময় ডেডলক হওয়ার সম্ভাবনা বাড়ে।
  • ট্রানজেকশনগুলো অনেকক্ষণ খোলা রাখা। দীর্ঘ ট্রানজেকশন লেখাগুলো ব্লক করতে পারে, ভ্যাকুয়ামের ক্লিনআপ বিলম্ব করে এবং বিভ্রান্তিকর টাইমআউট তৈরি করে।

একটি বাস্তব উদাহরণ: চেকআউটে, আপনি ইনভেন্টরি রিজার্ভ করেন, অর্ডার তৈরি করেন, তারপর কার্ড চার্জ করেন। যদি আপনি চার্জটিকে একই ট্রানজেকশনের ভিতরে করেন, আপনি নেটওয়ার্ক অপেক্ষায় ইনভেন্টরি লক ধরে রাখতে পারেন। যদি চার্জ সফল হয় কিন্তু পরে আপনার ট্রানজেকশন রোলব্যাক হয়, আপনি গ্রাহককে চার্জ করে ফেলেছেন কিন্তু অর্ডার নেই।

একটি নিরাপদ প্যাটার্ন হলো: ট্রানজেকশনকে ডাটাবেস স্টেটে ফোকাসেড রাখুন (রিজার্ভ ইনভেন্টরি, অর্ডার তৈরি, পেমেন্ট pending রেকর্ড করা), কমিট করুন, তারপর বাইরের এপিআই কল করুন, এবং নতুন একটি সংক্ষিপ্ত ট্রানজেকশনে ফলাফল ফেরত লিখুন। অনেক দল এটি পেন্ডিং স্ট্যাটাস ও একটি ব্যাকগ্রাউন্ড জব দিয়ে বাস্তবায়ন করে।

অলে-অর-নাথিং অপারেশনের দ্রুত চেকলিস্ট

যখন একটি ওয়ার্কফ্লোতে একাধিক ধাপ থাকে (ইনসার্ট, আপডেট, চার্জ, পাঠানো), লক্ষ্য সহজ: বা সবকিছু রেকর্ড হবে, বা কিছুই না।

ট্রানজেকশন বাউন্ডারি

প্রয়োজনীয় প্রতিটি ডাটাবেস রাইট এক ট্রানজেকশনের ভিতরে রাখুন। যদি একটি ধাপ ব্যর্থ হয়, রোলব্যাক করুন এবং ডেটা ঠিক আগের মত রাখুন।

সাফল্যের শর্ত স্পষ্ট করুন। উদাহরণ: "অর্ডার তৈরি হয়েছে, স্টক রিজার্ভ হয়েছে, এবং পেমেন্ট স্টেটাস রেকর্ড করা হয়েছে।" এর চেয়ে কম কিছুই সাফল্য নয়।

  • সমস্ত প্রয়োজনীয় রাইট একক BEGIN ... COMMIT ব্লকের ভিতরে ঘটে।
  • ডাটাবেসে একটি এক স্পষ্ট ডান অবস্থা আছে (শুধু অ্যাপ মেমরিতে নয়)।
  • কোনো এরর হলে ROLLBACK যায়, এবং কলার স্পষ্ট ব্যর্থ ফল পায়।

সেফটি রেল (রিট্রাই যেন ক্ষতি না করে)

একই রিকুয়েস্ট রিট্রাই হতে পারে ধরে নিন। ডাটাবেসকে কেবল-একবার নিয়মগুলো প্রয়োগ করতে সাহায্য করুন।

  • অন-চেয়ার একবার হওয়া অ্যাকশনের জন্য ইউনিক কনস্ট্রেইন্ট ব্যবহার করুন (প্রতি অর্ডারের জন্য এক পেমেন্ট রেকর্ড, বা প্রতি আইটেম প্রতি অর্ডারের একটি রিজার্ভ)।
  • রিট্রাই নিরাপদ ও পুনরাবৃত্তিযোগ্য রাখুন (একই ইনপুট একই চূড়ান্ত অবস্থা দেয়, ডুপ্লিকেট নয়)।

ট্রানজেকশনগুলো সংক্ষিপ্ত রাখুন

ট্রানজেকশনের ভিতরে সর্বনিম্ন কাজ করুন, এবং লক ধরে রেখে নেটওয়ার্ক কল এড়িয়ে চলুন।

  • ট্রানজেকশনগুলো সংক্ষিপ্ত রাখুন, এবং টাইমআউট সেট করুন যাতে সেগুলো লटका না থাকে।
  • ধীর কাজ (পেমেন্ট প্রোভাইডারকে কল করা) ট্রানজেকশনের বাইরে করুন, তারপর একটি নতুন সংক্ষিপ্ত ট্রানজেকশনে ফলাফল রেকর্ড করুন।

ফলাফলের নজর রাখুন

আপনি কোথায় ভেঙেছে তা দেখতে না পারলে, আপনি অনুমান করতে থাকবেন।

  • প্রতিটি ব্যর্থতার জন্য ওয়ার্কফ্লো স্টেপ এবং একটি রিকোয়েস্ট আইডি লগ করুন।
  • রোলব্যাক রেট ও লক টাইমআউট মনিটর করুন যাতে আংশিক-রাইট ঝুঁকি সময়মতো ধরা যায়।

উদাহরণ: এমন একটি চেকআউট ফ্লো যা ব্যর্থতায়ও কনসিস্টেন্ট থাকে

চেকআউট-এ এটি প্রয়োগ করুন
টাইমআউট, রিট্রাই ও কনকারেন্সির নিচে সঠিক থাকা একটি অর্ডার ফ্লো তৈরি করুন।

একটি চেকআউটে কয়েকটি ধাপ আছে যা একসাথে চলে: অর্ডার ক্রিয়েট করা, ইনভেন্টরি রিজার্ভ করা, পেমেন্ট চেষ্টা রেকর্ড করা, তারপর অর্ডারের স্ট্যাটাস চিহ্নিত করা।

ধরুন ব্যবহারকারী 1টি আইটেম কেনার জন্য Buy ক্লিক করেছে।

একটি নিরাপদ ফ্লো (ডিবি কাজ এক ইউনিট)

একটি ট্রানজেকশনের ভিতরে কেবল ডাটাবেস পরিবর্তনগুলো করুন:

  • orders টেবিলে একটি সারি ইনসার্ট করুন স্ট্যাটাস pending_payment সহ।
  • স্টক রিজার্ভ করুন (যেমন, inventory.available হ্রাস করা বা একটি reservations সারি তৈরি করা)।
  • ক্লায়েন্ট-প্রদান করা idempotency_key সহ একটি payment_intents সারি ইনসার্ট করুন (যা ইউনিক)।
  • একটি outbox সারি ইনসার্ট করুন যেমন "order_created"।

যদি কোন স্টেটমেন্ট ব্যর্থ হয় (স্টক শেষ, কনস্ট্রেইন্ট এরর, ক্র্যাশ), Postgres পুরো ট্রানজেকশন রোলব্যাক করে দেবে। আপনি এমন কিছুও পাবেন না যেমন অর্ডার আছে কিন্তু রিজার্ভ নেই, বা রিজার্ভ আছে কিন্তু অর্ডার নেই।

যদি পেমেন্ট মাঝপথে ব্যর্থ হয়?

পেমেন্ট প্রোভাইডার আপনার ডাটাবেসের বাইরে, তাই এটাকে আলাদা ধাপ হিসেবে বিবেচনা করুন।

প্রোভাইডারের কল যদি আপনি কমিট করার আগে ব্যর্থ করে, ট্রানজেকশন বাতিল করুন এবং কিছুই লেখা হবে না। যদি প্রোভাইডারের কল কমিটের পরে ব্যর্থ হয়, একটি নতুন ট্রানজেকশন চালান যা পেমেন্ট চেষ্টাকে failed হিসেবে চিহ্নিত করে, রিজার্ভ মুক্ত করে, এবং অর্ডার স্ট্যাটাস canceled করে।

দ্বিতীয় অর্ডার তৈরি না করে রিট্রাই

ক্লায়েন্ট প্রতি চেকআউট চেষ্টা একটি idempotency_key পাঠাক। এটি payment_intents(idempotency_key) এ একটি ইউনিক ইনডেক্স দিয়ে জোর করুন (অথবা আপনি চাইলে orders-এ)। রিট্রাইতে আপনার কোড বিদ্যমান সারিগুলো দেখে এবং নতুন ইনসার্ট না করে চলতে থাকবে।

ইমেল ও নোটিফিকেশন

ট্রানজেকশনের ভিতরে ইমেল পাঠাবেন না। একই ট্রানজেকশনে একটি আউটবক্স রেকর্ড লিখুন, তারপর কমিটের পরে একটি ব্যাকগ্রাউন্ড ওয়ার্কার ইমেল পাঠাক। এভাবে আপনি কখনই রোলব্যাক হওয়া অর্ডারের জন্য ইমেল পাঠাবেন না।

পরবর্তী ধাপ: এই সপ্তাহে এক ওয়ার্কফ্লোতে এটি প্রয়োগ করুন

একটি ওয়ার্কফ্লো বাছুন যা একাধিক টেবিল ছোঁয়: সাইনআপ + ওয়েলকাম ইমেল এনকিউ, চেকআউট + ইনভেন্টরি, ইনভয়েস + লেজার এন্ট্রি, বা প্রজেক্ট তৈরি + ডিফল্ট সেটিংস।

ধাপগুলো আগে লিখুন, তারপর যেগুলো সবসময় সত্য থাকা দরকার সেগুলো (আপনার ইনভারিয়েন্টগুলো) নির্ধারণ করুন। উদাহরণ: "একটি অর্ডার হয় সম্পূর্ণরূপে পেইড এবং রিজার্ভ করা, অথবা পেইড নয় এবং রিজার্ভ নয়। কখনই অর্ধ-রিজার্ভেড নয়।" ঐ নিয়মগুলোকে একটি সব-বা-কিছুই ইউনিটে রূপান্তর করুন।

একটি সহজ পরিকল্পনা:

  • নির্দিষ্ট SQL অপারেশনগুলো ক্রমে তালিকাভুক্ত করুন (রিড, ইনসার্ট, আপডেট, ডিলিট)।
  • প্রথমে অনুপস্থিত ডাটাবেস কনস্ট্রেইন্ট যোগ করুন (ইউনিক কী, ফোরেন কী, চেক কনস্ট্রেইন্ট)।
  • রিট্রাই ডুপ্লিকেট না করবে এমন করে একটি idempotency কী যোগ করুন।
  • ধাপগুলো এক ট্রানজেকশনে মোড়ান এবং সাফল্য পয়েন্ট স্পষ্ট করুন (সব চেক পাস করলে কেবল কমিট)।
  • নিরাপদ রিট্রাই কেমন হবে তা সিদ্ধান্ত নিন (একই idempotency কী, একই আউটকাম)।

তারপর রুক্ষ কেসগুলি ইচ্ছাকৃতভাবে টেস্ট করুন। ধাপ 2-এর পরে ক্র্যাশ সিমুলেট করুন, কমিটের ঠিক আগে টাইমআউট সিমুলেট করুন, এবং UI থেকে ডাবল-সাবমিট সিমুলেট করুন। লক্ষ্য হওয়া উচিত বোরিং আউটকাম: কোন孤立 সারি নেই, দুবার চার্জ নেই, চিরস্থায়ী পেন্ডিং নয়।

আপনি যদি দ্রুত প্রোটোটাইপ করছেন, তাহলে হ্যান্ডলার ও স্কিমা জেনারেট করার আগে প্ল্যানিং-ফার্স্ট টুলে ওয়ার্কফ্লো স্কেচ করা সাহায্য করে। উদাহরণ হিসেবে, Koder.ai (koder.ai) এর একটি Planning Mode আছে যা স্ন্যাপশট ও রোলব্যাক সাপোর্ট করে, এবং ট্রানজেকশন বাউন্ডারি ও কনস্ট্রেইন্টে ইটারেট করার সময় সহায়ক হতে পারে।

এই সপ্তাহে এক ওয়ার্কফ্লো করুন। দ্বিতীয়টি অনেক দ্রুত হবে।

সাধারণ প্রশ্ন

কখন আমার Postgres ট্রানজ্যাকশন ব্যবহার করা উচিত?

একজন ব্যবহারকারীর একটি কাজ যদি একাধিক সারি বা টেবিল বদলায় এবং সেগুলোর মধ্যে সামঞ্জস্য থাকা জরুরি হয়, তখন ট্রানজ্যাকশন ব্যবহার করুন। এর সাধারণ উদাহরণ হলো চেকআউট, অ্যাকাউন্টে টাকা স্থানান্তর, ভূমিকা নির্ধারণসহ সাইনআপ এবং ইনভেন্টরি সংরক্ষণ।

Postgres-এ COMMIT কী করে?

একটি কমিট ট্রানজ্যাকশনের সব পরিবর্তন স্থায়ী করে এবং অন্য ডেটাবেস সেশনে দৃশ্যমান করে। প্রয়োজনীয় সব যাচাই ও লেখা সফল হওয়ার পরেই কমিট করুন।

আমি ROLLBACK করলে কী হয়?

রোলব্যাক ট্রানজ্যাকশন বাতিল করে এবং এর কমিট না করা পরিবর্তনগুলো মুছে দেয়। প্রয়োজনীয় কোনো ধাপ ব্যর্থ হলে আপনার কোডের রোলব্যাক করা উচিত, তারপর কলার সামলাতে পারে এমন একটি ত্রুটি ফেরত দেওয়া উচিত।

সব ট্রানজ্যাকশন কুয়েরির কি একই কানেকশন দরকার?

সেই ওয়ার্কফ্লোর ডেটাবেস রিড ও রাইটগুলো BEGIN থেকে COMMIT বা ROLLBACK পর্যন্ত একই ডেটাবেস কানেকশনে রাখুন। আলাদা কানেকশন আলাদা ট্রানজ্যাকশন তৈরি করে।

ট্রানজ্যাকশনের ভেতরে কি পেমেন্ট বা ইমেইল API কল করা উচিত?

না। ট্রানজ্যাকশন ডেটাবেসের পরিবর্তনগুলো একসঙ্গে রাখে, কিন্তু কার্ড চার্জ, ইমেইল বা তৃতীয় পক্ষের API কল ফিরিয়ে নেয় না। আগে আপনার ডেটাবেস স্টেট কমিট করুন, একটি আউটবক্স ইভেন্ট বা পেন্ডিং স্ট্যাটাস রেকর্ড করুন, তারপর বাহ্যিক কাজটি আলাদাভাবে চালান।

দুইজন ব্যবহারকারীকে শেষ পণ্যটি কেনা থেকে কীভাবে আটকাব?

শেয়ার করা কোনো মান, যেমন উপলব্ধ ইনভেন্টরি, যাচাই ও আপডেট করার আগে সারিটি লক করুন। উদাহরণ হিসেবে, FOR UPDATE দিয়ে ইনভেন্টরি সারিটি সিলেক্ট করুন, স্টক যাচাই করুন, তা কমান এবং কমিটের আগে অর্ডার তৈরি করুন।

রিট্রাই কীভাবে ডুপ্লিকেট অর্ডার এড়াতে পারে?

প্রতিটি রিকোয়েস্টকে একটি স্থিতিশীল আইডেমপোটেন্সি কী দিন এবং একটি ইউনিক কনস্ট্রেইন্ট দিয়ে তা প্রয়োগ করুন। রিট্রাই সেই কনস্ট্রেইন্টে আঘাত করলে আরেকটি অর্ডার বা পেমেন্ট রেকর্ড ইনসার্ট না করে বিদ্যমান ফলাফল লোড করে ফেরত দিন।

রিট্রাইকে কেন নতুন ট্রানজ্যাকশন শুরু করতে হয়?

নতুন একটি ট্রানজ্যাকশনে শুরু থেকে আবার চেষ্টা করুন। কোনো স্টেটমেন্টে ত্রুটি হলে Postgres বর্তমান ট্রানজ্যাকশনকে বাতিল হিসেবে চিহ্নিত করে, তাই রোলব্যাক না করা পর্যন্ত সেটি নিরাপদে চালিয়ে যাওয়া যায় না।

কোন ডেটাবেস কনস্ট্রেইন্টগুলো অসামঞ্জস্যপূর্ণ ডেটা ঠেকাতে সাহায্য করে?

বৈধ রেফারেন্সের জন্য ফরেন কী, প্রয়োজনীয় ফিল্ডের জন্য NOT NULL, বৈধ মানের জন্য CHECK কনস্ট্রেইন্ট এবং যে রেকর্ডগুলো একবারই থাকা উচিত তাদের জন্য ইউনিক কনস্ট্রেইন্ট ব্যবহার করুন। অ্যাপ্লিকেশন কোডে বাগ থাকলেও এই নিয়মগুলো ডেটা রক্ষা করে।

ট্রানজ্যাকশন ছোট রাখা উচিত কেন?

ট্রানজ্যাকশনকে দ্রুত ডেটাবেসের কাজেই সীমিত রাখুন। দীর্ঘ ট্রানজ্যাকশন লক ধরে রাখে, অপেক্ষার সময় বাড়ায় এবং ডেডলক বা টাইমআউট ঘটাতে পারে, বিশেষ করে নেটওয়ার্ক কলের জন্য অপেক্ষা করলে।

Related posts