Kako obezbediti doslednost podataka između MySQL i Redis?
Priredio: Chenmo Wang Er, pogledaj link za preštampavanje, autor: Lou Zai, pogledaj originalni link.
Kako obezbediti doslednost podataka između MySQL i Redis? Sa ovim pitanjem sam se susreo veoma rano, ali ga nikada nismo detaljno proučili. Prošlog meseca sam pregledao kurs iz Geeka i u jednom članku je ovo bilo posebno objašnjeno. Pošto su me pratiooci pitali baš to pitanje, osećam da je vredno izdvojiti poseban članak.
Pročitao sam mnogo povezanih članaka, ali mi se čini da nijedan nije dobro objašnjen. Mnogi članci opisuju različite strategije, kao što su Cache-Aside strategija, Read-Through / Write-Through strategija i Write-Back strategija, što mi zaista ne deluje previše korisno, a zatim neki članci pokrivaju samo delimične slučajeve i ne navode optimalno rešenje.
Da odmah dam zaključak: Pod uslovom zahteva za rad u realnom vremenu, ne postoji rešenje u kojem oba sistema u potpunosti ostaju dosledni — postoje samo rešenja konačne doslednosti (eventual consistency). Na osnovu brojnih rešenja sa interneta, sažeo sam 6 pristupa — pogledajte sadržaj:

Loša rešenja
1. Prvo upiši u MySQL, zatim upiši u Redis

Objašnjenje dijagrama:
- Ovo je sekventni dijagram koji opisuje redosled poziva zahteva;
- Narandžasta linija je zahtev A, crna linija je zahtev B;
- Narandžasti tekst označava podatke koji su na kraju nedosledni između MySQL i Redis;
- Podatak se ažurira sa 10 na 11;
- Svi naredni dijagrami imaju isto značenje i neće se ponovo objašnjavati.
Zahtevi A i B oba prvo upisuju u MySQL, a zatim u Redis. Pod visokom istovremenošću, ako zahtev A zastane pri upisu u Redis, a zahtev B već redom završi ažuriranje podataka, javlja se problem prikazan na dijagramu.
Ovaj dijagram je veoma jasno nacrtan, tako da nema potrebe da dalje objašnjavamo, ali postoji jedan preduslov: za čitanje zahteva, prvo se čita iz Redis, a ako nema podatka, čita se iz baze podataka, međutim zahtevi za čitanje ne upisuju nazad u Redis. Drugim rečima, zahtevi za čitanje ne ažuriraju Redis.
2. Prvo upiši u Redis, zatim upiši u MySQL

Isto kao i „prvo upiši u MySQL, zatim upiši u Redis" — pogledajte dijagram i odmah je jasno.
3. Prvo obriši Redis, zatim upiši u MySQL
Ovaj dijagram se razlikuje od prethodnih. Ranije su i zahtev A i zahtev B bili zahtevi za ažuriranje, dok je ovde zahtev A za ažuriranje, ali je zahtev B zahtev za čitanje, i zahtev B pri čitanju upisuje nazad u Redis.

Zahtev A prvo briše keš, ali zbog mogućeg zastoja podatak nije ažuriran u MySQL, što dovodi do nedoslednosti podataka.
Verovatnoća ovog slučaja je prilično velika, jer ažuriranje MySQL u zahtevu A može potrajati duže, dok su prva dva koraka zahteva B samo upiti i biće veoma brza.
Dobra rešenja
4. Prvo obriši Redis, zatim upiši u MySQL, zatim ponovo obriši Redis
Za rešenje „prvo obriši Redis, zatim upiši u MySQL", kako bismo rešili konačnu nedoslednost, dovoljno je ponovo obrisati Redis. Ovo je takozvano „dvostruko brisanje keša".

Radi lakšeg čitanja dijagrama, za plavi tekst, „obriši keš 10" mora doći nakon „upiši nazad u keš 10". Kako obezbediti da zaista dođe nakon? Prvo rešenje koje se navodi na internetu je da poslednje brisanje u zahtevu A sačeka 500 ms.
Ovo rešenje, samo pogledajte — svakako ga ne bih koristio, previše je trivijalno i rizik nije pod kontrolom.
Postoji li bolje rešenje? Preporučujem asinhrono serijsko brisanje, odnosno stavljanje zahteva za brisanje u red poruka.

Asinhrono brisanje ne utiče na produkcione poslove, a serijska obrada obezbeđuje ispravno brisanje u uslovima istovremenosti.
Šta ako dvostruko brisanje ne uspe? Na internetu se predlaže dodavanje vremena isteka keša u Redis, što ne mogu da prihvatim. Lično preporučujem uvođenje mehanizma za ponovni pokušaj — može se iskoristiti mehanizam za ponavljanje reda poruka, ili napraviti sopstvenu tabelu koja beleži broj pokušaja — metoda ima puno.
Kratak rezime:
- Za „dvostruko brisanje keša" ne koristiti slepo
sleep 500 ms;- Pomoću asinhrone i serijske obrade reda poruka realizovati poslednje brisanje keša;
- Ako brisanje keša ne uspe, dodati mehanizam za ponovni pokušaj.
5. Prvo upiši u MySQL, zatim obriši Redis

Za gorenavedeni slučaj, pri prvom upitu, zahtev B čita podatak 10, ali je podatak u MySQL 11. Ovo je nedoslednost koja se javlja samo jednom i koja je prihvatljiva za poslove koji ne zahtevaju jaku doslednost. (Kada nije prihvatljivo? Na primer za flash prodaju, servis za upravljanje zalihama, itd.)
Kada zahtev B izvrši drugi upit, pošto ne pogodi Redis, ponovo će upitati bazu podataka, a zatim upisati nazad u Redis.

Ovde je potrebno ispuniti 2 uslova:
- Keš je upravo automatski istekao;
- Vreme potrebno da zahtev B iz baze podataka pročita 10 i upiše nazad u keš duže je od vremena potrebnog da zahtev A upiše u bazu i obriše keš.
Za drugi uslov, svi znamo da ažuriranje baze podataka sigurno traje duže od upita, pa je verovatnoća ovog slučaja veoma mala, a istovremeno ispunjavanje oba uslova je još manje.
6. Prvo upiši u MySQL, a zatim preko Binlog asinhrono ažuriraj Redis
Ovo rešenje prvenstveno osluškuje na Binlog MySQL, a zatim asinhrono ažurira podatak u Redis. Preduslov za ovo rešenje je da zahtevi za čitanje ne upisuju nazad u Redis.

Ovo rešenje obezbeđuje konačnu doslednost između MySQL i Redis, ali ako zahtev B usput treba da upita podatak — ako nema podatka u kešu, upitaće direktno bazu; ako pak ima podatka u kešu, upit i dalje može pokazati nedoslednost.
Dakle, ovo rešenje je konačno rešenje za postizanje konačne doslednosti, ali ne može da garantuje rad u realnom vremenu.
Poređenje nekoliko rešenja
Uporedimo gore diskutovanih 6 rešenja:
- Prvo upiši u Redis, zatim upiši u MySQL
- Ovo rešenje zasigurno ne bih koristio — ako baza podataka padne, a vi ste upisali podatak u keš, u bazi nema podatka, što je katastrofalno;
- Video sam da su kolege ranije ovo radili — ako upis u bazu ne uspe, vrše suprotnu operaciju nad Redis, ali šta ako i ta suprotna operacija ne uspe? Da li onda treba dodati i ponovni pokušaj?
- Prvo upiši u MySQL, zatim upiši u Redis
- Za projekte sa niskom istovremenošću i niskim zahtevima za doslednost, mnogi rade baš ovako — i ja sam ranije često tako radio, ali ne preporučujem;
- Kada Redis nakratko postane nedostupan, potrebno je aktivirati alarm i zatim rešavati oflajn.
- Prvo obriši Redis, zatim upiši u MySQL
- Ovaj pristup nikada nisam koristio, možete ga direktno zanemariti.
- Prvo obriši Redis, zatim upiši u MySQL, zatim ponovo obriši Redis
- Iako je ovaj pristup izvodljiv, čini mi se previše komplikovanim — još morate dodati red poruka za asinhrono brisanje Redis.
- Prvo upiši u MySQL, zatim obriši Redis
- Ovaj pristup više preporučujem — ako brisanje Redis ne uspe, možete pokušati još nekoliko puta, a u suprotnom aktivirati alarm;
- Ovo rešenje je najbolje među rešenjima u realnom vremenu i preporučuje se u scenarijima visoke istovremenosti.
- Prvo upiši u MySQL, a zatim preko Binlog asinhrono ažuriraj Redis
- Za državnu katastrofu u drugoj lokaciji, agregaciju podataka i sl., preporučuje se ovaj pristup — na primer binlog + kafka, pri čemu doslednost podataka može dostići nivo sekundi;
- U čisto visoko-istovremenim scenarijima, kao što su kupovina na brzinu ili flash prodaja, ne preporučuje se ovaj pristup.
Lični zaključak:
- Rešenje za doslednost u realnom vremenu: usvojiti strategiju „prvo upiši u MySQL, zatim obriši Redis". Iako će i ovde povremeno doći do nedoslednosti, uslovi koje treba ispuniti prilično su strogi, pa je ovo optimalno rešenje koje pod uslovom rada u realnom vremenu maksimalno ispunjava zahteve doslednosti.
- Rešenje za konačnu doslednost: usvojiti „prvo upiši u MySQL, a zatim preko Binlog asinhrono ažuriraj Redis". Kombinacijom Binlog i reda poruka može se asinhrono ažurirati Redis, što je optimalno rešenje za konačnu doslednost.
Priredio: Chenmo Wang Er, pogledaj link za preštampavanje, autor: Lou Zai, pogledaj originalni link.
