Detaillierte Erklärung von Redis-Caching-Problemen

Cache-Penetration
Cache-Penetration bedeutet, dass die vom Client angeforderten Daten weder im Cache noch in der Datenbank vorhanden sind. Der Cache wird daher nie wirksam, und diese Anfragen treffen direkt auf die Datenbank.
Wenn ein böswilliger Benutzer zahlreiche Threads verwendet, um gleichzeitig auf nicht vorhandene Daten zuzugreifen, erreichen alle diese Anfragen die Datenbank, was wahrscheinlich zu einem Absturz der Datenbank führt.
Lösung
Leere Objekte cachen
Idee: Wenn ein Benutzer eine ID anfordert, die weder in Redis noch in der Datenbank existiert, cachen wir den Nullwert für diese ID direkt in Redis. Wenn der Benutzer diese ID erneut anfordert, trifft Redis einen Treffer (Nullwert-Treffer) und fragt nicht mehr die Datenbank ab.
Vorteile: einfach zu implementieren, leicht zu warten
Nachteile:

Zusätzlicher Speicherverbrauch (kann durch Hinzufügen eines TTL behoben werden)


- Kann kurzfristige Inkonsistenzen verursachen (durch Steuerung der TTL-Zeit kann dies bis zu einem gewissen Grad gemildert werden): Wenn der Nullwert gecacht wird und wir gerade den Wert in der Datenbank gesetzt haben, erhält der Benutzer bei einer Abfrage null zurück, obwohl der Wert tatsächlich in der Datenbank existiert. Dies führt zu einer Inkonsistenz (kann behoben werden, indem beim Einfügen von Daten die vorherigen Nulldaten automatisch überschrieben werden).
Bloom-Filter
Zwischen dem Client und Redis wird eine Bloom-Filter-Schicht eingefügt. Beim Zugriff des Benutzers prüft der Bloom-Filter, ob die Daten existieren. Wenn sie nicht existieren, wird die Anfrage direkt abgelehnt; wenn sie existieren, wird der normale Prozess verarbeitet.
Wie bestimmt ein Bloom-Filter, ob Daten existieren?

Der Bloom-Filter kann vereinfacht als Byte-Array verstanden werden, das Binärbits speichert. Um festzustellen, ob Daten in der Datenbank existieren, werden die Daten nicht direkt im Bloom-Filter gespeichert, sondern über einen Hash-Algorithmus wird ein Hashwert berechnet. Diese Hashes werden dann in Binärbits umgewandelt und im Bloom-Filter gespeichert. Bei der Prüfung, ob die Daten existieren, kann die entsprechende Position auf 0/1 geprüft werden (diese Art der Existenzprüfung ist eine probabilistische Statistik, nicht 100 % genau; wenn etwas als nicht vorhanden angezeigt wird, existiert es nicht unbedingt, sodass weiterhin ein Penetrationsrisiko besteht).
Vorteile: geringer Speicherverbrauch, keine redundanten Schlüssel (binär)
Nachteile:

komplex zu implementieren
Es besteht die Möglichkeit von Fehlurteilen (nicht unbedingt genau)


Cache leerer Objekte – Java-Implementierung
/**
* Cache-Penetration
*
* @param id
* @return
*/
public Shop queryWithPassThrough(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1. Shop-Cache aus Redis abfragen
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. Prüfen, ob der Eintrag existiert
if (StrUtil.isNotBlank(shopJson)) {
// 3. Vorhanden, direkt zurückgeben
return JSONUtil.toBean(shopJson, Shop.class);
}
// Prüfen, ob ein Nullwert getroffen wurde
if (shopJson != null) {
// Fehlermeldung zurückgeben
return null;
}
// 4. Nicht vorhanden, Datenbank nach ID abfragen
Shop shop = getById(id);
// 5. Nicht vorhanden, Fehler zurückgeben
if (shop == null) {
// Nullwert in Redis schreiben (Cache-Penetration)
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 6. Vorhanden, in Redis schreiben
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
// 7. Zurückgeben
return shop;
}
Cache-Avalanche
Cache-Avalanche bedeutet, dass eine große Anzahl von Cache-Schlüsseln gleichzeitig ungültig wird oder der Redis-Dienst gleichzeitig ausfällt, wodurch zahlreiche Anfragen die Datenbank erreichen und enormen Druck verursachen.


Lösung

Zufallswerte zum TTL verschiedener Schlüssel hinzufügen (um das Problem der gleichzeitigen Ungültigkeit zu lösen): Beim Cache-Warm-up müssen die Daten aus der Datenbank vorab in Batches in den Cache importiert werden. Da die Daten gleichzeitig importiert werden, ist der TTL-Wert dieser Daten identisch. Dies kann dazu führen, dass die Daten zu einem bestimmten Zeitpunkt gleichzeitig ablaufen, was zu einer Avalanche führt. Um dieses Problem zu lösen, können wir beim Importieren eine Zufallszahl zum TTL hinzufügen (z. B. TTL = 30 ± 1–5), sodass die Ablaufzeiten dieser Schlüssel über einen Zeitraum verteilt werden, anstatt gleichzeitig ungültig zu werden, und so eine Avalanche vermieden wird.
Redis-Cluster verwenden, um die Dienstverfügbarkeit zu verbessern (Redis-Ausfall beheben): Mithilfe des Redis-Sentinel-Mechanismus kann der Sentinel beim Ausfall eines Servers automatisch einen Ersatzserver auswählen, und Master und Slave können Daten synchronisieren, um hohe Redis-Verfügbarkeit zu gewährleisten.
Eine Degradierungs- und Rate-Limiting-Strategie zum Cache-Geschäft hinzufügen: z. B. schnelles Fehlschlagen, Dienstverweigerung und Verhindern, dass Anfragen an die Datenbank weitergeleitet werden.
Mehrstufige Caches zum Geschäft hinzufügen: Browser können Caches hinzufügen (üblicherweise für statische Ressourcen), der Reverse-Proxy-Server Nginx kann Caches hinzufügen, bei Nginx-Cache-Misses wird Redis angefragt, bei Redis-Cache-Misses erreicht die Anfrage die JVM, und innerhalb der JVM kann auch ein lokaler Cache eingerichtet werden, bevor schließlich die Datenbank erreicht wird.
Cache-Breakdown
Das Cache-Breakdown-Problem, auch als Hot-Key-Problem bekannt, tritt auf, wenn ein Schlüssel, der stark parallel zugriffen wird und eine komplexe Cache-Rekonstruktionslogik hat, plötzlich ausfällt. Zahlreiche Zugriffsanfragen haben in diesem Moment enorme Auswirkungen auf die Datenbank.
Cache-Rekonstruktion: Der Cache in Redis wird nach Ablauf ungültig und muss nach dem Ablaufen neu aus der Datenbank abgefragt und in Redis geschrieben werden. Der Prozess der Abfrage und Konstruktion von Daten aus der Datenbank kann komplex sein, z. B. Multi-Table-Join-Abfragen erfordern, und schließlich werden die Ergebnisse gecacht. Dieser Vorgang kann lange dauern (zehn oder sogar hunderte Millisekunden). Während dieser Zeit gibt es keinen Cache in Redis, und eingehende Anfragen greifen direkt auf die Datenbank zu.


Lösung
Mutex
Wenn eine Thread-Anfrage einen Cache-Miss feststellt, wird vor der Datenbankabfrage eine Sperre gesetzt und nach dem Schreiben in den Cache wieder freigegeben. Auf diese Weise erwerben auch andere Threads bei einem Cache-Miss den Mutex, bevor sie die Datenbank abfragen. Wenn der Erwerb fehlschlägt, pausiert der Thread für eine gewisse Zeit und versucht es dann erneut.
Offensichtlich können andere Threads erst nach dem Schreiben in den Cache Daten erhalten. Obwohl die Konsistenz gewährleistet werden kann, ist die Leistung relativ schlecht, und es kann zu Deadlocks führen.


Java-Implementierung


/**
* Sperre erwerben
*
* @param key
* @return
*/
private boolean tryLock(String key) {
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
return BooleanUtil.isTrue(flag);
}


/**
* Sperre freigeben
*
* @param key
*/
private void unlock(String key) {
stringRedisTemplate.delete(key);
}


/**
* Mutex
*
* @param id
* @return
*/
public Shop queryWithMutex(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1. Shop-Cache aus Redis abfragen
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. Prüfen, ob der Eintrag existiert
if (StrUtil.isNotBlank(shopJson)) {
// 3. Vorhanden, direkt zurückgeben
return JSONUtil.toBean(shopJson, Shop.class);
}
// Prüfen, ob ein Nullwert getroffen wurde
if (shopJson != null) {
// Fehlermeldung zurückgeben
return null;
}

// 4. Cache-Rekonstruktion implementieren
// 4.1 Mutex erwerben
String lockKey = LOCK_SHOP_KEY + id;
Shop shop = null;
try {
boolean isLock = tryLock(lockKey);
// 4.2 Prüfen, ob der Erwerb erfolgreich war
if (!isLock) {
// 4.3 Fehlgeschlagen, pausieren und erneut versuchen
Thread.sleep(50);
// rekursiv
return queryWithMutex(id);
}
// 4.4 Erfolgreich, Datenbank nach ID abfragen
shop = getById(id);
// Rekonstruktionsverzögerung simulieren
Thread.sleep(200);
// 5. Nicht vorhanden, Fehler zurückgeben
if (shop == null) {
// Nullwert in Redis schreiben (Cache-Penetration)
stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
// 6. Vorhanden, in Redis schreiben
stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
// 7. Mutex freigeben
unlock(lockKey);
}
// 8. Zurückgeben
return shop;
}
Testen wir es mit JMeter: Senden wir 1.000 Anfragen. Wir sehen, dass alle Anfragen durchgehen und die Datenbank nur einmal abgefragt wird.



Logischer Ablauf
Wie der Name schon sagt, handelt es sich nicht um ein tatsächliches Ablaufen – es kann als nie ablaufend betrachtet werden. Wenn wir Daten in Redis cachen, setzen wir kein TTL, sondern fügen beim Speichern der Daten ein Ablaufzeitfeld hinzu (kein TTL, basierend auf aktueller Zeit + Ablaufzeit, logisch verwaltete Zeit), sodass jeder Thread bei einer Abfrage einen Treffer erzielt und nur logisch prüfen muss, ob die Daten abgelaufen sind.
Wie in der folgenden Abbildung dargestellt: Wenn Thread 1 beim Abfragen des Caches feststellt, dass die logische Zeit abgelaufen ist, muss der Cache rekonstruiert werden, wobei zunächst der Mutex erworben wird. Anstatt die Cache-Rekonstruktion selbst durchzuführen, wird ein unabhängiger Thread gestartet. Nach Abschluss der Cache-Rekonstruktion wird die Sperre freigegeben, und Thread 1 gibt direkt die abgelaufenen Daten zurück. Wenn auch andere Threads einen Cache-Miss haben, wird bei fehlendem Mutex-Erwerb direkt die abgelaufene Daten zurückgegeben. Obwohl die Leistung gewährleistet ist, kann die Konsistenz nicht garantiert werden.


Java-Implementierung
/**
* Cache-Warm-up
*
* @param id
* @param expireSeconds logische Ablaufzeit
*/
public void saveShop2Redis(Long id, Long expireSeconds) throws InterruptedException {
// 1. Shop-Daten abfragen
Shop shop = getById(id);
Thread.sleep(200);
// 2. Logische Ablaufzeit kapseln
RedisData redisData = new RedisData();
redisData.setData(shop);
redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
// 3. In Redis schreiben
stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(redisData));
}

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

/**
* Logischer Ablauf
*
* @param id
* @return
*/
public Shop queryWithLogicalExpire(Long id) {
String key = CACHE_SHOP_KEY + id;
// 1. Shop-Cache aus Redis abfragen
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. Prüfen, ob der Eintrag existiert
if (StrUtil.isBlank(shopJson)) {
// 3. Cache-Miss, direkt zurückgeben
return null;
}
// 4. Treffer, JSON muss zuerst in Objekt deserialisiert werden
RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
LocalDateTime expireTime = redisData.getExpireTime();
// 5. Prüfen, ob abgelaufen
if (expireTime.isAfter(LocalDateTime.now())) {
// 5.1 Nicht abgelaufen, Shop-Informationen direkt zurückgeben
return shop;
}
// 5.2 Abgelaufen, muss rekonstruiert werden
// 6. Cache-Rekonstruktion
// 6.1 Mutex erwerben
String lockKey = LOCK_SHOP_KEY + id;
boolean isLock = tryLock(lockKey);
// 6.2 Prüfen, ob die Sperre erfolgreich erworben wurde
if (isLock) {
// 6.3 Erfolgreich, unabhängigen Thread für Cache-Rekonstruktion starten
CACHE_REBUILD_EXECUTOR.submit(() -> {
try {
// Cache rekonstruieren
this.saveShop2Redis(id, 20L);
} catch (Exception e) {
throw new RuntimeException(e);
} finally {
// Sperre freigeben
unlock(lockKey);
}
});
}
// 6.4 Abgelaufene Shop-Informationen zurückgeben
return shop;
}

Verwandte Artikel

Weitere Sonderangebote entdecken

  1. Kurznachrichtendienst (SMS) und E-Mail-Dienst

    50,000 email package starts as low as USD 1.99, 120 short messages start at only USD 1.00

phone Kontakt
Hi, I'm Alibaba Cloud AI Assistant!
I can help with questions and solutions.