رفتن به متن راهنما
همه‌ی راهنماها

حالت مشترک با Redis

محدودیت نرخ، مدارشکن، سقف هم‌زمانی، سهمیهٔ تلاش مجدد و دورهٔ انتظار را به یک سرور Redis ببرید تا همهٔ پردازه‌ها یک سهمیه و یک مدار باز ببینند.

برنامه Reqtide PHP 0.1.0آخرین بررسی زمان خواندن 21 دقیقه

در این صفحه

بستهٔ enaxon/reqtide-redis نسخهٔ 0.x (بتا) است. روی خط فرمان، در Docker و در CI آزموده شده است. Sentinel، Redis Cluster، Valkey و replica پشتیبانی نمی‌شوند. برای PHP-FPM سه lane در CI وجود دارد (PHP 8.3، 8.4 و 8.5 با nginx و شش worker): حالت بین درخواست‌ها و workerها مشترک است. محدودیت‌های Reqtide را ببینید.

بلوک‌های کد این صفحه از packages/redis/examples (opens in a new tab) آمده‌اند. خروجی‌های نمایش‌داده‌شده، خروجی ثبت‌شدهٔ همان اسکریپت‌ها روی یک Redis محلی است.

چه وقت به حالت مشترک نیاز است

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

بستهٔ Redis حالت پنج سیاست را به یک سرور Redis منتقل می‌کند: محدودیت نرخ، مدارشکن، سقف اجرای هم‌زمان (Bulkhead)، سهمیهٔ تلاش مجدد و دورهٔ انتظار. هر پردازه‌ای که از یک رجیستری (registry) مشترک استفاده کند، از همان سهمیه برمی‌دارد و همان مدار باز را می‌بیند.

چه زمانی لازم است

  • چند worker یا چند سرور یک API را صدا می‌زنند و محدودیت API برای مجموع آن‌ها اعمال می‌شود.
  • یک وابستگی کند باید مدار را هم‌زمان برای همهٔ پردازه‌ها باز کند.
  • سرور مقصد خواسته است صبر کنید و همهٔ پردازه‌ها باید صبر کنند.

چه زمانی لازم نیست

  • یک پردازهٔ طولانی‌مدت همهٔ فراخوانی‌ها را انجام می‌دهد، مثل یک worker تنها، یک کار خط فرمان یا یک daemon. پایپ‌لاین درون‌حافظه کافی است.
  • نمی‌توانید بپذیرید که با از دست رفتن داده‌های Redis، محافظت هم از بین برود. این بسته در برابر از دست رفتن داده دوام نمی‌آورد. بخش چه چیزی از بین می‌رود را ببینید.

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

نصب

با دستور composer require enaxon/reqtide enaxon/reqtide-redis نصب کنید. هر دو بسته باید نسخهٔ یکسان داشته باشند. نصب را ببینید.

کلون را یک بار برای بستهٔ اصلی و یک بار برای پوشهٔ این بسته ثبت کنید. گزینهٔ symlink: false باعث می‌شود Composer بسته‌ها را داخل vendor/ کپی کند:

JSON
{
    "repositories": [
        { "type": "path", "url": "../reqtide", "options": { "symlink": false } },
        { "type": "path", "url": "../reqtide/packages/redis", "options": { "symlink": false } }
    ]
}
Bash
composer require "enaxon/reqtide:@dev" "enaxon/reqtide-redis:@dev"

نام vendor برابر enaxon است. نام بسته را دقیقاً کپی کنید. این بسته به PHP نسخهٔ 8.3 یا بالاتر، افزونهٔ openssl و predis/predis نسخهٔ 3.7 یا بالاتر از شاخهٔ 3.x نیاز دارد. همان نسخهٔ enaxon/reqtide را هم می‌خواهد، پس بستهٔ اصلی از مخزن اول می‌آید.

نام بستهٔ اصلی را در دستور هم بنویسید. آداپتور همان نسخهٔ dev بستهٔ اصلی را لازم دارد. پروژه‌ای که حداقل پایداری (minimum stability) پیش‌فرض را دارد، بدون این کار آن را رد می‌کند. این دستور در 2026-10-11 با Composer 2 روی PHP 8.3 در Docker، از یک کلون و با همین مخزن‌های path آزموده شد.

راه‌اندازی در چهار گام

میزبان گام‌های ۱، ۲ و ۴ را انجام می‌دهد و برنامه گام ۳ را.

گام ۱: رجیستری را بسازید

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

PHP
$registry = new RedisRegistry('example-' . bin2hex(random_bytes(6)), 'example-shared-limit', random_bytes(32));
$admin = new RedisRegistryAdmin(connection(), $registry);
$admin->initialize();

$policy = $admin->inspectServer();
check($policy->protectsState(), 'the server must use maxmemory-policy noeviction');
  • آرگومان اول پیشوند کلید است. همهٔ کلیدهای رجیستری با آن شروع می‌شوند.
  • آرگومان دوم هویت رجیستری است: 1 تا 64 حرف، رقم، نقطه، زیرخط یا خط تیره. اگر رجیستری با هویت یا سقف‌های دیگری وجود داشته باشد، initialize() آن را نمی‌پذیرد.
  • آرگومان سوم رازی با 32 بایت یا بیشتر است. Reqtide از آن برای هش کردن نام tenant و subject در کلیدها استفاده می‌کند. راز را تا پایان عمر رجیستری نگه دارید، چون با راز تازه همهٔ کلیدهای هش‌شده عوض می‌شوند. مقداری را که جای دیگری اعتبارنامه است به کار نبرید. مثال از random_bytes(32) استفاده می‌کند، چون در پایان رجیستری را دور می‌اندازد.
  • دو سقف اختیاری maxPartitions (پیش‌فرض 10,000) و maxRecords (پیش‌فرض 20,000) هستند. هر کدام بین 1 و 1,000,000 است. بخش سقف‌ها را ببینید.

می‌توانید initialize() را با همان مقدارها دوباره صدا بزنید. connection() و check() کمک‌تابع‌های مثال‌اند و در support.php قرار دارند. connection() گزینه‌های اتصال را از محیط مثال می‌سازد. در پروژهٔ خودتان RedisConnectionOptions را با کاربر مدیریت می‌سازید. بخش‌های تنظیمات سرور و TLS را ببینید.

گام ۲: سرور را بررسی کنید و کاربرها را بسازید

متد inspectServer() مقدارهای maxmemory-policy و maxmemory را می‌خواند. وقتی سیاست noeviction باشد یا سرور سقف حافظه نداشته باشد، protectsState() مقدار true برمی‌گرداند. اگر سرور بتواند کلیدها را پاک کند، false برمی‌گرداند. اگر سیاست خوانده نشود، مثلاً چون کاربر اجازهٔ اجرای CONFIG GET ندارد، null برمی‌گرداند. مثال تا وقتی پاسخ true نباشد متوقف می‌شود. دلیلش در تنظیمات سرور آمده است.

سپس برای برنامه یک کاربر Redis بسازید که فقط قاعده‌های RedisAcl::execution($registry) را دارد. برای میزبان کاربری با RedisAcl::administration($registry) بسازید. توکن‌های دقیق رجیستری خودتان را چاپ کنید:

PHP
echo implode(' ', RedisAcl::execution($registry));

نتیجه شکلی شبیه این دارد. فهرست فرمان‌ها در اینجا کوتاه است و از کتابخانه می‌آید، پس آن را چاپ کنید و از این صفحه کپی نکنید:

Text
ACL SETUSER app on ><password> resetkeys resetchannels -@all +evalsha +script|load +hset ... ~billing-prod:*

این کاربر فقط می‌تواند اسکریپت‌های همراه بسته و فرمان‌هایی را اجرا کند که آن اسکریپت‌ها به کار می‌برند، آن هم فقط روی کلیدهای زیر پیشوند رجیستری. کاربر مدیریت همین قاعده‌ها را دارد و چهار فرمان دیگر: SCAN و UNLINK برای پاک‌سازی رجیستری بازنشسته، CONFIG GET برای inspectServer() و INFO برای بررسی نسخهٔ سرور.

گام ۳: پایپ‌لاین را بسازید

پایپ‌لاین را به‌جای compileInMemory() روی محیط Redis بسازید. ساختن محیط هیچ اتصالی باز نمی‌کند. تا اولین عملیات چیزی خوانده یا نوشته نمی‌شود. backend فقط برای سیاست‌هایی ساخته می‌شود که تعریف پایپ‌لاین پیکربندی کرده است.

PHP
/** What each "process" does: build the same pipeline on the Redis environment. */
$build = static function () use ($registry) {
    $builder = PipelineBuilder::profile('shared-limit', 'v1')
        ->partition('example', 'local', 'reports-api')
        ->attemptTimeout(Duration::seconds(2), AttemptTimeoutRequirement::Cooperative)
        // Three tokens, one refilled per hour: three calls for the life of this script, across both pipelines.
        ->rateLimit(PipelineBuilder::tokenBucket(3, 1, Duration::seconds(3600)));

    return $builder->compile(RedisEnvironment::for($builder->definition(), ExceptionClassifier::create(), connection(), $registry));
};

طبقه‌بند (classifier) را مثل compileInMemory() خودتان می‌دهید. ساعت هر تصمیم، ساعت سرور Redis است، پس ساعت میزبان‌های PHP برای حالت اهمیتی ندارد.

گزینه‌های اتصال به‌ترتیب این مقدارها را می‌گیرند: میزبان، مهلت اتصال، مهلت خواندن و نوشتن (برحسب ثانیه)، سپس درگاه، کاربر و گذرواژه. هر دو مهلت الزامی‌اند، باید بزرگ‌تر از صفر و حداکثر 3,600 ثانیه باشند. نام کاربر به گذرواژه نیاز دارد. گذرواژه هرگز در var_dump دیده نمی‌شود و هرگز در پیام استثنا نمی‌آید. این گزینه‌ها سریال‌سازی را نمی‌پذیرند.

متد RedisEnvironment::explain() پایپ‌لاین کامپایل‌شده و حالت مشترک آن را توصیف می‌کند. هیچ فراخوانی شبکه‌ای انجام نمی‌دهد. دامنهٔ حالت، ساعتی که تصمیم می‌گیرد، مهلت I/O، TLS، هویت رجیستری و سقف‌های آن را نشان می‌دهد. میزبان، گذرواژه، پیشوند کلید و راز را هرگز چاپ نمی‌کند.

PHP
$connection = new RedisConnectionOptions('127.0.0.1', 0.5, 1.5, 1, 'app', 'not-a-real-password');
$registry = new RedisRegistry('example-explain', 'example-explain', random_bytes(32), 500, 900);
PHP
$explanation = RedisEnvironment::explain($pipeline, $connection, $registry);
say("profile: {$explanation->profileId->value} {$explanation->profileVersion->value}");
say('enabled policies: ' . implode(', ', $explanation->enabledPolicies));
$text = '';
foreach ($explanation->capabilityNotes as $note) {
    say("  {$note}");
    $text .= $note . "\n";
}
Text
profile: explain-shared v1
enabled policies: attempt, retry(maxAttempts=1), rate_limit, concurrency
  invocation=synchronous
  settlement=inline_return_or_throw
  attempt_timeout=unsupported
  cancellation=checkpoint_only
  scheduling=bounded_blocking
  queue_ownership=none
  detached_completion=unsupported
  nonblocking=not_claimed
  physical_stop=not_guaranteed
  require_finite_backend_io=false
  state_scope=shared_authority
  state_clock=redis_server
  state_io_timeout_seconds=1.5
  state_tls=false
  registry_identity=example-explain
  registry_schema=1
  registry_max_partitions=500
  registry_max_records=900
ok

گام ۴: نگهداری را اجرا کنید

برای نگهداری، (new RedisRegistryMaintenance($connection, $registry))->step() را از یک worker یا یک زمان‌بند اجرا کنید. تا وقتی نتیجه more می‌گوید، دوباره آن را صدا بزنید. چیزی آن را خودکار اجرا نمی‌کند. بخش نگهداری را ببینید.

یک مثال قابل اجرا

مثال دو پایپ‌لاین را جداگانه می‌سازد. هر کدام اتصال Redis خودش را دارد، مثل دو پردازهٔ PHP. هر دو یک رجیستری را به اشتراک می‌گذارند و از یک سطل توکن (token bucket) سه‌تایی برمی‌دارند. مهم نیست کدام پایپ‌لاین درخواست بدهد، فقط سه فراخوانی پاسخ می‌گیرد.

قطعه‌کدهای این صفحه این importها را لازم دارند:

PHP
use Enaxon\Reqtide\Error\ProblemCarrier;
use Enaxon\Reqtide\Exception\AdmissionDeferredException;
use Enaxon\Reqtide\Exception\AdmissionRejectedException;
use Enaxon\Reqtide\Execution\AttemptContext;
use Enaxon\Reqtide\Outcome\ExceptionClassifier;
use Enaxon\Reqtide\Pipeline\PipelineBuilder;
use Enaxon\Reqtide\Policy\AttemptTimeoutRequirement;
use Enaxon\Reqtide\Redis\RedisEnvironment;
use Enaxon\Reqtide\Redis\RedisRegistry;
use Enaxon\Reqtide\Redis\RedisRegistryAdmin;
use Enaxon\Reqtide\Value\Duration;

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

PHP
$served = ['A' => 0, 'B' => 0];
$callable = static function (string $who) use (&$served): \Closure {
    return static function (AttemptContext $attempt) use ($who, &$served): string {
        ++$served[$who];

        return 'served';
    };
};

$pipelines = ['A' => $build(), 'B' => $build()];
$sequence = ['A', 'B', 'A', 'B', 'A'];
foreach ($sequence as $i => $who) {
    $pipeline = $pipelines[$who];
    $number = $i + 1;
    try {
        $pipeline->execute($callable($who), $pipeline->newContext(Duration::seconds(30), operationId: 'shared-limit-' . $number));
        say("call {$number} via {$who}: served");
    } catch (AdmissionDeferredException|AdmissionRejectedException $e) {
        check($e instanceof ProblemCarrier, 'a refusal carries a Problem');
        say("call {$number} via {$who}: refused ({$e->problem()->code->value})");
    }
}
say("served by A: {$served['A']}, served by B: {$served['B']}");
check($served['A'] + $served['B'] === 3, 'exactly three calls were served across both pipelines');

$admin->retire();
$admin->purgeRetired();
say('ok');

از یک کلون مخزن، این دستور Redis 7.4.11 را در Docker بالا می‌آورد، سه اسکریپت مثال را اجرا می‌کند و خروجی آن‌ها را با فایل‌های ثبت‌شده مقایسه می‌کند:

Bash
tools/run-redis-examples.sh

خروجی:

Text
call 1 via A: served
call 2 via B: served
call 3 via A: served
call 4 via B: refused (rate.exhausted)
call 5 via A: refused (rate.exhausted)
served by A: 2, served by B: 1
ok

فراخوانی‌های 1 تا 3 پاسخ می‌گیرند، دو تا از طریق پایپ‌لاین A و یکی از طریق B. فراخوانی‌های 4 و 5 با کد مشکل rate.exhausted رد می‌شوند و callable آن‌ها هرگز اجرا نمی‌شود. دو پایپ‌لاین در PHP هیچ چیز مشترکی ندارند. سهمیه در Redis است.

تنظیمات سرور

  • حداقل نسخه: Redis 7.0. پروژه با Redis 7.4.11 و 8.2.10 آزمون می‌شود. اسکریپت‌ها از redis.acl_check_cmd استفاده می‌کنند که Redis 7.0 اضافه کرده است. متدهای initialize() و inspectServer() سرور قدیمی‌تر را با InvalidConfigurationException رد می‌کنند. روی سرور قدیمی‌تر، هر اسکریپت پیش از اجرای اولین فرمانش شکست می‌خورد.
  • سیاست maxmemory-policy noeviction را تنظیم کنید. اگر سیاست حافظه کلیدها را خودکار حذف کند (eviction)، سرور می‌تواند یک پارتیشن را بدون هیچ علامتی پاک کند و محافظت آن پارتیشن از صفر شروع شود. با noeviction، سرور پر نوشتن را رد می‌کند و فراخوانی با یک شکست روشن تمام می‌شود. فقط از دست رفتن خود رجیستری گزارش می‌شود.
  • حالت در حافظهٔ Redis است. دربارهٔ راه‌اندازی مجدد، بخش چه چیزی از بین می‌رود را ببینید.
  • سرور مستقل، RESP2، یک اتصال مستقیم. گزینه‌ای برای تلاش مجدد و اتصال پایدار (persistent) وجود ندارد. هیچ فرمانی دو بار فرستاده نمی‌شود.
  • اسکریپت‌ها با خلاصه (digest) فرستاده می‌شوند. کتابخانه متن اسکریپت را فقط وقتی با SCRIPT LOAD بارگذاری می‌کند که سرور حافظهٔ نهان اسکریپت‌هایش را از دست داده باشد.

TLS

در گزینه‌های اتصال tls: true را بگذارید. بدون آن، گذرواژه و همهٔ فرمان‌ها به‌صورت متن ساده روی شبکه می‌روند؛ پس روی هر شبکه‌ای که کاملاً زیر کنترل شما نیست، از TLS استفاده کنید. گواهی سرور و نام میزبان همیشه بررسی می‌شوند و هیچ گزینه‌ای این بررسی را خاموش نمی‌کند. دو گزینهٔ دیگر هم هست:

  • گزینهٔ caFile مسیر فایل CA است، برای CA خصوصی. این فایل باید وجود داشته باشد، وگرنه گزینه‌ها InvalidConfigurationException پرتاب می‌کنند.
  • گزینهٔ tlsServerName نامی است که وقتی با آدرس وصل می‌شوید باید بررسی شود.

گزینه‌ها caFile یا tlsServerName را بدون tls: true نمی‌پذیرند. یک مثال:

PHP
new RedisConnectionOptions('redis.internal', 1.0, 0.5, 6379, 'app', $password, tls: true, caFile: '/etc/ssl/redis-ca.pem')

اتصال TLS را به‌صورت محلی روی سرورهایی آزموده‌ایم که برای هر اجرا یک CA آزمایشی داشتند. رد شدن TLS، گذرواژهٔ اشتباه و نبودن یک قاعدهٔ ACL، همه با StateUnavailableException تمام می‌شوند.

وقتی مشکلی پیش می‌آید

کتابخانه هرگز به شکست یک فراخوانی حالت با حالت محلی تازه پاسخ نمی‌دهد. هرگز یک فرمان را دو بار نمی‌فرستد. هر شکست را به یکی از استثناهای زیرساختی Core نگاشت می‌کند. callable محافظت‌شده اجرا نمی‌شود، مگر اینکه جدول چیز دیگری بگوید.

چه شدچه چیزی می‌گیریدچه نوشته شد
سرور در دسترس نیست، TLS رد شده است، گذرواژه اشتباه است، کاربر قاعدهٔ لازم را ندارد یا سرور پر است (noeviction)استثنای StateUnavailableException. callable اجرا نمی‌شود.هیچ چیز
اتصال پس از ارسال یک فرمان قطع شد یا مهلتش تمام شدبرای نوشتن IndeterminateStateException و برای خواندن StateUnavailableException. کتابخانه فرمان را دوباره نمی‌فرستد.برای نوشتن نامعلوم است: ممکن است یک بار اعمال شده باشد
رجیستری وجود ندارداستثنای StateUnavailableExceptionهیچ چیز. رجیستری ساخته نمی‌شود.
داده‌های ذخیره‌شده خراب استاستثنای CorruptStateExceptionهیچ چیز از این فراخوانی. پاک‌سازی حالت منقضی که پیش از پیدا شدن خرابی انجام شده، می‌ماند و شمارنده‌های رجیستری با آن هماهنگ می‌مانند.
کلیدی زیر پیشوند مقداری از نوع نادرست دارداستثنای CorruptStateExceptionهیچ چیز از این فراخوانی
به سقف رجیستری رسیده‌ایداستثنای StateCapacityExceededException، بعد از اینکه کتابخانه حالت منقضی را پس گرفت و قدیمی‌ترین رکوردهای تمام‌شده را بیرون انداختهیچ چیز
هر شکست حالت چگونه تمام می‌شود و کتابخانه چه چیزی می‌نویسد.

اینکه پس از یک نتیجهٔ نامطمئن چه بر سر عملیات می‌آید، به مرحله بستگی دارد:

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

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

نگهداری

متد RedisRegistryMaintenance::step() حالتی را پاک می‌کند که دیگر از چیزی محافظت نمی‌کند. چیزی آن را خودکار صدا نمی‌زند. هر گام در یک فراخوانی اتمی اسکریپت، حداکثر 128 رکورد تمام‌شده و حداکثر 128 نامزد را بررسی می‌کند. این‌ها را پاک می‌کند:

  • رکوردهای تمام‌شده (tombstoneهای نهایی‌سازی و رکوردهای تمام‌شدهٔ سهمیهٔ تلاش مجدد و مدار) پس از نگهداری 120 ثانیه‌ای؛
  • پارتیشن‌های محدودیت نرخ و سهمیهٔ تلاش مجدد که با حالت تازه برابرند، پس از 120 ثانیه بدون نوشتن؛
  • اجاره‌های منقضی (lease) در پارتیشن‌های سقف هم‌زمانی، و خود پارتیشن وقتی چیزی در آن نمانده باشد؛
  • دوره‌های انتظاری که پایان و مهلت پاک‌سازی آن‌ها گذشته است؛
  • مدارهای بستهٔ بدون تاریخچه، مجوز یا رکورد تمام‌شده، پس از 120 ثانیه بدون نوشتن.

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

جایگاه ذخیره‌شده همان سر صف و ترتیب فهرست است. هر گام از جایی ادامه می‌دهد که گام قبلی ایستاده، روی هر ماشین و پس از هر راه‌اندازی مجدد. گامی که more گزارش کند، یک دستهٔ کامل پیدا کرده و چیزی حذف کرده است؛ پس دوباره آن را صدا بزنید تا more برابر false شود. رکوردهایی که از نظر ثانیه موعدشان رسیده ولی از نظر لحظهٔ دقیق هنوز نه، more را روشن نمی‌کنند. به این ترتیب یک حلقه در یک ثانیه کار یکسان را تکرار نمی‌کند و گام زمان‌بندی‌شدهٔ بعدی آن‌ها را حذف می‌کند. فیلد damaged پارتیشن‌هایی را می‌شمارد که گام نتوانسته بخواند. آن‌ها دست‌نخورده می‌مانند و هر درخواستی که آن‌ها را بخواند با CorruptStateException شکست می‌خورد. مقدار بیشتر از صفر یعنی اپراتور باید وارد عمل شود: کلیدها را بررسی کند و اگر خرابی توضیحی ندارد، رجیستری را بازنشسته و پاک‌سازی کند. یک گام هرگز کل رجیستری را پیمایش نمی‌کند. هیچ رویدادی نمی‌نویسد، هیچ تصمیمی را عوض نمی‌کند و رجیستری گم‌شده را نمی‌سازد.

کارهای اپراتور

میزبان این کارها را با اتصال خودش اجرا می‌کند. کتابخانه هرگز آن‌ها را صدا نمی‌زند.

کارفراخوانی
بستن یک مدار و کهنه کردن مجوزهایشمتد RedisCircuitAdmin::reset($policyIdentity)
بازنشسته کردن یک رجیستریمتد RedisRegistryAdmin::retire() و بعد purgeRetired() برای پاک کردن باقی‌مانده
بررسی رجیستری بدون نوشتنمتد RedisRegistryAdmin::verify()
بررسی سیاست سرورمتد RedisRegistryAdmin::inspectServer() (به CONFIG GET و INFO نیاز دارد)
کارهای اپراتور و فراخوانی هر کدام.
  • متد reset() مدار را می‌بندد، تاریخچه و همهٔ مجوزهای فعال آن را دور می‌ریزد و آن را به نسل تازه می‌برد. نسل تازه را برمی‌گرداند، یا اگر پارتیشن وجود نداشته باشد، null. پاسخ گم‌شده یعنی نتیجه نامعلوم است و reset هرگز تکرار نمی‌شود. هویت همان هویت سیاست مدار است، مثلاً $builder->definition()->circuit->identity.
  • متد retire() فراداده‌های رجیستری را پاک می‌کند. کلیدهای پارتیشن و رکورد می‌مانند و وضعیت برگشتی نشان می‌دهد چند تا. متد purgeRetired() آن‌ها را صفحه‌صفحه پاک می‌کند و تعداد کلیدهای پاک‌شده را برمی‌گرداند. تا وقتی رجیستری وجود دارد اجرا نمی‌شود. پس از یک وقفه دوباره اجرایش کنید.
  • متد initialize() پیشوندی را که هنوز فهرست یا صف tombstone یک رجیستری بازنشسته را دارد، رد می‌کند. ابتدا پاک‌سازی کنید.

چه چیزی با راه‌اندازی مجدد یا حذف کلید از بین می‌رود

حالت در حافظهٔ Redis است.

  • راه‌اندازی مجدد بدون ماندگاری (persistence)، flush یا failover آن را از بین می‌برد. موجودی سهمیه، مدارهای باز، اجاره‌ها و سهمیه‌های تلاش مجدد هم شامل این می‌شوند.
  • رجیستری گم‌شده هر فراخوانی محافظت‌شده را با StateUnavailableException رد می‌کند، تا وقتی که دوباره آن را مقداردهی اولیه کنید. Reqtide هرگز حالت تازه را جای حالت گم‌شده به کار نمی‌برد.
  • پارتیشن گم‌شده در رجیستری سالم گزارش نمی‌شود. محافظت آن پارتیشن از صفر شروع می‌شود. این وقتی رخ می‌دهد که سرور کلیدها را حذف کند، و برای همین noeviction را تنظیم می‌کنید.
  • شمارنده‌های رجیستری و جایگاه ذخیره‌شدهٔ پاک‌سازی بخشی از حالت‌اند، پس همراه آن از بین می‌روند.

محافظتی که در برابر از دست رفتن داده در راه‌اندازی مجدد یا failover دوام بیاورد، برای نسخهٔ اول برنامه‌ریزی نشده است.

سقف‌ها

محدودیتمعنی
فقط سرور مستقلیک سرور Redis و یک اتصال مستقیم. Sentinel، replica و Valkey آزموده نشده‌اند و پشتیبانی نمی‌شوند.
خوشهٔ Redis Clusterعمداً پشتیبانی نمی‌شود. اسکریپت‌ها علاوه بر کلیدهایی که به آن‌ها داده می‌شود، به کلیدهایی هم دست می‌زنند که خودشان می‌سازند: عضوهای فهرست رجیستری و ورودی‌های صف tombstone. الگوی ACL با نام ~prefix:* این کلیدها را می‌پوشاند.
نسخهٔ Redis 7.0 یا بالاترسرورهای قدیمی‌تر رد می‌شوند. روی 7.4.11 و 8.2.10 آزموده شده است.
اجرا با PHP-FPMسه lane در CI: PHP 8.3، 8.4 و 8.5، هر کدام با nginx و شش worker. فراتر از این lane‌ها ادعایی نمی‌شود.
هزینهٔ یک فراخوانیفراخوانی اسکریپت اتمی است و تا پایان اجرا سرور را مسدود می‌کند. بیشتر فراخوانی‌ها به چند کلید دست می‌زنند. دو مسیر به نسبت رکوردهای یک پارتیشن زمان می‌برند: مداری که وضعیتش عوض می‌شود همهٔ مجوزهای باقی‌ماندهٔ خود را پاک می‌کند، و پارتیشن سقف هم‌زمانی اجاره‌های منقضی را پاک می‌کند.
سقف maxRecordsاین دو مسیر را محدود می‌کند. می‌تواند تا 1,000,000 باشد. آن را تا جایی که بار اجازه می‌دهد پایین نگه دارید.
پنج سیاستمحدودیت نرخ، مدارشکن، سقف هم‌زمانی، سهمیهٔ تلاش مجدد و دورهٔ انتظار. مهلت کل، تأخیر بین تلاش‌های مجدد و مهلت هر تلاش در هر پردازه محلی می‌مانند.
سقف‌های بستهٔ Redis.

نمایش شکست وقتی Redis از کار افتاده است

مثال دوم به سرور نیاز ندارد. به درگاهی اشاره می‌کند که هیچ چیز روی آن گوش نمی‌دهد. Reqtide نمی‌تواند حالت را بخواند، پس نمی‌تواند تصمیم بگیرد فراخوانی مجاز است یا نه. callable اجرا نمی‌شود و یک استثنا با نوع مشخص می‌گیرید.

Bash
docker compose -f tools/docker/compose.yaml run --rm php83 php packages/redis/examples/02-redis-down-failure-demo.php
PHP
$unreachable = new RedisConnectionOptions('127.0.0.1', 0.5, 0.5, 1);
$registry = new RedisRegistry('example-down', 'example-down', random_bytes(32));

$builder = PipelineBuilder::profile('redis-down', 'v1')
    ->partition('example', 'local', 'reports-api')
    ->attemptTimeout(Duration::seconds(2), AttemptTimeoutRequirement::Cooperative)
    ->rateLimit(PipelineBuilder::tokenBucket(3, 1, Duration::seconds(3600)));
$pipeline = $builder->compile(RedisEnvironment::for($builder->definition(), ExceptionClassifier::create(), $unreachable, $registry));

گزینه‌های اتصال به درگاه 1 میزبان محلی اشاره می‌کنند، با مهلت نیم‌ثانیه. پایپ‌لاین محدودیت نرخ دارد، پس هر فراخوانی به حالت نیاز دارد:

PHP
$invocations = 0;
$work = static function (AttemptContext $attempt) use (&$invocations): string {
    ++$invocations;

    return 'done';
};

try {
    $pipeline->execute($work, $pipeline->newContext(Duration::seconds(30), operationId: 'redis-down-1'));
    check(false, 'a call with no reachable state must not succeed');
} catch (StateUnavailableException $e) {
    say('execute(): ' . $e::class);
}
say("callable invocations: {$invocations}");
check($invocations === 0, 'the callable was not run');

$completion = $pipeline->executeDetailed($work, $pipeline->newContext(Duration::seconds(30), operationId: 'redis-down-2'));
say('detailed disposition: ' . $completion->disposition->value);
check($completion->disposition !== CompletionDisposition::Succeeded, 'detailed mode does not report success');
check($invocations === 0, 'still not run');
Text
execute(): Enaxon\Reqtide\Exception\StateUnavailableException
callable invocations: 0
detailed disposition: failed
ok
  • شکست حالت یک استثنای زیرساختی است. با رد شدن توسط یک سیاست فرق دارد.
  • اجرای callable صفر بار بود.
  • چیزی نوشته نشد، چون شکست پیش از ارسال هر فرمان رخ داد.
  • متد executeDetailed() وضعیت failed را گزارش می‌کند و هرگز succeeded را گزارش نمی‌کند.