ویرگول
ورودثبت نام
معین کولیوند
معین کولیوند
معین کولیوند
معین کولیوند
خواندن ۳ دقیقه·۲ ماه پیش

وقتی تراکنش‌ها هم‌زمان می‌شوند: مقابله با Race Condition و Double Spending

در یک سرویس انتقال پول ، وقتی دو درخواست برداشت از یک حساب تقریباً همزمان میرسن، هر دو درخواست دقیقاً در یک لحظه موجودی رو می‌خونن. هر دو می‌بینن که موجودی کافیه، پس هر دو تراکنش تایید و با موفقیت اجرا میشن. نتیجه این میشه که بیشتر از پول موجود برداشت میشه: موجودی واقعی منفی میشه، اما دیتابیس هنوز یک مقدار مثبت نشون میده.

۲. مشکل

این دقیقاً همان Race Condition و Double Spending بود. سیستم هیچ مکانیزمی برای جلوگیری از تراکنش‌های هم‌زمان روی یک حساب نداشت و عملیات مالی به صورت اتمیک اجرا نمی‌شد.

۳. چرا این مشکل رخ می‌دهد؟

عملیات انتقال پول شامل سه مرحله است:

  1. خواندن موجودی فعلی

  2. محاسبه موجودی جدید

  3. نوشتن موجودی جدید

وقتی دو thread یا process هم‌زمان این سه مرحله را اجرا کنند، Race Condition ایجاد می‌شود. به عبارتی، نیاز داریم مکانیزمی که بگوید: فقط یکی از شما می‌تواند در یک زمان روی این حساب کار کند.

۴. راه‌حل: Distributed Lock با Redis

برای جلوگیری از مشکل، از Distributed Lock استفاده می‌کنیم.

نحوه کار:

  • قبل از عملیات روی حساب، سعی می‌کنیم یک قفل روی آن حساب بگیریم.

  • اگر قفل گرفته شد، عملیات اجرا می‌شود و بعد قفل آزاد می‌شود.

  • اگر نتوانستیم قفل بگیریم، باید صبر کنیم تا process دیگری کارش تمام شود.

دستور Redis برای قفل:

SET key value NX PX milliseconds
  • NX: فقط اگر key وجود ندارد، تنظیم شود.

  • PX milliseconds: مدت زمان انقضای قفل؛ اگر process کرش کند، قفل خودکار آزاد می‌شود.

سناریو درخواست و نحوه بررسی:

  • درخواست یک تلاش می‌کند قفل حساب را بگیرد و موفق می‌شود.

  • درخواست دو تلاش می‌کند قفل را بگیرد، اما به دلیل اینکه قفل در اختیار موبایل است، ناموفق می‌ماند و منتظر می‌ماند.

  • درخواست یک حساب را می‌خواند ( 1000 دلار) و بررسی می‌کند که برداشت ۶۰۰ دلار امکان‌پذیر است . مبلغ ۶۰۰ دلار از موجودی کم می‌شود و موجودی جدید ۴۰۰ دلار ثبت می‌شود .

  • درخواست یک قفل را آزاد می‌کند .

  • در این نقطه، درخواست دوم می‌تواند دوباره تلاش کند و درخواست دوم موفق می‌شود قفل را بگیرد.

  • موجودی را می‌خواند ( 400 دلار) و بررسی می‌کند که آیا می‌تواند ۷۰۰ دلار برداشت کند . چون موجودی کافی نیست، تراکنش رد می‌شود. در نهایت، قفل توسط درخواست دوم رها می‌شود.

۵. نکات مهم در پیاده‌سازی

  1. استفاده از Token منحصر به فرد: برای آزادسازی امن قفل، معمولاً از UUID استفاده می‌کنیم.

  2. تعیین TTL مناسب: طول قفل باید به اندازه کافی باشد تا عملیات تمام شود، اما نه طولانی که سیستم قفل بماند.

  3. Retry با Exponential Backoff: اگر قفل در دسترس نبود، با تأخیر و افزایش تدریجی دوباره تلاش می‌کنیم.

  4. جلوگیری از Deadlock: هنگام گرفتن چند قفل، همیشه ترتیب مشخص داشته باشیم.

  5. گارانتی Idempotency: هر عملیات یک شناسه یکتا داشته باشد تا درخواست‌های تکراری دوباره اجرا نشوند.

  6. Atomic Operations در Redis: برای update موجودی، می‌توان از WATCH/MULTI/EXEC یا Lua script استفاده کرد.

۶. نتیجه‌گیری

Race Condition و Double Spending یکی از چالش‌های اصلی در عملیات مالی است. استفاده از Redis Distributed Lock یک راه‌حل ساده و قدرتمند است که تراکنش‌ها را ایمن می‌کند و هم‌زمانی را کنترل می‌کند.

۷. کد کامل پروژه

پیاده‌سازی کامل سیستم، شامل تست‌های race condition و سناریوهای مختلف، در GitHub موجود است:
https://github.com/your-username/distributed-wallet-redis-lock

در این repository می‌توانید ببینید:

  • پیاده‌سازی کامل Redis Lock

  • سیستم کیف پول با Kafka و FastStream

  • تست‌های مختلف race condition

  • سناریوهای Deadlock و Idempotency

مطالب مرتبط

برای درک بهتر مفاهیم Redis و مدیریت تراکنش‌ها، می‌توانید دو پست قبلی من را هم مطالعه کنید:

  • داستان Watch و Pipeline در Redis

  • Transaction در Redis

redis
۰
۰
معین کولیوند
معین کولیوند
شاید از این پست‌ها خوشتان بیاید