ویرگول
ورودثبت نام
معین کولیوند
معین کولیوند
معین کولیوند
معین کولیوند
خواندن ۶ دقیقه·۱۵ روز پیش

داستان WATCH و Pipeline در Redis

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

یک مشکل واقعی: فروش بیش از حد

تصور کنید یک فروشگاه آنلاین دارید. آخرین محصول محبوب شما (مثلاً آیفون جدید) فقط یک عدد در انبار مونده. حالا دو کاربر دقیقاً همزمان دکمه «خرید» رو می‌زنن. چی اتفاق می‌افته؟

بدون مدیریت درست، ممکنه هر دو نفر بتونن محصول رو بخرن! یعنی شما یک محصول رو به دو نفر فروختید. این یعنی سردرگمی، شکایت مشتری، و مشکلات بزرگ.

این مشکل رو Race Condition می‌گیم - وقتی چند نفر همزمان می‌خوان یک چیز رو تغییر بدن و نتیجه‌ش پیش‌بینی‌پذیر نیست.

چرا MULTI/EXEC کافی نیست؟

خب، توی قسمت قبل یاد گرفتیم که MULTI و EXEC چطوری چند دستور رو اتمیک اجرا می‌کنن. اما یک مشکل داره:

شما نمی‌تونید قبل از شروع Transaction تصمیم بگیرید!

مثلاً باید اول موجودی رو چک کنید، ببینید کافیه یا نه، بعد تصمیم بگیرید که آیا بخرید یا نه. اما بین زمانی که موجودی رو چک می‌کنید تا زمانی که Transaction رو شروع می‌کنید، ممکنه کاربر دیگه‌ای محصول رو خریده باشه!

اینجاست که WATCH وارد میدون میشه.


WATCH: نگهبان داده‌های شما

WATCH مثل یک نگهبان عمل می‌کنه. بهش می‌گید: "این کلید رو زیر نظر بگیر. اگه کسی دست زد بهش، من رو خبر کن!"

مفهوم Optimistic Locking

اسم فانتزیش اینه: قفل خوش‌بینانه (Optimistic Locking).

چرا خوش‌بینانه؟ چون فرض می‌کنیم که احتمال تضاد کمه. یعنی نمی‌ریم از اول کلید رو قفل کنیم (Pessimistic Locking)، بلکه می‌گیم "احتمالاً کسی دیگه الان داره با این کار نمی‌کنه، ولی اگه کار کرد، ما می‌فهمیم."

داستان دو مشتری

بیایید داستانی واقعی‌تر تعریف کنم:

کاربر الف و کاربر ب هر دو می‌خوان آخرین محصول رو بخرن.

بدون WATCH:

  1. کاربر الف موجودی رو می‌خونه → ۱ عدد ✓

  2. کاربر ب موجودی رو می‌خونه → ۱ عدد ✓

  3. کاربر الف موجودی رو کم می‌کنه → ۰ عدد

  4. کاربر ب موجودی رو کم می‌کنه → ۱- عدد

نتیجه؟ موجودی منفی شده! فاجعه!

با WATCH:

  1. کاربر الف می‌گه: "WATCH این محصول رو"

  2. کاربر ب می‌گه: "WATCH این محصول رو"

  3. کاربر الف موجودی رو می‌خونه → ۱ عدد

  4. کاربر ب موجودی رو می‌خونه → ۱ عدد

  5. کاربر الف Transaction رو شروع می‌کنه (MULTI)

  6. کاربر الف موجودی رو ۱ واحد کم می‌کنه

  7. کاربر الف Transaction رو تموم می‌کنه (EXEC) → موفق! ✓

  8. کاربر ب Transaction رو شروع می‌کنه (MULTI)

  9. کاربر ب موجودی رو ۱ واحد کم می‌کنه

  10. کاربر ب Transaction رو تموم می‌کنه (EXEC) → شکست!

کاربر ب متوجه میشه که بین زمان WATCH و EXEC، داده تغییر کرده. Redis بهش می‌گه: "ببخشید، کسی دیگه جلوتر از تو رفته. دوباره امتحان کن."

این همون چیزیه که ما می‌خواستیم! سیستم خودش مشکل رو تشخیص داده و جلوی فروش بیش از حد رو گرفته.

چطوری توی پروژه استفاده می‌کنیم؟

در پروژه‌ای که گذاشتم روی گیت‌هاب، یک سیستم مدیریت موجودی ساختم. وقتی کاربر می‌خواد محصول بخره، این مراحل رو طی می‌کنیم:

  1. WATCH کردن کلیدهای مهم - محصول و موجودی کاربر رو زیر نظر می‌گیریم

  2. خوندن وضعیت فعلی - موجودی الان چقدره؟

  3. بررسی شرایط - آیا موجودی کافیه؟

  4. محاسبات - اگه کافی بود، محاسبه می‌کنیم موجودی جدید چقدر میشه

  5. اجرای اتمیک - با MULTI/EXEC همه چیز رو یکجا اعمال می‌کنیم

اگه بین مرحله ۲ و ۵ کسی داده رو تغییر بده، Redis Transaction رو لغو می‌کنه و ما دوباره امتحان می‌کنیم.

دستور UNWATCH:

گاهی اوقات وسط کار متوجه میشیم که نیازی به ادامه Transaction نیست. مثلاً موجودی صفره و کاربر نمی‌تونه بخره.

در این حالت، باید UNWATCH رو صدا بزنیم تا Redis بدونه دیگه نیازی به نظارت روی اون کلیدها نداریم. این کار باعث میشه منابع آزاد بشن و عملکرد بهتر بشه.


حالا بیایم سراغ موضوع دوم: Pipeline.

مشکل: شبکه کنده!

فرض کنید می‌خواید ۱۰۰ تا محصول رو به Redis اضافه کنید. هر بار یک دستور SET می‌فرستید.

هر دستور Redis این مسیر رو طی می‌کنه:

  1. برنامه شما دستور رو می‌فرسته به Redis

  2. Redis دستور رو اجرا می‌کنه

  3. Redis جواب رو برمی‌گردونه به برنامه شما

اگه هر بار این رفت‌وبرگشت ۱ میلی‌ثانیه طول بکشه، برای ۱۰۰ تا دستور چقدر زمان می‌بره؟

۱۰۰ میلی‌ثانیه!

حالا اگه بگم می‌تونید همین کار رو توی ۱ میلی‌ثانیه انجام بدید، چی؟

راه‌حل: Pipeline

Pipeline یعنی "لوله". تصور کنید یک لوله دارید که همه دستوراتتون رو توش می‌ریزید و یکجا می‌فرستید به Redis. بعدش Redis هم یکجا همه رو اجرا می‌کنه و جواب‌ها رو برمی‌گردونه.

نتیجه؟ به جای ۱۰۰ بار رفت‌وبرگشت، فقط یک بار رفت‌وبرگشت داریم!

تفاوت Pipeline با Transaction

یک سوال مهم: آیا Pipeline همون Transaction هست؟

جواب: نه!

Pipeline دو حالت داره:

1. Pipeline بدون Transaction

وقتی Pipeline رو بدون Transaction استفاده می‌کنید، دستورات شما سریع اجرا میشن ولی اتمیک نیستن.

یعنی اگه وسط کار یکی از دستورات خطا بده، بقیه دستورات همچنان اجرا میشن.

این حالت برای کارهایی مثل Seeding عالیه - وقتی می‌خواید انبوه داده رو یکجا بریزید توی Redis.

در پروژه من، وقتی اولین بار برنامه رو اجرا می‌کنید، یک سری داده‌های اولیه (محصولات، کاربران) رو به Redis اضافه می‌کنم. اگه این کار رو بدون Pipeline بکنم، خیلی کنده میشه. ولی با Pipeline، همه چی یکجا و سریع لود میشه.

2. Pipeline با Transaction

وقتی Pipeline رو با Transaction ترکیب می‌کنید، هم سرعت Pipeline رو دارید هم ایمنی Transaction رو.

یعنی دستورات شما سریع اجرا میشن و اگه مشکلی پیش بیاد، همه لغو میشن.

این حالت برای کارهایی مثل خرید محصول عالیه - می‌خواید سریع باشه و همزمان ایمن.

مثال واقعی از پروژه

در پروژه من، یک تابع seed_products دارم که محصولات اولیه رو به Redis اضافه می‌کنه.

اگه بدون Pipeline بود:

  • برای هر محصول یک بار رفت‌وبرگشت → کند!

  • زمان کل: حدود ۱۵۰ میلی‌ثانیه

با Pipeline:

  • همه محصولات یکجا → سریع!

  • زمان کل: حدود ۵ میلی‌ثانیه

۳۰ برابر سریع‌تر!


ترکیب WATCH و Pipeline:

حالا وقتشه که این دو رو باهم ترکیب کنیم.

تصور کنید می‌خواید یک سیستم سبد خرید بسازید:

  1. کاربر چند تا محصول رو به سبدش اضافه می‌کنه

  2. وقتی روی "تسویه حساب" می‌زنه، باید همه محصولات رو یکجا بخره

این کار پیچیدگی‌هاش زیاده:

  • باید همه محصولات رو چک کنید (موجودی کافیه؟)

  • باید همه تغییرات رو یکجا اعمال کنید (اتمیک باشه)

  • باید سریع باشه (کاربر منتظر نمونه)

راه‌حل:

  1. با WATCH همه محصولات سبد رو زیر نظر بگیرید

  2. موجودی همه رو چک کنید

  3. اگه همه چی اوکی بود، با Pipeline + Transaction همه رو یکجا خرید کنید

اگه بین چک کردن و خرید، کسی یکی از محصولات رو خریده باشه، Transaction شکست می‌خوره و کاربر متوجه میشه.


عملکرد: اعداد واقعی

بیایید نگاهی به اعداد واقعی بندازیم.

تست ۱: ذخیره ۱۰۰ محصول

بدون Pipeline:

  • زمان: ۱۵۰ میلی‌ثانیه

  • تعداد رفت‌وبرگشت شبکه: ۱۰۰ بار

با Pipeline:

  • زمان: ۵ میلی‌ثانیه

  • تعداد رفت‌وبرگشت شبکه: ۱ بار

  • بهبود: ۳۰ برابر سریع‌تر!

تست ۲: خرید همزمان (Race Condition)

بدون WATCH:

  • ۲ کاربر همزمان خرید می‌کنن

  • موجودی منفی میشه

  • فروش بیش از حد اتفاق می‌افته

با WATCH:

  • ۲ کاربر همزمان خرید می‌کنن

  • یکی موفق میشه ✓

  • دومی شکست می‌خوره و دوباره امتحان می‌کنه ✓

  • موجودی همیشه درسته ✓


تست‌های واقعی

در پروژه، چند تست Integration نوشتم که تضمین می‌کنه همه چی درست کار می‌کنه:

تست خرید موفق

یک کاربر یک محصول می‌خره. بررسی می‌کنیم که:

  • موجودی محصول کم شده؟ ✓

  • موجودی کاربر زیاد شده؟ ✓

تست جلوگیری از فروش بیش از حد

موجودی رو روی ۲ تا می‌ذاریم. سه بار می‌خواییم بخریم. نتیجه:

  • دو بار اول موفق ✓

  • بار سوم شکست می‌خوره ✓

  • موجودی صفر میشه (نه منفی!) ✓

تست کاربران مختلف

دو کاربر مختلف محصول می‌خرن. بررسی می‌کنیم:

  • هر کاربر موجودی جداگانه داره ✓

  • موجودی کل محصول درست کم شده ✓

همه این تست‌ها رو می‌تونید خودتون اجرا کنید و ببینید چطوری کار می‌کنن.


نتیجه‌گیری

در این قسمت یاد گرفتیم که:

  1. WATCH چطوری با "قفل خوش‌بینانه" از Race Condition جلوگیری می‌کنه

  2. Pipeline چطوری با کاهش رفت‌وبرگشت شبکه، عملکرد رو تا ۱۰۰ برابر بهبود می‌ده

  3. چطوری این دو رو با هم ترکیب کنیم برای سیستم‌های حرفه‌ای

  4. چه موقع از کدوم استفاده کنیم و چه موقع نه

این مفاهیم توی پروژه‌های واقعی خیلی کاربردی هستن. از 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 در ردیس.

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