Kako procesirati prekoračenje vremena narudžbe? Koristimo ovu šemu
Ovo je scenariosko pitanje, kada intervijuter pita:
- Kako procesirati prekoračenje vremena narudžbe?
- Korisnik naruči, postoji 15 minuta kašnjenja plaćanja narudžbe, kako procesirati?
Kako treba odgovoriti? Na Alibaba Cloud developers nalogu sam pronačeo vrlo kvalitetan sadržaj, ovde sam ga organizovao i podelio s vama kao referencu.
Referentni odgovor:
Scenario narudžbe sa kašnjenjem plaćanja može koristiti šemu odgođenih zadataka message queue. To je, kada korisnik naruči, narudžba se stavlja u odgođeni red, kašnjenje je 15 minuta proverava status plaćanja, ako se ne plati u roku, narudžba se otkazuje. Ovaj metod može efikasno smanjiti opterećenje sistema pod visokom concurencijom, smanjiti pritisak polling baze podataka, i kroz automatski mehanizam raspoređivanja message queue osigurati da se narudžba tačno i na vreme procesira. Istovremeno, ovaj dizajn može kroz idempotenciju poruka, distribuirano deployedment itd. poboljšati pouzdanost i proširivost sistema.
Dobro, zatim ćemo analizirati.
U poslovnoj delatnosti preduzeća, narudžba predstavlja namere transakcije između dve strane o proizvodu ili usluzi. Transakcioni narudžbom kreira ovu namenu transakcije, nakon toga kupac može da plati, prodavac može da isporuči.
U e-commerce scenariju, kupac i prodavac nisu licno transakcioni, u mnogim slučajevima potrebno je automatsko zatvaranje narudžbe putem obrade prekoračenja vremena, sledi tok narudžbe:

Kao što je prikazano na gornjoj slici, u toku narudžbe mnogi koraci zahtevaju obradu prekoračenja vremena, uključujući ali ne ograničavajući se na:
- Kupac nije platio u roku: npr. ako nije platio za 15 minuta, narudžba se automatski otkazuje.
- Prodavac nije isporučio u roku: npr. ako prodavac nije isporučio za mesec dana, narudžba se automatski otkazuje.
- Kupac nije primio robu u roku: npr. nakon što prodavac pošalje robu, ako kupac ne klikne da je primio za 14 dana, sistem automatski potvrđuje primljenu robu.
1. JDK ugrađeni odgođeni red
JDK pruža strukturu odgođenog reda DelayQueue, u suštini je enkapsulirao PriorityQueue, može sortirati elemente.

- Stavi narudžbu u DelayQueue, sortiraj po vremenu prekoračenja, od najmanjeg do najvećeg.
- Pokreni nit koja stalno proverava glavu reda, ako je vreme prekoračenja narudžbe došlo, izvadi je iz reda i procesiraj prekoračenje, ažuriraj status narudžbe u bazu podataka.
- Da bi se spričilo gubitku podataka u memoriji pri restartovanju mašine, pri svakom pokretanju mašine treba inicijalizovati nezavršene narudžbe iz baze podataka i dodati ih u DelayQueue.
Prednosti: jednostavno, ne trebaju third-party komponente, troškovi su niski.
Mane:
- Sve narudžbe sa prekoračenjem treba staviti u DelayQueue, zauzima mnogo memorije.
- Ne može se distribuirano procesirati, može se samo izabrati jedan leader u klasteru da procesira, efikasnost je niska.
- Nije pogodno za scenario velike količine narudžbi.
2. RabbitMQ odgođene poruke
RabbitMQ odgođene poruke imaju dva glavna rešenja:
- RabbitMQ Delayed Message Plugin
- Poruka TTL + Dead Letter Exchange
RabbitMQ Delayed Message Plugin je zvanični plugin za odgođene poruke, iako je lako za upotrebu, nije visoko dostupan, ako čvor otpadne doći će do gubitka poruka. Citiram zvanični tekst:
Delayed messages are stored in a Mnesia table (also see Limitations below) with a single disk replica on the current node. They will survive a node restart. While timer(s) that triggered scheduled delivery are not persisted, it will be re-initialised during plugin activation on node start. Obviously, only having one copy of a scheduled message in a cluster means that losing that node or disabling the plugin on it will lose the messages residing on that node.
Rešenje poruka TTL + Dead Letter Exchange, prvo treba razumeti dva koncepta:
①, TTL: vreme života poruke. RabbitMQ može postaviti TTL na red i poruku, ako se postavi na red, sve poruke u redu imaju isto vreme isteka. Kada protekne ovo vreme, smatramo da je poruka "mrtva", zovemo je mrtvom porukom.
②, Dead Letter Exchange (DLX): poruka će ući u dead letter exchange ako zadovolji sledeće uslove
- Poruku je Consumer odbacio i metod reject ima parametar requeue false, tj. neće biti vraćena u red da bi je koristili drugi potrošači.
- Poruka čiji je TTL istekao.
- Poruka koja je odbačena jer je red pun.
Tok odgođene poruke je sledeći:

- Definišite BizQueue, prima dead letter poruke i vrši poslovnu potrošnju.
- Definišite dead letter exchange (DLXExchange), vežite BizQueue, prima odgođene poruke i prosljeđuje ih BizQueue.
- Definišite grupu odgođenih redova DelayQueue_xx, postavite različite TTL, za procesiranje fiksiranih odgođenih 5s, 10s, 30s itd. nivoa odgađanja, i vežite za DLXExchange.
- Definišite DelayExchange, prima poslovne odgođene poruke i prosljeđuje ih u različite odgođene redove na osnovu vremena odgađanja.
Prednosti: može podržati masovne odgođene poruke, podržava distribuirano procesiranje.
Mane:
- Nefleksibilno, može podržavati samo fiksne nivoe odgađanja.
- Složeno za upotrebu, treba konfigurisati hrpu odgođenih redova.
3. RocketMQ vremenske poruke
RocketMQ podržava vremenske poruke u bilo kojem sekundnom intervalu, kao što je prikazano

Lako za upotrebu, samo postavite vreme odgađanja prilikom slanja poruke, kao primer u java kodu:
MessageBuilder messageBuilder = null;
Long deliverTimeStamp = System.currentTimeMillis() + 15L * 60 * 1000; // kašnjenje 15 minuta
Message message = messageBuilder
.setTopic("topic") // postavi ključ indeksa poruke, možeš pretraživati određenu poruku po ključnoj reči.
.setKeys("messageKey") // postavi Tag poruke, potrošač koristi Tag za filtriranje poruka.
.setTag("messageTag") // postavi vreme odgađanja
.setDeliveryTimestamp(deliverTimeStamp) // telo poruke
.setBody("messageBody".getBytes())
.build();
SendReceipt sendReceipt = producer.send(message);
System.out.println(sendReceipt.getMessageId());Kako RocketMQ implementira vremenske poruke?
U RocketMQ, koristi se klasičan algoritam vremenskog točka. Kroz TimerWheel opisuje različite trenutke vremenskog točka, kroz TimerLog beleži poruke u različitim trenucima.
Svaki "kanal" TimerWheel-a predstavlja jedan trenutak, istovremeno postoji firstPos koji pokazuje prvi TimerLog slog svih vremenskih poruka u ovom trenutku, i lastPos koji pokazuje poslednji TimerLog slog svih vremenskih poruka u ovom trenutku. I za poruke u istom trenutku, njihovi TimerLog-ovi će biti povezani kroz prevPos u listu.

Kada treba dodati novi slog, npr. sada treba dodati "1-4". Tada ćete postaviti prevPos novog sloga na trenutni lastPos, tj. "1-3", zatim izmeniti lastPos da pokazuje na "1-4". Tako su svi TimerLog slogovi u istom kanalu povezani.

Prednosti
- Visoka preciznost, podržava bilo koji trenutak.
- Nizak prag za upotrebu, isto kao i obične poruke.
Mane
- Ograničenje upotrebe: maksimalno vreme odgađanja je 24 sata.
- Visoki troškovi: svaka narudžba zahteva novu vremensku poruku, i neće se odmah konzumirati, MQ snosi veliki trošak skladištenja.
- Veliki broj poruka u istom trenutku može dovesti do kašnjenja poruka: implementacija vremenskih poruka zahteva da prvo čekaju u vremenskom skladištu pre isporuke, kada vreme istekne, poruke se isporučuju potrošačima. Dakle, ako veliki broj vremenskih poruka postavite isto vreme isteka, u tom trenutku će istovremeno biti procesirane mnoge poruke, što će izazvati preveliki pritisak na sistem, dovesti do kašnjenja isporuke poruke, uticati na preciznost vremena.
4. Redis nadzor isteka
Redis podržava nadzor isteka, takođe može postići istu sposobnost kao RocketMQ vremenske poruke, konkreti koraci su sledeći:
①, redis konfiguraciona datoteka omogući "notify-keyspace-events Ex"

②, monitoriraj povratni poziv isteka key-a, kao primer u java kodu
RedisListenerConfig
@Configuration
public class RedisListenerConfig {
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory factory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(factory);
return container;
}
}RedisKeyExpirationListener
@Component
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern) {
String expiredKey = message.toString();
System.out.println("Čuo ključ: " + expiredKey + "je istekao");
}
}Tok korišćenja Redis za procesiranje prekoračenja vremena narudžbe je sledeći:

Ovo rešenje na prvi pogled deluje bez problema, ali u stvarnoj produkciji se ne preporučuje, pogledajmo princip Redis isteka vremena
Svaki put kada postavite vreme isteka za key, Redis će staviti taj key sa vremenom isteka u expires rečnik, u redisDb kroz expires polje održava:
typedef struct redisDb {
dict *dict; /* Održava sve parove key-vrednost */
dict *expires; /* Rečnik isteka, održava key-ove sa postavljenim vremenom isteka */
// Ostali članovi se mogu dodati ovde
} redisDb;Expires rečnik je u suštini lista, svaki čvor ima sledeću strukturu:
- key je pokazivač, pokazuje na neki key objekat.
- value je long long integer, čuva vreme isteka key-a.

Redis uglavnom koristi strategije periodičnog brisanja i lenjog brisanja za brisanje isteklih key-eva
- Periodično brisanje: svakih izvesnog vremena (podrazumevano 100ms) nasumično bira neke key-ove sa postavljenim vremenom isteka, proverava da li su istekli, ako jesu, briše ih. Razlog za ovo je taj, što se ograničava trajanje i učestalost izvršenja brisanja da bi se smanjio uticaj na CPU. Inače, svakih 100ms trebalo bi da prođe kroz sve key-ove sa postavljenim vremenom isteka, to bi previše opteretilo CPU.
- Lenjo brisanje: ne briše istekle key-ove aktivno, svaki put kada se pristupa bazi podataka key-u, proverava da li je key istekao, ako jeste, briše taj key. Lenjo brisanje ima problem, ako je key već istekao, ali se stalno ne pristupa, key će zauvek ostati u bazi.
Na osnovu navedenog principa može se videti, da Redis brisanje isteka nije precizno, u scenariju procesiranja prekoračenja vremena narudžbe, lenjo brisanje se u suštini ne može koristiti, ne može se garantovati da će key biti odmah obrisan kada istekne, još manje može garantovati odmah obaveštenje. Ako je količina narudžbi velika, kašnjenje nekoliko minuta je takođe moguće.
Redis obaveštenje o isteku je takođe nepouzdano, kada Redis šalje obaveštenje o isteku, ako se aplikacija uprano restartuje, moguće je da se izgubi događaj obaveštenja, što će dovesti do toga da se narudžba ne može zatvoriti, postoji problem sa stabilnošću. Ako morate koristiti rešenje nadzora isteka Redis-a, predlažem da kroz zadatak sa fiksnim vremenom napravite mehanizam kompenzacije.
5. Zadatak sa fiksnim vremenom distribuirano batch procesiranje
Rešenje zadataka sa fiksnim vremenom distribuirano batch procesiranje, tj. kroz zadatak sa fiksnim vremenom stalno polling bazu podataka narudžbe, izvlači istekle narudžbe, distribuira ih na različite mašine distribuirano procesiranje:

Korišćenje rešenja zadataka sa fiksnim vremenom batch procesiranje ima sledeće prednosti:
- Visoka stabilnost: zasnovano na obaveštenjima (kao MQ i Redis), uvek je strah od gubitka događaja obaveštenja u ekstremnim situacijama. Korišćenje zadataka batch procesiranja, samo treba osigurati idempotenciju posla, ako ovaj batch ne izvuče neke narudžbe, ili se pri procesiranju narudžbe aplikacija restartuje, sledeći batch ponovo može izvući i procesirati, stabilnost je vrlo visoka.
- Visoka efikasnost: zasnovano na MQ rešenju, treba jedna vremenska poruka po narudžbi, consumer pri procesiranju vremenske poruke takođe mora ažurirati narudžbu po narudžbi, qps baze je visok. Korišćenje zadataka batch procesiranje, jednom izvlači batch narudžbi, procesira završeno, može batch ažurirati status narudžbe, smanjuje qps baze. U scenariju masovnog procesiranja narudžbi, batch procesiranje je najefikasnije.
- Održavost: zasnovano na skladištenju baze podataka, lako je vršiti izmene, pauziranje, otkazivanje itd. operacije nad narudžbama, šta vidiš to dobijaš. Ako se poslovni proces ne uspe, možeš direktno kroz SQL da izmeniš bazu da radiš batch održavanje.
- Niski troškovi: u poređenju sa drugim rešenjima koja trebaju third-party komponente skladištenja, ponovno korišćenje baze znatno smanjuje troškove.
Ali korišćenje zadatka sa fiksnim vremenom ima prirodni nedostatak: ne može postići visoku preciznost. Vreme kašnjenja zadatka sa fiksnim vremenom određuje se periodom raspoređivanja zadatka. Ako frekvenciju postavite malom, qps baze će biti prilično visok, lako može dovesti do prevelikog pritiska na bazu, što utiče na normalni posao na liniji.
Dakle obično treba izdvojiti center za vremensko odlaganje i bazu za vremensko odlaganje da posebno radi raspoređivanje vremenskog odlaganja narudžbi, u Alibasi, gotovo svi poslovi koriste rešenje batch procesiranja zasnovano na zadatku sa fiksnim vremenom za procesiranje prekoračenja vremena narudžbe, SLA može postići ispod 30 sekundi:

Kako da se različiti čvorovi centra za vremensko odlaganje koordiniraju da bi izvlačili različite podatke?
Obično rešenje je korišćenje sistema raspoređivanja zadataka, open-source sistemi raspoređivanja zadataka uglavnom podržavaju model šarke, prilično pogodno za polling baze podataka podeljene na tabele, npr. jedna šarka predstavlja jednu podeljenu tabelu. Ali ako je podeljenih tabela posebno mnogo, konfiguracija modela šarke je prilično teška. Pored toga, ako postoji samo jedna velika tabela, ili centar za vremensko odlaganje koristi drugo skladište, ova dva modela nisu baš pogodna.
Alibaba sistem raspoređivanja zadataka SchedulerX, ne samo da je kompatibilan sa mainstream open-source sistemima raspoređivanja zadataka i Spring @Scheduled oznakom, već je razvio laki MapReduce model, za bilo koje heterogene izvore podataka, nekoliko linija koda može ostvariti masovno procesiranje podataka na nivou sekundi.
①, implementiranjem map funkcije, kroz kod sam konstruišem šarke, SchedulerX će dodeliti šarke ravnomerno na različite čvorove centra za vremensko odlaganje distribuirano izvršenje.

②, implementiranjem reduce funkcije, možeš raditi agregaciju, možeš proceniti koje šarke nisu uspele u ovom batch-u, tako da obavestiš downstrim za procesiranje.

Korišćenje SchedulerX zadataka batch procesiranje rešenja takođe ima sledeće prednosti:
- Bez održavanja, niski troškovi: nije potrebno sami graditi sistem raspoređivanja zadataka, upravlja se u cloudu.
- Posmatljivost: pruža istoriju izvršenja zadataka, pregled steka, log servis, trace link itd.
- Visoka dostupnost: podržava dvoclan aktivno-aktivno disaster recovery, podržava više kanala monitoringa i alarma.
- Mešovito raspoređivanje: može upravljati mašinama u Aliyun cloudu, takođe može upravljati mašinama van Aliyun cloudu.
Zaključak
Ako je preciznost vremena visoka, vreme prekoračenja je u okviru 24 sata, i nema pritiska vršnog opterećenja, preporučujem rešenje RocketMQ vremenskih poruka.
U e-commerce poslovnom scenariju, mnogo scenarija prekoračenja narudžbe je preko 24 sata, preciznost vremena nije tako osetljiva, i postoji veliki broj narudžbi koje treba batch procesirati, preporučujem rešenje batch procesiranja zasnovano na zadatku sa fiksnim vremenom.
