Barcha loyihalarIshlamoqda

LiveCommerce

Jonli efir paytidagi cheklangan flash sale platformasi: 100 dona uchun 1 000 xarid urinishi, ortiqcha sotuv 0

Bu loyiha nimani isbotlaydi

  • To'g'rilik: 100 dona uchun 1 000 urinish, 3 run — 0 ortiqcha sotuv, to'lov va sotilgan dona nomuvofiqligi 0, HTTP xato 0; har run oxirida 5 ta invariant tekshiriladi
  • Nosozlikka chidamlilik: Kafka yoki to'lov bazasi haqiqatan to'xtatilganda ham 0 ta event yo'qolmaydi, 0 ta ikki marta pul yechilmaydi (chaos testlari)
  • O'lchab tezlashtirish: sotuv tugashi 1 daq 27 s → ~23 s, buyurtma yaratish p95 5.72 s → 2.68 s; connection pool gipotezasi o'lchov bilan rad etildi
  • Production'dagi biznes xavfini dalil bilan yopish: bitta hisob to'lanmagan buyurtmalar bilan butun zaxirani band qila olardi; partial unique index bilan yopildi (15 → 0 o'rniga 15 → 5)
  • Chegara himoyasi: login, signup va buyurtma uchun rate limit; mijoz IP'si ishonchli proxy zanjiridan olinadi, soxta X-Forwarded-For 429'dan qutqarmaydi (production'da tekshirildi)
  • Operatsiya: SHA-tagli deploy, health'ga bog'langan rollback, production'da ikki yo'nalishli mashq, restartdan keyin ham token yaroqli qoladigan JWT kaliti
Rol
Yakka: arxitektura, 7 ta servis, frontend, testlar, yuk testi, deploy va operatsiya (127 commit)
Muddat
2026.09 – 2026.10
Texnologiyalar
  • Kotlin
  • Spring Boot
  • Spring Cloud Gateway
  • Spring Data JPA
  • PostgreSQL
  • Apache Kafka
  • WebSocket (STOMP)
  • Flyway
  • Toss Payments
  • OpenTelemetry
  • Jaeger
  • Next.js
  • TypeScript
  • Tailwind CSS
  • JUnit 5
  • k6
  • Docker
  • Caddy

Asosiy raqamlar

Ko'rsatkichQiymatTuriDalil
Ortiqcha sotuv (100 dona, 1 000 urinish, 3 run)0O'lchanganDalilni ko'rish
Sotilgan = pul yechilgan100 / 100O'lchanganDalilni ko'rish
Buyurtma yaratish p952,68 sO'lchanganDalilni ko'rish
To'lov yuborish p950,63 sO'lchanganDalilni ko'rish
Sotuv tugash vaqti23 sO'lchanganDalilni ko'rish
Zaxira yangilanishi yetib borishi (500 tomoshabin)99,2 %O'lchanganDalilni ko'rish
Tomoshabinlar soni xabarlari kamayishi98 %O'lchanganDalilni ko'rish
Chaos testlari: yo'qolgan event / ikki marta to'lov0O'lchanganDalilni ko'rish

Hal qilinadigan muammo

Jonli efir paytidagi cheklangan sotuvda buyurtmalar bir necha soniyaga to'planadi, zaxira, to'lov va yetkazish esa turli servislarda. Flash sale'da "sotib bo'lindi" odatiy holat, shuning uchun avval pul olinsa har biri refund va PG komissiyasiga aylanadi; bitta servis to'xtasa event yo'qolishi yoki ikki marta pul yechilishi mumkin.

Arxitektura

api-gateway (Spring Cloud Gateway) JWT'ni tekshiradi, identity sarlavhalarini qayta yozadi, rate limit va WebSocket proxy'ni bajaradi. order-service'dagi Saga orkestratori avval zaxirani 10 daqiqaga band qiladi, keyin to'lov oladi (AWAITING_STOCK → AWAITING_PAYMENT → PAYMENT_CONFIRMING → CONFIRMING_STOCK → COMPLETED, to'lov oynasi 5 daqiqa). inventory (band qilish, tasdiqlash, qaytarish, muddati o'tganlarni tozalash), payment (Toss tasdiqlash va bekor qilish, Idempotency-Key), live (STOMP chat, real-time zaxira, tomoshabinlar soni), auth (RS256 JWT, JWKS, refresh token rotatsiyasi) va delivery (yetkazish, HMAC kuryer webhook'lari) har biri o'z PostgreSQL bazasiga ega va bir-birining ma'lumotini faqat API yoki Kafka event orqali o'qiydi. Event'lar har bir servisning transactional outbox'idan traceparent bilan chiqadi, shuning uchun Kafka orqali o'tsa ham butun saga bitta Jaeger trace.

Ma'lumotlar modeli

Asosiy cheklovlar va indekslar

  • database per service: PostgreSQL 5개, 서비스 간 FK 없음, order_id / member_id UUID와 Kafka 이벤트로만 연결 (Flyway 마이그레이션 21개)
  • orders.uq_orders_one_open_per_member_product: 열린 상태 partial UNIQUE (V5)
  • orders.version (@Version), 인덱스 (status, updated_at), partial payment_deadline WHERE status='AWAITING_PAYMENT'
  • product_stock.quantity_available CHECK (>= 0): 코드가 틀려도 DB가 초과 판매를 막음, unit_price는 서버 측
  • inventory_reservations.order_id UNIQUE: 중복 이벤트가 재고를 두 번 줄이지 않음, partial index expires_at WHERE status='HELD'
  • payments.order_id UNIQUE: 주문 하나에 결제 하나
  • tracking_events.event_id UNIQUE: 택배 웹훅 재전송도 한 번만 적용
  • refresh_tokens.token_hash UNIQUE, family_id 인덱스: 재사용된 토큰은 가족 전체를 폐기
  • outbox_events.trace_parent (W3C), partial index created_at WHERE status='PENDING'

Asosiy loyihaviy qarorlar

Avval zaxira, keyin to'lov (hold-then-pay)

Vaziyat
Flash sale'da "sotib bo'lindi" odatiy holat; avval pul olinsa har biri refund va PG komissiyasiga aylanadi.
Qaror
Zaxira 10 daqiqaga band qilinadi, 5 daqiqalik to'lov oynasi ochiladi, to'lov tasdiqlangach hold tasdiqlanadi.
Evaziga
Zaxira 10 daqiqa band turadi, shuning uchun bir xaridorga bitta ochiq buyurtma qoidasi kerak bo'ldi.

Choreography emas, orkestratsiya

Vaziyat
Holat mashinasi, tiklash va admin'dan qayta urinish bitta joyda bo'lishi kerak.
Qaror
order-service saga'ni boshqaradi, qotib qolgan qadamlarni tiklash scheduler'i har 5 s da qayta yuritadi.
Evaziga
order-service markaziy bo'g'in bo'ladi.

Transactional outbox, broker tasdiqlagandan keyin PUBLISHED

Vaziyat
Kafka to'xtasa ham event yo'qolmasin.
Qaror
Holat o'zgarishi va event bitta tranzaksiyada yoziladi; relay parallel yuboradi va faqat ketma-ket tasdiqlangan prefiksni PUBLISHED qiladi.
Evaziga
Dublikat bo'lishi mumkin — iste'molchilar idempotent qilingan.

Toss chaqiruvi tranzaksiyadan tashqarida, natija outbox bilan bitta tranzaksiyada

Vaziyat
Tashqi API sekinlashsa DB ulanishi ushlanib qolmasin.
Qaror
TransactionTemplate bilan Toss tasdig'i tashqarida chaqiriladi, natija va outbox event bitta tranzaksiyada yoziladi.
Evaziga
Natija yozilmay qolsa — tiklash scheduler'i Idempotency-Key bilan qayta urinadi.

Lock'siz skalyar o'qish bilan "sotib bo'lindi"ni oldindan rad etish

Vaziyat
Sotuv tugagach ham hamma FOR UPDATE navbatida kutardi (p95 5.8 s).
Qaror
Qulfdan oldin bitta son o'qiladi va 0 bo'lsa rad etiladi. Entity o'qilmaydi — aks holda Hibernate keyingi FOR UPDATE'da eski keshlangan nusxani qaytarib, ortiqcha sotuvga yo'l ochardi.
Evaziga
Zaxira qaytarilayotgan lahzada xaridor "sotib bo'lindi" javobini olishi mumkin; ortiqcha sotuv baribir imkonsiz.

Bitta ochiq buyurtma — partial unique index

Vaziyat
Bir hisob butun zaxirani to'lamasdan band qilardi; parallel so'rovlarda ham bazada kafolat kerak edi.
Qaror
Ochiq statuslar uchun (member_id, product_id) partial unique index (V5); buzilsa 409 OPEN_ORDER_EXISTS va mavjud orderId qaytadi.
Evaziga
Xaridor yangi buyurtmadan oldin eskisini to'lashi yoki bekor qilishi kerak; UI uni mavjud buyurtmaga qaytaradi.

Rate limit gateway xotirasida, buyurtmalar foydalanuvchi bo'yicha

Vaziyat
Redis'siz oddiy yechim; Koreyada ko'p xaridor bitta CGNAT IP'dan chiqadi.
Qaror
Login 10/daq, signup 5/soat IP bo'yicha; buyurtma 20/daq foydalanuvchi bo'yicha; IP XFF zanjiridagi eng o'ngdagi public manzildan olinadi.
Evaziga
Gateway bir nusxa bo'lsagina to'g'ri; login cheklovi ko'p IP'dan bitta hisobga hujumni to'xtatmaydi.

JWT kaliti .env'da, kid — RFC 7638 thumbprint

Vaziyat
Restart va deploy'dan keyin token yaroqli qolsin, gateway JWKS keshi bilan mos kelsin.
Qaror
JWT_SIGNING_KEY serverdagi .env'dan (chmod 600) yuklanadi, bir xil kalit doim bir xil kid beradi; kalit berilmasa compose start bo'lmaydi.
Evaziga
Kalit almashtirilganda eski tokenlar qisqa vaqt rad etiladi.

Oldin va keyin

Oldinsell-out 1 min 27 s · order p95 5.72 sDalilni ko'rish
Keyinsell-out ~23 s · order p95 2.68 sDalilni ko'rish
Nima o'zgartirildi
Topilgan 4 sabab tuzatildi: sotuv tugagach ham FOR UPDATE navbati → qulfdan oldin skalyar o'qish; har topic'da 1 partition va 1 consumer (zaxira band qilish p95 17.8 s) → 6 partition va 6 thread (kalit orderId); outbox relay 500 ms da bittalab tasdiq kutib yuborishi → 100 ms, parallel batch; 500 tomoshabinga har kirishda son yuborish (O(n²)) → faqat o'zgarganda, soniyasiga ko'pi bilan bir marta. To'lov yuborish p95 0.71 → 0.63 s, zaxira band qilinguncha p95 17.77 → 4.80 s, yakunlanguncha p95 33.55 → 11.04 s, tomoshabinlar soni xabarlari 454 583 → 8 151. "Oldin" kechikishlari kesilgan: k6 sekin buyurtmalarni 15–20 s da tashlab ketgan, shuning uchun haqiqiy farq kattaroq.

Nosozlik tarixi

Belgi
2026-10-09 production'da lc-hoard-check.sh bitta test akkaunt bilan ketma-ket to'lanmagan buyurtmalar yaratdi: 2×201, zaxira 15 → 0. Ya'ni bitta foydalanuvchi 10 daqiqalik hold davomida jonli efirdagi barcha mahsulotni boshqalardan yopib qo'ya olardi.
Sabab
Saga har bir POST /api/orders uchun alohida hold ochardi, "bitta xaridor — bitta ochiq buyurtma" qoidasi hech qayerda yo'q edi.
Yechim
V5 migratsiya — ochiq statuslar (AWAITING_STOCK, AWAITING_PAYMENT, PAYMENT_CONFIRMING, CONFIRMING_STOCK) uchun (member_id, product_id) partial unique index. Controller oldindan tekshiradi, poygada esa DataIntegrityViolationException index nomi bo'yicha ajratilib, 409 OPEN_ORDER_EXISTS va mavjud orderId qaytadi; frontend shu buyurtmani davom ettiradi.
Takrorlanmasligi uchun
Lokal Postgres'da ikki parallel tranzaksiyadan ikkinchisi 1.5 s kutib rad etildi; production'da qayta tekshiruv: 1×201 + 409, zaxira 5 da qoldi. 10 ta parallel so'rovdan aynan bittasi 201 ekanini tekshiruvchi integration test qo'shildi; k6 flash-sale.js endi har bir VU'ga o'z foydalanuvchisini beradi (USERS ≥ VUS tekshiriladi).

Test va sifat

Testlar soni
46
Qamrov
—
CI
—
Test vositalari
JUnit 5 (통합 24, 카오스 3, gateway 단위 14, SigningKeys 5), 실행 중인 전체 스택에 실제 HTTP, k6 3종

Deploy va ekspluatatsiya

Deploy muhiti
iwinv VPS'da docker-compose.prod.yml: 16 konteyner (7 Spring servis, Next.js, 5 PostgreSQL, Kafka KRaft, Jaeger, Caddy edge). Hech bir konteyner port publish qilmaydi; faqat edge tashqi proxy tarmog'iga ulangan, /api/* va /ws/* ni gateway'ga, qolganini frontend'ga yuboradi. JVM'lar USER app (root emas), har servisga xotira chegarasi (Spring 512 MB, PostgreSQL 256 MB, Kafka 768 MB). PAYMENTS_FAKE_ENABLED=false. deploy.sh: conflict-marker tekshiruvi → git pull --ff-only → SHA tegli 8 ta image build → up -d → har servisning /actuator/health va frontend edge ichidan 60×3 s tekshiriladi → muvaffaqiyatda APP_TAG va .deploy-history. 2026-10-10 o'lchovi: deploy 2 daq 38 s (build 116 s), uzilish ~27 s.
Backup
Server umumiy cron backup'i (har kuni 03:00, 7 kun) log'ida livecommerce'ning 5 ta PostgreSQL bazasi dump qilingani ko'rinadi; tiklash testi hali qilinmagan.
Rollback
rollback.sh <sha>: 8 ta image borligini tekshiradi, keyin build'siz up -d --no-deps --force-recreate. Migratsiyalar Flyway bilan faqat oldinga (V5 faqat index qo'shadi, eski kod ishlaydi). Production mashqida rollback 48 s (uzilish ~36 s), qayta oldinga 39 s (~26 s).
Monitoring
lc-diagnose.sh bitta buyruq bilan: konteyner holati, restart/OOM, xotira, 24 soatlik ERROR soni, public endpoint kechikishi, statuslar bo'yicha buyurtmalar, 15 daqiqadan ortiq osilganlar, outbox PENDING, zaxira, DB hajmlari, Kafka lag. 2026-10-09: 9 kun uzluksiz, 0 restart, 0 OOM, 0 ERROR, Kafka lag 0, PENDING 0. Jaeger (OpenTelemetry, sampling 20%) bilan butun saga trace'i.

Skrinshotlar

Cheklovlar va xulosalar

Hozirgi cheklovlar

  • Yuk raqamlari bitta noutbukda o'lchangan (±25%), production throughput emas; keyingi qadam — alohida yuk mashinasi va production'ga o'xshash muhit
  • V5 dan keyin flash-sale.js qayta o'lchanmagan, oldin/keyin jadvali eski skript bilan; keyingi qadam — yangi skript bilan 3 run medianasi
  • live-service holati (tomoshabinlar, sessiya) xotirada, bitta instance; restartda tomoshabinlar qayta ulanadi; keyingi qadam — Redis pub/sub yoki broker relay
  • Rate limiter xotirada: gateway ko'paytirilsa limit instance soniga ko'payadi, restartda nolga tushadi; login limiti faqat IP bo'yicha, ko'p IP'dan bitta akkauntga hujumni to'xtatmaydi; keyingi qadam — Redis limiter va akkaunt bo'yicha limit
  • JWT kaliti env'da, KMS emas, rotatsiya yo'q; keyingi qadam — JWKS'da ikki kalit bilan rotatsiya va secret manager
  • Deploy paytida ~26–36 s uzilish (bitta instance, rolling/blue-green yo'q), bu vaqtda gateway 503 emas, 500 qaytaradi — mijoz qayta urinish kerakligini bilmaydi
  • outbox_events va refresh_tokens tozalanmaydi, vaqt o'tishi bilan o'sadi; keyingi qadam — retention job
  • Bo'lishilgan 8 GB VPS, swap 813 MB — xotira bosimida kechikish oshishi mumkin
  • CI (GitHub Actions billing), alerting, backup tiklash mashqi va frontend avtomatik testlari yo'q
  • To'lovlar Toss test kalitlari bilan, video simulyatsiya

Asosiy kod

Hozirgi holat va keyingi qadamlar

100 dona uchun 1 000 ta xarid urinishi (200 VU) 3 marta yugurildi: har safar ortiqcha sotuv 0, to'lov va sotilgan dona o'rtasida nomuvofiqlik 0, HTTP xato 0; har run oxirida 5 ta invariant admin API orqali tekshirildi. O'lchov bilan to'siqlar topilib, sotuv tugash vaqti 1 daq 27 s dan ~23 s ga tushdi, connection pool gipotezasi esa o'lchov bilan rad etildi. Production'da bitta hisob 2 ta to'lanmagan buyurtma bilan butun zaxirani (15 → 0) band qila olishi partial unique index bilan yopildi (15 → 5); auth restartidan keyin bir xil token turli servislarda turlicha javob olgan JWT kaliti muammosi (401/200) tuzatildi (0eeaf2c); login va buyurtmaga cheklov qo'shildi (085ded0). Shuningdek broker yo'qligida outbox tranzaksiyasi daqiqalab qulf ushlashi (881afda), partitsiya ko'paytirilgandan keyin consumer'lar yangi partitsiyalarni ko'rmagani, oldingi run'dagi hold muddati tugab sotilgan 101 ko'ringan k6 invariant xatosi va boshqa loyiha konteyneri 8080 portni egallagani sabab 14 ta integration test yiqilgani ham sababi topilib hal qilindi.