Mundarija (21)
- Bu darsda
- 1. Nega bu kerak?
- 2. Distributed lock
- 2.1 SET NX PX
- 2.2 Tuzoq: muddat ish davomida tugadi
- 2.3 Token bilan xavfsiz ochish
- 2.4 Bu ham yetarli emasmi?
- 3. Rate limit
- 3.1 Fixed window
- 3.2 Chegara muammosi
- 3.3 Sliding window (Sorted Set)
- 3.4 Token bucket (Lua)
- 4. Ko'p uchraydigan xatolar
- 5. Mashqlar
- 1-mashq (oson): to'ldiring
- 2-mashq (o'rta): fixed va sliding
- 3-mashq (qiyin): token bucket
- 6. Loyiha qadami: kanon tajribasi, loyihaga hali kirmaydi
- 7. Real ishda
- Xulosa
- Manbalar
Redis distributed lock va rate limit: SET NX PX, token, sliding window
Qisqacha: Distributed lock (taqsimlangan qulf) — bir nechta serverdan faqat bittasi ishni bajarishi uchun:
SET qulf <token> NX PX 30000. Kanonda 10 parallel urinishdan 1 tasi oldi. Tuzoq: ish qulf muddatidan uzoq cho'zilsa, boshqa server qulfni oladi, birinchisi esa oxiridaDELqilib boshqaning qulfini o'chiradi — kanonda uchinchi server ham kirdi. Davosi: tasodifiy token va "faqat o'zimniki bo'lsa o'chir" Lua skripti. Rate limit: fixed window oddiy, lekin oyna chegarasida 2 baravar o'tkazadi (kanon: 1.05 s ichida 10 ta, limit 5/s); sliding window va token bucket bu muammoni yo'qotadi.
Bu darsda
SET NX PXbilan qulf olasiz va uni token + Lua bilan xavfsiz ochasiz.- Qulf muddati ish davomida tugashi xavfini va fencing token g'oyasini tushuntirasiz.
- Redlock va unga qilingan tanqidni bilasiz.
- Fixed window, sliding window va token bucket limitlarini yozasiz va solishtirasiz.
Oldin bilishingiz kerak: Atomiklik: MULTI, Lua va pipelining, Qulflar: row, table va advisory, String, TTL va kalit nomlash.
Oldingi darsda xizmatlar orasida xabar almashdik. Endi ular orasidagi kelishuv: kim ishni bajaradi va mijoz qancha so'rov yuborishi mumkin. Hamma chiqishlar — kanon tajribasi (Redis 8.10.2, node-redis 6.3.0).
1. Nega bu kerak?
vazifalar-api uchta serverda ishlayapti. Har kecha soat 02:00 da "kunlik hisobot" yuboriladi. Uchala server ham cron'ni ishga tushiradi — foydalanuvchi bitta hisobot o'rniga uchta email oladi. Ishni bitta server qilishi kerak. Bu — taqsimlangan qulf (distributed lock).
Ikkinchi muammo: kimdir login formasiga sekundiga 1 000 ta parol yuboryapti. Bitta IP'dan daqiqasiga 5 tadan ko'p urinishga ruxsat bermaslik kerak — va hisob uchala serverda umumiy bo'lishi kerak. Bu — rate limit (so'rovlarni cheklash).
Ikkalasi uchun ham umumiy, tez, atomik joy kerak. Postgres'da advisory lock bor, lekin u ulanishga bog'langan va har urinish bazaga yuk. Redis — tabiiy tanlov.
2. Distributed lock
2.1 SET NX PX
const natija = await Promise.all(Array.from({ length: 10 }, (_, i) =>
r.set("t60:qulf:hisobot", `ishchi-${i}`, { NX: true, PX: 2000 })));olganlar: 1 | egasi: ishchi-0 | PTTL: true10 ta "server" bir vaqtda qulfni olmoqchi bo'ldi — bittasi oldi (OK), qolganlari null. NX — faqat kalit yo'q bo'lsa; PX 2000 — 2 soniyada o'zi ochiladi. Muddat majburiy: qulf egasi yiqilsa (server o'chdi), qulf abadiy qolib, hech kim ishni bajarolmay qolardi.
2.2 Tuzoq: muddat ish davomida tugadi
Kanon ataylab yomon ssenariyni sinadi: A 200 ms'lik qulf oldi, lekin ishi 400 ms davom etdi.
B oldi: OK
A ning DEL'idan keyin qulf: null — B himoyasiz qoldi, C ham kira oladi: OKsequenceDiagram
participant A
participant R as Redis
participant B
participant C
A->>R: SET qulf A NX PX 200
Note over A: ish 400 ms davom etadi
Note over R: 200 ms, qulf o'zi o'chdi
B->>R: SET qulf B NX PX 2000
R-->>B: OK
A->>R: DEL qulf
Note over R: B ning qulfi o'chdi!
C->>R: SET qulf C NX PX 2000
R-->>C: OKDiagrammaga qarang. Qulf muddati tugadi — B uni to'g'ri oldi. Lekin A ishini tugatib, "o'z" qulfini DEL qildi — aslida B ning qulfini. Endi C ham kirdi: B va C bir vaqtda ishlayapti. Qulf himoya qilishi kerak bo'lgan narsa buzildi.
2.3 Token bilan xavfsiz ochish
Har qulf olishda tasodifiy token (UUID) yoziladi. Ochishda: "qulfdagi qiymat mening tokenim bo'lsagina o'chir". Bu ikki amal (tekshirish va o'chirish) atomik bo'lishi kerak — Lua skript (Atomiklik):
import { randomUUID } from "node:crypto";
const OCH = `if redis.call('GET', KEYS[1]) == ARGV[1]
then return redis.call('DEL', KEYS[1])
else return 0 end`;
const tokenA = randomUUID();
await r.set("t60:qulf", tokenA, { NX: true, PX: 200 });
// … A ning ishi cho'zildi, B qulfni oldi (tokenB bilan) …
await r.eval(OCH, { keys: ["t60:qulf"], arguments: [tokenA] });A token bilan ochmoqchi: 0 | qulf hali B'niki: true
B ochdi: 1Nima bo'ldi? Vaqt bo'yicha:
| Qadam | A | B | Qulfda |
|---|---|---|---|
| 1 | SET … tokenA NX PX 200 → OK |
— | tokenA |
| 2 | ish cho'zildi | — | 200 ms o'tdi, bo'sh |
| 3 | — | SET … tokenB NX → OK |
tokenB |
| 4 | Lua: "tokenA bo'lsa o'chir" → 0 |
— | tokenB (tegilmadi) |
| 5 | — | Lua: "tokenB bo'lsa o'chir" → 1 |
bo'sh |
A ning urinishi — 0 (token mos emas), qulf B da qoldi. B o'z tokeni bilan ochdi — 1. Kanon skriptni bitta qatorda yozgan; bu yerda o'qish uchun bo'lindi.
2.4 Bu ham yetarli emasmi?
Token "boshqaning qulfini o'chirish"ni yo'qotdi. Lekin asosiy muammo qoldi: A ning muddati tugagan — A buni bilmaydi va ishni davom ettiryapti. B ham ishlayapti. Kanon xulosasi:
Eslatma: muddat tugashining o'zi (GC pauza, sekin ish) — fencing token kerak (Kleppmann tanqidi); Redlock — bir nechta mustaqil Redis, bu yerda sinalmagan.- GC pauza — Node'ning axlat yig'uvchisi jarayonni bir necha soniya to'xtatishi mumkin (V8 xotira modeli va garbage collection); tarmoq ham kechikadi. A uyg'onganda qulf allaqachon boshqada.
- Fencing token (to'siq raqami) — har qulf olishda o'sib boradigan raqam (
INCR). Ish natijasini yozadigan joy (baza) eski raqamli yozuvni rad etadi: "sizning raqamingiz 33, men 34 ni ko'rganman". Bu Martin Kleppmann'ning 2016-yilgi tanqidi markazidagi g'oya. - Redlock — Redis muallifi taklif qilgan algoritm: 5 ta mustaqil Redis'dan ko'pchiligida qulf olish. Bitta Redis o'chganda ham ishlaydi. Kleppmann ko'rsatdi: soat va pauza muammolari tufayli Redlock ham to'g'rilikni kafolatlamaydi. Kanonda sinalmagan.
Amaliy xulosa (hujjat va tanqidga ko'ra):
| Maqsad | Yechim |
|---|---|
| Samaradorlik: ish ikki marta bo'lsa — faqat ortiqcha xarajat (ikki email) | bitta Redis, SET NX PX + token |
| To'g'rilik: ikki marta — zarar (ikki marta pul yechish) | baza tranzaksiyasi, noyob cheklov, fencing token |
Pul va qoldiq kabi narsalarni Redis qulfiga ishonib qo'ymang — ular Postgres'da qatorni qulflash yoki UNIQUE cheklov bilan himoyalanadi (Amaliyot: race condition'larni hal qilish).
Tekshirib ko'ring: Qulf muddatini 2 s qo'ydingiz, ish odatda 0.5 s. Nega baribir 30 s qo'yish va ish davomida muddatni uzaytirish tavsiya qilinadi?
Javob
"Odatda 0.5 s" — kafolat emas: baza sekinlashishi, GC pauza yoki tarmoq kechikishi ishni 2 s dan oshirishi mumkin, va qulf ish o'rtasida ochiladi. Uzunroq muddat bu xavfni kamaytiradi; yiqilgan server uchun esa muddat baribir ishlaydi (30 s keyin boshqasi oladi). Uzoq ishda qulf egasi vaqti-vaqti bilan muddatni uzaytiradi (token tekshiradigan Lua bilan PEXPIRE) — "watchdog" naqshi.
3. Rate limit
3.1 Fixed window
Eng oddiy usul: vaqt bo'laklarga (oynaga) bo'linadi, har oynada hisoblagich. Kalitga oyna qo'shilmaydi — kalitning o'zi oyna boshida yaratilib, TTL bilan o'ladi:
async function fixed(kalit, limit, oynaS) {
const n = await r.multi()
.incr(kalit)
.expire(kalit, oynaS, "NX")
.exec();
return n[0] <= limit;
}INCR + EXPIRE … NX bitta MULTI da: birinchi so'rov kalitni yaratadi va muddat qo'yadi, keyingilari faqat oshiradi (NX — muddat yo'q bo'lsagina qo'yadi; String, TTL va kalit nomlash).
8 so'rov: ok ok ok ok ok 429 429 429Limit daqiqasiga 5 — oltinchidan boshlab 429 Too Many Requests (Status kodlarni to'g'ri tanlash).
Atomiklik tekshiruvi — 200 parallel so'rov, limit 50:
o'tdi: 50 | hisoblagich: 200Aniq 50 — INCR atomik, ortiqcha o'tgan yo'q. Hisoblagich 200 — rad etilganlar ham sanaldi.
3.2 Chegara muammosi
1 s oyna, chegaradan oldin 5 + keyin 5: 10 tasi o'tdi ~1.05 s ichida (limit 5/s)Limit sekundiga 5. Mijoz oyna oxirida 5 ta, yangi oyna boshida yana 5 ta yubordi — 1.05 soniya ichida 10 ta o'tdi. Fixed window ikki oyna chegarasida limitning ikki baravarini o'tkazib yuboradi. Login urinishlarida bu hujumchiga ikki baravar ko'p parol sinash imkonini beradi.
3.3 Sliding window (Sorted Set)
Har so'rov vaqti Sorted Set'ga ball sifatida yoziladi (Hash, List, Set va Sorted Set). Har so'rovda: oxirgi oynadan eskilarini o'chirish, yangisini qo'shish, sanash:
async function sliding(kalit, limit, oynaMs) {
const hozir = Date.now();
const [, , n] = await r.multi()
.zRemRangeByScore(kalit, 0, hozir - oynaMs)
.zAdd(kalit, {
score: hozir,
value: `${hozir}-${Math.random()}`,
})
.zCard(kalit)
.pExpire(kalit, oynaMs)
.exec();
return n <= limit;
}chegaradan 0.55 s keyin yana 5: ok ok ok ok ok 429 429 429 429 429 (rad etilganlar ham sanaladi — oddiy variant)Birinchi 5 ta o'tdi; 0.55 soniyadan keyin yana 5 ta — hammasi rad etildi: oxirgi 1 000 ms ichida allaqachon 5 ta bor. Chegara portlashi yo'q.
Uch detal:
valuenoyob bo'lishi kerak — bir millisekundda ikki so'rov kelsa, bir xil element bitta deb sanalardi. Shuning uchunvaqt-tasodif.- Bu oddiy variantda rad etilgan so'rov ham Set'ga yoziladi va keyingi oynani band qiladi — doim urinayotgan mijoz hech qachon o'tolmaydi. Bu ba'zan maqsadga mos (hujumchi uchun), ba'zan yo'q. Ehtiyotkor variant: avval sanash, limit ichida bo'lsagina qo'shish (Lua bilan, atomik).
- Narxi — xotira: har so'rov alohida element. Daqiqasiga 10 000 so'rovli limitda 10 000 element.
3.4 Token bucket (Lua)
Token bucket (tangalar chelagi): chelakda eng ko'pi bilan N ta tanga. Har so'rov bitta tanga oladi. Tanga bo'lmasa — 429. Chelak doimiy tezlik bilan to'lib turadi. Natija: qisqa "portlash"ga ruxsat (sig'imgacha), uzoq muddatda o'rtacha tezlik cheklangan.
Hisob Lua skriptda — o'qish, hisoblash va yozish atomik:
local kalit, sigim, tezlik, hozir =
KEYS[1], tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
local h = redis.call('HMGET', kalit, 'token', 'vaqt')
local token = tonumber(h[1]) or sigim
local vaqt = tonumber(h[2]) or hozir
token = math.min(sigim, token + (hozir - vaqt) / 1000 * tezlik)
local ruxsat = 0
if token >= 1 then token = token - 1; ruxsat = 1 end
redis.call('HSET', kalit, 'token', token, 'vaqt', hozir)
redis.call('PEXPIRE', kalit, math.ceil(sigim / tezlik * 1000))
return ruxsatHolat hash'da: token (qolgan tangalar) va vaqt (oxirgi hisob). O'tgan vaqtga qarab tangalar qo'shiladi, sig'imdan oshmaydi. Kanonning birinchi qatorini telefon uchun ikkiga bo'ldik.
7 darhol + 1 s dan keyin 3: ok ok ok ok ok 429 429 ok ok 429Sig'im 5, tezlik sekundiga 2. Darhol 7 so'rov: 5 tasi o'tdi (chelak to'la edi), 2 tasi — 429. 1 soniyadan keyin chelakka 2 tanga tushdi — keyingi 3 tadan 2 tasi o'tdi.
Vaqtni (hozir) skript Node'dan oldi (Date.now()). Bir nechta serverning soatlari farq qilsa, hisob buziladi. Muqobil — skript ichida Redis'ning TIME buyrug'i.
| Usul | Xotira | Chegara portlashi | Murakkablik |
|---|---|---|---|
| Fixed window | 1 kalit | bor (2×) | juda oddiy |
| Sliding window (ZSET) | har so'rov | yo'q | o'rta |
| Token bucket | 1 hash | boshqariladigan portlash | Lua |
Ko'p loyihalarda tayyor kutubxona ishlatiladi: rate-limiter-flexible (Redis, Postgres, xotira), express-rate-limit + Redis store. Ular ichida shu algoritmlar va Lua skriptlar. Rate limit'ni HTTP darajasida (sarlavhalar, per-IP va per-user) 28-qismda to'liq quramiz.
4. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| Ish ikki marta bajarildi | qulf muddati ish davomida tugadi | uzunroq muddat, watchdog, idempotent ish |
| Boshqa serverning qulfi o'chib ketdi | ochishda oddiy DEL |
token + Lua |
| Qulf abadiy qoldi | PX/EX siz SET NX |
muddat majburiy |
| Pul ikki marta yechildi | to'g'rilik Redis qulfiga ishonilgan | baza tranzaksiyasi, UNIQUE, fencing |
| Limit chegarada 2× o'tkazadi | fixed window | sliding window yoki token bucket |
| Limit hech qachon tiklanmaydi | INCR bor, EXPIRE yo'q |
EXPIRE … NX bir MULTI da |
| Limit serverlar orasida farq qiladi | hisob har server xotirasida | Redis'da umumiy hisob |
5. Mashqlar
Mashqlar kurs/mashqlar/27/60-qulf-limit/ da.
1-mashq (oson): to'ldiring
- Qulf olish buyrug'i:
SET qulf <token> PX 30000 - Qulfni xavfsiz ochish uchun qiymatda saqlanadigan narsa —
- Oyna chegarasida limitning ikki baravarini o'tkazadigan usul —
- Limitdan oshganda HTTP status kodi —
- Har qulfda o'sadigan, eski egani rad etishga yordam beradigan raqam —
Yechim
- NX — faqat kalit yo'q bo'lsa.
- token — tasodifiy UUID; ochishda Lua bilan solishtiriladi.
- fixed window — kanonda 1.05 s ichida 10 ta (limit 5/s).
- 429 — Too Many Requests.
- fencing token — Kleppmann tanqidi.
2-mashq (o'rta): fixed va sliding
Soxta soat bilan ikkala algoritmni yozing: fixedWindow(limit, oynaMs) va slidingWindow(limit, oynaMs) — har biri (hozir) => boolean funksiya qaytarsin. Fixed: oyna raqami Math.floor(hozir / oynaMs). Sliding: so'rov vaqtlari massivi, eskilari o'chiriladi, faqat ruxsat berilganlar yoziladi. Kanon chegara ssenariysi: limit 5/1000 ms, vaqt 900–940 ms da 5 so'rov, 1000–1040 ms da yana 5 so'rov. Har biri nechta o'tkazadi?
Yechim
function fixedWindow(limit, oynaMs) {
let oyna = -1;
let soni = 0;
return (hozir) => {
const joriy = Math.floor(hozir / oynaMs);
if (joriy !== oyna) {
oyna = joriy;
soni = 0;
}
soni++;
return soni <= limit;
};
}
function slidingWindow(limit, oynaMs) {
let vaqtlar = [];
return (hozir) => {
vaqtlar = vaqtlar.filter((t) => t > hozir - oynaMs);
if (vaqtlar.length >= limit) return false;
vaqtlar.push(hozir);
return true;
};
}
const sorovlar = [
900, 910, 920, 930, 940, 1000, 1010, 1020, 1030, 1040,
];
for (const [nom, f] of [
["fixed", fixedWindow(5, 1000)],
["sliding", slidingWindow(5, 1000)],
]) {
const javob = sorovlar.map((t) => (f(t) ? "ok" : "429"));
const otdi = javob.filter((x) => x === "ok").length;
console.log(`${nom}: ${otdi} o'tdi — ${javob.join(" ")}`);
}Konsolda:
fixed: 10 o'tdi — ok ok ok ok ok ok ok ok ok ok
sliding: 5 o'tdi — ok ok ok ok ok 429 429 429 429 429Bizning ssenariyda fixed window 140 ms ichida 10 tasini o'tkazdi (kanonda ~1.05 s ichida 10 ta): 940 ms va 1000 ms turli oynalarda. Sliding window oxirgi 1 000 ms ni sanaydi — chegara yo'q. Bizning sliding'da rad etilgan so'rov yozilmaydi (ehtiyotkor variant), kanon esa hammasini yozgan; bu ssenariyda natija bir xil.
3-mashq (qiyin): token bucket
Kanondagi Lua mantiqini JavaScript'da yozing: tokenBucket(sigim, tezlik) → (hozir) => boolean. Holat: token (boshida sigim) va vaqt. Har chaqiruvda: token = min(sigim, token + (hozir - vaqt) / 1000 * tezlik), keyin 1 dan katta yoki teng bo'lsa — bittasini olib true. Kanon ssenariysini takrorlang: sig'im 5, tezlik 2/s; 0 ms da 7 so'rov, 1000 ms da 3 so'rov.
Yechim
function tokenBucket(sigim, tezlik) {
let token = sigim;
let vaqt = null;
return (hozir) => {
if (vaqt !== null) {
const qoshildi = ((hozir - vaqt) / 1000) * tezlik;
token = Math.min(sigim, token + qoshildi);
}
vaqt = hozir;
if (token >= 1) {
token -= 1;
return true;
}
return false;
};
}
const olish = tokenBucket(5, 2);
const javob = [];
for (let i = 0; i < 7; i++) javob.push(olish(0));
for (let i = 0; i < 3; i++) javob.push(olish(1000));
console.log(javob.map((x) => (x ? "ok" : "429")).join(" "));Konsolda:
ok ok ok ok ok 429 429 ok ok 429Kanon chiqishi bilan aynan bir xil. 7 ta darhol — chelakda 5 ta bor edi. 1 soniyada 2 × 1 = 2 tanga qo'shildi — 3 tadan 2 tasi o'tdi. Bitta Node jarayonida bu yopilish (closure) yetarli (Closure). Bir nechta serverda esa holat Redis hash'ida va hisob Lua'da bo'lishi kerak — aks holda har server o'z chelagini sanaydi va umumiy limit N baravar oshadi.
6. Loyiha qadami: kanon tajribasi, loyihaga hali kirmaydi
vazifalar-api ga bu darsda kod qo'shilmaydi. Rate limit ma'nosi foydalanuvchini tanishda — "kim so'rov yubordi" (IP yoki hisob) — bu 28-qism (auth va backend xavfsizligi), u yerda login urinishlari uchun Redis asosidagi limiter quramiz. Taqsimlangan qulf kerak bo'ladigan cron va fon ishlari — 29-qismda. Kesh stampede himoyasidagi SET NX PX ni Caching naqshlari darsida ko'rdik. O'zingiz sinab ko'ring: ikki terminalda bir vaqtda SET vz:qulf:sinov 1 NX PX 10000 bering — qaysi biri OK oldi?
7. Real ishda
- Cron bir nusxada — bir nechta server/pod'da kunlik vazifani bitta bajaradi (BullMQ'ning "repeatable job" lari buni o'zi hal qiladi).
- Login va OTP himoyasi — IP va telefon raqami bo'yicha limit (SMS pumping firibgarligiga qarshi).
- Tashqi API kvotasi — Telegram, to'lov tizimi API'lariga sekundiga N so'rovdan oshmaslik (token bucket).
- Intervyuda: "Redis'da distributed lock qanday quriladi?", "Nega qulfni token bilan ochish kerak?", "Redlock haqida nima bilasiz?", "Fixed va sliding window farqi?"
Xulosa
- Qulf:
SET qulf <token> NX PX <ms>; muddat majburiy; ochish — token tekshiradigan Lua. - Oddiy
DELboshqaning qulfini o'chiradi (kanonda C ham kirdi). - Muddat tugashi (GC, kechikish) qoladi — to'g'rilik kerak bo'lsa fencing token va baza; Redlock ham kafolat emas.
- Fixed window:
INCR+EXPIRE NX, atomik (200 dan aniq 50), lekin chegarada 2×. - Sliding window (ZSET) va token bucket (Lua) chegara portlashini yo'qotadi.
Keyingi dars: Redis persistence va operatsiya — Redis qayta ishga tushganda nima qoladi (RDB va AOF o'lchovi), xotira to'lganda nima bo'ladi va Valkey bilan farq.
Manbalar
- Redis hujjati — Distributed locks (SET NX PX, token, Redlock): https://redis.io/docs/latest/develop/use/patterns/distributed-locks/
- M. Kleppmann, "How to do distributed locking" (2016-02-08): https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html
- Rate limiting naqshlari: https://redis.io/glossary/rate-limiting/
- rate-limiter-flexible: https://github.com/animir/node-rate-limiter-flexible
- Chiqishlar — kanon o'lchovi (IlmHamroh, 2026-10): Redis 8.10.2, Node 24.21.0, node-redis 6.3.0.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!