Bu darsda nima bor
1–4-qismlar EDAni noldan quradi: nega kerak, 4 ta asosiy tushuncha, xabar turlari va NestJS+RabbitMQ implementatsiya. 5–6-qismlar — productionda hal qilinishi shart bo‘lgan ishonchlilik (idempotency/outbox/DLQ) va broker tanlovi. 7-qism EDAning qorong‘i tomoni va uni qachon ishlatmaslik kerakligini ochadi.
Nega kerak — bog‘liqlik muammosi
Oddiy o‘xshatish: har bir kishiga alohida qo‘ng‘iroq qilib “buni qil” deyish o‘rniga, e’lonlar taxtasiga xabar osasiz: “Buyurtma yaratildi”. Kim qiziqsa — o‘qib, o‘z ishini qiladi. Siz kim o‘qishini, qachon o‘qishini bilishingiz shart emas.
1-mavzuda mikroservislar bir-birini to‘g‘ridan chaqirardi. Lekin shunday chaqiruv ikki tomonni mahkam bog‘laydi. Buyurtma xizmati email, ombor, analitika va sodiqlikni o‘zi chaqirsa:
// MUAMMO: hamma narsani birma-bir, SINXRON chaqiramiz
async createOrder(dto) {
const order = await this.orders.save(dto);
await this.email.sendConfirmation(order); // kutamiz...
await this.inventory.reserve(order.items); // kutamiz...
await this.analytics.track(order); // kutamiz...
await this.loyalty.addPoints(order.userId); // kutamiz...
return order;
// email xizmati 7s sekin bo'lsa -> mijoz 7s kutadi
// analytics o'lik bo'lsa -> BUTUN buyurtma yiqiladi
// yangi "consumer" qo'shsang -> SHU kodni yana o'zgartirasan
}
1. Producer hamma consumer javobini kutadi — eng sekin halqa butun zanjirni sekinlashtiradi.
2. Bitta consumer o‘lik bo‘lsa — butun amal yiqiladi (mo‘rtlik).
3. Yangi reaksiya qo‘shmoqchi bo‘lsang (masalan SMS) — producer kodini har safar o‘zgartirasan (yopiq emas).
4. Producer 4 ta xizmatning manzilini, kontraktini va xatosini bilishga majbur.
Xizmatlar bir-birini chaqirmaydi. Producer faqat “nima sodir bo‘lganini” e’lon qiladi (order.created) va darhol davom etadi. Qiziqqan consumerlar o‘zi obuna bo‘lib, asinxron reaksiya bildiradi. Producer ularni bilmaydi ham — bu bo‘sh bog‘lanish (loose coupling).
Asosiy g‘oya — 4 ta tushuncha
EDA atigi 4 ta tushunchadan iborat. Ularni aniq tushunsang, qolgani oson:
| Tushuncha | Ma’nosi |
|---|---|
| Event (hodisa) | Sodir bo‘lib bo‘lgan fakt — o‘tgan zamonda nomlanadi: OrderCreated, PaymentFailed. O‘zgarmas (immutable), bekor qilib bo‘lmaydi. |
| Producer | Hodisani e’lon qiluvchi. Kim ushlashini bilmaydi va bilishi shart emas. |
| Broker | Hodisani qabul qilib, saqlab, obunachilarga yetkazuvchi vositachi (RabbitMQ, Kafka). |
| Consumer | Hodisaga obuna bo‘lib, reaksiya bildiruvchi. Bir hodisani ko‘p consumer ushlashi mumkin. |
// YECHIM: producer faqat FAKTNI e'lon qiladi, kim ushlashini bilmaydi
async createOrder(dto) {
const order = await this.orders.save(dto);
this.bus.emit('order.created', new OrderCreated(order)); // bir zumda qaytadi
return order;
}
// email/inventory/analytics/loyalty -> har biri mustaqil tinglaydi.
// Yangi consumer qo'shish: producer'ga TEGMAYSAN.
Sinxronda producer consumerni biladi. EDAda consumer producerni biladi (qaysi eventga obuna bo‘lishni o‘zi tanlaydi). Bog‘liqlik yo‘nalishi teskari aylandi — shuning uchun producer hech narsani o‘zgartirmasdan yangi consumerlar qo‘shiladi.
Command/Event va Queue/Pub-Sub
EDAda eng ko‘p chalkashtiriladigan narsa — xabar turlari va tarqatish shakllari. Bularni aralashtirsang, arxitekturang noto‘g‘ri ishlaydi.
Xabar turlari — Command vs Event
| Command (buyruq) | Event (hodisa) | |
|---|---|---|
| Ma’no | “Buni qil” — buyruq | “Bu sodir bo‘ldi” — fakt |
| Nomlash | Buyruq mayli: SendEmail | O‘tgan zamon: OrderCreated |
| Qabul qiluvchi | Bitta handler (aniq egasi bor) | 0..N consumer (kim qiziqsa) |
| Bog‘liqlik | Yuboruvchi natijani biladi | Yuboruvchi kim ushlashini bilmaydi |
Tarqatish shakli — Queue vs Pub/Sub
Bu eng muhim farq. Queue (navbat): bitta xabarni faqat bitta consumer oladi — yuk bir nechta nusxa o‘rtasida taqsimlanadi (competing consumers). Pub/Sub (fan-out): bitta xabarning nusxasi har bir obunachiga boradi.
Bitta xizmatning bir nechta nusxasi orasida ishni bo‘lish kerakmi? → Queue (har nusxa raqobatlashadi). Turli xizmatlar bir xil hodisaga reaksiya bildirishi kerakmi? → Pub/Sub (har xizmatga o‘z navbati). RabbitMQda buni exchange + har consumerga alohida navbat orqali quriladi.
NestJS implementatsiya (RabbitMQ)
@EventPattern · emit · ack1-mavzudagi loyihani davom ettiramiz, lekin endi haqiqiy pub/sub bilan: Orders bitta hodisa tashlaydi, uchta mustaqil xizmat uni oladi. RabbitMQ ishlatamiz.
1 · Producer — hodisani e’lon qiladi
// orders/src/app.module.ts — producer EVENT_BUS klientini ulaydi
ClientsModule.register([{
name: 'EVENT_BUS',
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
exchange: 'domain_events', // umumiy topic-exchange
exchangeType: 'topic',
wildcards: true,
queueOptions: { durable: true },
},
}])
// orders.service.ts
this.bus.emit('order.created', { // 'order.created' = routing key
eventId: randomUUID(),
orderId: order.id,
userId: order.userId,
total: order.total,
});
2 · Consumer — o‘z navbati bilan obuna bo‘ladi
// notifications/src/main.ts — HAR consumer O'Z navbatiga ega (fan-out shu tarzda)
const app = await NestFactory.createMicroservice<MicroserviceOptions>(AppModule, {
transport: Transport.RMQ,
options: {
urls: ['amqp://localhost:5672'],
exchange: 'domain_events',
exchangeType: 'topic',
wildcards: true,
queue: 'notifications_queue', // <-- inventory_queue, analytics_queue ... har xil
routingKey: 'order.*', // qaysi hodisalarni tinglaydi
queueOptions: { durable: true },
noAck: false, // qo'lda tasdiqlash
},
});
await app.listen();
3 · Handler — reaksiya va tasdiq (ack)
// notifications/src/order.consumer.ts
@EventPattern('order.created')
async handle(@Payload() data: any, @Ctx() ctx: RmqContext) {
const channel = ctx.getChannelRef();
const msg = ctx.getMessage();
try {
await this.email.sendConfirmation(data.userId, data.orderId);
channel.ack(msg); // muvaffaqiyat -> tasdiq
} catch (err) {
// requeue=false -> qayta navbatga solmaymiz, DLQ ga ketadi
channel.nack(msg, false, false);
}
}
exchange, wildcards, routingKey kabi sozlama nomlari @nestjs/microservices versiyasiga qarab biroz farq qilishi mumkin. Asosiy g‘oya o‘zgarmaydi: har consumer xizmatga alohida durable navbat + ularni bitta exchangega bog‘lash = fan-out.
Ishonchlilik — idempotency, outbox, DLQ
at-least-once · outbox · DLQBu yerda “ishlaydi” bilan “productionda ishonchli” farqlanadi. Tarmoq va broker mukammal emas: xabar yo‘qolishi, ikki marta yetishi yoki tartibsiz kelishi mumkin. Professional EDA aynan shularni boshqaradi.
Yetkazish kafolatlari — afsona va haqiqat
| Kafolat | Ma’nosi | Amalda |
|---|---|---|
| at-most-once | Ko‘pi bilan 1 marta (yo‘qolishi mumkin) | Kam ishlatiladi (ma’lumot yo‘qoladi) |
| at-least-once | Kamida 1 marta (takror bo‘lishi mumkin) | Standart tanlov + idempotency |
| exactly-once | Aniq 1 marta | Deyarli afsona — amalda “at-least-once + idempotency” bilan taqlid qilinadi |
Demak qoida: at-least-once’ni qabul qil, consumerni idempotent qil.
1 · Idempotency — takrorga chidamlilik
// IDEMPOTENCY: bir hodisa 2 marta yetsa ham, bir marta ishlanadi
@EventPattern('order.created')
async handle(@Payload() data, @Ctx() ctx) {
const channel = ctx.getChannelRef(); const msg = ctx.getMessage();
if (await this.processed.exists(data.eventId)) { // allaqachon ishlangan?
return channel.ack(msg); // -> tashlab yubor (no-op)
}
await this.doWork(data);
await this.processed.save(data.eventId); // belgilab qo'yamiz
channel.ack(msg);
}
2 · Outbox pattern — eng muhim professional naqsh
Bazaga yozish va brokerga event yuborish — ikki alohida tizim, ularni bitta atomik amalda bajarib bo‘lmaydi. Bazaga yozdingu, event yuborishdan oldin server qulasa — ma’lumot bor, lekin hech kim xabardor emas. Yoki teskarisi.
// 1) DUAL-WRITE muammosini hal: order + outbox BITTA tranzaksiyada
await this.prisma.$transaction([
this.prisma.order.create({ data: order }),
this.prisma.outbox.create({ data: {
id: randomUUID(), type: 'order.created',
payload: JSON.stringify(order), sent: false,
}}),
]);
// endi "baza yozildi-yu, event yo'qoldi" holati BO'LMAYDI
// 2) Alohida RELAY yuborilmaganlarni brokerga uzatadi
@Cron('*/2 * * * * *')
async relay() {
const pending = await this.prisma.outbox.findMany({
where: { sent: false }, take: 50, orderBy: { createdAt: 'asc' },
});
for (const e of pending) {
this.bus.emit(e.type, { eventId: e.id, ...JSON.parse(e.payload) });
await this.prisma.outbox.update({ where: { id: e.id }, data: { sent: true } });
}
}
3 · Dead Letter Queue (DLQ) — “o‘lik xatlar”
Bir xabar qayta-qayta xato bersa (poison message), uni cheksiz qayta urinish butun navbatni bloklaydi. Yechim — bunday xabarni alohida DLQga surib, navbatni bo‘shatish; keyin uni tinch o‘rganasan.
// DLQ: nack qilingan (ishlanmagan) xabar yo'qolmaydi, "o'lik xat navbati"ga ketadi
queueOptions: {
durable: true,
deadLetterExchange: 'dlx', // nack -> shu exchange'ga
deadLetterRoutingKey: 'order.failed',
}
// dlx -> dead_letter_queue ga bog'lanadi; u yerdagi xabarlarni
// keyin qo'lda yoki avtomatik qayta ishlaysan / tahlil qilasan
EDAda global tartib kafolati yo‘q. Tartib muhim bo‘lsa (masalan, bitta hisob bo‘yicha amallar), bitta kalit (aggregate id)ni bitta navbat/partisiyaga yo‘naltirasan — shunda shu kalit ichida tartib saqlanadi. Kafkada bu partition key orqali tabiiy bo‘ladi.
Kafka vs RabbitMQ va event sxemasi
broker tanlovi · versioningIkki yetakchi broker bor va ular tubdan farq qiladi. To‘g‘ri tanlash — professional qaror.
| Mezon | RabbitMQ | Kafka |
|---|---|---|
| Model | “Aqlli broker” — navbat, marshrutlash | “Doimiy log” — ketma-ket yozuv |
| Yetkazish | Broker consumerga itaradi (push) | Consumer o‘zi o‘qiydi (pull), offsetni boshqaradi |
| Saqlash | Ishlangach o‘chadi | Muddat bo‘yicha saqlanadi — qayta o‘qish (replay) mumkin |
| Tartib | Navbat ichida | Partisiya ichida (kalit bo‘yicha) |
| Hajm | O‘rta–yuqori | Juda yuqori (millionlab/sek) |
| Marshrutlash | Kuchli (exchange, routing key) | Oddiy (topic + partition) |
| Eng mos | Vazifa navbatlari, murakkab marshrut, RPC | Event oqimi, log, analitika, event sourcing, replay |
“Ishni bajaruvchilarga taqsimla, murakkab marshrut kerak” → RabbitMQ. “Katta hajmli hodisa oqimi, tarix saqlansin, keyin qayta o‘qiy olay” → Kafka. Ko‘p kompaniyalar ikkalasini birga ishlatadi.
Event sxemasini loyihalash — Thin vs Fat
Eventga qancha ma’lumot solish kerak? Ikki yondashuv, ikki tomonlama kelishuv:
// THIN event (notification) — faqat ID. Consumer kerak bo'lsa o'zi so'rab oladi.
class OrderCreated {
constructor(public eventId: string, public orderId: string) {}
}
// + Kichik, bog'liqlik kam. - Consumer qo'shimcha so'rov qiladi (chaqiruv ko'payadi).
// FAT event (event-carried state transfer) — kerakli ma'lumot ichida.
class OrderCreated {
constructor(
public eventId: string, public orderId: string,
public userId: string, public total: number, public items: Item[],
) {}
}
// + Consumer mustaqil, qo'shimcha so'rovsiz. - Kattaroq, ma'lumot dublikati.
Sxema versiyalash — consumerlarni sindirmaslik
Event — bu xizmatlar orasidagi ommaviy kontrakt. Uni o‘zgartirsang, sen bilmagan consumerlar sinadi. Oltin qoida — faqat qo‘shimcha (backward-compatible) o‘zgartirishlar:
// SXEMA VERSIYALASH: eski consumerlarni sindirmaslik uchun FAQAT qo'shimcha
// o'zgartirishlar (additive) qil — maydon o'chirma, tip o'zgartirma.
interface OrderCreatedV1 { eventId: string; orderId: string; total: number; }
interface OrderCreatedV2 extends OrderCreatedV1 {
currency?: string; // YANGI maydon optional -> eski consumer buzilmaydi
}
// Katta o'zgarishda: 'order.created.v2' nomli YANGI hodisa chiqar,
// eskisini bir muddat parallel qo'llab-quvvatla, keyin to'xtat.
Qorong‘i tomoni va qachon ishlatmaslik
observability · eventual consistencyEDA bepul emas. U bog‘liqlikni kamaytiradi, lekin murakkablikni boshqa joyga ko‘chiradi. Buni bilmaslik — “event spaghetti”ga olib keladi.
1. Ko‘rinmas oqim: “bu event qayoqqa boradi?” — kodga qarab bilib bo‘lmaydi, chunki producer consumerni bilmaydi.
2. Yashirin bog‘liqlik: event sxemasi orqali xizmatlar baribir bog‘langan — uni o‘zgartirsang, uzoqdagi consumer sinadi.
3. Zanjirli hodisalar (event storm): bitta event boshqasini, u yana boshqasini chaqiradi — nazoratdan chiqadi.
4. Debug qiyin: xato asinxron, bir necha hop narida sodir bo‘ladi.
Yechim — Observability majburiy
EDAda kuzatuvchanliksiz yashab bo‘lmaydi. Har eventga correlation_id (butun oqimni bog‘lovchi) va causation_id (sababni ko‘rsatuvchi) qo‘shasan:
// Har eventga kuzatuv ma'lumotini qo'shamiz (12-mavzu: Observability bilan bog'liq)
this.bus.emit('order.created', {
eventId: randomUUID(),
correlationId: ctx.correlationId, // BUTUN oqim bo'ylab bir xil -> trace
causationId: ctx.currentEventId, // bevosita sabab bo'lgan event
...payload,
});
// Endi loglarda correlationId bo'yicha filtrlab, butun zanjirni ko'rasan.
EDAda ma’lumot darhol izchil bo‘lmaydi. Foydalanuvchi buyurtma berdi-yu, “Mening buyurtmalarim”da hali ko‘rinmasligi mumkin (event hali ishlanmagan). UI buni hisobga olishi kerak: “Qabul qilindi, ishlanmoqda…” kabi holatlar.
Oddiy CRUD — bitta amal, bitta natija bo‘lsa, event keraksiz murakkablik.
Darhol javob/izchillik kerak bo‘lsa (masalan, “balansni hozir ko‘rsat”) — sinxron chaqiruv to‘g‘riroq.
Hammasini eventga aylantirma. EDA — bitta voqea ko‘p mustaqil reaksiya keltirganda yoki xizmatlarni ajratish kerak bo‘lganda kuchli. Aks holda — oddiy chaqiruvdan boshla.
Eslab qolish kerak bo‘lgan 5 jumla
- Event = o‘tgan zamondagi o‘zgarmas fakt (
OrderCreated). Producer e’lon qiladi, kim ushlashini bilmaydi — bog‘liqlik teskari aylanadi. - Command = bitta egasi; Event = 0..N consumer. Queue = yuk taqsimlash (bir nusxa); Pub/Sub = fan-out (har kimga nusxa).
- “at-least-once’ni qabul qil, consumerni idempotent qil”. Tartib kerak bo‘lsa — aggregate kaliti bo‘yicha partisiya.
- Outbox pattern dual-write muammosini hal qiladi; DLQ poison message’dan saqlaydi; event sxemasini faqat qo‘shimcha o‘zgartir.
- RabbitMQ = navbat/marshrut; Kafka = log/replay. EDA bepul emas — correlation_id va kuzatuvchanlik majburiy; oddiy CRUDda ishlatma.
Keyingi mavzu — tezlik va masshtabning birinchi quroli:
“4-mavzu: Caching” — Redis, cache-aside, invalidatsiya, stampede va NestJS implementatsiyasi bilan.