mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
- 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
426 lines
9.6 KiB
Markdown
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/
|
|
|