IlmHamroh
JavaScript Full-stack/30-qism. NestJS46/65-dars18 daqiqa
Mundarija (28)

Transport'lar chuqur NestJS'da: Redis, NATS, RabbitMQ, Kafka, gRPC va o'z transportingiz

Qisqacha: Nest mikroservis kodi transportdan deyarli mustaqil: @MessagePattern va ClientProxy bir xil, faqat transport sozlamasi o'zgaradi. Lekin ishonchlilik transportga bog'liq. TCP — to'g'ridan ulanish: xizmat o'chiq bo'lsa, xabar yo'qoladi. Broker'lar (RabbitMQ, Kafka, NATS JetStream) xabarni saqlaydi. RabbitMQ transportida Nest sukut bo'yicha noAck: true — ishchi yiqilsa, xabar yo'qoladi; kafolat uchun noAck: false va qo'lda ack. gRPC — .proto shartnoma, deadline va status kodlari bilan. Har transportning npm paketi alohida: masalan Redis transporti ioredis ni talab qiladi.

Bu darsda

  • Nest transportlarini (TCP, Redis, NATS, RabbitMQ, Kafka, MQTT, gRPC) va har biriga kerak paketni bilasiz.
  • gRPC transportida @GrpcMethod, server oqimi, RpcException status kodi va deadline'ni ishlatasiz.
  • RabbitMQ'da noAck, durable va qo'lda ack nima uchun kerakligini tushuntirasiz.
  • Serializer bilan xabar formatini boshqarasiz va retry bilan ulanish xatosini yumshatasiz.
  • O'z transportingizni (CustomTransportStrategy) yozib, Nest transportlari ichidan qanday tuzilganini ko'rasiz.

Oldin bilishingiz kerak: Mikroservis asoslari NestJS'da, Boshqa xabar broker'lari, gRPC va Protocol Buffers, BullMQ ishonchlilik.

Oldingi darsda TCP bilan mikroservis yozdik. Shu darsning oxirgi qismida xizmatni o'chirib, gateway nima qaytarishini ko'rishni taklif qilgandik. Bu darsni shu savoldan boshlaymiz.

1. Nega bu kerak?

Hisobot xizmati qayta ishga tushirilyapti — 2 soniya. Shu orada gateway send qildi (biz ishga tushirdik, NestJS 12.2.0):

text
3) servissiz: connect ECONNREFUSED 127.0.0.1:4595

Tarjimasi: "ulanish rad etildi: 127.0.0.1:4595". TCP transportida xabar hech qayerda saqlanmaydi: mijoz to'g'ridan xizmat soketiga yozadi. Xizmat yo'q — xabar yo'q. Hodisa (emit) bo'lsa, hech kim bilmaydi ham.

Brokerli transportda zanjirning o'rtasida uchinchi dastur — broker turadi. Mijoz xabarni brokerga beradi, broker uni navbatda saqlaydi va xizmat tayyor bo'lganda yetkazadi. Boshqa xabar broker'lari darsida RabbitMQ, Kafka va NATS'ni xom kutubxonalar bilan sinagandik. Bu dars — ularning Nest transporti sifatidagi ko'rinishi.

O'xshatish: telefon va pochta qutisi. TCP — telefon qo'ng'irog'i: odam javob bermasa, gap yetmaydi. Broker — pochta qutisi: xat qutida kutadi, egasi qaytganda o'qiydi. Lekin quti ham "xat o'qildi" degan belgini (ack) olmaguncha xatni o'chirmasligi kerak.

2. Transportlar xaritasi

2.1 Jadval va paketlar

Nest @nestjs/microservices paketida hamma transport kodi bor, lekin har birining klient kutubxonasi alohida o'rnatiladi. Ro'yxat — 12.2.0 paketining peerDependencies idan (biz paket ichidan ko'chirdik):

Transport Kerakli paket Xususiyati
TCP — brokersiz, eng sodda
REDIS ioredis Pub/Sub: saqlamaydi
NATS @nats-io/transport-node tez, queue group; saqlash — JetStream
RMQ amqplib, amqp-connection-manager navbat, ack, marshrutlash
KAFKA kafkajs jurnal, partition, qayta o'qish
MQTT mqtt IoT qurilmalari
GRPC @grpc/grpc-js, @grpc/proto-loader .proto shartnoma, HTTP/2

Ikki diqqat qiladigan joy:

  • NATS — Nest 12 yangi @nats-io/transport-node paketini ishlatadi (nats.js 3-avlodi). Eski maqolalardagi nats paketi 12 da ishlamaydi.
  • Redis — ioredis. vazifalar-api esa 27-qismdan beri yagona Redis klienti sifatida node-redis ishlatadi (29-qism qarori). Redis transportini qo'shish — ikkinchi Redis klienti, ikkinchi ulanish sozlamasi. Bu ham kanon uchun "yo'q" deyish sabablaridan biri.

2.2 Bir xil kod — boshqa transport

Controller kodi o'zgarmaydi. Faqat createMicroservice va ClientsModule sozlamasi:

ts
// TCP
{ transport: Transport.TCP,
  options: { host: "127.0.0.1", port: 4591 } }
// Redis
{ transport: Transport.REDIS,
  options: { host: "localhost", port: 6379 } }
// NATS
{ transport: Transport.NATS,
  options: { servers: ["nats://localhost:4222"], queue: "hisobot" } }

Bu Nest mikroservislarining eng kuchli tomoni: bugun TCP bilan boshlab, ertaga RabbitMQ'ga o'tish — sozlama o'zgarishi. Lekin "kod o'zgarmaydi" — "xulq o'zgarmaydi" degani emas. Keyingi bo'limlar shu farq haqida.

Diqqat: Redis, NATS, RabbitMQ va Kafka transportlarini biz sinamadik: ular Docker'da broker talab qiladi. Bu bo'limlardagi sozlamalar — Nest 12.2.0 paketining tur fayllaridan (microservice-configuration.interface.d.ts) va manba kodidan, natija chiqishlari yo'q. gRPC, TCP va o'z transportimiz — ishga tushirilgan.

3. Redis va NATS

Redis transporti — Pub/Sub ustida (Pub/Sub va Redis Streams darsida ko'rgansiz). Xabar faqat shu daqiqada obuna bo'lganlarga yetadi: xizmat o'chiq bo'lsa — xabar yo'qoladi, TCP'dagidek. Afzalligi — xizmatlar bir-birining manzilini bilmaydi, hammasi Redis'ga ulanadi; bir nechta nusxa bo'lsa, hodisa hammasiga boradi. wildcards: true — Redis PSUBSCRIBE naqshlari (vazifa.*).

NATS transporti — tez xabar tizimi. Muhim sozlama — queue (queue group): bir xil queue dagi xizmat nusxalaridan har xabarni bittasi oladi — yuk taqsimlanadi. queue siz — har nusxa oladi. Oddiy NATS ham xabarni saqlamaydi; saqlash va qayta yetkazish — JetStream. Nest 12.2.0 ning NATS transporti manba kodida JetStream yo'q (biz qidirib ko'rdik) — u oddiy NATS ustida ishlaydi.

Ikkalasi ham "tez, lekin kafolatsiz" toifasida. Kafolat kerak bo'lsa — RabbitMQ yoki Kafka.

4. RabbitMQ: ack va durable

4.1 Sozlama

ts
{
  transport: Transport.RMQ,
  options: {
    urls: ["amqp://localhost:5672"],
    queue: "hisobot",
    queueOptions: { durable: true },
    noAck: false,
    prefetchCount: 10,
  },
}

Uch sozlama ishonchlilikni hal qiladi:

  • queueOptions: { durable: true } — navbat broker qayta ishga tushganda ham saqlanadi (Boshqa xabar broker'lari). Usiz — RabbitMQ qayta yonganda navbat va undagi xabarlar yo'qoladi.
  • noAck — xabar qachon "bajarildi" deb hisoblanadi. Nest manbasida sukut qiymati: RMQ_DEFAULT_NOACK = true (biz 12.2.0 paketining constants.js faylida ko'rdik). Rasmiy hujjatning RabbitMQ sahifasi ham shuni yozadi: sukut — true, false qo'lda tasdiqlash rejimini yoqadi. Ya'ni xabar ishchiga berilishi bilan broker uni o'chiradi. Handler xato bersa yoki jarayon yiqilsa — xabar yo'q.
  • prefetchCount — bitta ishchi bir vaqtda nechta tasdiqlanmagan xabar oladi. Kichik qiymat — yuk nusxalar orasida tekis taqsimlanadi.

4.2 Qo'lda ack

noAck: false bo'lsa, handler xabarni o'zi tasdiqlashi shart:

ts
import {
  Ctx, EventPattern, Payload, RmqContext,
} from "@nestjs/microservices";

@EventPattern("vazifa.bajarildi")
async bajarildi(@Payload() d: Vazifa, @Ctx() ctx: RmqContext) {
  const kanal = ctx.getChannelRef();
  const xabar = ctx.getMessage();
  await this.email.yubor(d); // ish
  kanal.ack(xabar); // faqat muvaffaqiyatdan keyin
}

ack chaqirilmasa — xabar "tasdiqlanmagan" bo'lib qoladi va ishchi ulanishi uzilganda broker uni boshqa ishchiga qayta beradi. Xato bo'lsa, kanal.nack(xabar, false, true) — qayta navbatga, yoki false — tashlash (dead letter'ga). Nest manbasida yana bir xulq bor: noAck: false bo'lganda handler'i yo'q pattern kelsa, Nest xabarni o'zi nack qiladi (qayta navbatga qo'ymasdan) va ogohlantirish yozadi.

Kafolat bepul emas. "Kamida bir marta" (at-least-once) yetkazishda bir xabar ikki marta kelishi mumkin: ishchi ishni bajardi, ack dan oldin yiqildi — broker xabarni qayta beradi. Shuning uchun handler idempotent bo'lishi kerak (3-mashq) — BullMQ ishonchlilik darsidagi qoida.

Tekshirib ko'ring: noAck: false qilindi, lekin handler'da kanal.ack(xabar) yozish unutildi. Nima bo'ladi?

Javob

Xabarlar bajariladi, lekin broker ularni "tasdiqlanmagan" deb ushlab turadi. prefetchCount (masalan 10) to'lgach, broker bu ishchiga yangi xabar bermaydi — ishchi "qotib" qoladi. Ishchi qayta ishga tushsa, tasdiqlanmagan xabarlarning hammasi qayta yetkaziladi va ikkinchi marta bajariladi. Ya'ni ack'ni unutish — ham to'xtash, ham takror.

5. Kafka

ts
{
  transport: Transport.KAFKA,
  options: {
    client: { clientId: "hisobot", brokers: ["localhost:9092"] },
    consumer: { groupId: "hisobot-iste'molchi" },
  },
}

Kafka — xabarlarni o'chirmaydigan jurnal. groupId — iste'molchi guruhi: guruh ichida har partition'ni bitta nusxa o'qiydi. Har xabar handler'ga KafkaContext bilan keladi: ctx.getTopic(), ctx.getPartition(), ctx.getMessage().offset — qaysi bo'lak, qaysi o'rin.

Kafka transportida pattern — topic nomi. Nest manbasida Kafka serveri pattern sifatida RegExp ni ham qabul qiladi: @EventPattern(/^vazifa\./) — bir nechta topic'ni bitta handler bilan tinglash (biz faqat kodda ko'rdik, sinamadik). So'rov-javob (send) Kafka'da ham ishlaydi, lekin javob uchun alohida "reply" topic kerak va mijoz subscribeToResponseOf(pattern) ni oldindan chaqirishi shart — Kafka tabiatan hodisa uchun.

6. gRPC — sinab ko'rdik

6.1 Shartnoma

gRPC brokersiz, shuning uchun Docker'siz ishga tushirdik. gRPC darsidagi kabi, avval .proto fayl:

protobuf
syntax = "proto3";
package hisobot;

service HisobotXizmati {
  rpc Statistika (StatSorovi) returns (Stat);
  rpc Oqim (StatSorovi) returns (stream Stat);
  rpc Sekin (StatSorovi) returns (Stat);
}

message StatSorovi {
  int32 egasi = 1;
}

message Stat {
  int32 egasi = 1;
  int32 jami = 2;
  int32 bajarilgan = 3;
}

6.2 Server

ts
import { GrpcMethod, RpcException } from "@nestjs/microservices";
import { status } from "@grpc/grpc-js";

@Controller()
class HisobotGrpc {
  @GrpcMethod("HisobotXizmati", "Statistika")
  statistika(d: { egasi: number }) {
    if (d.egasi === 99) {
      throw new RpcException({
        code: status.NOT_FOUND, message: "Egasi 99 topilmadi",
      });
    }
    return { egasi: d.egasi, jami: 12, bajarilgan: 5 };
  }

  @GrpcMethod("HisobotXizmati", "Oqim")
  oqim(d: { egasi: number }): Observable<object> {
    return interval(10).pipe(
      take(3),
      map((i) => ({ egasi: d.egasi, jami: i + 1, bajarilgan: 0 })),
    );
  }
}

const srv = await NestFactory
  .createMicroservice<MicroserviceOptions>(GrpcModule, {
    transport: Transport.GRPC,
    options: {
      package: "hisobot", protoPath: PROTO, url: "127.0.0.1:4593",
    },
  });

@GrpcMethod("Xizmat", "Metod") — .proto dagi metodga bog'lanadi; pattern o'rnida xizmat va metod nomi. Observable qaytargan metod — server oqimi (server streaming): har next — bitta Stat xabari.

6.3 Mijoz va natija

ts
const mijoz = ClientProxyFactory.create({
  transport: Transport.GRPC,
  options: {
    package: "hisobot", protoPath: PROTO, url: "127.0.0.1:4593",
  },
}) as unknown as ClientGrpc;
const x = mijoz.getService<Xizmat>("HisobotXizmati");

await firstValueFrom(x.statistika({ egasi: 7 }));
await firstValueFrom(x.oqim({ egasi: 7 }).pipe(toArray()));
await firstValueFrom(x.statistika({ egasi: 99 }));
const muddat = { deadline: Date.now() + 100 };
await firstValueFrom(x.sekin({ egasi: 1 }, new Metadata(), muddat));

getService — .proto dan yasalgan klient: har rpc metodi — Observable qaytaradigan funksiya (nomlar kichik harf bilan: Statistika → statistika). Natija (biz ishga tushirdik, @grpc/grpc-js 1.14.6):

text
1) gRPC unary: {"egasi":7,"jami":12,"bajarilgan":5}
   server stream: [{"egasi":7,"jami":1,"bajarilgan":0},{"egasi":7,"jami":2,"bajarilgan":0},{"egasi":7,"jami":3,"bajarilgan":0}]
   RpcException → code 5, details: Egasi 99 topilmadi
   deadline 100 ms → code 4, details: Deadline exceeded after 0.100s,remote_addr=127.0.0.1:4593
   sekin, deadline yo'q: {"egasi":1,"jami":0,"bajarilgan":0}

Uch narsaga qarang:

  • RpcException + code — gRPC'ning standart status kodi mijozga yetdi: 5 = NOT_FOUND. TCP'da RpcException obyekti aynan borardi; gRPC'da u code va details ga aylanadi — gRPC protokoli faqat shuni tashiydi.
  • Deadline — mijoz "100 ms ichida javob ber" dedi, xizmat 300 ms uxladi: kod 4 = DEADLINE_EXCEEDED. Bu oldingi darsdagi rxjs timeout dan yaxshiroq: deadline protokolning o'zida, server ham uni ko'radi va kerak bo'lsa ishni to'xtatadi. Uchinchi argument — gRPC'ning chaqiruv sozlamasi, Nest uni @grpc/grpc-js ga o'zgarishsiz uzatadi.
  • Server oqimi — uch xabar, toArray() ularni bitta massivga yig'di.

Tekshirib ko'ring: Gateway gRPC chaqiruviga 2 soniya deadline berdi. Xizmat 1.5 soniyada o'zi boshqa xizmatni chaqiradi. Ikkinchi chaqiruvga qancha deadline berish to'g'ri?

Javob

Qolgan vaqtdan kam: 2 − 1.5 = 0.5 soniyadan kichik. Aks holda gateway allaqachon voz kechgan javob uchun ikkinchi xizmat ishlab yotadi. gRPC'da deadline so'rov bilan birga (grpc-timeout sarlavhasida) keladi va server uni ko'radi; keyingi chaqiruvga qolgan vaqtni o'zingiz uzatasiz. Har bo'g'in o'z ulushini oladi — buni "timeout budjeti" deyishadi (Idempotentlik va timeout budjeti arxitektura darsida).

7. Serializer

Har transport xabarni tarmoqqa chiqarishdan oldin seriyalaydi. TCP va Redis — JSON. Sukut seriyalash nimani yo'qotadi:

ts
const tana = {
  vaqt: new Date("2026-11-02T00:00:00Z"),
  royxat: new Map([[1, "a"]]),
};
// xizmat tanani o'zgarishsiz qaytaradi
await firstValueFrom(oddiy.send("aks", tana));
text
2) oddiy: {"vaqt":"2026-11-02T00:00:00.000Z","royxat":{}}

Date — satr, Map — bo'sh obyekt. Ma'lumot jim yo'qoldi.

Serializer — mijoz yoki xizmatning xabarni tarmoq formatiga aylantiradigan funksiyasi; deserializer — teskarisi. Nest ularni sozlamada qabul qiladi:

ts
const versiyali = ClientProxyFactory.create({
  transport: Transport.TCP,
  options: {
    host: "127.0.0.1", port: 4594,
    serializer: {
      serialize: (p: ReadPacket) => ({
        ...p,
        data: { v: 2, manba: "gateway", ...(p.data as object) },
      }),
    },
  },
});
text
   serializer bilan: {"v":2,"manba":"gateway","matn":"salom"}

Serializer har xabarga versiya va manbani qo'shdi — xizmat uni qaytardi. Amalda serializer uch ish uchun ishlatiladi: format konverti (versiya, kim yubordi, kuzatuv id'si), boshqa tilda yozilgan xizmat bilan moslashish (masalan Go xizmati Nest'ning {pattern, data, id} formatini bilmaydi) va boshqa format (Protobuf, MessagePack).

8. Retry

Xizmat qayta ishga tushayotganda bir-ikki urinish xatosi — normal. rxjs retry operatori Observable'ni xatoda qayta obuna qiladi. Demoda xizmat 350 ms dan keyin ishga tushdi, mijoz esa darhol so'radi:

ts
const natija = await firstValueFrom(
  new Observable((o) => {
    urinish++;
    return mijoz.send("aks", { urinish }).subscribe(o);
  }).pipe(retry({ count: 5, delay: 200 })),
);
text
   retry({count:5, delay:200}) → {"urinish":3} (3-urinishda)

Birinchi ikki urinish ECONNREFUSED oldi, uchinchisi (≈400 ms) ishga tushgan xizmatga yetdi. new Observable o'rami nega kerak? send sovuq, retry unga qayta obuna bo'ladi — o'ram urinishlarni sanash uchun. Oddiy holatda mijoz.send(...).pipe(retry(...)) yetarli.

Retry'ning uchta qoidasi:

  • Faqat idempotent amalda. "Statistikani o'qi" — xavfsiz. "Pul o'tkaz" — timeout'dan keyin qayta yuborsangiz, ikki marta o'tishi mumkin (Idempotency-Key).
  • Kutish bilan. delay siz retry xizmatni urinishlar bilan "bo'g'adi". Eksponensial kutish va jitter — BullMQ ishonchlilik darsidan.
  • Chegara bilan. count siz — cheksiz urinish.

TCP mijozining o'z retryAttempts va retryDelay sozlamalari ham bor (tur faylida). Ular ulanishni qayta tiklash uchun; xabarni qayta yuborish — retry operatori ishi.

9. O'z transportingiz

Nest'ning har transporti ikki klass: server (Server dan meros, CustomTransportStrategy) va mijoz (ClientProxy dan meros). O'zimiz yozib ko'rsak, ular qanday ishlashi ochiq ko'rinadi. "Broker" sifatida oddiy EventEmitter (bitta jarayon ichida):

ts
const shina = new EventEmitter();

class ShinaServer extends Server implements CustomTransportStrategy {
  listen(cb: () => void) {
    type Javob = (w: WritePacket) => void;
    shina.on("xabar", async (p: Paket, javob: Javob) => {
      const h = this.getHandlerByPattern(p.pattern);
      if (!h) {
        javob({ err: `handler yo'q: ${JSON.stringify(p.pattern)}`,
          isDisposed: true });
        return;
      }
      const ctx = new BaseRpcContext([]);
      const natija = await h(p.data, ctx);
      const natija$ = this.transformToObservable(natija);
      this.send(natija$, (w) => javob(w));
    });
    shina.on("hodisa", (p: ReadPacket) => {
      void this.handleEvent(p.pattern, p, new BaseRpcContext([]));
    });
    cb();
  }
  close() { shina.removeAllListeners(); }
  on() { throw new Error("kerak emas"); }
  unwrap<T>(): T { return shina as T; }
}

class ShinaMijoz extends ClientProxy {
  async connect() {}
  async close() {}
  unwrap<T>(): T { return shina as T; }
  protected publish(p: ReadPacket, cb: (w: WritePacket) => void) {
    shina.emit("xabar", { ...p, id: String(Math.random()) }, cb);
    return () => {};
  }
  protected async dispatchEvent<T>(p: ReadPacket): Promise<T> {
    shina.emit("hodisa", p);
    return undefined as T;
  }
}

const srv = await NestFactory.createMicroservice(ShinaModule, {
  strategy: new ShinaServer(),
});
text
4) custom transport: Salom, Malika!
   hodisa shinadan keldi: 7
   noma'lum: "handler yo'q: {\"cmd\":\"yoq\"}"

Uch qatorli natija — @MessagePattern({ cmd: "salom" }) va @EventPattern("vazifa.bajarildi") li oddiy controller o'zgarishsiz ishladi. Transport yozuvchining vazifasi kichik:

  • Server: listen da xabar manbasiga obuna bo'lish; getHandlerByPattern (pattern'ni o'zi normallashtiradi) bilan handler'ni topish; transformToObservable + send bilan javob qaytarish; hodisani handleEvent ga berish.
  • Mijoz: publish — xabarni yuborish va javobni cb ga berish; dispatchEvent — hodisani yuborish.

Shu ikki klass bilan Nest'ni istalgan tizimga ulash mumkin: AWS SQS, Google Pub/Sub, o'z ichki shinangiz. Ommaviy jamiyat paketlarida ko'plari shunday yozilgan.

flowchart LR
  C["Controller (o'zgarmaydi)"] --- S["Server: listen, send, handleEvent"]
  S --- T{"Transport"}
  T --- TCP["TCP"]
  T --- RMQ["RabbitMQ"]
  T --- G["gRPC"]
  T --- O["o'zingizniki"]
  M["ClientProxy: publish, dispatchEvent"] --- T

Diagrammaning markazi — Transport bo'g'ini: controller va ClientProxy uni bilmaydi.

10. JS modelida ack

RabbitMQ'dagi noAck farqini kichik modelda ko'raylik: ikki xabar, ishchi birinchisini birinchi marta olganda yiqiladi. Blok brauzerda ishlaydi:

js
// noAck: true — olindi = bajarildi;
// noAck: false — faqat ack'dan keyin
class Broker {
  #navbat = [];
  constructor(noAck) {
    this.noAck = noAck;
  }
  yubor(xabar) {
    this.#navbat.push(xabar);
  }
  yetkaz(ishlovchi) {
    while (this.#navbat.length > 0) {
      const xabar = this.#navbat.shift(); // navbatdan olindi
      try {
        ishlovchi(xabar);
        // noAck: false bo'lsa — shu yerda ack
      } catch (e) {
        console.log(`  ${xabar}: ${e.message}`);
        if (!this.noAck) this.#navbat.push(xabar); // qayta navbatga
      }
    }
  }
}

for (const noAck of [true, false]) {
  const b = new Broker(noAck);
  ["xat-1", "xat-2"].forEach((x) => b.yubor(x));
  let birinchiMarta = true;
  const bajarildi = [];
  b.yetkaz((x) => {
    if (x === "xat-1" && birinchiMarta) {
      birinchiMarta = false;
      throw new Error("ishchi yiqildi");
    }
    bajarildi.push(x);
  });
  console.log(`noAck: ${noAck} → bajarildi: ${bajarildi.join(", ")}`);
}

Konsolda:

text
  xat-1: ishchi yiqildi
noAck: true → bajarildi: xat-2
  xat-1: ishchi yiqildi
noAck: false → bajarildi: xat-2, xat-1

noAck: true — Nest RabbitMQ transportining sukuti — xat-1 ni yo'qotdi: navbatdan olindi va yiqildi, broker uni qayta bermaydi. noAck: false da xat navbatga qaytdi va ikkinchi urinishda bajarildi. Soddalashtirilgan joyi: haqiqiy RabbitMQ'da xabar ishchi ulanishi uzilganda yoki nack bilan qaytadi; modelda esa catch uni darhol qaytaradi.

11. Kanon tajribasi: nega loyihaga kirmaydi

Kanon tajribasi faqat TCP transportini o'lchadi (oldingi dars). Redis, NATS, RabbitMQ, Kafka, gRPC transportlari va hybrid ilova — kanonda sinalmagan: brokerlar Docker konteyner talab qiladi, ularni xom kutubxonalar bilan Boshqa xabar broker'lari darsida sinagandik. gRPC va o'z transportimiz bu darsda bizning demomizda sinaldi.

vazifalar-api da xabar almashish ehtiyoji bor — 29-qismda u BullMQ navbati va transactional outbox bilan hal qilingan. Nest mikroservis transporti bu ishni qayta qilardi, lekin yomonroq: BullMQ'da qayta urinish, kechiktirish va DLQ tayyor, Nest RMQ transportida esa ularni o'zingiz yozasiz. Kanon qarori — navbat uchun BullMQ, mikroservis transporti — kerak emas.

12. Ko'p uchraydigan xatolar

Xato Nima bo'ladi Tuzatish
RMQ sukut noAck: true ishchi yiqilsa xabar yo'qoladi noAck: false + ack
durable yo'q broker qayta yonsa navbat yo'qoladi queueOptions: { durable: true }
Eski nats paketi Nest 12 NATS transporti ishlamaydi @nats-io/transport-node
Date/Map tarmoqqa satr yoki {} JSON turlari yoki serializer
Idempotent bo'lmagan amalga retry ikki marta bajariladi idempotentlik kaliti
gRPC'da deadline yo'q sekin xizmat zanjirni osadi { deadline: Date.now() + ms }

13. Mashqlar

Mashqlar kurs/mashqlar/30/46-transportlar/ da.

1-mashq (oson): bo'shliqlarni to'ldiring

  • Nest RabbitMQ transportida noAck ning sukut qiymati —
  • Nest 12 NATS transporti talab qiladigan paket — @nats-io/
  • gRPC'da NOT_FOUND statusining raqami —
  • Muddat o'tganda gRPC qaytaradigan status nomi —
  • Nest Redis transporti qaysi Redis klientini ishlatadi? —
Yechim
  • true — RMQ_DEFAULT_NOACK = true; kafolat uchun false va qo'lda ack.
  • transport-node — nats.js 3-avlodi.
  • 5 — status.NOT_FOUND; deadline — 4.
  • DEADLINE_EXCEEDED — Deadline exceeded after 0.100s.
  • ioredis — loyihadagi node-redis'dan farqli, ikkinchi klient bo'lardi.

2-mashq (o'rta): serializer va deserializer

Xabarni "konvert"ga soladigan juftlik yozing. serializer.serialize(data) — JSON.stringify({ v: 2, data }) qaytarsin. deserializer.deserialize(matn) — JSON'ni ochsin, v 2 bo'lmasa versiya N qo'llanmaydi xatosini tashlasin, aks holda data ni qaytarsin. Ishora: JSON.parse va JSON.stringify.

Yechim
js
// Serializer va deserializer: xabar "konvert"ga solinadi
const serializer = {
  serialize: (data) => JSON.stringify({ v: 2, data }),
};
const deserializer = {
  deserialize(matn) {
    const k = JSON.parse(matn);
    if (k.v !== 2) throw new Error(`versiya ${k.v} qo'llanmaydi`);
    return k.data;
  },
};

const tarmoqdan = serializer.serialize({ id: 7, matn: "Non olish" });
console.log(tarmoqdan);
console.log(deserializer.deserialize(tarmoqdan));
try {
  deserializer.deserialize(JSON.stringify({ v: 1, data: {} }));
} catch (e) {
  console.log(e.message);
}

Konsolda:

text
{"v":2,"data":{"id":7,"matn":"Non olish"}}
{ id: 7, matn: 'Non olish' }
versiya 1 qo'llanmaydi

Versiyali konvert xizmatlar alohida deploy qilinganda kerak: yangi gateway 2-versiyani yuboradi, eski xizmat hali 1-versiyani kutadi. Aniq xato "jim noto'g'ri ishlash"dan yaxshi. Haqiqiy tizimlarda bir muddat ikkala versiyani ham qabul qilish (expand-contract g'oyasi) odatiy.

3-mashq (qiyin): takrorni tashlaydigan iste'molchi

noAck: false bilan yetkazish — "kamida bir marta": broker bir xabarni ikki marta berishi mumkin. To'lov xabarlari ro'yxati berilgan, unda a1 ikki marta keldi. Balansni hisoblang, lekin bir id faqat bir marta qo'shilsin. Ishora: Set — bajarilgan id lar; tekshiruv ishdan oldin, qo'shish — ishdan keyin.

Yechim
js
// "Kamida bir marta" yetkazish: takrorni iste'molchi o'zi tashlaydi
const qayta = [
  { id: "a1", summa: 35000 },
  { id: "a2", summa: 28000 },
  { id: "a1", summa: 35000 }, // broker qayta yubordi
];

const bajarilgan = new Set();
let balans = 0;
for (const x of qayta) {
  if (bajarilgan.has(x.id)) {
    console.log(`${x.id}: takror — o'tkazildi`);
    continue;
  }
  balans += x.summa;
  bajarilgan.add(x.id);
  console.log(`${x.id}: +${x.summa}`);
}
console.log(`balans: ${balans}`);

Konsolda:

text
a1: +35000
a2: +28000
a1: takror — o'tkazildi
balans: 63000

98 000 o'rniga 63 000 — to'g'ri. Haqiqiy tizimda Set xotirada emas, bazada bo'ladi: UNIQUE (xabar_id) ustunli jadval va ish bilan bitta tranzaksiyada yoziladi. 29-qismdagi webhook va to'lov darslarida (inbox naqshi) aynan shu qilingan. "Bir marta" kafolatini hech bir broker to'liq bermaydi — uni "kamida bir marta" + idempotent iste'molchi yasaydi.

14. Loyiha qadami: kanon tajribasi, loyihaga kirmaydi

vazifalar-api o'zgarmaydi. Oldingi darsdagi demo loyihangizga gRPC transportini qo'shib ko'ring: .proto yozing, @GrpcMethod bilan hisobot metodini ulang, mijozda deadline bering va xizmatni ataylab sekinlashtiring. DEADLINE_EXCEEDED ni o'z ko'zingiz bilan ko'rish — bu darsning eng foydali mashqi.

15. Real ishda

  • RabbitMQ + Nest — buyurtma, email, hisobot ishlari; review'da birinchi savol: "noAck nima? durable bormi?".
  • Kafka + Nest — katta oqimli tizimlar: to'lov hodisalari, analitika, audit jurnali.
  • gRPC — ichki xizmatlar orasida, ayniqsa boshqa tillar (Go, Java) bilan: .proto — umumiy shartnoma.
  • Intervyu savollari: "At-most-once va at-least-once farqi?", "Nest'da transportni almashtirsa, kod o'zgaradimi?", "gRPC deadline va timeout farqi?", "Idempotent iste'molchi nima?".

Xulosa

  • Controller kodi transportdan mustaqil; ishonchlilik — transportga bog'liq. Har transportning o'z npm paketi bor.
  • TCP, Redis Pub/Sub, oddiy NATS — xabarni saqlamaydi; RabbitMQ, Kafka — saqlaydi.
  • RabbitMQ: Nest sukuti noAck: true — kafolat uchun noAck: false, qo'lda ack, durable: true.
  • gRPC: @GrpcMethod, Observable — server oqimi, RpcException({ code }) → status kodi, deadline protokolda.
  • Serializer — format va versiya; retry — faqat idempotent amalga, kutish va chegara bilan.
  • O'z transportingiz: Server + ClientProxy — listen, send, publish, dispatchEvent.

Keyingi dars: CQRS moduli — buyruqlar (command) va so'rovlarni (query) ajratish: CommandBus, QueryBus, EventBus, saga va qachon CQRS ortiqcha.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Transport'lar chuqur NestJS'da: Redis, NATS, RabbitMQ, Kafka, gRPC va o'z transportingiz — IlmHamroh