Dans les scénarios à haute concurrence, la mise en cache est un outil essentiel pour améliorer les performances du système. Cependant, des problèmes tels que la pénétration du cache, le passage à travers le cache et l'avalanche de cache peuvent surcharger le système. Cet article explore ces problèmes en détail et propose des solutions efficaces, illustrées par des exemples de code en Go.
1. Pénétration du cache (Cache Penetration)
Description du problème
La pénétration du cache se produit lorsque chaque requête ne parvient pas à trouver de correspondance dans le cache, ce qui oblige à interroger la base de données à chaque fois. Cela peut entraîner une augmentation significative de la pression sur la base de données.
Solution
Pour les données inexistantes, stockez une valeur nulle dans le cache pour éviter les requêtes répétées et inutiles vers la base de données. Il est également crucial de définir une durée d'expiration pour ces valeurs nulles mises en cache afin d'éviter une consommation inutile de mémoire.
// Exemple de code
func fetchDataFromCacheOrDB(dataKey string) (string, error) {
// Tenter de récupérer depuis le cache
cachedData, cacheErr := distributedCache.Get(dataKey)
if cacheErr == nil {
return cachedData, nil
}
// Si le cache est vide ou expire, récupérer depuis la base de données
dbData := queryFromDatabase(dataKey)
// Enregistrer les données récupérées dans le cache avec une durée d'expiration
if dbData != "" {
distributedCache.Set(dataKey, dbData, cacheExpiryDuration)
} else {
// Pour les données inexistantes, enregistrer une valeur nulle avec une courte durée d'expiration
distributedCache.Set(dataKey, "", shortCacheExpiry)
}
return dbData, nil
}
2. Passage à travers le cache (Cache Breakdown)
Description du problème
Dans des conditions de haute concurrence, si plusieurs requêtes interrogent simultanément la même clé de cache et que cette clé expire juste à ce moment-là, cela peut entraîner une surcharge de la base de données car de nombreuses requêtes accèdent directement à la base de données.
Solution
Mettez en œuvre un mécanisme de verrouillage. Seul le premier thread à obtenir le verrou interrogera la base de données. Après avoir reçu la réponse de la base de données, il mettra à jour le cache. Les autres threads, après avoir acquis le verrou, devront interroger à nouveau le cache pour éviter les accès répétés à la base de données.
// Exemple de code
var mutexMap = sync.Map{} // Pour stocker les verrous par clé
func fetchDataWithLocking(dataKey string) (string, error) {
if val, ok := mutexMap.Load(dataKey); ok {
// Si un verrou existe déjà, attendez qu'il soit libéré
lock, ok := val.(*sync.Mutex)
if ok {
lock.Lock()
defer lock.Unlock()
}
} else {
// Créez et acquérez un nouveau verrou pour cette clé
newMutex := &sync.Mutex{}
newMutex.Lock()
if _, loaded := mutexMap.LoadOrStore(dataKey, newMutex); loaded {
// Si le verrou a été créé par un autre thread pendant notre tentative, libérez le nouveau verrou et attendez le verrou existant
newMutex.Unlock()
if val, ok := mutexMap.Load(dataKey); ok {
if lock, ok := val.(*sync.Mutex); ok {
lock.Lock()
defer lock.Unlock()
}
}
} else {
// Nous avons réussi à stocker le nouveau verrou
defer mutexMap.Delete(dataKey) // Supprimez le verrou une fois l'opération terminée
defer newMutex.Unlock()
}
}
// Tenter de récupérer depuis le cache
cachedData, cacheErr := distributedCache.Get(dataKey)
if cacheErr == nil {
return cachedData, nil
}
// Récupérer depuis la base de données
dbData := queryFromDatabase(dataKey)
// Enregistrer les données récupérées dans le cache avec une durée d'expiration
if dbData != "" {
distributedCache.Set(dataKey, dbData, cacheExpiryDuration)
}
return dbData, nil
}
3. Avalanche de cache (Cache Avalanche)
Description du problème
Lorsque de nombreuses données du cache expirent simultanément, un grand nombre de requêtes accèdent directement à la base de données backend, ce qui peut potentiellement entraîner la défaillance de la base de données.
Solutions
- Utiliser des clusters pour réduire la probabilité de défaillance.
- Implémenter des limites de débit (rate limiting) et des dégradations (circuit breakers) pour protéger les services backend.
- Définir des durées d'expiration de cache raisonnables et aléatoires pour disperser les moments d'expiration.
- Pré-charger les données fréquemment consultées (hot data) pour rafraîchir le cache à l'vaance.
- Ajouter une composante aléatoire à l'expiratoin du cache pour éviter les expirations simultanées.
- Utiliser un système de cache à plusieurs niveaux (par exemple, cache local et cache distribué).
- Mettre en place une surveillance en temps réel et des alertes pour détecter les anomalies rapidement et prendre des mesures.
// Exemple de code
import (
"math/rand"
"time"
)
func calculateRandomizedExpiration() time.Duration {
// Ajouter une variation aléatoire à la durée d'expiration par défaut
randomOffset := time.Duration(rand.Intn(300)) * time.Second // Variation jusqu'à 5 minutes
return cacheExpiryDuration + randomOffset
}
func fetchDataWithRandomExpiration(dataKey string) (string, error) {
// Tenter de récupérer depuis le cache
cachedData, cacheErr := distributedCache.Get(dataKey)
if cacheErr == nil {
return cachedData, nil
}
// Récupérer depuis la base de données
dbData := queryFromDatabase(dataKey)
// Enregistrer les données récupérées dans le cache avec une expiration aléatoire
if dbData != "" {
distributedCache.Set(dataKey, dbData, calculateRandomizedExpiration())
} else {
// Pour les données inexistantes, enregistrer une valeur nulle avec une courte durée d'expiration
distributedCache.Set(dataKey, "", shortCacheExpiry)
}
return dbData, nil
}
4. Résolution du problème d'expiration concentrée des données "chaudes"
Description du problème
Lorsque les données "chaudes" expirent de manière concentrée, cela peut entraîner un grand nombre de requêtes accédant simultanément à la base de données, provoquant un pic de charge sur celle-ci.
Solutions
- Définir des durées d'expiration différentes pour disperser les moments d'expiration.
- Utiliser un mécanisme de verrouillage pour garantir qu'un seul thread met à jour le cache.
- Implémenter une stratégie de "ne jamais expirer" pour les données critiques, où un travailleur en arrière-plan met à jour le cache avant son expiration.
// Exemple de code
const neverExpire = 0 // Une valeur spéciale pour indiquer une expiration indéfinie
func fetchHotDataWithUpdate(dataKey string) (string, error) {
// Tenter de récupérer depuis le cache
cachedData, cacheErr := distributedCache.Get(dataKey)
if cacheErr == nil {
return cachedData, nil
}
// Utiliser le même mécanisme de verrouillage que pour le passage à travers le cache
if val, ok := mutexMap.Load(dataKey); ok {
lock, ok := val.(*sync.Mutex)
if ok {
lock.Lock()
defer lock.Unlock()
}
} else {
newMutex := &sync.Mutex{}
newMutex.Lock()
if _, loaded := mutexMap.LoadOrStore(dataKey, newMutex); loaded {
newMutex.Unlock()
if val, ok := mutexMap.Load(dataKey); ok {
if lock, ok := val.(*sync.Mutex); ok {
lock.Lock()
defer lock.Unlock()
}
}
} else {
defer mutexMap.Delete(dataKey)
defer newMutex.Unlock()
}
}
// Vérifier à nouveau le cache après avoir acquis le verrou
cachedData, cacheErr = distributedCache.Get(dataKey)
if cacheErr == nil {
return cachedData, nil
}
// Récupérer depuis la base de données
dbData := queryFromDatabase(dataKey)
// Enregistrer les données récupérées dans le cache avec une expiration indéfinie
if dbData != "" {
distributedCache.Set(dataKey, dbData, neverExpire)
// Planifier une tâche pour mettre à jour périodiquement ce cache
go scheduleCacheRefresh(dataKey, dbData)
}
return dbData, nil
}
// Fonction fictive pour planifier le rafraîchissement du cache
func scheduleCacheRefresh(dataKey string, currentData string) {
// Logique pour mettre à jour le cache avant son expiration (par exemple, après 23 heures si l'expiration est de 24 heures)
// Cette fonction serait implémentée avec un mécanisme de planification plus robuste.
}
En appliquant ces stratégies, vous pouvez mieux gérer les problèmes liés à la mise en cache, garantissant ainsi la stabilité et les performances de votre système. Le choix de la solution appropriée dépendra de votre scénario métier spécifique et de vos exigences.