Files
Caspian.WorkflowFramework/docs/workflow-cache-beginner.fa.md
T

68 KiB
Raw Blame History

آموزش کش ورکفلو از ابتدا: از UseCache تا Redis و اجرای چندنودی

این راهنما برای کسی نوشته شده که می‌خواهد کد و تنظیمات پروژه‌های Caspian را بفهمد، حتی اگر قبلاً با کش، Redis، Dependency Injection یا اجرای چندنودی کار نکرده باشد.

مثال‌ها بر اساس کد فعلی Caspian.Elsa.Core، Caspian.WorkflowFramework و NextBankParsian هستند. نسخهٔ ماژول‌های تغییرکردهٔ Core، 1.1.0-preview.10 و نسخهٔ Framework در سورس و مصرف NextBank، 1.1.0-preview.28 است. این راهنما در تاریخ 2026-10-10 با ساختار فعلی فایل‌های تنظیمات بازسازی شده است.

برای فهرست گستردهٔ فایل‌های تغییرکرده و جزئیات پیاده‌سازی، مستند مفصل تغییرات را بخوانید. راهنمای کوتاه تنظیمات هم برای مراجعهٔ سریع موجود است.

خلاصهٔ سریع؛ اگر فقط چند دقیقه وقت دارید

موضوع مطلب اصلی
محل کش UseCaching، provider و مدت کش را یک‌بار برای همان ماژول Elsa تنظیم می‌کند.
فعال‌کردن مصرف‌کننده‌ها سه UseCache، کش تعریف‌ها، Triggerها و lookup HTTP ورودی را به همان provider متصل می‌کنند.
انتخاب Redis RegisterRedis قابلیت انتخاب را ثبت می‌کند؛ Provider=Redis در فایل یا UseRedis در کد آن را انتخاب می‌کند.
اتصال کش و قفل Redis از IConnectionMultiplexer میزبان استفاده می‌کنند. هر process اتصال خودش را به Redis مشترک دارد.
تنظیمات فعلی NextBank Sentinel در appsettings.json پایهٔ Web، Admin و Workers است؛ تنظیمات لوکال در فایل Development قرار می‌گیرند.
فضای داده دیتابیس Redis برابر 1؛ پیشوند کش nextbank:workflow:cache: و پیشوند قفل nextbank:workflow:locks: است.
دادهٔ اصلی تعریف‌ها و وضعیت اجرا در SQL می‌مانند. Redis کپی موقت دادهٔ قابل اشتراک را نگه می‌دارد.
اعتبار شمارهٔ اعتبار از SQL خوانده می‌شود تا کلیدهای وضعیت قدیمی مصرف نشوند.
قفل برای هماهنگی عملیات هم‌زمان است و پس از آزادشدن می‌تواند سریع از Redis حذف شود.
تعداد کلیدها نتیجهٔ جست‌وجو کش می‌شود؛ تعداد کلیدها با تعداد تعریف‌های SQL برابر نیست.
اولویت مدت کش پیش‌فرض Framework، سپس فایل، سپس مقدار صریح callback. مدت پیش‌فرض Framework یک روز است.
چندنودی میزبان‌های مرتبط باید SQL ورکفلو، Redis، دیتابیس Redis و پیشوندهای هماهنگ داشته باشند.

دو پرسش را جدا در نظر بگیرید: UseCaching تعیین می‌کند کش کجا و چگونه نگه‌داری شود؛ UseCache تعیین می‌کند کدام بخش از آن استفاده کند. provider قفل نیز جداگانه تنظیم می‌شود.

برای مطالعهٔ عمیق، از فصل ۱ ادامه دهید. مثال‌های فایل و کد در فصل‌های ۱۳ تا ۱۸، اعتبار و قفل در فصل‌های ۲۰ تا ۲۴ و روش اجرا و عیب‌یابی در فصل‌های ۲۶ و ۲۷ توضیح داده شده‌اند.

۱. ابتدا بدانیم «ورکفلو» و «اجرای ورکفلو» چه تفاوتی دارند

فرض کنید یک ورکفلو داریم که مراحلش چنین است:

ثبت درخواست ← بررسی درخواست ← انتظار برای تأیید ← ادامهٔ پردازش

این نقشهٔ مراحل، «تعریف ورکفلو» است. وقتی درخواست اول وارد سیستم می‌شود، یک اجرای مشخص از این تعریف ساخته می‌شود. درخواست دوم می‌تواند اجرای دیگری از همان تعریف داشته باشد.

اصطلاح معنی ساده مثال
Workflow Definition نقشه و تنظیمات مراحل ورکفلو تعریف «بررسی درخواست»
DefinitionId شناسهٔ منطقی آن تعریف شناسه‌ای که نسخه‌های مختلف همان تعریف را به هم مرتبط می‌کند
نسخهٔ تعریف یکی از نسخه‌های ذخیره‌شدهٔ نقشه نسخهٔ ۱ یا ۲ تعریف
Workflow Instance یک اجرای مشخص از یک تعریف اجرای مربوط به درخواست شمارهٔ ۱۰۰
WorkflowInstanceId شناسهٔ همان اجرای مشخص شناسهٔ اجرای درخواست شمارهٔ ۱۰۰
Trigger مشخصات رویداد یا شرط شروع ورکفلو شروع با یک درخواست HTTP مشخص
Bookmark اطلاعات نقطه‌ای که اجرای ورکفلو در آن منتظر ادامه است انتظار برای دریافت تأیید

یک تعریف می‌تواند چند نسخه و تعداد زیادی اجرای مستقل داشته باشد. پس تعداد تعریف‌ها، نسخه‌ها، اجراها و کلیدهای کش را نباید یک عدد فرض کنیم.

۲. کش چیست و چه کمکی می‌کند؟

کش یک کپی موقت از نتیجهٔ خواندن اطلاعات است. اگر دوباره همان اطلاعات را بخواهیم و کپی هنوز معتبر باشد، می‌توانیم از آن استفاده کنیم.

مثلاً برنامه از دیتابیس می‌پرسد:

آخرین نسخهٔ منتشرشدهٔ تعریف A را بده.

نتیجه دریافت و در کش ذخیره می‌شود. درخواست بعدی برای همان جست‌وجو می‌تواند نتیجه را از کش بخواند. اگر تعریف تغییر کند، باید تشخیص دهیم که کپی قبلی دیگر قابل استفاده نیست.

در پروژهٔ ما، تعریف‌های اصلی و وضعیت اجرای ورکفلو در SQL باقی می‌مانند. Redis محل نگه‌داری کپی‌های موقتِ قابل اشتراک است.

کش به معنی حذف تمام درخواست‌های SQL نیست. در حالت کش مشترک، برنامه برای بررسی اعتبار کش یک query کوچک به SQL می‌زند. صرفه‌جویی اصلی در تکرار خواندن داده‌های قابل کش است.

چند واژه که در ادامه می‌بینید:

واژه معنی
Cache Hit نتیجهٔ قابل استفاده در کش پیدا شده است
Cache Miss نتیجهٔ قابل استفاده پیدا نشده و باید از مسیر اصلی خوانده شود
Payload خود داده‌ای که داخل ورودی کش نگه‌داری می‌شود
TTL زمان باقی‌مانده تا انقضای کلید
Absolute Expiration پایان اعتبار در زمانی ثابت نسبت به ایجاد ورودی
Sliding Expiration پایان اعتبار بعد از مدت مشخصی بدون خواندن؛ خواندن معتبر زمان را تمدید می‌کند
Revision شمارهٔ وضعیت اعتبار کش

۳. MemoryCache و Redis چه تفاوتی دارند؟

MemoryCache اطلاعات را داخل حافظهٔ process برنامه نگه می‌دارد. اگر دو process از برنامه اجرا کنید، هرکدام حافظهٔ کش خودشان را دارند.

Redis یک سرویس مستقل است. چند process می‌توانند به همان Redis وصل شوند و داده‌های مشترک را از آن بخوانند.

موضوع MemoryCache Redis
محل دادهٔ کش حافظهٔ process برنامه سرویس Redis
اشتراک بین processها هر process کپی خودش را دارد با اتصال و فضای کلید مشترک، داده مشترک است
پس از restart برنامه کش آن process از بین می‌رود restart برنامه به‌تنهایی داده‌های Redis را پاک نمی‌کند
نیاز به اتصال شبکه برای خود کش محلی ندارد دارد
مناسب‌بودن برای شیء اجرایی محلی بله، بسته به مصرف‌کننده معمولاً داده باید قابل تبدیل به متن یا ساختار ذخیره‌شدنی باشد

اگر یک سرور دو node داشته باشد، منظور می‌تواند دو process یا دو نمونهٔ اجرای برنامه باشد. حتی روی همان سرور، حافظهٔ این دو process یکی نیست.

در اجرای چندسروری، localhost روی هر سرور به خود همان سرور اشاره می‌کند. برای دادهٔ مشترک، همهٔ نودهای مرتبط باید به Redis مشترک وصل شوند.

۴. هر پروژه در این مسیر چه مسئولیتی دارد؟

پروژه مسئولیت
Caspian.Elsa.Core پیاده‌سازی اصلی موتور ورکفلو، قرارداد کش و providerهای آن
Elsa.Server.Web در Core برنامهٔ میزبان برای اجرای مستقیم موتور، مثلاً هنگام استفاده از Studio
Caspian.Elsa.Studio رابط کاربری طراحی و مدیریت؛ با بک‌اند ارتباط دارد
Caspian.WorkflowFramework پکیجی که امکانات Core را برای برنامهٔ مصرف‌کننده پیکربندی می‌کند
NextBankParsian.Web میزبان درخواست‌های Web و اجرای مربوط به آن‌ها
NextBankParsian.Admin میزبان مدیریت و عملیات مرتبط با ورکفلو
NextBankParsian.Workers میزبان پردازش‌های پس‌زمینه

این نمودار رابطهٔ پکیج‌ها و داده را نشان می‌دهد. پکیج، سرویس شبکه‌ای جداگانه نیست؛ کد آن داخل برنامهٔ میزبان اجرا می‌شود.

flowchart LR
    Core["پکیج‌های Core"] --> Framework["پکیج WorkflowFramework"]
    Framework --> Web["NextBank Web"]
    Framework --> Admin["NextBank Admin"]
    Framework --> Workers["NextBank Workers"]
    Web --> Redis["Redis مشترک"]
    Admin --> Redis
    Workers --> Redis
    Web --> SQL["SQL مشترک ورکفلو"]
    Admin --> SQL
    Workers --> SQL

هر میزبان، سرویس‌ها و اشیای خودش را می‌سازد. اشتراک داده از طریق Redis و SQL انجام می‌شود.

۵. Provider یعنی چه؟

Provider یعنی پیاده‌سازیِ محل ذخیره و خواندن کش. در این پروژه، Memory و Redis دو provider هستند.

مصرف‌کنندهٔ کش می‌گوید «این مقدار را ذخیره کن» یا «مقدار این کلید را بخوان». provider کار مربوط به محل ذخیره را انجام می‌دهد.

مثلاً برای یک مصرف‌کننده، عملیات چنین است:

نتیجهٔ جست‌وجوی تعریف A را ذخیره کن.

وقتی provider برابر Memory باشد، ذخیره در حافظهٔ همان process انجام می‌شود. وقتی Redis باشد، دادهٔ قابل ذخیره به Redis فرستاده می‌شود.

در Core، قرارداد ICacheProvider این عملیات را تعریف می‌کند. به همین دلیل، برای افزودن provider جدید لازم نیست منطق جست‌وجوی تعریف‌ها را از ابتدا بنویسیم.

۶. UseCaching چه کاری می‌کند؟

این قسمت از کد Framework تنظیمات مشترک کش را انتخاب می‌کند:

.UseCaching(cache =>
{
    cache.Duration = TimeSpan.FromDays(1);
    cache.RegisterRedis();
    cache.UseConfiguration(
        builder.Configuration.GetSection("Workflow:Cache"));

    configureCache?.Invoke(cache);
})

هر خط را جدا بررسی کنیم:

خط کار واقعی
cache.Duration = TimeSpan.FromDays(1) مدت پیش‌فرض کش در Framework را یک روز قرار می‌دهد
cache.RegisterRedis() نام Redis را به‌عنوان provider قابل انتخاب ثبت می‌کند
cache.UseConfiguration(...) تنظیمات بخش Workflow:Cache را می‌خواند و provider را انتخاب می‌کند
configureCache?.Invoke(cache) اگر مصرف‌کننده callback داده باشد، آن را اجرا می‌کند

RegisterRedis() به‌تنهایی Redis را انتخاب نمی‌کند. این خط می‌گوید «امکان انتخاب Redis وجود دارد». انتخاب با تنظیم Provider در فایل یا با UseRedis(...) در کد انجام می‌شود.

اگر تنظیم زیر خوانده شود، Redis انتخاب می‌شود:

{
  "Workflow": {
    "Cache": {
      "Provider": "Redis"
    }
  }
}

اگر provider از تنظیمات یا کد انتخاب نشده باشد، پیش‌فرض عمومی Core برابر Memory است. وجود یک سرور Redis یا نصب پکیج Redis، به‌تنهایی provider کش Elsa را تغییر نمی‌دهد.

۷. پس چرا داخل سه UseCache نام Redis وجود ندارد؟

بعد از انتخاب تنظیمات مشترک، این خطوط مصرف‌کننده‌های کش را فعال می‌کنند:

management.UseCache();
runtime.UseCache();
.UseHttp(http => http.UseCache())

تفاوت دو نوع فراخوانی این است:

فراخوانی سؤالی که جواب می‌دهد
UseCaching(...) کش را کجا و با چه تنظیماتی نگه داریم؟
management.UseCache() کش مربوط به تعریف‌های ورکفلو فعال باشد؟
runtime.UseCache() کش مربوط به Triggerها و هماهنگی مرتبط با Runtime فعال باشد؟
http.UseCache() نتیجهٔ پیدا‌کردن ورکفلو برای درخواست HTTP ورودی کش شود؟

سه مصرف‌کننده به تنظیمات مشترک همان ماژول Elsa متصل‌اند. نام Redis در آن‌ها تکرار نمی‌شود، چون provider قبلاً انتخاب شده است.

flowchart TD
    Settings["Workflow:Cache در فایل تنظیمات"] --> Feature["UseCaching / CacheFeature"]
    Feature --> Manager["مدیر کش مشترک در همین ماژول"]
    Management["management.UseCache"] --> Manager
    Runtime["runtime.UseCache"] --> Manager
    Http["http.UseCache"] --> Manager
    Manager --> Provider["provider انتخاب‌شده: Redis یا Memory"]

این استفاده از تنظیم مشترک است. برنامه از روی وجود Redis در شبکه، provider را حدس نمی‌زند.

اگر در تنظیمات مشترک Provider را به Memory تغییر دهید و برنامه را دوباره اجرا کنید، این سه خط همچنان همان شکل قبلی را دارند، ولی از provider جدید استفاده می‌کنند.

۸. این اشتراک در کد چگونه پیاده شده است؟

برای این بخش فقط سه مفهوم لازم داریم:

مفهوم معنی ساده
Module مجموعهٔ قابلیت‌های نصب‌شدهٔ Elsa در این میزبان
Feature یکی از قابلیت‌های آن مجموعه، مثلاً قابلیت کش
DI یا Dependency Injection سازوکاری که سرویس‌های ثبت‌شده را هنگام نیاز به کلاس‌ها می‌دهد

در Module، Featureها بر اساس نوع نگه‌داری می‌شوند. وقتی چند مصرف‌کننده تنظیم CacheFeature را درخواست کنند، به همان Feature متعلق به آن ماژول دسترسی پیدا می‌کنند.

در CacheConsumerFeature، این ارتباط مشخص است:

public CacheFeature Cache => Module.Configure<CacheFeature>();

مدیر کش، یعنی ICacheManager، از طریق DI به مصرف‌کننده‌ها داده می‌شود. مدیر کش هم از ICacheProvider انتخاب‌شده استفاده می‌کند.

مصرف‌کنندهٔ کش
    ↓
ICacheManager
    ↓
ICacheProvider
    ↓
Redis یا Memory

در ساختار فعلی، provider کش در سطح یک ماژول مشترک است. انتخاب صریح دو provider متفاوت برای مصرف‌کننده‌های همان ماژول خطا می‌دهد. این ساختار برای سه مصرف‌کنندهٔ بالا، سه انتخاب مستقل و پنهانی ایجاد نمی‌کند.

۹. management.UseCache دقیقاً چه چیزی را کش می‌کند؟

Management با تعریف‌های ورکفلو کار می‌کند. کش آن شامل خواندن یک تعریف، فهرست تعریف‌ها، خلاصه‌ها و بعضی نتایج جست‌وجو و شمارش است.

مثلاً این دو درخواست می‌توانند دو کلید متفاوت داشته باشند:

تعریف A، نسخهٔ منتشرشده
فهرست تعریف‌های منتشرشده، صفحهٔ اول

کلید دوم می‌تواند چند تعریف را داخل یک مقدار نگه دارد. کلیدها بر اساس نوع عملیات، ورودی جست‌وجو، صفحه‌بندی، نوع داده و Tenant ساخته می‌شوند.

گراف اجراییِ ورکفلو از تعریف معتبر بازسازی می‌شود. اشیای اجرایی و delegateهای آمادهٔ اجرا مستقیماً به Redis فرستاده نمی‌شوند.

منبع این رفتار: CachingWorkflowDefinitionStore و CachingWorkflowDefinitionService.

۱۰. runtime.UseCache دقیقاً چه کاری دارد؟

Runtime باید بتواند Triggerهای مربوط به یک رویداد را پیدا کند. کش Runtime نتیجهٔ خواندن Triggerها را نگه می‌دارد.

برای مثال، هنگام دریافت یک رویداد، موتور می‌پرسد:

کدام تعریف‌های ورکفلو Trigger متناظر با این رویداد دارند؟

نتیجهٔ این جست‌وجو می‌تواند کش شود. این کار در CachingTriggerStore انجام می‌شود.

در تغییر جدید Core، این قابلیت لایهٔ هماهنگی تغییر Bookmarkها را هم ثبت می‌کند. Bookmarkها همچنان در SQL هستند، ولی تغییرشان برای تازه‌شدن جدول مسیرهای HTTP نودهای دیگر ثبت می‌شود.

وضعیت اجرای ورکفلو، Bookmarkها و لاگ‌ها با این خط به Redis منتقل نمی‌شوند. تنظیم قفل اجرای چندنودی هم از مسیر جداگانهٔ DistributedLocking انجام می‌شود.

چرا شرط ثابت useCaching حذف شد؟

قبلاً در Framework داشتیم:

const bool useCaching = true;

if (useCaching)
    runtime.UseCache();

چون مقدار ثابت برابر true بود، این شرط همیشه اجرا می‌شد. اکنون کنترل خواندن و نوشتن دادهٔ کش با Workflow:Cache:Enabled انجام می‌شود.

با Enabled=false، لایه‌های ثبت‌شده همچنان وجود دارند، اما مدیر کش برای خواندن داده از مسیر اصلی استفاده می‌کند. ثبت و هماهنگی تغییرات در حالت Redis نیز ادامه پیدا می‌کند.

۱۱. http.UseCache چه کاربردی دارد؟

این کد دو کار را ترکیب می‌کند:

.UseHttp(http => http.UseCache())

UseHttp قابلیت‌های HTTPِ Elsa را فعال می‌کند. http.UseCache کشِ جست‌وجوی ورکفلو برای درخواست HTTP ورودی را فعال می‌کند.

فرض کنید در یک تعریف، HTTP Endpoint زیر ساخته‌اید:

POST /workflows/review

وقتی درخواست برسد، Elsa باید تعریف و Trigger متناظر را پیدا کند. نتیجهٔ پیدا‌کردن آن‌ها قابل کش است.

در Redis، اطلاعاتی مانند شناسهٔ نسخهٔ تعریف و Triggerها ذخیره می‌شود. گراف اجرایی بعداً از تعریف معتبر ساخته می‌شود.

اگر Activityِ SendHttpRequest دارید که از داخل ورکفلو سرویس دیگری را صدا می‌زند، http.UseCache پاسخ آن سرویس را کش نمی‌کند. کش عمومی پاسخ‌های APIهای NextBank هم موضوع جداگانه‌ای است.

این قابلیت برای استفاده از کش Redisِ تعریف‌ها الزامی نیست. کاربردش زمانی است که ورکفلوهای دارای HTTP Endpoint ورودی را اجرا می‌کنید.

منبع: CachingHttpWorkflowLookupService.

۱۲. چرا Configure قبلی حذف شد؟

قبلاً مدت کش را جداگانه ثابت می‌کردیم:

builder.Services.Configure<CachingOptions>(
    options => options.CacheDuration = TimeSpan.FromDays(1));

اکنون در ابتدای تنظیم مشترک، یک روز فقط مقدار پیش‌فرض است:

cache.Duration = TimeSpan.FromDays(1);
cache.UseConfiguration(
    builder.Configuration.GetSection("Workflow:Cache"));

فرض کنید فایل تنظیمات این مقدار را دارد:

{
  "Workflow": {
    "Cache": {
      "Duration": "00:30:00"
    }
  }
}

ترتیب نتیجه چنین می‌شود:

پیش‌فرض Framework: یک روز
    ↓
مقدار فایل: سی دقیقه
    ↓
مقدار صریح callback، اگر تعیین شده باشد

اگر ثبت ثابت قبلی بعد از تنظیم Feature باقی می‌ماند، می‌توانست مقدار نهایی Options را دوباره یک روز کند. آن ثبت حذف شد تا مدت کش از همین مسیر مشترک تعیین شود.

خود CacheFeature هنوز مقدار نهایی را در CachingOptions.CacheDuration ثبت می‌کند. پس مصرف‌کننده‌ها Options را دریافت می‌کنند؛ فقط محل تعیین مقدار آن یکپارچه شده است.

پیش‌فرض عمومی Core یک دقیقه است. Framework آن را پیش از خواندن تنظیمات، یک روز قرار می‌دهد. مقدار صریح فایل یا callback می‌تواند هر دو را تغییر دهد.

۱۳. سه بخش تنظیمات را از هم تشخیص بدهیم

در فایل تنظیمات سه بخش مهم داریم:

مسیر مسئولیت
Redis در ریشه آدرس و مشخصات اتصال Redis برنامهٔ میزبان
Workflow:Cache انتخاب provider و تنظیمات کش ورکفلو
Workflow:Runtime:DistributedLocking انتخاب و تنظیمات قفل‌های مشترک

برای مثال، آدرس شبکه در Redis:Hosts قرار دارد. پیشوند کش در Workflow:Cache:Redis:KeyPrefix و پیشوند قفل در Workflow:Runtime:DistributedLocking:KeyPrefix قرار دارد.

Database چیست؟

Redis می‌تواند چند دیتابیس منطقی شماره‌دار داشته باشد. در این پروژه شمارهٔ 1 انتخاب شده است. این شماره با نام دیتابیس SQL تفاوت دارد.

Redis:Database دیتابیس پیش‌فرض اتصال میزبان است. کش و قفل می‌توانند دیتابیس مورد استفادهٔ خودشان را صریحاً مشخص کنند. در تنظیمات فعلی، هر سه مقدار برابر 1 هستند.

KeyPrefix چیست؟

پیشوند به ابتدای نام کلید اضافه می‌شود تا کلیدهای بخش‌های مختلف قابل تشخیص باشند:

nextbank:workflow:cache:...  ← دادهٔ کش ورکفلو
nextbank:workflow:locks:...  ← قفل‌ها

یک اتصال Redis می‌تواند هر دو گروه را نگه دارد. متفاوت‌بودن پیشوند، به معنی ساخته‌شدن دو اتصال شبکه نیست.

پیشوند کش فقط برای تعریف‌ها نیست؛ سایر داده‌های قابل کشِ فعال‌شده در همان ماژول، مانند Triggerها و نتیجهٔ جست‌وجوی HTTP، نیز از provider و پیشوند مشترک استفاده می‌کنند. نوع داده و ورودی جست‌وجو در ساخت بخش داخلی کلید نقش دارند.

۱۴. اتصال مشترک با IConnectionMultiplexer چگونه کار می‌کند؟

IConnectionMultiplexer سرویس مدیریت ارتباط با Redis در کتابخانهٔ StackExchange.Redis است. کلاس‌هایی که به Redis نیاز دارند، می‌توانند این سرویس را از DI بگیرند.

در NextBank، کد زیر آن را ثبت می‌کند:

builder.Services.RegisterCache(builder.Configuration);

در Elsa.Server.Web، ثبت اتصال با این helper انجام می‌شود:

services.AddWorkflowRedis(configuration);

در هر دو حالت، اتصال به‌صورت Singleton ثبت شده است. یعنی داخل همان ServiceProvider، مصرف‌کننده‌ها همان نمونهٔ سرویس را دریافت می‌کنند.

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

Singleton متعلق به همان process و ServiceProvider است. Web، Admin و Workers هرکدام سرویس اتصال خودشان را دارند؛ اگر تنظیماتشان یکی باشد، به همان Redis مشترک وصل می‌شوند.

داخل process مربوط به Web:
    کش ورکفلو ─┐
    قفل‌ها ────┼── IConnectionMultiplexer همان میزبان ── Redis
    سایر مصرف‌کنندگان اتصال ─┘

در تنظیم پیش‌فرض provider کش Redis، ConnectionFactory سرویس میزبان را می‌گیرد. provider قفل Redis نیز همان سرویس را دریافت می‌کند.

بنابراین تغییر آدرس‌های بخش اصلی Redis روی سایر مصرف‌کنندگان همان اتصال در NextBank، مانند کش عمومی یا Redis Streamها، هم اثر دارد.

کش عمومی NextBank و کش Elsa دو سازوکار جدا هستند. داشتن RegisterCache به‌تنهایی provider کش Elsa را Redis نمی‌کند؛ انتخاب Workflow:Cache:Provider یا UseRedis همچنان لازم است.

۱۵. تنظیم کامل بخش‌های مربوط به لوکال

این نمونه فقط بخش‌های مربوط به اتصال، کش و قفل را نشان می‌دهد. آن را با سایر تنظیمات برنامه ترکیب کنید؛ تنظیمات SQL و سرویس‌های دیگر برنامه همچنان لازم‌اند.

{
  "Redis": {
    "AllowAdmin": true,
    "Ssl": false,
    "ConnectTimeout": 5000,
    "ConnectRetry": 2,
    "Database": 1,
    "ServiceName": "",
    "Hosts": [
      { "Host": "localhost", "Port": 6379 }
    ]
  },
  "Workflow": {
    "Cache": {
      "Enabled": true,
      "Provider": "Redis",
      "Duration": "1.00:00:00",
      "Redis": {
        "Database": 1,
        "KeyPrefix": "nextbank:workflow:cache:"
      }
    },
    "Runtime": {
      "DistributedLocking": {
        "Provider": "Redis",
        "Database": 1,
        "KeyPrefix": "nextbank:workflow:locks:",
        "LockAcquisitionTimeout": "00:00:10"
      }
    }
  }
}
گزینه معنی
AllowAdmin اجازهٔ عملیات مدیریتی Redis در تنظیمات این اتصال
Ssl استفاده از ارتباط TLS برای اتصال Redis
ConnectTimeout زمان انتظار اتصال، به میلی‌ثانیه؛ اینجا پنج ثانیه
ConnectRetry تعداد تلاش مجدد اتصال در تنظیمات کتابخانه
ServiceName خالی استفاده از اتصال مستقیم در این نمونه، بدون Sentinel
Enabled اجازهٔ خواندن و نوشتن payload کش
Provider زیر Cache محل ذخیرهٔ کش ورکفلو
Duration مدت پایهٔ انقضای ورودی‌های کش
Provider زیر DistributedLocking فناوری قفل اجرای مشترک
LockAcquisitionTimeout حداکثر انتظار برای گرفتن قفل؛ اینجا ده ثانیه

قالب مدت کش TimeSpan است:

1.00:00:00 = یک روز
00:30:00  = سی دقیقه
00:05:00  = پنج دقیقه

نوع انقضا را مصرف‌کننده تعیین می‌کند. مثلاً کش Trigger از انقضای ثابت نسبت به زمان ایجاد استفاده می‌کند و کش خواندن Store تعریف‌ها از انقضای تمدیدشونده با خواندن استفاده می‌کند. پس همهٔ کلیدها لزوماً دقیقاً یک روز بعد از اولین ذخیره حذف نمی‌شوند.

در Core، فایل واقعی appsettings.Development.json است. زمان انتظار قفل فعلی Core از تنظیم ده‌ثانیه‌ای مسیر قدیمی در فایل پایه به ارث می‌رسد؛ نمونهٔ بالا همان مقدار را در مسیر جدید صریح کرده است.

در NextBank، تنظیمات محلی در فایل‌های appsettings.development.json قرار دارند و در Git نادیده گرفته می‌شوند. نمونهٔ لوکال بالا، اتصال مستقل را نشان می‌دهد. هنگام اعمال آن روی فایل پایهٔ دارای سه Sentinel، ترکیب آرایهٔ Hosts را طبق فصل بعد در نظر بگیرید.

۱۶. تنظیم سرور و نقش Sentinel

Sentinel برای پیدا‌کردن و پایش Redis master استفاده می‌شود. در تنظیم شما، پورت 26379 متعلق به Sentinelهاست. برنامه از نام سرویس Redis1 برای پیدا‌کردن master مربوط استفاده می‌کند؛ داده‌های کش روی Redis master ذخیره می‌شوند.

در ساختار فعلی NextBank، تنظیمات سرور داخل فایل پایهٔ appsettings.json هر سه میزبان قرار دارند. فایل appsettings.Production.json جداگانه‌ای وجود ندارد. مثال زیر بخش‌های مرتبط را نشان می‌دهد؛ سایر تنظیمات برنامه مانند ConnectionStrings باید در فایل حفظ شوند:

{
  "Redis": {
    "AllowAdmin": true,
    "Ssl": false,
    "ConnectTimeout": 5000,
    "ConnectRetry": 2,
    "Database": 1,
    "ServiceName": "Redis1",
    "Hosts": [
      {
        "Host": "192.168.234.146",
        "Port": 26379
      },
      {
        "Host": "192.168.234.145",
        "Port": 26379
      },
      {
        "Host": "192.168.234.144",
        "Port": 26379
      }
    ]
  },
  "Workflow": {
    "Cache": {
      "Enabled": true,
      "Provider": "Redis",
      "Duration": "1.00:00:00",
      "Redis": {
        "Database": 1,
        "KeyPrefix": "nextbank:workflow:cache:"
      }
    },
    "Runtime": {
      "DistributedLocking": {
        "Provider": "Redis",
        "Database": 1,
        "KeyPrefix": "nextbank:workflow:locks:",
        "LockAcquisitionTimeout": "00:00:10"
      }
    }
  }
}

فایل‌های واقعی عبارت‌اند از:

در هر سه میزبان، کش و قفل از دیتابیس Redis 1 استفاده می‌کنند. مدت پایهٔ کش یک روز و حداکثر انتظار گرفتن قفل Runtime ده ثانیه است. همهٔ نودهایی که روی همان مجموعهٔ ورکفلو کار می‌کنند باید به همان SQL ورکفلو و Redis مشترک وصل باشند.

ترکیب فایل‌ها و آرایهٔ Hosts را درست بفهمیم

فایل appsettings.json پایه ابتدا خوانده می‌شود. اگر فایل مربوط به محیط وجود داشته باشد، مقادیر آن روی فایل پایه اعمال می‌شوند. در ساختار فعلی NextBank، فایل جداگانهٔ Production وجود ندارد؛ محیط Production تنظیمات Sentinel را از فایل پایه دریافت می‌کند. در Development، فایل appsettings.development.json روی پایه اعمال می‌شود.

تنظیمات بر اساس کلید ترکیب می‌شوند. آرایهٔ Hosts نیز کلیدهای شماره‌دار مانند Redis:Hosts:0:Host دارد. اگر فایل پایه سه endpoint و فایل Development یک endpoint داشته باشد، فقط شمارهٔ صفر جایگزین می‌شود؛ شماره‌های یک و دو از پایه باقی می‌مانند. نوشتن یک آرایهٔ کوتاه‌تر به معنی پاک‌شدن آرایهٔ قبلی نیست.

وقتی قصد دارید در لوکال فقط یک Redis فعال باشد، باید اثر endpointهای اضافی پایه را هم بردارید. در helper اتصال فعلی NextBank، Host خالی کنار گذاشته می‌شود، بنابراین این نمونهٔ override قابل استفاده است:

{
  "Redis": {
    "ServiceName": "",
    "Hosts": [
      { "Host": "localhost", "Port": 6379 },
      { "Host": "", "Port": 26379 },
      { "Host": "", "Port": 26379 }
    ]
  }
}

این نمونه روش تنظیم لوکال روی یک فایل پایهٔ دارای سه Sentinel را آموزش می‌دهد. عدد پورتِ ردیف دارای Host خالی مصرف نمی‌شود. در فایل‌های Development فعلی فقط ردیف صفر تعیین شده است؛ پس ترکیب فعلی فایل‌ها هنوز دو endpoint دیگر پایه را نگه می‌دارد. برای تشخیص تنظیم واقعی، مقادیر نهاییِ ترکیب‌شده را بررسی کنید، نه فقط یک فایل را.

متغیرهای محیطی، User Secrets و خط فرمان نیز می‌توانند مقادیر فایل‌ها را تغییر دهند. نام محیط و بزرگی و کوچکی حروف نام فایل، به‌خصوص روی Linux، باید با روش بارگذاری میزبان سازگار باشد. روی سرور باید فایل پایهٔ دارای تنظیمات Sentinel همراه خروجی باشد و override لوکال در محیط Production اعمال نشود.

۱۷. تنظیم از کد چگونه انجام می‌شود؟

در NextBank، ثبت اتصال میزبان و دریافت اتصال معتبر SQL از قبل انجام می‌شود. پس از آن، می‌توانید تنظیمات فایل را مصرف کنید:

builder.Services.RegisterCache(builder.Configuration);
builder.AddWorkflowFramework(connectionString);

connectionString در این مثال، اتصال معتبر SQL ورکفلو است که برنامهٔ میزبان آن را خوانده و در صورت نیاز رمزگشایی کرده است.

برای تعیین صریح بعضی مقادیر کش در callback، شکل فراخوانی چنین است:

using Caspian.WorkflowFramework;
using Elsa.Extensions;
using Microsoft.Extensions.DependencyInjection;
using StackExchange.Redis;

builder.AddWorkflowFramework(connectionString, cache =>
{
    cache.Duration = TimeSpan.FromMinutes(30);
    cache.UseRedis(redis =>
    {
        redis.ConnectionFactory =
            sp => sp.GetRequiredService<IConnectionMultiplexer>();

        redis.Database = 1;
        redis.KeyPrefix = "nextbank:workflow:cache:";
    });
});

sp همان ServiceProvider است. GetRequiredService سرویس ثبت‌شدهٔ اتصال Redis میزبان را دریافت می‌کند. ConnectionFactory مشخص می‌کند provider کش، اتصال را از کجا بگیرد.

در تنظیم فعلی، دریافت اتصال میزبان رفتار پیش‌فرض هم هست؛ نوشتن صریح این factory، تصمیم را در کد نشان می‌دهد.

callback بعد از فایل اجرا می‌شود. مقدارهایی که صریحاً تعیین می‌کنید، بر مقدار فایل اولویت دارند. تنظیماتی که در callback تعیین نشده‌اند، از مسیر قبلی باقی می‌مانند؛ مثلاً انتخاب Redis به‌تنهایی مقدار Enabled=false را true نمی‌کند.

در استفادهٔ مستقیم از Core، این قطعه داخل تنظیم Workflow Management قابل استفاده است:

management.UseCache(cache =>
{
    cache.UseRedis(redis =>
    {
        redis.ConnectionFactory =
            sp => sp.GetRequiredService<IConnectionMultiplexer>();

        redis.Database = 1;
        redis.KeyPrefix = "nextbank:workflow:cache:";
    });
});

این قطعه جایگزین ثبت کامل persistence و سایر قابلیت‌های Core نیست. provider انتخاب‌شده، برای سایر مصرف‌کننده‌های کش همان ماژول هم استفاده می‌شود.

قفل‌های Runtime در Framework فعلی از تنظیمات DistributedLocking خوانده می‌شوند؛ callback بالا تنظیمات کش را تغییر می‌دهد.

۱۸. اگر Memory یا کش غیرفعال بخواهیم چه می‌شود؟

برای Memory می‌توانید تنظیم کنید:

{
  "Workflow": {
    "Cache": {
      "Enabled": true,
      "Provider": "Memory",
      "Duration": "00:30:00"
    }
  }
}

خطوط management.UseCache()، runtime.UseCache() و http.UseCache() تغییر نمی‌کنند. provider مشترکِ انتخاب‌شده Memory خواهد بود، مگر اینکه در callback صریحاً انتخاب دیگری کرده باشید.

برای کنارگذاشتن خواندن و نوشتن payload کش در حالت Redis:

{
  "Workflow": {
    "Cache": {
      "Enabled": false,
      "Provider": "Redis"
    }
  }
}

مدیر کش در این حالت داده را از Store اصلی می‌خواند. هماهنگی تغییرات و مسیرهای HTTP در حالت Redis همچنان برقرار می‌ماند. تغییر provider کش یا Enabled، provider قفل را خودکار تغییر نمی‌دهد.

بعد از تغییر provider یا تنظیمات مربوط، برنامه را دوباره راه‌اندازی کنید. این تنظیمات هنگام راه‌اندازی Featureها مصرف می‌شوند؛ بارگذاری مجدد فایل به‌تنهایی به معنی تعویض زندهٔ provider نیست.

۱۹. چه چیزهایی مشترک و چه چیزهایی محلی می‌مانند؟

داده یا شیء محل در حالت Redis این پیاده‌سازی دلیل
کپی تعریف‌ها و نتیجهٔ جست‌وجوی آن‌ها Redis چند نود به همان اطلاعات تعریف نیاز دارند
کپی Triggerها Redis نتیجهٔ جست‌وجوی Trigger قابل اشتراک است
نتیجهٔ جست‌وجوی ورکفلوی HTTP دادهٔ قابل ذخیره در Redis پیدا‌کردن تعریف و Trigger متناظر قابل کش است
گراف اجرایی از تعریف معتبر در process ساخته می‌شود شیء اجرایی کامل مستقیم در Redis ذخیره نمی‌شود
کد C# کامپایل‌شده، اسکریپت یا قالب آماده حافظهٔ همان process هر process اشیای اجرایی خودش را دارد
جدول مسیرهای HTTP حافظهٔ هر process، با کنترل اعتبار SQL هر نود جدول آمادهٔ خودش را بازسازی می‌کند
تعریف اصلی، نسخه‌ها، وضعیت اجرا و لاگ‌ها SQL دادهٔ اصلی و پایدار سیستم هستند
Bookmarkها SQL اطلاعات انتظار و ادامهٔ اجرای ورکفلو هستند
کلیدهای قفل Redis، در زمان نیاز به قفل هماهنگی عملیات هم‌زمان نودها

نتیجهٔ جست‌وجوی خالی هم می‌تواند کش شود. مثلاً اگر هیچ Triggerی برای یک رویداد پیدا نشد، همین پاسخ خالی می‌تواند جلوی تکرار خواندن دادهٔ مشابه را بگیرد.

فایل‌ها، کش عمومی NextBank، Redis Streamها و سایر قابلیت‌های برنامه تنظیمات خودشان را دارند. اشتراک اتصال Redis به معنی یکی‌شدن همهٔ این قابلیت‌ها نیست.

۲۰. چرا برای کش Redis یک جدول SQL اضافه شد؟

مشکل فقط محل نگه‌داری کپی نیست. باید بدانیم کپی هنوز معتبر است یا نه.

فرض کنید Web نتیجهٔ تعریف A را کش کرده است. Admin تعریف A را تغییر می‌دهد. Web در خواندن بعدی باید بتواند تشخیص دهد که نتیجهٔ قبلی دیگر متعلق به وضعیت جدید نیست.

در این پیاده‌سازی، EFCoreCacheConsistencyStore شمارهٔ اعتبار و وضعیت تغییر ناتمام را از جدول Elsa.CacheMutations در SQL می‌خواند.

این شماره «Revision کش» است. با نسخهٔ تجاری تعریف ورکفلو تفاوت دارد.

قبل از تغییر مرتبط:
    Revision کش = 14

بعد از ثبت و تکمیل تغییر:
    Revision کش = یک مقدار جدید

هنگام خواندن، مدیر کش از Revision فعلی برای ساخت کلید استفاده می‌کند. درخواستی که Revision جدید را از SQL می‌خواند، کلید مربوط به Revision قبلی را مصرف نمی‌کند.

در حالت تغییر ناتمام، خواندن و نوشتن کش مربوط کنار گذاشته می‌شود و Store اصلی خوانده می‌شود. بعد از خواندن داده برای یک Cache Miss نیز وضعیت دوباره بررسی می‌شود؛ فقط اگر وضعیت معتبر مانده باشد، نتیجه در کش قرار می‌گیرد.

این ترتیب، خواندن عادی از کش مشترک را نشان می‌دهد:

خواندن وضعیت اعتبار از SQL
    ↓
ساخت کلید با Revision فعلی
    ↓
خواندن Redis
    ├── نتیجه موجود است: استفاده از نتیجه
    └── نتیجه موجود نیست: خواندن Store اصلی
                              ↓
                        بررسی دوبارهٔ اعتبار
                              ↓
                  ذخیرهٔ نتیجه، اگر هنوز معتبر است

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

برای SQL Server، migration آمادهٔ این تغییر AddCacheMutationFences است. schema مربوط باید پیش از مصرف کش مشترک وجود داشته باشد. برنامه‌ها قابلیت اجرای migration در startup دارند؛ انتخاب SQL مقصد باید هنگام راه‌اندازی بررسی شود.

جدول اعتبار، جای جدول WorkflowDefinitions را نمی‌گیرد. اولی اعتبار کپی موقت را ثبت می‌کند و دومی دادهٔ اصلی تعریف‌ها را نگه می‌دارد.

۲۱. چرا تعداد کلیدهای Redis با تعداد تعریف‌های SQL برابر نیست؟

در startup، کل جدول تعریف‌ها خودکار به Redis کپی نمی‌شود. کش هنگام خواندن اطلاعات پر می‌شود؛ به این روش معمولاً Cache-aside می‌گویند.

درخواست تعداد احتمالی کلید برای همان نتیجه
خواندن یک تعریف با فیلتر مشخص یک کلید
خواندن یک صفحه شامل ۲۰ تعریف یک کلید با مقدار شامل آن صفحه
خواندن همان اطلاعات با فیلتر یا صفحه‌بندی دیگر کلید دیگر
نتیجهٔ خالی یک جست‌وجو یک کلید با مقدار خالی

بنابراین ۲۸ تعریف ممکن است داخل چند نتیجهٔ فهرست قرار بگیرند، یا هنوز همهٔ آن‌ها خوانده و کش نشده باشند. چند کلید هم می‌توانند به جست‌وجوهای متفاوت دربارهٔ یک تعریف مربوط باشند.

شکل کلید دادهٔ کش در Redis چنین است:

nextbank:workflow:cache:v1:{revision}:{hash}
بخش معنی
nextbank:workflow:cache: پیشوند تنظیم‌شده
v1 نسخهٔ قرارداد ساخت کلید
revision شمارهٔ اعتبار کش
hash مقدار محاسبه‌شده از نوع داده و کلید منطقی جست‌وجو

اگر در ابزار Redis پوشه‌های 14، 18 یا 28 می‌بینید، این اعداد در این ساختار Revision هستند. تعداد تعریف‌ها یا شمارهٔ نسخهٔ یک ورکفلو نیستند.

در provider فعلی، مقدار کش به‌صورت Redis Hash ذخیره می‌شود. بخش payload دادهٔ ذخیره‌شده را نشان می‌دهد. مقدار [] یا صفحه‌ای با items خالی، می‌تواند نتیجهٔ معتبرِ یک جست‌وجوی خالی باشد.

در ابزار Redis ممکن است این فیلدها را هم ببینید:

فیلد یا ستون معنی
payload نتیجهٔ ذخیره‌شده، به شکل متن JSON
slide مدت انقضای تمدیدشونده به میلی‌ثانیه؛ صفر یعنی این نوع انقضا تعیین نشده
deadline زمان پایان انقضای ثابت؛ صفر یعنی سقف ثابت تعیین نشده
ستون TTL ابزار زمان باقی‌ماندهٔ واقعی کلید در Redis

صفر بودن deadline به‌تنهایی به معنی بی‌انقضا بودن کلید نیست؛ ممکن است slide برای آن تعیین شده باشد و Redis زمان انقضای فعال داشته باشد.

برای بررسی، مقدار payload و Revision را هم ببینید. شمارش کلیدها به‌تنهایی معیار صحت کش نیست. Refresh، انتخاب Database درست و محدودیت Fetch در ابزار مشاهده نیز روی چیزی که می‌بینید اثر دارند.

۲۲. قفل چه تفاوتی با کش دارد؟

کش به پرسش «اطلاعات را از کجا بخوانیم؟» مربوط است. قفل به پرسش «آیا الان اجازه داریم این عملیات مشترک را انجام دهیم؟» مربوط است.

فرض کنید Web و Workers هر دو بخواهند همان نمونهٔ ورکفلو را ادامه دهند. قفلِ مربوط به آن نمونه باعث هماهنگی این دو عملیات می‌شود.

Web قفل نمونهٔ X را می‌گیرد.
    ↓
Workers برای همان قفل باید منتظر بماند یا طبق عملیاتش نتواند آن را بگیرد.
    ↓
Web عملیات محافظت‌شده را تمام می‌کند و قفل را آزاد می‌کند.
    ↓
نود دیگر می‌تواند برای گرفتن همان قفل تلاش کند.

در DistributedWorkflowClient، اجرای یک نمونه و ادامهٔ اجرای آن با قفل workflow-instance:{WorkflowInstanceId} انجام می‌شوند.

با پیشوند تنظیم‌شده، نام نهایی این است:

nextbank:workflow:locks:workflow-instance:{WorkflowInstanceId}

دو اجرای مستقل از یک تعریف، WorkflowInstanceId متفاوت دارند. این قفل برای همان نمونهٔ مشخص است؛ همهٔ اجراهای یک تعریف را زیر یک قفل عمومی قرار نمی‌دهد.

قفل در مسیرهای دیگری هم استفاده می‌شود:

عملیات کاربرد
انتشار تعریف هماهنگی تغییر وضعیت انتشار همان تعریف
ذخیرهٔ تعریف از API مربوط هماهنگی درخواست‌های هم‌زمان ذخیره
پردازش صف Bookmark هماهنگی پردازش مشترک صف
بعضی taskهای دارای SingleNodeTask هماهنگی اجرای بخش محافظت‌شدهٔ کار بین نودها

runtime.UseDistributedRuntime() قابلیت Runtime توزیع‌شده را فعال می‌کند. UseConfiguredDistributedLocking(...) در میزبان یا Framework، provider قفل و تنظیمات آن را انتخاب می‌کند. runtime.UseCache() هم مصرف‌کنندهٔ کش Runtime را فعال می‌کند. این سه فراخوانی مسئولیت‌های جدا دارند.

۲۳. چرا بعد از اجرای برنامه کلید قفل نمی‌بینیم؟

ثبت provider قفل، به معنی ساخت کلید دائمی قفل نیست. کلید زمانی لازم می‌شود که عملیات محافظت‌شده قفل بگیرد.

ترتیب عادی چنین است:

گرفتن قفل ← انجام عملیات ← آزادشدن قفل و حذف کلید مربوط

یک عملیات کوتاه ممکن است قبل از Refresh کردن ابزار Redis تمام شود. قفل‌های پس‌زمینه هم ممکن است فقط برای مدت کوتاهی دیده شوند.

اگر ورکفلو روی یک Bookmark منتظر کاربر بماند، اجرای فعلی می‌تواند برگردد و قفل آن نوبت آزاد شود. هنگام ادامهٔ اجرا دوباره قفل گرفته می‌شود. افزودن Activity تأخیر به ورکفلو لزوماً باعث نگه‌داشتن قفل در تمام مدت انتظار نمی‌شود.

LockAcquisitionTimeout=00:00:10 یعنی حداکثر انتظار برای گرفتن قفل در عملیات Runtime مربوط ده ثانیه است. مدت نگه‌داشتن قفل یا TTL آن نیست. مدت یک‌روزهٔ کش نیز زمان قفل را تعیین نمی‌کند. بعضی عملیات دیگر، مانند API ذخیرهٔ تعریف، زمان انتظار خودشان را دارند.

چگونه قفل را در Debug ببینیم؟

  1. در DistributedWorkflowClient روی خط var result = await func();، بعد از گرفتن قفل، Breakpoint بگذارید.
  2. یک ورکفلو را از مسیر Runtime توزیع‌شده اجرا کنید.
  3. وقتی روی Breakpoint متوقف شد، بلافاصله Database 1 را در ابزار Redis Refresh کنید.
  4. کلیدهای دارای الگوی nextbank:workflow:locks:* را بررسی کنید.
  5. با ادامهٔ اجرا و پایان عملیات، کلید مربوط باید حذف شود.

این مشاهدهٔ Debug را کوتاه انجام دهید؛ توقف طولانی process روش مناسبی برای آزمون زمان‌بندی و نگه‌داری قفل توزیع‌شده نیست.

۲۴. اگر ارتباط قطع شود، چه اتفاقی می‌افتد؟

وضعیت رفتار مرتبط در پیاده‌سازی فعلی
خواندن کش Redis با خطای Redis مواجه شود خطا ثبت می‌شود و نتیجه از Store اصلی بازسازی می‌شود
نوشتن کش Redis با خطای Redis مواجه شود خطا ثبت می‌شود؛ دادهٔ اصلی و ثبت اعتبار SQL همچنان مرجع‌اند
payload کش قابل خواندن نباشد نتیجه به‌عنوان Cache Miss بازسازی می‌شود
SQLِ اعتبار در دسترس نباشد اعتبار کپی مشترک قابل اثبات نیست و مسیر مربوط می‌تواند خطا بدهد
قفل Redis قابل گرفتن نباشد رفتار گرفتن قفل، انتظار، timeout یا خطای همان عملیات اعمال می‌شود

در حالت انتخاب Redis برای کش Elsa، خطای Redis باعث جایگزینی payload با کش Memory محلی نمی‌شود؛ Store اصلی خوانده می‌شود. رفتار fallback کش عمومی NextBank، رفتار جداگانه‌ای است.

اگر در SQL تغییر ناتمام ثبت شده باشد، کش مربوط موقتاً کنار گذاشته می‌شود. ثبت ناتمام را بدون بررسی وضعیت writer و دادهٔ واقعی SQL کامل یا حذف نکنید. مستند مفصل روش بررسی این وضعیت را توضیح می‌دهد.

هنگام وجود Transaction.Current نیز مسیر عمومی مدیر کش از کش عبور می‌کند و factory اصلی را اجرا می‌کند. بنابراین همهٔ خواندن‌ها الزاماً کلید Redis ایجاد نمی‌کنند.

۲۵. تغییرات هر پروژه را با دلیل مرور کنیم

پروژه یا بخش تغییر اصلی دلیل
Core / Caching قرارداد provider، تنظیمات مشترک و مدیر کش جدید انتخاب Memory یا Redis و امکان توسعهٔ provider
Core / Caching.Redis ذخیرهٔ payload، دیتابیس، پیشوند و انقضا نگه‌داری دادهٔ قابل اشتراک در Redis
Core / Management اتصال کش تعریف‌ها به provider مشترک مصرف همان تنظیمات کش
Core / Runtime کش Trigger و هماهنگی تغییر Bookmark پوشش دادهٔ قابل کش و تازه‌شدن مسیرهای مرتبط
Core / HTTP کش جست‌وجو و کنترل اعتبار جدول مسیرهای محلی هماهنگی lookup و مسیرهای HTTP در نودها
Core / EF Core ثبت پایدار اعتبار و migration SQL Server تشخیص کش قدیمی و تغییر ناتمام
Elsa.Server.Web ثبت اتصال میزبان و خواندن تنظیمات کش و قفل اجرای لوکال Core با Redis
WorkflowFramework خواندن فایل، callback کش و helper قفل رساندن امکانات Core به برنامهٔ مصرف‌کننده
NextBank Web و Admin تنظیم Development و Production و مصرف پکیج جدید اتصال و تنظیم هماهنگ میزبان‌ها
NextBank Workers همان سیاست کش و قفل و ثبت اتصال پیش از Framework استفاده از امکانات مشترک در پردازش پس‌زمینه
Studio در این مرحله تغییر جدیدی برای Redis ندارد کش و قفل در بک‌اند مدیریت می‌شوند

حذف Configure<CachingOptions> ثابت، دلیلش حفظ اولویت تنظیمات فایل و callback بود. حذف شرط ثابت useCaching=true، کنترل payload کش را به تنظیم Enabled منتقل کرد. افزودن http.UseCache، کش lookup HTTP ورودی را فعال کرد.

۲۶. برای اجرا و بررسی عملی چه کنیم؟

اجرای Core از Visual Studio

  1. Redis لوکال باید روی localhost:6379 در دسترس باشد.
  2. Elsa.Server.Web را startup قرار دهید و پروفایل Development را اجرا کنید.
  3. اتصال SQL تنظیم‌شدهٔ Core و schema اعتبار کش باید آماده باشند؛ startup می‌تواند migration را اجرا کند.
  4. از Studio یا API، یک تعریف را بخوانید و همان جست‌وجو را تکرار کنید.
  5. در Redis، Database 1 و پیشوند nextbank:workflow:cache: را بررسی کنید.

تعریف‌ها هنگام نیاز کش می‌شوند. برای دیدن قفل، عملیات محافظت‌شده مثل اجرای نمونه لازم است و کلید آن می‌تواند سریع حذف شود.

اجرای NextBank در لوکال

برای اجرای لوکال، تنظیمات نهایی Redis باید فقط endpoint لوکال مورد نظر را داشته باشند. ترکیب آرایهٔ Hosts را مطابق فصل ۱۶ بررسی کنید. هر سه پروژه در تنظیمات پکیج، نسخهٔ 1.1.0-preview.28 را مصرف می‌کنند.

برای ماشین دیگر یا CI، همان نسخهٔ پکیج و وابستگی‌های لازم باید در یک feed در دسترس باشند. اشاره‌کردن سورس به یک نسخه، به‌تنهایی به معنی انتشار آن نسخه روی registry نیست.

اجرای NextBank روی سرور

همهٔ نودهای مرتبط باید نسخه‌های هماهنگ داشته باشند و به همان SQL ورکفلو، Redis مشترک، Database و پیشوندهای مربوط وصل شوند. در ساختار فعلی، فایل پایهٔ appsettings.json تنظیمات Sentinel را دارد؛ محیط اجرا و overrideهای نهایی باید صحیح باشند.

اگر دو برنامه به دیتابیس‌های Workflow مستقل در SQL وصل هستند، برای کش و قفل آن‌ها فضای مستقل انتخاب کنید. ممکن است دو دیتابیس مستقل شمارهٔ Revision یکسانی داشته باشند؛ شمارهٔ یکسان به معنی دادهٔ یکسان نیست.

تغییر تعریف، نمونهٔ در حال اجرای نسخهٔ قبلی را خودکار به نسخهٔ جدید مهاجرت نمی‌دهد. اعتبار کش و مهاجرت اجرای ورکفلو دو موضوع جدا هستند.

۲۷. عیب‌یابی با چند نشانهٔ ساده

نشانه چه چیزی را بررسی کنیم؟
کلید کش نمی‌بینم Database، پیشوند، Refresh/Fetch، provider نهایی، Enabled و اینکه خواندن قابل کش انجام شده باشد
Redis نصب است ولی کش محلی است RegisterRedis کافی نیست؛ انتخاب Provider=Redis یا UseRedis را بررسی کنید
تنظیم فایل تغییر کرد ولی رفتار ثابت ماند restart برنامه، callback صریح، منابع تنظیمات دیگر و فایل محیط انتخاب‌شده
در لوکال endpointهای سرور هم دیده می‌شوند ترکیب شماره‌های آرایهٔ Hosts در فایل پایه و Development
تعداد کلیدها کمتر از تعریف‌هاست نتیجهٔ query کش می‌شود؛ همهٔ تعریف‌ها در startup پیش‌بارگذاری نمی‌شوند
چند گروه با شماره‌های مختلف می‌بینم Revisionهای مختلف و کلیدهای قدیمی با TTL باقی‌مانده
قفل نمی‌بینم انجام عملیات محافظت‌شده، زمان کوتاه قفل و Database/پیشوند صحیح
فقط کلیدهایی با نتیجهٔ خالی می‌بینم فیلتر واقعی درخواست، نسخهٔ منتشرشده یا draft و payload را بررسی کنید
خواندن جدید کلید ایجاد نمی‌کند تراکنش جاری، ثبت ناتمام SQL و خطاهای Redis را هم بررسی کنید
خطای نبودن سرویس اتصال Redis دارم ثبت IConnectionMultiplexer میزبان یا تنظیم معتبر ConnectionFactory
تغییر یک نود برای بقیه قابل مشاهده نیست یکسان‌بودن SQL، Redis، نسخهٔ پکیج‌ها، Database و پیشوندها

این جدول نقطهٔ شروع بررسی است. از روی یک نشانه، بدون دیدن تنظیمات نهایی و لاگ‌ها، علت قطعی اعلام نمی‌کنیم.

۲۸. اعتبارسنجی این راهنما و محدودهٔ آزمون‌ها

نمونه‌های این راهنما با مقادیر فعلی بخش‌های Redis و Workflow در فایل‌های پایهٔ Web، Admin و Workers تطبیق داده شده‌اند. فایل پایه و Development با Configuration واقعی .NET ترکیب شدند؛ این بررسی شامل اتصال شبکه به Redis یا SQL نیست. مقادیر حساس اتصال SQL در نمونه‌های آموزشی نمایش داده نشده‌اند.

در گزارش آزمون مرحلهٔ قبل با پکیج preview.27، سه ServiceProvider و اتصال مستقل ساخته شدند. اشتراک payload کش بین Web و Workers و جلوگیری از گرفتن همان قفل توسط Admin و Workers، تا زمان آزادشدن آن توسط Web، تأیید شد. این گزارش مربوط به پیکربندی همان مرحله است و آزمون اجرایی جدیدِ نسخهٔ preview.28 یا تنظیمات فعلی محسوب نمی‌شود.

در مرحلهٔ تغییر زیرساخت Core نیز ۲۲ تست کش، ۹ تست Runtime و سناریوهای process مستقل موفق گزارش شدند. آزمون‌های اجرایی آن مرحله Redis ایزوله و SQLite مشترک داشتند. از این نتایج، اجرای migration روی SQL Server عملیاتی، عملکرد واقعی چندسروری یا موفقیت failover Sentinel نتیجه‌گیری نمی‌شود.

برای بررسی اجرای واقعی، تنظیمات نهایی میزبان، لاگ‌ها و آزمون‌های همان محیط را مبنا قرار دهید. جزئیات زیرساخت و آزمون‌ها در مستند مفصل آمده است.

۲۹. چند تمرین برای اطمینان از فهم موضوع

سؤال پاسخ
فقط cache.RegisterRedis() نوشته‌ام. آیا Redis انتخاب شده؟ هنوز انتخاب لازم است: Provider=Redis در فایل یا UseRedis در کد.
runtime.UseCache() بدون نام Redis دارم. از کجا provider را می‌فهمد؟ از CacheFeature مشترک همان ماژول و مدیر کش ثبت‌شده.
Provider کش را Memory کردم. آیا قفل هم File می‌شود؟ provider قفل مستقل تنظیم می‌شود.
Duration را سی دقیقه کرده‌ام، ولی callback یک ساعت تعیین کرده است. کدام اعمال می‌شود؟ مقدار صریح callback که بعد از فایل اجرا می‌شود.
یک کلید، صفحه‌ای شامل ۲۰ تعریف دارد. آیا برای آن صفحه ۲۰ کلید لازم است؟ همان نتیجهٔ صفحه می‌تواند در یک کلید ذخیره شود.
LockAcquisitionTimeout ده ثانیه است. آیا قفل بعد از ده ثانیهٔ عملیات حتماً آزاد می‌شود؟ این گزینه حداکثر انتظار گرفتن قفل است؛ مدت عملیات را تعیین نمی‌کند.
دو node روی دو سرور هرکدام به localhost وصل‌اند. آیا کش مشترک دارند؟ فقط اگر اتصال‌ها در نهایت به سرویس مشترک هدایت شده باشند؛ localhost عادی هر سرور به خود همان سرور اشاره می‌کند.

۳۰. اگر می‌خواهید کد را بخوانید، از این فایل‌ها شروع کنید

فایل چه چیزی در آن ببینیم؟
Framework DependencyInjection ترتیب انتخاب کش و فعال‌کردن مصرف‌کننده‌ها
CacheFeature انتخاب provider، Enabled، Duration و ثبت مدیر کش
CacheConsumerFeature اتصال مصرف‌کننده‌ها به تنظیم مشترک
ICacheProvider قرارداد خواندن، نوشتن و حذف
ProviderCacheManager Cache Hit/Miss، کنترل اعتبار و ساخت کلید
RedisCacheFeatureExtensions تفاوت RegisterRedis و UseRedis
RedisCacheProvider ذخیرهٔ payload و انقضای Redis
NextBank RegisterCache ساخت اتصال میزبان از بخش اصلی Redis
Framework helper قفل خواندن تنظیمات قفل، دیتابیس، پیشوند و زمان انتظار
Core host helper قفل تنظیم قفل هنگام اجرای مستقیم Core

برای افزودن provider جدیدِ کش، قرارداد ICacheProvider را پیاده‌سازی کنید، نام و روش تنظیم آن را در Feature ثبت کنید و رفتار ذخیره، انقضا و سازگاری با کش مشترک را بررسی کنید. مصرف‌کننده‌های UseCache همچنان از مسیر مشترک استفاده خواهند کرد. جزئیات توسعهٔ provider در راهنمای مفصل آمده است.