Redis intervju pitanja, 57 Redis pitanja (46.000 reči, 286 crteža), neophodno čitanje za preokret intervjua 👍

Uvod
46.000 reči 286 crteža, detaljno obrazloženih 57 najčešćih Redis intervju pitanja (da nema teških pitanja za učenje), kandidati koji nauče ova Redis pitanja će ovaj put pretući intervjua, mislim da je sigurno (ručni pas). Organizovao: Chenmo Wang Er, pritisnilink za preuzimanje, autor: Sanfen E, pritisnilink za originalni tekst.
Svetla verzija je pogodnija za štampanje, što je i način koji mnogi studenti vole, štampanje za učenje je efikasnije.

- aprila 2025. godine počeo sam sa radom na drugom izdanju.
- Za česta pitanja, obeležiću gde se pojavljuju u „Vodiču za Java intervjue (plaćeno)“, koja kompanija, koji je originalni zadatak, i dodaću zvezdicu 🌟, sadržaj je jasan; ako želiš da uštediš vreme, možeš da prioritizuješ učenje ovih pitanja kako bi što pre upoznao protivnika i bio neporažen u sto bitaka.
- Razlikujem elitan odgovor na pitanja od objašnjenja principa i osnova, da kandidati ne samo da znaju šta, već i zašto, uz efikasan odgovor na intervjuu.
- Kombinujem sa projektima (Tehnički Pai, pmhub) u organizaciji izražavanja, da intervjuer maksimalno oseti tvoju iskrenost, a ne mehaničko učenje.
- Popravio sam probleme iz prvog izdanja, uključujući povratne informacije prijatelja iz privatnih poruka, komentare u sekciji za komentare na sajtu, kao i issue iz GitHub repozitorijuma, da bi ovaj vodič za intervjue bio potpuniji.
- Dodao sam neke offer ponude koje su prijatelji iz Erge programerske planine dobili, zahvalnost na preokretu intervjua, i priznanje za izmenu životopisa, kako bih motivisao sve i dao više samopouzdanja.
- Unapredio sam formatiranje, dodao crteže, reorganizovao odgovore, da bi bili više konverzacionalni i bliže očekivanjima intervjua.

Uključio sam Ergeov napredni put za Java, napredni put za JVM, napredni put za konkurentno programiranje, i sve verzije preokreta intervjua, pokrivajući osnove Java-e, Java kolekcije, Java konkurentnost, JVM, Spring, MyBatis, računarske mreže, operativne sisteme, MySQL, Redis, RocketMQ, distribuirane sisteme, mikroservise, dizajn obrazaca, Linux itd. 16 glavnih tema, ukupno više od 400.000 reči, više od 2000 crteža, može se reći da je iskrenost puna.
Pokažimo tamnu verziju PDF-a, formatiranje je jasno, font je elegantan, pogodnije za noćno čitanje, noću će biti ugodnije.

Osnove
1.🌟 Kaži šta je Redis?
Redis je vrsta NoSQL baze podataka zasnovane na parovima ključ-vrednost.

Glavna karakteristika je što podatke drži u memoriji, u poređenju sa direktnim pristupom disku relacionih baza podataka, brzina čitanja i pisanja je mnogo veća, u suštini može da dostiže nivou odgovora u mikrosekundama.
Zato u nekim scenama sa visokim zahtevima za performanse, poput keširanja vrućih podataka, sprečavanja preopterećenja interfejsa, koristiće se Redis.
Ne samo to, Redis podržava perzistenciju, može asinhrono da upiše podatke iz memorije na disk, da bi se mogli vratiti posle pada servisa i ponovnog pokretanja.
Koji je razlika između Redis-a i MySQL-a?
Redis pripada nerelacionim bazama podataka, podaci se čuvaju u memoriji u formi parova ključ-vrednost; MySQL pripada relacionim bazama podataka, podaci se čuvaju na disku u formi redova i kolona.

U stvarnom razvoju, MySQL će se koristiti kao glavna memorija, Redis kao keš, tako što se prvo proverava Redis, ako ga nema zatim se proverava MySQL i upisuje nazad u Redis kako bi se poboljšala ukupna performansa sistema.
Gde u projektu koristiš Redis?
U Tehnički Pai realni projekat postoje mnoga mesta gde se koristi Redis, na primer lista aktivnih korisnika koristi zset, lista dozvoljenih autora koristi set.

Takođe Session nakon prijave korisnika, mapa sajta SiteMap, redom koriste string i heš tabelu dva tipa podataka Redis-a.

Jedna od izazovnijih primena je korišćenje Lua skripti da se enkapsulira Redis setnex komanda za realizaciju distribuirane brave, kako bi se osiguralo da u scenariju visoke konkurentnosti, visokofrekventni pristup vrućim člancima ne probije MySQL u kratkom vremenu.

Da li ste deployed Redis?
Prva verzija odgovora:
Samo lokalno sam deployed samostalnu verziju, preuzeo sam Redis instalacioni paket, raspakovao sam i pokrenuo redis-server komandu.
Druga verzija odgovora:
Imam iskustva u produkcionom okruženju sa samostalnom verzijom Redis-a, sa zvaničnog sajta sam preuzeo izvorni kod, raspakovao i izvršio make && make install kompajliranu instalaciju. Zatom uredio redis.conf datoteku, uključio remote pristup, podesio lozinku, ograničio memoriju, podesio strategiju isteka memorije, uključio AOF perzistenciju itd.:
bind 0.0.0.0 # dozvoli remote pristup
requirepass your_password # podesi lozinku
maxmemory 4gb # ograniči memoriju, izbegni OOM
maxmemory-policy allkeys-lru # strategija istiskivanja memorije
appendonly yes # uključi AOF perzistencijuTreća verzija odgovora:
Koristio sam Docker da povuče Redis sliku i izvrši kontejnersku deployed.
docker run -d --name redis -p 6379:6379 redis:7.0-alpineDa li ste deployed Redis visoku dostupnost?
Imam iskustva sa deployed mehanizmom čuvana, ovo je relativno zrelo rešenje visoke dostupnosti, naše produkciono okruženje deployed jedan glavni i dva potčinjena Redis primera, plus tri Sentinel čvora koji ih prate. Sentinel konfiguracija je relativno jednostavna, uglavnom podesila uslove za presudu neuspeha i prag vremenskog isteka.
Konfiguracija glavnog čvora:
port 6379
appendonly yesKonfiguracija potčinjenog čvora:
replicaof 192.168.1.10 6379Konfiguracija čvora čuvana:
sentinel monitor mymaster 192.168.1.10 6379 2
sentinel down-after-milliseconds mymaster 5000
sentinel failover-timeout mymaster 60000
sentinel parallel-syncs mymaster 1Kada se glavni čvor pokvari, Sentinel može automatski da detektuje i dogovori novi glavni čvor, ovaj proces traje oko 10-15 sekundi.
U drugom velikom projektu koristili smo Redis Cluster klaster rešenje. Ovaj projekat ima veliku količinu podataka i brzo raste, potrebna je sposobnost horizontalnog proširenja. Deployed smo 6 glavnih čvorova, svaki glavni čvor ima jednog potčinjenog čvora, formirajući inicijalni klaster od 3 glavna i 3 potčinjena. Redis Cluster podešavanje je komplikovanije od Sentinel, treba pravilno podesiti komunikaciju između čvorova klastera, mapiranje fragmenata itd.
redis-server redis-7000.conf
redis-server redis-7001.conf
...
# koristi redis-cli za kreiranje klastera
# Redis automatski hešira ključeve u 16384 slota
# glavni čvorovi ravnomerno dele slotove, potčinjeni čvorovi automatski prate
redis-cli --cluster create \
127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 \
127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 \
--cluster-replicas 1Redis Cluster najveća prednost je automatska fragmentacija podataka, možemo prosto dodajući čvorove proširiti kapacitet klastera. Pored toga, njegovo prebacivanje pri neuspehu je takođe veoma brzo, obično se završi za nekoliko sekundi.
Za neke lake aplikacije, koristio sam i rešenje glavno-potčinjena replikacija plus ručno prebacivanje pri neuspehu. Glavni čvor je zadužen za čitanje i pisanje, potčinjeni čvorovi za čitanje. Pri ručnom prebacivanju, prvo ćemo podići potčinjenog čvora na glavnog, a zatim rekonfigurisati ostale potčinjene čvorove.
# 1. ukloni identitet potčinjenog čvora
redis-cli -h <slave-ip> slaveof no one
# 2. preusmeri ostale potčinjene čvorove na novi glavni čvor
redis-cli -h <other-slave-ip> slaveof <new-master-ip> <port>Koji keš baze podataka ste koristili, osim redis-a?
U Tehnički Pai realni projekat takođe se koriste Guava Cache i Caffeine kao lokalni keš, Guava Cache je pogodan za keširanje malih razmera, Caffeine je bolji u performansama, podržava više naprednih karakteristika.

Caffeine se obično koristi kao sekundarni keš, uglavnom za čuvanje nekih podataka koje se retko menjaju, kako bi se smanjilo opterećenje Redis-a.
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak Huaweijevog prvog intervjua: kaži razliku između Redis-a i HashMap-a
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak prvog intervjua ByteDance komercijalnog tima: razlika između Redis-a i MySQL-a
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 7. intervjua Agricultural Bank of China Java backend: osnovni znanja o Redis-u
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak prvog intervjua Huawei OD kandidata 1: razumevanje Redis-a, deployed rešenje?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 3. intervjua Agricultural Bank of China Java backend: gde u projektu se koristi Redis
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 3. tehničkog intervjua 360 kandidata: da li ste koristili redis, za šta
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak intervjua China Merchants Bank kandidata 6 China Merchants Bank Network Technology: da li razumete MySQL, Redis?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak Baidu kandidata 1 Erxin Yiyin 25 stažiranje Java backend: gde u projektu se koristi redis keš, zašto je redis brz?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 9. intervjua državnih preduzeća kandidata: koja baza podataka se najviše koristi (rekao sam MySQL i Redis)
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak intervjua Honor kandidata 4: koja je razlika između Redis-a i MySQL-a?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 4. intervjua Hikvision kandidata: Redis deployed
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak prvog intervjua Huawei OD kandidata 1: razumevanje Redis-a, deployed rešenje?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 30. kandidata Tencent Music intervju: koje su Redis deployed metode, koje su prednosti i mane?
memo: 23. septembra 2025. izmenjeno do ovde, danas sam pomogao prijatelju da promeni životopis, primio sam povratnu informaciju jedne prijateljice da se od 16. avgusta pridružila planini, svaki dan se puni i uči na planini, naučila je mnogo stvari. Za ovakvu pozitivnu povratnu informaciju sam veoma srećan.

2.Šta Redis može da radi?
Redis može da se koristi kao keš, na primer staviti visokofrekventne detalje članka, informacije o proizvodu, informacije o korisniku u Redis, i postaviti vreme isteka kako bi se osigurala konzistentnost podataka, time se može smanjiti pritisak pristupa bazi podataka.

Redis Zset može da se koristi za realizaciju liste poena, liste vrućih tema, sortiranje kroz score polje, uzimanje prvih N elemenata, može se realizovati funkcija TOPN liste.

Korišćenjem Redis SETNX komande ili Redisson-a može se realizovati distribuirana brava, osiguravajući da u isto vreme samo jedan čvor može držati bravu; da bi se sprečilo mrtvo zaključavanje, može se postaviti vreme isteka za bravu, automatski se oslobađa nakon isteka; i najbolje pokrenuti praćenje niti, kada zadatak još uvek nije završen, automatski produžiti bravu.

Ako je interfejs za sekundalnu kupovinu, može se koristiti Lua skripta za realizaciju algoritma token bucket, ograničavanje da se u sekundi može obraditi samo N zahteva.
-- KEYS[1]: ključ token bucket-a
-- ARGV[1]: kapacitet bucketa
-- ARGV[2]: stopa generisanja tokena (po sekundi)
-- ARGV[3]: trenutna vremenska oznaka (sekunde)
local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'timestamp')
local tokens = tonumber(bucket[1]) or ARGV[1]
local last_time = tonumber(bucket[2]) or ARGV[3]
local rate = tonumber(ARGV[2])
local capacity = tonumber(ARGV[1])
local now = tonumber(ARGV[3])
-- izračunaj novi broj tokena
local delta = math.max(0, now - last_time)
local add_tokens = delta * rate
tokens = math.min(capacity, tokens + add_tokens)
last_time = now
local allowed = 0
if tokens >= 1 then
tokens = tokens - 1
allowed = 1
end
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'timestamp', last_time)
redis.call('EXPIRE', KEYS[1], 3600) -- vreme isteka može se podesiti
return allowedPozivanje Lua skripte u Javi:
// parametri token bucket-a
int capacity = 10; // kapacitet bucketa
int rate = 2; // 2 tokena po sekundi
long now = System.currentTimeMillis() / 1000;
String key = "token_bucket:user:123";
// poziv Lua skripte, povratna vrednost 1 znači prošlo, 0 znači ograničeno
Long allowed = (Long) redis.eval(luaScript, 1, key, String.valueOf(capacity), String.valueOf(rate), String.valueOf(now));Šta treba razmatrati kada redis koristiš kao keš, u pogledu poslovnih aspekata?
Jedna tip je klasičan dizajn keš sistema (probijanje, probijanje zastavice, lavina), druga tip su problemi poslovnog keša koji su usko povezani sa poslovnom logikom (konzistentnost podataka, granularnost keša itd.).
Kada se podaci u bazi promene, kako osigurati da se podaci u kešu takođe sinhrono ažuriraju? Ako se nekoordinisano dobro, korisnici će videti „prljave podatke“.
Pored toga, trebamo li keširati kompletni objekat koji sadrži različite povezane informacije, ili samo one najčešće korišćene osnovne polja?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 7. intervjua Agricultural Bank of China Java backend: osnovni znanja o Redis-u
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 7. kandidata ByteDance Java backend stažiranja prvog intervjua: kaži zašto koristiti Redis za čuvanje liste dozvola?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 20. kandidata ByteDance testa prvog intervjua: kakve benefite ima redis, zašto koristiti redis
memo: 28. aprila 2025. izmenjeno do ovde, danas sam pomogao prijatelju da promeni životopis, sreo sam jednog prijatelja koji je magistrirao na Southeast University, planina može da privuče toliko odličnih prijatelja, stvarno sam veoma srećan.

3.🌟Koje su tipove podataka Redis?
Redis podržava pet osnovnih tipova podataka: string, lista, heš, set i uređeni set.

Postoje i tri proširena tipa podataka: Bitmap za operacije na nivou bitova, HyperLogLog za procenu kardinalnosti, GEO za čuvanje i pretraživanje geografskih koordinata.
Detaljno kaži za string?
String je najosnovniji tip podataka, može čuvati tekst, brojeve ili binarne podatke, maksimalni kapacitet je 512 MB.

Pogodno za keširanje jednog objekta, na primer verification kod, token, brojač itd.
Detaljno kaži za listu?
Lista je uređena kolekcija elemenata, podržava umetanje/brisanje elemenata s početka ili kraja, često se koristi za red poruka ili listu zadataka.

Detaljno kaži za heš?
Heš je kolekcija parova ključ-vrednost, pogodan za čuvanje objekata, poput informacija o proizvodu, korisniku itd. Na primer value = {name: 'Chenmo Wang Er', age: 18}.

Detaljno kaži za set?
Set je neuređen i bez ponavljanja, podržava operacije preseka i unije, efikasnost upita može dostići nivo O(1), uglavnom se koristi za uklanjanje duplikata, labele, zajedničke prijatelje itd.

Detaljno kaži za uređeni set?
Elementi uređenog set-a su poređani po poenima, podržavaju opseg upita, pogodni za liste najboljih ili redove prioriteta.

Detaljno kaži za Bitmap?
Bitmap može kompaktno čuvati grupu binarnih bitova u kontinuiranoj memoriji, svaki bit predstavlja status objekta, na primer da li je prisutan, da li je aktivan itd.

Na primer korisnik 0 je prisutan 1, korisnik 1 nije prisutan 0, korisnik 2 je prisentan, Redis će staviti ove statuse u kontinualni binarni niz 101, za 100 miliona korisnika prisustvo je potrebno samo 100,000,000 / 8 / 1024 ≈ 12MB prostora, stvarno ušteda do ludila.
Detaljno kaži za HyperLogLog?
HyperLogLog je verovatnosna struktura podataka za statistiku kardinalnosti, može u samo 12KB memoriji da broji jedinstvene elemente u masivnim skupovima podataka, greška je samo 0.81%.

Na osnovu unapređenog LogLog algoritma, prvo hešira svaki element u binarni niz, zatim uzima prvih 14 bitova za grupisanje, stavlja u 16384 baketa, beleži najveći broj vodećih nula u svakoj grupi, konačno koristi aproksimacionu formulu za izračunavanje ukupne kardinalnosti.

baketa, svaki bucket 6 Bit, tačno 16384 * 6 /8 / 1024 K = 12KB, 8 bit = 1 bajt.
Uzmimo jednostavan primer, pretpostavimo da postoji magična heš funkcija koja može da raširi element u binarni broj, na primer:
| Element | Heš vrednost | Broj vodećih nula |
|---|---|---|
| userA | 000100101… | 3 |
| userB | 001010011… | 2 |
| userC | 000000101… | 6 |
Može se primetiti, što je duža heš vrednost, ima više vodećih nula, što takođe ukazuje na to da je više elemenata u skupu.
Primer statističkog sistema UV velikih veb sajtova:
public class UVCounter {
private Jedis jedis;
public void recordVisit(String date, String userId) {
String key = "uv:" + date;
jedis.pfadd(key, userId);
}
public long getUV(String date) {
return jedis.pfcount("uv:" + date);
}
public long getUVBetween(String startDate, String endDate) {
List<String> keys = getDateKeys(startDate, endDate);
return jedis.pfcount(keys.toArray(new String[0]));
}
}Detaljno kaži za GEO?
GEO se koristi za čuvanje i pretraživanje informacija o geografskoj lokaciji, može se koristiti za izračunavanje distance između dve tačke, pronalaženje drugih elemenata u radijusu određene lokacije.
Uobičajeni scenariji primene uključuju: ljudi ili prodavnice u blizini, izračunavanje distance između dostavljača i prodavnice, ocena da li je korisnik ušao u određenu oblast itd.
Na osnovu ZSet implementacije, kroz Geohash algoritam kodira geografsku širinu i dužinu u score.

Na primer pri pretrazi prodavnica u blizini, Redis će na osnovu geografske širine i dužine centralne tačke izračunati mogući Geohash opseg, uradi opseg upit na ZSet-u, nakon što dobije kandidate, koristi Haversine formulu za precizno izračunavanje sferne distance, izabere konačne lokacije koje ispunjavaju uslove.
public class NearbyShopService {
private Jedis jedis;
private static final String SHOP_KEY = "shops:geo";
// dodaj prodavnicu
public void addShop(String shopId, double longitude, double latitude) {
jedis.geoadd(SHOP_KEY, longitude, latitude, shopId);
}
// pretrazi prodavnice u blizini
public List<GeoRadiusResponse> getNearbyShops(
double longitude,
double latitude,
double radiusKm) {
return jedis.georadius(SHOP_KEY,
longitude,
latitude,
radiusKm,
GeoUnit.KM,
GeoRadiusParam.geoRadiusParam()
.withCoord()
.withDist()
.sortAscending()
.count(20));
}
// izračunaj distancu između dve prodavnice
public double getShopDistance(String shop1Id, String shop2Id) {
return jedis.geodist(SHOP_KEY,
shop1Id,
shop2Id,
GeoUnit.KILOMETERS);
}
}Zašto koristiti heš tip umesto string tipa serijskog čuvanja?
Heš može da čita ili menja samo jedno polje, dok string zahteva da se odjednom izvuče ceo objekat.

Na primer ako postoji korisnički objekat user = {name: 'Chenmo Wang Er', age: 18}, ako se koristi heš za čuvanje, može se direktno izmeniti age polje:
redis.hset("user:1", "age", 19);Ako se koristi string za čuvanje, prvo treba izvući ceo objekat, izmeniti ga i vratiti nazad:
String userJson = redis.get("user:1");
User user = JSON.parseObject(userJson, User.class);
user.setAge(19);
redis.set("user:1", JSON.toJSONString(user));
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak prvog intervjua ByteDance komercijalnog tima: kaži Redis zset, šta je skok- lista, koliko slojeva indeksa treba konstruisati pri umetanju jednog čvora
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 9. kandidata ByteDance Feishu backend tehničkog prvog intervjua: tipovi podataka Redis-a, ZSet implementacija
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak E kandidata Xiaomi letnje stažiranje prvog intervjua: koliko razumete Redis, kaži česte strukture podataka i scenarije primene
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 23. kandidata Tencent QQ backend tehničkog prvog intervjua: tipovi podataka Redis-a
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 7. kandidata Kuaishou Java backend tehničkog prvog intervjua: kaži uobičajene strukture podataka Redis-a
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 7. intervjua Agricultural Bank of China Java backend: osnovni znanja o Redis-u
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 11. kandidata Huawei intervju: u projektu se koristi redis, koje tipove podataka ima redis? koji se scenariji koriste? zašto koristiti heš tip umesto string tipa serijskog čuvanja?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 1. kandidata OPPO intervju: uobičajene strukture podataka Redis-a
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 9. kandidata Meituan prvog intervjua: tipovi struktura podataka redis-a?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 22. kandidata Alibaba Cloud intervju: scenariji primene naprednih struktura podataka redis-a
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 29. kandidata Tencent Java backend prvog intervjua: koji je princip Redis-a garantovanja atomičnosti incr komande?
memo: 29. aprila 2025. izmenjeno do ovde, danas jedan prijatelj mi je poslao poruku kaže da je dobio offer od Amazon-a, plata je takođe vrlo visoka, pita me treba li da prihvati? Stvarno čestitam 🎉.

4.🌟Zašto je Redis brz?
Prvo, svi podaci Redis-a se nalaze u memoriji, a brzina čitanja i pisanja memorije je sama po sebi nekoliko redova veličine brža od diska.

Drugo, Redis koristi model pogona događaja zasnovan na IO multipleksing tehnologiji za rukovanje zahtevima klijenata i izvršavanje Redis komandi.

IO multipleksing tehnologija omogućava da u jednom thread-u istovremeno sluša hiljade klijentskih veza, rešavajući performansne troškove tradicionalnog IO modela gde svaka veza zahteva nezavistan thread.

IO multipleksing kontinuirano sluša zahteve, zatim stavlja spremne zahteve u red, i uređeno ih prenosi dileri fajl događaja, konačno ih procesira procesor događaja izvršavajući odgovarajuće accept, read i write zahteve.

Redis bira optimalnu IO multipleksing tehnologiju na osnovu operativnog sistema, na primer Linux koristi epoll, macOS koristi kqueue itd.
// kreiranje i korišćenje epoll-a
int epfd = epoll_create(1024); // kreiranje epoll instance
struct epoll_event ev, events[MAX_EVENTS];
// dodavanje događaja praćenja
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_sock, &ev);
// čekanje na događaj
while (1) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
// obrada spremnih fajl deskriptora
}
}Pre Redis 6.0, uključujući uspostavljanje veze, čitanje zahteva, slanje odgovora, i izvršavanje komandi sve se izvršavalo sekvencijalno u glavnom thread-u, time se izbegava takmičenje brave i promena konteksta u okviru više threadova, jer su većina operacija Redis-a u memoriji, usko grlo je uglavnom memorijska operacija i mrežna komunikacija, a ne CPU.

Da bi se dodatno rešilo performansko usko grlo mrežnog IO-a, Redis 6.0 uvde mehanizam više threadova, razdvaja mrežni IO i izvršavanje komandi, mrežni IO prepusta thread pool-u za obradu, dok se izvršavanje komandi i dalje dešava u glavnom thread-u, time se može u potpunosti iskoristiti performansa više-jezgrenih CPU-a.

Glavni thread se fokusira na izvršavanje komandi, mrežni IO dele ostali threadovi, u okruženju više-jezgrenog CPU-a, performanse Redis-a se mogu značajno poboljšati.

Treće, Redis je učinio ekstremnu optimizaciju podložnih struktura podataka, na primer string podložna struktura dinamički string podržava dinamičko proširenje, predodelu suvišnog prostora, može smanjiti memorijske fragmentacije i troškove alokacije memorije.

Sumarno:

- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak prvog intervjua Tencent Java backend stažiranja: zašto Redis ima visoku performansu čitanja i pisanja?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak K kandidata Xiaomi letnje regrutacije prvog intervjua: zašto je redis brz, strategija istiskivanja perzistencija
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 1. kandidata ByteDance Java backend tehničkog prvog intervjua: zašto je single-thread Redis tako brz?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 1. kandidata WeBank Java backend prvog intervjua: zašto je Redis tako brz?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 1. kandidata Baidu Erxin Yiyin 25 stažiranje Java backend: gde u projektu se koristi redis keš, zašto je redis brz?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 8. kandidata Dewu prvog intervjua: zašto je Redis brz
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 21. kandidata ByteDance TikTok Mall prvog intervjua: zašto redis može da radi visoku konkurentnost
memo: 30. aprila 2025. izmenjeno do ovde, danas jedan prijatelj mi je poslao poruku kaže da je dobio stažiranj offer od Didi-a, stvarno čestitam 🎉.

5. Možeš li detaljno da kažeš IO multipleksing?
IO multipleksing je tehnologija koja dozvolja jednom procesu da istovremeno nadgleda više fajl deskriptora, omogućavajući programu da efikasno obrađuje više konkurentnih veza bez kreiranja velikog broja threadova.

Glavna ideja IO multipleksinga: dozvoliti jednom thread-u da čeka više fajl deskriptora da budu spremni, zatim da radi operaciju nad spremnim deskriptorima. Na ovaj način se mogu obraditi konkurentne veze bez korišćenja više threadova ili više procesa.

Glavni mehanizmi implementacije uključuju select, poll, epoll, kqueue i IOCP itd.
Kaži razlike između select, poll, epoll, kqueue i IOCP?
Mana select-a je što jedan proces može nadgledati limitiran broj fajl deskriptora, obično 1024, i svaki poziv zahteva kopiranje skupa fajl deskriptora iz korisničkog režima u kernel režim, zatim iterira kroz njih da pronađe spremne, performanse su loše.
// osnovno korišćenje select-a
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
// primer koda
fd_set readfds;
FD_ZERO(&readfds); // isprazni skup
FD_SET(sockfd, &readfds); // dodaj socket za praćenje
select(sockfd + 1, &readfds, NULL, NULL, NULL);
if (FD_ISSET(sockfd, &readfds)) { // proveri da li je spreman
// obradi događaj čitanja
}Prednost poll-a je što nema ograničenja broja fajl deskriptora, ali svaki poziv i dalje zahteva kopiranje skupa fajl deskriptora iz korisničkog režima u kernel režim, i dalje zahteva iteriranje, performanse su i dalje loše.
// osnovno korišćenje poll-a
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
// primer koda
struct pollfd fds[MAX_EVENTS];
fds[0].fd = sockfd;
fds[0].events = POLLIN; // prati događaj čitanja
poll(fds, 1, -1);
if (fds[0].revents & POLLIN) {
// obradi događaj čitanja
}epoll je Linux-specifičan mehanizam IO multipleksinga, podržava masovne konkurentne veze, koristi model pogona događaja, performanse su više. Njegov princip rada je registrovanje fajl deskriptora u kernelu, zatim kroz mehanizam obaveštenja o događaju obrađuje spremne fajl deskriptore, ne zahteva polling, ne zahteva kopiranje podataka, nema ograničenja broja, zato su performanse veoma visoke.
// osnovno korišćenje epoll-a
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
// primer koda
int epfd = epoll_create(1);
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN;
ev.data.fd = sockfd;
epoll_ctl(epfd, EPOLL_CTL_ADD, sockfd, &ev);
while (1) {
int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);
for (int i = 0; i < nfds; i++) {
if (events[i].data.fd == sockfd) {
// obradi događaj čitanja
}
}
}kqueue je mehanizam IO multipleksinga BSD/macOS sistema, sličan epoll-u, podržava masovne konkurentne veze, koristi model pogona događaja.
int kqueue(void);
int kevent(int kq, const struct kevent *changelist, int nchanges, struct kevent *eventlist, int nevents, const struct timespec *timeout);IOCP je mehanizam IO multipleksinga Windows sistema, koristi model completion port-a umesto obaveštenja o događaju.
HANDLE CreateIoCompletionPort(HANDLE FileHandle, HANDLE ExistingCompletionPort, ULONG_PTR CompletionKey, DWORD NumberOfConcurrentThreads);Daj primer za IO multipleksing?
Na primer, ja sam profesorka matematike, na satu sam postavila pitanje: „Ko danas će da dokaže Pitagorinu teoremu?“
Učenik mali Wang je digao ruku, ja sam dala mali Wang da odgovori; mali Li je digao ruku, ja sam dala mali Li da odgovori; mali Zhang je digao ruku, ja sam dala mali Zhang da odgovori.
Ovaj model je IO multipleksing, ja samo treba da čekam na podijumu, ko diže ruku taj odgovara, ne treba da ih jedan po jedan pitam.

Redis koristi epoll ovakav mehanizam IO multipleksinga, u modelu jednog thread-a realizuje efikasan mrežni IO, time podržava obradu visokih konkurentnih zahteva.
Daj primer razlike između blokirajućeg IO-a i IO multipleksinga?
Pretpostavimo da sam profesorka, dala sam učenicima da reše jedan zadatak.
Moj prvi izbor: redom proveravam svakog učenika, prvo proverim A učenika, zatim B, posle C, D... ako se jedan učenik zaglavi, ceo razred će biti zakašnjen.
Ovo je blokirajući IO, nema konkurentne sposobnosti.

Moj drugi izbor, ja stojim na podijumu i čekam, ko diže ruku tomu idem da proverim. C, D dižu ruku, ja idem da proverim C, D odgovore, zatim se vraćam na podijum i čekam. U ovom trenutku E, A opet dižu ruku, zatim idem da obradim E i A.
Koji su principi implementacije select, poll i epoll-a?
Select i poll prolaze kroz sve fajl deskriptore i prosleđuju ih kernelu, kernel iterira i procenjuje koji su spremni.
Select šalje fajl deskriptore FD kroz BitsMap u kernel, poling sve FD, pozivanjem file->poll funkcije proverava da li ima odgovarajući događaj, ako nema dodaje task u FD odgovarajuću file listu čekanja na buđenje, čeka da događaj stigne i bude buđen.

Poll je rešio problem gornje granice broja veza, više ne koristi BitsMap za prosleđivanje FD, zamenjeno je dinamički niz pollfd, ali suštinski je i dalje linearna iteracija, performanse se nisu mnogo poboljšale.

Model select-a i poll-a je jednom kopirati parametre u kernel prostor, kad ima rezultata opet kopirati nazad.
Epoll registruje praćene FD u crveno-crno stablo kernela, kernel pri okidanju događaja stavlja spremne FD u ready listu. Aplikacija kroz epoll_wait dobija spremne FD, time izbegava troškove iteracije kroz sve veze.

Najveća prednost epoll-a: podržava pogon događaja + ivično okidanje, kod ADD se kopira jednom, kod epoll_wait koristi MMAP i deli prostor sa korisnikom, direktno kopira podatke u korisnički prostor, zato u scenariju visoke konkurentnosti performanse su daleko više od select-a i poll-a.
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 21. kandidata ByteDance TikTok Mall prvog intervjua: da li razumete io multipleksing?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 4. kandidata Kuaishou prvog intervjua: koji su principi implementacije i razlike select/poll/epoll u IO multipleksingu?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 19. kandidata ByteDance Tomato Novel prvog intervjua: IO multipleksing u Linux-u
memo: 1. maja 2025. izmenjeno do ovde, danas dok sam pomagao prijatelju da promeni životopis, sreo sam jednog studenta Beijing Jiaotong University-a, još jedan 211 univerzitet, planina stvarno ima mnogo talenata, svi se zajedno trudio (ponos).

6. Zašto je Redis ranije birao single thread?
Prvo, single thread model ne treba da razmišlja o kompleksnom mehanizmu brave, ne postoji problem mrtvog zaključavanja, uslovi trke u okviru više threadova, razvoj je brži, i lakše se održava.

Drugo, Redis je IO intenzivan a ne CPU intenzivan, uglavnom je ograničen memorijom i mrežnim IO-om, a ne CPU računarskom moći, single thread može izbeći troškove promene konteksta thread-a.
Čak i kada na običnom Linux serveru pokrenemo Redis servis, može da obradi 1.000.000 korisničkih zahteva za 1s.
Treće, single thread može garantovati atomičnost izvršavanja komandi, bez potrebe za dodatnim mehanizmima sinhronizacije.

Iako je Redis prvobitno usvojio single thread dizajn, kasnije verzijama su u specifičnim aspektima uvele više threadova, na primer Redis 4.0 je uveo asinhroni više threadove, za čišćenje prljavih podataka, oslobađanje nekoristenih veza, brisanje velikih ključeva itd.
/* Obriši ključ, vrednost i povezane stavke isteka (ako postoje) iz baze podataka.
* Ako oslobađanje objekta vrednosti zahteva puno operacija alokacije memorije,
* objekat može biti stavljen u listu odloženog oslobađanja, umesto sinhronog
* oslobađanja. Lista odloženog oslobađanja će se kasnije reciklirati u drugom threadu
* bio.c. */
#define LAZYFREE_THRESHOLD 64
int dbAsyncDelete(redisDb *db, robj *key) {
/* Brisanje stavki iz rečnika isteka ne oslobađa ključevi sds,
* jer deli sa glavnim rečnikom. */
if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);
/* Ako objekat vrednosti sadrži samo malu količinu alocirane memorije,
* korišćenje odloženog oslobađanja će zapravo biti sporije...
* Zato ispod određenog praga, direktno sinhrono oslobađamo objekat. */
dictEntry *de = dictUnlink(db->dict,key->ptr);
if (de) {
robj *val = dictGetVal(de);
// izračunaj dobit od oslobađanja vrednosti
size_t free_effort = lazyfreeGetFreeEffort(val);
/* Ako je posao oslobađanja objekta prevelik, dodaj objekat u listu
* odloženog oslobađanja za pozadinsku obradu.
* Napomena: ako se objekat deli, sada ga je nemoguće osloboditi. Ova situacija
* se retko dešava, ali ponekad neki delovi implementacije Redis core-a mogu
* pozvati incrRefCount() da zaštite objekat, zatim pozvati dbDelete(). U
* ovom slučaju, nastavićemo da izvršavamo i stićemo do dictFreeUnlinkedEntry()
* poziva, što je ekvivalentno samo pozivanju decrRefCount(). */
// samo kada je dobit od oslobađanja veća od određene vrednosti, izvršiće se asinhrono brisanje,
// inače će se degradirati na sinhrono brisanje
if (free_effort > LAZYFREE_THRESHOLD && val->refcount == 1) {
atomicIncr(lazyfree_objects,1);
bioCreateBackgroundJob(BIO_LAZY_FREE,val,NULL,NULL);
dictSetVal(db->dict,de,NULL);
}
}
/* Oslobodi ključ-vrednosni par, ako smo val polje postavili na NULL da bi
* kasnije odloženo oslobodili, onda oslobodi samo ključ. */
if (de) {
dictFreeUnlinkedEntry(db->dict,de);
if (server.cluster_enabled) slotToKeyDel(key->ptr);
return 1;
} else {
return 0;
}
}Zvanično objašnjenje: https://redis.io/topics/faq
memo: 2. maja 2025. izmenjeno do ovde, danas dok sam pomagao prijatelju da promeni životopis, sreo sam jednog studenta Tongji University-a, osećam da se moj rad sve više viđa od više ljudi, stvarno sam veoma srećan.

7. O čemu je reč kada Redis 6.0 koristi više threadova?
Više threadova Redis 6.0 se koriste samo za obradu mrežnog IO-a, uključujući čitanje i pisanje mrežnih podataka, i analizu zahteva.
│ Izvršenje komandi u jednom threadu │
│ ↑ ↓ │
┌─────────┐ ┌─┴────────────┴──┐
│ I/O nit 1 │ ←→ │ │
├─────────┤ │ │
│ I/O nit 2 │ ←→ │ Glavni nit │
├─────────┤ │ │
│ I/O nit 3 │ ←→ │ │
└─────────┘ └─────────────────┘Izvršavanje komandi je i dalje single thread, ovaj dizajn se zove „IO thread-ing“, može maksimalno poboljšati brzinu odgovora Redis-a u visokom opterećenju.

---- Ovaj deo se ne mora učiti na intervjuu, olakšava razumevanje start ----
Ova promena je uglavnom zato što se sa poboljšanjem mrežnog propusnog opsega i performansi servera, usko grlo Redis-a postepeno prelazi sa CPU-a na mrežni IO:
- Propusni opseg se poboljšao sa 10Gbps na 100Gbps, čak i više.
- Broj konkurentnih zahteva se popeo sa nekoliko hiljada na desetine hiljada, čak stotine hiljada.
Single thread u scenariju visokog opterećenja se pojavio jasno performansko usko grlo u obradi mrežnog IO-a, tim razvojnih Redis-a kroz istraživanje otkrio da pri obradi velikih paketa, više od 80% CPU vremena single thread Redis-a troši na mrežni IO, a stvarno izvršavanje komandi zauzima samo oko 20%.

Mrežni model više threadova Redis 6.0 uglavnom sadrži tri ključne korake:
- I dalje glavni nit prima zahteve za povezivanje klijenata.
- Glavni nit distribuira zahteve povezivanja više IO niti za obradu, glavni nit je zadužena za analizu i izvršavanje komandi.
- Nakon završetka izvršavanja komandi, više IO niti vraća rezultate klijentu.
// Glavna petlja događaja Redis-a (uprošćena verzija)
void beforeSleep(struct aeEventLoop *eventLoop) {
// 1. glavni nit dodeljuje čitanje IO nitovima
handleClientsWithPendingReadsUsingThreads();
// 2. čeka da IO niti završe čitanje
waitForIOThreads();
// 3. glavni nit obrađuje komande
processInputBuffer();
// 4. glavni nit dodeljuje pisanje IO nitovima
handleClientsWithPendingWritesUsingThreads();
}Redis 6.0 podrazumevano i dalje koristi single thread režim, ali može se uključiti multi-thread režim kroz konfiguracionu datoteku ili parametre komandne linije.
# uključi multi-thread režim
io-threads 4
# uključi multi-thread pisanje (Redis 6.0 podrazumevano uključuje samo multi-thread čitanje)
io-threads-do-reads yesPreporučuje se postaviti broj IO niti na polovinu broja CPU jezgri, obično ne preporučuje se više od 8.
Nakon više testiranja, Redis 6.0 pri obradi malih paketa od 1-200 bajtova, performanse se poboljšavaju 1.5-2 puta; pri obradi velikih paketa od preko 1KB, poboljšanje je oko 3-5 puta.
----ovaj deo se ne mora učiti na intervjuu, olakšava razumevanje end ----
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 30. kandidata Tencent Music intervju: gde se koriste više threadovi uvedeni u redis6.0
8. Kaži uobičajene komande Redis-a (dopuna)
- aprila 2024. dopunjeno
Odgovor jednom rečenicom (takođe ne moraš sve učiti, izaberi tri):
Redis podržava više struktura podataka, uobičajenih komandi je takođe dosta, na primer za operisanje string-om možeš koristiti SET/GET/INCR, za heš HSET/HGET/HGETALL, za listu LPUSH/LPOP/LRANGE, za set SADD/SISMEMBER, za uređeni set ZADD/ZRANGE/ZINCRBY itd., opšte komande uključuju EXPIRE/DEL/KEYS itd.
----ovaj deo se ne mora učiti na intervjuu, olakšava razumevanje start----
①, komande za operisanje string-om su:
| Komanda | Uloga | Primer |
|---|---|---|
SET key value | Podesi string ključ-vrednost | SET name jack |
GET key | Nabavi string vrednost | GET name |
INCR key | Broj se povećava za 1 | INCR count |
DECR key | Broj se smanjuje za 1 | DECR stock |
INCRBY key N | Povećaj za N | INCRBY views 10 |
APPEND key value | Dodaj string | APPEND log "done" |
GETRANGE key start end | Nabavi podstring | GETRANGE name 0 3 |
MSET k1 v1 k2 v2 | Podesi više ključ-vrednosti batch | MSET a 1 b 2 |
②, komande za operisanje liste su:
LPUSH key value: Umeće vrednost na početak liste ključa.RPUSH key value: Umeće vrednost na kraj liste ključa.LPOP key: Uklanja i vraća prvi element liste ključa.RPOP key: Uklanja i vraća poslednji element liste ključa.LRANGE key start stop: Nabavlja elemente u određenom opsegu liste ključa.
③, komande za operisanje set-a su:
SADD key member: Dodaje element u set ključa.SREM key member: Uklanja element iz seta ključa.SMEMBERS key: Vraća sve elemente u setu ključa.
④, komande za operisanje uređenog set-a su:
ZADD key score member: Dodaje člana u uređeni set ključa ili ažurira njegov poen.ZRANGE key start stop [WITHSCORES]: Vraća članove uređenog seta ključa u opsegu indeksa, opciono WITHSCORES parametar vraća poene.ZREVRANGE key start stop [WITHSCORES]: Vraća članove uređenog seta ključa u određenom opsegu, sortirano po opadajućim poenima.ZREM key member: Uklanja jednog ili više članova iz uređenog seta ključa.
⑤, komande za operisanje heša su:
HSET key field value: Postavlja vrednost polja field u heš tabeli ključa na vrednost.HGET key field: Nabavlja vrednost polja field u heš tabeli ključa.HGETALL key: Nabavlja sva polja i vrednosti u heš tabeli ključa.HDEL key field: Briše jedno ili više polja iz heš tabele ključa.
Detaljno kaži za set komandu?
SET komanda se koristi za postavljanje string ključa, podržava vreme isteka i uslovno pisanje, često se koristi za postavljanje keša, realizaciju distribuirane brave, produženje Sessiona itd.
SET key value [EX seconds | PX milliseconds | EXAT timestamp | PXAT timestamp-milliseconds | KEEPTTL] [NX | XX] [GET]Podrazumevano, SET će prebrisati postojeću vrednost ključa.
Podržava više načina postavljanja vremena isteka, na primer EX postavlja vreme isteka u sekundama, PX postavlja vreme isteka u milisekundama.
Podržava uslovno pisanje, može realizovati atomične operacije, na primer NX samo postavlja vrednost ako ključ ne postoji, XX samo postavlja vrednost ako ključ postoji.

Realizacija keša:
SET user:profile:{userid} {JSON podaci} EX 3600 # čuvaj korisnički profil, i postavi 1 sat istekaRealizacija distribuirane brave:
SET lock:resource_name {random_vrednost} EX 10 NX # uzmi bravu, automatski se oslobađa nakon 10 sekundiČuvanje Session-a:
SET session:{sessionid} {session_podaci} EX 1800 # čuvaj korisničku sesiju, 30 minuta istekaKoja je vremenska kompleksnost sadd komande?
SADD podržava dodavanje više elemenata odjednom, povratna vrednost je stvarni broj uspešno dodatih elemenata, vremenska kompleksnost je O(N).
redis-cli SADD myset "apple" "banana" "orange"Da li razumete incr komandu?
INCR je atomična komanda, može povećati vrednost navedenog ključa za 1, ako ključ ne postoji, prvo će ga postaviti na 0, zatim izvršiti operaciju povećanja za 1.

Često se koristi za realizaciju brojača poseta veb sajtu, broja lajkova na članku itd.; kombinacijom sa vremenom isteka realizuje ograničavač brzine; generisanje distribuiranih jedinstvenih ID-ova; smanjenje zaliha itd.
# ograniči korisnika na maksimalno 10 poseta u minuti
FUNCTION limit_api_call(user_id)
current = INCR("rate:"+user_id)
IF current == 1 THEN
EXPIRE("rate:"+user_id, 60)
END
IF current > 10 THEN
RETURN false # prekoračeno ograničenje
ELSE
RETURN true # dozvoli pristup
END
END
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 1. kandidata JD tehničkog prvog intervjua: kaži uobičajene komande Redis-a
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 3. kandidata Agricultural Bank of China Java backend intervjua: toliko dobro kažeš, koja je funkcija za postavljanje key value Redis-a
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 1. kandidata Kuaishou glavne stanice tehničkog odeljenja prvog intervjua: koja je vremenska kompleksnost Redis sadd komande?
memo: 3. maja 2025. izmenjeno do ovde, danas jedan prijatelj mi je poslao poruku kaže da je dobio offer za softversko razvojno stažiranje od Midea-a, iako sam nije zadovoljan, ali privremeno nema drugih boljih, predložio sam mu da prvo proba 🎉.

9. Koliko QPS može da dostigne single thread Redis? (dopuna)
- aprila 2024. dopunjeno
Prema zvaničnom benchmark testu, običan server Redis instance obično može dostići oko sto hiljada QPS po sekundi.

----ovaj deo se ne mora učiti na intervjuu, olakšava razumevanje start ----
Performanse QPS (broj zahteva po sekundi) Redis-a zavise od više faktora, uključujući hardversku konfiguraciju, mrežno kašnjenje, strukturu podataka, tip komande itd.
Može se izvršiti benchmark test kroz redis-benchmark komandu:
redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 10000-h: specificira adresu Redis servera, podrazumevano je 127.0.0.1.-p: specificira port Redis servera, podrazumevano je 6379.-c: broj konkurentnih veza, odnosno koliko klijenata istovremeno vrši test.-n: ukupan broj zahteva, odnosno koliko ukupno zahteva treba izvršiti tokom testa.
Pre 2023. godine, koristio sam macOS, 4 GHz četvorojezgreni Intel Core i7, 32 GB 1867 MHz DDR3, rezultati testa su sledeći:

Može se videti, može obraditi više od 100.000 zahteva po sekundi.
QPS = ukupan broj zahteva / ukupno vreme = 10000 / 0.09 ≈ 111111 QPSKašnjenje je takođe veoma nisko, 99% zahteva je završeno u roku od 0.3ms.
----ovaj deo se ne mora učiti na intervjuu, olakšava razumevanje end ----
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 1. kandidata ByteDance Java backend tehničkog prvog intervjua: koliki je QPS single thread Redis-a?
Perzistencija
10.🌟Koje su načini perzistencije Redis-a?
Postoje dva glavna, RDB i AOF. RDB ostvaruje perzistenciju kroz kreiranje snapshot-a u određenom trenutku, AOF kroz snimanje svake komande pisanja.

Ova dva načina se mogu koristiti zasebno ili istovremeno. Na ovaj način može se garantovati da nakon restarta Redis servera neće izgubiti podatke, kroz RDB i AOF datoteke za vraćanje originalnih podataka u memoriju.

Detaljno kaži za RDB?
Mehanizam RDB perzistencije može u određenom vremenskom intervalu sačuvati podatke Redis-a u određenom trenutku u RDB datoteku na disku, kada se Redis restartuje, može vratiti podatke učitavanjem ove RDB datoteke.

RDB perzistencija može biti ručno okinuta kroz save i bgsave komande, ili automatski kroz save instrukciju u konfiguracionoj datoteci.

save komanda će blokirati Redis proces, dok se RDB datoteka ne kreira.

bgsave komanda će u pozadini fork-ovati podproces za izvršavanje RDB perzistencije, glavni proces neće biti blokiran.

U kojim uslovima će se automatski okiniti RDB perzistencija?
Prvi, u Redis konfiguracionoj datoteci postaviti parametre RDB perzistencije save <seconds> <changes>, znači da u određenom vremenskom intervalu, ako se određeni broj ključeva promeni, automatski će se okiniti RDB perzistencija.
save 900 1 # 900 sekundi (15 minuta) ima 1 ključ koji se menja, okida snapshot
save 300 10 # 300 sekundi (5 minuta) ima 10 ključeva koji se menjaju, okida snapshot
save 60 10000 # 60 sekundi ima 10000 ključeva koji se menjaju, okida snapshotDrugi, pri replikaciji glavni-potčinjeni, kada se potčinjeni čvor prvi put poveže na glavni čvor, glavni čvor automatski izvršava bgsave generiše RDB datoteku, i šalje je potčinjenom čvoru.

Treći, ako nije uključena AOF, pri izvršavanju shutdown komande, Redis automatski čuva jednom RDB datoteku, kako bi se osiguralo da se podaci neće izgubiti.
Detaljno kaži za AOF?
AOF ostvaruje perzistenciju snimanjem svake komande pisanja i njihovim dodavanjem u AOF datoteku, nakon što Redis server padne, može vratiti podatke ponovnim izvršavanjem ovih komandi.

Kada Redis izvrši operaciju pisanja, dodaje komandu pisanja u AOF bafer; Redis će prema strategiji sinhronizacije upisivati podatke iz bafera u AOF datoteku.

Kada AOF datoteka postane prevelika, Redis automatski vrši AOF prepisivanje, uklanja suvišne komande, na primer višestruko set i del istog ključa, generiše novu AOF datoteku; kada se Redis restartuje, čita komande iz AOF datoteke i ponovo ih izvršava, da vrati podatke.
Da li razumete strategiju upisivanja AOF-a na disk?
Kada Redis upisuje podatke iz AOF bafera u AOF datoteku, uključuje dva sistemska poziva: write upisuje podatke u bafer operativnog sistema, fsync osvežava podatke iz bafera OS-a na disk.
Ovde se upisivanje na disk odnosi na tri strategije: always, everysec i no.

- always: odmah nakon izvršavanja komande pisanja poziva fsync sinhronizuje na disk, time može garantovati da se podaci ne gube, ali performanse su loše.
- everysec: svake sekunde poziva jednom fsync, odjednom sinhronizuje više komandi na disk, performanse su dobre, vreme gubitka podataka je 1 sekunda.
- no: ne poziva fsync aktivno, odlučuje operativni sistem, performanse su najbolje, ali vreme gubitka podataka je neizvesno, zavisi od strategije keša operativnog sistema, može izgubiti mnogo podataka.
Može se podesiti kroz appendfsync parametar u konfiguracionoj datoteci.
appendfsync everysec # svake sekunde fsync jednomKaži mehanizam prepisivanja AOF-a?
Kako će AOF datoteka kontinuirano rasti sa povećanjem operacija pisanja, kako bi se rešio ovaj problem, Redis pruža mehanizam prepisivanja za kompresiju i optimizaciju AOF datoteke.

AOF prepisivanje može biti okinuto na dva načina, prvi je ručno izvršiti BGREWRITEAOF komandu, pogodno za scenarije gde je potrebno odmah smanjiti veličinu AOF datoteke.
Drugi je u Redis konfiguracionoj datoteci postaviti parametre automatskog prepisivanja, na primer auto-aof-rewrite-percentage i auto-aof-rewrite-min-size, znači da kada AOF datoteka premaši određenu veličinu, automatski se okida prepisivanje.
auto-aof-rewrite-percentage 100 # podrazumevana vrednost 100, znači koliko procenata trenutna AOF datoteka naraste u poređenju sa veličinom nakon poslednjeg prepisivanja, okida prepisivanje
auto-aof-rewrite-min-size 64mb # podrazumevana vrednost 64MB, znači minimalna veličina AOF datoteke da bi se razmatralo prepisivanjeKoji je konkretan proces AOF prepisivanja?
Nakon što primi instrukciju prepisivanja, Redis kreira podproces, fork-uje kopiju podataka potpuno istu kao glavni proces, zatim iterira kroz sve parove ključ-vrednost u memoriji, generiše najmanji broj komandi potrebnih za njihovu rekonstrukciju.

Na primer više RPUSH komandi može se spojiti u jednu RPUSH sa više parametara;
Na primer ako se ključ postavi pa zatim obriše, sve operacije nad ovim ključem neće biti upisane u novi AOF.
Na primer koristiti SADD key member1 member2 member3 umesto više zasebnih SADD key memberX.
Dok podproces vrši AOF prepisivanje, glavni proces može nastaviti da obrađuje komande od klijenata.
Kako bi se osigurala konzistentnost podataka, Redis koristi mehanizam bafera AOF prepisivanja, glavni proces pri izvršavanju operacija pisanja simultano upisuje komande u staru AOF datoteku i bafer prepisivanja.
Nakon što podproces završi prepisivanje, šalje signal glavnom procesu, glavni proces nakon prijema dodaje komande iz bafera prepisivanja na kraj nove AOF datoteke, zatim poziva operativni sistem rename, zamenjuje staru AOF datoteku novom AOF datotekom.
Glavni proces (fork)
│
├─→ Podproces (generiše novu AOF datoteku)
│ │
│ ├─→ Memorijski snapshot
│ ├─→ Upis u privremenu AOF datoteku
│ ├─→ Obaveštava glavni proces o završetku
│
├─→ Glavni proces (dodaje bafer u novu AOF datoteku)
├─→ Zamenjuje staru AOF datoteku
├─→ Prepisivanje završenoTokom prepisivanja AOF-a, Redis server će biti u specifičnom stanju:
- aof_child_pid nije 0, znači da postoji podproces koji vrši AOF prepisivanje
- aof_rewrite_buf_blocks lista nije prazna, čuva sadržaj bafera AOF prepisivanja
Ako se u konfiguracionoj datoteci postavi no-appendfsync-on-rewrite na yes, tokom prepisivanja se može privremeno pauzirati fsync operacija AOF datoteke.
appendonly yes # uključi AOF
appendfilename "appendonly.aof" # naziv AOF datoteke
appendfsync everysec # strategija upisivanja na disk
no-appendfsync-on-rewrite no # da li se privremeno isključuje fsync tokom prepisivanja
auto-aof-rewrite-percentage 100 # koliko procenata raste AOF datoteka od poslednjeg prepisivanja da bi se okidalo prepisivanje
auto-aof-rewrite-min-size 64mb # minimalna veličina AOF datoteke da bi se dozvolilo prepisivanjeKoju tip podatka čuva AOF datoteka?
AOF datoteka čuva podatke komandi pisanja koje je Redis server primio, sačuvane u formatu Redis protokola.
Karakteristika ovog formata je da svaka komanda počinje sa *, zatim sledi broj parametara, pre svakog parametra stoji $ znak, zatim sledi dužina parametra u bajtovima, zatim se stvarni sadržaj parametra.

Komanda može biti upisana dva puta tokom AOF prepisivanja, kakav uticaj to ima?
Tokom AOF prepisivanja komanda se istovremeno upisuje u postojeću AOF datoteku i bafer prepisivanja, ovaj mehanizam je namerno dizajniran, neće dovesti do problema dupliranja ili nekonzistentnosti podataka.

Zato što su nova i stara datoteka odvojene, postojeće komande se upisuju u trenutnu AOF datoteku, komande iz bafera prepisivanja se konačno upisuju u novu AOF datoteku, nakon završetka, nova datoteka zamenjuje staru kroz atomičnu rename operaciju. Dve datoteke su potpuno odvojene, neće dovesti do dupliranja komandi u istoj AOF datoteci.
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak K kandidata Xiaomi letnje regrutacije prvog intervjua: zašto je redis brz, strategija istiskivanja perzistencija
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 7. kandidata Kuaishou Java backend tehničkog prvog intervjua: kaži načine perzistencije Redis-a
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 1. kandidata male kompanije intervjua zbirke Java backend: koji su načini perzistencije Redis-a? Koje su razlike između RDB-a i AOF-a? Koji se Redis brže oporavlja nakon pada?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 18. kandidata Meituan intervju Chengdu dođe kući: redis perzistencija
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 1. kandidata Zuoyebang Java backend prvog intervjua: mehanizam redis perzistencije
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 1. kandidata OPPO intervju: rešenje Redis perzistencije
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 9. kandidata Dewu intervju: koji su osnovni tipovi podataka Redis-a? Koja je perzistencija Redis-a? Koje su prednosti i mane?
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 3. kandidata Didi vozač mrežni backend razvoj prvog intervjua: redis perzistencija
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 1. kandidata Kuaishou glavne stanice tehničkog odeljenja prvog intervjua: kako se garantuje pouzdanost Redis podataka? Komanda se može upisati dva puta tokom AOF prepisivanja, kakav uticaj to ima?
memo: 4. maja 2025. izmenjeno do ovde, danas jedan prijatelj mi je poslao poruku kaže da je odštampano PDF izdanje perzistencije konkurentnog programiranja i JVM preokreta intervjua, iskreno, volim i izgled ovog omota, haha.

11. Koje su prednosti i mane RDB-a i AOF-a?
RDB kroz fork podprocesa u specifičnom trenutku radi potpunu bekap memoriskih podataka, generiše snapshot datoteku u binarnom formatu. Najveća prednost je visoka efikasnost bekap-a i oporavka, datoteka je kompaktna, brzina oporavka je brza, pogodna za bekap i migraciju podataka velikih razmera.
Mana je mogućnost gubitka svih promena podataka između dva snapshot-a.

AOF beleži svaku komandu izmene podataka. Ovaj način dodavanja log-a omogućava AOF-u da pruži gotovo real-time bekap podataka, rizik od gubitka podataka može se kontrolisati u roku od 1 sekunde čak i potpuno izbeći.
Mana je što je veličina datoteke veća, brzina oporavka je sporija.
Uzporedimo tabelom:
| Poredak | RDB (snapshot) | AOF (log komandi) |
|---|---|---|
| Integritet podataka | ❌ Može izgubiti nekoliko minuta podataka | ✅ Najviše gubi 1 sekundu podataka |
| Brzina oporavka | ✅ Brza (direktno učitava binarni snapshot) | ❌ Spora (redom replay) |
| Veličina datoteke | ✅ Mala (nakon kompresije) | ❌ Velika (dodavanje komandi) |
| Performanse uticaja | ✅ Nisko (čuvanje nakon fork) | ❌ Više (svaki put se beleži svako pisanje) |
| Način pisanja | Periodično puno pisanje | Svaku komandu pisanja beleži |
| Scenarij primene | Hladni bekap, disaster recovery | Real-time perzistencija, sigurnost podataka |
| Podrazumevano stanje | Podrazumevano uključeno | Redis 7 podrazumevano takođe uključeno |
| Mehanizam prepisivanja | Nema | Ima (BGREWRITEAOF) |
| Podrška za hibridno | Posle Redis 4.0 podržava kombinovanu upotrebu (aof-use-rdb-preamble) |
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 1. kandidata male kompanije intervjua zbirke Java backend: koji su načini perzistencije Redis-a? Koje su razlike između RDB-a i AOF-a? Koji se Redis brže oporavlja nakon pada?
12. Kako izabrati između RDB-a i AOF-a?
Pri izboru rešenja Redis perzistencije, ja ću razmotriti sa dva dimenzija: poslovni zahtevi i tehničke karakteristike.
Ako je scenarij keširanja, može se prihvatiti određeni stepen gubitka podataka, ja ću naginjati ka izboru RDB ili uopšte ne koristiti perzistenciju. Snapshot način RDB-a ima mali uticaj na performanse, a brzina oporavka je brza, vrlo pogodan za ovakve scenarije.

Ali ako se radi o porudžbini ili plaćanju takvog ključnog poslovanja, gubitak podataka će imati ozbiljne posledice, tada AOF postaje neophodan izbor. Podešavanjem sinhronizacije jednom u sekundi, mogu se potencijalni rizici od gubitka podataka ograničiti na prihvatljiv nivo.

U stvarnom projektu, ja više naginjem ka korišćenju hibridnog režima RDB + AOF.
appendonly yes # uključi AOF
appendfsync everysec # jednom u sekundi fsync na disk
aof-use-rdb-preamble yes # uključi hibridnu perzistenciju, pri restitiranju prvo učitava RDB, RDB kao hladni bekap, AOF kao real-time sinhronizacija
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 18. kandidata Meituan intervju Chengdu dođe kući: kad koristiti rdb kad aof
13. Kako Redis vraća podatke?
Kada se Redis servis restartuje, prvo će tražiti AOF datoteku, ako postoji, kroz ponovno izvršavanje komandi u njoj će vratiti podatke; ako ne postoji ili nije uključena AOF, pokušaće da učita RDB datoteku, direktno učitava binarne podatke u memoriju za vraćanje.

Ako je AOF datoteka oštećena, Redis će pokušati da popravi AOF datoteku kroz redis-check-aof alat, ili direktno koristiti --repair parametar za popravak.
redis-check-aof --repair appendonly.aofIako Redis pruža redis-check-rdb alat za proveru integriteta RDB datoteke, ne podržava popravak RDB datoteke, samo može koristiti za verifikaciju integriteta datoteke.
redis-check-rdb dump.rdb
- Vodič za Java intervjue (plaćeno) uključuje originalni zadatak 4. kandidata Meituan prvog intervjua: kako rešiti gubitak podataka u Redis memoriji
memo: 5. maja 2025. izmenjeno do ovde, danas dok sam pomagao prijatelju da promeni životopis, sreo sam prijatelja magistrirao na HUST, 985 univerzitet +1, trenutno u Kini ima 39 univerziteta 985, nadam se da će uskoro moći prikupiti sve.😄

14.🌟 Da li razumete hibridnu perzistenciju Redis 4.0?
Da.
Hibridna perzistencija kombinuje prednosti RDB-a i AOF-a, rešava njihove nedostatke. Pre Redis 4.0, mi smo se suočili sa rizikom gubitka podataka RDB-a ili problemom sporog oporavka AOF-a, teško postići oboje.

Radni princip hibridne perzistencije je veoma domišljen: tokom AOF prepisivanja, prvo čuva memorijske podatke snapshot u formatu RDB na početak AOF datoteke, zatim dodaje komande tokom prepisivanja u formatu AOF na kraj datoteke.

Na ovaj način, kada treba vratiti podatke, Redis prvo učitava podatke u formatu RDB za brzo vraćanje većine podataka, zatim kroz ponovno izvršavanje komandi vraća najnovije podatke, time može garantovati integritet podataka dok poboljšava brzinu oporavka.
Kako podesiti režim perzistencije?
Uključivanje hibridne perzistencije je veoma jednostavno, samo u konfiguracionoj datoteci postaviti aof-use-rdb-preamble yes.
aof-use-rdb-preamble yesKako ste podesili RDB i AOF u razvoju?
Za većinu produkcionih okruženja, ja naginjem ka korišćenju hibridnog režima perzistencije, kombinujući prednosti RDB-a i AOF-a.
# uključi AOF
appendonly yes
# koristi hibridnu perzistenciju
aof-use-rdb-preamble yes
# jednom u sekundi sinhronizuj AOF, balansira performanse i sigurnost
appendfsync everysec
# uslovi AOF prepisivanja: datoteka raste 100% i barem doseže 64MB
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# RDB strategija bekap-a
save 900 1 # 15 minuta ima 1 izmenu
save 300 10 # 5 minuta ima 10 izmena
save 60 10000 # 1 minut ima 10000 izmenaZa čisto scenarij keširanja, ili lokalni razvoj, ja ću samo uključiti RDB, isključiti AOF:
# isključi AOF
appendonly no
# opuštenija RDB strategija
save 3600 1 # 1 sat ima 1 izmenu
save 300 100 # 5 minuta ima 100 izmenaZa finansijske i druge sisteme visokog stepena konzistentnosti, obično dinamiki postavljam appendfsync na always u kritičnim vremenskim prozorima:
# Omogući AOF
appendonly yes
# Koristi hibridnu perzistenciju
aof-use-rdb-preamble yes
# Sinhronizuj svaku naredbu (koristiti pažljivo, veliki uticaj na performanse)
# Obično ću dinamiki promeniti na always u kritičnim vremenskim prozorima
appendfsync always
# Češći RDB snimci
save 300 1 # 1 izmena u roku od 5 minuta
save 60 100 # 100 izmena u roku od 1 minutaDodatno, za scenarije visoke konkurentnosti, treba postaviti no-appendfsync-on-rewrite yes da se izbegne uticaj AOF prepisivanja na performanse glavnog procesa; za velike instance, takođe treba postaviti rdb-save-incremental-fsync yes da bi se smanjio uticaj čuvanja velikih RDB datoteka na performanse.
# Ne fsync-uj tokom AOF prepisivanja, tokom AOF prepisivanja, glavni proces neće izvršiti fsync operaciju nad AOF baferom novih zapisa (tj. ne prisiljava na upis na disk), već će jednom upisati sve na disk nakon završetka prepisivanja.
no-appendfsync-on-rewrite yes
# RDB snimci koriste inkrementalni fsync, tj. nakon upisa određene količine podataka izvrši se jedan fsync, kako bi se podaci sinhronizovali na disk u serijama.
rdb-save-incremental-fsync yes
- Pitanja sa intervjua iz ByteDance: Koji je mehanizam perzistencije Redis-a?
- Pitanja sa intervju-a iz malih kompanija: Koji method oporavka je brži kada Redis padne?
- Pitanja sa intervju-a iz Meituan: Kako podesiti režim perzistencije
- Pitanja sa prvog intervju-a iz Meituan: Koju perzistenciju podataka industrija koristi, prednosti i mane dva metoda perzistencije
- Pitanja sa prvog intervju-a iz Zuoyebang: Mogu li se mešati dva mehanizma perzistencije Redis-a
memo: 6. maja 2025. izmenjeno do ovde, danas tokom izmene CV-a člana, susreo sam člana sa master studija Northeastern University i osnovnih studija Hefei University of Technology, zaista izuzetno odličan, nadam se da svi mogu da se međusobno podstičemo i napredujemo zajedno kroz ovu platformu.

Visoka dostupnost
15. Da li poznajete master-slave replikaciju?
Master-slave replikacija omogućava slave čvorovima da održavaju kopije podataka master čvora. U ovoj arhitekturi, jedan master čvor može da se poveže sa više slave čvorova, čime se formira struktura jedan master više slave-ova. Master čvor je zadužen za obradu write operacija, slave čvorovi automatski sinhronizuju promene podataka sa master čvora i obrađuju read zahteve, čime se ostvaruje razdvajanje čitanja i pisanja.

Koja je glavna namena master-slave replikacije?
Prvo, glavni čvor je zadužen za obradu zahteva za pisanje, a potčinjeni čvorovi su zaduženi za obradu zahteva za čitanje, čime se ostvaruje razdvajanje čitanja i pisanja, smanjuje pritisak na glavni čvor i povećava konkurentna sposobnost sistema.

Drugo, potčinjeni čvorovi mogu služiti kao rezerva podataka glavnog čvora. Kada dođe do kvara na glavnom čvoru, potčinjeni čvor može brzo biti unapređen u novi glavni čvor, čime se osigurava visoka dostupnost sistema.

U kojim situacijama dolazi do neusklađenosti podataka pri replikaciji?
Replikacija glavni-potčinjeni u Redis-u se vrši asinhrono, stoga dolazi do neusklađenosti podataka na potčinjenom čvoru kada dođe do pada glavnog čvora, mrežnih fluktuacija ili visokog kašnjenja replikacije.

Na primer, ako glavni čvor padne nakon upisa podataka, ali potčinjeni čvor nije stigao da ih replicira, dolazi do neusklađenosti podataka.
Vremenska linija: →
Klijent → ka glavnom čvoru SET user:1 Erge → glavni čvor uspešno obradjuje ✅
↓
Spremam se da pošaljem potčinjenom čvoru (asinhrona replikacija)... još uvek nije završeno slanje ❌
↓
—— Iznenada glavni čvor pada (mašina se onesposobi, mreza se prekida) 💥 ——
↓
Sentinel detektuje kvar, failover: potčinjeni čvor se unapređuje u novi glavni čvor 🧠
↓
Klijent nastavlja zahtev: GET user:1 ❓→ potčinjeni čvor vraća: prazno ❌ (podaci nisu sinhronizovani)Još jedan često zanemaren faktor je pritisak memorije na glavnom čvoru. Kada memorija glavnog čvora približi gornjoj granici i omogućena je strategija istiskivanja, određeni ključevi mogu biti automatski obrisani, a ako se te operacije brisanja ne sinhronizuju na vreme, potčinjeni čvor zadržava podatke koji glavni čvor više nema.

Koja su rešenja za neusklađenost podataka pri replikaciji?
Prvo, optimizacija na mrežnom nivou. U idealnom slučaju, glavni i potčinjeni čvorovi trebalo bi da se rasporede u istoj mrežnoj oblasti kako bi se izbeglo kašnjenje između različitih oblasti.
Drugo, prilagođavanja na nivou konfiguracije. Na primer, povećanje veličine i vremena života bafera za nakupljanje replikacije kako bi se nakon ponovnog povezivanja potčinjenog čvora izvela inkrementalna sinhronizacija umesto pune sinhronizacije, čime se se maksimalno smanjuje kašnjenje glavno-potčinjene sinhronizacije.
repl-backlog-size 1mb # Podrazumevana vrednost 1MB, označava veličinu bafera replikacije glavnog čvora
repl-backlog-ttl 3600 # Podrazumevana vrednost 3600 sekundi, označava vreme života bafera replikacije glavnog čvoraTreće, uvođenje mehanizma za monitoring i automatsku popravku, redovna provera konzistentnosti podataka između glavnih i potčinjenih čvorova.
Na primer, poređenjem razlike offset-a glavnog i potčinjenog čvora se može proceniti da li je potčinjena biblioteka zaostala. Jednom kada se prekoroči setirani prag, potčinjeni čvor se uklanja i ponovo vrši potpuna sinhronizacija.

- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz Dewua 1: Redis distribuiran, glavno-potčinjeno, šta učiniti kada jedan čvor padne
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz Xiaomi F: razlika između Redis glavno-potčinjene arhitekture i glavno-potčinjenog Sentinel-a
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz Shouqianba 1 Java backend prvi krug: Na čemu se oslanja rešavanje single-point kvara u Redis-u? Da li glavno-potčinjeni režim koristi asinhronu ili sinhronu replikaciju?
memo: 7. maja 2025. izmenjeno do ovde, danas tokom izmene CV-a člana kluba, dobio sam priznanje člana kluba na izmenu CV-a: “Ovaj CV bi sada trebao biti prilično savršen”, mislim da je reč “savršen” malo preterana pohvala, haha, ali ipak sam veoma srećan.

16.Koliko vrsta čestih topologija glavno-potčinjene strukture postoji?
Postoji uglavnom tri vrste.
Najosnovnija je jedan glavni-jedan potčinjen, ovaj režim je pogodan za male projekte. Jedan glavni čvor je zadužen za pisanje, jedan potčinjeni čvor je zadužen za čitanje i rezervu podataka. Iako je ova struktura jednostavna, troškovi održavanja su niski.

Sa rastom poslovanja i povećanjem zahteva za čitanje, može se razmotriti proširenje na strukturu jedan glavni-više potčinjenih. Glavni čvor je zadužen za pisanje, više potčinjenih čvorova može podeliti pritisak.

U scenarijima međuregionalne implementacije, drvolika glavno-potčinjena struktura može efikasno smanjiti opterećenje glavnog čvora i količinu podataka koja se mora preneti potčinjenim čvorovima. Uvođenjem srednjeg sloja replikacije, potčinjeni čvorovi ne samo da mogu replicirati podatke glavnog čvora, već mogu istovremeno delovati kao glavni čvorovi za druge potčinjene čvorove i nastaviti replikaciju na niže slojeve.

17.Znate li princip glavno-potčinjene replikacije Redis-a?
Znam.
Replikacija glavni-potčinjeni u Redis-u odnosi se na sinhronizaciju izmena podataka glavnog čvora ka potčinjenom čvoru putem asinhrone replikacije, čime se ostvaruje rezerva podataka i razdvajanje čitanja i pisanja. Ovaj proces se grubo može podeliti u tri faze: uspostavljanje veze, sinhronizacija podataka i propagacija naredbi.

U fazi uspostavljanja veze, potčinjeni čvor se povezuje sa glavnim čvorom izvršavanjem naredbe replicaof. Nakon uspostavljanja veze, potčinjeni čvor šalje naredbu psync glavnom čvoru i zatraži sinhronizaciju podataka. U ovom trenutku glavni čvor kreira vezu i bafer replikacije za taj potčinjeni čvor.

Faza sinhronizacije podataka se deli na potpunu sinhronizaciju i inkrementalnu sinhronizaciju. Kada se potčinjeni čvor prvi put povezuje sa glavnim čvorom, pokreće se potpuna sinhronizacija.

U ovom procesu, glavni čvor će pokrenuti podproces fork da bi generisao RDB datoteku, dok istovremeno kešira naredbe pisanja primljene tokom generisanja datoteke u bafer replikacije. Zatim šalje RDB datoteku potčinjenom čvoru, potčinjeni čvor briše svoje podatke i učitava ovu RDB datoteku. Nakon završetka prenosa RDB-a, glavni čvor šalje keširane naredbe pisanja potčinjenom čvoru na izvršenje, osiguravajući potpunu konzistentnost podataka.

Nakon što glavni i potčinjeni čvorovi završe potpunu sinhronizaciju, uglavnom se oslanjaju na fazu propagacije naredbi za održavanje inkrementalne sinhronizacije podataka. Glavni čvor će svaku put izvršenu naredbu pisanja slati u realnom vremenu svim potčinjenim čvorovima.

Nakon verzije Redis 2.8, glavni čvor će održavati bafer za nakupljanje replikacije za svaki potčinjeni čvor, koji se koristi za čuvanje najskorijih naredbi pisanja.

Pri inkrementalnoj replikaciji, glavni čvor će privremeno sačuvati kopiju naredbi pisanja koje treba sinhronizovati u baferu za nakupljanje replikacije. Na ovaj način, kada dođe do prekida mrežne veze između potčinjenog i glavnog čvora, nakon ponovnog povezivanja potčinjeni čvor može kopirati još uvek nesinhronizovane naredbi pisanja iz bafera za nakupljanje replikacije.

memo: 8. maja 2025. izmenjeno do ovde, danas je član kluba objavio post na planeti kažeći da je dobio praksu u Tencent-u, zaista treba čestitati. Uočio sam da se pitanja u iskustvu sa intervjua uglavnom koncentrišu na Tehnički projekat, MySQL i računarske mreže.

18.Opravdajte detaljno potpunu sinhronizaciju i inkrementalnu sinhronizaciju?
Potpuna sinhronizacija prenosi kompletn skup podataka glavnog čvora potčinjenom čvoru, obično se dešava kada se potčinjeni čvor prvi put povezuje sa glavnim čvorom.

U ovom trenutku, potčinjeni čvor šalje naredbu psync ? -1 zahtevajući sinhronizaciju. ? označava da potčinjeni čvor nema ID glavnog čvora, -1 označava da nema ofseta. Nakon prijema, glavni čvor će odgovoriti potčinjenom čvoru sa FULLRESYNC. Istovremeno će uključiti dva parametra: runid glavne biblioteke i ofset replikacije offset.
Zatim pokrenuti podproces fork kako bi se generisala RDB datoteka i nove naredbi pisanja se stavljaju u bafer replikacije.
Nakon što potčinjena biblioteka primi RDB datoteku, briše stare podatke i učitava novu RDB datoteku. Nakon završetka učitavanja, potčinjeni čvor će poslati potvrdnu poruku glavnom čvoru, a glavni čvor će zatim poslati podatke iz bafera replikacije potčinjenom čvoru, osiguravajući da su podaci potčinjenog čvora identični glavnom čvoru.
Cena potpune sinhronizacije je vrlo visoka, jer generisanje potpune RDB datoteke zauzima mnogo CPU-a i diskovskog UI-a; prenose preko mreže takođe troši mnogo propusnog opsega.
Stoga je Redis nakon verzije 2.8 uveo koncept inkrementalne sinhronizacije, sa ciljem da se izbegne potpuna sinhronizacija nakon prekida i ponovnog povezivanja.
Inkrementalna sinhronizacija zavisi od tri ključna elementa:
①. Ofset replikacije: Glavni i potčinjeni čvorovi održavaju po jedan ofset replikacije koji beleži broj prenesenih bajtova. Svaki put kada glavni čvor prenese N bajtova podataka, sopstveni ofset replikacije se povećava za N; svaki put kada potčinjeni čvor primi N bajtova podataka, takođe se odgovarajuće povećava sopstveni ofset.
②. ID glavnog čvora: Svaki glavni čvor ima jedinstven ID, tj. ID replikacije, koji se koristi za označavanje verzije podataka glavnog čvora. Kada glavni čvor prođe kroz restart ili promenu uloge, ID će se promeniti.
③. Bafer za nakupljanje replikacije: Fiksne dužine FIFO red koji glavni čvor održava, podrazumevana veličina je 1M. Dok glavni čvor šalje naredbe potčinjenom čvoru, istovremeno upisuje naredbe u ovaj bafer.
Kada se potčinjeni čvor prekine i ponovo poveže sa glavnim čvorom, poslaće naredbu psync{runId}{offset}, noseći prethodno zabeleženi ID glavnog čvora i ofset replikacije.

Nakon prijema ove naredbe, glavni čvor će proveriti runId i offset:
Ako se ID glavnog čvora ne poklapa sa runId-om koji je dao potčinjeni čvor, to znači da se glavni čvor promenio i mora se izvršiti potpuna sinhronizacija.
Ako se ID poklapa, glavni čvor će potražiti da li su podaci nakon ofseta koji zahteva potčinjeni čvor još uvek u baferu za nakupljanje replikacije.
Ako jesu, šalju se samo inkrementalni podaci počevši od tog ofseta – to je inkrementalna sinhronizacija; inače to znači da je vreme prekida bilo predugo i da je bafer za nakupljanje prekrio ove podatke, pa je potrebna potpuna sinhronizacija.

Prednost inkrementalne sinhronizacije je očigledna: prenose se samo podaci naredbi tokom perioda prekida, što znatno smanjuje količinu mrežnog prenosa i opterećenje glavnih i potčinjenih čvorova, potčinjeni čvorovi ne moraju da brišu i ponovo učitavaju podatke i mogu brže da prate stanje glavnog čvora.
U okruženjima sa čestim pisanjem ili nestabilnom mrežom trebalo bi povećati veličinu bafera za nakupljanje replikacije kako bi se osiguralo da se nakon kratkog prekida može izvršiti inkrementalna sinhronizacija umesto potpune sinhronizacije.
repl-backlog-size 1mb # Podrazumevana vrednost 1MB, označava veličinu bafera replikacije glavnog čvora
repl-backlog-ttl 3600 # Podrazumevana vrednost 3600 sekundi, označava vreme života bafera replikacije glavnog čvoramemo: 9. maja 2025. izmenjeno do ovde, danas tokom izmene CV-a člana kluba, sreo sam člana kluba sa master studijama na Hebei University i osnovnim studijama na Donghua University of Technology, nadam se da ova velika porodica može doneti više pomoći i podrške svima.

19.Koji problemi postoje u glavno-potčinjenoj replikaciji?
Najveći izazov glavno-potčinjene replikacije u Redis-u dolazi od njene asinhrone osobine – glavni čvor će odmah odgovoriti klijentu nakon obrade naredbe pisanja, bez čekanja na potvrdu potčinjenih čvorova, što u određenim okolnostima može dovesti do neusklađenosti podataka.

Još jedan čest problem je uticaj potpune sinhronizacije na sistem. Potpuna sinhronizacija zauzima mnogo CPU i IO resursa, posebno u slučajevima sa velikim količinama podataka, što dovodi do smanjenja performansi glavnog čvora.
Znate li problem cepanja mozga?
U arhitekturi Sentinel u Redis-u, tipična manifestacija cepanja mozga je: kada dođe do mrežnog kvara između glavnog čvora i Sentinel-a odnosno potčinjenih čvorova, ali je veza sa klijentima normalna – tada dva “glavna čvora” istovremeno obrađuju spoljašnje zahteve.
Sentinel smatra da je glavni čvor već van mreže, pa će izabrati jednog potčinjenog čvora kao novi glavni čvor. Ali originalni glavni čvor nije svestan situacije i i dalje nastavlja da obrađuje zahteve klijenata.

Kada se mreža originalnog glavnog čvora oporavi, uvidi da već postoji novi glavni čvor i originalni glavni čvor će se automatski degradirati u potčinjeni čvor. U procesu degradacije, moraće da izvrši potpunu sinhronizaciju sa novim glavnim čvorom, pri čemu će se podaci originalnog glavnog čvora obrisati. To dovodi do toga da svi podaci koje su klijenti napisali u periodu kvara originalnog glavnog čvora budu izgubljeni.

Kako bi se sprečila ovakva gubitka podataka, Redis nudi parametre min-slaves-to-write i min-slaves-max-lag.
Ova dva parametra mogu podesiti minimalni broj potčinjenih čvorova koji moraju biti na mreži, kao i maksimalno kašnjenje potčinjenih čvorova.
# Podesiti minimalni broj potčinjenih čvorova potreban za sinhronizaciju podataka glavnog čvora
min-slaves-to-write 1
# Podesiti maksimalno kašnjenje ACK poruka od potčinjenih čvorova ka glavnom čvoru prilikom sinhronizacije podataka (u sekundama)
min-slaves-max-lag 10Nakon postavljanja ova dva parametra, ako glavni čvor ne može da se poveže sa određenim brojem potčinjenih čvorova ili ako potčinjeni čvorovi prekorače vreme odgovora, glavni čvor će odbiti zahteve za pisanje, čime se izbjegaju sukobi podataka tokom cepanja mozga.
Konkretno, kada dođe do mrežne partcije i prekid veze između glavnog čvora i potčinjenih čvorova odnosno Sentinel-a, ali je veza između glavnog čvora i klijenata normalna – zbog toga što glavni čvor ne može da se poveže ni sa jednim potčinjenim čvorom ili kašnjenje prekorači podešenu vrednost, na primer ako je konfigurisano min-slaves-to-write 1, glavni čvor će automatski odbiti sve zahteve za pisanje.
Istovremeno na drugoj strani mreže, Sentinel će detektovati da je glavni čvor "van mreže" i izabraće jednog potčinjenog čvora kao novi glavni čvor. Pošto je originalni glavni čvor već prestao da prihvata pisanje, neće doći do novih izmena podataka, i nakon oporavka mreže, čak i ako se originalni glavni čvor degradira u potčinjeni i izvrši potpunu sinhronizaciju, neće biti gubitka podataka iz perioda mrežne partcije jer jednostavno nije bilo novog pisanja.
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata 30 Tencent Music: Koje su mane glavno-potčinjene replikacije? Problem cepanja mozga u Redis-u
memo: 10. maja 2025. danas sam konfigurisao okruženje preduslova za novi projekat gotovo dovoljno, još uvek fali tutorijal za instalaciju Kafka-a. Svaki dan malo koraka napredak, nastojim da se članovima kluba predstavim pre jesenjeg regrutacije.

20.Znate li mehanizam Sentinel u Redis-u?
Sentinel u Redis-u služi za praćenje stanja rada glavno-potčinjenog klastera i automatski vrši prebacivanje pri kvaru glavnog čvora.

Ključne funkcionosti uključuju monitoring, obaveštavanje i automatsko prebacivanje pri kvaru. Sentinel redovno proverava da li glavni i potčinjeni čvorovi rade kao što se očekuje, i kada detektuje kvar glavnog čvora, bira novi glavni čvor među potčinjenim čvorovima i obaveštava klijente da se povežu sa novim glavnim čvorom.
# Informacije o glavnom čvoru koji se prati + koliko Sentinel-a mora saglasiti da se smatra da je čvor van mreže
sentinel monitor mymaster 127.0.0.1 6379 2
# Koliko dugo bez odgovora se označava kao “subjektivno van mreže”
sentinel down-after-milliseconds mymaster 5000
# Vreme timeout-a za prebacivanje pri kvaru
sentinel failover-timeout mymaster 60000
# Koliko potčinjenih čvorova istovremeno može sinhronizovati podatke sa novim glavnim čvorom
sentinel parallel-syncs mymaster 1
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz BYD 1: Da li poznajete mehanizam Redis Sentinel-a?
21.Znate li radni princip Sentinel-a?
Radni princip Sentinel-a se može sažeti u 4 ključna koraka: redovni monitoring, subjektivno van mreže, izbor vođe i prebacivanje pri kvaru.
Prvo, Sentinel redovno šalje PING naredbe svim Redis čvorovima da proveri da li su dostupni. Ako u određenom vremenu ne primi odgovor, Sentinel će označiti taj čvor kao “subjektivno van mreže”.

Kada jedan Sentinel proceni da je glavni čvor subjektivno van mreže, konsultovaće mišljenje drugih Sentinel-a, i ako se dostigne konfigurisani kvorum, glavni čvor će biti označen kao “objektivno van mreže”.

Zatim počinje prebacivanje pri kvaru. U ovom procesu, Sentinel će prvo izabrati vođu, a vođa će zatim među potčinjenim čvorovima odabrati najpogodniji čvor kao novi glavni čvor; kriterijumi biranja uključuju ofset replikacije, prioritet i druge faktore.

Nakon što se odredi novi glavni čvor, Sentinel će mu poslati naredbu SLAVEOF NO ONE da ga unapredi u glavni čvor, zatim poslati naredbu SLAVEOF drugim potčinjenim čvorovima da pokazuju na novi glavni čvor, i konačno, preko mehanizma objava/pretplata obavestiti klijente da se glavni čvor promenio.

U pravoj implementaciji, kako bi se osigurala pouzdanost mehanizma Sentinel, obično se preporučuje rasporediti najmanje tri Sentinel čvora, i ti čvorovi treba da budu raspoređeni na različitim fizičkim mašinama, čime se smanjuje rizik od jednog tačkastog kvara.

Istovremeno, podešavanje kvoruma je takođe veoma kritično – obično se preporučuje da se postavi na polovinu broja Sentinel-a plus jedan, čime se osigurava da sistem i dalje može normalno raditi pri kvaru manjine Sentinel-a, a istovremeno se izbjegava problem cepanja mozga uzrokovan mrežnom particijom.
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz OPPO 1: Kako razumeti Redis Sentinel i Cluster? Objasnite približne principe
memo: evo, pohvala jednog čitaoca na Java putu napredovanja, ja sam takođe čovek i treba mi emocionalna rezonancija svih, haha, neka bude više pohvala 😄

22.Znate li izbor vođe u Redis-u?
Redis koristi Raft algoritam za realizaciju izbora vođe, sa ciljem da se pri kvaru glavnog čvora izabere jedan Sentinel koji će biti zadužen za izvršenje operacije prebacivanja pri kvaru.

Proces glasanja izgleda ovako:
①. Kada jedan Sentinel potvrdi da je glavni čvor objektivno van mreže, poslaće zahtev drugim Sentinel čvorovima, izrazivši želju da sam izvrši glavno-potčinjenu promenu, i tražići od svih drugih Sentinel-a da glasaju. Kandidat će prvo glasati za sebe (1 glas), a zatim čekati rezultate glasanja od drugih Sentinel čvorova.
// Funkcija sentinelAskMasterStateToOtherSentinels u sentinel.c
void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master) {
dictIterator *di;
dictEntry *de;
di = dictGetIterator(master->sentinels);
while((de = dictNext(di)) != NULL) {
sentinelRedisInstance *sentinel = dictGetVal(de);
int retval;
// Slanje zahteva za glasanje samo kada se uđe u fazu izbora vođe
if (master->failover_state == SENTINEL_FAILOVER_STATE_SELECT_LEADER) {
// Slanje specijalne naredbe is-master-down-by-addr zahteva glasanje
retval = redisAsyncCommand(sentinel->cc,
sentinelReceiveVoteFromSentinel, sentinel,
"SENTINEL is-master-down-by-addr %s %d %llu %s",
master->addr->ip, master->addr->port,
(unsigned long long)master->failover_epoch,
// Ovde se šalje sopstveni runid zahtev za glasanje
sentinelGetMyRunID());
} else {
// Inače samo pitaj stanje glavnog čvora, ne traži glasanje
retval = redisAsyncCommand(sentinel->cc,
sentinelReceiveIsMasterDownReply, sentinel,
"SENTINEL is-master-down-by-addr %s %d %llu *",
master->addr->ip, master->addr->port,
(unsigned long long)0);
}
}
dictReleaseIterator(di);
}②. Sentinel čvor koji primi zahtev će proceniti – ako su dnevnik kandidata i sopstveni dnevnik jednako novi, broj mandata je manji od sopstvenog, i ranije nije glasao – dati će pozitivan glas Y. Inače će odgovoriti N.
// Deo funkcije sentinelCommand u sentinel.c (obrada SENTINEL naredbi)
// Obrada is-master-down-by-addr naredbe
else if (!strcasecmp(c->argv[1]->ptr,"is-master-down-by-addr")) {
/* SENTINEL IS-MASTER-DOWN-BY-ADDR <ip> <port> <current-epoch> <runid> */
sentinelRedisInstance *ri;
char *master_ip = c->argv[2]->ptr;
int master_port = atoi(c->argv[3]->ptr);
long long req_epoch = strtoull(c->argv[4]->ptr,NULL,10);
char *req_runid = c->argv[5]->ptr;
int isdown = 0;
char *leader = "*";
long long leader_epoch = -1;
ri = sentinelGetMasterByAddress(master_ip, master_port);
if (ri) {
isdown = ri->flags & SRI_S_DOWN;
// Ocena da li je zahtev za glasanje
if (req_runid[0] != '*') {
// Provera da li je već glasano u trenutnoj konfiguracionoj epohi
if (req_epoch > sentinel.current_epoch) {
// Ažuriranje sopstvene konfiguracione epohe
sentinel.current_epoch = req_epoch;
}
// Ako smatramo da je glavni čvor van mreže i u ovoj epohi još nismo glasali, glasajmo
if (isdown && sentinel.current_epoch == req_epoch &&
sentinel.leader_epoch < req_epoch)
{
// Zabeleži informacije o glasanju
sentinel.leader_epoch = req_epoch;
sentinel.leader = sdsnew(req_runid);
leader = req_runid;
leader_epoch = req_epoch;
}
}
}
// Vrati rezultat glasanja
addReplyMultiBulkLen(c,3);
addReplyLongLong(c, isdown);
addReplyBulkCString(c, leader);
addReplyLongLong(c, leader_epoch);
}③. Nakon što primi glasove, kandidat će sumirati sopstvene glasove i ako dobije više od polovine glasova čvorova u klasteru, biće izabran za vođu.
// Funkcija sentinelReceiveVoteFromSentinel u sentinel.c
void sentinelReceiveVoteFromSentinel(redisAsyncContext *c, void *reply, void *privdata) {
sentinelRedisInstance *sentinel = privdata;
sentinelRedisInstance *master = sentinel->master;
redisReply *r = reply;
char *leader = NULL;
// Obrada odgovora
if (r->type == REDIS_REPLY_ARRAY && r->elements == 3) {
// Parsiranje informacija o vođi iz odgovora
if (r->element[1]->type == REDIS_REPLY_STRING)
leader = r->element[1]->str;
// Provera da li je glas dat za nas
if (leader && strcmp(leader, sentinelGetMyRunID()) == 0) {
// Zabeleži dobijanje glasa
dictAdd(master->sentinels_voted, sdsnew(sentinel->runid), sentinel);
}
}
// Provera da li je dobijena većina glasova
if (master->failover_state == SENTINEL_FAILOVER_STATE_SELECT_LEADER) {
int voters = dictSize(master->sentinels) + 1; // +1 jer uključuje sebe
int votes = dictSize(master->sentinels_voted) + 1; // Sopstveni glas se takođe broji
// Ako je dobijena većina glasova (više od polovine)
if (votes >= voters/2+1) {
// Postaj vođa, započni prebacivanje pri kvaru
sentinelEvent(LL_WARNING, "+elected-leader", master, "%@");
master->failover_state = SENTINEL_FAILOVER_STATE_FAILOVER_IN_PROGRESS;
sentinelFailoverSelectSlave(master);
}
}
}④. Ako nijedan Sentinel u ovoj rundi glasanja ne dobije više od polovine glasova, ovaj izbor će propasti, i zatim se sledi sledeća runda izbora. Kako bi se sprečilo neograničeno propadanje izbora, svaki Sentinel ima vreme timeout-a za izbore, i to je nasumično.
// Funkcija sentinelFailoverSelectLeader u sentinel.c
void sentinelFailoverSelectLeader(sentinelRedisInstance *master) {
// Provera da li je isteklo vreme za izbore
mstime_t election_timeout = SENTINEL_ELECTION_TIMEOUT * 2;
if (mstime() - master->failover_start_time > election_timeout) {
// Isteklo vreme za izbore, resetuj stanje
sentinelEvent(LL_WARNING, "-failover-abort-timeout", master, "%@");
sentinelAbortFailover(master);
return;
}
// ... Ostala logika izbora ...
// Ako nema dovoljno glasova i nije isteklo vreme, nastavi čekati
}Ovde je SENTINEL_ELECTION_TIMEOUT_MIN obično 0, a SENTINEL_ELECTION_TIMEOUT_MAX obično 2000 milisekundi. Na ovaj način će svaki Sentinel započeti izbor nakon nasumičnog vremena od 0-2 sekunde, čime se smanjuju sukobi pri izboru.
Preporučeno čitanje:Detaljno objašnjenje procesa izbora glavnog čvora Raft algoritma
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata 8 backend development jesenja regrutacija prvi krug: Kako se biraju potčinjeni čvorovi kada glavni čvor padne u Raft-u
memo: 12. maja 2025. izmenjeno do ovde, danas član kluba mi je posao poruku na WeChat-u kažeći da je dobio offer iz tri velika tehnološka giganta: Ant Group, Meituan i Tencent – zaista je izuzetno odličan.

23.Kako se bira novi glavni čvor?
Sentinel prilikom biranja novog glavnog čvora postupa veoma precizno.

Prvo, Sentinel će izvršiti prvi krug osnovne selekcije na svim potčinjenim čvorovima, eliminišući one čvorove koji ne zadovoljavaju osnovne uslove. Na primer, čvorove koji su van mreže, čvorove sa nestabilnom mrežnom vezom, i čvorove kojima je prioritet podešen na 0 i koji se eksplicitno ne uključuju u proces biranja.
// Prvi krug selekcije: eliminisanje potčinjenih čvorova koji ne zadovoljavaju osnovne uslove
for (int i = 0; i < numslaves; i++) {
sentinelRedisInstance *slave = slaves[i];
// Eliminiši potčinjene čvorove koji su van mreže
if (slave->flags & (SRI_S_DOWN|SRI_O_DOWN)) continue;
// Eliminiši potčinjene čvorove sa prekinutom vezom
if (slave->link->disconnected) continue;
// Eliminiši potčinjene čvorove koji su se nedavno (unutar 5 sekundi) isključili
if (mstime() - slave->link->last_avail_time > 5000) continue;
// Eliminiši čvorove koji nisu uspostavili glavno-potčinjenu replikaciju
if (slave->slave_priority == 0) continue;
// Pronađi prvi potčinjeni čvor koji zadovoljava uslove
selected = i;
break;
}Zatim, Sentinel će sortirati preostale potčinjene čvorove i izabrati najpogodniji glavni čvor.
// Funkcija compareSlaves u sentinel.c
int compareSlaves(sentinelRedisInstance *a, sentinelRedisInstance *b) {
// 1. Prvo uporediti korisnički podešene prioritete – manja vrednost znači viši prioritet
if (a->slave_priority != b->slave_priority)
return (a->slave_priority < b->slave_priority) ? 1 : 2;
// 2. Ako su prioriteti isti, uporediti ofsete replikacije – veći ofset znači novije podatke
if (a->slave_repl_offset > b->slave_repl_offset) return 1;
else if (a->slave_repl_offset < b->slave_repl_offset) return 2;
// 3. Ako su i ofseti replikacije isti, uporediti leksikografski redosled ID-eva izvršavanja
return (strcmp(a->runid, b->runid) < 0) ? 1 : 2;
}Postoje tri kriterijuma sortiranja:
①. Prioritet potčinjenog čvora: Manja vrednost slave-priority znači viši prioritet; potčinjeni čvor sa prioritetom 0 neće biti izabran.
②. Ofset replikacije: Veći ofset znači da su podaci na potčinjenom čvoru noviji, odnosno replikacija je potpunija.
③. ID izvršavanja: Ako su i prioritet i ofset isti, upoređuje se leksikografski redosled ID-eva izvršavanja – manji ID ima prioritet.
Nakon izbora novog glavnog čvora, Sentinel će mu poslati naredbu SLAVEOF NO ONE da ga unapredi u glavni čvor.
// Funkcija sentinelFailoverPromoteSlave u sentinel.c
void sentinelFailoverPromoteSlave(sentinelRedisInstance *master) {
// ... Logika za biranje najboljeg potčinjenog čvora ...
// Slanje naredbe SLAVEOF NO ONE izabranom potčinjenom čvoru da postane glavni čvor
retval = redisAsyncCommand(slave->link->cc,
sentinelReceivePromotionResponseFromSlave, master,
"SLAVEOF NO ONE");
// Ažuriranje stanja
master->promoted_slave = slave;
slave->flags |= SRI_PROMOTED;
// Beleženje loga
sentinelEvent(LL_WARNING, "+promoted-slave", slave, "%@");
sentinelEvent(LL_WARNING, "+failover-state-wait-promotion", master, "%@");
}Nakon toga, Sentinel će sačekati da se završi promena uloge novog glavnog čvora, proveravajući INFO naredbom da li se njegova uloga već promenila u master radi potvrde. Nakon uspešne potvrde, ažuriraće ciljeve replikacije svih potčinjenih čvorova da pokazuju na novi glavni čvor.
SLAVEOF new-master-ip new-master-portmemo: 13. maja 2025., danas član kluba mi je posao poruku na WeChat-u kažeći da je dobio offer iz Ctrip-a – Ctrip je sada drugi nivo velikih internet kompanija, svaka čestitka je zaslužena.

24.Znate li Redis klaster?
Glavno-potčinjena replikacija ostvaruje razdvajanje čitanja i pisanja i rezervu podataka, Sentinel mehanizam ostvaruje automatsko prebacivanje pri kvaru glavnog čvora.

Klaster arhitektura je dalje proširenje i poboljšanje prva dva rešenja, rešavajući ograničenje veličine memorije Redis-a na jednoj mašini putem particionisanja podataka. Kada se baza korisnika poveća sa miliona na desetine miliona, dovoljno je jednostavno dodati čvorove u klaster kako bi se lako nosili sa stalno rastućom količinom podataka i pritiskom pristupa.
Na primer, podatke u režimu jedne instance možemo podeliti u proseku na 5 delova, zatim pokrenuti 5 Redis instanci, gde svaka instanca čuva 5G podataka, čime se ostvaruje klasterizacija.

25.Molim vas detaljno objasnite Redis Cluster? (dopuna)
Dodato 26. aprila 2024. godine
Redis Cluster je zvanično rešenje distribuiranog klastera koje nudi Redis. Njegova ključna ideja je decentralizacija, koristi P2P režim i nema koncepta centralnog čvora. Svaki čvor čuva i podatke i stanje celog klastera, a čvorovi međusobno razmenjuju informacije putem gossip protokola.

U pogledu particionisanja podataka, Redis Cluster koristi mehanizam heš slotova kako bi podelio ceo klaster na 16384 jedinice.

Na primer, ako imamo 4 Redis instance, tada će svaka instanca biti zadužena za više od 4000 heš slotova.

Pri računanju broja heš slota, Redis Cluster će prvo izračunati heš vrednost ključa putem CRC16 algoritma, a zatim primeniti modul operaciju na tu heš vrednost kako bi dobio integer između 0 i 16383.
slot = CRC16(key) mod 16384Na ovaj način se podaci mogu ravnomerno rasporediti na različite čvorove, čime se izbjegava problem nebalansiranja podataka.

Kada je potrebno sačuvati ili upariti jedan par ključ-vrednost, Redis Cluster će prvo izračunati broj heš slota za taj ključ, a zatim na osnovu broja heš slota pronaći odgovarajući čvor za izvršenje operacije.
Preporučeno čitanje:Redis Cluster
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz ByteDance 1 Java backend prvi krug: Redis particionisani klaster? Kako se vrši mapiranje između podataka i instanci?
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz Kuaishou 1 odsek glavne stanice tehnički odsek: Kako se implementira Redis klaster?
memo: 14. maja 2025., danas član kluba mi je posao poruku na WeChat-u kažeći da je dobio summer praksu u Baidu-u i Meituan-u – maj je zaista mesec kada cveta i donosi plodove.

26.Kako se podaci particioniraju u klasteru?
Postoje tri uobičajena načina particionisanja podataka: particionisanje ostatkom čvora, konzistentno heširanje i heš slotovi.
Particionisanje ostatkom čvora je jednostavno i jasno – putem računanja heš vrednosti ključa, zatim uzimanja ostatka pri deljenju sa brojem čvorova, dobija se indeks ciljnog čvora.
target_node = hash(key) % N // N je broj čvorova
Mana je što nakon dodavanja novog čvora, kada se broj čvorova promeni iz N u N+1, gotovo svi rezultati ostatka će se promeniti, što dovodi do toga da većina keša postane nevažeća.
Kako bi se rešio problem masovne migracije podataka uzrokovane promenom čvorova, pojavilo se particionisanje konzistentnim heširanjem: ono zamišlja ceo prostor heš vrednosti kao prsten, i čvorovi i podaci se mapiraju na ovaj prsten. Podaci se dodeljuju prvom čvoru koji se susreće u smeru kazaljke na satu.

Pametnost ovog dizajna leži u tome što se, kada se promeni broj čvorova, samo deo podataka mora realocirati. Na primer, kada se proširujemo sa 5 na 8 čvorova, teoretski samo oko 3/8 podataka treba migrirati, što značajno smanjuje pritisak na sistem pri proširenju.
Ali konzistentno heširanje i dalje ima jedan problem: raspodela podataka je nebalansirana. Na primer, u gornjem primeru, količina podataka na čvoru 1 i čvoru 2 je približno ista, ali količina podataka na čvoru 3 je daleko manja od njih.
Particioniranje heš slotovima Redis Klastera vrši poboljšanja na osnovu konzistentnog heširanja i ostatka čvora.

On deli ceo prostor heš vrednosti na 16384 slota, gde svaki čvor odgovara za deo slotova, a podaci se posle računanja putem CRC16 algoritma moduliraju sa 16384 kako bi se odredilo kom slotu pripadaju.
slot = CRC16(key) % 16384
Pretpostavimo da u sistemu imamo 4 čvora kojima je dodeljeno 16 slotova (0-15);
- Slotovi 0-3 se nalaze na čvoru node1;
- Slotovi 4-7 se nalaze na čvoru node2;
- Slotovi 8-11 se nalaze na čvoru node3;
- Slotovi 12-15 se nalaze na čvoru node4.
Ako se u ovom trenutku obriše node2, dovoljno je ponovo dodeliti slotove 4-7, na primer dodeliti slotove 4-5 čvoru node1, slot 6 čvoru node3, a slot 7 čvoru node4, pri čemu raspodela podataka po čvorovima ostaje prilično balansirana.
Ako se u ovom trenutku doda node5, takođe je dovoljno dodeliti deo slotova node5, na primer migrirati slotove 3, 7, 11 i 15 ka node5, dok ostali slotovi na čvorovima ostaju zadržani.
Budući da je broj slotova upravo 2 na 14. stepen, i ovo ima sličnu pametnost kao što dužina niza u HashMap-u mora biti stepen dvojke. Ono može garantovati da nakon proširenja većina podataka ostaje na pozicijama pre proširenja, samo manji deo podataka treba migrirati na nove slotove.
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz Xiaomi summer praks E prvi krug: Da li poznate Redis konzistentno heširanje
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz ByteDance 1 Java backend prvi krug: Da li se promeni pozicija heš slotova nakon proširenja Redis-a?
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz ByteDance 8 Java backend praks prvi krug: Redis particionisani klaster, kako se particioniše, koje su prednosti
memo: 15. maja 2025., danas član kluba mi je posao poruku na WeChat-u kažeći da je nakon što se pridružio planeti, tačno na vreme, dobio praksu u Didi-ju, pogledao sam vremensku liniju – prošlo je manje od mesec dana, zaista je neverovatno snažan.

27.Možete li objasniti principe Redis klastera?
Izgradnja Redis klastera počinje dodavanjem i rukovanjem čvorova. Svaki čvor uključuje klaster režim podešavanjem cluster-enabled yes. Zatim se vrši rukovanje putem CLUSTER MEET kako bi se druga strana dodala u liste čvorova.

Ovaj proces je vrlo pametno dizajniran: čvor A šalje MEET poruku, čvor B odgovara sa PONG i šalje PING, čvor A odgovara sa PONG, i time se uspostavlja dvosmerna komunikaciona veza.

Zanimljivo je što, usled primene Gossip protokola, ne treba da svaki par čvorova izvrši rukovanje. U implementaciji klastera sa više čvorova dovoljno je da prvi čvor izvrši rukovanje sa ostalim čvorovima, a preostali čvorovi će automatski otkriti i povezati se međusobno putem širenja informacija.

Nakon završetka rukovanja, heš slotovi se mogu dodeliti glavnim čvorovima putem naredbe CLUSTER ADDSLOTS. Kada se svih 16384 slota potpuno deli, klaster zvanično ulazi u spremno stanje.

Detekcija kvarova i oporavak su ključni za osiguravanje visoke dostupnosti Redis klastera. Svake sekunde, čvor šalje PING poruke određenom broju nasumičnih čvorova, i kada uoči da određeni čvor dugo ne odgovara na PING poruke, označiće ga kao subjektivno van mreže.

Kada više od polovine glavnih čvorova smatra da je određeni čvor subjektivno van mreže, taj čvor će biti označen kao “objektivno van mreže”.

Ako je čvor koji je van mreže glavni čvor, jedan od njegovih potčinjenih čvorova će biti izabran za novi glavni čvor i preuzeće heš slotove za koje je odgovarao originalni glavni čvor.

Koliko najmanje fizičkih čvorova je potrebno za implementaciju Redis klastera?
Za implementaciju Redis klastera pogodnog za proizvodno okruženje, sa tehničkog aspekta, potrebno je najmanje 3 fizička čvora.
Ovo postavljanje minimalnog broja čvorova nije kruti tehnički zahtev Redis-a, već praktična razmatranja bazirana na principu visoke dostupnosti.
S praktičnog aspekta, najklasičnija konfiguracija Redis klastera je 3 glavna + 3 potčinjena, ukupno 6 Redis instanci. S obzirom na to da su potrebna 3 glavna čvora i 3 potčinjena čvora, i da svaki par glavni-potčinjeni ne može biti na istoj fizičkoj mašini, potrebno je najmanje 3 fizička čvora, gde svaki fizički čvor pokreće 1 glavni čvor i potčinjeni čvor drugog glavnog čvora.
- Fizički čvor 1: Glavni čvor A + potčinjeni čvor B'
- Fizički čvor 2: Glavni čvor B + potčinjeni čvor C'
- Fizički čvor 3: Glavni čvor C + potčinjeni čvor A'
Ovaj način unakrsne implementacije može osigurati da pri kvaru bilo kog fizičkog čvora, bude pogođen najviše jedan glavni čvor i jedan potčinjeni čvor drugog glavnog čvora.
memo: 16. maja 2025., danas prilikom izmene CV-a, sreo sam člana kluba sa osnovnim studijama na Henan University of Technology i master studijama na Zhengzhou University – takođe se nadam da ova zajednica može pomoći više studenta, bez obzira odakle dolaze, da ovde pronađu onoga u sebi koji želi napredovanje i žele da dobiju kvalitetan offer.

28.Opišite dinamičko proširenje i smanjenje Redis klastera?
Ključni mehanizam dinamičkog proširenja i smanjenja Redis klastera ostvaruje se putem realokacije heš slotova.

Kada je potrebno proširenje, prvo se novi čvor dodaje u klaster putem naredbe CLUSTER MEET; zatim se koristi naredba reshard da se deo heš slotova ponovo deli novom čvoru.

----Ovaj deo za intervju ne morate da učite na pamćenje start----
Priprema novog čvora:
# redis.conf
port 6382
cluster-enabled yes
cluster-config-file nodes-6382.conf
cluster-node-timeout 5000
appendonly yesZatim pokrenuti novi čvor:
redis-server /path/to/redis-6382.confSledeće, koristiti naredbu CLUSTER MEET da se novi čvor doda u klaster:
redis-cli -p 6379 cluster meet 127.0.0.1 6382Proveriti da li se novi čvor pridružio:
redis-cli -p 6379 cluster nodesZatim, ponovo dodeliti heš slotove:
redis-cli --cluster reshard 127.0.0.1:6379U unos uneti opseg heš slotova za migraciju.
# Uneti broj slotova za migraciju, na primer 4096 (ako se prosečno deli, 16384/4=4096).
How many slots do you want to move (from 16384 total slots)? 4096
# Uneti ID čvora 6382 (može se dobiti naredbom cluster nodes).
What is the receiving node ID? <ID čvora 6382>
# Uneti all (označava migraciju prosečno sa svih čvorova).
Source node IDs? all
# Uneti yes (označava potvrdu migracije).
Do you want to proceed with the proposed reshard plan (yes/no)? yesProveriti proveru statusa dodele slotova:
redis-cli -p 6379 cluster slotsVerifikovati stanje klastera:
redis-cli -p 6382 cluster infoTakođe moguće je direktno u jednom koraku:
redis-cli --cluster reshard 127.0.0.1:6379 --cluster-from all --cluster-to <ID čvora 6382> --cluster-slots 4096 --cluster-yes----Ovaj deo za intervju ne morate da učite na pamćenje end----
Smanjenje je reverzna operacija: prvo se migriraju svi slotovi koje čvor za isključenje odgovara na druge čvorove, zatim se čvor uklanja iz klastera naredbom CLUSTER FORGET.
Ceo proces proširenja i smanjenja podržava operacije bez prekida, ne zahteva zaustavljanje, i to zahvaljujući mehanizmu MOVED i ASK preusmeravanja Redis klastera. Kada klijent pristupa ključu koji nije na trenutnom čvoru, dobiće odgovor o preusmeravanju koji ga vodi da se poveže sa pravim čvorom.
Koja je razlika između MOVED i ASK preusmeravanja?
MOVED preusmeravanje odražava trajnu promenu heš slota. Kada klijent zahteva jedan ključ, ali slot u kojem se ključ nalazi nije na trenutnom čvoru, čvor će vratiti MOVED odgovor koji obaveštava klijenta kojem čvoru sada pripada taj slot. Obično se dešava nakon što klaster završi reparticionisanje, i relacija dodele slotova se već stabilizovala.

Na primer, ako se određeni slot premesti sa čvora A na čvor B, i ako klijent i dalje zahteva ključeve iz tog slota sa čvora A, dobiće MOVED odgovor koji sugeriše da se poveže sa čvorom B.
ASK preusmeravanje se pojavljuje tokom procesa migracije slotova, označavajući da zahtevani ključ je možda već migriran sa izvornog čvora na ciljni čvor, ali migracija još uvek nije završena.

memo: 17. maja 2025., danas član kluba mi je posao poruku na WeChat-u kažeći da je dobio offer za Java backend razvoj u podružnom društvu državnog preduzeća i offer za Android razvoj u Xiaomi-u, pitao me kako da bira?

Dizajn keširanja
29.🌟Šta je probijanje keša?
Probijanje keša označava situaciju kada nakon isteka keša određenih toplih podataka, veliki broj zahteva probije keš i direktno pristupa bazi podataka, što dovodi do toga da baza podataka u trenutku podnosi ogroman pritisak.

Postoje dva uobičajena rešenja za probijanje keša:
Prvo je korišćenje ekskluzivne brave. Kada keš istekne, prva nit koja pristupa prvo će dobiti bravu i biti zadužena za rekonstrukciju keša, dok ostale niti čekaju ili pokušavaju ponovo.

Iako ova strategija može dovesti do kašnjenja pojedinih zahteva, implementacija je relativno jednostavna. U projektu Tehničke škole, koristili smo distribuiranu bravu Redisson kako bismo osigurali da samo jedna instanca servisa može ažurirati keš.
String cacheKey = "product::" + productId;
RLock lock = redissonClient.getLock("lock::" + productId);
if (lock.tryLock(10, TimeUnit.SECONDS)) {
try {
String result = cache.get(cacheKey);
if (result == null) {
result = database.queryProductById(productId);
cache.set(cacheKey, result, 60 * 1000); // postavi keš
}
} finally {
lock.unlock();
}
}Druga je strategija nikada istekla. Same stavke keša nemaju podešeno vreme isteka, odnosno nikada ne ističu, ali se u vrednosti keša održava logičko vreme isteka. Kada keš logički istekne, istovremeno se vraća stara vrednost i asinhrono se pokreće nit za ažuriranje keša.
public String getData(String key) {
CacheItem item = cache.get(key);
if (item == null) {
// keš ne postoji, sinhrono učitavanje
String data = db.query(key);
cache.set(key, new CacheItem(data, System.currentTimeMillis() + expireTime));
return data;
} else if (item.isLogicalExpired()) {
// logički istekao, asinhrono osvežavanje
asyncRefresh(key);
// vrati stare podatke
return item.getData();
}
return item.getData();
}
// asinhrono osvežavanje keša
private void asyncRefresh(final String key) {
threadPool.execute(() -> {
// ponovo pretraži bazu podataka
String newData = db.query(key);
// ažuriraj keš
cache.set(key, new CacheItem(newData, System.currentTimeMillis() + expireTime));
});
}memo: 18. maja 2025. izmenjeno do ovde, danas prilikom izmene CV-a člana kluba, sreo sam člana kluba sa Northwestern Polytechnical University, što je još jedna 985 institucija, nadam se da ova zajednica može okupiti sve 985 institucije i pomoći studentima sa više univerziteta, nadajući se da svi mogu dobiti zadovoljavajući offer.

Šta je probijanje keša?
Probijanje keša odnosi se na situaciju kada pretraga podataka ne pogodi u kešu jer ti podaci uopšte ne postoje, pa zahtevi direktno padaju na bazu podataka. Ako su ove vrste upita veoma česte, to može stvoriti veliki pritisak na bazu podataka.

Probijanje keša uzrokovano je istekom keša jednog toplih podataka, dok se probijanje dešava zato što traženi podaci ne postoje – uzrok može biti problem u sopstvenom poslovnom kodu ili zlonameran napad, na primer web crawler.
Postoje dva uobičajena rešenja: prvo je Bloom filter, vrlo efikasna strukturna organizacija podataka koja se koristi za ocenu da li element pripada skupu.
Mi možemo heširati sve moguće postojeće podatke u Bloom filter; prilikom upita prvo proverimo Bloom filter – ako on smatra da podaci ne postoje, odmah vratimo prazno; inače zatim proverimo keš. Na ovaj način se mogu izbegnuti nevažeći upiti keša.

Primer koda:
public String getData(String key) {
// ključ ne postoji u kešu
String cacheResult = cache.get(key);
if (cacheResult != null) {
return cacheResult;
}
// Bloom filter procenjuje da li ključ možda postoji
if (!bloomFilter.mightContain(key)) {
return null; // sigurno ne postoji, direktno vrati
}
// možda postoji, pretraži bazu podataka
String dbResult = db.query(key);
// stavi rezultat u keš, uključujući praznu vrednost
cache.set(key, dbResult != null ? dbResult : "", expireTime);
return dbResult;
}Bloom filter ima pogrešne pozitivne rezultate – može smatrati da određeni podaci postoje, iako u stvari ne postoje. Ali nikada neće propustiti negativan rezultat – ako Bloom filter smatra da određeni podaci ne postoje, oni zaista ne postoje. Stoga može efikasno presresti upite nepostojećih podataka i smanjiti pritisak na bazu podataka.
Drugo je keširanje praznih vrednosti. Za nepostojeće podatke, upisujemo praznu vrednost u keš i postavljamo razumno vreme isteka. Na ovaj način sledeći istovetni upit može direktno vratiti rezultat iz keša, bez pristupa bazi podataka.

Primer koda:
public String getData(String key) {
String cacheResult = cache.get(key);
// keš pogodak, uključujući prazne vrednosti
if (cacheResult != null) {
// specijalna vrednost označava prazan rezultat
if (cacheResult.equals("")) {
return null;
}
return cacheResult;
}
// keš nije pogodio, pretraži bazu podataka
String dbResult = db.query(key);
// upiši u keš, prazne vrednosti takođe se keširaju, ali sa kraćim vremenom isteka
int expireTime = dbResult == null ? EMPTY_EXPIRE_TIME : NORMAL_EXPIRE_TIME;
cache.set(key, dbResult != null ? dbResult : "", expireTime);
return dbResult;
}Implementacija metoda keširanja praznih vrednosti je relativno jednostavna, ali potrebno je postaviti razumno vreme isteka za prazne vrednosti, kako bi se izbeglo da keš i dalje vraća prazno nakon što baza podataka doda te podatke.
U stvarnim projektima, takođe je potrebno uraditi neke obrade na nivou interfejsa, na primer validaciju parametara i presretanje očigledno nerazumnih zahteva, ili ograničavanje protoka i blokiranje IP adresa za koje se sumnja da su napadi.
memo: 19. maja 2025., danas član kluba mi je posao poruku na WeChat-u kažeći da je dobio praksu za test inženjering u Didi-ju, trenutno još uvek traži, pitao me šta treba da dalje uči, moj odgovor je bio, ako je dobio praksu za leto, nastavi sa jesenjom regrutacijom, uz iskustvo prakse u Didi-ju veoma je snažno. Dok se pripremate za letnju i jesenju regrutaciju, nemojte biti previše anksiozni, održavajte dobre navike učenja, jesenja regrutacija neće biti problem.

Šta je lavina keša?
Lavina keša odnosi se na situaciju u kojoj u određenom vremenskom periodu veliki broj keševa istekne istovremeno ili keš servis iznenada padne, što dovodi do toga da veliki broj zahteva direktno juri na bazu podataka, prouzrokujući naglo povećanje pritiska na bazu podataka, pa čak i izazivajući kolaps sistema.

Probijanje keša izazvano je istekom jednog toplih podataka, probijanje se dešava zbog zahteva za nepostojeće podatke, dok lavina keša nastaje zbog isteka velikog opsega keša.
Postoje tri glavna uzroka i strategije rešavanja lavine keša.
Prvo: istek velikog broja keševa istovremeno, rešenje je dodavanje nasumičnog vremena isteka.
public void setCache(String key, String value) {
// osnovno vreme isteka, na primer 30 minuta
int baseExpireSeconds = 1800;
// dodaj nasumično vreme isteka, opseg 0-300 sekundi
int randomSeconds = new Random().nextInt(300);
// konačno vreme isteka je osnovno vreme plus nasumično vreme
cache.set(key, value, baseExpireSeconds + randomSeconds);
}Drugo: pad keš servisa, rešenje je korišćenje klastera keša visoke dostupnosti.
Na primer, korišćenje Redis Klastera za izgradnju klastera sa više čvorova kako bi se osiguralo da podaci imaju rezerve na više čvorova i da je podržano automatsko prebacivanje pri kvaru.

Za neke visokofrekventne ključne podatke moguće je konfigurisati lokalni keš kao sekundarni keš kako bi se smanjio pritisak na Redis. U projektu Tehničke škole, usvojili smo strategiju višenivelskog keširanja, uključujući korišćenje lokalnog keša Caffeine kao sekundarnog keša; kada Redis naiđe na problem, automatski se prebacuje na lokalni keš.

Ovaj proces se zove “degradacija keša”, osiguravajući da sistem može nastaviti da pruža usluge kada Redis doživi kvar.
LoadingCache<String, UserPermissions> permissionsCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(this::loadPermissionsFromRedis);
public UserPermissions loadPermissionsFromRedis(String userId) {
try {
return redisClient.getPermissions(userId);
} catch (Exception ex) {
// Redis obrada izuzetaka, pokušaj dobavljanje iz lokalnog keša
return permissionsCache.getIfPresent(userId);
}
}Treće: keš servis radi normalno ali količina konkurentnih zahteva prevazilazi nosivost keš servisa – u ovom slučaju mogu se usvojiti mere ograničavanja protoka i degradacije.
public String getData(String key) {
try {
// pokušaj dobavljanje podataka iz keša
return cache.get(key);
} catch (Exception e) {
// keš servis abnormalno, pokreni prekid (circuit breaker)
if (circuitBreaker.shouldTrip()) {
// direktno dobavi iz baze podataka i uđi u režim degradacije
circuitBreaker.trip();
return getFromDbDirectly(key);
}
throw e;
}
}
private String getFromDbDirectly(String key) {
// implementiraj zaštitu ograničenja protoka
if (!rateLimit.tryAcquire()) {
// prekoračuje prag ograničenja protoka, vrati rezervne podatke ili podrazumevanu vrednost
return getDefaultValue(key);
}
// ograničenje protoka prošlo, pretraži bazu podataka
return db.query(key);
}
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz Tencent 22 summer praks prvi krug: Lavina keša, kako rešiti
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz Kuaishou 7 Java backend prvi krug: Recite mi o probijanju keša, probijanju keša, lavini keša
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz ByteDance 7 Java backend praks prvi krug: Da li pad Redis-a utiče na sistem dozvola?
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz ByteDance 7 Java backend praks prvi krug: Recite mi rešenja za scenarije lavine Redis-a, probijanja, probijanja
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz Xiaomi F: Česti problemi i rešenja keširanja (proširuje se na višenivelsko keširanje), implementacijske ideje višenivelskog keširanja (redis, nginx, lokalni keš)
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz TP Lianzhou 5 Java backend prvi krug: Kako rešiti probijanje keša
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz Li Auto 2 prvi krug: Kako razumeti lavinu keša, probijanje keša i probijanje keša?
memo: 20. maja 2025., danas član kluba mi je posao poruku na WeChat-u kažeći da je dobio četiri offer-a na jesenjoj regrutaciji, uključujući Taillon Bank i Bank of Communications, pitao me kako da bira, iskreno rečeno, kad sam pročitao, smatrao sam da je teško izabrati, 😄ali ipak vredi čestitati. Dok se pripremate za jesenju regrutaciju, nemojte se žuriti, trud će se uvek isplatiti.

30.🌟Možete li objasniti Bloom filter?
Bloom filter je probabilistička struktura podataka izuzetno efikasna po pitanju prostora, koristi se za brzo ocenjivanje da li element pripada skupu. Njegova karakteristika je da može sa izuzetno malom potrošnjom memorije oceniti da element “sigurno nije u skupu” ili “možda je u skupu”, često se koristi za rešavanje problema probijanja Redis keša.

----Ovaj deo za intervju ne morate da učite na pamćenje start----
Jezgro Bloom filtera sastoji se od vrlo dugog binarnog vektora i niza heš funkcija.
- Pri inicijalizaciji, kreira se niz bitova dužine m, početne vrednosti su sve 0, istovremeno se bira k različitih heš funkcija
- Kada se dodaje element, k heš funkcija izračunava k heš vrednosti, zatim se primenjuje modul m da bi se dobilo k pozicija, i binarni bitovi na svim tim pozicijama se postavljaju na 1
- Kada je potrebno proceniti da li element pripada skupu, isto se koristi k heš funkcija da bi se izračunalo k pozicija – ako je binarni bit na bilo kojoj od tih pozicija 0, element sigurno nije u skupu; ako su svi 1, element je možda u skupu
public class BloomFilter<T> {
private BitSet bitSet;
private int bitSetSize;
private int numberOfHashFunctions;
public BloomFilter(double falsePositiveProbability, int expectedNumberOfElements) {
// na osnovu očekivanog broja elemenata i očekivane stope pogrešne procene, izračunaj optimalnu veličinu niza bitova i broj heš funkcija
this.bitSetSize = calculateOptimalBitSetSize(expectedNumberOfElements, falsePositiveProbability);
this.numberOfHashFunctions = calculateOptimalNumberOfHashFunctions(expectedNumberOfElements, bitSetSize);
this.bitSet = new BitSet(bitSetSize);
}
public void add(T element) {
int[] hashes = createHashes(element);
for (int hash : hashes) {
bitSet.set(Math.abs(hash % bitSetSize), true);
}
}
public boolean mightContain(T element) {
int[] hashes = createHashes(element);
for (int hash : hashes) {
if (!bitSet.get(Math.abs(hash % bitSetSize))) {
return false; // ako je bilo koji bit 0, element sigurno ne postoji
}
}
return true; // svi bitovi su 1, element možda postoji
}
// druge pomoćne metode, kao što su izračunavanje heš vrednosti, izračunavanje optimalnih parametara itd.
}----Ovaj deo za intervju ne morate da učite na pamćenje end----
Da li Bloom filter ima pogrešne procene?
Da, Bloom filter ima pogrešne procene. Može pogrešno smatrati da određeni element pripada skupu, iako element zapravo ne pripada skupu.

Ali ako Bloom filter smatra da određeni element nije u skupu, on zaista nije tu.
Uzrok pogrešne procene su heš kolizije. U Bloom filteru, više različitih elemenata se može mapirati na istu poziciju. Kako se u Bloom filter dodaje sve više elemenata, binarni niz sadrži sve više jedinica, verovatnoća heš kolizije raste, i raste stopa pogrešne procene.

Stopa pogrešne procene zavisi od sledeća 3 faktora:
- Veličina binarnog niza (m): m određuje broj bitova oznake koji mogu biti sačuvani. Ako je binarni niz premali, verovatnoća heš kolizije će se povećati, što dovodi do više stope pogrešne procene.
- Broj heš funkcija (k): k određuje broj bita koje se označavaju u binarnom nizu za svaki element. Što je više heš funkcija, verovatnoća kolizije će se odgovarajuće menjati. Ako je heš funkcija premalo, filter će brzo postati neprecizan; ako je previše, stopa pogrešne procene će takođe rasti, a efikasnost opasti.
- Broj dodanih elemenata (n): Što je više n, veća je verovatnoća heš kolizije, što dovodi do više stope pogrešne procene.
Da bi se smanjila stopa pogrešne procene, može se povećati veličina binarnog niza ili smanjiti broj dodanih elemenata.
Da bi se potpuno rešio problem pogrešnih procena Bloom filtera, kada Bloom filter vrati "možda postoji", može se izvršiti dodatna potvrda putem baze podataka.
Da li Bloom filter podržava brisanje?
Bloom filter ne podržava operaciju brisanja – to je jedno od njegovih važnih ograničenja.
Kada dodamo element, postavićemo k pozicija u binarnom nizu na 1. Pošto više različitih elemenata može deliti isti bit, ako pokušamo da obrišemo element i ponesemo k odgovarajućih bitova na 0, možemo pogrešno uticati na rezultate ocene drugih elemenata.
Na primer, ako element A i element B oba postave poziciju 5 na 1, i ako se prilikom brisanja elementa A pozicija 5 resetuje na 0, tada će upit za element B proizvesti pogrešan rezultat "ne postoji", što je u suprotnosti sa osnovnim karakteristikama Bloom filtera.
Ako se želi realizacija operacije brisanja, može se koristiti brojački Bloom filter – on čuva brojač umesto jednog bita na svakoj poziciji. Na ovaj način se operacija brisanja može realizovati smanjenjem vrednosti brojača, ali to povećava potrošnju memorije.
public class CountingBloomFilter<T> {
private int[] counters;
private int size;
private int hashFunctions;
public CountingBloomFilter(int size, int hashFunctions) {
this.size = size;
this.hashFunctions = hashFunctions;
this.counters = new int[size];
}
public void add(T element) {
int[] positions = getHashPositions(element);
for (int position : positions) {
counters[position]++;
}
}
public void remove(T element) {
int[] positions = getHashPositions(element);
for (int position : positions) {
if (counters[position] > 0) {
counters[position]--;
}
}
}
public boolean mightContain(T element) {
int[] positions = getHashPositions(element);
for (int position : positions) {
if (counters[position] == 0) {
return false;
}
}
return true;
}
private int[] getHashPositions(T element) {
// kod za izračunavanje heš pozicije
}
}Zašto koristiti Bloom filter umesto heš tabele?
Najistaknutija prednost Bloom filtera je efikasnost memorije.
Ako želimo da procenimo da li je 1 milijarda korisničkih ID-eva posećivalo određenu stranicu, korišćenje heš tabele zahteva najmanje 10G memorije (svaki ID zahteva najmanje 8 bajtova), dok Bloom filter zahteva samo 1.2G memorije.
m ≈ -n*ln(p)/ln(2)² ≈ -10⁹*ln(0.01)/ln(2)² ≈ 9.6 milijardi bitova ≈ 1.2GB
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz ByteDance 7 Java backend praks prvi krug: Da li ste čuli za Bloom filter?
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz TP Lianzhou 5 Java backend prvi krug: Princip Bloom filtera, da li je prihvatljiva stopa pogrešne procene od 5% na ovaj način?
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz Meituan 9 prvi krug: Bloom filter? Prednosti Bloom filtera? Zašto se ne može koristiti heš tabela umesto Bloom filtera?
- Java vodič za intervju (plaćeni) beleži pitanja sa intervjua kandidata iz Li Auto 2 prvi krug: Dopunsko pitanje: Objasnite Bloom filter
memo: 20. maja 2025., danas član kluba objavio post kažeći da je dobio letnju praksu u Didi-ju, posebno se zahvalio na "prevenciji intervjua".

31.🌟Kako osigurati konzistentnost podataka između keša i baze podataka?
U projektu Tehničke škole, za podatke poput oznaka članaka gde je dozvoljena kratkotrajna neusklađenost, koristim Cache Aside + mehanizam TTL isteka kako bih osigurao konzistentnost keša i baze podataka.

Konkretan metod je: pri čitanju prvo proveriti Redis, ako nije pogodjeno, zatim proveriti MySQL, istovremeno postaviti razumno vreme isteka za keš; pri ažuriranju prvo ažurirati MySQL, zatim obrisati Redis.
Ovaj metod je jednostavan i efikasan, pogodan za scenarije gde se mnogo čita a malo piše. Vreme isteka TTL takođe može garantovati da će, čak i ako operacija ažuriranja ne uspe i keš nije pravovremeno obrisan, vreme isteka osigurati eventualnu konzistentnost podataka.
// logika čitanja
public UserInfo getUser(String userId) {
// prvo proveri keš
UserInfo user = cache.get("user:" + userId);
if (user != null) {
return user;
}
// keš nije pogodio, pretraži bazu podataka
user = database.selectUser(userId);
if (user != null) {
// stavi u keš, postavi razumno vreme isteka
cache.set("user:" + userId, user, 3600);
}
return user;
}
// logika ažuriranja
public void updateUser(UserInfo user) {
// prvo ažuriraj bazu podataka
database.updateUser(user);
// obriši keš
cache.delete("user:" + user.getId());
}Zatim, zašto brisati keš umesto ga ažurirati?
Prilikom prvobitnog dizajniranja strategije keširanja, razmišljao sam i o direktnom ažuriranju keša, ali kroz praksu sam otkrio da je brisanje keša bolja opcija.

Najvažniji razlog je u konkurentnom okruženju: pretpostavimo da imamo dve konkurentne operacije ažuriranja – ako usvojimo strategiju ažuriranja keša, može se desiti sledeći problem vremenskog redosleda:
- Operacije A i B se dešavaju istovremeno, A prvo ažurira MySQL na vrednost 10, B zatim ažurira MySQL na vrednost 11. Ali pri ažuriranju keša, B može prvo izvršiti postavljanje keša na 11, a zatim A izvršiti postavljanje keša na 10. To rezultira neusklađenošću: MySQL je 11 ali Redis je 10.
Kod usvajanja strategije brisanja, bez obzira da li A ili B prvo obriše keš, sledeće operacije čitanja će uvek dobiti najnoviju vrednost iz MySQL-a.
Dodatno, brisanje keša je relativno mnogo brže od ažuriranja keša.

Budući da je operacija brisanja samo prosta DEL naredba, dok ažuriranje može zahtevati ponovno serijalizaciju celog objekta pre upisa u keš.
Zatim, zašto prvo ažurirati bazu podataka, a zatim obrisati keš?
Izbor ovog redosleda operacija sam duboko razumeo nakon što sam se "pecao" u stvarnim projektima. Pretpostavimo da usvojimo strategiju "prvo obrisati keš, zatim ažurirati bazu podataka" – u visokokonkurentnom scenariju može se desiti sledeći problem:
- Nit A želi da ažurira korisničke informacije, prvo briše keš
- Nit B upravo u tom trenutku želi da pročita te korisničke informacije, uočava da je keš prazan, pa upituje bazu podataka – u ovom trenutku još uvek je stara vrednost
- Nit B ponovo stavlja pronađenu staru vrednost u keš
- Nit A završava ažuriranje baze podataka
Rezultat je da baza podataka ima novu vrednost, ali keš i dalje čuva staru vrednost.

Ako se koristi strategija prvo ažuriranje baze podataka, a zatim brisanje keša, čak i ako se pojavi slična istovremena situacija, najgori slučaj je samo kratkotrajno čitanje starih vrednosti iz keša, ali zahtevi posle brisanja keša će direktno dobijati najnovije vrednosti iz baze podataka.
Pored toga, ako se prvo obriše keš, a zatim ažurira baza podataka, kada ažuriranje baze podataka ne uspe, keš je već obrisan. To će dovesti do toga da će se u kratkom roku svi zahtevi za čitanje probiti do baze podataka i stvoriti dodatni pritisak na bazu podataka.

Ako se prvo ažurira baza podataka, a zatim obriše keš, kada ažuriranje baze podataka ne uspe, keš ostaje u originalnom stanju i sistem i dalje može normalno da pruža usluge.
public void updateUser(User user) {
try {
// Prvo ažuriraj bazu podataka
database.updateUser(user);
// Zatim obriši keš
cache.delete("user:" + user.getId());
} catch (DatabaseException e) {
// Ažuriranje baze podataka neuspešno, keš ostaje u originalnom stanju, sistem i dalje može normalno da pruža usluge
log.error("Database update failed", e);
throw e;
} catch (CacheException e) {
// Brisanje keša neuspešno, baza podataka je ažurirana, podaci će se automatski uskladiti posle TTL
log.warn("Cache deletion failed, will be eventually consistent", e);
// Može se izabrati da se ne baca izuzetak, jer postoji TTL kao zaštita
}
}memo:22. maja 2025. godine, danas sam tokom izrade životopisa za prijatelja naišao na studenta koji je završio osnovne studije na Severozapadnom poljoprivrednom univerzitetu i magistarske studije na Univerzitetu za elektroniku i nauci, tako da smo odmah sakupili dva univerziteta iz 985 lige. Ako prijatelji u zajednici nešto dobiju, molim vas da date preporuku mladim studentima, da svi mogu da imaju koristi od toga i dobiju bolje ponude.

A ako postoji visok zahtev za konzistentnostš između keša i baze podataka, šta treba raditi?
Kada poslovanje zahteva visoku konzistentnost između keša i baze podataka, na primer u sistemima za plaćanje, upravljanju zalihama i slično, ja koristim više strategija da osiguram snažnu konzistentnost.

Prva, uvođenje reda poruka da osigura da keš na kraju bude obrisan, na primer ubacivanje zapisa lokalne poruke u transakciju ažuriranja baze podataka, nakon potvrde transakcije asinhrono šalje MQ-u da obriše keš.

Čak i ako brisanje keša ne uspe, mehanizam ponovnog pokušaja reda poruka može osigurati konačnu konzistentnost.
@Transactional
public void updateUser(UserInfo user) {
// Ažuriraj bazu podataka u transakciji
database.updateUser(user);
// Zapiši informacije o kešu koji treba obrisati u istoj transakciji
LocalMessage message = new LocalMessage("CACHE_DELETE", "user:" + user.getId());
database.insertLocalMessage(message);
// Eksplicitno objavi događaj za prijem od strane slušača
eventPublisher.publishEvent(new UserUpdateEvent(this, "user:" + user.getId()));
}
// Pošalji MQ poruku nakon potvrde transakcije
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void sendCacheDeleteMessage(UserUpdateEvent event) {
messageQueue.send("cache-delete-topic", event.getCacheKey());
}Druga, korišćenje Canal za slušanje MySQL binlog-a, kada se podaci ažuriraju, zapiši promene podataka u red poruka, potrošač poruke čuje promenu i obriše keš.

Prednost ovog rešenja je potpuno razdvajanje poslovnog koda i logike održavanja keša.
@CanalListener
public class CacheUpdateListener {
@EventHandler
public void handleUserUpdate(UserUpdateEvent event) {
// Izvuci informacije o promenama iz binlog događaja
String userId = event.getUserId();
// Pošalji poruku o brisanju keša
CacheDeleteMessage message = new CacheDeleteMessage();
message.setCacheKey("user:" + userId);
messageQueue.send("cache-delete-topic", message);
}
}
// Potrošač sluša red poruka
@KafkaListener(topics = "cache-delete-topic")
public void handleCacheDeleteMessage(CacheDeleteMessage message) {
// Obriši keš
cache.delete(message.getCacheKey());
}Naravno, ako je poslovanje relativno jednostavno i ne treba red poruka, može se smanjiti vremenski prozor neusklađenosti keša i baze podataka kroz strategiju odloženog dvostrukog brisanja, nakon prvog brisanja keša, posle određenog vremena, pokušaj ponovo da obrišeš keš.

Ovaj metod se uglavnom odnosi na situacije kada keš ne postoji, ali su upisani prljavi podaci.
public void updateUser(UserInfo user) {
// Prvo brisanje keša, smanjuje vremenski prozor neusklađenosti
cache.delete("user:" + user.getId());
// Ažuriraj bazu podataka
database.updateUser(user);
// Odmah obriši keš
cache.delete("user:" + user.getId());
// Odloženo brisanje, radi mogućih istovremenih čitanja
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(1000); // Vreme odložavanja se podešava prema kašnjenju glavno-podređene sinhronizacije
cache.delete("user:" + user.getId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}Konačno, bez obzira koju strategiju koristiš, najbolje je da postaviš razumno vreme isteka za keš kao konačnu zaštitu. Čak i ako svi mehanizmi aktivnog brisanja ne uspeju, TTL može osigurati da se podaci na kraju usklade:
// Podesi različite TTL vrednosti prema važnosti podataka
public void setCache(String key, Object value, DataImportance importance) {
int ttl;
switch (importance) {
case HIGH: // Ključni podaci, kratki TTL
ttl = 300; // 5 minuta
break;
case MEDIUM: // Obični podaci
ttl = 1800; // 30 minuta
break;
case LOW: // Manje važni podaci
ttl = 3600; // 1 sat
break;
}
cache.setWithTTL(key, value, ttl);
}Iako je ovaj metod jednostavan, može osigurati da čak i u ekstremnim situacijama uticaj neusklađenosti podataka bude kontrolisan.
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa drugog tehničkog intervjua za Huaweijevu iskustvenu kolegu 8: Kako osigurati konačnu konzistentnost podataka?
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa prvog tehničkog intervjua za QQ pozadinske tehnologije od Tencentove iskustvene kolege 23: Problem konzistentnosti podataka
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa prvog intervjua za Java pozadinski razvoj od WeBankove iskustvene kolege 1: Znaš li za problem konzistentnosti MySQL-a i keša?
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa prvog tehničkog intervjua za Java pozadinski razvoj od Meituanove iskustvene kolege 3: Kako osigurati konzistentnost redis keša i baze podataka, zašto tako dizajnirati
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa tehničkog intervjua za Javu od BYD-ove iskustvene kolege 12: Kako rešiti problem konzistentnosti redis i mysql keša
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa tehničkog intervjua za pozadinski razvoj od ByteDance-ove iskustvene kolege 17: Kako rešiti konzistentnost dvostrukog pisanja
- Java vodič za intervju (plaćeno) uključuje originalno pitanje intervjua od JD-ove iskustvene kolege 9: Kada podaci i keš redis-a nisu konzistentni, kako to tretirati
memo:Dopunjeno 23. maja 2025. godine, danas sam tokom izrade životopisa prijatelja video vrlo toplu zahvalnicu, prijatelj je rekao da je svaka rečena reč u životopisu bolja od prethodnih, zaista sam se osećao zadovoljnim, osećao sam da se moj trud isplatio.😄

32.Kako osigurati konzistentnost lokalnog keša i distribuiranog keša?
U praktičnom projektu Technical Pai, da bi smanjio opterećenje Redis-a, dodao sam još jedan sloj lokalnog keša Caffeine.

Da bih osigurao konzistentnost Caffeine i Redis keša, koristim strategiju kojom se pri ažuriranju podataka svim instancama aplikacija šalje obaveštenje o ažuriranju keša kroz Redis pub/sub mehanizam, a instance koje prime obaveštenje odmah ažuriraju ili brišu lokalni keš.

@Service
public class CacheService {
private final RedisTemplate redisTemplate;
private final CaffeineCache localCache;
public void updateData(String key, Object value) {
// Ažuriraj bazu podataka
database.update(key, value);
// Ažuriraj distribuirani keš
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
// Pošalji obaveštenje o ažuriranju keša
CacheUpdateMessage message = new CacheUpdateMessage(key, "UPDATE", value);
redisTemplate.convertAndSend("cache-update-channel", message);
}
@EventListener
public void handleCacheUpdate(CacheUpdateMessage message) {
if ("UPDATE".equals(message.getAction())) {
localCache.put(message.getKey(), message.getValue());
} else if ("DELETE".equals(message.getAction())) {
localCache.invalidate(message.getKey());
}
}
}Uvažavajući mogućnost gubitka poruka, takođe uvodim mehanizam verzija kao dopunu. Svaki put kada se podaci dobijaju iz Redis-a, dodaje se najnovija verzija. Pre dobijanja podataka iz lokalnog keša, prvo proveri da li je tvoja verzija najnovija, ako se utvrdi da je verzija zastarela, aktivno dobijaj najnovije podatke iz Redis-a.
@Component
public class VersionBasedCacheManager {
@Autowired
private StringRedisTemplate redisTemplate;
// Koristi Caffeine za izgradnju lokalnog keša: najviše 1000 stavki, ističe 10 minuta posle upisa
private final Cache<String, VersionedData> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
/**
* Dobij keširane podatke, prioritizuj lokalni keš, po potrebi učitaj iz Redis-a
*/
public Object get(String key) {
VersionedData cached = localCache.getIfPresent(key); // Uzmi iz lokalnog keša
// Dobij broj verzije iz Redis-a
String versionStr = redisTemplate.opsForValue().get(key + ":version");
// Ako nije pronađen broj verzije u Redis-u, podaci su možda već nevažeći, prinudno osveži
if (versionStr == null) {
return loadAndCache(key);
}
long remoteVersion = Long.parseLong(versionStr);
// Ako nema lokalnog keša ili je verzija starija od Redis-a, prinudno osveži
if (cached == null || cached.getVersion() < remoteVersion) {
return loadAndCache(key);
}
// Lokalni keš je pogodjen i verzija je najnovija, direktno vrati
return cached.getData();
}
/**
* Učitaj podatke i verziju iz Redis-a i upiši u lokalni keš
*/
private Object loadAndCache(String key) {
Object data = redisTemplate.opsForValue().get(key);
String versionStr = redisTemplate.opsForValue().get(key + ":version");
if (data != null && versionStr != null) {
long version = Long.parseLong(versionStr);
localCache.put(key, new VersionedData(data, version));
}
return data;
}
}Ako se na više mesta u projektu koristi logika dvostrukog keša, kako da dizajnirate ovaj deo?
Moja ideja je da apstrahujem dvostruki keš u jednu objedinjenu komponentu. Dizajniram CacheManager kao glavni ulaz, pružajući osnovne operacije kao što su get, put, evict, izvršavajući kompletan proces: prvo proveri lokalni keš, zatim distribuirani keš, i konačno bazu podataka.
public class CacheManager {
private final LocalCache localCache;
private final RedisCache redisCache;
private final Database database;
public CacheManager(LocalCache localCache, RedisCache redisCache, Database database) {
this.localCache = localCache;
this.redisCache = redisCache;
this.database = database;
}
public Object get(String key) {
// Prvo proveri lokalni keš
Object value = localCache.get(key);
if (value != null) {
return value;
}
// Zatim proveri distribuirani keš
value = redisCache.get(key);
if (value != null) {
// Ažuriraj lokalni keš
localCache.put(key, value);
return value;
}
// Konačno proveri bazu podataka
value = database.get(key);
if (value != null) {
// Ažuriraj distribuirani i lokalni keš
redisCache.put(key, value);
localCache.put(key, value);
}
return value;
}
}Znaš li razliku između lokalnog keša i Redis-a?
Redis može biti raspoređen na više čvorova, podržava particionisanje podataka, glavno-podređenu replikaciju i klaster. Lokalni keš se može koristiti samo na jednom serveru.
Za podatke koji se čitaju ekstremno često, relativno su stabilni i dozvoljavaju kratkotrajnu neusklađenost, prvo biram lokalni keš. Na primer, informacije o konfiguraciji sistema, podaci o korisničkim dozvolama, informacije o kategorijama proizvoda itd.
Za podatke koji zahtevaju real-time sinhronizaciju, često se menjaju i koje moraju deliti više servisa, biram Redis. Na primer, informacije o korisničkoj sesiji, podaci korpe za kupovinu, real-time statistički podaci itd.
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa prvog intervjua za Java pozadinski razvoj od ByteDance-ove iskustvene kolege 7: Kako osigurati konzistentnost podataka dvostrukog keša i Redis keša?
- Java vodič za intervju (plaćeno) uključuje originalno pitanje intervjua od Huawei-ove iskustvene kolege 11: Kako se koristi guava cache i redis u kombinaciji? Ako se na više mesta u projektu koristi logika dvostrukog keša, kako da dizajnirate ovaj deo?
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa drugog tehničkog intervjua od Qunarove iskustvene kolege 1: Koja je razlika između redis-a i lokalnog keša, koji je efikasniji
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa prvog intervjua od Pinduoduo-ove iskustvene kolege 8: Kako osigurati konzistentnost keša
33.Šta je topao ključ (hot key)?
Topao ključ (hot key) odnosi se na ključ koji se često pristupa u vrlo kratkom vremenskom periodu. Na primer, informacije o detaljima proizvoda koji se prodaje u velikim količinama tokom velikih akcijskih prodaja, lični podaci zvezda kada se pojave tračevi, popularne teme itd., mogu postati topao ključ.
Budući da Redis koristi model jedne niti, veliki broj zahteva koncentrisanih na isti ključ će dovesti do naglog porasta CPU upotrebe tog Redis čvora i produžetka vremena odgovora.
U Redis klaster okruženju, topao ključ takođe može dovesti do nebalansirane distribucije podataka, gde jedan čvor podnosi preveliki pritisak dok su drugi čvorovi relativno mirni.

Još ozbiljnija situacija je kada topao ključ istekne ili bude pogrešno obrisan, što može izazvati problem proboja keša.
Kako pratiti tople ključeve?
Privremeno rešenje može koristiti komandu redis-cli --hotkeys za praćenje toplih ključeva u Redis-u.
redis-cli -h <address> -p <port> -a<password> — hotkey
Ili pri pristupanju kešu, održavaj lokalni brojač, kada broj pristupa određenom ključu premaši postavljeni prag u roku od jednog minuta, označi ga kao topao ključ.
@Component
public class HotKeyDetector {
private final ConcurrentHashMap<String, AtomicLong> accessCounter = new ConcurrentHashMap<>();
private final int HOT_KEY_THRESHOLD = 1000;
public boolean isHotKey(String key) {
long count = accessCounter.computeIfAbsent(key, k -> new AtomicLong(0))
.incrementAndGet();
return count > HOT_KEY_THRESHOLD;
}
}34.Kako tretirati tople ključeve?
Najefikasnije rešenje je dodati lokalni keš, keširati tople ključeve u lokalnoj memoriji, tako da zahtevi ne moraju pristupati Redis-u.

Za neke posebno tople ključeve, možeš ih podeliti na više podključeva, zatim nasumično distribuirati na različite Redis čvorove. Na primer, podeliti hot_product:12345 u hot_product:12345:1, hot_product:12345:2 i druge kopije, pri čitanju nasumično biraj jednu od njih.

public String getHotData(String key) {
if (isHotKey(key)) {
// Nasumično biraj jednu kopiju
int replica = ThreadLocalRandom.current().nextInt(HOT_KEY_REPLICAS);
return redis.get(key + ":" + replica);
}
return redis.get(key);
}35.Kako tretirati velike ključeve (big keys)?
Veliki ključ (big key) odnosi se na keš ključ koji zauzima veliki memorijski prostor, na primer par ključ-vrednost veći od 10M. Uobičajeni tipovi velikih ključeva uključuju: List, Set, Hash strukture koje sadrže veliki broj elemenata, String tip koji čuva velike datoteke, i JSON podaci koji sadrže složene ugniježđene objekte itd.
U situacijama sa ograničenom memorijom, može dovesti do nedostatka Redis memorije. Pored toga, veliki ključevi takođe mogu dovesti do kašnjenja sinhronizacije glavno-podređene replikacije, pa čak i izazvati zagušenje mreže.
Možeš koristiti komandu redis-cli --bigkeys za praćenje velikih ključeva u Redis-u.

Ili napiši skriptu za kompletno skeniranje:
@Component
public class BigKeyScanner {
private final RedisTemplate redisTemplate;
private final int BIG_KEY_THRESHOLD = 1024 * 1024; // 1MB
public List<BigKeyInfo> scanBigKeys() {
List<BigKeyInfo> bigKeys = new ArrayList<>();
// Koristi SCAN komandu za prolazak kroz sve ključeve
ScanOptions options = ScanOptions.scanOptions().count(1000).build();
Cursor<byte[]> cursor = redisTemplate.executeWithStickyConnection(
connection -> connection.scan(options)
);
while (cursor.hasNext()) {
String key = new String(cursor.next());
long memory = getKeyMemoryUsage(key);
if (memory > BIG_KEY_THRESHOLD) {
bigKeys.add(new BigKeyInfo(key, memory, getKeyType(key)));
}
}
return bigKeys;
}
private long getKeyMemoryUsage(String key) {
// Koristi MEMORY USAGE komandu za dobijanje memorijske zauzetošću ključa
return redisTemplate.execute((RedisCallback<Long>) connection ->
connection.memoryUsage(key.getBytes())
);
}
}Za problem velikih ključeva, najosnovnije rešenje je podeliti veliki ključ na više malih ključeva za čuvanje. Na primer, podeliti Hash koji sadrži veliki broj korisničkih informacija na više malih Hash-eva.

public void splitBigKey(String bigKey) {
Map<String, String> bigData = redisTemplate.opsForHash().entries(bigKey);
// Podeli veliki ključ na više malih ključeva
for (Map.Entry<String, String> entry : bigData.entrySet()) {
String smallKey = bigKey + ":" + entry.getKey();
redisTemplate.opsForValue().set(smallKey, entry.getValue());
}
// Obriši originalni veliki ključ
redisTemplate.delete(bigKey);
}Pored toga, za JSON podatke, možeš primeniti Gzip kompresiju pre čuvanja, iako će povećati nekoliko CPU troškova, u scenarijima osetljivim na memoriju je vredno.
public void setCompressedData(String key, Object data) {
try {
String json = objectMapper.writeValueAsString(data);
byte[] compressed = compress(json.getBytes());
redisTemplate.opsForValue().set(key, compressed);
} catch (Exception e) {
log.error("Failed to compress data", e);
}
}
private byte[] compress(byte[] data) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(out)) {
gzip.write(data);
}
return out.toByteArray();
}Preporučujem za čitanje:
- Alibaba: Otkrij i tretiraj Redis velike ključeve i tople ključeve
- Dong Zonglei: Otkrivanje i rešenja za Redis tople ključeve
- Java vodič za intervju (plaćeno) uključuje pitanje koje se pojavilo na intervjuu Huawei OD: Pričaj o Redis toplim ključevima i velikim ključevima
memo:24. maja 2025. godine, danas prijatelj mi je poslao privatnu poruku da je dobio ponudu za praksu u Honor Tongsoft, čestitam mu!🎉

36.Kako se radi zagrevanje keša (cache heating)?
Zagrevanje keša odnosi se na unapredno učitavanje toplih podataka u keš pri pokretanju sistema ili u određeno vreme, kako bi se izbeglo da se veliki broj zahteva direktno obraća bazi podataka prilikom hladnog pokretanja.

Postoji više metoda zagrevanja keša. U praktičnom projektu Technical Pai, ja ću unapred učitati popularne članke u Redis prilikom pokretanja projekta, i svake noći u ponoć redovno ažurirati najnovije mapa sajta u Redis-u, kako bih osigurao da korisnici mogu dobiti keširane podatke pri prvom pristupanju, čime se smanjuje pritisak na bazu podataka.
/**
* Koristi rešenje sa tajmerom, osvežava mapa sajta svakog dana u 5:15 časova, osiguravajući konzistentnost podataka
*/
@Scheduled(cron = "0 15 5 * * ?")
public void autoRefreshCache() {
log.info("Počinjem osvežavanje URL adresa sitemap.xml, kako bih izbegao probleme s neusklađenošću podataka!");
refreshSitemap();
log.info("Osvežavanje završeno!");
}
@Override
public void refreshSitemap() {
initSiteMap();
}
private synchronized void initSiteMap() {
long lastId = 0L;
RedisClient.del(SITE_MAP_CACHE_KEY);
while (true) {
List<SimpleArticleDTO> list = articleDao.getBaseMapper().listArticlesOrderById(lastId, SCAN_SIZE);
// Osveži informacije o mapi sajta, stavi u Redis
Map<String, Long> map = list.stream().collect(Collectors.toMap(s -> String.valueOf(s.getId()), s -> s.getCreateTime().getTime(), (a, b) -> a));
RedisClient.hMSet(SITE_MAP_CACHE_KEY, map);
if (list.size() < SCAN_SIZE) {
break;
}
lastId = list.get(list.size() - 1).getId();
}
}
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa drugog tehničkog intervjua od ByteDance-ove iskustvene kolege 1: Šta je zagrevanje keša? Kako rešiti?
37.Čuo si za problem "bez dna"? Kako rešiti?
Jezgro problema "bez dna" je u tome što se sa povećanjem broja čvorova keša, iako se ukupni kapacitet skladištenja i teorijska propusnost povećavaju, vreme odgovora pojedinačnog zahteva se produžava.
Osnovni uzrok ovog problema je povećanje troškova mrežne komunikacije. Kada se broj čvorova poveća sa nekoliko desetina na nekoliko hiljada, klijent mora komunicirati sa više čvorova.
Zatim dolazi fragmentacija distribucije podataka. Sa povećanjem broja čvorova, podaci postaju više fragmentisani, relevantni podaci koji su se ranije mogli dobiti na jednom čvoru se sada mogu distribuirati na više čvorova.
Za ovaj problem može se primeniti nekoliko rešenja:
Prvo, može se kombinovati više zahteva istog čvora u jedan paketni zahtev, smanjujući broj mrežnih povratnih putovanja.
public Map<String, Object> batchGet(List<String> keys) {
// Grupiši ključeve po čvorovima
Map<String, List<String>> nodeKeysMap = groupKeysByNode(keys);
Map<String, Object> results = new ConcurrentHashMap<>();
// Istovremeno pristupaj svim čvorovima
List<CompletableFuture<Void>> futures = nodeKeysMap.entrySet().stream()
.map(entry -> CompletableFuture.runAsync(() -> {
String node = entry.getKey();
List<String> nodeKeys = entry.getValue();
// Dobavi podatke tog čvora u paketima
Map<String, Object> nodeResults = getFromNode(node, nodeKeys);
results.putAll(nodeResults);
}))
.collect(Collectors.toList());
// Čekaj da se svi zahtevi završe
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
return results;
}Drugo, može se koristiti algoritam konzistentnog heširanja za optimizaciju distribucije podataka, smanjujući troškove migracije i redistribucije podataka.
public class LocalityAwareSharding {
public String getNodeForKey(String key, String category) {
// Podaci iste kategorije se raspoređuju na isti čvor koliko je moguće
String shardKey = category + ":" + (key.hashCode() % SHARDS_PER_CATEGORY);
return consistentHash.getNode(shardKey);
}
// Korisnički podaci su na istom čvoru koliko je moguće
public String getUserDataNode(String userId) {
return "user_cluster_" + (userId.hashCode() % USER_CLUSTERS);
}
}Redis održavanje
38.Kako tretirati nedostatak memorije u Redis-u?
Kada Redis prijavi nedostatak memorije, obično je zato što je fizička memorija koju Redis koristi već bliska ili premašila konfigurisano ograničenje maksimalne memorije. U ovom slučaju može se preduzeti nekoliko koraka:
Prvo, koristi INFO memory komandu da proveriš stanje memorije Redis-a, da vidiš da li je zaista dostignuto ograničenje maksimalne memorije.
redis-cli INFO memory
Drugo, ako server još uvek ima dostupnu memoriju, izmeni parametar maxmemory u redis.conf, povećaj ograničenje maksimalne memorije Redis-a. Na primer, postavi maksimalnu memoriju na 8GB:
maxmemory 8gbTreće, izmeni parametar maxmemory-policy za prilagođavanje strategije eliminacije memorije. Na primer, možeš odabrati strategiju allkeys-lru, tako da Redis automatski briše najmanje korišćene ključeve.
maxmemory-policy allkeys-lrumemo:Dopunjeno 25. maja 2025. godine, danas tokom izrade životopisa prijatelja sam naišao na prijatelja sa osnovnim studijama na Xi'an Jiaotong Univerzitetu i magistarskim studijama na Shanghai Jiaotong Univerzitetu, 985 osnovne i magistarske diplome su zaista vrhunske, uradiću sve što mogu da mu pomognem da osvoji SSP ponudu u jesenjej regrutaciji, napred!

39.Koje su strategije isteka ključeva Redis-a?
Redis uglavnom koristi dve strategije brisanja isteklih ključeva da osigura da istekli ključevi mogu biti pravovremeno obrisani, uključujući leňerno brisanje i redovno brisanje.

Leňerno brisanje je najosnovnija strategija. Kada klijent pristupa ključu, Redis će proveriti da li je ključ već istekao, ako je istekao, odmah će ga obrisati i vratiti nil.
// Simuliraj logiku leňernog brisanja
public Object get(String key) {
RedisKey redisKey = getKeyFromMemory(key);
if (redisKey != null && isExpired(redisKey)) {
// ključ je istekao, obriši i vrati null
deleteKey(key);
return null;
}
return redisKey != null ? redisKey.getValue() : null;
}Prednost ove strategije je u tome što nema dodatnih CPU troškova, proverava se samo prilikom pristupa ključu. Ali problem je u tome što ako istekli ključ nikada ne bude pristupljen, zauziaće memoriju zauvek.

Zato postoji strategija redovnog brisanja. Redis će redovno nasumično birati neke ključeve sa postavljenim vremenom isteka za proveru, brisati one koji su već istekli. Ovaj proces se podrazumevano izvršava 10 puta u sekundi, svaki put nasumično bira 20 ključeva za proveru.
----ovaj deo ne mora da se uči za intervju start----
Može se koristiti config get hz komanda za proveru frekvencije Redis internih zakazanih zadataka.

Vrednost hz od “10” znači da Redis izvršava 10 puta zakazane zadatke u sekundi. Može se podesiti kroz CONFIG SET hz 20.

----ovaj deo ne mora da se uči za intervju end----
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa prvog intervjua letnje prakse od Tencentove iskustvene kolege 22: Strategija brisanja Redis ključeva
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa drugog tehničkog intervjua od Qunarove iskustvene kolege 1: Eliminacija i istek memorije redis-a
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa prvog tehničkog intervjua za Java pozadinski razvoj od JD-ove iskustvene kolege 5: Strategija isteka redis ključeva
40.🌟Koje su strategije eliminacije memorije Redis-a?
Kada korišćenje memorije približi ograničenju maxmemory, Redis će odlučiti koje ključeve obrisati na osnovu strategije eliminacije memorije kako bi se smanjio pritisak na memoriju.

Postoji osam često korišćenih strategija eliminacije memorije. Podrazumevana je noeviction, kada nema dovoljno memorije neće obrisati nijedan ključ, direktno će vratiti grešku, u produkcionom okruženju se gotovo ne koristi.
Zatim sledi allkeys-lru, allkeys-lfu i allkeys-random za sve ključeve. lru će obrisati najmanje korišćene ključeve, najčešće se koristi u scenarijima čistog keširanja, može automatski zadržati tople podatke; lfu će obrisati ključeve sa najnižom učestalošću pristupa, pogodnije za sisteme koji duže rade; random će nasumično obrisati neke ključeve, obično se ne preporučuje.
Zatim sledi volatile-lru, volatile-lfu, volatile-ttl i volatile-random za ključeve sa postavljenim vremenom isteka.
lru se često koristi u scenarijima mešovitog skladištenja.
@Service
public class HybridStorageService {
// Važni podaci nemaju vreme isteka, privremeni podaci imaju vreme isteka
public void storeData(String key, Object data, DataImportance importance) {
if (importance == DataImportance.HIGH) {
// Važni podaci nemaju vreme isteka, neće biti eliminisani pod volatile-* strategijom
redisTemplate.opsForValue().set(key, data);
} else {
// Privremeni podaci imaju vreme isteka, mogu biti eliminisani pod volatile-* strategijom
redisTemplate.opsForValue().set(key, data, Duration.ofHours(1));
}
}
}lfu je pogodan za scenarije gde je potrebno zaštititi neke važne podatke od eliminacije; ttl prvo briše ključeve koji će uskoro istći, preporučuje se u sistemima za upravljanje korisničkim sesijama; random se i dalje retko koristi.
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa prvog intervjua od Xiaomi-ve proljetne regrutacije K: Zašto je redis brz, strategija eliminacije, perzistencija
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa drugog tehničkog intervjua od Qunarove iskustvene kolege 1: Eliminacija i istek memorije redis-a
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa prvog intervjua za Java pozadinski razvoj od Zuoyebangove iskustvene kolege 1: Strategija eliminacije memorije redis-a
41.Koja je razlika između LRU i LFU?
LRU je skraćenica od Least Recently Used, bazira se na vremenskoj dimenziji, eliminiše najmanje korišćene ključeve.
LFU je skraćenica od Least Frequently Used, bazira se na dimenziji broja, eliminiše ključeve sa najnižom učestalošću pristupa.
Pretpostavimo da u kešu postoje tri podatka A, B, C. U LRU scenariju, ako je redosled pristupa A→B→C→A, tada je LRU redosled B→C→A, ako je potrebno eliminisati, prvo će se obrisati B.
Ali u LFU scenariju, ako je A pristupljen 5 puta, B 2 puta, C 1 put, bez obzira na recentni redosled pristupa, prvo će se eliminisati C, jer ima najnižu učestalost pristupa.
LRU je pogodniji za scenarije sa jasnom vremenskom lokalnošću, na primer na vijest sajtu, korisnici su više zainteresovani za najnovije vijesti, dok se pristup jučerašnjim vijestima drastično smanjuje. U ovakvoj situaciji, LRU može dobro zadržati sadržaj koji korisnici trenutno interesuju.
LFU je pogodniji za scenarije sa dugoročnim obrascima pristupa, više naglašava “toplotu”, na primer na e-commerce platformi, određeni proizvodi mogu dugoro zadržati stanje popularnosti, iako im je vremenski interval pristupa duži, zbog visoke učestalosti pristupa LFU će prvo zadržati informacije o ovim proizvodima.
- Java vodič za intervju (plaćeno) uključuje originalno pitanje intervjua od Ele.me, iskusnog kolege 19 iz Alibaba sistema: Mehanizam eliminacije memorije redis-a, proširivanje na LRU i LFU
memo:27. maja 2025. godine, danas prijatelj mi je poslao privatnu poruku da je dobio ponudu za praksu od Heliao i Dewu, čestitam mu!🎉 Čak se i posebno zahvalio na prethodnim izmenama njegovog životopisa i savetima za učenje.

42.Kako rešiti kada dođe do blokiranja Redis-a?
Blokiranje Redis-a u produkcionom okruženju je prilično ozbiljan problem. Kada primetite da Redis usporava, ja ću prvo koristiti monitor komandu da vidim komande koje se trenutno izvršavaju, ili koristiti slowlog komandu da vidim dnevnik sporih upita.
# Vidi trenutno izvršavane komande
redis-cli MONITOR
# Vidi dnevnik sporih upita
redis-cli SLOWLOG GET 10
# Proveri stanje klijentskih veza
redis-cli CLIENT LISTObično su veliki ključevi (big keys) jedan od glavnih uzroka blokiranja Redis-a. Na primer, direktno brisanje DEL skupa koji sadrži nekoliko miliona elemenata će dovesti do blokiranja Redis-a nekoliko sekundi ili čak duže.
U ovom slučaju možeš koristiti UNLINK komandu umesto DEL za asinhrono brisanje, izbegavajući blokiranje glavne niti.
# Koristi UNLINK za asinhrono brisanje velikog ključa
redis-cli UNLINK big_keyZa vrlo velike kolekcije možeš koristiti SCAN komandu za delimično brisanje.
public void safeBatchProcess(String key) {
ScanOptions options = ScanOptions.scanOptions().count(1000).build();
Cursor<String> cursor = redisTemplate.opsForSet().scan(key, options);
while (cursor.hasNext()) {
String member = cursor.next();
// Delimično obradi, izbjegavaj blokiranje
processElement(member);
}
}Pored toga, kada Redis koristi više memorije od fizičke memorije, operativni sistem će zameniti deo memorije na disk, u ovom trenutku će doći do usporavanja odgovora Redis-a. Moj način rješavanja je:
Koristi free -h za provjeru korišćenja memorije; potvrdi da li je postavka Redis-ovog maxmemory razumna; ako se desila zamena memorije, odmah prilagodi maxmemory i očisti neke važne podatke.
Veliki broj klijentskih veza takođe može dovesti do blokiranja, u ovom slučaju je najbolje provjeriti konfiguraciju poola veza.
@Configuration
public class RedisConnectionConfig {
@Bean
public JedisConnectionFactory jedisConnectionFactory() {
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(200); // Maksimalan broj veza
poolConfig.setMaxIdle(50); // Maksimalan broj mirnih veza
poolConfig.setMinIdle(10); // Minimalan broj mirnih veza
poolConfig.setMaxWaitMillis(3000); // Maksimalno vrijeme čekanja za dobijanje veze
poolConfig.setTestOnBorrow(true); // Testiraj validnost pri dobijanju veze
return new JedisConnectionFactory(poolConfig);
}
}Redis aplikacije
43.Kako Redis realizuje asinhroni red poruka?
Redis realizacija asinhronog reda poruka je vrlo praktično tehničko rešenje, najjednostavniji način je korišćenje List zajedno sa LPUSH i RPOP komandama.

@Service
public class SimpleRedisQueue {
private final RedisTemplate<String, Object> redisTemplate;
// Proizvođač: šalje poruke u red
public void sendMessage(String queueName, Object message) {
redisTemplate.opsForList().leftPush(queueName, message);
}
// Potrošač: dobija poruke iz reda
public Object receiveMessage(String queueName) {
return redisTemplate.opsForList().rightPop(queueName);
}
// Blokirajuće potrošnja, izbjegavaj pollanje
public Object blockingReceive(String queueName, int timeoutSeconds) {
List<Object> result = redisTemplate.opsForList()
.rightPop(queueName, timeoutSeconds, TimeUnit.SECONDS);
return result != null && !result.isEmpty() ? result.get(0) : null;
}
}Pored toga, može se koristiti Redis Pub/Sub za realizaciju jednostavne emitovanja i pretplate poruka.
@Service
public class RedisPubSubService {
private final RedisTemplate<String, Object> redisTemplate;
// Objavi poruku na navedeni kanal
public void publish(String channel, Object message) {
redisTemplate.convertAndSend(channel, message);
}
// Pretplati se na kanal
@PostConstruct
public void subscribe() {
redisTemplate.setMessageListener((message, pattern) -> {
System.out.println("Received message: " + message);
});
redisTemplate.getConnectionFactory().getConnection().subscribe(
new ChannelTopic("myChannel").getTopic().getBytes()
);
}
}Objavljivač objavljuje poruku na navedeni kanal, a klijenti koji su pretplaćeni na taj kanal mogu primiti poruku.

Ali oba ova načina su nepouzdana, jer nema ACK mehanizma tako da ne može garantirati da pretplatnici sigurno primaju poruke, niti podržava perzistenciju poruka.
44.Kako Redis realizuje odloženi red poruka?
Odloženi red poruka je vrlo čest u stvarnom poslovanju, na primer otkaz narudžbe nakon isteka vremena, planirana podsećanja i slično. Iako Redis nije profesionalni red poruka, može vrlo dobro realizovati funkciju odloženog reda.
Ključna ideja je koristiti uređenu karakteristiku ZSet-a, uzeti poruku kao član, i vreme izvršenja poruke kao bod. Tako će se poruke automatski sortirati prema vremenu izvršenja, mi samo trebamo redovno skenirati poruke pre trenutnog vremena za obradu.

@Service
public class DelayedMessageQueue {
private final RedisTemplate<String, Object> redisTemplate;
// Pošalji odloženu poruku
public void sendDelayedMessage(String queueName, Object message, long delaySeconds) {
// Izračunaj vreme izvršenja poruke
long executeTime = System.currentTimeMillis() + (delaySeconds * 1000);
// Dodaj poruku u ZSet, uzimajući vreme izvršenja kao bod
redisTemplate.opsForZSet().add(queueName, message, executeTime);
log.info("Slanje odložene poruke: {}, kašnjenje: {} sekundi", message, delaySeconds);
}
// Potroši odložene poruke
@Scheduled(fixedDelay = 1000) // Skendiraj jednom u sekundi
public void consumeDelayedMessages() {
String queueName = "delayed:queue";
long currentTime = System.currentTimeMillis();
// Dobij poruke koje su istekle (bod <= trenutno vreme)
Set<Object> messages = redisTemplate.opsForZSet()
.rangeByScore(queueName, 0, currentTime);
for (Object message : messages) {
try {
// Obradi poruku
processMessage(message);
// Ukloni iz reda nakon uspešne obrade
redisTemplate.opsForZSet().remove(queueName, message);
log.info("Uspešno obrađena odložena poruka: {}", message);
} catch (Exception e) {
log.error("Neuspešna obrada odložene poruke: {}", message, e);
// Može se realizovati mehanizam ponovnog pokušaja
handleFailedMessage(queueName, message);
}
}
}
}U konkretnoj realizaciji, prilikom slanja odložene poruke od strane proizvođača, izračunaćemo vremensku oznaku kada poruka treba da se izvrši, zatim ćemo koristiti ZADD komandu da dodamo poruku u ZSet.
ZADD delay_queue 1617024000 task1Potrošač kroz planirani zadatak koristi ZRANGEBYSCORE komandu da dobije sve poruke pre trenutnog vremena.
ZREMRANGEBYSCORE delay_queue -inf 1617024000Nakon završetka obrade koristi ZREM za brisanje poruke.
ZREM delay_queue task1U praktičnom projektu Technical Pai, koristio sam ovaj metod za realizaciju funkcije planiranog objavljivanja članaka. Kada autor objavljuje članak, može odabrati budući vremenski trenutak, na primer za 30 minuta, sistem će poslati odloženu poruku u odloženi red, a planirani zadatak će izvaditi ovu poruku iz odloženog reda i objaviti članak nakon 30 minuta.
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa prvog tehničkog intervjua za QQ pozadinske tehnologije od Tencentove iskustvene kolege 23: Redis realizuje odloženi red
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa prvog intervjua za Java pozadinski razvoj od ByteDance-ove iskustvene kolege 8: Redis struktura podataka, koju strukturu koristiti za realizaciju odloženog reda poruka
memo:Dopunjeno 28. maja 2025. godine, danas prijatelj u VIP grupi je poslao poruku da je dobio ponudu za letnju praksu u Honor, iako vreme nije rano, što je bliže kraju, lakše je dobiti ponudu.

45.🌟Podržava Redis transakcije?
Da, Redis podržava jednostavne transakcije, može upakovati multi, exec, discard i watch komande, a zatim ih izvršiti redom jednom.

Osnovni tok je korišćenje multi za otvaranje transakcije, zatim izvršavanje niza komandi, i konačno exec za potvrdu. Ove komande će biti stavljene u red, i masovno izvršene prilikom exec.

Kada je klijent u stanju bez transakcije, sve komande poslate Redis servis će biti odmah izvršene; ali kada klijent uđe u transakciju stanje, ove komande će biti stavljene u transakcioni red, i odmah vratiti QUEUED, što znači da je komanda stavljena u red.

Kada se izvrši exec komanda, Redis će izvršiti sve komande u transakcionom redu po redu FIFO. Nakon što se sve komande u transakcionom redu potpuno izvrše, Redis će vratiti niz koji sadrži rezultate svake komande.
discard komanda se koristi za otkazivanje transakcije, očistiće transakcioni red i izaći iz stanja transakcije.

watch komanda se koristi za praćenje jednog ili više ključeva, ako se ovaj ključ promeni drugim komandama pre izvršenja transakcije, transakcija će biti prekinuta.

Ali Redis transakcija se mnogo razlikuje od MySQL, ne podržava rollback, niti podržava nivoe izolacije.
Objasni princip Redis transakcije?
Princip Redis transakcije nije komplikovan, srž je mehanizam "prvo u red, zatim izvrši".

Kada se izvrši MULTI komanda, Redis će označiti ovog klijenta transakcijskim markerom, što znači da se komande koje ovaj klijent kasnije šalje neće odmah izvršiti, već će biti stavljene u red i čekati.

Kada Redis primi EXEC komandu, izvadiće komande iz reda jedna po jedna i izvršiti ih. Budući da je Redis jedno-nitni, ovaj proces neće biti prekinut drugim komandama, što osigurava atomičnost Redis transakcije.

Kada se izvrši WATCH komanda, Redis će dodati ključ u globalni rečnik za praćenje; sve dok se ovi ključevi ne promene drugim klijentima pre EXEC, Redis će označiti relevantne klijente kao "prljave", kada se otkrije da je transakcija ometana prilikom EXEC, odmah će otkazati celu transakciju.
// Globalni rečnik za praćenje
dict *watched_keys;
typedef struct watchedKey {
robj *key;
redisDb *db;
} watchedKey;DISCARD radi vrlo jednostavne i direktnne stvari, prvo proverava da li je klijent zaista u transakcijskom stanju, ako nije prijavljuje grešku; ako je u transakcijskom stanju, očistiće transakcioni red i izaći iz transakcijskog stanja.
void discardCommand(client *c) {
if (!(c->flags & CLIENT_MULTI)) {
addReplyError(c,"DISCARD without MULTI");
return;
}
discardTransaction(c);
addReply(c,shared.ok);
}Na šta treba obratiti pažnju kod Redis transakcija?
Najvažnija stvar je da Redis transakcija ne podržava rollback, jednom kada se pozove EXEC komanda, sve komande će biti izvršene, čak i ako se neke komande možda neuspešno izvrše.
Zašto Redis transakcija ne podržava rollback?
Osnovni dizajn koncept Redis-a je jednostavan, efikasan, a ne potpune ACID karakteristike. Implementacija rollback-a zahteva čuvanje velike količine stanja tokom izvršenja i povratno izvršavanje komandi prilikom greške kako bi se povratilo originalno stanje. Ovo će povećati kompleksnost i performanski trošak Redis-a.

Da li Redis transakcija zadovoljava atomičnost? Kako poboljšati?
Redis transakcija ne može zadovoljiti standardnu atomičnost, jer ne podržava rollback transakcije, to znači, ako se neka komanda neuspešno izvrši, cela transakcija se neće automatski vratiti u početno stanje.
// Transakcija prenosa novca
redisTemplate.multi();
redisTemplate.opsForValue().decrement("user:1:balance", 100); // Uspešno
redisTemplate.opsForList().leftPush("user:1:balance", "log"); // Greška tipa, neuspešno
redisTemplate.opsForValue().increment("user:2:balance", 100); // Ipak će se izvršiti
List<Object> results = redisTemplate.exec();
// Rezultat: Korisnik 1 je odbijen novac, korisnik 2 je primio novac, ali log operacija u sredini nije uspela
// Ovo odgovara Redis definiciji atomičnosti, ali ne odgovara poslovnim očekivanjimaMože se koristiti Lua skripta kao zamena za transakciju. Tokom izvršenja skripte, Redis neće obraćati druge komande, i možemo u skripti obraditi celokupnu poslovnu logiku, uključujući provere uslova i rukovanje greškama, osiguravajući da se ili uspešno izvrši ili zadrži početno stanje, neće se desiti situacija da se jedna komanda neuspešno izvrši a druge uspešno.
@Service
public class ImprovedTransactionService {
public boolean atomicTransfer(String fromUser, String toUser, int amount) {
String luaScript =
"local from_key = KEYS[1] " +
"local to_key = KEYS[2] " +
"local amount = tonumber(ARGV[1]) " +
// Proveri saldo izlaznog računa
"local from_balance = redis.call('GET', from_key) " +
"if not from_balance then return -1 end " +
"from_balance = tonumber(from_balance) " +
"if from_balance < amount then return -2 end " +
// Proveri da li postoji ulazni račun
"if redis.call('EXISTS', to_key) == 0 then return -3 end " +
// Sve provere su prošle, izvrši prenosenje
"redis.call('DECRBY', from_key, amount) " +
"redis.call('INCRBY', to_key, amount) " +
// Zabeleži log prenosa
"local log = from_key .. ':' .. to_key .. ':' .. amount " +
"redis.call('LPUSH', 'transfer:log', log) " +
"return 1";
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setScriptText(luaScript);
script.setResultType(Long.class);
Long result = redisTemplate.execute(script,
Arrays.asList("user:" + fromUser + ":balance", "user:" + toUser + ":balance"),
amount);
return result != null && result == 1;
}
}Kako se manifestuju ACID karakteristike Redis transakcije?
Izvršenje pojedinačne Redis komande je atomično, ali Redis nije dodao nijedan mehanizam za održavanje atomičnosti na transakcijama, tako da ako se neka komanda neuspešno izvrši tokom Redis transakcije, druge komande će i dalje nastaviti da se izvršavaju, neće doći do rollback-a.

Konzistentnost se odnosi na to, ako su podaci konzistentni pre izvršenja transakcije, nakon izvršenja transakcije, bez obzira da li je transakcija uspešno izvršena, podaci bi takođe trebalo biti konzistentni. Ali Redis transakcija ne garantuje konzistentnost, jer ako se neka komanda u transakciji neuspešno izvrši, druge komande će se i dalje izvršiti, i doći će do neusklađenosti podataka.
Redis izvršava transakcije jedno-nitno i neće prekinuti, sve dok ne izvrši sve komande u transakcionom redu. Stoga smatram da Redis transakcije imaju karakteristiku izolacije.

Perzistentnost Redis transakcije potpuno zavisi od mehanizma perzistencije samog Redis-a, ako je uključen AOF, komande u transakciji će biti zabeležene kao celina u AOF datoteku, naravno zavisi i od fsync strategije AOF-a.
Ako je samo uključen RDB, komande u transakciji mogu biti izgubljene pre sledećeg snimka. Ako nijedan nije uključen, sigurno ne zadovoljava perzistentnost.
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa prvog intervjua Huawei-a: Pričaj o Redis transakcijama
- Erge programiranje planeta prijatelj Zhen Yunmian Meituan AI intervju originalno pitanje: Šta je redis transakcija, kako se manifestiraju njene ACID osobine
- Java vodič za intervju (plaćeno) uključuje originalno pitanje sa prvog intervjua od Kuaishou kolege 4: Da li Redis transakcija zadovoljava atomičnost? Kako poboljšati?
memo:29. maja 2025. godine, danas tokom izrade životopisa prijatelja sam naišao na prijatelja sa osnovnim, magistarskim i doktorskim studijama na Southeast University, 3 univerziteta iz 985 lige, ovo je prijatelj sa najvišim obrazovanjem koje znam.

46.Imaš iskustva sa Lua skriptama za rad sa Redis-om?
Lua skripte su prvo rešenje za kompleksne Redis operacije, na primer atomsko smanjivanje zaliha, distribuirane brave, ograničavanje protoka i druge poslovne scenarije, sve se može realizovati kroz Lua skripte.

U scenariju sekundarnog kupovania (flash sale), možeš koristiti Lua skriptu da napišeš sve logike provere zajedno: prvo vidi da li ima dovoljno zaliha, zatim da li je korisnik već kupio, samo kada su svi uslovi zadovoljeni smanjuje se zaliha. Budući da se cela skripta izvršava atomično, Redis neće obraćati druge komande tokom izvršenja, tako da se može potpuno rešiti problem prekomernog prodavanja.
// Ova skripta za flash sale me je spasila
String luaScript =
"local stock = redis.call('GET', KEYS[1]) " +
"if not stock or tonumber(stock) < tonumber(ARGV[2]) then " +
" return -1 " + // Nedovoljno zaliha
"end " +
"if redis.call('SISMEMBER', KEYS[2], ARGV[1]) == 1 then " +
" return -2 " + // Ponovljena kupovina
"end " +
"redis.call('DECRBY', KEYS[1], ARGV[2]) " +
"redis.call('SADD', KEYS[2], ARGV[1]) " +
"return 1";U scenariju distribuirane brave, na početku sam koristio SETNX komandu za realizaciju, ali sam otkrio da ako program abnormalno završi, brava će ostati mrtva. Kasnije sam dodao vreme isteka, ali sam otkrio da se može pogrešno obrisati brava drugih niti. Na kraju sam koristio Lua skriptu da potpuno rešim ovaj problem, osiguravajući da samo vlasnik brave može osloboditi bravu.
// Skripta za otključavanje je posebno važna, mora se verifikovati da je tvoja brava pre brisanja
private final String UNLOCK_SCRIPT =
"if redis.call('GET', KEYS[1]) == ARGV[1] then " +
" return redis.call('DEL', KEYS[1]) " +
"else " +
" return 0 " +
"end";Čak možeš koristiti Lua skriptu za realizaciju kliznog prozor ograničavača protoka, jednim putem kompletirajući tri operacije: čišćenje isteklih podataka, provera brojanja, dodavanje novih zapisa, a sve je potpuno atomično.
// Klizni prozor ograničavanje protoka, jasna logika, dobre performanse
String luaScript =
"local key = KEYS[1] " +
"local now = tonumber(ARGV[1]) " +
"local window = tonumber(ARGV[2]) " +
"local limit = tonumber(ARGV[3]) " +
// Prvo očisti istekle zapise
"redis.call('ZREMRANGEBYSCORE', key, 0, now - window) " +
// Proveri trenutni broj zahteva
"local current = redis.call('ZCARD', key) " +
"if current < limit then " +
" redis.call('ZADD', key, now, now) " +
" return 1 " +
"else " +
" return 0 " +
"end";memo:30. maja 2025. godine, danas prijatelj u planetu je poslao poruku da je dobio ponudu od Kingsoft Office, pitao me da li da bira cpp ili go, moj predlog možete videti da li je razuman, bez obzira na izbor, zaista čestitam prijatelju!

47.Znaš li za Redis cevovod Pipeline?
Znam, Pipeline dozvoljava klijentu da pošalje više komandi Redis serveru odjednom, bez čekanja na odgovor jedne komande pre slanja naredne. Redis server će izvršavati komande redom i sve rezultate upakovati vratiti klijentu.

U normalnim okolnostima, svako izvršenje Redis komande zahteva jedan mrežni povratni put: pošalji komandu -> čekaj odgovor -> pošalji sledeću komandu.
Klijent Redis server
| |
|------- SET key1 val1 ---->|
|<------ OK ---------------|
|------- SET key2 val2 ---->|
|<------ OK ---------------|
|------- GET key1 -------->|
|<------ val1 -------------|Ako se veliki broj zahteva šalje sekvencijalno, mrežno kašnjenje će značajno povećati ukupno vreme izvršenja zahteva, ako je jedno RTT vreme 1 milisekunda, 3 će biti 3 milisekunde. Sa Pipeline-om, možeš odjednom poslati 3 komande, ukupno vreme će biti samo 1 milisekunda.
@Service
public class RedisBatchService {
public void batchInsertUsers(List<User> users) {
// Pogrešan pristup bez Pipeline - vrlo sporo
// for (User user : users) {
// redisTemplate.opsForValue().set("user:" + user.getId(), user);
// }
// Pravi pristup sa Pipeline-om
redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
for (User user : users) {
String key = "user:" + user.getId();
byte[] keyBytes = key.getBytes();
byte[] valueBytes = serialize(user);
connection.set(keyBytes, valueBytes);
}
return null; // Pipeline ne zahteva povratnu vrednost
}
});
}
}Naravno, Pipeline ne treba biti što veći, preveliki će zauzeti previše memorije, obično se preporučuje da svaki Pipeline sadrži 1000 do 5000 komandi. Može se prilagoditi prema stvarnoj situaciji.
public void smartBatchInsert(List<String> data) {
int batchSize = 1000; // Empirijska vrednost, prilagodi prema veličini podataka
for (int i = 0; i < data.size(); i += batchSize) {
List<String> batch = data.subList(i, Math.min(i + batchSize, data.size()));
redisTemplate.executePipelined(new RedisCallback<Object>() {
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
for (String item : batch) {
connection.set(item.getBytes(), item.getBytes());
}
return null;
}
});
}
}U kojim scenama je pogodno koristiti Pipeline?
Kada je potrebno masovno ubacivati, ažurirati ili brisati podatke, ili kada treba izvršiti veliki broj sličnih komandi. Na primer: zagrevanje keša prilikom pokretanja sistema -> masovno učitavanje toplih podataka; na primer masovno ažuriranje statističkih podataka; na primer uvoz/izvoz velikih količina podataka; na primer masovno brisanje isteklih ili nevažećih keševa.
Znaš li za osnovne principe Pipeline-a?
Znam, u suštini je to ideja baferovanja. U praktičnom projektu Technical Pai, u RedisClient klasi sam enkapsulirao internu klasu PipelineAction za keširanje komandi.

add metoda pakuje komande u Runnable objekte i stavlja ih u List. Kada se izvrši execute metoda, poziva RedisTemplate-ovu executePipelined metodu za otvaranje cevovodnog modala i slanje više komandi Redis serveru.

Redis server nakon čitanja komandi iz ulaznog bafera, će prema RESP protokolu rastaviti komande, i zatim redom izvršiti te komande. Rezultati izvršenja će se upisati u izlazni bafer, i konačno svi rezultati će se odjednom vratiti klijentu.
typedef struct client {
sds querybuf; // Ulazni bafer
list *reply; // Lanac izlaznog bafera
unsigned long reply_bytes; // Veličina izlaznog bafera
} client;
- Java vodič za intervju (plaćeno) uključuje originalno pitanje intervjua od JD-ove iskustvene kolege 8: Razumevanje pipeline-a, u kojim scenama je pogodno koristiti pipeline? Znaš li za osnovne slojeve pipeline-a?
memo:1. juna 2025. godine, danas prijatelj u planetu je poslao poruku da je dobio ponudu od Best Thought, on je sa privatnog koledža, zadovoljan je ovim rezultatom, i takođe zahvaljuje Mianfan Nixian i praktičnim projektima planete, što ga je oslobodilo mutnih dana. Čestitam mu!

48.🌟Može li Redis realizovati distribuiranu bravu?
Distribuirana brava je mehanizam brave za kontrolu pristupa deljenim resursima više različitih procesa u distribuiranom sistemu. Može osigurati da u istom trenutku samo jedan čvor može pristupiti resursu, čime se izbjegavaju problemi istovremenosti u distribuiranim scenarijima.
Može se koristiti Redis SETNX komanda za realizaciju jednostavne distribuirane brave. Na primer SET key value NX PX 3000 kreira distribuiranu bravu sa nazivom key, vlasnikom brave je value. NX osigurava da se uspešno kreira samo ako ključ ne postoji, EX postavlja vreme isteka za sprečavanje mrtve brave.

Kako Redis osigurava da SETNX neće doći do konflikta?
Kada koristimo SET key value NX EX 30 komandu za zaključavanje, Redis će tretirati celu operaciju kao jednu atomičnu instrukciju. Budući da je obrada komandi Redis-a jedno-nitna, u istom trenutku se može izvršavati samo jedna komanda.
Na primer, dva klijenta A i B istovremeno zahtevaju istu bravu:
Klijent A: SET lock_key uuid_a NX EX 30
Klijent B: SET lock_key uuid_b NX EX 30Iako ova dva zahteva mogu stići do Redis servera skoro istovremeno, Redis će strogo po redu dolaska obrađivati. Pretpostavimo da je A-jev zahtev stigao prvi, Redis će prvo izvršiti A-jevu SET komandu, tada se lock_key postavlja na uuid_a.
Kada se obrađuje B-jev zahtev, zato što lock_key već postoji, NX uslov nije zadovoljen, tako da će B-jeva SET komanda neuspešno, vratiti NULL. Na taj način se osigurava da samo A može dobiti bravu.
Ključna tačka je u NX semantici: NOT EXISTS, uspešno se postavlja samo kada ključ ne postoji. Pri izvršenju ove komande, Redis će prvo proveriti da li ključ postoji, ako ne postoji tek će postaviti vrednost, ceo proces je atomičan i neće biti prekinut drugim komandama.
Koji problemi postoje kod SETNX, kako rešiti?
Kada koristite SETNX za kreiranje distribuiranih zaključavanja, iako možete izbeći mrtvo zaključavanje postavljanjem vremena isteka, dolazi do pogrešnog brisanja zaključavanja. Na primer, kada nit A dobije zaključavanje, vreme izvršavanja posla je duže, zaključavanje ističe. U tom trenutku nit B dobija zaključavanje, ali kada nit A završi poslovnu logiku, pokuša da obriše zaključavanje, i tada briše zaključavanje niti B.

Možete rešiti problem isteka zaključavanja kroz mehanizam automatskog produženja, na primer mehanizam pasara u Redissonu, koji pokreće periodični zadatak u pozadini koji svakog određenog vremena proverava da li zaključavanje još uvek drži trenutna nit, ako da, automatski produžava vreme isteka. Tako se izbegava mrtvo zaključavanje i sprečava prerano puštanje zaključavanja.

Koliko poznajete Redisson?
Redisson je Java klijent zasnovan na Redisu, ne samo jednostavno omotava Redis operacije, već pruža mnogo distribuiranih struktura podataka i usluga, na primer najčešće korišćeno distribuirano zaključavanje.
RLock lock = redisson.getLock("lock");
lock.lock();
try {
// uradi nešto
} finally {
lock.unlock();
}Distribuirano zaključavanje Redissona je mnogo kompletnije od SETNX, njegov mehanizam pasara nam omogućuje da preskočimo ručno postavljanje vremena isteka pri dobijanju zaključavanja, interno omotava periodični zadatak koji svakih 10 sekundi proverava, ako trenutna nit još uvek drži zaključavanje automatski produžava 30 sekundi.
private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
}
private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
RFuture<Long> ttlRemainingFuture;
if (leaseTime != -1) {
// ručno postavljanje vremena isteka
ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else {
// pokretanje mehanizma pasara, korišćenje podrazumevanog vremena isteka od 30 sekundi
ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
}
// obrada uspešnog dobijanja zaključavanja
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e != null) {
return;
}
// ako je uspešno dobijeno zaključavanje i pokrenut mehanizam pasara
if (ttlRemaining == null) {
if (leaseTime != -1) {
internalLockLeaseTime = unit.toMillis(leaseTime);
} else {
scheduleExpirationRenewal(threadId); // pokretanje pasara
}
}
});
return ttlRemainingFuture;
}Pored toga, Redisson takođe pruža distribuirani limitator protoka RRateLimiter, zasnovan na algoritmu token bucket-a, korišćen za kontrolu frekvencije pristupa u distribuiranoj okolini.
// API ograničenje protoka interfejsa
@RestController
public class ApiController {
@Autowired
private RedissonClient redissonClient;
@GetMapping("/api/data")
public ResponseEntity<?> getData() {
RRateLimiter limiter = redissonClient.getRateLimiter("api.data");
limiter.trySetRate(RateType.OVERALL, 100, 1, RateIntervalUnit.MINUTES);
if (limiter.tryAcquire()) {
// obrada zahteva
return ResponseEntity.ok(processData());
} else {
// okidač ograničenja protoka
return ResponseEntity.status(429).body("Rate limit exceeded");
}
}
}Možete li detaljnije objasniti mehanizam pasara u Redissonu?
Mehanizam pasara u Redissonu je mehanizam automatskog produženja koji rešava problem isteka distribuiranog zaključavanja.
Osnovni princip je ovakav: kada se pozove metoda lock() za zaključavanje, ako nije eksplicitno postavljeno vreme isteka, Redisson podrazumevano daje zaključavanju vreme isteka od 30 sekundi i istovremeno pokreće periodični zadatak "pasar" koji svakih 10 sekundi (podrazumevano 1/3 vremena isteka) proverava da li zaključavanje još uvek drži trenutna nit, ako da, automatski produžava vreme isteka na 30 sekundi.

// pseudokod prikazuje osnovnu logiku
private void renewExpiration() {
Timeout task = commandExecutor.getConnectionManager()
.newTimeout(new TimerTask() {
@Override
public void run(Timeout timeout) {
// koristi Lua skriptu za proveru i produženje
if (redis.call("get", lockKey) == currentThreadId) {
redis.call("expire", lockKey, 30);
// rekurzivni poziv, nastavlja sledeće produženje
renewExpiration();
}
}
}, 10, TimeUnit.SECONDS);
}Lua skripta za produženje će proveriti da li se vrednost zaključavanja poklapa sa trenutnom niti, ako da produžava vreme isteka. Tako se garantuje da samo pravi držalac zaključavanja može da ga produži.
Kada se pozove metoda unlock(), zadatak pasara se otkazuje. Ili ako se poslovna logika završi ali zaboravi unlock, pasar će nam automatski proveriti zaključavanje, ako zaključavanje više ne pripada trenutnoj niti, takođe će automatski zaustaviti produženje.
Tako nam ne treba briga što će se zaključavanje prerano pustiti zbog previše dugog izvršavanja posla, takođe se izbegava problem ručnog procenjivanja vremena isteka, i rešava se problem mrtvog zaključavanja u distribuiranoj okolini.
Da li je proces provere zaključavanja u mehanizmu pasara atomarna operacija?
Da, Redisson koristi Lua skriptu da garantuje atomičnost provere zaključavanja.

Redis pri izvršavanju Lua skripte, cele skripte tretira kao jednu naredbu, za to vreme ne izvršava druge naredbe. Stoga su hexists provera i expire produženje izvršeni atomarno.
Koliko poznajete Redlock?
Redlock je algoritam distribuiranog zaključavanja koji je predložio autor Redis antirez, korišćen za rešavanje problema single point of failure kada se jedan Redis instanca koristi kao distribuirano zaključavanje.
Osnovna ideja Redlock-a je ostvarivanje tolerancije na greške istovremenim dobijanjem zaključavanja na više potpuno nezavisnih Redis instanci.

Metoda minLocksAmount vraća locks.size()/2 + 1, što je tačno princip "većina odlučuje" koji zahteva Redlock algoritam. Metoda failedLocksLimit će izračunati dozvoljeni broj neuspešnih zaključavanja, osiguravajući da čak i ako deo instanci ne uspe, dok god broj uspešnih instanci prelazi polovinu smatra se da je dobijanje zaključavanja uspešno.
Crveno zaključavanje će pokušati redom da dobije zaključavanje sa svim Redis instancama i zabeleži broj uspešno dobijenih zaključavanja, kada broj dosegne minLocksAmount smatra se da je uspešno dobijeno, inače pušta već dobijena zaključavanja i vraća neuspeh.
Iako Redlock ima neke kontroverze, na primer problem drifta sata, problem cepanja mozga uzrokovanog mrežnom particijom, ipak je relativno zrelo rešenje distribuiranog zaključavanja.
Da li crveno zaključavanje može garantovati 100% uspešno zaključavanje?
Ne, Redlock ne može garantovati 100% uspešno zaključavanje, ovo je određeno osnovnim karakteristikama distribuiranog sistema.
Kada dođe do mrežne particije, klijent možda neće moći da komunicira sa dovoljno brojem Redis instanci. Na primer u implementaciji sa 5 Redis instanci, ako mrežna particija prouzrokuje da klijent može da pristupi samo 2 instance, ono kako god neće moći da zadovolji princip "većina odlučuje" crvenog zaključavanja, dobijanje zaključavanja će neizostano biti neuspešno.
public boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException {
// ...
for (ListIterator<RLock> iterator = locks.listIterator(); iterator.hasNext();) {
RLock lock = iterator.next();
boolean lockAcquired;
try {
lockAcquired = lock.tryLock(awaitTime, newLeaseTime, TimeUnit.MILLISECONDS);
} catch (RedisResponseTimeoutException e) {
lockAcquired = false; // neuspeh usled timeouta mreže
} catch (Exception e) {
lockAcquired = false; // neuspeh usled drugih izuzetaka
}
// ako preostali broj instanci koje se mogu pokušati nije dovoljan da dostigne većinu, direktno izađi
if (locks.size() - acquiredLocks.size() == failedLocksLimit()) {
break;
}
}
// provera da li je dostignut uslov većine
if (acquiredLocks.size() >= minLocksAmount(locks)) {
return true;
} else {
unlockInner(acquiredLocks);
return false; // nije dostignuta većina, neuspeh dobijanja
}
}Drift sata takođe može uticati na stopu uspeha. Iako su sve instance dostupne, ako postoji jasan drift sata između različitih Redis instanci, ili ako klijent potroši previše vremena u procesu dobijanja zaključavanja, na primer mrežno kašnjenje, GC pauza itd., može dovesti do toga da zaključavanje istekne pre nego što se dobijanje završi, tako da dobijanje ne uspe.
U praktičnim primenama, može se poboljšati stopa uspeha zaključavanja mehanizmom ponavljanja.
for (int i = 0; i < maxRetries; i++) {
if (redLock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS)) {
return true;
}
Thread.sleep(retryDelay);
}
return false;Da li se u projektu koristi distribuirano zaključavanje?
Projekat sam koristio distribuirano zaključavanje Redisson da bih osigurao sekvencijalno izvršavanje ažuriranja statusa procesa i da ne bude ometano od strane drugih servisa procesa.
Osnovna struktura
49.🌟Koje osnovne strukture podataka ima Redis?
Razlog što je Redis brz, pored čitanja i pisanja zasnovanog na memoriji, je i vrlo pažljivo dizajnirane osnovne strukture podataka. Redis ukupno ima 8 osnovnih osnovnih struktura podataka, ja ću reći po redu važnosti.

Prvo SDS, ovo je dinamički string koji je Redis sam implementirao, zadržava dužinu originalnog stringa C jezika, tako da je vremenska kompleksnost dobijanja dužine O(1), na toj osnovi takođe podržava dinamičko proširivanje i čuvanje binarnih podataka.

Zatim rečnik, na nižem nivou implementiran nizom+ulancanom listom heš tabele. Njegov dizajn je vrlo pametan, koristi dve heš tabele, inače se koristi prva, pri rehash se koristi druga, tako se može inkrementalno vršiti proširenje, ne blokira previše dugo.

Zatim komprimovana lista ziplist, ovaj dizajn je vrlo zanimljiv. Redis je uštedeo memoriju dizajnirao ovu kompaktnu strukturu podataka, sve elemente čuva kontinuirano u jednom delu memorije. Ali ima jedan fatalni problem "lančano ažuriranje", kada modificiramo jedan element, može prouzrokovati da svi naredni elementi moraju ponovo da se kodiraju, performanse će se drastično smanjiti.

Da bi rešio problem komprimovane liste, Redis kasnije dizajnirao quicklist. Ova ideja dizajna je pametna, deli ziplist u male komade, zatim sa dvostruko ulančanom listom povezuje ove komade. Tako se zadržava prednost uštede memorije ziplist-a, i izbegava problem lančanog ažuriranja, jer svaki maki komad ziplist-a nije prevelik.

Kasnije, Redis je ponovo dizajnirao listpack, ovo može reći da je savršena zamena za ziplist. Njegova najveća karakteristika je što svaki element beleži samo sopstvenu dužinu, ne beleži dužinu prethodnog elementa, tako se potpuno rešava problem lančanog ažuriranja. Redis 5.0 je već zamenio ziplist sa listpack.

Skok lista skiplist se uglavnom koristi u ZSet-u. Njegov dizajn je pametan, kroz više slojeve pokazivača ostvaruje brzo pretraživanje, prosečna vremenska kompleksnost je O(log N). U poređenju sa crveno-crnim stablom, implementacija skok liste je jednostavnija, i podržava pretragu opsega, ovo je važno za Redis-ove uređene skupove.

Takođe postoji skup celih brojeva intset, kada Set sadrži samo cele brojeve i mali broj elemenata se koristi, interno je sortiran niz, pretraga koristi binarnu pretragu.

Konačno dvostruko ulančana lista LinkedList, rane verzije Redis-a će se koristiti u List, ali u Redis 3.2 je zamenjena sa quicklist, jer je problem čiste ulančane liste što memorija nije kontinuirana, utiče na performanse CPU keša.

Možete li kratko predstaviti ulančanu listu?
Ulančana lista Redis-a linkedlist je struktura dvostruke ulančane liste bez ciklusa, slična LinkedList u Javi.
Čvorove predstavlja listNode, svaki čvor ima pokazivače na prethodni i sledeći čvor, prethodni od glavnog čvora i sledeći od repnog čvora su null.

Možete li detaljnije reći o skupu celih brojeva?
Skup celih brojeva je vrlo finezna struktura podataka u Redis-u, kada Set sadrži samo elemente celih brojeva i broj nije velik, podrazumevano ne prelazi 512, Redis će koristiti intset da čuva ove podatke.

Najzanimljiviji deo intset-a je mehanizam nadogradnje tipa. Ima tri načina kodiranja: 16-bitni, 32-bitni i 64-bitni, dinamicki će se prilagođavati prema veličini sačuvanih celih brojeva. Na primer inače su sačuvani mali celi brojevi, 16-bitno kodiranje je dovoljno, ali iznenada se ubaci jedan vrlo veliki broj, premašuje opseg 16-bitnog, u tom trenutku će ceo niz biti nadograđen na 32-bitno kodiranje.
typedef struct intset {
uint32_t encoding; // način kodiranja: 16-bitni, 32-bitni ili 64-bitni
uint32_t length; // broj elemenata
int8_t contents[]; // niz koji čuva elemente
} intset;Naravno, ova nadogradnja ima cenu, jer treba ponovo dodeliti memoriju i kopirati podatke, i nije reverzibilna, ali prednost je što može uštedeti memoriju, posebno pri čuvanju velikog broja malih celih brojeva.
Pored toga, svi elementi u nizu su poredjani od malog do velikog, tako se može koristiti binarna pretraga za lociranje elemenata, vremenska kompleksnost je O(log N).
Možete li reći osnovni princip zset-a?
ZSet je najkompleksniji tip podataka u Redis-u, ima dva načina osnovne implementacije: komprimovana lista i skok lista.

Kada je broj sačuvanih elemenata manji od 128, i veličina svih sačuvanih elemenata je manja od 64 bajta, Redis će koristiti način kodiranja komprimovane liste; inače koristi skok listu.
Naravno, oba uslova mogu biti prilagođena kroz parametre.
Kada se bira komprimovana lista kao osnovna implementacija, svaki element će koristiti dva susedna čvora za čuvanje: prvi čvor čuva člana elementa, drugi čvor čuva vrednost elementa.

Svi elementi su poređani po vrednosti od malog do velikog, mali su blizu glave liste, veliki su blizu repa liste.
Ali je nedostatak komprimovane liste taj što se pretraga može vršiti samo sekvencijalno, vremenska kompleksnost je O(N), a u najgorem slučaju, operacije umetanja i brisanja mogu izazvati lančano ažuriranje.
Kada je broj elemenata veliki ili su elementi veliki, Redis će koristiti način kodiranja skiplist; ovaj dizajn je vrlo pametan, istovremeno koristi dve strukture podataka:
typedef struct zset {
zskiplist *zsl; // skok lista
dict *dict; // rečnik
} zset;Skok lista čuva sve elemente poređane po vrednosti i podržava pretragu opsega (kao ZRANGE, ZRANGEBYSCORE), prosečna vremenska kompleksnost je O(log N). Heš tabela se koristi za čuvanje relacije mapiranja članova i vrednosti, vremenska kompleksnost pretrage je O(1).

Iako se istovremeno koriste dve strukture, one će deliti iste elemente člana i vrednosti kroz pokazivače, tako da neće trošiti dodatnu memoriju.
Da li znate zašto Redis 7.0 koristi listpack da zameni ziplist?
Odgovor: glavno je rešiti jedan osnovni problem komprimovane liste — lančano ažuriranje. U komprimovanoj listi, svaki čvor mora da beleži informaciju o dužini prethodnog čvora.

Kada se umece ili briše čvor, ako ova operacija prouzrokuje promenu dužine određenog čvora, tada sledeći čvorovi možda moraju ažurirati svoje polje "dužina prethodnog čvora". U najgorem slučaju, jedna operacija može pokrenuti ažuriranje cele liste, vremenska kompleksnost će se od O(1) degradirati do O(n²).
Dizajnerska filozofija listpack-a je potpuno drugačija. On dopušta svakom čvoru da beleži samo informaciju o sopstvenoj dužini, ne više zavisi od dužine prethodnog čvora. Tako se od korena izbegava problem lančanog ažuriranja.

Čvorovi u listpack-u ne čuvaju dužinu prethodnog čvora, već čuvaju tip kodiranja trenutnog čvora, podatke i dužinu.

Kako se dešava lančano ažuriranje?
Na primer ako imamo komprimovanu listu, gde nekoliko čvorova ima dužinu od 253 bajta. U kodiranju ziplist-a, ako je dužina prethodnog čvora manja od 254 bajta, nam treba samo 1 bajt da čuvamo ovu informaciju o dužini.

Ali ako ispred ovih čvorova umećemo čvor dužine 254 bajta, tada čvorovi koji su ranije trebali samo 1 bajt za čuvanje dužine sada trebaju 5 bajta da čuvaju informaciju o dužini. Ovo će prouzrokovati da informacije o dužini svih sledećih čvorova moraju biti ažurirane.

50.Zašto Redis ne koristi originalne stringove C jezika?
Prvo, stringovi C jezika su u stvari nizovi znakova, završavaju se sa \0, to znači ako sami podaci sadrže \0 bajt, biće pogrešno smatrani kao kraj stringa. Ali Redis treba da čuva različite tipove podataka, uključujući slike, serijalizovane objekte i druge binarne podatke, u ovim podacima je vrlo verovatno da sadrže \0.

Drugo, ako treba dobiti dužinu stringa, C jezik može samo pozvati funkciju strlen(), vremenska kompleksnost je O(N), jer mora da prođe kroz ceo string dok ne naiđe na \0.
Treće, stringovi C jezika neće automatski proveravati granice, ako u niz znakova upišete podatke koji premašuju njegov kapacitet, doći će do prekoračenja bafera.
Četvrto, stringovi C jezika ne podržavaju dinamičko proširenje, ako treba izmeniti sadržaj, mora ponovo dodeliti memoriju i kopirati podatke, trošak je velik.

SDS koji je dizajnirao Redis savršeno rešava ove probleme, dobijanje dužine može direktno kroz polje len, vremenska kompleksnost je O(1); polje free će zabeležiti preostali prostor, tako da Redis može dinamički proširivati prema strategiji preddodele, ne treba ponovo dodeliti memoriju pri dodavanju podataka; i ne zavisi od \0 završetka, može čuvati proizvoljne binarne podatke.
struct sds {
int len; // dužina stringa
int free; // preostali prostor
char buf[]; // niz znakova
}51.Jeste li proučavali izvorni kod rečnika Redis-a?
Da, proučavao sam. Rečnik Redis-a je podeljen na tri sloja, najspoljniji sloj je dict struktura, uključuje dve heš tabele ht[0] i ht[1], korišćene za čuvanje parova ključ-vrednost. Svaka heš tabela se sastoji od niza i ulančane liste, niz se koristi za brzo lociranje, ulančana lista za rešavanje heš kolizije.

// struktura najspoljnijeg sloja rečnika
typedef struct dict {
dictht ht[2]; // dve heš tabele! Ovo je ključno
long rehashidx; // rehash indeks, -1 znači da se ne vrši rehash
// ...
} dict;
// struktura heš tabele
typedef struct dictht {
dictEntry **table; // niz heš tabele
unsigned long size; // veličina heš tabele
unsigned long sizemask; // maska veličine heš tabele, korišćena za izračunavanje indeksa
unsigned long used; // broj postojećih čvorova u heš tabeli
} dictht;
// čvor heš tabele
typedef struct dictEntry {
void *key; // ključ
void *val; // vrednost
struct dictEntry *next; // pokazuje na sledeći čvor heš tabele, formira ulančanu listu
} dictEntry;Najbitnija karakteristika rečnika je inkrementalni rehash, ovo je mislio da je najgenijalniji deo. Kod tradicionalne heš tabele proširenje se vrši odjednom, ali Redis nije ovakav.
Kada faktor opterećenja aktivira uslov rehash, Redis će dodeliti novi prostor heš tabeli 1, obično dvostruko veći od heš tabele 0, zatim postavi rehashidx na 0.
Ključno je sledeće, Redis neće odjednom migrirati sve podatke iz heš tabele 0 u heš tabelu 1, već pri svakoj operaciji rečnika, migrira sve parove ključ-vrednost na poziciji rehashidx heš tabele 0. Nakon migracije jednog slota, rehashidx se povećava, dok se cela heš tabela 0 ne migrira.

Genijalnost ovog dizajna je što je trošak rehash-a raspodeljen na svaku operaciju. Pretpostavimo da imamo heš tabelu sa nekoliko miliona ključeva, ako se rehash odjednom može trebati nekoliko stotina milisekundi, za jednoniti Redis to je katastrofalno. Ali kroz inkrementalni rehash, svaka operacija dodaje samo mali dodatni trošak, korisnici skoro ne osećaju kašnjenje.
Za vreme rehash-a, operacija pretrage će prvo tražiti u heš tabeli 0, ako ne nadje onda traži u heš tabeli 1; ali novi ubačeni podaci će biti smešteni samo u heš tabelu 1. Tako se može garantovati potpunost podataka, i izbeći dupliranje podataka.
Šta raditi sa heš kolizijom?
Redis rešava heš koliziju metodom lančanja adresa, svaki slot heš tabele je u stvari glavni pokazivač ulančane liste, kada se heš vrednosti više ključeva mapiraju na isti slot, ti ključevi će biti povezani u obliku ulančane liste.
. Pri pretrazi treba proći kroz celu listu, u najgorem slučaju vremenska kompleksnost je O(n), ali obično su liste prilično kratke.
Pored toga, heš funkcija koju je dizajnirao Redis je u distribuciji prilično ravnomerna, može efektivno smanjiti pojavu heš kolizije.
/* MurmurHash2, autor Austin Appleby
* Napomena - ovaj kod pravi nekoliko pretpostavki o ponašanju vaše mašine -
* 1. Možemo čitati 4-bajtnu vrednost sa bilo koje adrese bez rušenja
* 2. sizeof(int) == 4
*
* I ima nekoliko ograničenja -
*
* 1. Neće raditi inkrementalno.
* 2. Neće dati iste rezultate na little-endian i big-endian
* mašinama.
*/
unsigned int dictGenHashFunction(const void *key, int len) {
/* 'm' i 'r' su konstante mešanja generisane van mreže.
Nisu zaista 'magične', samo se ispostavlja da dobro rade. */
uint32_t seed = dict_hash_function_seed;
const uint32_t m = 0x5bd1e995;
const int r = 24;
/* Inicijalizuj heš na 'nasumičnu' vrednost */
uint32_t h = seed ^ len;
/* Mešaj 4 bajta u heš */
const unsigned char *data = (const unsigned char *)key;
while(len >= 4) {
uint32_t k = *(uint32_t*)data;
k *= m;
k ^= k >> r;
k *= m;
h *= m;
h ^= k;
data += 4;
len -= 4;
}
/* Obradi poslednje nekoliko bajata ulaznog niza */
switch(len) {
case 3: h ^= data[2] << 16;
case 2: h ^= data[1] << 8;
case 1: h ^= data[0]; h *= m;
};
/* Napravi nekoliko finalnih mešanja heša da se osigura da su poslednji
* bajti dobro uključeni. */
h ^= h >> 13;
h *= m;
h ^= h >> 15;
return (unsigned int)h;
}52.🌟Da li poznajete skok listu?
Skok lista je vrlo pametna struktura podataka, na osnovu sortiranje ulančane liste formira više slojeva indeksa, najniži sloj sadrži sve podatke, svaki viši sloj, broj čvorova se prepolovi.

Njena osnovna ideja je "zamenuti prostor vremenom", kroz više slojeva indeksa preskočiti veliki broj čvorova, tako poboljšati efikasnost pretrage.

Svaki čvor ima 50% šanse da se pojavi samo u 1. sloju, 25% šanse da se pojavi u 2. sloju, itd. Pri pretrazi kreće od najvišeg sloja horizontalno, kada je vrednost sledećeg čvora veća od cilja, skoči jedan sloj niže, dok ne pronađe ciljni čvor.

Kako ubaciti čvor u skok listu?
Prvo pronaći poziciju umetanja, od najvišeg sloja glavnog čvora, u svakom sloju naći prethodni čvor pozicije gde treba ubaciti, koristeći niz update da zabeleži ove prethodne čvorove. Ovaj proces pretrage je isti kao obična pretraga, u svakom sloju kreće se desno dok vrednost sledećeg čvora ne bude veza od vrednosti za ubacivanje, zatim se spušta u sledeći sloj.
// beleži poziciju umetanja u svakom sloju
zskiplistNode *update[ZSKIPLIST_MAXLEVEL];
zskiplistNode *x;
int i, level;
// pretraga od najvišeg sloja
x = zsl->header;
for (i = zsl->level-1; i >= 0; i--) {
// horizontalno kretanje u trenutnom sloju, pronalaženje pozicije umetanja
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
sdscmp(x->level[i].forward->ele, ele) < 0)))
{
x = x->level[i].forward;
}
update[i] = x; // beleženje prethodnog čvora u svakom sloju
}Zatim nasumično generišemo broj slojeva novog čvora. Obično koristimo petlju, svaki put ima 50% šanse da nastavi gore, dok nasumično ne padne ili ne dosegne ograničenje maksimalnog broja slojeva.
// generisanje nasumičnog broja slojeva u Redis-u
int zslRandomLevel(void) {
int level = 1;
while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level < ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}
// generisanje broja slojeva novog čvora
level = zslRandomLevel();Nakon kreiranja novog čvora, od najnižeg sloja do najvišeg sloja novog čvora, u svakom sloju vrši standardna operacija umetanja ulančane liste. U ovom koraku treba iskoristiti prethodno zabeleženi niz update, ubaciti novi čvor na pravu poziciju, zatim ažurirati relaciju povezivanja prednjih i zadnjih pokazivača.
// ažuriranje naprednih pokazivača
for (i = 0; i < level; i++) {
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;
// ažuriranje informacija o rasponu
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
// ažuriranje raspona neuključenih slojeva
for (i = level; i < zsl->level; i++) {
update[i]->level[i].span++;
}
// ažuriranje nazadnih pokazivača
x->backward = (update[0] == zsl->header) ? NULL : update[0];
if (x->level[0].forward)
x->level[0].forward->backward = x;
else
zsl->tail = x;
// ažuriranje dužine skok liste
zsl->length++;Simulacija procesa umetanja skok liste, pretpostavimo da se podaci ubacuju redom 22, 19, 7, 3, 37, 11, 26.

Ako ubacimo čvor 67 u skok listu koja je već distribuirana sa 1, 14, 27, 31, 44, 56, 63, 70, 80, 91, proces umetanja je ovakav:

Zašto zset koristi skok listu?
Prvo, skok lista je prirodno uređena struktura podataka, pretraga, umetanje i brisanje mogu održavati vremensku kompleksnost O(log n).
Drugo, skok lista podržava pretragu opsega, nakon pronalaska početne pozicije može direktno prolaziti kroz donju ulančanu listu redom, zadovoljava ZRANGE dobijanje elemenata po rangiranju, ili ZRANGEBYSCORE dobijanje elemenata po opsegu vrednosti.
Mislim da je to razlog zašto mnogi ljudi biraju da se "rotljaju" u razvoju interneta, plata je mnogo viša od drugih industrija, iako je intenzitet rada razvoja interneta veliki, najmanje se može nagrađivati za trud.
Kako je definisana skok lista?
Skok lista je u suštini višeslojna ulančana lista, najniži sloj je uređena ulančana lista koja sadrži sve elemente, gornji sloj kao sloj indeksa, sadrži deo čvorova donjeg sloja; broj slojeva određuje nasumični algoritam, teoretski može biti neograničen visok.

Čvor skok liste sadrži score vrednost, objekat člana obj, jedan nazadni pokazivač backward, i niz slojeva level. Svaki sloj sadrži forward napredni pokazivač i span informaciju o rasponu.
typedef struct skiplistNode {
double score; // vrednost (koristi se za sortiranje)
robj *obj; // objekat podataka
struct skiplistNode *backward; // nazadni pokazivač
struct skiplistLevel {
struct skiplistNode *forward; // napredni pokazivač
unsigned int span; // raspon (udaljenost do sledećeg čvora)
} level[]; // niz slojeva
} skiplistNode;Skok lista sadrži pokazivače glavnog i repnog čvora, ukupan broj čvorova length i trenutni maksimalni broj slojeva level.
typedef struct skiplist {
struct skiplistNode *header, *tail; // glavni i repni čvor
unsigned long length; // broj čvorova
int level; // maksimalni broj slojeva
} skiplist;Šta koristi span raspon?
span beleži koliko je čvorova na najnižem nivou prešao između trenutnog čvora i sledećeg čvora, njegova glavna uloga je brzo pronalazenje ranga određene vrednosti u ZSet-u.

Na primer kada izvršimo ZRANK naredbu, ako nema span, treba od glavnog čvora proći kroz svaki čvor, dok ne pronađe ciljnu vrednost, vremenska kompleksnost je O(n).
// upit rangiranja bez span - O(n)
int getRankWithoutSpan(skiplist *zsl, double score, robj *obj) {
skiplistNode *x = zsl->header->level[0].forward;
int rank = 0;
while (x) {
if (x->score == score && equalStringObjects(x->obj, obj)) {
return rank + 1; // rangiranje kreće od 1
}
rank++;
x = x->level[0].forward;
}
return 0;
}Ali sa span, kada pretražujemo od višeg ka nižem sloju, možemo direktno preskočiti neke čvorove, brzo locirati opseg gde se nalazi ciljna vrednost. Tako se vremenska kompleksnost može smanjiti na O(log n).
long skiplistGetRank(skiplist *zsl, double score, robj *obj) {
skiplistNode *x = zsl->header;
unsigned long rank = 0;
// pretraga od najvišeg sloja
for (int i = zsl->level - 1; i >= 0; i--) {
while (x->level[i].forward &&
(x->level[i].forward->score < score ||
(x->level[i].forward->score == score &&
compareStringObjects(x->level[i].forward->obj, obj) < 0))) {
rank += x->level[i].span; // akumuliranje raspona
x = x->level[i].forward;
}
// pronalazak ciljnog čvora
if (x->level[i].forward &&
x->level[i].forward->score == score &&
equalStringObjects(x->level[i].forward->obj, obj)) {
rank += x->level[i].span;
return rank;
}
}
return 0;
}Zašto je efikasnost pretrage opsega skok liste veća od rečnika?
Rečnik raspoređuje čuvanje parova ključ-vrednost kroz heš funkciju, elementi su u memoriji raspoređeni neuređeno, nema nikakvog relacije redosleda. A skok lista je prirodno uređena struktura podataka, svi elementi su poređani od male ka velikoj vrednosti.

Kada treba vršiti pretragu opsega, rečnik mora proći kroz sve elemente, jedan po jedan proveriti da li je svaki element u određenom opsegu, vremenska kompleksnost je O(n). Na primer tražiti sve elemente čija je vrednost između 60 i 80, rečnik može samo skenirati celu heš tabelu, jer ne zna gde se nalaze elementi koji uslov zadovoljavaju.
A pretraga opsega skok liste je mnogo efikasnija. Prvo koristi O(log n) vreme da pronađe početnu poziciju opsega, zatim prolazi kroz donju uređenu ulančanu listu redom, dok ne premaši opseg. Ukupna vremenska kompleksnost je O(log n + k), gde je k veličina skupa rezultata. Ova razlika u efikasnosti je vrlo jasna kod velikih količina podataka.

Ovo je takođe važan razlog zašto Redis zset koristi skok listu a ne čistu heš tabelu, jer zset često treba ZRANGE, ZRANGEBYSCORE ove vrste operacija opsega. U stvari Redis zset je kombinacija skok liste i heš tabele: skok lista garantuje uređenost i podršku za pretragu opsega, heš tabela garantuje efikasnost pretrage O(1), one se međusobno dopunjuju.
53.Jesi li čuo za komprimovanu listu?
Odgovor: komprimovana lista je kompaktna struktura podataka koju je Redis dizajnirao za uštedu memorije, sve podatke će kontinuirano čuvati u jednom delu memorije.
Cela struktura sadrži informacije zaglavlja, kao što su ukupan broj bajtova, offset repa, broj čvorova, i kontinuirane podatke čvorova.

Kada su količina podataka list, hash i set mala i vrednosti nisu velike, osnova će koristiti komprimovanu listu za implementaciju.

Obično, svaki čvor sadrži tri dela: dužinu prethodnog čvora, tip kodiranja i stvarne podatke.

Dužina prethodnog čvora služi za podršku obilaska od nazad ka napred; kada je dužina prethodnog čvora manja od 254 bajta, koristi 1 bajt za čuvanje; inače koristi 5 bajtova, prvi bajt se postavlja na 254, preostala četri bajta čuvaju stvarnu dužinu.

Tip kodiranja će prema stvarnoj situaciji podataka odabrati najkompaktniji način čuvanja.

Ali komprimovana lista ima jedan fatalni problem, to je lančano ažuriranje. Kada umetanje ili brisanje čvora prouzrokuje promenu dužine određenog čvora, može uticati na polje "dužina prethodnog čvora" koje čuvaju svi sledeći čvorovi, u najgorem slučaju vremenska kompleksnost će se degradirati do O(n²).

Da li broj čvorova ziplist može premašiti 65535?
Neće.
Tip polja Zllen je uint16_t, maksimalna vrednost je 65535, to jest 16 na kvadrat, tako da broj čvorova komprimovane liste neće premašiti 65535.
Kada je broj čvorova manji od 65535, ovo polje će čuvati stvarni broj; inače je ovo polje fiksno na 65535, stvarni broj čuvanih elemenata treba izračunati prolaskom kroz čvorove jedan po jedan.
Koliko znaš o tipovima kodiranja ziplist?
Tipovi kodiranja ziplist su vrlo fine dizajnirani, uglavnom podeljeni na dve velike kategorije: kodiranje stringova i kodiranje celih brojeva, cilj je koristiti najmanji broj bajtova za čuvanje podataka.
Na primer mali celi brojevi od 0 do 12 su direktno kodirani u polju tipa, treba samo 1 bajt.
| Kodiranje | Dužina | Opis |
|---|---|---|
| 11000000 | 1bajt | int16_t tip celog broja, 2 bajta |
| 11010000 | 1bajt | int32_t tip celog broja, 4 bajta |
| 11100000 | 1bajt | int64_t tip celog broja, 8 bajtova |
| 11110000 | 1bajt | 24-bitni označeni celi broj, 3 bajta |
| 1111xxxx | 1bajt | opseg podataka [0-12], podaci su uključeni u kodiranje |

Za kodiranje stringova, postoje tri formata prema dužini stringa. Dužina manja od 63 bajtova koristi jedno-bajtno kodiranje koje počinje sa 00, preostalih 6 bita čuva dužinu. Dužina između 63 i 16383 koristi dvo-bajtno kodiranje koje počinje sa 01, preostalih 14 bitova čuva dužinu. Dužina veća od 16383 koristi kodiranje koje počinje sa 10, iza sledi 4 bajta koji čuvaju dužinu.
| Kodiranje | Dužina | Opis |
|---|---|---|
| 00pppppp | 1bajt | string 0-63 bajtova |
| 01pppppp qqqqqqqq | 2bajta | string 64-16383 bajtova |
| 10______ qqqqqqqq rrrrrrrr ssssssss tttttttt | 5bajtova | string 16384-4294967295 bajtova |

54.Jesi li čuo za quicklist?
quicklist je uveden u verziji Redis 3.2, specijalno korišćen za osnovnu implementaciju List, u stvari je hibridna struktura podataka, kombinuje prednosti komprimovane liste i dvostruke ulančane liste.

U ranim verzijama, List će koristiti dve različite osnovne strukture podataka prema broju i veličini elemenata, kada je broj elemenata mali ili su elementi mali, koristiće komprimovanu listu; inače dvostruku ulančanu listu.
Ali ovaj dizajn ima jedan problem, to je kada je broj elemenata u List velik, komprimovana lista će smanjiti performanse zbog lančanog ažuriranja, a dvostruka ulančana lista će zauzeti više memorije.
quicklist je pametno rešio ovaj problem deljenjem List na više malih ziplist, zatim povezivanjem u dvostruku ulančanu listu kroz pokazivače.

Podrazumevano, svaki ziplist može sačuvati 8KB podataka, ako je veličina svakog elementa tačno 1KB, tada jedan quicklist može sačuvati 8 elemenata. 80 takvih elemenata će biti podeljeno u 10 ziplist.
Tako se zadržava memorijska kompaktnost komprimovane liste, i smanjuje se broj pokazivača dvostruke ulančane liste, dodatno se smanjuju memorijski troškovi.

Pored toga, quicklist ima i jednu važnu karakteristiku, to je konfigurabilnost, može kontrolisati veličinu svakog čvora ziplist kroz faktor popunjavanja. Kada je faktor popunjavanja pozitivan broj, takođe može ograničiti maksimalni broj elemenata koji svaki ziplist sadrži.
# faktor popunjavanja, podrazumevano -2 (8KB)
list-max-ziplist-size 10Ako želite dodatno uštedeti memoriju, quicklist takođe podržava LZF kompresiju srednjih čvorova, kada je dubina kompresije 1, znači da su svi čvorovi osim po jednog čvora na početku i kraju kompresovani.
# dubina kompresije, podrazumevano 0 (bez kompresije)
list-compress-depth 1
Jesi li čuo za LZF algoritam kompresije?
LZF je brz algoritam za kompresiju bez gubitaka, uglavnom korišćen za smanjenje prostora za čuvanje podataka. Njegova osnovna ideja je ostvarivanje kompresije pronalaskom ponavljajućih podataka, kroz klizeći prozor pronalazi ponavljajuće sekvence bajtova, i zamenjuje te sekvence kraćim referencama.
Ulazni podaci: "hello world hello redis"
Korak 1: obrada "hello world "
- uspostavljanje rečnika, beleženje pozicija sekvenci bajtova
Korak 2: nailazi se na ponavljajuće "hello"
- pronalazi se prethodna pozicija "hello" u rečniku
- zamenjuje se sa (udaljenost, dužina) parom: (12, 5)
Izlaz: "hello world " + (12,5) + " redis"Dodatno
55.Ako u Redis-u ima 100 miliona ključeva, od kojih 100 hiljada ključeva počinje određenim poznatim prefiksom, kako da ih sve pronađeš?
Koristiću SCAN naredbu sa parametrom MATCH da rešim.
Na primer za pronaći ključeve koji počinju sa user:, može se izvršiti SCAN 0 MATCH user:* COUNT 1000.
Prednost SCAN je u tome što je zasnovan na inkrementalnoj iteraciji kursora, svaki put vraća samo mali broj rezultata, ne blokira server. Može se početi od kursora 0, svaki put obrati listu vraćenih ključeva, zatim sa sledećim vraćenim kursorom nastaviti skeniranje, dok se kursor ne vrati na 0 što znači da je skeniranje završeno.
Primer koda sa Spring Data Redis:
@Service
public class RedisKeyService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public List<String> scanKeysByPrefix(String prefix, int batchSize) {
List<String> keys = new ArrayList<>();
ScanOptions options = ScanOptions.scanOptions()
.match(prefix + "*")
.count(batchSize)
.build();
try (Cursor<String> cursor = redisTemplate.scan(options)) {
while (cursor.hasNext()) {
keys.add(cursor.next());
}
}
return keys;
}
}Nemoj koristiti KEYS naredbu, jer KEYS će blokirati Redis server dok prođe kroz sve ključeve, u produkcijskoj okolini izvršavanje KEYS nad 100 miliona ključeva je vrlo opasno.
56.Koju ulogu može igrati Redis u sceni sekundnog ubijanja?
Sekundno ubijanje je vrlo specifična poslovna scena, karakteristika mu je da u ekstremno kratkom vremenu veliki broj korisnika projuri u sistem, postavlja vrlo visoke zahteve za sposobnost istovremenog obrade, brzinu odziva i konzistentnost podataka. U ovoj sceni, Redis kao visokoperformansa baza podataka u memoriji može igrati višestruke ključne uloge.
Na primer pre početka sekundnog ubijanja, možemo unapred učitati informacije o proizvodu, podatke o zalihama itd. u Redis, tako da veliki broj zahteva za čitanje korisnika može direktno dobiti odgovor iz Redis-a, ne mora svaki put pristupati bazi podataka, tako se može mnogo smanjiti pritisak pristupa bazi podataka.

Drug, Redis ima prirodne prednosti u kontroli zaliha. Jedan od osnovnih problema sekundnog ubijanja je lako pretvaranje zaliha. Atomične operacije koje Redis pruža kao što su DECR, DECRBY itd. naredbe, mogu garantovati tačnost brojanja zaliha u okolini visokog istovremenosti.

Složeniju logiku može se realizovati kroz Lua skriptu, jer Lua skripta u Redis-u se izvršava atomično, tako može sadržati složene logike sudbe i operacije, na primer prvo proveriti da li je zaliha dovoljna, zatim smanjiti, ceo ovaj proces neće biti prekinut od strane drugih operacija.
Treće, distribuirano zaključavanje Redis-a može osigurati da operacije istovremenog kupovine istog proizvoda od strane više korisnika budu međusobno isključujuće, istovremeno garantuje konzistentnost podataka, takođe se može koristiti za sprečavanje duplog naručivanja korisnika.

Četvrto, ograničavanje protoka i poravnavanje vrha. U trenutku početka sekundnog ubijanja, možda istovremeno stiže nekoliko desetina hiljada zahteva, ako se ne kontroliše, lako dovodi do rušenja sistema. Redis može realizovati više algoritama ograničavanja protoka, na primer jednostavno ograničavanje protoka brojača, token bucket ili leak bucket algoritam itd.

Kroz algoritam ograničavanja protoka možemo kontrolisati broj zahteva koje sistem može obraditi u jedinici vremena, deo koji premašuje može stati u red ili biti direktno odbijen, tako se štiti stabilno pokretanje sistema.
Kako konkretno Redis vrši poravnavanje vrha?
Suština poravnavanja vrha je da se privremeni visoki protok zahteva spremsi, kroz redove, ograničavanje protoka itd. mehanizme, sistem procesira zahteve prihvatljivom brzinom.
Prvi korak je predgrevanje keša. Pre početka aktivnosti sekundnog ubijanja, unapred učitaj "vruće" podatke kao informacije o proizvodu u Redis. Tako kada korisnici pristupaju stranici proizvoda, mogu direktno čitati iz Redis, baza podataka gotovo da nema pritisak.

Drugi korak je uvođenje reda poruka, posebno za operacije pisanja kao naručivanje, ne treba korisniku previše dugo čekati, ali pozadinska obrada narudžbine, smanjenje zaliha itd. operacije su prilično teške. Zato možeš koristiti Redis List da napraviš red, ili direktno RocketMQ standardni posrednik poruka, korisnik nakon naručivanja odmah dobija "narudžbina je uspešno poslata", zatim baci podatke narudžbine u red, pozadinski servis polako konzumira. Tako se garantuje korisničko iskustvo, i izbegava da sistem bude srušen privremenim zahtevima za pisanje.

Treći korak, u aktivnost sekundnog ubijanja može se dodati faza odgovaranja na pitanja, samo korisnici koji tačno odgovore mogu učestovati u aktivnosti sekundnog ubijanja, tako se može maksimalno smanjiti nevažeći zahtevi.

Kompletnije rešenje za poravnavanje vrha sekundnog ubijanja:
@Service
public class SeckillServiceImpl implements SeckillService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private OrderService orderService;
@Autowired
private CommodityService commodityService;
/**
* Ulaz zahteva sekundnog ubijanja
*/
public Result seckill(Long userId, Long commodityId) {
// 1. ograničenje frekvencije zahteva korisnika
if (!countRateLimit("user:" + userId, 5, 60)) {
return Result.error("Prečesti zahtevi");
}
// 2. da li je proizvod u vremenu sekundnog ubijanja
if (!isInSeckillTime(commodityId)) {
return Result.error("Sekundno ubijanje nije počelo ili se završilo");
}
// 3. da li još uvek ima zaliha (brzo propadanje)
String stockKey = "seckill:stock:" + commodityId;
Integer stock = Integer.valueOf(redisTemplate.opsForValue().get(stockKey));
if (stock != null && stock <= 0) {
return Result.error("Proizvod je već rasprodat");
}
// 4. globalno ograničenje protoka
if (!acquireToken("global", 1000, 100)) {
// sistem je preopterećen, stavlja zahtev u red za odloženu obradu
enqueueDelayedRequest(userId, commodityId);
return Result.success("Zahtev sekundnog ubijanja je primljen, obrađuje se u redu");
}
// 5. provera da li je korisnik već kupio
if (hasUserBought(userId, commodityId)) {
return Result.error("Već ste kupili ovaj proizvod");
}
// 6. stavlja zahtev u red, vraća status čekanja
String requestId = generateRequestId(userId, commodityId);
enqueueRequest(userId, commodityId, requestId);
return Result.success("Zahtev sekundnog ubijanja je podnesen, molim sačekajte rezultat", requestId);
}
/**
* Asinhrona obrada zahteva sekundnog ubijanja
*/
@Scheduled(fixedRate = 50) // obrađuje seriju svakih 50ms
public void processSeckillQueue() {
String queueKey = "seckill:queue";
// batch obrada, kontrola brzine obrade
for (int i = 0; i < 10; i++) {
String requestJson = redisTemplate.opsForList().leftPop(queueKey);
if (requestJson == null) {
break;
}
SeckillRequest request = JSON.parseObject(requestJson, SeckillRequest.class);
try {
// izvršavanje osnovne logike sekundnog ubijanja
boolean success = doSeckill(request.getUserId(), request.getCommodityId());
// ažuriranje statusa zahteva, olakšava korisničku pretragu
String statusKey = "seckill:status:" + request.getRequestId();
redisTemplate.opsForValue().set(statusKey, success ? "SUCCESS" : "FAILED", 1, TimeUnit.HOURS);
} catch (Exception e) {
log.error("Obrada zahteva sekundnog ubijanja neuspešna", e);
// zabilježi status neuspheha
String statusKey = "seckill:status:" + request.getRequestId();
redisTemplate.opsForValue().set(statusKey, "ERROR", 1, TimeUnit.HOURS);
}
}
}
/**
* Osnovna logika sekundnog ubijanja
*/
private boolean doSeckill(Long userId, Long commodityId) {
// korišćenje Lua skripte za garantovanje atomičnih operacija
String script =
"-- provera zaliha\n" +
"local stockKey = KEYS[1]\n" +
"local stock = tonumber(redis.call('get', stockKey))\n" +
"if stock == nil or stock <= 0 then\n" +
" return 0\n" +
"end\n" +
"\n" +
"-- provera da li se kupuje ponovo\n" +
"local boughtKey = KEYS[2]\n" +
"local hasBought = redis.call('sismember', boughtKey, ARGV[1])\n" +
"if hasBought == 1 then\n" +
" return -1\n" +
"end\n" +
"\n" +
"-- smanjenje zaliha i evidencija kupovine\n" +
"redis.call('decr', stockKey)\n" +
"redis.call('sadd', boughtKey, ARGV[1])\n" +
"\n" +
"-- povratak uspeha\n" +
"return 1";
String stockKey = "seckill:stock:" + commodityId;
String boughtKey = "seckill:bought:" + commodityId;
Long result = (Long) redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Arrays.asList(stockKey, boughtKey),
userId.toString()
);
if (result == 1) {
// kreiranje narudžbine (može dodatno asinhronizovati)
createOrder(userId, commodityId);
return true;
}
return false;
}
// ostale pomoćne metode...
}Kako vrši ograničavanje protoka?
Ograničavanje protoka je za kontrolu brzine zahteva sistema, sprečava da sistem bude sružen previše zahteva.
Najejednostavniji metod Redis-a za ograničavanje protoka je zasnovan na brojaču fiksnog prozora. Na primer ograničiti korisnika na najviše 100 pristupa u minutu, koristimo INR naredbu da svakom korisniku damo brojač, ključ je rate_limit:KorisnikID:Vremenska oznaka minuta, svaki zahtev dodaje 1, istovremeno postavlja 60 sekundi isteka. Ako se broj preko 100 odbija zahtev.
// pseudokod
String key = "rate_limit:" + userId;
// pokušaj da dobiješ trenutni broj
Long count = redis.get(key);
// ako ključ ne postoji, postavi na 1 i postavi vreme isteka
if (count == null) {
redis.setex(key, 60, "1"); // 60 sekundi prozor
return true; // dozvoljen pristup
}
// ako broj nije premašio ograničenje
if (count < maxRequests) {
redis.incr(key);
return true; // dozvoljen pristup
}
return false; // odbijen pristupOvaj metod je jednostavan i grub, ali ima jedan problem to je da ivično vreme ima vrhove, na primer korisnik u 59. sekundi pristupio 100 puta, u 61. sekundi opet 100 puta, ekvivalentno 200 puta u 2 sekunde.
Drugi je metod ograničenja protoka kliznog prozora, realizovan kroz ZSET Redis-a, svaki put vremensku oznaku zahteva čuva kao score, zatim koristi ZREMRANGEBYSCORE da briše stare podatke izvan prozora, zatim ZCARD da broji broj zahteva u trenutnom prozoru. Tako je ograničenje protoka prilično ravnomerno.
// pseudokod
String key = "sliding_window:" + userId;
long now = System.currentTimeMillis();
// dodavanje trenutnog zahteva u uređeni skup, score je trenutna vremenska oznaka
redis.zadd(key, now, String.valueOf(now));
// uklanjanje podataka zahteva pre nego vremenskog prozora
redis.zremrangeByScore(key, 0, now - windowSize);
// postavljanje vremena isteka ključa, izbegavanje da hladni korisnici trajno zauzimaju memoriju
redis.expire(key, windowSize / 1000 + 1);
// dobijanje broja zahteva u trenutnom prozoru
Long count = redis.zcard(key);
return count <= maxRequests;U praktičnom razvoju, obično se koristi algoritam token bucket, to je kao kupovanje automobila u Pekingu/Šangaju, moraš dobiti broj da bi imao pravo, ako ne dobiješ samo čekaš sledeći put (😁).
Možeš u Redis čuvati dve vrednosti, jedan je broj tokena, drugi je vreme poslednjeg ažuriranja. Pri svakom zahtevu koristi Lua skriptu da izračuna koliko tokena treba dopuniti, zatim proveri da li ima dovoljno tokena.

-- Redis Lua skripta realizuje algoritam token bucket
local key = KEYS[1] -- ključ ograničenja protoka
local max_permits = tonumber(ARGV[1]) -- maksimalni broj tokena
local permits_per_second = tonumber(ARGV[2]) -- broj tokena koji se generišu po sekundi
local required_permits = tonumber(ARGV[3]) -- traženi broj tokena
-- dobijanje trenutnog vremena
local time = redis.call('time')
local now_micros = tonumber(time[1]) * 1000000 + tonumber(time[2])
-- dobijanje vremena poslednjeg ažuriranja i trenutno sačuvanog broja tokena
local last_micros = tonumber(redis.call('hget', key, 'last_micros') or 0)
local stored_permits = tonumber(redis.call('hget', key, 'stored_permits') or 0)
-- izračunavanje broja novih tokena u vremenskom intervalu
local interval_micros = now_micros - last_micros
local new_permits = interval_micros * permits_per_second / 1000000
stored_permits = math.min(max_permits, stored_permits + new_permits)
-- procena da li je tokena dovoljno
local result = 0
if stored_permits >= required_permits then
-- tokena dovoljno, ažuriranje broja tokena i vremena
stored_permits = stored_permits - required_permits
result = 1
end
-- ažuriranje podataka u Redis
redis.call('hset', key, 'last_micros', now_micros)
redis.call('hset', key, 'stored_permits', stored_permits)
redis.call('expire', key, 10) -- postavljanje vremena isteka, izbegavanje dugotrajnog zauzeća memorije
return result57.Nakon što se klijent sruši kako server Redis to oseti?
TCP keepalive je glavni mehanizam koji Redis koristi za detektovanje statusa konekcije klijenta, podrazumevana vrednost je 300 sekundi.
# za scenarije niskog kašnjenja, postavljeno na 60 sekundi, znači da se keepalive detekcija šalje svakih 60 sekundi
config set tcp-keepalive 60Kada klijent i server nemaju nikakvu interakciju podataka u određenom vremenu, Redis server će poslati ACK detekcioni paket TCP, ako se više puta ne odgovori, TCP stek će obavestiti Redis server da je konekcija prekinuta, zatim Redis server će očistiti odgovarajuće resurse konekcije, osloboditi konekciju.

Pored toga postoji parametar timeout, korišćen za kontrolu vremena isteka neaktivnosti konekcije klijenta.
# znači da 600 sekundi bez naredbe prekida konekciju
config set timeout 600Podrazumevana vrednost je 0, znači nikada ne prekidati konekciju; kada se postavi na nevrednost, ako klijent u određenom vremenu nije poslao nijednu naredbu, server će aktivno prekinuti konekciju.
Redis server će redovno proveravati da li su neaktivne konekcije istekle, frekvenciju provere kontroliše parametar hz; ovo pomaže oslobađanje resursa gde je klijent izašao anomalno ali TCP konekcija nije normalno zatvorena.
Različiti poolovi konekcija takođe imaju sopstvene mehanizme detekcije konekcije, na primer Jedis pool može omogućiti detekciju konekcije postavljanjem testOnBorrow i testWhileIdle.
# da li je omogućen pool konekcija
spring.redis.jedis.pool.enabled=true
# maksimalni broj konekcija pool (negativna vrednost znači bez ograničenja)
spring.redis.jedis.pool.max-active=200
# maksimalni broj neaktivnih konekcija pool
spring.redis.jedis.pool.max-idle=200
# minimalni broj neaktivnih konekcija pool
spring.redis.jedis.pool.min-idle=50
# maksimalno vreme čekanja blokiranja pool konekcije (negativna vrednost znači bez ograničenja)
spring.redis.jedis.pool.max-wait=3000
# interval provere neaktivnih konekcija (milisekunde)
spring.redis.jedis.pool.time-between-eviction-runs=60000Čitav mesec i po, drugo izdanje "mianzha nixi" Redis deo je konačno završeno, ovo izdanje se može reći da je potpuno preuređeno, svaki dan sam potrošio mnogo energije na njemu, može se reći da je promenjeno izgledom, ima osećaj "nakon dva meseca treba pogledati drugačije" (od 18.000 reči eksplodiralo do 46.000 reči, dodaci hrane istovremeno razlikuju visoku i nisku frekvenciju verziju).

Na internetu zapravo nije malo "osmostrukih", neki su i plaćeni, mislim da je to dobra stvar, može svima dati više izbora, ali kvalitet "mianzha nixi" ko razume taj razume.

"Mianzha nixi" drugo izdanje je na osnovu početne verzije gosta Sanfen iz zajednice, dodao Ergova sopstvenena razmišljanja, rezultat uključivanja više od 1000 stvarnih intervjua, i od 24. do 25. generacije, pa do 26. generacije, pomoglo mnogo drugova. Buduće 27. i 28. generacije će takođe imati koristi, tako da će dobiti željene ponude.
Pomoć svima je drago, i u procesu rekonstrukcije "mianzha nixi", sam takođe mnogo narastao, mnoge slabije osnove su ojačane, stoga drugo izdanje "mianzha nixi" nije samo poklon svima, već i zapis moje tehničke transformacije.



Često mislim da sam mirna osoba, ne želim da se takmičim s drugima, ne želim namerno da promovišem svoja dela.
Volim da čekam da cvetaju.
Ako mislite da je "mianzha nixi" dobro, možete reći mlađim da postoji ovaj besplatan materijal za učenje, pomoći mi u uspostavljanju reputacije.
Još uvek ću nastaviti da optimizem, nije sigurno kada će stići treće izdanje, ali ću se truditi.
Neka svi imaju svetlu budućnost.
Uključio sam Ergov napredni put u Javi, napredni put JVM, napredni put istovremenog programiranja, i sve verzije "mianzha nixi", pokriva Java osnovu, Java kolekciju, Java istovremenost, JVM, Spring, MyBatis, računarsku mrežu, operativni sistem, MySQL, Redis, RocketMQ, distribuirani sistem, mikroservise, dizajn šablon, Linux itd. 16 velikih tema, ukupno preko 400.000 reči, 2000+ ručno crtanih ilustracija, zaista je puno iskrenosti.
Ovaj put je i dalje tri verzije, svetla, tamna i epub verziju. Prikazaću jednu epub verziju, neki drugovi hitno trebaju ovu verziju, tako da ih zadovoljavam.

Detaljno objašnjenje 57 visokofrekventnih pitanja intervjua Redis, ovaj put ću sigurno prebiti intervjues, mislim da je sigurno (ručno pas). Organizovao: Chenmo Wang Er, klikni link reprikcije, autor: Sanfen, klikni link originala.
Ništa me ne zaustavlja – osim cilja, iako na obali ima ruža, zelene senke, mirne luke, ja sam brod bez vezi.
Serije sadržaja:
- Mianzha nixi Java SE deo 👍
- Mianzha nixi Java okvir kolekcije 👍
- Mianzha nixi Java istovremeno programiranje 👍
- Mianzha nixi JVM deo 👍
- Mianzha nixi Spring deo 👍
- Mianzha nixi Redis deo 👍
- Mianzha nixi MyBatis deo 👍
- Mianzha nixi MySQL deo 👍
- Mianzha nixi operativni sistem deo 👍
- Mianzha nixi računarska mreža deo 👍
- Mianzha nixi RocketMQ deo 👍
- Mianzha nixi distribuirani sistem deo 👍
- Mianzha nixi mikroservisi deo 👍
- Mianzha nixi dizajn šablon deo 👍
- Mianzha nixi Linux deo 👍
- Mianzha nixi OpenClaw deo 👍
- Mianzha nixi Skills deo 👍
