حالت تاریک
فصل ۱۲ — کیف پول دیجیتال (Digital Wallet)
خلاصهٔ تحلیلی — این متن چکیدهٔ مفاهیم فصل به زبان فارسی است، نه ترجمهٔ واژهبهواژه.
گام ۱ — نیازمندیها و برآورد
طراحی بکاند کیف پول با تمرکز فقط بر انتقال موجودی بین دو کیف. نیازمندیها: ۱ میلیون TPS، دسترسپذیری ۹۹٫۹۹٪، تضمین تراکنشی و — مهمتر از همه — reproducibility: بتوان موجودی تاریخی هر لحظه را با بازپخش دادهها از ابتدا بازسازی کرد (reconciliation فقط مغایرت را نشان میدهد، نه علت را).
برآورد: هر انتقال دو عملیات (کسر + واریز) است ← ۲ میلیون TPS واقعی. با ~۱۰۰۰ TPS بهازای هر نود دیتابیس، ~۲۰۰۰ نود لازم است؛ پس یک هدف طراحی، بالا بردن TPS هر نود برای کاهش هزینهٔ سختافزار است.
گام ۲ — سه طرح تکاملی
تنها API: POST /v1/wallet/balance_transfer با from/to، amount (باز هم string، نه double)، currency و transaction_id (کلید حذف تکرار).
طرح ۱: شاردینگ در حافظه (Redis)
نگاشت «حساب←موجودی» بین چند نود Redis با hash(account) % n پخش میشود؛ اطلاعات پارتیشن در ZooKeeper؛ wallet service بیحالت دستور را به دو نود مربوط میفرستد. ایراد قاطع: دو بهروزرسانی روی دو نود، اتمیک نیستند — اگر سرویس بین دو بهروزرسانی سقوط کند، انتقال ناقص میماند.
طرح ۲: تراکنش توزیعشده روی دیتابیسهای رابطهای
Redis با دیتابیسهای تراکنشی جایگزین میشود؛ اما انتقال بین دو شارد هنوز نیازمند تراکنش توزیعشده است:
- 2PC (دو-فازی): راهحل سطح-پایین متکی به خود دیتابیس (استاندارد X/Open XA). فاز prepare قفلها را تا رسیدن پاسخ همهٔ نودها نگه میدارد ← کارایی ضعیف؛ coordinator هم نقطهٔ شکست واحد است.
- TC/C (Try-Confirm/Cancel): تراکنش جبرانی سطح-برنامه. Try: کسر از A (تراکنش محلی کامل)؛ Confirm: واریز به C؛ Cancel: برگرداندن پول به A اگر Try شکست. برخلاف 2PC که فازهایش درون یک تراکنشاند، هر فاز TC/C تراکنش محلی مستقلی است — فاز دوم 2PC «تکمیل تراکنش ناتمام» است، فاز دوم TC/C «خنثیسازی تراکنش کاملشده (undo)». مزیت: مستقل از نوع دیتابیس؛ هزینه: پیچیدگی در لایهٔ برنامه.
- جدول phase status (در دیتابیسِ حساب مبدأ) پیشرفت TC/C را ذخیره میکند تا پس از restart سرویس، ادامهٔ کار مشخص باشد.
- حالت نامتوازن: پایان فاز Try، ۱ دلار «گم» است (A−۱ و C دستنخورده) — نقض موقت اصل حسابداری که چون برنامهمحور است، برنامه آن را میبیند و مدیریت میکند.
- ترتیب معتبر عملیات: فقط «اول کسر، بعد واریز» درست است. واریز زودهنگام به C خطر برداشتهشدن پول توسط دیگری قبل از Cancel را دارد؛ اجرای موازی کسر/واریز هم پیچیدگی خطا میسازد.
- اجرای out-of-order: ممکن است Cancel قبل از Try به نودی برسد ← نود پرچم out-of-order ثبت میکند و Try بعدی با دیدن پرچم، شکست برمیگرداند.
- Saga: استاندارد de-facto در microservice ها. عملیات بهترتیب خطی اجرا میشوند؛ در شکست، از همان نقطه به عقب با تراکنشهای جبرانی rollback میشود (برای n عملیات، 2n عملیات آماده کنید). هماهنگی یا choreography (رویدادمحورِ غیرمتمرکز — با زیاد شدن سرویسها مدیریت state machine داخلی سخت میشود) یا orchestration (هماهنگکنندهٔ مرکزی — انتخاب معمول کیف پول). مقایسه: TC/C امکان اجرای موازی دارد و برای سیستمهای حساس به تأخیر با عملیات زیاد بهتر است؛ Saga خطی اما همراستا با معماری microservice.
طرح ۳: Event sourcing (پاسخ به auditor ها)
تراکنش توزیعشده کار میکند اما ردگیری علت تغییر موجودی و ممیزی را نمیدهد. Event sourcing (از DDD) با چهار مفهوم:
- Command: نیت بیرونی (درخواست انتقال) — در صف FIFO (مثل Kafka).
- Event: نتیجهٔ اعتبارسنجی command؛ واقعیت قطعی و deterministic (زمان گذشته: «منتقل شد»). یک command میتواند صفر تا چند event بسازد و تولید event میتواند تصادفی/وابسته به I/O باشد — اما خود event ها قطعیاند. ترتیب event ها تابع ترتیب command هاست.
- State: موجودی همهٔ حسابها (نگاشت حساب←موجودی).
- State machine: اعتبارسنجی command ← تولید event، و اعمال event ← بهروزرسانی state. باید قطعی باشد (بدون تصادف و I/O بیرونی).
Reproducibility مزیت محوری است: همهٔ تغییرات ابتدا بهصورت تاریخچهٔ تغییرناپذیر ذخیره میشوند و دیتابیس فقط «نمای» موجودی است. با بازپخش event ها از ابتدا: موجودی هر لحظهٔ تاریخی به دست میآید، درستی موجودی فعلی اثبات میشود، و درستی کد جدید با اجرای دو نسخه روی همان event ها مقایسه میشود — پاسخ هر سه سؤال auditor.
CQRS: بهجای انتشار state، خود event ها منتشر میشوند؛ یک state machine نویسنده و چندین state machine فقط-خواندنی که نماهای دلخواه (موجودی برای کوئری کلاینت، نمای بازهٔ زمانی برای بررسی double charge و غیره) میسازند. نماهای خواندنی عقبترند اما میرسند — eventually consistent.
گام ۳ — بررسی عمیق
Event sourcing پرکارایی (محلیسازی)
- Command/event روی دیسک محلی بهجای Kafka: append فقط-افزودنی = نوشتن ترتیبی بسیار سریع (حتی روی HDD)؛ کش اخیرها در حافظه. تکنیک mmap هر دو را یکجا میدهد (فایل دیسک بهصورت آرایهٔ حافظهای).
- State محلی: SQLite (رابطهای فایلمحور) یا RocksDB (key-value مبتنی بر LSM tree، بهینه برای نوشتن).
- Snapshot: ذخیرهٔ دورهای state (مثلاً ساعت ۰۰:۰۰ برای تیم مالی) در object storage؛ بازپخش از snapshot ادامه مییابد نه از ابتدا.
قابلیت اطمینان با اجماع
تحلیل: computation از روی داده بازیابی میشود؛ پس فقط دوام داده مهم است. state و snapshot از event بازتولید میشوند؛ command هم کافی نیست چون تولید event غیرقطعی است ← فقط event list نیازمند تضمین اطمینان بالاست. راهحل: replication مبتنی بر اجماع (Raft) — تا وقتی اکثریت نودها زندهاند (۳ از ۵)، event list ها یکسان و بدون از دست رفتن داده و با حفظ ترتیباند. leader دستورها را میگیرد، event میسازد و replicate میکند؛ همهٔ نودها event ها را اعمال میکنند. سقوط leader ← انتخاب خودکار leader جدید (کلاینت command پاسخنگرفته را دوباره میفرستد)؛ سقوط follower ← retry تا بازگشت.
Event sourcing توزیعشده
دو محدودیت باقی میماند: پاسخ CQRS به کلاینت کند است (polling) و ظرفیت یک گروه Raft محدود است.
- Pull به Push: بهجای polling دورهای، reverse proxy بین کاربر و نودها مینشیند و state machine فقط-خواندنی بهمحض دریافت event، وضعیت را به proxy push میکند — پاسخ شبهreal-time و اجرای همزماننما.
- شاردینگ + تراکنش توزیعشده: داده بین چند گروه Raft پارتیشن میشود و TC/C یا Saga روی آنها اجرا میشود. جریان happy path با Saga: coordinator رکورد phase status میسازد ← A:−$1 به پارتیشن ۱ (leader ← command list ← event ← اجماع Raft ← اجرا ← مسیر خواندن CQRS وضعیت را push میکند) ← ثبت موفقیت ← C:+$1 به پارتیشن ۲ با همان چرخه ← تکمیل تراکنش و پاسخ به کاربر.
نکات کلیدی فصل
- مسیر تکامل پاسخ را حفظ کنید: Redis (سریع اما غیراتمیک) ← دیتابیس + 2PC/TC/C/Saga (اتمیک اما غیرقابلممیزی) ← event sourcing (قابلبازتولید) ← فایلمحور + Raft + CQRS + شاردینگ (سریع، مطمئن، مقیاسپذیر).
- در TC/C فقط «اول کسر، بعد واریز» ترتیب معتبر است و پرچم out-of-order را فراموش نکنید.
- تنها دادهٔ نیازمند دوام سختگیرانه event list است — بقیه بازتولیدشدنیاند.
- قطعی بودن state machine شرط لازم reproducibility است؛ تصادف و I/O فقط در مرحلهٔ تولید event مجاز است.
- نوشتن ترتیبی روی دیسک محلی (mmap، LSM) از دیتابیس/صف شبکهای بسیار سریعتر است — کلید رسیدن به TPS بالای هر نود.
یادداشتهای مترجم
- event sourcing، CQRS، snapshot، 2PC، TC/C، Saga و Raft لاتین حفظ شدهاند.