سلام، من مهران هستم و در این مقاله قصد دارم به یکی از مباحث مهم در دیتابیس ردیس، یعنی مفهوم تراکنش (Transaction) و بهینهسازی عملکرد با استفاده از پایپلاین (Pipeline) بپردازم.
درک تفاوت این دو و نحوه استفاده صحیح از آنها در کتابخانه های کلاینت (مثل Redis-py در پایتون)، تاثیر مستقیمی روی عملکرد و سلامت دادههای شما دارد.
تراکنشها در ردیس (Redis Transactions) :
ردیس برای مدیریت تراکنشها ۵ دستور کلیدی دارد: MULTI, EXEC, DISCARD, WATCH, و UNWATCH.
هدف اصلی تراکنشها در ردیس، تضمین اتمیک بودن (Atomicity) دستورات است. به این معنی که مجموعهای از دستورات (مثل SET, HSET و...) به صورت یک بلوک واحد اجرا شوند و در حین اجرا، هیچ کلاینت دیگری نتواند دستوری را بین این دستورات تزریق یا اجرا کند.
مکانیزم عملکرد Multi و Exec :
وقتی کلاینت دستور MULTI را ارسال میکند، سرور ردیس وارد حالت تراکنش میشود. از این لحظه به بعد، دستورات ارسالی توسط کلاینت اجرا نمیشوند، بلکه در یک صف (Queue) سمت سرور ذخیره میشوند. این فرآیند تا زمانی که کلاینت دستور EXEC را ارسال نکند، ادامه دارد.
اما با ارسال EXEC:
1. تمام دستورات موجود در صف، به صورت ترتیبی و پشت سر هم اجرا میشوند.
2. تا پایان اجرای تمام دستورات این صف، نوبت به اجرای دستورات سایر کلاینتها نمیرسد (ایزولاسیون در سطح اجرا).
پایپ لاین (Pipeline) و کاهش Round Trips :
در کتابخانههای کلاینت ردیس (مانند redis-py در پایتون)، مفهومی به نام Pipeline وجود دارد که اغلب با تراکنش اشتباه گرفته میشود.
هدف اصلی Pipeline، کاهش تعداد Round Trips (رفت و برگشتهای شبکه) بین کلاینت و سرور است.
در حالت عادی، برای هر دستور، کلاینت باید منتظر پاسخ سرور بماند و سپس دستور بعدی را بفرستد. اما در Pipeline، کلاینت مجموعهای از دستورات را یکجا ارسال میکند و سپس پاسخها را یکجا دریافت میکند. این کار باعث کاهش تاخیر شبکه و بهبود چشمگیر عملکرد (Throughput) میشود.
چالش Race Condition در تراکنشهای ساده :
استفاده از MULTI و EXEC به تنهایی، اتمیک بودن اجرا را تضمین میکند، اما شرطیسازی (Condition) را خیر.
فرض کنید دو کلاینت همزمان میخواهند موجودی یک کالا را چک کرده و در صورت موجود بودن، آن را رزرو کنند. اگر هر دو کلاینت همزمان MULTI کنند، ممکن است هر دو قبل از اینکه دیگری EXEC کند، موجودی را خوانده و هر دو موفق به رزرو شوند (کاهش موجودی به عدد منفی برسد).
چرا؟ چون در داخل صف تراکنش، نمیتوان دستور شرطی (If) گذاشت.
راه حل:
برای حل این مشکل دو راه وجود دارد:
1. استفاده از دستور WATCH: که یک نوع قفل خوشبینانه (Optimistic Locking) ایجاد میکند. اگر کلیدی که Watch شده باشد، قبل از اجرای EXEC توسط کلاینت دیگری تغییر کند، تراکنش شما لغو (Fail) میشود.
2. استفاده از Lua Scripts: که اجرای کد را به صورت اتمیک در سمت سرور ردیس انجام میدهد (پیشنهاد میشود).
پایپلاین تراکنشی در مقابل غیر تراکنشی :
در کتابخانه redis-py، ما دو نوع Pipeline داریم:
۱. پایپلاین تراکنشی (Transactional Pipeline)
این حالت پیشفرض است و معادل احاطه کردن دستورات با MULTI و EXEC است.
pipe = conn.pipeline() # یا pipeline(transaction=True)
مزیت: تضمین اتمیک بودن دستورات.
عیب: سربار کمی بیشتر به دلیل دستورات Multi/Exec.
۲. پایپلاین غیر تراکنشی (Non-transactional Pipeline)
در این حالت، دستورات به صورت دستهجمعی (Batch) ارسال میشوند اما بدون MULTI و EXEC.
pipe = conn.pipeline(transaction=False)
مزیت: سریعترین حالت ممکن برای ارسال حجم زیادی داده (مثل نوشتن لاگهای حجیم).
عیب: هیچ تضمینی وجود ندارد که دستورات وسط کار توسط کلاینت دیگری قطع شوند (Atomicity ندارد).
مقایسه عملکرد (Benchmark)

بر اساس تستهای عملکردی، اولویت سرعت به شرح زیر است:
1. Non-transactional Pipeline: (سریعترین) - به دلیل حذف سربار تراکنش.
2. Transactional Pipeline: (سریع) - کمی کندتر از حالت قبل به دلیل MULTI/EXEC.
3. دستورات تکی (بدون Pipeline): (کندترین) - به دلیل رفتوبرگشت شبکه برای هر دستور.
استفاده از هر دو نوع Pipeline (تراکنشی و غیر تراکنشی) در مقایسه با ارسال دستورات به صورت تکی، میتواند عملکرد را تا چندین برابر (بسته به شبکه و حجم داده) بهبود بخشد. اما انتخاب بین این دو، بستگی به نیاز شما به سلامت دادهها (Data Integrity) دارد.
جمعبندی نهایی:
اگر سلامت دادهها و اتمیک بودن عملیات مهم است (مثل انتقال وجه یا رزرو کالا): از Transactional Pipeline یا Lua Scripts استفاده کنید.
اگر سرعت مهم است و از دست دادن یا تداخل موقت دادهها مشکلی ندارد (مثل نوشتن لاگ یا آپدیتهای آماری): از Non-transactional Pipeline استفاده کنید.
منابع:
Redis in Action
Redis Deep Dive
مستندات رسمی Redis-py