Barcha loyihalarIshlamoqda

FlashDrop

Ortiqcha berilmaydigan navbat kuponlari: 4 concurrency usuli yozilib, k6 bilan o'lchab solishtirildi

Bu loyiha nimani isbotlaydi

  • Muammoni isbotlash: lock'siz usul 50 o'rniga 300 ta kupon berdi, remaining_quantity esa 0 emas, 46 da qoldi
  • To'g'ri yechimlar: qolgan uch strategiya 300 so'rovdan aniq 50 tasini o'tkazdi, DB'da ham aynan 50 ta yozuv
  • Natijani tushuntirish: bu yukda optimistic lock FOR UPDATE'dan nega tez chiqqani topildi — FOR UPDATE rad etiladigan 250 so'rovni ham navbatda kutdiradi
  • O'lchov intizomi: warm-up, 3 run mediana, TCP backlog'dan qisman rad etilgan runlar avtomatik yaroqsiz; qayta o'lchov xulosani o'zgartirganda README tuzatildi
  • Operatsiya: SHA-tagli deploy va rollback, har kungi zaxira va tiklash testi, conflict belgisi deploy bo'lgan holatdan keyin qo'yilgan himoya
Rol
Yakka: dizayn, backend, testlar, yuk testi, frontend, deploy va ekspluatatsiya (21 commit)
Muddat
2026.09 – 2026.10
Texnologiyalar
  • Java
  • Spring Boot
  • Spring Data JPA
  • PostgreSQL
  • Redis
  • Lua
  • Flyway
  • Bucket4j
  • JUnit 5
  • Testcontainers
  • k6
  • Docker
  • Caddy
  • Cloudflare

Asosiy raqamlar

Ko'rsatkichQiymatTuriDalil
Redis Lua throughput873 req/sO'lchanganDalilni ko'rish
Redis Lua p95178 msO'lchanganDalilni ko'rish
SELECT FOR UPDATE throughput188 req/sO'lchanganDalilni ko'rish
SELECT FOR UPDATE p951 465 msO'lchanganDalilni ko'rish
Optimistic lock throughput331 req/sO'lchanganDalilni ko'rish
Optimistic lock p95831 msO'lchanganDalilni ko'rish
Lock'siz: 50 kupondan haqiqatda berilgani300O'lchanganDalilni ko'rish
Rollback buyrug'i vaqti1,2 sO'lchanganDalilni ko'rish
Backup'dan DB tiklash (11/11 jadval)0,22 sO'lchanganDalilni ko'rish

Hal qilinadigan muammo

50 dona kuponga bir vaqtda 300 ta so'rov keladi va bitta ham ortiqcha chiqmasligi kerak. Oddiy "o'qi → tekshir → yoz" mantig'i lost update tufayli 50 o'rniga 300 ta kupon chiqaradi. Bir xil biznes qoidasini 4 usulda yozib, to'g'rilik, throughput va latency o'rtasidagi muvozanatni real yuklama ostida o'lchash kerak edi.

Arxitektura

Caddy statik frontend'ni beradi va /api/* ni Spring Boot'ga uzatadi. So'rov avval Bucket4j rate limiter'dan o'tadi (holati Redis'da). Har strategiyaning o'z endpoint'i, entity'si va jadvali bor. Redis usulida qoldiq va "olganlar" to'plami bitta Lua skriptda atomik tekshiriladi, PostgreSQL'ga yozish @Async bilan fonda.

Ma'lumotlar modeli

Asosiy cheklovlar va indekslar

  • campaign_*.code VARCHAR(50) NOT NULL UNIQUE
  • claim_*: UNIQUE (campaign_id, user_id): 중복 발급을 DB에서 차단하고 existsByCampaignIdAndUserId 조회를 빠르게 함
  • campaign_optimistic.version BIGINT NOT NULL DEFAULT 0: @Version 낙관적 락
  • V9 CHECK (remaining_quantity >= 0): for_update, optimistic, redis에만 적용 (no_lock은 oversell이 보이도록 일부러 제외)
  • claim_redis.id UUID: 앱에서 생성해 DB 기록을 기다리지 않고 응답
  • Redis 키 (claim.lua): stock(DECR), claimed set(SADD). 결과 1 발급, 0 품절, -1 캠페인 없음, -2 이미 받음

Asosiy loyihaviy qarorlar

Redis: Lua'da DECR + SADD, DB'ga @Async

Vaziyat
FOR UPDATE barcha so'rovlarni bitta hot row'da navbatga qo'ydi (p95 1465 ms).
Qaror
SISMEMBER → GET → DECR + SADD bitta claim.lua ichida: hot path'da Postgres ham, check-then-act oralig'i ham yo'q.
Evaziga
Redis va DB orasida ilova qulasa, kupon DB'da yo'qolishi mumkin.

Optimistic retry ikki bean'ga bo'lingan

Vaziyat
@Transactional proxy self-invocation'ni ushlamaydi, bitta klassda bo'lsa tranzaksiya amalda qo'llanmaydi.
Qaror
OptimisticClaimService (retry, maks. 20) → OptimisticClaimAttempt (@Transactional, @Version)
Evaziga
Bitta mantiq ikki klassga bo'linadi.

Har strategiyaga alohida jadval va endpoint

Vaziyat
k6 natijalari bir-biriga aralashmasin.
Qaror
To'rt strategiyaning har biri o'z campaign / claim jadvallari, entity va endpoint'iga ega.
Evaziga
Kod va migratsiyalar takrorlanadi (8 jadval).

Lock'siz usul ataylab qoldirilgan

Vaziyat
Muammoni raqam bilan ko'rsatish uchun.
Qaror
Lost update aynan sodir bo'ladigan endpoint saqlandi, uning jadvaliga CHECK ham qo'yilmadi.
Evaziga
Production'da bunday endpoint bo'lmaydi.

Idempotency va qoldiq chegarasi bazada: UNIQUE(campaign_id, user_id), CHECK (remaining_quantity >= 0)

Vaziyat
Ilova xatosi ham ikki marta berolmasin va qoldiq manfiy bo'lmasin.
Qaror
Har bir claim jadvaliga UNIQUE, uchta to'g'ri strategiyaga V9 CHECK; production'da remaining_quantity = -1 yozish rad etildi.
Evaziga
Redis strategiyasida ikki joyda tekshiriladi.

Rate limit kaliti — userId

Vaziyat
Loyihadagi yagona identifikator.
Qaror
Bucket4j + Redis (Lettuce) token bucket userId bo'yicha, bir nechta nusxada ham limit umumiy.
Evaziga
Mijoz uni o'zgartira oladi; production'da IP bilan birga kerak.

Oldin va keyin

Oldin1 cold run: Redis 240.8 rps · p95 582 ms
Keyinwarm-up + median of 3: Redis 873 rps · p95 178 msDalilni ko'rish
Nima o'zgartirildi
Birinchi jadval JIT va connection pool hali tayyor bo'lmagan bitta sovuq rundan olingan edi. scripts/bench.ps1'ga warm-up, 3 run, mediana va yaroqlilik qoidasi (muvaffaqiyatli + sold out < 300 bo'lsa yaroqsiz) qo'shilib, qayta o'lchanganda ikki xulosa teskari bo'ldi: lock'siz usul "eng sekin" emas, "tez, lekin noto'g'ri" (80.4 → 307 rps), optimistic lock esa "eng sekin" emas, FOR UPDATE'dan 1.76 marta tez (114.1 → 331 rps). Sababi: findByCodeForUpdate qoldiqni tekshirishdan oldin qulf oladi. Yaxshilash (qulfdan oldin oddiy SELECT bilan sotuv tugaganini tekshirish) hali qilinmagan.

Nosozlik tarixi

Belgi
2026-10-09 deploy'dan keyin flashdrop.javohir.dev'da UI buzildi, sahifada xom <<<<<<< HEAD matni chiqdi.
Sabab
Lokal repo GitHub'dan orqada qolgan edi. Push rad etilgach git pull merge'ida 4 ta faylda conflict chiqdi va belgilar tozalanmasdan commit qilindi. Frontend Caddy orqali git ishchi papkasidan berilgani uchun git pull bilan darhol production'ga chiqdi; backend rollback'i esa frontend'ni qamramadi.
Yechim
frontend/index.html oxirgi ishlagan commit'dan tiklandi, README'lar qayta yozildi, tasodifan commit bo'lgan IDE build natijasi (bin/) olib tashlanib .gitignore'ga qo'shildi va qayta deploy qilindi (7ac0754).
Takrorlanmasligi uchun
deploy.sh endi git grep bilan upstream'da conflict belgisi bormi tekshiradi va topsa deploy'ni to'xtatadi, merge faqat --ff-only bilan (a2dd204). Yashirin oqibat ham topildi: conflict tuzatilganda README'lar (ko/en/uz) eski sovuq run jadvaliga qaytib qolgan edi; portfolio matnini repo bilan solishtirganda topilib, qayta o'lchangan natijalar tiklandi (a29c567).

Test va sifat

Testlar soni
4
Qamrov
—
CI
—
Test vositalari
JUnit 5, Testcontainers (PostgreSQL 17, Redis 7), virtual threads, k6, bench.ps1

Deploy va ekspluatatsiya

Deploy muhiti
iwinv VPS, Docker Compose: app (Spring Boot, root bo'lmagan, 1 GB xotira cheklovi), web (Caddy: statik konsol + /api/* proxy), PostgreSQL 17, Redis 7, seed (demo kampaniyalar). Postgres va Redis portlari ochilmagan. deploy.sh: conflict belgisi tekshiruvi → git merge --ff-only → image SHA tegi bilan ham build → app va web yangilanadi, SHA .deploy-history'ga yoziladi. HikariCP 50, Tomcat accept-count 500.
Backup
Kunlik pg_dump (03:00 KST, cron), 7 kun saqlanadi, serverdagi 9 ta Postgres bazasini qamraydi. 2026-10-09 03:00 da avtomatik bajarilgani tasdiqlangan. Tiklash sinalgan: FlashDrop 0.22 s (11/11 jadval), LedgerCore 0.66 s (14/14)
Rollback
rollback.sh oldingi SHA image'ini latest qilib, build'siz app'ni qayta yaratadi. Production'da buyruq real 0m1.201s, tashqaridan kuzatilganda 03:32:00–03:32:07 oralig'ida 502, keyin yana 200.
Monitoring
pg_stat_statements yoqilgan, DB PG Insight'ga monitoring tarmog'i orqali ulangan

Skrinshotlar

Cheklovlar va xulosalar

Hozirgi cheklovlar

  • Redis va Postgres orasidagi bo'shliq: Redis'da kupon berilgan, lekin @Async yozuvdan oldin ilova qulasa Postgres'da yo'q. @Async xatolari yo'qoladi (handler yo'q), executor navbati (500) to'lsa DECR'dan keyin TaskRejectedException chiqadi; campaign_redis.remaining_quantity init'dan keyin sinxronlanmaydi, reconciliation job yo'q
  • FOR UPDATE yo'li optimal emas: qulf qoldiqni tekshirishdan oldin olinadi, rad etiladigan so'rovlar ham navbat kutadi; qulfdan oldin oddiy SELECT hali qo'shilmagan
  • Rate limit kaliti zaif: userId mijozdan keladi va o'zgartirilishi mumkin; production'da IP bilan birga kerak
  • Testlar kam: 4 ta parallellik testi bor, unit va HTTP darajasidagi testlar yo'q. NoLockConcurrencyTest ataylab qizil, shuning uchun gradlew test hech qachon to'liq yashil bo'lmaydi
  • Bitta instansiya: rollback buyrug'i 1.2 s, lekin JVM ishga tushguncha ~8 s 502 qaytadi; health check'li almashtirish yo'q, deploy'da ham health kutilmaydi, backup'lar baza bilan bir diskda
  • O'lchov muhiti: raqamlar bitta noutbukdan (Windows + Docker Desktop, i7-8565U); OS listen backlog'i ba'zi runlarda so'rovlarni rad etgan
  • Konsol hisoblagichlari brauzer sessiyasiga xos, sahifa yangilanganda nolga tushadi; umumiy metrika endpoint'i yo'q

Qayta qursam

  • Fonda yozish uchun boshidan Redis Stream yoki Outbox: ACK faqat DB commit'dan keyin, yo'qolgan xabar qayta o'qiladi, qo'shimcha reconciliation job
  • Birinchi kundan CI (testlar, conflict tekshiruvi, kichik k6 smoke), frontend'ni image ichiga olish va health check bilan blue-green deploy

Asosiy kod

Hozirgi holat va keyingi qadamlar

Lock'siz usul 50 o'rniga 300 ta kupon berdi (remaining_quantity 0 emas, 46 da qoldi), qolgan uch strategiya 300 so'rovdan aniq 50 tasini o'tkazdi va DB'da ham aynan 50 ta yozuv qoldi. Redis FOR UPDATE'ga nisbatan throughput bo'yicha 4.6, p95 bo'yicha 8.2 marta yaxshi chiqdi. Raqamlar 1 warm-up'dan keyingi 3 ta yaroqli run medianasi; muvaffaqiyatli + sold out yig'indisi 300 dan kam bo'lgan runlar (TCP backlog tufayli qisman rad etilgan) avtomatik yaroqsiz deb belgilanib, qayta yugurildi. Rate limit: bitta userId bilan 12 so'rovdan 6 tasi o'tdi, 6 tasi 429 (5 token + greedy refill).