در دنیای معماری نرمافزار مدرن، بهویژه در میکروسرویسها و برنامههای با تراکم بالا، حفظ یکپارچگی داده حیاتی است. وقتی چندین نمونه از یک برنامه همزمان تلاش میکنند یک منبع را تغییر دهند، شرایط مسابقه رخ میدهد. این موضوع میتواند منجر به فساد داده، زیان مالی یا ناپایداری سیستم شود. اگرچه پایگاههای داده رابطهای یکپارچگی تراکنشی قوی ارائه میدهند، اما اغلب در بارهای کاری با خواندن سنگین به گلوگاه تبدیل میشوند. در اینجا است که Redis، یک ذخیرهساز ساختار داده در حافظه، به عنوان یک مکانیزم هماهنگسازی با عملکرد بالا درخشش میکند.
پیادهسازی قفلهای توزیعشده به برنامه شما امکان همگامسازی دسترسی به منابع مشترک را در گرههای مختلف یک خوشه میدهد. در این پست، ما ظرافتهای پیادهسازی قفلهای توزیعشده مقاوم با استفاده از Redis را بررسی میکنیم و از پیادهسازیهای پایه فراتر رفته تا به موانع رایج مانند قفلشدگی (deadlock) و مشکلات انقضای قفل بپردازیم.
آناتومی یک قفل توزیعشده
در هسته خود، یک قفل توزیعشده به دو عمل اصلی نیاز دارد: اخذ (acquire) و رها کردن (release). برای اخذ قفل روی یک منبع خاص، یک کلاینت باید بررسی کند که آیا قفل وجود دارد یا خیر. اگر وجود نداشته باشد، کلاینت آن را با یک مقدار خاص (اغلب یک شناسه یکتا مانند UUID) و یک زمان انقضا (TTL) تنظیم میکند. اگر عملیات تنظیم موفقیتآمیز باشد، قفل اخذ میشود. برای رها کردن قفل، کلاینت باید مطمئن شود که هنوز قفل را در اختیار دارد و قبل از حذف کلید، بررسی کند که مقدار با شناسه آن مطابقت دارد.
اگرچه این موضوع ساده به نظر میرسد، اما انجام آن با استفاده از دستورات جداگانه EXISTS و SET پر از خطر است. بین بررسی و تنظیم، ممکن است یک فرآیند دیگر قفل را اخذ کند که منجر به یک شرایط مسابقه میشود. بنابراین، اتمیک بودن غیرقابل مذاکره است.
اخذ اتمیک با SET NX
Redis دستور SET را با پرچمهای NX (Not Exist - وجود ندارد) و EX (Expiration - انقضا) ارائه میدهد. این به شما امکان میدهد یک کلید را به صورت اتمیک فقط در صورتی که از قبل وجود نداشته باشد تنظیم کنید و همزمان زمان تا انقضای آن را نیز تعیین نمایید. این کار پنجره آسیبپذیری موجود در عملیات غیراتمیک را از بین میبرد.
در اینجا یک مثال پایتون با استفاده از کتابخانه redis-py برای اخذ ایمن قفل آورده شده است:
import redis
import uuid
import time
# اتصال به Redis
r = redis.Redis(host='localhost', port=6379, db=0)
def acquire_lock(resource_name):
# تولید یک توکن یکتا برای این نمونه قفل
token = str(uuid.uuid4())
# تلاش برای اخذ قفل به صورت اتمیک
# NX: فقط در صورت عدم وجود تنظیم شود
# EX: تنظیم انقضا بر حسب ثانیه
lock_acquired = r.set(resource_name, token, nx=True, ex=10)
if lock_acquired:
return token
else:
return None
# مثال استفاده
token = acquire_lock("my_critical_resource")
if token:
try:
# انجام عملیات بخش بحرانی
print("Lock acquired. Performing updates...")
finally:
release_lock("my_critical_resource", token)
else:
print("Failed to acquire lock, resource is busy.")
پارامتر ex=10 حیاتی است. بدون آن، اگر فرآیندی که قفل را در اختیار دارد دچار خطا شود، قفل هرگز رها نخواهد شد و این امر باعث قفلشدگی دائمی برای سایر گرهها میشود. انقضا به عنوان یک تور ایمنی عمل میکند.
مشکل رها کردن و اتمیک بودن
رها کردن قفل به اندازه اخذ آن حیاتی است. یک پیادهسازی ساده ممکن است فقط کلید را حذف کند. با این حال، اگر قفل به طور طبیعی منقضی شود در حالی که کلاینت هنوز در حال اجرای وظیفه خود است، کلید توسط منطق انقضا حذف میشود. اگر کلاینت سپس به طور کورکورانه DEL را فراخوانی کند، ممکن است قفلی را حذف کند که توسط یک فرآیند دیگر دوباره اخذ شده است.
برای حل این مشکل، ما باید مالکیت را قبل از حذف تأیید کنیم. از آنجا که GET استاندارد به دنبال آن DEL اتمیک نیست، از یک اسکریپت Lua کوچک استفاده میکنیم. اسکریپتهای Lua در Redis به صورت اتمیک و در یک رشته واحد اجرا میشوند که تضمین میکند هیچ دستور دیگری نمیتواند در طول فرآیند بررسی و حذف مداخله کند.
def release_lock(resource_name, token):
# اسکریپت Lua برای بررسی و حذف
script = """
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
"""
r.eval(script, 1, resource_name, token)
ملاحظات پیشرفته: Redlock و تمدید
اگرچه رویکرد پایه برای بسیاری از سناریوها کار میکند، اما محدودیتهایی دارد. اگر تأخیر شبکه افزایش یابد یا توقف GC (جمعآوری زباله) رشته اصلی را متوقف کند، قفل ممکن است زودتر از موعد منقضی شود. در سیستمهای بسیار حیاتی، پیادهسازی الگوریتم Redlock (پیشنهاد شده توسط antirez) توصیه میشود. Redlock تلاش میکند قفلها را روی چندین نمونه Redis مستقل اخذ کند. یک قفل زمانی اخذ شده در نظر گرفته میشود که کلاینت بتواند اکثریت این قفلها را در یک بازه زمانی مشخص اخذ و نگه دارد.
علاوه بر این، برای وظایف طولانیمدت، ممکن است نیاز به پیادهسازی یک مکانیزم تمدید قفل (اغلب به نام watchdog) داشته باشید. این شامل یک رشته پسزمینه است که به طور دورهای TTL قفل را تا زمانی که دارنده قفل فعال باقی بماند، گسترش میدهد و اطمینان حاصل میکند که قفل در طول عملیات طولانی منقضی نمیشود.
نتیجهگیری
قفلهای توزیعشده یک ابزار قدرتمند برای حفظ یکپارچگی داده در معماریهای مبتنی بر Redis هستند. با بهرهگیری از دستورات اتمیک مانند SET NX و اسکریپتنویسی Lua، میتوانید سیستمهای مقاومی بسازید که شرایط مسابقه را به طور مؤثر جلوگیری میکنند. با این حال، توسعهدهندگان باید نسبت به موارد حاشیهای مانند انحراف ساعت، پارتیشنبندی شبکه و خطاهای فرآیند هوشیار باشند. همیشه سادهترین راه حلی را انتخاب کنید که نیازهای یکپارچگی شما را برآورده میکند و فقط زمانی به الگوریتمهای پیچیدهتری مانند Redlock فکر کنید که همخطی دقیق (linearizability) ضروری باشد. با پیادهسازی صحیح، Redis میتواند به عنوان یک لایه هماهنگسازی بسیار مقیاسپذیر برای سیستمهای توزیعشده شما عمل کند.