turash/dev_guides/07_redis_go.md
Damir Mukimov 4a2fda96cd
Initial commit: Repository setup with .gitignore, golangci-lint v2.6.0, and code quality checks
- Initialize git repository
- Add comprehensive .gitignore for Go projects
- Install golangci-lint v2.6.0 (latest v2) globally
- Configure .golangci.yml with appropriate linters and formatters
- Fix all formatting issues (gofmt)
- Fix all errcheck issues (unchecked errors)
- Adjust complexity threshold for validation functions
- All checks passing: build, test, vet, lint
2025-11-01 07:36:22 +01:00

426 lines
9.6 KiB
Markdown

# Redis Go Client Development Guide
**Library**: `github.com/redis/go-redis/v9`
**Used In**: MVP - Caching, sessions, match results
**Purpose**: Redis client for caching and real-time data
---
## Where It's Used
- **Match results caching** (TTL: 5-15 minutes)
- **Session management**
- **Rate limiting**
- **Pub/sub for real-time updates** (future)
---
## Official Documentation
- **GitHub**: https://github.com/redis/go-redis
- **GoDoc**: https://pkg.go.dev/github.com/redis/go-redis/v9
- **Redis Docs**: https://redis.io/docs/
- **Commands Reference**: https://redis.io/commands/
---
## Installation
```bash
go get github.com/redis/go-redis/v9
```
---
## Key Concepts
### 1. Client Setup
```go
import (
"context"
"github.com/redis/go-redis/v9"
)
func NewRedisClient(addr, password string, db int) (*redis.Client, error) {
rdb := redis.NewClient(&redis.Options{
Addr: addr, // "localhost:6379"
Password: password, // "" for no password
DB: db, // 0
})
// Verify connection
ctx := context.Background()
if err := rdb.Ping(ctx).Err(); err != nil {
return nil, err
}
return rdb, nil
}
```
### 2. Basic Operations
```go
ctx := context.Background()
// Set value
err := rdb.Set(ctx, "key", "value", 0).Err()
// Set with expiration
err := rdb.Set(ctx, "key", "value", 5*time.Minute).Err()
// Get value
val, err := rdb.Get(ctx, "key").Result()
if err == redis.Nil {
// Key doesn't exist
}
// Delete
err := rdb.Del(ctx, "key").Err()
// Check if key exists
exists, err := rdb.Exists(ctx, "key").Result()
// Set if not exists (NX)
err := rdb.SetNX(ctx, "key", "value", time.Minute).Err()
```
### 3. Hash Operations
```go
// Set hash field
err := rdb.HSet(ctx, "business:123", "name", "Factory A").Err()
// Set multiple hash fields
err := rdb.HSet(ctx, "business:123", map[string]interface{}{
"name": "Factory A",
"email": "contact@factorya.com",
}).Err()
// Get hash field
name, err := rdb.HGet(ctx, "business:123", "name").Result()
// Get all hash fields
business, err := rdb.HGetAll(ctx, "business:123").Result()
// Increment hash field
count, err := rdb.HIncrBy(ctx, "business:123", "match_count", 1).Result()
```
### 4. List Operations
```go
// Push to list
err := rdb.LPush(ctx, "matches:123", "match1").Err()
// Pop from list
match, err := rdb.RPop(ctx, "matches:123").Result()
// Get list length
length, err := rdb.LLen(ctx, "matches:123").Result()
// Get list range
matches, err := rdb.LRange(ctx, "matches:123", 0, 10).Result()
```
### 5. Set Operations
```go
// Add to set
err := rdb.SAdd(ctx, "businesses", "business:123").Err()
// Check if member exists
exists, err := rdb.SIsMember(ctx, "businesses", "business:123").Result()
// Get set members
members, err := rdb.SMembers(ctx, "businesses").Result()
// Remove from set
err := rdb.SRem(ctx, "businesses", "business:123").Err()
```
### 6. Sorted Sets (for ranking)
```go
// Add to sorted set
err := rdb.ZAdd(ctx, "match_scores", redis.Z{
Score: 95.5,
Member: "match:123",
}).Err()
// Get top matches (by score)
matches, err := rdb.ZRevRange(ctx, "match_scores", 0, 9).Result()
// Get with scores
matchesWithScores, err := rdb.ZRevRangeWithScores(ctx, "match_scores", 0, 9).Result()
```
### 7. JSON Operations
```go
import "github.com/redis/go-redis/v9"
// Set JSON
err := rdb.JSONSet(ctx, "business:123", "$", business).Err()
// Get JSON
var business Business
err := rdb.JSONGet(ctx, "business:123", "$").Scan(&business)
```
---
## MVP-Specific Patterns
### Match Results Caching
```go
type MatchCache struct {
client *redis.Client
}
// Cache match results with TTL
func (c *MatchCache) CacheMatches(ctx context.Context, flowID string, matches []Match, ttl time.Duration) error {
key := fmt.Sprintf("matches:%s", flowID)
// Serialize matches
data, err := json.Marshal(matches)
if err != nil {
return err
}
return c.client.Set(ctx, key, data, ttl).Err()
}
// Get cached matches
func (c *MatchCache) GetMatches(ctx context.Context, flowID string) ([]Match, error) {
key := fmt.Sprintf("matches:%s", flowID)
data, err := c.client.Get(ctx, key).Bytes()
if err == redis.Nil {
return nil, ErrCacheMiss
}
if err != nil {
return nil, err
}
var matches []Match
if err := json.Unmarshal(data, &matches); err != nil {
return nil, err
}
return matches, nil
}
// Invalidate cache
func (c *MatchCache) Invalidate(ctx context.Context, flowID string) error {
key := fmt.Sprintf("matches:%s", flowID)
return c.client.Del(ctx, key).Err()
}
// Invalidate pattern (all matches for a business)
func (c *MatchCache) InvalidatePattern(ctx context.Context, pattern string) error {
keys, err := c.client.Keys(ctx, pattern).Result()
if err != nil {
return err
}
if len(keys) > 0 {
return c.client.Del(ctx, keys...).Err()
}
return nil
}
```
### Session Management
```go
type SessionManager struct {
client *redis.Client
}
func (s *SessionManager) CreateSession(ctx context.Context, userID string) (string, error) {
sessionID := uuid.New().String()
key := fmt.Sprintf("session:%s", sessionID)
err := s.client.Set(ctx, key, userID, 24*time.Hour).Err()
if err != nil {
return "", err
}
return sessionID, nil
}
func (s *SessionManager) GetSession(ctx context.Context, sessionID string) (string, error) {
key := fmt.Sprintf("session:%s", sessionID)
userID, err := s.client.Get(ctx, key).Result()
if err == redis.Nil {
return "", ErrSessionNotFound
}
return userID, err
}
func (s *SessionManager) DeleteSession(ctx context.Context, sessionID string) error {
key := fmt.Sprintf("session:%s", sessionID)
return s.client.Del(ctx, key).Err()
}
```
### Rate Limiting
```go
type RateLimiter struct {
client *redis.Client
}
func (r *RateLimiter) Allow(ctx context.Context, key string, limit int, window time.Duration) (bool, error) {
// Sliding window rate limiting
now := time.Now().Unix()
windowStart := now - int64(window.Seconds())
pipe := r.client.Pipeline()
// Remove old entries
pipe.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(windowStart, 10))
// Count current requests
pipe.ZCard(ctx, key)
// Add current request
pipe.ZAdd(ctx, key, redis.Z{
Score: float64(now),
Member: uuid.New().String(),
})
// Set expiry
pipe.Expire(ctx, key, window)
results, err := pipe.Exec(ctx)
if err != nil {
return false, err
}
count := results[1].(*redis.IntCmd).Val()
return count < int64(limit), nil
}
// Token bucket implementation
func (r *RateLimiter) TokenBucket(ctx context.Context, key string, capacity int, refillRate float64) (bool, error) {
script := `
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local tokens = tonumber(ARGV[2])
local refillTime = tonumber(ARGV[3])
local bucket = redis.call('HMGET', key, 'tokens', 'lastRefill')
local currentTokens = tonumber(bucket[1]) or capacity
local lastRefill = tonumber(bucket[2]) or 0
local now = redis.call('TIME')
local currentTime = tonumber(now[1])
-- Refill tokens
local elapsed = currentTime - lastRefill
local newTokens = math.min(capacity, currentTokens + elapsed * tokens)
if newTokens >= 1 then
redis.call('HMSET', key, 'tokens', newTokens - 1, 'lastRefill', currentTime)
redis.call('EXPIRE', key, refillTime)
return 1
else
return 0
end
`
result, err := r.client.Eval(ctx, script, []string{key}, capacity, refillRate, int(window.Seconds())).Result()
if err != nil {
return false, err
}
return result.(int64) == 1, nil
}
```
---
## Pub/Sub (Future Use)
```go
// Publisher
func PublishMatchUpdate(ctx context.Context, client *redis.Client, userID string, match Match) error {
channel := fmt.Sprintf("matches:%s", userID)
data, err := json.Marshal(match)
if err != nil {
return err
}
return client.Publish(ctx, channel, data).Err()
}
// Subscriber
func SubscribeMatchUpdates(ctx context.Context, client *redis.Client, userID string) (<-chan Match, error) {
channel := fmt.Sprintf("matches:%s", userID)
pubsub := client.Subscribe(ctx, channel)
matches := make(chan Match)
go func() {
defer close(matches)
for {
msg, err := pubsub.ReceiveMessage(ctx)
if err != nil {
return
}
var match Match
if err := json.Unmarshal([]byte(msg.Payload), &match); err != nil {
continue
}
select {
case matches <- match:
case <-ctx.Done():
return
}
}
}()
return matches, nil
}
```
---
## Performance Tips
1. **Use pipelines** for multiple commands
2. **Use connection pooling** - configure MaxRetries, PoolSize
3. **Use pipelining** - batch operations
4. **Set appropriate TTL** - avoid memory leaks
5. **Use keys patterns carefully** - `KEYS` is blocking, use `SCAN` for production
---
## Error Handling
```go
val, err := rdb.Get(ctx, "key").Result()
if err == redis.Nil {
// Key doesn't exist
} else if err != nil {
// Other error
return err
}
```
---
## Tutorials & Resources
- **Redis Commands**: https://redis.io/commands/
- **go-redis Examples**: https://github.com/redis/go-redis/tree/master/example
- **Redis Patterns**: https://redis.io/docs/manual/patterns/