diff --git a/go.mod b/go.mod index e7130d6..6c82cea 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/jmoiron/sqlx v1.4.0 github.com/joho/godotenv v1.5.1 github.com/lib/pq v1.10.9 - github.com/redis/go-redis/v9 v9.16.0 + github.com/redis/go-redis/v9 v9.17.2 golang.org/x/crypto v0.42.0 google.golang.org/grpc v1.76.0 google.golang.org/protobuf v1.36.10 diff --git a/go.sum b/go.sum index aa22628..c4226ce 100644 --- a/go.sum +++ b/go.sum @@ -102,6 +102,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= +github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= diff --git a/internal/competition/config/config.go b/internal/competition/config/config.go index 7245d9c..3748ffd 100644 --- a/internal/competition/config/config.go +++ b/internal/competition/config/config.go @@ -21,6 +21,10 @@ type Config struct { DBPassword string DBName string JWTSecret string + RedisAddr string + RedisPassword string + RedisDB int + CacheEnabled bool } func Load() (*Config, error) { @@ -37,6 +41,10 @@ func Load() (*Config, error) { DBPassword: getEnv("POSTGRES_PASSWORD", "postgres"), DBName: getEnv("POSTGRES_DATABASE", "postgres"), JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-in-production"), + RedisAddr: getEnv("REDIS_ADDR", "localhost:6379"), + RedisPassword: getEnv("REDIS_PASSWORD", ""), + RedisDB: mustGetInt("REDIS_DB", 0), + CacheEnabled: mustGetBool("CACHE_ENABLED", true), }, nil } diff --git a/internal/competition/repository/postgres/competition.go b/internal/competition/repository/postgres/competition.go index a2778a5..45dc692 100644 --- a/internal/competition/repository/postgres/competition.go +++ b/internal/competition/repository/postgres/competition.go @@ -2,6 +2,7 @@ package postgres import ( "context" + "encoding/json" "fmt" "strings" "time" @@ -11,18 +12,31 @@ import ( "github.com/google/uuid" "github.com/jmoiron/sqlx" + "github.com/redis/go-redis/v9" +) + +const ( + competitionCachePrefix = "competition:" ) type CompetitionRepository struct { - db *sqlx.DB + db *sqlx.DB + redisClient *redis.Client + cacheEnabled bool } -func NewCompetitionRepository(db *sqlx.DB) repository.CompetitionRepository { +func NewCompetitionRepository(db *sqlx.DB, redisClient *redis.Client, cacheEnabled bool) repository.CompetitionRepository { return &CompetitionRepository{ - db: db, + db: db, + redisClient: redisClient, + cacheEnabled: cacheEnabled, } } +func (r *CompetitionRepository) cacheKey(id string) string { + return competitionCachePrefix + id +} + func (r *CompetitionRepository) Create(ctx context.Context, c *pb.Competition) (*pb.Competition, error) { query := `INSERT INTO competitions (state, title, description, image_url, start_time, end_time, type, participation_type) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at, updated_at` @@ -45,14 +59,42 @@ func (r *CompetitionRepository) Create(ctx context.Context, c *pb.Competition) ( c.CreatedAt = createdCompetition.CreatedAt c.UpdatedAt = createdCompetition.UpdatedAt + if r.cacheEnabled { + data, err := json.Marshal(c) + if err == nil { + r.redisClient.Set(ctx, r.cacheKey(c.Id), data, 10*time.Minute).Err() + } + } + return c, nil } func (r *CompetitionRepository) Get(ctx context.Context, id uuid.UUID) (*pb.Competition, error) { + if r.cacheEnabled { + val, err := r.redisClient.Get(ctx, r.cacheKey(id.String())).Result() + if err == nil { + var competition pb.Competition + if json.Unmarshal([]byte(val), &competition) == nil { + return &competition, nil + } + } + } + query := `SELECT id, state, title, description, image_url, start_time, end_time, type, participation_type, created_at, updated_at FROM competitions WHERE id = $1` var competition pb.Competition err := r.db.GetContext(ctx, &competition, query, id) - return &competition, err + if err != nil { + return nil, err + } + + if r.cacheEnabled { + data, err := json.Marshal(&competition) + if err == nil { + r.redisClient.Set(ctx, r.cacheKey(id.String()), data, 10*time.Minute).Err() + } + } + + return &competition, nil } func (r *CompetitionRepository) Update(ctx context.Context, c *pb.Competition) (*pb.Competition, error) { @@ -77,13 +119,25 @@ func (r *CompetitionRepository) Update(ctx context.Context, c *pb.Competition) ( } c.UpdatedAt = updatedCompetition.UpdatedAt + if r.cacheEnabled { + r.redisClient.Del(ctx, r.cacheKey(c.Id)).Err() + } + return c, nil } func (r *CompetitionRepository) Delete(ctx context.Context, id uuid.UUID) error { query := `DELETE FROM competitions WHERE id = $1` _, err := r.db.ExecContext(ctx, query, id) - return err + if err != nil { + return err + } + + if r.cacheEnabled { + r.redisClient.Del(ctx, r.cacheKey(id.String())).Err() + } + + return nil } func (r *CompetitionRepository) List(ctx context.Context, opts repository.ListCompetitionsOptions) ([]*pb.Competition, int, error) { @@ -149,5 +203,9 @@ func (r *CompetitionRepository) ChangeState(ctx context.Context, id uuid.UUID, s return nil, err } + if r.cacheEnabled { + r.redisClient.Del(ctx, r.cacheKey(id.String())).Err() + } + return r.Get(ctx, id) } \ No newline at end of file diff --git a/internal/competition/server/server.go b/internal/competition/server/server.go index 7b72d62..a63c278 100644 --- a/internal/competition/server/server.go +++ b/internal/competition/server/server.go @@ -14,6 +14,7 @@ import ( "github.com/jmoiron/sqlx" _ "github.com/lib/pq" + "github.com/redis/go-redis/v9" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) @@ -26,8 +27,9 @@ const ( type Server struct { grpcServer *grpc.Server - config *config.Config - db *sqlx.DB + config *config.Config + db *sqlx.DB + redisClient *redis.Client } func New(cfg *config.Config) *Server { @@ -43,6 +45,12 @@ func (s *Server) Start() error { } s.db = db + s.redisClient = redis.NewClient(&redis.Options{ + Addr: s.config.RedisAddr, + Password: s.config.RedisPassword, + DB: s.config.RedisDB, + }) + if err := s.registerGRPCServices(); err != nil { return fmt.Errorf("failed to register gRPC services: %w", err) } @@ -63,7 +71,7 @@ func (s *Server) Start() error { } func (s *Server) registerGRPCServices() error { - compRepo := postgres.NewCompetitionRepository(s.db) + compRepo := postgres.NewCompetitionRepository(s.db, s.redisClient, s.config.CacheEnabled) compService := service.NewCompetitionService(compRepo) compHandler := grpcHandlers.NewCompetitionHandler(compService) @@ -89,5 +97,11 @@ func (s *Server) Stop() { } } + if s.redisClient != nil { + if err := s.redisClient.Close(); err != nil { + log.Printf("failed to close redis client: %v", err) + } + } + log.Println("competition server stopped") }