
در یک سرویس انتقال پول ، وقتی دو درخواست برداشت از یک حساب تقریباً همزمان میرسن، هر دو درخواست دقیقاً در یک لحظه موجودی رو میخونن. هر دو میبینن که موجودی کافیه، پس هر دو تراکنش تایید و با موفقیت اجرا میشن. نتیجه این میشه که بیشتر از پول موجود برداشت میشه: موجودی واقعی منفی میشه، اما دیتابیس هنوز یک مقدار مثبت نشون میده.
این دقیقاً همان Race Condition و Double Spending بود. سیستم هیچ مکانیزمی برای جلوگیری از تراکنشهای همزمان روی یک حساب نداشت و عملیات مالی به صورت اتمیک اجرا نمیشد.
عملیات انتقال پول شامل سه مرحله است:
خواندن موجودی فعلی
محاسبه موجودی جدید
نوشتن موجودی جدید
وقتی دو thread یا process همزمان این سه مرحله را اجرا کنند، Race Condition ایجاد میشود. به عبارتی، نیاز داریم مکانیزمی که بگوید: فقط یکی از شما میتواند در یک زمان روی این حساب کار کند.
برای جلوگیری از مشکل، از Distributed Lock استفاده میکنیم.
قبل از عملیات روی حساب، سعی میکنیم یک قفل روی آن حساب بگیریم.
اگر قفل گرفته شد، عملیات اجرا میشود و بعد قفل آزاد میشود.
اگر نتوانستیم قفل بگیریم، باید صبر کنیم تا process دیگری کارش تمام شود.
SET key value NX PX milliseconds
NX: فقط اگر key وجود ندارد، تنظیم شود.
PX milliseconds: مدت زمان انقضای قفل؛ اگر process کرش کند، قفل خودکار آزاد میشود.
درخواست یک تلاش میکند قفل حساب را بگیرد و موفق میشود.
درخواست دو تلاش میکند قفل را بگیرد، اما به دلیل اینکه قفل در اختیار موبایل است، ناموفق میماند و منتظر میماند.
درخواست یک حساب را میخواند ( 1000 دلار) و بررسی میکند که برداشت ۶۰۰ دلار امکانپذیر است . مبلغ ۶۰۰ دلار از موجودی کم میشود و موجودی جدید ۴۰۰ دلار ثبت میشود .
درخواست یک قفل را آزاد میکند .
در این نقطه، درخواست دوم میتواند دوباره تلاش کند و درخواست دوم موفق میشود قفل را بگیرد.
موجودی را میخواند ( 400 دلار) و بررسی میکند که آیا میتواند ۷۰۰ دلار برداشت کند . چون موجودی کافی نیست، تراکنش رد میشود. در نهایت، قفل توسط درخواست دوم رها میشود.
استفاده از Token منحصر به فرد: برای آزادسازی امن قفل، معمولاً از UUID استفاده میکنیم.
تعیین TTL مناسب: طول قفل باید به اندازه کافی باشد تا عملیات تمام شود، اما نه طولانی که سیستم قفل بماند.
Retry با Exponential Backoff: اگر قفل در دسترس نبود، با تأخیر و افزایش تدریجی دوباره تلاش میکنیم.
جلوگیری از Deadlock: هنگام گرفتن چند قفل، همیشه ترتیب مشخص داشته باشیم.
گارانتی Idempotency: هر عملیات یک شناسه یکتا داشته باشد تا درخواستهای تکراری دوباره اجرا نشوند.
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 و مدیریت تراکنشها، میتوانید دو پست قبلی من را هم مطالعه کنید: