339 lines
10 KiB
Go
339 lines
10 KiB
Go
//! Service de cache Redis pour optimiser les performances
|
|
//!
|
|
//! Ce service implémente une stratégie cache-aside avec invalidation automatique
|
|
//! pour améliorer les performances des requêtes fréquentes.
|
|
|
|
package services
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// CacheService gère le cache Redis avec différentes stratégies
|
|
type CacheService struct {
|
|
client *redis.Client
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// CacheConfig contient la configuration du cache
|
|
type CacheConfig struct {
|
|
DefaultTTL time.Duration
|
|
UserTTL time.Duration
|
|
TrackTTL time.Duration
|
|
RoomTTL time.Duration
|
|
}
|
|
|
|
// DefaultCacheConfig retourne la configuration par défaut du cache
|
|
func DefaultCacheConfig() *CacheConfig {
|
|
return &CacheConfig{
|
|
DefaultTTL: 5 * time.Minute,
|
|
UserTTL: 5 * time.Minute,
|
|
TrackTTL: 30 * time.Minute,
|
|
RoomTTL: 1 * time.Minute,
|
|
}
|
|
}
|
|
|
|
// NewCacheService crée un nouveau service de cache
|
|
func NewCacheService(client *redis.Client, logger *zap.Logger) *CacheService {
|
|
return &CacheService{
|
|
client: client,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Set stocke une valeur dans le cache avec TTL
|
|
func (c *CacheService) Set(ctx context.Context, key string, value interface{}, ttl time.Duration) error {
|
|
data, err := json.Marshal(value)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal value: %w", err)
|
|
}
|
|
|
|
err = c.client.Set(ctx, key, data, ttl).Err()
|
|
if err != nil {
|
|
c.logger.Error("Failed to set cache value",
|
|
zap.String("key", key),
|
|
zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
c.logger.Debug("Cache value set",
|
|
zap.String("key", key),
|
|
zap.Duration("ttl", ttl))
|
|
|
|
return nil
|
|
}
|
|
|
|
// Get récupère une valeur du cache
|
|
func (c *CacheService) Get(ctx context.Context, key string, dest interface{}) error {
|
|
data, err := c.client.Get(ctx, key).Result()
|
|
if err != nil {
|
|
if err == redis.Nil {
|
|
return ErrCacheMiss
|
|
}
|
|
c.logger.Error("Failed to get cache value",
|
|
zap.String("key", key),
|
|
zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
err = json.Unmarshal([]byte(data), dest)
|
|
if err != nil {
|
|
c.logger.Error("Failed to unmarshal cache value",
|
|
zap.String("key", key),
|
|
zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
c.logger.Debug("Cache value retrieved", zap.String("key", key))
|
|
return nil
|
|
}
|
|
|
|
// Delete supprime une valeur du cache
|
|
func (c *CacheService) Delete(ctx context.Context, key string) error {
|
|
err := c.client.Del(ctx, key).Err()
|
|
if err != nil {
|
|
c.logger.Error("Failed to delete cache value",
|
|
zap.String("key", key),
|
|
zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
c.logger.Debug("Cache value deleted", zap.String("key", key))
|
|
return nil
|
|
}
|
|
|
|
// DeletePattern supprime toutes les clés correspondant à un pattern
|
|
func (c *CacheService) DeletePattern(ctx context.Context, pattern string) error {
|
|
keys, err := c.client.Keys(ctx, pattern).Result()
|
|
if err != nil {
|
|
c.logger.Error("Failed to get keys by pattern",
|
|
zap.String("pattern", pattern),
|
|
zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
if len(keys) > 0 {
|
|
err = c.client.Del(ctx, keys...).Err()
|
|
if err != nil {
|
|
c.logger.Error("Failed to delete keys by pattern",
|
|
zap.String("pattern", pattern),
|
|
zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
c.logger.Debug("Cache keys deleted by pattern",
|
|
zap.String("pattern", pattern),
|
|
zap.Int("count", len(keys)))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Exists vérifie si une clé existe dans le cache
|
|
func (c *CacheService) Exists(ctx context.Context, key string) (bool, error) {
|
|
count, err := c.client.Exists(ctx, key).Result()
|
|
if err != nil {
|
|
c.logger.Error("Failed to check cache key existence",
|
|
zap.String("key", key),
|
|
zap.Error(err))
|
|
return false, err
|
|
}
|
|
|
|
return count > 0, nil
|
|
}
|
|
|
|
// SetUser met en cache les données d'un utilisateur
|
|
func (c *CacheService) SetUser(ctx context.Context, userID uuid.UUID, user interface{}, config *CacheConfig) error {
|
|
key := fmt.Sprintf("user:%d", userID)
|
|
return c.Set(ctx, key, user, config.UserTTL)
|
|
}
|
|
|
|
// GetUser récupère les données d'un utilisateur depuis le cache
|
|
func (c *CacheService) GetUser(ctx context.Context, userID uuid.UUID, dest interface{}) error {
|
|
key := fmt.Sprintf("user:%d", userID)
|
|
return c.Get(ctx, key, dest)
|
|
}
|
|
|
|
// DeleteUser supprime les données d'un utilisateur du cache
|
|
func (c *CacheService) DeleteUser(ctx context.Context, userID uuid.UUID) error {
|
|
key := fmt.Sprintf("user:%d", userID)
|
|
return c.Delete(ctx, key)
|
|
}
|
|
|
|
// SetTrack met en cache les métadonnées d'un track
|
|
func (c *CacheService) SetTrack(ctx context.Context, trackID int64, track interface{}, config *CacheConfig) error {
|
|
key := fmt.Sprintf("track:%d", trackID)
|
|
return c.Set(ctx, key, track, config.TrackTTL)
|
|
}
|
|
|
|
// GetTrack récupère les métadonnées d'un track depuis le cache
|
|
func (c *CacheService) GetTrack(ctx context.Context, trackID int64, dest interface{}) error {
|
|
key := fmt.Sprintf("track:%d", trackID)
|
|
return c.Get(ctx, key, dest)
|
|
}
|
|
|
|
// DeleteTrack supprime les métadonnées d'un track du cache
|
|
func (c *CacheService) DeleteTrack(ctx context.Context, trackID int64) error {
|
|
key := fmt.Sprintf("track:%d", trackID)
|
|
return c.Delete(ctx, key)
|
|
}
|
|
|
|
// SetRoom met en cache les données d'une room/conversation
|
|
func (c *CacheService) SetRoom(ctx context.Context, roomID int64, room interface{}, config *CacheConfig) error {
|
|
key := fmt.Sprintf("room:%d", roomID)
|
|
return c.Set(ctx, key, room, config.RoomTTL)
|
|
}
|
|
|
|
// GetRoom récupère les données d'une room depuis le cache
|
|
func (c *CacheService) GetRoom(ctx context.Context, roomID int64, dest interface{}) error {
|
|
key := fmt.Sprintf("room:%d", roomID)
|
|
return c.Get(ctx, key, dest)
|
|
}
|
|
|
|
// DeleteRoom supprime les données d'une room du cache
|
|
func (c *CacheService) DeleteRoom(ctx context.Context, roomID int64) error {
|
|
key := fmt.Sprintf("room:%d", roomID)
|
|
return c.Delete(ctx, key)
|
|
}
|
|
|
|
// SetMessages met en cache une liste de messages
|
|
func (c *CacheService) SetMessages(ctx context.Context, roomID int64, page int, messages interface{}, config *CacheConfig) error {
|
|
key := fmt.Sprintf("messages:%d:page:%d", roomID, page)
|
|
return c.Set(ctx, key, messages, config.RoomTTL)
|
|
}
|
|
|
|
// GetMessages récupère une liste de messages depuis le cache
|
|
func (c *CacheService) GetMessages(ctx context.Context, roomID int64, page int, dest interface{}) error {
|
|
key := fmt.Sprintf("messages:%d:page:%d", roomID, page)
|
|
return c.Get(ctx, key, dest)
|
|
}
|
|
|
|
// DeleteRoomMessages supprime tous les messages d'une room du cache
|
|
func (c *CacheService) DeleteRoomMessages(ctx context.Context, roomID int64) error {
|
|
pattern := fmt.Sprintf("messages:%d:*", roomID)
|
|
return c.DeletePattern(ctx, pattern)
|
|
}
|
|
|
|
// SetUserTracks met en cache la liste des tracks d'un utilisateur
|
|
func (c *CacheService) SetUserTracks(ctx context.Context, userID uuid.UUID, page int, tracks interface{}, config *CacheConfig) error {
|
|
key := fmt.Sprintf("user_tracks:%d:page:%d", userID, page)
|
|
return c.Set(ctx, key, tracks, config.TrackTTL)
|
|
}
|
|
|
|
// GetUserTracks récupère la liste des tracks d'un utilisateur depuis le cache
|
|
func (c *CacheService) GetUserTracks(ctx context.Context, userID uuid.UUID, page int, dest interface{}) error {
|
|
key := fmt.Sprintf("user_tracks:%d:page:%d", userID, page)
|
|
return c.Get(ctx, key, dest)
|
|
}
|
|
|
|
// DeleteUserTracks supprime tous les tracks d'un utilisateur du cache
|
|
func (c *CacheService) DeleteUserTracks(ctx context.Context, userID uuid.UUID) error {
|
|
pattern := fmt.Sprintf("user_tracks:%d:*", userID)
|
|
return c.DeletePattern(ctx, pattern)
|
|
}
|
|
|
|
// SetSearchResults met en cache les résultats de recherche
|
|
func (c *CacheService) SetSearchResults(ctx context.Context, query string, results interface{}, config *CacheConfig) error {
|
|
key := fmt.Sprintf("search:%s", query)
|
|
return c.Set(ctx, key, results, config.DefaultTTL)
|
|
}
|
|
|
|
// GetSearchResults récupère les résultats de recherche depuis le cache
|
|
func (c *CacheService) GetSearchResults(ctx context.Context, query string, dest interface{}) error {
|
|
key := fmt.Sprintf("search:%s", query)
|
|
return c.Get(ctx, key, dest)
|
|
}
|
|
|
|
// InvalidateUserCache invalide tout le cache lié à un utilisateur
|
|
func (c *CacheService) InvalidateUserCache(ctx context.Context, userID uuid.UUID) error {
|
|
patterns := []string{
|
|
fmt.Sprintf("user:%d", userID),
|
|
fmt.Sprintf("user_tracks:%d:*", userID),
|
|
fmt.Sprintf("user_sessions:%d:*", userID),
|
|
}
|
|
|
|
for _, pattern := range patterns {
|
|
if err := c.DeletePattern(ctx, pattern); err != nil {
|
|
c.logger.Error("Failed to invalidate user cache pattern",
|
|
zap.String("pattern", pattern),
|
|
zap.Error(err))
|
|
}
|
|
}
|
|
|
|
c.logger.Info("User cache invalidated", zap.String("user_id", userID.String()))
|
|
return nil
|
|
}
|
|
|
|
// InvalidateTrackCache invalide tout le cache lié à un track
|
|
func (c *CacheService) InvalidateTrackCache(ctx context.Context, trackID int64) error {
|
|
patterns := []string{
|
|
fmt.Sprintf("track:%d", trackID),
|
|
"search:*", // Invalider les recherches car le track peut apparaître dans les résultats
|
|
}
|
|
|
|
for _, pattern := range patterns {
|
|
if err := c.DeletePattern(ctx, pattern); err != nil {
|
|
c.logger.Error("Failed to invalidate track cache pattern",
|
|
zap.String("pattern", pattern),
|
|
zap.Error(err))
|
|
}
|
|
}
|
|
|
|
c.logger.Info("Track cache invalidated", zap.Int64("track_id", trackID))
|
|
return nil
|
|
}
|
|
|
|
// InvalidateRoomCache invalide tout le cache lié à une room
|
|
func (c *CacheService) InvalidateRoomCache(ctx context.Context, roomID int64) error {
|
|
patterns := []string{
|
|
fmt.Sprintf("room:%d", roomID),
|
|
fmt.Sprintf("messages:%d:*", roomID),
|
|
}
|
|
|
|
for _, pattern := range patterns {
|
|
if err := c.DeletePattern(ctx, pattern); err != nil {
|
|
c.logger.Error("Failed to invalidate room cache pattern",
|
|
zap.String("pattern", pattern),
|
|
zap.Error(err))
|
|
}
|
|
}
|
|
|
|
c.logger.Info("Room cache invalidated", zap.Int64("room_id", roomID))
|
|
return nil
|
|
}
|
|
|
|
// GetStats retourne les statistiques du cache
|
|
func (c *CacheService) GetStats(ctx context.Context) (*CacheStats, error) {
|
|
info, err := c.client.Info(ctx, "memory", "stats").Result()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parser les informations Redis pour extraire les métriques
|
|
stats := &CacheStats{
|
|
Info: info,
|
|
}
|
|
|
|
return stats, nil
|
|
}
|
|
|
|
// CacheStats contient les statistiques du cache
|
|
type CacheStats struct {
|
|
Info string `json:"info"`
|
|
}
|
|
|
|
// ErrCacheMiss est retourné quand une clé n'existe pas dans le cache
|
|
var ErrCacheMiss = fmt.Errorf("cache miss")
|
|
|
|
// Close ferme la connexion Redis
|
|
func (c *CacheService) Close() error {
|
|
return c.client.Close()
|
|
}
|