عنوان:

‫استفاده از قفل‌های توزیع‌شده برای جلوگیری از اجرای مجدد وظایف پشت‌صحنه


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۲/۲۲ ۰۸:۵۳
آدرس: www.dntips.ir
مقیاس‌پذیری و پایداری سیستم‌های توزیع‌شده، از اهمیت ویژه‌ای برخوردار است. با گسترش استفاده از معماری‌های مبتنی بر ابر (Cloud) و استقرار برنامه‌ها در چندین نمونه (Instance) روی پلتفرم‌هایی مانند Kubernetes یا Azure App Service، مدیریت وظایف پشت‌صحنه (Background Tasks) به چالشی پیچیده تبدیل شده است. یکی از مشکلات رایج در این سناریوها، اجرای همزمان یک وظیفه توسط چندین نمونه از برنامه است که می‌تواند منجر به ناسازگاری داده‌ها، مصرف غیرضروری منابع یا حتی خطاهای سیستمی شود. برای رفع این مشکل، استفاده از قفل‌های توزیع‌شده (Distributed Locks) به‌عنوان راهکاری مؤثر مطرح می‌شود. این مقاله به بررسی مفهوم قفل‌های توزیع‌شده، نحوه پیاده‌سازی آن‌ها با استفاده از کتابخانه Medallion.DistributedLock و کاربرد آن در جلوگیری از اجرای مجدد وظایف پشت‌صحنه در برنامه‌های دات‌نت (.NET) می‌پردازد.

قفل‌های توزیع‌شده چیست و چرا اهمیت دارند؟
قفل‌های توزیع‌شده مکانیزمی هستند که امکان هماهنگی بین چندین نمونه از یک برنامه را در یک محیط توزیع‌شده فراهم می‌کنند. برخلاف قفل‌های محلی (Local Locks) که تنها در یک فرایند یا ماشین خاص عمل می‌کنند، قفل‌های توزیع‌شده در سطح سیستم‌های مختلف هماهنگی ایجاد می‌کنند تا اطمینان حاصل شود که تنها یک نمونه در هر لحظه به یک منبع یا وظیفه خاص دسترسی دارد. این مکانیزم مشابه انتخاب یک رهبر (Leader Election) سبک است که تضمین می‌کند تنها یک نمونه وظیفه موردنظر را اجرا کند.
فرض کنید برنامه‌ای دارید که هر پنج دقیقه یک وظیفه پشت‌صحنه مانند پردازش داده‌ها یا ارسال اعلان‌ها را اجرا می‌کند. اگر این برنامه روی چندین نمونه مستقر شده باشد، بدون هماهنگی مناسب، همه نمونه‌ها ممکن است وظیفه را به‌صورت همزمان اجرا کنند. این امر می‌تواند به مشکلاتی مانند پردازش تکراری داده‌ها یا بار اضافی روی پایگاه داده منجر شود. قفل‌های توزیع‌شده با ایجاد یک قفل مشترک در یک مخزن مرکزی مانند Redis، این مشکل را حل می‌کنند. Redis به دلیل سرعت بالا، سادگی و قابلیت اطمینان، گزینه‌ای محبوب برای پیاده‌سازی قفل‌های توزیع‌شده است.

معرفی کتابخانه Medallion.DistributedLock
کتابخانه Medallion.DistributedLock یک ابزار متن‌باز و قدرتمند است که امکان پیاده‌سازی قفل‌های توزیع‌شده را در برنامه‌های دات‌نت فراهم می‌کند. این کتابخانه از چندین مخزن پشتیبان (Backend) مانند Redis، SQL Server و PostgreSQL پشتیبانی می‌کند، اما Redis به دلیل عملکرد بهینه و سهولت استفاده، اغلب ترجیح داده می‌شود. این کتابخانه با ارائه رابط‌های ساده و انعطاف‌پذیر، به توسعه‌دهندگان اجازه می‌دهد قفل‌های توزیع‌شده را به‌راحتی در پروژه‌های خود ادغام کنند. در ادامه، نحوه استفاده از این کتابخانه با Redis برای جلوگیری از اجرای همزمان وظایف پشت‌صحنه توضیح داده می‌شود.

پیاده‌سازی قفل توزیع‌شده با Redis
برای استفاده از قفل‌های توزیع‌شده در یک برنامه دات‌نت، ابتدا باید کتابخانه Medallion.RedisDistributedLock را نصب کنیم. این کار با اجرای دستور زیر در محیط خط فرمان دات‌نت انجام می‌شود:
dotnet add package Medallion.RedisDistributedLock
پس از نصب کتابخانه، باید سرویس قفل توزیع‌شده را در برنامه ثبت کنیم. این کار معمولاً در فایل Program.cs انجام می‌شود. کد زیر نشان می‌دهد چگونه یک ارائه‌دهنده قفل (Distributed Lock Provider) مبتنی بر Redis را پیکربندی کنیم:
builder.Services.AddSingleton<IDistributedLockProvider>(sp =>
{
    return new RedisDistributedLockProvider("localhost:6379");
});
در این کد، رشته اتصال به سرور Redis (مانند "localhost:6379") به‌عنوان پارامتر به RedisDistributedLockProvider ارسال می‌شود. این سرویس به برنامه اجازه می‌دهد قفل‌های توزیع‌شده را ایجاد و مدیریت کند.
برای ساختاربندی بهتر، می‌توانیم یک کلاس سفارشی برای مدیریت قفل‌ها تعریف کنیم:
internal class RedisDistributedLockProvider(string _connectionString) : IDistributedLockProvider
{
    public IDistributedLock CreateLock(string name)
    {
        var connection = ConnectionMultiplexer.Connect(_connectionString);
        return new RedisDistributedLock(name, connection.GetDatabase());
    }
}
این کلاس با دریافت رشته اتصال به Redis، قفل‌هایی با نام‌های مشخص ایجاد می‌کند که می‌توانند در وظایف مختلف استفاده شوند.

ادغام قفل در وظیفه پشت‌صحنه
اکنون که سرویس قفل توزیع‌شده آماده است، می‌توان آن را در یک وظیفه پشت‌صحنه استفاده کرد. فرض کنید کلاسی به نام ScheduledBackgroundJob داریم که وظیفه‌ای را هر پنج دقیقه اجرا می‌کند. برای جلوگیری از اجرای همزمان این وظیفه توسط چندین نمونه، از قفل توزیع‌شده استفاده می‌کنیم. کد زیر نحوه پیاده‌سازی این سناریو را نشان می‌دهد:
private readonly IDistributedLockProvider _lockProvider;

public ScheduledBackgroundJob(IServiceScopeFactory scopeFactory, IDistributedLockProvider lockProvider)
{
    _scopeFactory = scopeFactory;
    _lockProvider = lockProvider;
}

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
    var @lock = _lockProvider.CreateLock("ScheduledBackgroundJob");
    using var timer = new PeriodicTimer(TimeSpan.FromMinutes(5));

    do
    {
        await using var handle = await @lock.TryAcquireAsync(TimeSpan.FromSeconds(30), stoppingToken);
        if (handle is not null)
        {
            await DoWorkAsync(stoppingToken);
        }
    }
    while (await timer.WaitForNextTickAsync(stoppingToken)
            && !stoppingToken.IsCancellationRequested);
}
در این کد، متد TryAcquireAsync تلاش می‌کند قفل را برای مدت 30 ثانیه به‌دست آورد. اگر قفل با موفقیت کسب شود (یعنی هیچ نمونه دیگری قفل را در اختیار ندارد)، وظیفه اصلی (DoWorkAsync) اجرا می‌شود. در غیر این صورت، نمونه منتظر می‌ماند تا در چرخه بعدی تلاش کند. این مکانیزم تضمین می‌کند که تنها یک نمونه در هر لحظه وظیفه را اجرا کند.

ثبت سرویس در برنامه
برای اطمینان از عملکرد صحیح وظیفه پشت‌صحنه، باید آن را به همراه وابستگی‌هایش در فایل Program.cs ثبت کنیم:
builder.Services.AddHostedService<ScheduledBackgroundJob>();
builder.Services.AddDbContext<MyDbContext>();
این کد وظیفه ScheduledBackgroundJob را به‌عنوان یک سرویس میزبانی‌شده (Hosted Service) ثبت می‌کند و زمینه پایگاه داده (Database Context) را برای استفاده در وظیفه فراهم می‌آورد.

مزایا و ملاحظات
استفاده از قفل‌های توزیع‌شده مزایای متعددی دارد. اولاً، این روش نیازی به سیستم‌های زمان‌بندی پیچیده (Scheduler) ندارد و با کمترین پیچیدگی، هماهنگی بین نمونه‌ها را فراهم می‌کند. ثانیاً، Redis به دلیل عملکرد بالا و تأخیر کم، گزینه‌ای ایده‌آل برای این سناریو است. با این حال، توسعه‌دهندگان باید به چند نکته توجه کنند. برای مثال، باید اطمینان حاصل کنند که سرور Redis به‌درستی پیکربندی شده و در برابر خرابی‌ها مقاوم است. همچنین، انتخاب مدت زمان قفل (Lock Timeout) باید با دقت انجام شود تا از قفل شدن دائمی (Deadlock) جلوگیری شود.
برای بهبود این پیاده‌سازی، می‌توان از الگوهای پیشرفته‌تر مانند قفل‌های قابل تمدید (Renewable Locks) استفاده کرد که به‌صورت دوره‌ای قفل را تمدید می‌کنند تا از آزاد شدن زودهنگام آن جلوگیری شود. علاوه بر این، بررسی ابزارهای جایگزین مانند Apache ZooKeeper یا Consul می‌تواند برای سناریوهای خاص مفید باشد، اگرچه پیچیدگی بیشتری به همراه دارند.

نتیجه‌گیری
قفل‌های توزیع‌شده راهکاری قدرتمند برای مدیریت وظایف پشت‌صحنه در برنامه‌های مقیاس‌پذیر دات‌نت هستند. با استفاده از کتابخانه Medallion.DistributedLock و مخزن Redis، توسعه‌دهندگان می‌توانند به‌سادگی از اجرای همزمان وظایف توسط چندین نمونه جلوگیری کنند. این مقاله با ارائه توضیحات گام‌به‌گام و مثال‌های عملی، نشان داد که چگونه می‌توان قفل‌های توزیع‌شده را در یک برنامه دات‌نت پیاده‌سازی کرد. با رعایت نکات مطرح‌شده و توجه به پیکربندی مناسب، توسعه‌دهندگان می‌توانند سیستم‌هایی پایدار و کارآمد طراحی کنند که در محیط‌های توزیع‌شده به‌خوبی عمل کنند.


مشاهده مطلب اصلی