در قسمت اول با هم درباره Transaction در Redis و نحوه کار MULTI و EXEC صحبت کردیم. امروز میخواهیم قدم بعدی را برداریم و دو مفهوم خیلی مهمتر را بررسی کنیم که توی دنیای واقعی کاربرد زیادی دارند.

تصور کنید یک فروشگاه آنلاین دارید. آخرین محصول محبوب شما (مثلاً آیفون جدید) فقط یک عدد در انبار مونده. حالا دو کاربر دقیقاً همزمان دکمه «خرید» رو میزنن. چی اتفاق میافته؟
بدون مدیریت درست، ممکنه هر دو نفر بتونن محصول رو بخرن! یعنی شما یک محصول رو به دو نفر فروختید. این یعنی سردرگمی، شکایت مشتری، و مشکلات بزرگ.
این مشکل رو Race Condition میگیم - وقتی چند نفر همزمان میخوان یک چیز رو تغییر بدن و نتیجهش پیشبینیپذیر نیست.
خب، توی قسمت قبل یاد گرفتیم که MULTI و EXEC چطوری چند دستور رو اتمیک اجرا میکنن. اما یک مشکل داره:
شما نمیتونید قبل از شروع Transaction تصمیم بگیرید!
مثلاً باید اول موجودی رو چک کنید، ببینید کافیه یا نه، بعد تصمیم بگیرید که آیا بخرید یا نه. اما بین زمانی که موجودی رو چک میکنید تا زمانی که Transaction رو شروع میکنید، ممکنه کاربر دیگهای محصول رو خریده باشه!
اینجاست که WATCH وارد میدون میشه.
WATCH مثل یک نگهبان عمل میکنه. بهش میگید: "این کلید رو زیر نظر بگیر. اگه کسی دست زد بهش، من رو خبر کن!"
اسم فانتزیش اینه: قفل خوشبینانه (Optimistic Locking).
چرا خوشبینانه؟ چون فرض میکنیم که احتمال تضاد کمه. یعنی نمیریم از اول کلید رو قفل کنیم (Pessimistic Locking)، بلکه میگیم "احتمالاً کسی دیگه الان داره با این کار نمیکنه، ولی اگه کار کرد، ما میفهمیم."
بیایید داستانی واقعیتر تعریف کنم:
کاربر الف و کاربر ب هر دو میخوان آخرین محصول رو بخرن.
بدون WATCH:
کاربر الف موجودی رو میخونه → ۱ عدد ✓
کاربر ب موجودی رو میخونه → ۱ عدد ✓
کاربر الف موجودی رو کم میکنه → ۰ عدد
کاربر ب موجودی رو کم میکنه → ۱- عدد
نتیجه؟ موجودی منفی شده! فاجعه!
با WATCH:
کاربر الف میگه: "WATCH این محصول رو"
کاربر ب میگه: "WATCH این محصول رو"
کاربر الف موجودی رو میخونه → ۱ عدد
کاربر ب موجودی رو میخونه → ۱ عدد
کاربر الف Transaction رو شروع میکنه (MULTI)
کاربر الف موجودی رو ۱ واحد کم میکنه
کاربر الف Transaction رو تموم میکنه (EXEC) → موفق! ✓
کاربر ب Transaction رو شروع میکنه (MULTI)
کاربر ب موجودی رو ۱ واحد کم میکنه
کاربر ب Transaction رو تموم میکنه (EXEC) → شکست!
کاربر ب متوجه میشه که بین زمان WATCH و EXEC، داده تغییر کرده. Redis بهش میگه: "ببخشید، کسی دیگه جلوتر از تو رفته. دوباره امتحان کن."
این همون چیزیه که ما میخواستیم! سیستم خودش مشکل رو تشخیص داده و جلوی فروش بیش از حد رو گرفته.
در پروژهای که گذاشتم روی گیتهاب، یک سیستم مدیریت موجودی ساختم. وقتی کاربر میخواد محصول بخره، این مراحل رو طی میکنیم:
WATCH کردن کلیدهای مهم - محصول و موجودی کاربر رو زیر نظر میگیریم
خوندن وضعیت فعلی - موجودی الان چقدره؟
بررسی شرایط - آیا موجودی کافیه؟
محاسبات - اگه کافی بود، محاسبه میکنیم موجودی جدید چقدر میشه
اجرای اتمیک - با MULTI/EXEC همه چیز رو یکجا اعمال میکنیم
اگه بین مرحله ۲ و ۵ کسی داده رو تغییر بده، Redis Transaction رو لغو میکنه و ما دوباره امتحان میکنیم.
گاهی اوقات وسط کار متوجه میشیم که نیازی به ادامه Transaction نیست. مثلاً موجودی صفره و کاربر نمیتونه بخره.
در این حالت، باید UNWATCH رو صدا بزنیم تا Redis بدونه دیگه نیازی به نظارت روی اون کلیدها نداریم. این کار باعث میشه منابع آزاد بشن و عملکرد بهتر بشه.
حالا بیایم سراغ موضوع دوم: Pipeline.
فرض کنید میخواید ۱۰۰ تا محصول رو به Redis اضافه کنید. هر بار یک دستور SET میفرستید.
هر دستور Redis این مسیر رو طی میکنه:
برنامه شما دستور رو میفرسته به Redis
Redis دستور رو اجرا میکنه
Redis جواب رو برمیگردونه به برنامه شما
اگه هر بار این رفتوبرگشت ۱ میلیثانیه طول بکشه، برای ۱۰۰ تا دستور چقدر زمان میبره؟
۱۰۰ میلیثانیه!
حالا اگه بگم میتونید همین کار رو توی ۱ میلیثانیه انجام بدید، چی؟
Pipeline یعنی "لوله". تصور کنید یک لوله دارید که همه دستوراتتون رو توش میریزید و یکجا میفرستید به Redis. بعدش Redis هم یکجا همه رو اجرا میکنه و جوابها رو برمیگردونه.
نتیجه؟ به جای ۱۰۰ بار رفتوبرگشت، فقط یک بار رفتوبرگشت داریم!
یک سوال مهم: آیا Pipeline همون Transaction هست؟
جواب: نه!
Pipeline دو حالت داره:
وقتی Pipeline رو بدون Transaction استفاده میکنید، دستورات شما سریع اجرا میشن ولی اتمیک نیستن.
یعنی اگه وسط کار یکی از دستورات خطا بده، بقیه دستورات همچنان اجرا میشن.
این حالت برای کارهایی مثل Seeding عالیه - وقتی میخواید انبوه داده رو یکجا بریزید توی Redis.
در پروژه من، وقتی اولین بار برنامه رو اجرا میکنید، یک سری دادههای اولیه (محصولات، کاربران) رو به Redis اضافه میکنم. اگه این کار رو بدون Pipeline بکنم، خیلی کنده میشه. ولی با Pipeline، همه چی یکجا و سریع لود میشه.
وقتی Pipeline رو با Transaction ترکیب میکنید، هم سرعت Pipeline رو دارید هم ایمنی Transaction رو.
یعنی دستورات شما سریع اجرا میشن و اگه مشکلی پیش بیاد، همه لغو میشن.
این حالت برای کارهایی مثل خرید محصول عالیه - میخواید سریع باشه و همزمان ایمن.
در پروژه من، یک تابع seed_products دارم که محصولات اولیه رو به Redis اضافه میکنه.
اگه بدون Pipeline بود:
برای هر محصول یک بار رفتوبرگشت → کند!
زمان کل: حدود ۱۵۰ میلیثانیه
با Pipeline:
همه محصولات یکجا → سریع!
زمان کل: حدود ۵ میلیثانیه
۳۰ برابر سریعتر!
حالا وقتشه که این دو رو باهم ترکیب کنیم.
تصور کنید میخواید یک سیستم سبد خرید بسازید:
کاربر چند تا محصول رو به سبدش اضافه میکنه
وقتی روی "تسویه حساب" میزنه، باید همه محصولات رو یکجا بخره
این کار پیچیدگیهاش زیاده:
باید همه محصولات رو چک کنید (موجودی کافیه؟)
باید همه تغییرات رو یکجا اعمال کنید (اتمیک باشه)
باید سریع باشه (کاربر منتظر نمونه)
راهحل:
با WATCH همه محصولات سبد رو زیر نظر بگیرید
موجودی همه رو چک کنید
اگه همه چی اوکی بود، با Pipeline + Transaction همه رو یکجا خرید کنید
اگه بین چک کردن و خرید، کسی یکی از محصولات رو خریده باشه، Transaction شکست میخوره و کاربر متوجه میشه.
بیایید نگاهی به اعداد واقعی بندازیم.
بدون Pipeline:
زمان: ۱۵۰ میلیثانیه
تعداد رفتوبرگشت شبکه: ۱۰۰ بار
با Pipeline:
زمان: ۵ میلیثانیه
تعداد رفتوبرگشت شبکه: ۱ بار
بهبود: ۳۰ برابر سریعتر!
بدون WATCH:
۲ کاربر همزمان خرید میکنن
موجودی منفی میشه
فروش بیش از حد اتفاق میافته
با WATCH:
۲ کاربر همزمان خرید میکنن
یکی موفق میشه ✓
دومی شکست میخوره و دوباره امتحان میکنه ✓
موجودی همیشه درسته ✓
در پروژه، چند تست Integration نوشتم که تضمین میکنه همه چی درست کار میکنه:
یک کاربر یک محصول میخره. بررسی میکنیم که:
موجودی محصول کم شده؟ ✓
موجودی کاربر زیاد شده؟ ✓
موجودی رو روی ۲ تا میذاریم. سه بار میخواییم بخریم. نتیجه:
دو بار اول موفق ✓
بار سوم شکست میخوره ✓
موجودی صفر میشه (نه منفی!) ✓
دو کاربر مختلف محصول میخرن. بررسی میکنیم:
هر کاربر موجودی جداگانه داره ✓
موجودی کل محصول درست کم شده ✓
همه این تستها رو میتونید خودتون اجرا کنید و ببینید چطوری کار میکنن.
در این قسمت یاد گرفتیم که:
WATCH چطوری با "قفل خوشبینانه" از Race Condition جلوگیری میکنه
Pipeline چطوری با کاهش رفتوبرگشت شبکه، عملکرد رو تا ۱۰۰ برابر بهبود میده
چطوری این دو رو با هم ترکیب کنیم برای سیستمهای حرفهای
چه موقع از کدوم استفاده کنیم و چه موقع نه
این مفاهیم توی پروژههای واقعی خیلی کاربردی هستن. از e-commerce گرفته تا سیستمهای مالی، همه جا بهشون احتیاج دارید.
کل کد پروژه رو میتونید از گیتهاب من دانلود کنید:
git clone https://github.com/moeinkolivand/redis-lock-inventory docker-compose up -d docker compose run --rm app pytest tests.py -v
همه تستها رو اجرا کنید و ببینید چطور کار میکنن
در قسمت بعدی میریم سراغ موضوعات پیشرفتهتر مثل Distributed Locks , انواع Lock در ردیس.