Mundarija (26)
- Bu darsda
- 1. Nega bu kerak?
- 2. Sxema: fayllar va vazifa_ilovalari
- 3. Dublikat: sha256 va ON CONFLICT
- 3.1 Bir xil bayt — bitta yozuv
- 3.2 Hash JS'da
- 4. Holat va cheklovlar
- 5. Biriktirish: RESTRICT va CASCADE
- 5.1 Biriktirish tranzaksiyasi
- 6. RLS: begona fayl ko'rinmaydi
- 7. Yetim fayllar
- 7.1 Qayerdan paydo bo'ladi
- 7.2 Katta jadvalda tezligi
- 7.3 Tozalash tartibi: avval ombor, keyin baza
- 8. Vaqtincha → doimiy
- 9. Kanon modulini o'z bazamizda yurgizdik
- 10. Hujumchi nigohi
- 11. Ko'p uchraydigan xatolar
- 12. Mashqlar
- 1-mashq (oson): to'g'ri yoki noto'g'ri
- 2-mashq (o'rta): yetimlar ro'yxati
- 3-mashq (qiyin): uzilgan tozalash
- 13. Loyiha qadami: 201-fayllar.sql va metama.ts
- 14. Real ishda
- Xulosa
- Manbalar
Fayl metama'lumotini DB'da boshqarish: egasi, dublikat va yetim fayllar
Qisqacha: Ombor faylning baytlarini saqlaydi, lekin uning egasini, asl nomini va qaysi vazifaga tegishli ekanini bilmaydi. Bu ma'lumot — metama'lumot — bazada turadi:
fayllarjadvali (kalit, tur, hajm, sha256, holat) vavazifa_ilovalari(qaysi fayl qaysi vazifada). Egasi ichidaUNIQUE (egasi_id, sha256)bir xil faylni ikki marta saqlamaydi,FOR UPDATEparallel so'rovlarda limitni ushlaydi, hech qayerga biriktirilmagan yetim (orphan) fayllar esa avval ombordan, keyin bazadan tozalanadi.
Bu darsda
- Fayl baytlari va metama'lumotini ikki joyda saqlash sababini tushunasiz.
fayllarvavazifa_ilovalarijadvallarini, ularning cheklovlarini (constraint) haqiqiy SQL bilan sinaysiz.- sha256 bilan dublikat aniqlashni va nega u faqat egasi ichida bo'lishini bilasiz.
- Biriktirish tranzaksiyasini,
RESTRICT/CASCADEva RLS ni ishlatasiz. - Yetim fayllarni qaysi tartibda tozalash kerakligini o'lchov bilan asoslaysiz.
Oldin bilishingiz kerak: Presigned URL'lar, Soft delete, timestamps va tarix jadvallari, UPSERT va MERGE, Row Level Security, SKIP LOCKED bilan DB-asosli navbat.
Oldingi darsda presigned yuklash oxirida "holat bazada saqlanadi" dedik. Endi shu bazani quramiz.
1. Nega bu kerak?
Malika vazifasiga chek rasmini biriktirdi. Ilova uchta savolga javob berishi kerak:
- "Bu vazifada qaysi fayllar bor va ular qanday nomlangan?" — ro'yxat sahifasi.
- "Bu faylni shu foydalanuvchi ko'ra oladimi?" — har yuklab olishda.
- "Qaysi fayllar hech kimga kerak emas?" — tozalash.
Ombor bularning hech biriga javob bera olmaydi. U kalitlarni prefiks bo'yicha sanay oladi, xolos — va Object storage darsida ko'rdik, har so'rov millisekundlar. 10 000 vazifa ro'yxati uchun omborni aylanib chiqish — imkonsiz. Baza esa indeks bilan bir so'rovda javob beradi.
Shuning uchun qoida: baytlar — omborda, ma'lumot — bazada. Bazaga baytlarni (bytea) yozmang: zaxira nusxa va replikatsiya og'irlashadi, SELECT * megabaytlar tortadi.
flowchart LR
A[vazifalar-api] -->|INSERT fayllar| D[(Postgres)]
A -->|PutObject kalit| S[(Ombor S3)]
D --- F["fayllar: kalit, mime, hajm, sha256, holat"]
D --- I["vazifa_ilovalari: vazifa, fayl, asl_nom"]
F -.->|kalit| SIkki joyda saqlashning narxi — sinxronlik. Bazada yozuv bor, omborda fayl yo'q (yoki aksincha) — bu darsning qolgan qismi shu muammo haqida.
2. Sxema: fayllar va vazifa_ilovalari
Kanon sxemasi (201-fayllar.sql, izohlar qisqartirildi):
CREATE TABLE fayllar (
id bigint GENERATED ALWAYS AS IDENTITY,
egasi_id bigint NOT NULL,
kalit text NOT NULL, -- S3 kaliti (uuid)
mime text NOT NULL, -- sehrli baytlardan
hajm bigint NOT NULL,
sha256 bytea, -- tasdiqgacha NULL
holat text NOT NULL DEFAULT 'kutilmoqda',
yaratilgan_vaqt timestamptz NOT NULL DEFAULT now(),
tasdiqlangan_vaqt timestamptz,
CONSTRAINT fayllar_pkey PRIMARY KEY (id),
CONSTRAINT fayllar_egasi_id_fkey FOREIGN KEY (egasi_id)
REFERENCES foydalanuvchilar (id) ON DELETE CASCADE,
CONSTRAINT fayllar_kalit_key UNIQUE (kalit),
CONSTRAINT fayllar_egasi_id_sha256_key UNIQUE (egasi_id, sha256),
CONSTRAINT fayllar_mime_check CHECK (mime IN ('image/png',
'image/jpeg', 'image/webp', 'application/pdf', 'text/plain')),
CONSTRAINT fayllar_hajm_check CHECK (hajm BETWEEN 1 AND 10485760),
CONSTRAINT fayllar_sha256_check CHECK (octet_length(sha256) = 32),
CONSTRAINT fayllar_holat_check
CHECK (holat IN ('kutilmoqda', 'tayyor')),
CONSTRAINT fayllar_tayyor_check CHECK ((holat = 'tayyor') =
(sha256 IS NOT NULL AND tasdiqlangan_vaqt IS NOT NULL))
);
CREATE TABLE vazifa_ilovalari (
vazifa_id bigint NOT NULL,
fayl_id bigint NOT NULL,
asl_nom text NOT NULL,
qoshilgan_vaqt timestamptz NOT NULL DEFAULT now(),
CONSTRAINT vazifa_ilovalari_pkey PRIMARY KEY (vazifa_id, fayl_id),
CONSTRAINT vazifa_ilovalari_vazifa_id_fkey FOREIGN KEY (vazifa_id)
REFERENCES vazifalar (id) ON DELETE CASCADE,
CONSTRAINT vazifa_ilovalari_fayl_id_fkey FOREIGN KEY (fayl_id)
REFERENCES fayllar (id) ON DELETE RESTRICT,
CONSTRAINT vazifa_ilovalari_asl_nom_check
CHECK (char_length(asl_nom) BETWEEN 1 AND 255)
);
CREATE INDEX vazifa_ilovalari_fayl_id_idx
ON vazifa_ilovalari (fayl_id);
CREATE INDEX fayllar_yaratilgan_vaqt_idx ON fayllar (yaratilgan_vaqt);Har qarorning sababi:
| Qaror | Nega |
|---|---|
kalit UNIQUE, uuid |
foydalanuvchi nomidan mustaqil (Fayl yuklash xavfsizligi) |
mime CHECK ro'yxati |
SVG/HTML bazaga ham kira olmaydi — ikkinchi himoya qatlami |
hajm 1…10 MB |
API limiti bilan bir xil |
sha256 NULL bo'lishi mumkin |
presigned yuklashda server baytlarni hali ko'rmagan |
tayyor_check |
"tayyor" = sha256 va tasdiq vaqti bor — yarim holat yo'q |
asl nom vazifa_ilovalari da |
bitta fayl ikki vazifada ikki xil nom bilan bo'lishi mumkin |
fayl FK — RESTRICT |
biriktirilgan faylni bevosita o'chirib bo'lmaydi |
Bog'lanish N:M (Bog'lanishlar: 1:1, 1:N, N:M): bitta vazifada ko'p fayl, bitta fayl ko'p vazifada.
Quyidagi SQL bloklarini biz o'z PostgreSQL 17.6 klasterimizda (ICU, UTC) shu sxema bilan ishga tushirdik — natijalar haqiqiy. Kanon PG 18.6 da xuddi shu so'rovlarni bajargan; farq faqat bitta xato xabarida (pastda aytamiz).
3. Dublikat: sha256 va ON CONFLICT
3.1 Bir xil bayt — bitta yozuv
Foydalanuvchi bitta chekni ikki vazifaga ikki marta yukladi. Ikkita bir xil obyekt saqlash — ortiqcha pul. Fayl mazmunining hash'i (sha256) bir xil bo'lsa — fayl bir xil.
INSERT INTO fayllar
(egasi_id, kalit, mime, hajm, sha256, holat, tasdiqlangan_vaqt)
VALUES (1, 'fayllar/1/a.png', 'image/png', 2048,
sha256('rasm'), 'tayyor', now())
RETURNING kalit, holat; kalit | holat
-----------------+--------
fayllar/1/a.png | tayyor
(1 row)
INSERT 0 1O'sha baytlar boshqa kalit bilan:
INSERT INTO fayllar
(egasi_id, kalit, mime, hajm, sha256, holat, tasdiqlangan_vaqt)
VALUES (1, 'fayllar/1/b.png', 'image/png', 2048,
sha256('rasm'), 'tayyor', now())
ON CONFLICT (egasi_id, sha256) DO NOTHING
RETURNING kalit; kalit
-------
(0 rows)
INSERT 0 0INSERT 0 0 — hech narsa yozilmadi, RETURNING bo'sh. Ilova buni "dublikat" deb tushunadi: eski yozuvni o'qiydi va yangi yuklangan fayllar/1/b.png obyektini ombordan o'chiradi.
Boshqa foydalanuvchi o'sha baytlarni yuklasa:
INSERT INTO fayllar
(egasi_id, kalit, mime, hajm, sha256, holat, tasdiqlangan_vaqt)
VALUES (2, 'fayllar/2/c.png', 'image/png', 2048,
sha256('rasm'), 'tayyor', now())
RETURNING egasi_id, kalit; egasi_id | kalit
----------+-----------------
2 | fayllar/2/c.png
(1 row)
INSERT 0 1Alohida yozuv. Nega butun tizim bo'yicha dedup qilmaymiz? Hujumchi "maxfiy-hisobot.pdf" ning taxminiy nusxasini yuklaydi. Javob "dublikat" bo'lsa (yoki yuklash sezilarli tezroq bo'lsa) — u boshqa odamda shu fayl borligini bildi. Bu — yon kanal (side channel) orqali ma'lumot sizishi. Egasi ichidagi dedup bunday ma'lumot bermaydi.
3.2 Hash JS'da
Server sha256 ni oqim o'tayotganda hisoblaydi (Katta fayllarni stream bilan yuklash). Mantiq toza JS'da:
const hex = (b) => [...new Uint8Array(b)]
.map((x) => x.toString(16).padStart(2, "0")).join("");
async function sha256(matn) {
const b = new TextEncoder().encode(matn);
return hex(await crypto.subtle.digest("SHA-256", b));
}
const fayllar = new Map(); // "egasi:sha256" → id
let keyingiId = 1;
async function yoz(egasi, matn) {
const kalit = `${egasi}:${await sha256(matn)}`;
if (fayllar.has(kalit)) {
return { id: fayllar.get(kalit), dublikat: true };
}
fayllar.set(kalit, keyingiId);
return { id: keyingiId++, dublikat: false };
}
console.log(await yoz(1, "rasm"));
console.log(await yoz(1, "rasm"));
console.log(await yoz(2, "rasm"));
console.log(await yoz(1, "Rasm"));
console.log((await sha256("pdf")).slice(0, 16));Konsolda:
{ id: 1, dublikat: false }
{ id: 1, dublikat: true }
{ id: 2, dublikat: false }
{ id: 3, dublikat: false }
c35b21d6ca39aa7cBitta harf (R) — butunlay boshqa hash. Oxirgi qator — Postgres'ning sha256('pdf') natijasi bilan bir xil boshlanadi (pastdagi tasdiqlash blokida ko'rasiz): hash qaysi tilda hisoblansa ham bir xil.
Diqqat:
Mapdagi tekshiruv "bor-yo'qligini so'rab, keyin yozish" — ikki parallel so'rovda ikkalasi ham "yo'q" deb ko'radi. Bazada buniUNIQUE+ON CONFLICThal qiladi: tekshiruv va yozish — bitta atomik amal. Biz kanon modulini o'z bazamizda yurgizdik: 10 ta parallel bir xil yuklash — 1 ta yozuv (pastda).
4. Holat va cheklovlar
Presigned yuklash boshlanganda server hali baytlarni ko'rmagan — sha256 NULL, holat kutilmoqda:
INSERT INTO fayllar (egasi_id, kalit, mime, hajm)
VALUES (1, 'vaqtincha/1/x.pdf', 'application/pdf', 5000),
(1, 'vaqtincha/1/y.pdf', 'application/pdf', 5000)
RETURNING kalit, holat, sha256; kalit | holat | sha256
-------------------+------------+--------
vaqtincha/1/x.pdf | kutilmoqda |
vaqtincha/1/y.pdf | kutilmoqda |
(2 rows)
INSERT 0 2Ikkalasi bir egasida, ikkalasida sha256 NULL — lekin UNIQUE (egasi_id, sha256) buzilmadi. Sababi: Postgres'da NULL'lar bir-biriga teng emas (NULL mantiqi), unique cheklov ularni to'qnashuv deb sanamaydi.
Endi cheklovlarni buzib ko'ramiz. \set VERBOSITY terse — faqat asosiy xabar, DETAIL qatorisiz (unda id va vaqt bor):
\set VERBOSITY terse
INSERT INTO fayllar (egasi_id, kalit, mime, hajm)
VALUES (1, 'z.svg', 'image/svg+xml', 10);
INSERT INTO fayllar (egasi_id, kalit, mime, hajm)
VALUES (1, 'z.png', 'image/png', 20000000);
UPDATE fayllar SET holat = 'tayyor'
WHERE kalit = 'vaqtincha/1/x.pdf';
INSERT INTO fayllar (egasi_id, kalit, mime, hajm, sha256)
VALUES (1, 'z2.png', 'image/png', 10, '\x00ff'::bytea);ERROR: new row for relation "fayllar" violates check constraint "fayllar_mime_check"
ERROR: new row for relation "fayllar" violates check constraint "fayllar_hajm_check"
ERROR: new row for relation "fayllar" violates check constraint "fayllar_tayyor_check"
ERROR: new row for relation "fayllar" violates check constraint "fayllar_sha256_check""fayllar jadvalidagi yangi qator …_check cheklovini buzadi":
- SVG — ruxsat ro'yxatida yo'q.
- 20 MB — 10 MB dan katta.
- Hash'siz "tayyor" — yarim holat taqiqlangan.
- 2 baytli "sha256" — hash 32 bayt bo'lishi shart.
To'g'ri tasdiqlash — hamma ustun bitta UPDATE da:
UPDATE fayllar
SET holat = 'tayyor', sha256 = sha256('pdf'),
tasdiqlangan_vaqt = now()
WHERE kalit = 'vaqtincha/1/x.pdf'
RETURNING holat, encode(sha256, 'hex') AS sha256; holat | sha256
--------+------------------------------------------------------------------
tayyor | c35b21d6ca39aa7cc3b79a705d989f1a6e88b99ab43988d74048799e3db926a3
(1 row)
UPDATE 1Hash JS'dagi c35b21d6ca39aa7c bilan boshlanadi. CHECK lar ilova xatosini ham ushlaydi: kod qaysidir joyda holatni yarim o'zgartirsa, baza rad etadi (CREATE TABLE va cheklovlar).
Tekshirib ko'ring: Nega
mimeustuniga baza darajasida ham CHECK qo'yildi, axir API sehrli baytlarni tekshiradi-ku?
Javob
Chuqur himoya (defense in depth). Ertaga kimdir yangi marshrut yozib, tekshiruvni unutishi, migratsiya skripti yoki admin panel bazaga to'g'ridan yozishi mumkin. CHECK — oxirgi to'siq: SVG hech qanday yo'l bilan "tayyor fayl" bo'lib kira olmaydi.
5. Biriktirish: RESTRICT va CASCADE
Faylni vazifaga ulaymiz:
INSERT INTO vazifa_ilovalari (vazifa_id, fayl_id, asl_nom)
SELECT v.id, f.id, 'Rasm (1).png'
FROM vazifalar v, fayllar f
WHERE v.egasi_id = 1 AND f.kalit = 'fayllar/1/a.png'
ORDER BY v.id LIMIT 1
RETURNING vazifa_id, asl_nom; vazifa_id | asl_nom
-----------+--------------
1 | Rasm (1).png
(1 row)
INSERT 0 1Biriktirilgan faylni bevosita o'chirishga urinamiz:
\set VERBOSITY terse
DELETE FROM fayllar WHERE kalit = 'fayllar/1/a.png';ERROR: update or delete on table "fayllar" violates foreign key constraint "vazifa_ilovalari_fayl_id_fkey" on table "vazifa_ilovalari""fayllar dagi o'chirish vazifa_ilovalari jadvalidagi tashqi kalit cheklovini buzadi". Kanon PostgreSQL 18.6 da xabar aniqroq — u RESTRICT ni nomi bilan aytadi:
ERROR: update or delete on table "fayllar" violates RESTRICT setting of foreign key constraint "vazifa_ilovalari_fayl_id_fkey" on table "vazifa_ilovalari"Bu — PG 17 va 18 farqi; ma'nosi bir xil. Endi vazifaning o'zini o'chiramiz:
DELETE FROM vazifalar
WHERE id = (SELECT vazifa_id FROM vazifa_ilovalari LIMIT 1);
SELECT count(*) AS ilovalar_qoldi FROM vazifa_ilovalari;DELETE 1
ilovalar_qoldi
----------------
0
(1 row)Vazifa o'chdi — CASCADE uning ilovalarini ham o'chirdi. Fayl esa qoldi: endi u hech qayerga biriktirilmagan — yetim. Bu ataylab: fayl boshqa vazifada ham bo'lishi mumkin edi, uni o'chirish qarori — alohida tozalash jarayonida (Foreign key va referential actions).
5.1 Biriktirish tranzaksiyasi
Kanon modulining biriktir funksiyasi bitta tranzaksiyada to'rt narsani tekshiradi:
await c.query("BEGIN");
// vazifa qatorini qulflaymiz — parallel biriktirishlar
// limitni birga oshirib yubormasin
const v = await c.query(
`SELECT 1 FROM vazifalar WHERE id = $1 AND egasi_id = $2
AND ochirilgan_vaqt IS NULL FOR UPDATE`,
[vazifaId, egasiId],
);
if (v.rowCount === 0) {
throw new IlovaXato("topilmadi", "Vazifa topilmadi");
}
const f = await c.query<{ holat: string }>(
"SELECT holat FROM fayllar WHERE id = $1 AND egasi_id = $2",
[faylId, egasiId],
);
const fq = f.rows[0];
if (!fq) throw new IlovaXato("topilmadi", "Fayl topilmadi");
if (fq.holat !== "tayyor") {
throw new IlovaXato("tayyor-emas", "Fayl hali tasdiqlanmagan");
}
const n = await c.query<{ soni: number }>(
`SELECT count(*)::int AS soni FROM vazifa_ilovalari
WHERE vazifa_id = $1`, [vazifaId],
);
if ((n.rows[0]?.soni ?? 0) >= MAKS_ILOVA) {
throw new IlovaXato("limit",
`Bitta vazifaga ko'pi bilan ${String(MAKS_ILOVA)} ta fayl`);
}
await c.query(
`INSERT INTO vazifa_ilovalari (vazifa_id, fayl_id, asl_nom)
VALUES ($1, $2, $3) ON CONFLICT DO NOTHING`,
[vazifaId, faylId, aslNom],
);
await c.query("COMMIT");- Vazifa — shu egasiniki va o'chirilmagan.
- Fayl — shu egasiniki: begona faylni o'z vazifangizga ulab, keyin "yuklab olib" bo'lmaydi.
- Fayl
tayyor— tasdiqlanmagan presigned yuklash biriktirilmaydi. - Limit — 10 ta.
FOR UPDATEbo'lmasa, 15 ta parallel so'rov hammasi "9 ta bor" deb ko'rib, limitni oshirib yuborardi (Qulflar: row, table va advisory).
ON CONFLICT DO NOTHING — ikkinchi marta biriktirish xato emas, o'zgarishsiz (idempotent).
6. RLS: begona fayl ko'rinmaydi
vazifalar bazasida RLS 27-qismda yoqilgan edi. 201 fayllarga ham siyosat qo'shadi: faqat o'zining fayllari; ilova — vazifa orqali va fayl ham o'ziniki.
\set VERBOSITY terse
BEGIN;
SET LOCAL ROLE vazifalar_ilova;
SELECT set_config('ilova.foydalanuvchi_id', '2', true);
SELECT egasi_id, kalit FROM fayllar;
INSERT INTO fayllar (egasi_id, kalit, mime, hajm)
VALUES (1, 'begona.png', 'image/png', 10);
ROLLBACK;BEGIN
SET
set_config
------------
2
(1 row)
egasi_id | kalit
----------+-----------------
2 | fayllar/2/c.png
(1 row)
ERROR: new row violates row-level security policy for table "fayllar"
ROLLBACK2-foydalanuvchi faqat o'z faylini ko'rdi; 1-foydalanuvchi nomiga yozish — RLS xatosi. Kod WHERE egasi_id = $1 ni unutsa ham, baza boshqa odamning faylini bermaydi (Row Level Security). Kanon moduli baribir har so'rovda egasini yozadi — RLS — ikkinchi qatlam.
7. Yetim fayllar
7.1 Qayerdan paydo bo'ladi
- Presigned yuklash boshlandi, foydalanuvchi sahifani yopdi —
kutilmoqdaqoldi. - Fayl tasdiqlandi, lekin vazifaga biriktirilmadi (forma yuborilmadi).
- Ilova olib tashlandi yoki vazifa o'chirildi (yuqoridagi
CASCADE).
Qoida: biriktirilmagan va 24 soatdan eski — yetim. 24 soat — foydalanuvchiga formani tugatishga vaqt.
UPDATE fayllar SET yaratilgan_vaqt = now() - interval '2 days'
WHERE egasi_id = 1;
SELECT kalit, holat FROM fayllar f
WHERE f.yaratilgan_vaqt < now() - interval '24 hours'
AND NOT EXISTS (SELECT 1 FROM vazifa_ilovalari i
WHERE i.fayl_id = f.id)
ORDER BY kalit;UPDATE 3
kalit | holat
-------------------+------------
fayllar/1/a.png | tayyor
vaqtincha/1/x.pdf | tayyor
vaqtincha/1/y.pdf | kutilmoqda
(3 rows)a.png ham yetim — uning vazifasi o'chirilgan edi. NOT EXISTS — "ilovalarda yo'q" (Subquery va LATERAL).
7.2 Katta jadvalda tezligi
Kanon 100 000 fayl (90 % biriktirilgan) bilan tozalash so'rovining rejasini oldi (PostgreSQL 18.6, Docker, kanon mashinasi; Buffers va Index Searches qatorlari — PG 18 ning yangi chiqishi):
Limit (actual rows=100.00 loops=1)
Buffers: shared hit=12213
-> LockRows (actual rows=100.00 loops=1)
Buffers: shared hit=12213
-> Nested Loop Anti Join (actual rows=100.00 loops=1)
Buffers: shared hit=12113
-> Index Scan using fayllar_yaratilgan_vaqt_idx on fayllar f (actual rows=1885.00 loops=1)
Index Cond: (yaratilgan_vaqt < (now() - '24:00:00'::interval))
Index Searches: 1
Buffers: shared hit=4673
-> Index Scan using vazifa_ilovalari_fayl_id_idx on vazifa_ilovalari i (actual rows=0.95 loops=1885)
Index Cond: (fayl_id = f.id)
Index Searches: 1885
Buffers: shared hit=7440
Planning:
Buffers: shared hit=346
Planning Time: 2.463 ms
Execution Time: 6.437 msPastdan yuqoriga o'qiymiz (EXPLAIN ANALYZE'ni o'qish):
Index Scan … fayllar_yaratilgan_vaqt_idx— eski fayllarni vaqt tartibida oladi;- har biri uchun
vazifa_ilovalari_fayl_id_idxda "biriktirilganmi?" —Nested Loop Anti Join(NOT EXISTSning rejasi); - 1 885 ta qator ko'rildi, 100 ta yetim topilgach
Limitto'xtatdi; LockRows—FOR UPDATE SKIP LOCKED.
6.4 ms — 100 000 fayl ichida. Ikkala indeks bo'lmasa — butun jadval skani. Jadval hajmi: fayllar 46 MB, vazifa_ilovalari 20 MB.
7.3 Tozalash tartibi: avval ombor, keyin baza
Kanon yetimlarniTozala:
const r = await c.query<{ id: string; kalit: string }>(
`SELECT f.id, f.kalit FROM fayllar f
WHERE f.yaratilgan_vaqt < now() - make_interval(secs => $1)
AND NOT EXISTS (SELECT 1 FROM vazifa_ilovalari i
WHERE i.fayl_id = f.id)
ORDER BY f.yaratilgan_vaqt
LIMIT $2
FOR UPDATE OF f SKIP LOCKED`,
[yoshSoniya, partiya],
);
for (const q of r.rows) await ombor.ochir(q.kalit);
if (r.rows.length > 0) {
await c.query("DELETE FROM fayllar WHERE id = ANY($1::bigint[])",
[r.rows.map((q) => q.id)]);
}
await c.query("COMMIT");Nega avval ombor? Ikki variantni solishtiring:
| Tartib | O'rtada uzilsa | Natija |
|---|---|---|
| Ombor → baza | qator bazada qoladi | keyingi ishga tushishda qayta topiladi; ombordan o'chirish idempotent (204) |
| Baza → ombor | qator yo'q, fayl omborda | hech kim bilmaydigan, abadiy pul to'lanadigan bayt |
Birinchi tartibda xato o'zini tuzatadi. 3-mashqda buni simulyatsiya qilasiz.
FOR UPDATE SKIP LOCKED — API ikki nusxada ishlasa, ikkala tozalovchi bir vaqtda yursa ham, biri qulflagan qatorni ikkinchisi o'tkazib yuboradi (SKIP LOCKED bilan DB-asosli navbat). Partiya (100 ta) — tranzaksiya uzoq ochiq qolmasin.
Tozalovchini kim chaqiradi? Rejali vazifa (cron) — Cron va rejali vazifalar darsida. vaqtincha/ prefiksi uchun ombor o'zining lifecycle qoidasi bilan ham tozalashi mumkin — u ham o'sha darsda.
8. Vaqtincha → doimiy
Presigned yuklash vaqtincha/<egasi>/<uuid> ga tushadi. Tasdiqdan keyin server uni fayllar/ ga ko'chiradi (FaylOmbori.kochir — CopyObject, baytlar server orqali o'tmaydi) va bazadagi kalit ni yangilaydi:
stateDiagram-v2
[*] --> kutilmoqda: forma, vaqtincha prefiksi
kutilmoqda --> tayyor: tasdiq, kochir fayllar prefiksi
kutilmoqda --> ochirildi: 24 soat, tasdiq yo'q
tayyor --> biriktirilgan: vazifa_ilovalari
biriktirilgan --> tayyor: ilova olindi
tayyor --> ochirildi: 24 soat, yetim
ochirildi --> [*]Foydasi: vaqtincha/ dagi hamma narsa — "hali ishonchsiz". Ombor darajasida ham ularni alohida qoida bilan tozalash, ko'rish huquqini berMASlik oson.
9. Kanon modulini o'z bazamizda yurgizdik
Kanon testlari Postgres va MinIO talab qiladi. Biz Docker'ni yoqmadik: kanon metama.ts ni o'z PostgreSQL 17.6 klasterimizda (sxema 001–201) va soxta ombor bilan (ochir faqat kalitni yozib boradi) ishga tushirdik — dedup, parallel yuklash, parallel biriktirish va tozalash:
const a = await yukla(1, "bir xil matn");
const b = await yukla(1, "bir xil matn");
const c = await yukla(2, "bir xil matn");
console.log("dublikat:", a.dublikat, b.dublikat, c.dublikat,
"| a=b:", a.fayl.id === b.fayl.id,
"| a=c:", a.fayl.id === c.fayl.id);
const p = await Promise.all(
Array.from({ length: 10 }, () => yukla(1, "parallel")));
console.log("parallel 10: noyob id",
new Set(p.map((x) => x.fayl.id)).size,
"| yangi", p.filter((x) => !x.dublikat).length);
// … 15 ta faylni bitta vazifaga parallel biriktirish,
// begona vazifa, yetimlarni tozalash (partiya 3)Konsolda (biz ishga tushirdik: Node 24.21.0, pg 8.23.1, PostgreSQL 17.6):
dublikat: false true false | a=b: true | a=c: false
parallel 10: noyob id 1 | yangi 1
parallel 15 biriktirish: o'tdi 10 | rad 5 | limit: Bitta vazifaga ko'pi bilan 10 ta fayl
begona vazifa: topilmadi
yetimlar: 8 | omborda o'chirildi: 8 | bazada qoldi: 10- 10 ta parallel bir xil yuklash — bitta yozuv, bittasi "yangi", to'qqizi "dublikat".
- 15 ta parallel biriktirish — aynan 10 tasi o'tdi (
FOR UPDATE). - Begona vazifaga ulash — "topilmadi" (403 emas: borligini ham aytmaymiz).
- Yetimlar: 5 ta biriktirilmagan
p-*+ 3 ta dublikat sinovidagi — 8; omborda ham 8 taochir; qolgani — biriktirilgan 10 ta.
10. Hujumchi nigohi
- Begona faylni biriktirish.
POST /vazifalar/7/ilovalar { faylId: 123 }— 123 boshqa odamniki. Tekshiruvsiz u o'z vazifasi orqali begona faylni yuklab oladi. Himoya:fayllar.egasi_id = $egasi+ RLSWITH CHECK. - Global dedup yon kanali. Yuqorida aytildi. Himoya: dedup faqat egasi ichida.
- Limit poygasi. Parallel so'rovlar limitni oshiradi. Himoya:
FOR UPDATEyokiSERIALIZABLE. - ID taxmin qilish.
GET /fayllar/124— ketma-ket ID. Himoya: har yuklab olishda egalik tekshiruvi; havola faqat muddatli presigned. - Asl nom bilan hujum.
asl_nomdagi"; filename=evil.exe— sarlavhaga tushganda. Himoya: nom tozalanadi (Fayl yuklash xavfsizligi), sarlavhacontentDispositionbilan (Fayl yuklab berish va arxivlash).
11. Ko'p uchraydigan xatolar
| Xato | Sabab | Davosi |
|---|---|---|
| Ro'yxat uchun omborni aylanib chiqish | metama'lumot bazada yo'q | fayllar + vazifa_ilovalari |
| Bir xil fayl o'nlab nusxada | dedup yo'q | UNIQUE (egasi_id, sha256) + ON CONFLICT |
| Dedup ishlamaydi (parallel) | "avval SELECT, keyin INSERT" | cheklov + ON CONFLICT |
| Fayl o'chdi, ilova "singan" | ON DELETE CASCADE fayl FK'da |
RESTRICT, tozalash yetimlar orqali |
| Omborda egasiz baytlar | avval bazadan o'chirildi | avval ombor, keyin baza |
| Tozalovchilar bir faylni ikki marta oladi | qulfsiz SELECT |
FOR UPDATE SKIP LOCKED |
| Tozalash so'rovi sekin | indeks yo'q | yaratilgan_vaqt va fayl_id indekslari |
12. Mashqlar
Mashqlar kurs/mashqlar/29/06-metama/ da.
1-mashq (oson): to'g'ri yoki noto'g'ri
- Fayl baytlarini
byteaustunida saqlash — tavsiya etilgan yo'l. UNIQUE (egasi_id, sha256)ikkita NULL hash'li qatorga ruxsat beradi.- Vazifa o'chirilsa, unga biriktirilgan fayl ham bazadan darhol o'chadi.
- Yetimlarni tozalashda avval ombordan o'chirish xavfsizroq.
Yechim
- Noto'g'ri — baytlar omborda, bazada faqat metama'lumot.
- To'g'ri — NULL'lar teng emas.
- Noto'g'ri —
CASCADEfaqatvazifa_ilovalarini o'chiradi; fayl yetim bo'lib qoladi va keyin tozalanadi. - To'g'ri — uzilsa qator qoladi va qayta urinish mumkin.
2-mashq (o'rta): yetimlar ro'yxati
yetimlar(fayllar, ilovalar, hozir, yosh = 24 soat) funksiyasini yozing: { id, kalit, yaratilgan } (ms) massivi va { vazifaId, faylId } ilovalaridan biriktirilmagan va yosh dan eski fayllar kalitlarini eng eskisidan boshlab qaytarsin. Pastdagi ma'lumot bilan ikki marta chaqiring: sukut bilan va ilovalar = [], yosh = 36 soat bilan. Ishora: Set bilan biriktirilgan ID'lar (Set).
Yechim
const SOAT = 3600_000;
const hozir = Date.parse("2026-10-28T12:00:00Z");
const oldin = (soat) => hozir - soat * SOAT;
const royxat = [
{ id: 1, kalit: "fayllar/1/a.png", yaratilgan: oldin(48) },
{ id: 2, kalit: "vaqtincha/1/x.pdf", yaratilgan: oldin(30) },
{ id: 3, kalit: "vaqtincha/1/y.pdf", yaratilgan: oldin(2) },
{ id: 4, kalit: "fayllar/2/c.png", yaratilgan: oldin(72) },
];
const ilovalar = [{ vazifaId: 1, faylId: 1 }];
function yetimlar(fayllar, ilovalar, hozir, yosh = 24 * SOAT) {
const ulangan = new Set(ilovalar.map((i) => i.faylId));
return fayllar
.filter((f) => !ulangan.has(f.id) && f.yaratilgan < hozir - yosh)
.sort((a, b) => a.yaratilgan - b.yaratilgan)
.map((f) => f.kalit);
}
console.log(yetimlar(royxat, ilovalar, hozir));
console.log(yetimlar(royxat, [], hozir, 36 * SOAT));Konsolda:
[ 'fayllar/2/c.png', 'vaqtincha/1/x.pdf' ]
[ 'fayllar/2/c.png', 'fayllar/1/a.png' ]y.pdf atigi 2 soatlik — foydalanuvchi hali formani to'ldirayotgan bo'lishi mumkin. Bu JS — SQL'dagi NOT EXISTS + ORDER BY yaratilgan_vaqt ning aynan o'zi; farqi — baza buni indeks bilan millionlab qatorda qiladi.
3-mashq (qiyin): uzilgan tozalash
Soxta ombor(buziladigan) berilgan: ochir(k) uning buziladigan-chi chaqiruvida xato tashlaydi. tozala(baza, s3) funksiyasini yozing: baza (Set) dagi har kalitni avval s3 dan, keyin bazadan o'chirsin. 3-chaqiruvda buziladigan ombor bilan bir marta, xatoni ushlab, holatni chiqaring; so'ng qayta chaqirib, yakuniy holatni chiqaring.
function ombor(buziladigan) {
const bor = new Set(["a", "b", "c", "d"]);
let chaqiruv = 0;
return {
bor,
ochir(k) {
chaqiruv++;
if (chaqiruv === buziladigan) throw new Error(`tarmoq: ${k}`);
bor.delete(k);
},
};
}Yechim
function ombor(buziladigan) {
const bor = new Set(["a", "b", "c", "d"]);
let chaqiruv = 0;
return {
bor,
ochir(k) {
chaqiruv++;
if (chaqiruv === buziladigan) throw new Error(`tarmoq: ${k}`);
bor.delete(k);
},
};
}
function tozala(baza, s3) {
for (const k of [...baza]) {
s3.ochir(k);
baza.delete(k);
}
}
const baza = new Set(["a", "b", "c", "d"]);
const s3 = ombor(3);
try { tozala(baza, s3); } catch (e) { console.log(e.message); }
console.log("baza:", [...baza], "| s3:", [...s3.bor]);
tozala(baza, s3);
console.log("baza:", [...baza], "| s3:", [...s3.bor]);Konsolda:
tarmoq: c
baza: [ 'c', 'd' ] | s3: [ 'c', 'd' ]
baza: [] | s3: []Uzilishdan keyin baza va ombor bir xil holatda — c va d ikkalasida bor. Qayta urinish hammasini tozaladi. Tartibni almashtiring (baza.delete avval) — uzilishdan keyin bazada d qoladi, omborda esa c va d: c endi hech qachon topilmaydi. [...baza] — Set ni aylanib chiqayotganda o'chirmaslik uchun nusxa.
13. Loyiha qadami: 201-fayllar.sql va metama.ts
Kanon ikki artefakt yozdi: sxema («Sxema» bo'limidagi CREATE TABLE lar + RLS siyosatlari) va metama.ts moduli. Modul funksiyalari:
| Funksiya | Nima qiladi |
|---|---|
tayyorFaylYoz(db, f) |
INSERT … ON CONFLICT DO NOTHING; dublikat bo'lsa eski qator, dublikat: true |
biriktir(db, egasi, vazifa, fayl, nom) |
tranzaksiya: egalik, holat, limit 10, FOR UPDATE, idempotent |
ilovalar(db, egasi, vazifa) |
vazifa fayllari ro'yxati (nom, tur, hajm, kalit) |
ilovaniOl(db, egasi, vazifa, fayl) |
ilovani olib tashlaydi (fayl yetim bo'ladi) |
yetimlarniTozala(db, ombor, yosh, partiya) |
avval ombor, keyin baza; SKIP LOCKED, partiyalab |
tayyorFaylYoz — dublikat holati:
export async function tayyorFaylYoz(
db: Pool | PoolClient,
f: { egasiId: number; kalit: string; mime: string;
hajm: number; sha256: string },
): Promise<{ fayl: Fayl; dublikat: boolean }> {
const r = await db.query<FaylQator>(
`INSERT INTO fayllar (egasi_id, kalit, mime, hajm, sha256,
holat, tasdiqlangan_vaqt)
VALUES ($1, $2, $3, $4, decode($5, 'hex'), 'tayyor', now())
ON CONFLICT (egasi_id, sha256) DO NOTHING
RETURNING id, egasi_id, kalit, mime, hajm, sha256, holat`,
[f.egasiId, f.kalit, f.mime, f.hajm, f.sha256],
);
const yangi = r.rows[0];
if (yangi) return { fayl: qatordan(yangi), dublikat: false };
const eski = await db.query<FaylQator>(
`SELECT id, egasi_id, kalit, mime, hajm, sha256, holat
FROM fayllar
WHERE egasi_id = $1 AND sha256 = decode($2, 'hex')`,
[f.egasiId, f.sha256],
);
const q = eski.rows[0];
if (!q) {
throw new Error("ON CONFLICT bo'ldi, lekin qator topilmadi" +
" (parallel o'chirildi)");
}
return { fayl: qatordan(q), dublikat: true };
}dublikat: true bo'lsa chaqiruvchi yangi yuklangan obyektni ombordan o'chiradi. decode($5, 'hex') — JS'dagi hex satr bytea ga.
Kanon testlari test/fayl-metama.test.ts da — 7 ta. Ular Postgres va MinIO bilan, har test fayli shablon bazadan vaqtinchalik nusxada ishlaydi. Tekshiriladi: dedup, parallel 10 → 1 yozuv, begona vazifa/fayl → topilmadi va limit, parallel 15 → aynan 10, yetimlar ombordan ham o'chadi, RESTRICT/CASCADE, RLS. Kanonda hammasi o'tdi (82/82). Bizda MinIO yo'q — to'plam ﹣ … # DATABASE_URL yoki S3_ENDPOINT yo'q bilan o'tkazildi, shuning uchun mantiqni «Kanon modulini o'z bazamizda yurgizdik» bo'limida soxta ombor bilan yurgizdik.
Commit xabari (modul va sxema uchun):
M='feat(fayl): fayllar va vazifa_ilovalari — egasi bo'yicha dedup,'
M+=' biriktirish, yetimlar (201)'
git commit -m "$M"vazifalar-api ga ulash 28-qism (auth) tugagach: migratsiya, uchta marshrut va tozalovchi cron.
14. Real ishda
- Fayl bilan ishlaydigan ko'p ilovalar shu naqshni qo'llaydi: metama'lumot bazada, baytlar object storage'da.
- Dedup — bulut xizmatlarida keng; foydalanuvchilararo dedup yon kanal sababli ehtiyotkorlik bilan qilinadi.
- Kvota — "foydalanuvchiga 1 GB":
SELECT sum(hajm) FROM fayllar WHERE egasi_id = $1— omborni so'ramasdan. - Intervyuda: "Fayl metama'lumotini qayerda saqlaysiz?", "Ombor va baza nomuvofiqligini qanday tozalaysiz?", "Dedup qanday ishlaydi?"
Xulosa
- Baytlar — omborda, metama'lumot (kalit, tur, hajm, sha256, holat, egasi) — bazada.
UNIQUE (egasi_id, sha256)+ON CONFLICT DO NOTHING— atomik dedup; faqat egasi ichida (yon kanal).CHECKlar yarim holatni (hash'siz "tayyor"), SVG va katta faylni baza darajasida to'sadi.- Fayl FK
RESTRICT, vazifa FKCASCADE— vazifa o'chsa fayl yetim bo'ladi. - Biriktirish tranzaksiyada: egalik, holat, limit,
FOR UPDATE. - Yetimlar: avval ombor, keyin baza;
FOR UPDATE SKIP LOCKED, partiya; 100 000 faylda 6.4 ms.
Keyingi dars: Rasmlarni qayta ishlash: sharp — yuklangan rasmdan kichik ko'rinishlar, WebP, EXIF/GPS tozalash va piksel bomba.
Manbalar
- PostgreSQL 18 — unique cheklovlar va NULL: https://www.postgresql.org/docs/18/ddl-constraints.html
- PostgreSQL 18 —
INSERT … ON CONFLICT: https://www.postgresql.org/docs/18/sql-insert.html - PostgreSQL 18 —
SELECT … FOR UPDATE SKIP LOCKED: https://www.postgresql.org/docs/18/sql-select.html#SQL-FOR-UPDATE-SHARE - PostgreSQL 18 — Row Security Policies: https://www.postgresql.org/docs/18/ddl-rowsecurity.html
- Kanon tajribasi (IlmHamroh, 2026-10-27): PostgreSQL 18.6 (Docker), 100 000 fayl EXPLAIN,
metama.tstestlari (Postgres + MinIO). SQL bloklari, soxta ombor bilan modul va mashq yechimlari — biz ishga tushirdik (2026-10-28, PostgreSQL 17.6, Node 24.21.0).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!