Taobao intervjuer: Kako dizajnirati sistem kupona?
1 Scenario (scena)
Uobičajena promotivna sredstva velikih e-trgovinskih kompanija:
- Kupon
- Grupna kupovina
- Sniženje cene (bargaining)
- Stari dovodi novog
1.1 Vrste kupona
- Kupon za popust pri dostizanju iznosa
- Kupon za direktno umanjenje
- Kupon za procenat popusta
1.2 Jezgarni procesi sistema kupona
1.2.1 Izdavanje kupona
Načini izdavanja: sinhrono slanje ili asinhrono slanje.
1.2.2 Preuzimanje kupona
- Ko može da preuzme?
Svi korisnici ili određeni korisnici.
- Gornja granica preuzimanja
Koliko kupona najviše može da se preuzme?
- Način preuzimanja
Aktivno preuzimanje od strane korisnika ili automatsko pasivno preuzimanje.
1.2.3 Korišćenje kupona
- Obim dejstva
Proizvod, trgovac, kategorija.
- Način izračunavanja
Da li su međusobno isključivi, da li je dostignut prag itd.
1.3 Raščlamba zahteva
1.3.1 Strana prodavca
- Kreiranje kupona
- Slanje kupona
1.3.2 Strana korisnika
- Preuzimanje kupona
- Podnošenje narudžbine
- Korišćenje kupona
- Plaćanje
2 Service (usluga)
2.1 Dizajn strukture usluge

2.2 Tehničke teškoće u dizajnu sistema kupona
- Distribuirane transakcije kupona, analiza distribuiranih problema koji se mogu pojaviti pri korišćenju kupona?
- Kako sprečiti prekomerno izdavanje?
- Kako masovno izdavati kupone korisnicima?
- Kako ograničiti uslove korišćenja kupona?
- Kako sprečiti korisnika da kupon preuzme više puta?
3 Storage (skladište)
3.1 Dizajn tabela
Serija kupona (šablon kupona), coupon_batch
Odnosi se na apstrakciju, šablon serije kupona; sadrži većinu atributa kupona.
Na primer, prodavac je kreirao seriju kupona, ukupno 1000 komada, sa vremenom korišćenja 2022-11-11 00:00:00 ~ 2022-11-11 23:59:59, uz pravilo da se mogu koristiti samo za proizvode iz digitalne kategorije, i da iznos od 100 donosi umanjenje od 50.
Kupon
Entitet izdat jednom korisniku, već vezan za korisnika.
Na primer, slanjem jednog kupona iz neke serije određenom korisniku, kupon tada pripada korisniku.
Pravilo
Korišćenje kupona ima pravila i uslove ograničenja; na primer, za kupon „iznos 100, umanjenje 50" potrebno je dostići prag od 100 juana da bi se koristio.

Tabela serije kupona coupon_batch

Tabela pravila rule:

Sadržaj pravila:
{
threshold: 5.01 // prag za korišćenje
amount: 5 // iznos umanjenja
use_range: 3 // obim korišćenja: 0—celo tržište, 1—prodavac, 2—kategorija, 3—proizvod
commodity_id: 10 // id proizvoda
receive_count: 1 // broj koji svaki korisnik može da preuzme
is_mutex: true // da li je međusobno isključiv: true znači isključiv, false znači neisključiv
receive_started_at: 2020-11-1 00:08:00 // vreme početka preuzimanja
receive_ended_at: 2020-11-6 00:08:00 // vreme završetka preuzimanja
use_started_at: 2020-11-1 00:00:00 // vreme početka korišćenja
use_ended_at: 2020-11-11 11:59:59 // vreme završetka korišćenja
}Tabela kupona coupon:
create table t_coupon
(
coupon_id int null comment 'ID kupona, primarni ključ',
user_id int null comment 'ID korisnika',
batch_id int null comment 'ID serije',
status int null comment '0-neiskorišćen, 1-iskorišćen, 2-istekao, 3-zamrznut',
order_id varchar(255) null comment 'odgovarajući ID narudžbine',
received_time datetime null comment 'vreme preuzimanja',
validat_time datetime null comment 'datum važenja',
used_time datetime null comment 'vreme korišćenja'
);3.2 Kreiranje kupona
1. Novo pravilo
INSERT INTO rule (name, type, rule_content)
VALUES("Pravilo umanjenja pri dostizanju iznosa", 0, '{
threshold: 100
amount: 10
......
}');2. Nova serija kupona
INSERT INTO coupon\_batch (coupon\_name, rule\_id, total\_count )
VALUES("Rolls-Royce 5 juana vaučer", 1010, 10000);3.3 Izdavanje kupona


Kako izdavati kupone velikom broju korisnika?
Asinhrono slanje!
Sistem za dosezanje
- SMS, imejl
Može se realizovati pozivom interfejsa treće strane.
- Poruka na sajtu (in-site poruka)
Realizuje se umetanjem zapisa u bazu.
Tabela poruka message
create table t_message
(
id int null comment 'ID poruke',
send_id int null comment 'id pošiljaoca',
rec_id int null comment 'id primaoca',
content vachar(255) comment 'sadržaj poruke na sajtu',
is_read int null comment 'da li je pročitano',
send_time datetime comment 'vreme slanja'
)
comment 'tabela poruka';Najpre razmotrimo situaciju sa malo korisnika: prodavac želi da svima pošalje poruku na sajtu, pa najpre prelazi korisničku tabelu, a zatim redom za svakog korisnika iz korisničke tabele umeće poruku u tabelu message. Tako, ako ima 100 korisnika, slanje jedne poruke na sajtu svima zahteva 100 operacija umetanja.
Broj korisnika sistema naraste na desetine hiljada
Slanje jedne poruke na sajtu zahteva ponavljano umetanje desetina hiljada podataka. A sadržaj tih desetina hiljada podataka je isti! Pretpostavimo da jedna poruka na sajtu zauzima 100K; tada jedno slanje poruke na sajtu troši i preko desetak MB. Zbog toga se prvobitna tabela može podeliti u dve tabele:
Tabela poruka message

Tabela sadržaja poruka message_content

Koraci slanja jedne poruke na sajtu
- U message_content se umeće sadržaj poruke na sajtu.
- U tabelu message se za sve korisnike umeće jedan zapis koji označava da postoji jedna poruka na sajtu.
Broj korisnika na nivou desetina miliona
Ovde se javlja problem [neaktivnih korisnika]: pretpostavimo da je deset miliona registrovanih korisnika; prema pravilu 80/20, aktivni korisnici čine 20%. Ako primenimo gore opisanu podelu u dve tabele, slanje jedne „poruke na sajtu" zahteva izvršavanje deset miliona operacija umetanja. Moguće je da preostalih 80% korisnika uglavnom više nikada neće da se prijavi; zapravo je potrebno umetati podatke samo za 20% korisnika.
Tabela poruka message:
create table t_message
(
id int null comment 'ID poruke',
# send_id int null comment 'id pošiljaoca', uklonjeno ovo polje
rec_id int null comment 'id primaoca',
message_id int null comment 'strani ključ, sadržaj poruke',
is_read int null comment 'da li je pročitano'
)
comment 'tabela poruka';
create table t_message_content
(
id int null comment 'id sadržaja poruke',
send_id int null comment 'id pošiljaoca',
content varchar(255) null comment 'sadržaj',
send_time datetime null comment 'vreme slanja'
);Operacija na strani korisnika
Nakon prijave, najpre se pretražuju podaci u message_content koji nemaju zapis u message, što označava nepročitane poruke na sajtu. Prilikom pregleda sadržaja poruke na sajtu, zatim se u message umeću relevantni zapisi.
Operacija na strani sistema
Prilikom slanja poruke na sajtu:
- U message_content se umeće samo glavni sadržaj poruke na sajtu.
- U message se ne umeće zapis.
Izdavanje kupona za 100K korisnika

Koji je problem? Dupla potrošnja dovodi do prekomernog izdavanja!
- Operacije pružaju fajl korisnika koji zadovoljavaju uslove, učitavaju ga u pozadinu za upravljanje izdavanjem kupona i biraju kupon koji žele da pošalju.
- Upravljački server na osnovu [ID korisnika], [ID serije kupona] generiše poruku i šalje je u MQ.
- Server kupona potroši poruku.
# Ne zaboravite da koristite transakciju!
INSERT INTO coupon (user_id, coupon_id,batch_id)
VALUES(1001, 66889, 1111);
UPDATE coupon_batch SET total_count = total_count - 1,
assign_count = assign_count + 1
WHERE batch_id = 1111 AND total_count > 0;3.4 Preuzimanje kupona
Koraci
- Provera preostale količine kupona
SELECT total_count FROM coupon_batch
WHERE batch_id = 1111;- Nova tabela korisnika kupona, umanjenje preostale količine
# Pazite na transakciju!
INSERT INTO coupon (user_id, coupon_id,batch_id)
VALUES(1001, 66889, 1111);
UPDATE coupon_batch SET total_count = total_count - 1,
assign_count = assign_count + 1
WHERE batch_id = 1111 AND total_count > 0;Tokom preuzimanja kupona od strane korisnika zapravo se može pojaviti scenario sličan flash prodaji. Koje se problemi javljaju u flash prodaji i kako ih rešiti?

Korisnik preuzima više puta ili previše
Provera podataka u Redis-u!
- Pre preuzimanja kupona, najpre se proveri keš.
# Proverava da li je element član skupaSISMEMBER KEY VALUE
SISMEMBER batch_id:1111:user_id 1001- Preuzimanje kupona.
- Nakon preuzimanja kupona, ažurira se keš.
# Dodaje jedan ili više elemenata u skup; elementi koji već postoje u skupu biće ignorisani
SADD KEY VALUE1......VALUEN
SADD batch_id:1111:user_id 10013.5 Korišćenje kupona
Kada proveriti pravila korišćenja kupona?
- Potvrda narudžbine (√)
- Podnošenje narudžbine
- Odmah plati
Na stranici za potvrdu narudžbine vrši se provera kupona:
- Provera da li je istekao
- Provera obima primene
- Provera da li je dostignut prag
- Provera međusobne isključivosti
Vraćanje upotrebljivih kupona

SELECT batch_id FROM coupon WHERE user_id = 1001 AND status = 0;
SELECT rule_id FROM coupon_batch WHERE batch_id = 1111;
SELECT name, type, rule_content FROM rule WHERE rule_id = 1010;Bira upotrebljive kupone i vraća rezultat

Pri istovremenom operisanju nad više usluga, kako garantovati konzistentnost?

Dizajn tabela
Tabela zapisa operacija kupona Coupon_opt_record
create table t_coupon_opt_record
(
user_id int null comment 'id korisnika',
coupon_id int null comment 'id kupona',
operating int null comment 'operacija: 0-zaključavanje, 1-realizacija, 2-otključavanje',
operated_at datetime null comment 'vreme operacije'
);TCC, Try-Confirm-Cancel, trenutno je glavno rešenje za distribuirane transakcije.
- Faza 1: Try
Resursi se zamrzavaju, rezervišu se resursi biznisa.
Prilikom kreiranja narudžbine status kupona se menja u „zamrznuto".
- Faza 2: Confirm
Potvrđuje se izvršenje poslovne operacije, radi se stvarna predaja, a resursi zamrznuti u prvom Try koraku se stvarno umanjuju.
Nakon uspešnog plaćanja narudžbine status kupona se menja u „iskorišćeno".
- Faza 3: Cancel
Otkaže se izvršenje poslovne operacije, odnosno ponište resursi biznisa rezervisani u Try fazi.
U slučaju neuspeha plaćanja, isteka vremena ili zatvaranja narudžbine, status kupona se menja u „neiskorišćeno".

4 Scale (proširenje)
4.1 Podsetnik za kupone koji uskoro ističu
Periodično skeniranje tabele kupona
Mana: količina skeniranih podataka je prevelika; sa porastom istorijskih podataka uticaće na glavni online biznis i konačno dovesti do sporih SQL upita.
Odložena poruka
Mana: vreme važenja nekih kupona je predugo (preko 30 dana), što može dovesti do velikog nakupljanja u MQ.
Dodavanje tabele obaveštenja
Prednost: količina skeniranih podataka je mala, efikasnost visoka. Brišu se beskorisni zapisi podataka koji su već obavešteni.
Dizajn tabele poruka obaveštenja (notify_msg)
create table t_notify_msg
(
id bigint auto_increment comment 'autoincrement primarni ključ',
coupon_id bigint null comment 'id kupona',
user_id bigint null comment 'id korisnika',
notify_day varchar(255) null comment 'datum za koji treba izvršiti obaveštenje',
notify_type int null comment 'tip obaveštenja, 1-podsetnik o isticanju',
notif_time timestamp null comment 'vreme obaveštenja, obaveštava se u danu u kom se nalazi taj trenutak',
status int null comment 'status obaveštenja: 0-početno stanje, 1-uspeh, 2-neuspeh',
constraint t_notify_msg_id_uindex
unique (id)
);
alter table t_notify_msg
add primary key (id);Podsetnik za istekle kupone
- Već pri kreiranju kupona u tabelu podsetnika notify_msg umeću se zapisi koji treba da podsete.
- Korisnički ID + ID serije + datum obaveštenja postavlja se kao jedinstveni indeks, čime se sprečavaju duplikati zapisa obaveštenja u istoj seriji i garantuje da će se obaveštavati samo jednom dnevno.
- Uspostavlja se indeks notify_time, indeks vremena obaveštenja; svakodnevno skeniranje obaveštenja radi se upitom preko te indeksne kolone, čime se povećava efikasnost upita.
- Nakon završenog obaveštenja podaci u ovoj tabeli gube značenje, pa se zakazanim zadatkom brišu.
4.2 Optimizacija na nivou baze podataka - indeksi


4.3 Interfejs za izdavanje kupona, zaštita ograničavanjem protoka
Ograničavanje protoka na front-end-u
Nakon jednog klika, dugme se kratko onemogući.

Ograničavanje protoka na back-end-u
Deo zahteva se direktno preusmerava na [stranicu o zauzetosti].
Referentni link: https://mp.weixin.qq.com/s/r9lgiOwV5cw8XmfCBUTcUA, Izvor: JavaEdge, Priredio: Chenmo Wang Er
