【保存版】GoでRedisを使いこなす完全ガイド – 実装からパフォーマンスチューニングまで

Goアプリケーションへのredisの導入方法

Goアプリケーションでredisを使用する際の基本的なセットアップから実装方法まで、詳しく解説していきます。

go-redisライブラリのインストールと基本設定

まず、Go言語でRedisを使用するために最も広く使われているライブラリ「go-redis」をインストールします。

go get github.com/redis/go-redis/v9

基本的な接続設定は以下のように行います:

package main

import (
    "context"
    "fmt"
    "github.com/redis/go-redis/v9"
)

func main() {
    // Redisクライアントの初期化
    rdb := redis.NewClient(&redis.Options{
        Addr:     "localhost:6379", // Redis サーバーのアドレス
        Password: "",               // パスワード(必要な場合)
        DB:       0,               // 使用するデータベース番号
    })

    // 接続テスト
    ctx := context.Background()
    _, err := rdb.Ping(ctx).Result()
    if err != nil {
        panic(err)
    }

    fmt.Println("Successfully connected to Redis!")
}

Redis接続の確立とエラーハンドリング

実践的なアプリケーションでは、適切なエラーハンドリングが重要です。以下に、より堅牢な接続処理の実装例を示します:

type RedisClient struct {
    client *redis.Client
    ctx    context.Context
}

func NewRedisClient(ctx context.Context) (*RedisClient, error) {
    // クライアントの初期化
    client := redis.NewClient(&redis.Options{
        Addr:         "localhost:6379",
        Password:     "",
        DB:           0,
        DialTimeout:  10 * time.Second,
        ReadTimeout:  30 * time.Second,
        WriteTimeout: 30 * time.Second,
        PoolSize:     10,
        PoolTimeout:  30 * time.Second,
    })

    // 接続テスト
    if err := client.Ping(ctx).Err(); err != nil {
        return nil, fmt.Errorf("failed to connect to redis: %v", err)
    }

    return &RedisClient{
        client: client,
        ctx:    ctx,
    }, nil
}

// クライアントのクローズ処理
func (rc *RedisClient) Close() error {
    return rc.client.Close()
}

環境変数を使用した安全な設定管理

本番環境での運用を考慮し、環境変数を使用した設定管理を実装します:

package config

import (
    "os"
    "strconv"
)

type RedisConfig struct {
    Host     string
    Port     string
    Password string
    DB       int
    PoolSize int
}

func LoadRedisConfig() (*RedisConfig, error) {
    // 環境変数から設定を読み込む
    db, err := strconv.Atoi(getEnvOrDefault("REDIS_DB", "0"))
    if err != nil {
        return nil, err
    }

    poolSize, err := strconv.Atoi(getEnvOrDefault("REDIS_POOL_SIZE", "10"))
    if err != nil {
        return nil, err
    }

    return &RedisConfig{
        Host:     getEnvOrDefault("REDIS_HOST", "localhost"),
        Port:     getEnvOrDefault("REDIS_PORT", "6379"),
        Password: getEnvOrDefault("REDIS_PASSWORD", ""),
        DB:       db,
        PoolSize: poolSize,
    }, nil
}

func getEnvOrDefault(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

この設定を使用する際は、以下のように環境変数を設定します:

export REDIS_HOST=redis.example.com
export REDIS_PORT=6379
export REDIS_PASSWORD=your_password
export REDIS_DB=0
export REDIS_POOL_SIZE=20

これにより、環境に応じて柔軟に設定を変更できる堅牢なRedis接続の基盤が整います。各機能は必要に応じてモジュール化し、アプリケーションの他の部分から簡単に利用できるようになります。

GoでのRedis基本操作マスターガイド

Redisの基本的なデータ操作について、実践的なコード例とともに解説していきます。

文字列データの読み書き操作

文字列は最も基本的なRedisのデータ型です。以下に基本的な操作方法を示します:

package main

import (
    "context"
    "fmt"
    "github.com/redis/go-redis/v9"
    "time"
)

func main() {
    ctx := context.Background()
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })

    // 文字列の設定
    err := rdb.Set(ctx, "user:name", "John Doe", 0).Err()
    if err != nil {
        panic(err)
    }

    // 文字列の取得
    val, err := rdb.Get(ctx, "user:name").Result()
    if err != nil {
        panic(err)
    }
    fmt.Println("user:name", val)

    // 有効期限付きで設定
    err = rdb.Set(ctx, "temporary:key", "一時的な値", time.Hour).Err()
    if err != nil {
        panic(err)
    }

    // 存在確認
    exists, err := rdb.Exists(ctx, "user:name").Result()
    if err != nil {
        panic(err)
    }
    fmt.Printf("Key exists: %v\n", exists == 1)
}

HashやListなど各データ型の効果的な使用方法

Redisの強力な機能の一つは、様々なデータ型をサポートしていることです。

Hashの操作例:

func handleHash(ctx context.Context, rdb *redis.Client) {
    // ユーザープロフィールをハッシュとして保存
    err := rdb.HSet(ctx, "user:1", map[string]interface{}{
        "name":  "田中太郎",
        "email": "tanaka@example.com",
        "age":   "30",
    }).Err()
    if err != nil {
        panic(err)
    }

    // 特定のフィールドを取得
    name, err := rdb.HGet(ctx, "user:1", "name").Result()
    if err != nil {
        panic(err)
    }

    // 全フィールドを取得
    all, err := rdb.HGetAll(ctx, "user:1").Result()
    if err != nil {
        panic(err)
    }
    fmt.Printf("User profile: %v\n", all)
}

Listの操作例:

func handleList(ctx context.Context, rdb *redis.Client) {
    // リストへの追加(末尾)
    err := rdb.RPush(ctx, "recent:logs", "ログイン操作", "データ更新", "ログアウト").Err()
    if err != nil {
        panic(err)
    }

    // リストの先頭に追加
    err = rdb.LPush(ctx, "recent:logs", "システム起動").Err()
    if err != nil {
        panic(err)
    }

    // 範囲を指定して取得
    logs, err := rdb.LRange(ctx, "recent:logs", 0, -1).Result()
    if err != nil {
        panic(err)
    }
    fmt.Printf("Logs: %v\n", logs)
}

有効期限(TTL)の設定とキー管理

キーの有効期限管理は、キャッシュシステムやセッション管理で重要です:

func handleExpiration(ctx context.Context, rdb *redis.Client) {
    // セッションデータの保存(1時間の有効期限付き)
    err := rdb.Set(ctx, "session:123", "セッションデータ", time.Hour).Err()
    if err != nil {
        panic(err)
    }

    // 有効期限の確認
    ttl, err := rdb.TTL(ctx, "session:123").Result()
    if err != nil {
        panic(err)
    }
    fmt.Printf("残り有効期限: %v\n", ttl)

    // 有効期限の更新
    err = rdb.Expire(ctx, "session:123", 2*time.Hour).Err()
    if err != nil {
        panic(err)
    }

    // キーのパターン検索
    keys, err := rdb.Keys(ctx, "session:*").Result()
    if err != nil {
        panic(err)
    }
    fmt.Printf("セッションキー一覧: %v\n", keys)

    // 注意: Keysコマンドは本番環境では使用を避け、
    // 代わりにSCANを使用することを推奨
    var cursor uint64
    var totalKeys []string
    for {
        var keys []string
        cursor, keys, err = rdb.Scan(ctx, cursor, "session:*", 10).Result()
        if err != nil {
            panic(err)
        }
        totalKeys = append(totalKeys, keys...)
        if cursor == 0 {
            break
        }
    }
}

このように、Redisの基本的なデータ型と操作を理解することで、様々なユースケースに対応できる柔軟なアプリケーションを構築することができます。次のセクションでは、これらの基本操作を活用した実践的なパターンについて説明していきます。

実践的なRedisパターンとベストプラクティス

実際のアプリケーション開発で使用される一般的なRedisパターンと、その実装方法について解説します。

キャッシュ層としての効率的な実装方法

キャッシュは最も一般的なRedisの使用パターンの一つです。以下に、効率的なキャッシュ実装の例を示します:

type CacheService struct {
    rdb *redis.Client
    ctx context.Context
}

// キャッシュから取得、なければDBから取得してキャッシュに保存
func (cs *CacheService) GetUserWithCache(userID string) (*User, error) {
    // キャッシュからの取得を試みる
    cacheKey := fmt.Sprintf("user:%s", userID)
    userData, err := cs.rdb.Get(cs.ctx, cacheKey).Result()

    if err == nil {
        // キャッシュヒット
        var user User
        err = json.Unmarshal([]byte(userData), &user)
        return &user, err
    }

    if err != redis.Nil {
        // 予期せぬエラー
        return nil, err
    }

    // キャッシュミス:DBから取得
    user, err := cs.getUserFromDB(userID)
    if err != nil {
        return nil, err
    }

    // キャッシュに保存(1時間の有効期限)
    userData, err = json.Marshal(user)
    if err != nil {
        return nil, err
    }

    err = cs.rdb.Set(cs.ctx, cacheKey, userData, time.Hour).Err()
    if err != nil {
        // キャッシュ保存エラーはログに記録するが、ユーザーデータは返す
        log.Printf("Failed to cache user data: %v", err)
    }

    return user, nil
}

// キャッシュの更新(Write-Through パターン)
func (cs *CacheService) UpdateUserWithCache(user *User) error {
    // まずDBを更新
    err := cs.updateUserInDB(user)
    if err != nil {
        return err
    }

    // キャッシュを更新
    userData, err := json.Marshal(user)
    if err != nil {
        return err
    }

    cacheKey := fmt.Sprintf("user:%s", user.ID)
    return cs.rdb.Set(cs.ctx, cacheKey, userData, time.Hour).Err()
}

分散ロック機能の実装手順

分散システムでの同期処理に使用する分散ロックの実装例です:

type DistributedLock struct {
    rdb    *redis.Client
    ctx    context.Context
    key    string
    value  string
    expiry time.Duration
}

func NewDistributedLock(rdb *redis.Client, key string) *DistributedLock {
    return &DistributedLock{
        rdb:    rdb,
        ctx:    context.Background(),
        key:    fmt.Sprintf("lock:%s", key),
        value:  uuid.New().String(),
        expiry: 30 * time.Second,
    }
}

// ロックの取得
func (dl *DistributedLock) Acquire() (bool, error) {
    return dl.rdb.SetNX(dl.ctx, dl.key, dl.value, dl.expiry).Result()
}

// ロックの解放(自分が取得したロックのみ解放可能)
func (dl *DistributedLock) Release() error {
    // Luaスクリプトを使用して、アトミックに確認と削除を行う
    script := `
        if redis.call("get", KEYS[1]) == ARGV[1] then
            return redis.call("del", KEYS[1])
        else
            return 0
        end
    `

    result, err := dl.rdb.Eval(dl.ctx, script, []string{dl.key}, dl.value).Result()
    if err != nil {
        return err
    }

    if result.(int64) == 0 {
        return fmt.Errorf("lock not owned")
    }

    return nil
}

// 使用例
func ExampleDistributedLock() {
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })

    lock := NewDistributedLock(rdb, "critical-section")

    acquired, err := lock.Acquire()
    if err != nil {
        panic(err)
    }

    if acquired {
        defer lock.Release()
        // クリティカルセクションの処理
    }
}

パブリッシュ/サブスクライブパターンの活用

イベント駆動型アーキテクチャでよく使用されるPub/Subパターンの実装例です:

type PubSubService struct {
    rdb *redis.Client
    ctx context.Context
}

// パブリッシャーの実装
func (ps *PubSubService) PublishEvent(channel string, event interface{}) error {
    eventData, err := json.Marshal(event)
    if err != nil {
        return err
    }

    return ps.rdb.Publish(ps.ctx, channel, eventData).Err()
}

// サブスクライバーの実装
func (ps *PubSubService) SubscribeToEvents(channel string, handler func([]byte) error) error {
    pubsub := ps.rdb.Subscribe(ps.ctx, channel)
    defer pubsub.Close()

    // サブスクリプション確認
    _, err := pubsub.Receive(ps.ctx)
    if err != nil {
        return err
    }

    ch := pubsub.Channel()
    for msg := range ch {
        if err := handler([]byte(msg.Payload)); err != nil {
            log.Printf("Error handling message: %v", err)
        }
    }

    return nil
}

// 使用例
func ExamplePubSub() {
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })

    pubSubService := &PubSubService{
        rdb: rdb,
        ctx: context.Background(),
    }

    // サブスクライバーの起動(別のゴルーチンで)
    go func() {
        err := pubSubService.SubscribeToEvents("user-events", func(data []byte) error {
            var event UserEvent
            if err := json.Unmarshal(data, &event); err != nil {
                return err
            }
            fmt.Printf("Received event: %+v\n", event)
            return nil
        })
        if err != nil {
            log.Printf("Subscription error: %v", err)
        }
    }()

    // イベントのパブリッシュ
    event := UserEvent{
        Type: "user_created",
        UserID: "123",
        Timestamp: time.Now(),
    }

    err := pubSubService.PublishEvent("user-events", event)
    if err != nil {
        log.Printf("Failed to publish event: %v", err)
    }
}

これらのパターンを適切に組み合わせることで、スケーラブルで信頼性の高いアプリケーションを構築することができます。次のセクションでは、これらの実装をさらに最適化するためのパフォーマンスチューニングについて説明します。

Redisパフォーマンスの最適化テクニック

Redisの性能を最大限に引き出すための最適化手法について、実装例とともに解説します。

コネクションプールの適切な設定方法

コネクションプールの適切な設定は、アプリケーションのパフォーマンスに大きな影響を与えます:

type PoolConfig struct {
    PoolSize     int
    MinIdleConns int
    MaxRetries   int
    DialTimeout  time.Duration
    ReadTimeout  time.Duration
    WriteTimeout time.Duration
    PoolTimeout  time.Duration
    IdleTimeout  time.Duration
}

func NewOptimizedRedisClient(cfg *PoolConfig) *redis.Client {
    return redis.NewClient(&redis.Options{
        Addr:         "localhost:6379",
        PoolSize:     cfg.PoolSize,     // 同時接続数の最大値
        MinIdleConns: cfg.MinIdleConns, // プール内の最小アイドル接続数
        MaxRetries:   cfg.MaxRetries,   // 再試行回数

        // タイムアウト設定
        DialTimeout:  cfg.DialTimeout,
        ReadTimeout:  cfg.ReadTimeout,
        WriteTimeout: cfg.WriteTimeout,
        PoolTimeout:  cfg.PoolTimeout,
        IdleTimeout:  cfg.IdleTimeout,
    })
}

// 使用例と負荷テスト
func ExamplePoolConfiguration() {
    cfg := &PoolConfig{
        PoolSize:     100,              // 同時リクエスト数に応じて調整
        MinIdleConns: 10,
        MaxRetries:   3,
        DialTimeout:  5 * time.Second,
        ReadTimeout:  3 * time.Second,
        WriteTimeout: 3 * time.Second,
        PoolTimeout:  4 * time.Second,
        IdleTimeout:  300 * time.Second,
    }

    rdb := NewOptimizedRedisClient(cfg)

    // プール統計情報の取得
    stats := rdb.PoolStats()
    fmt.Printf("Total Connections: %d\n", stats.TotalConns)
    fmt.Printf("Idle Connections: %d\n", stats.IdleConns)
    fmt.Printf("Stale Connections: %d\n", stats.StaleConns)
}

パイプラインを使用した処理の高速化

複数のコマンドを一括で実行するパイプラインを使用することで、ネットワークのラウンドトリップを削減できます:

type BatchProcessor struct {
    rdb *redis.Client
    ctx context.Context
}

func (bp *BatchProcessor) ProcessBatch(keys []string) error {
    // パイプラインの作成
    pipe := bp.rdb.Pipeline()

    // コマンドの一括登録
    for _, key := range keys {
        pipe.Get(bp.ctx, key)
    }

    // 一括実行
    cmds, err := pipe.Exec(bp.ctx)
    if err != nil {
        return err
    }

    // 結果の処理
    for i, cmd := range cmds {
        val, err := cmd.(*redis.StringCmd).Result()
        if err != nil && err != redis.Nil {
            fmt.Printf("Error processing key %s: %v\n", keys[i], err)
            continue
        }
        // 結果の処理
        fmt.Printf("Key: %s, Value: %s\n", keys[i], val)
    }

    return nil
}

// TxPipelineを使用したトランザクション処理の例
func (bp *BatchProcessor) ProcessBatchWithTransaction(keys []string) error {
    // トランザクションパイプラインの作成
    pipe := bp.rdb.TxPipeline()

    // コマンドの一括登録
    for _, key := range keys {
        pipe.Get(bp.ctx, key)
    }

    // トランザクション実行
    cmds, err := pipe.Exec(bp.ctx)
    if err != nil {
        return err
    }

    // 結果の処理
    for i, cmd := range cmds {
        val, err := cmd.(*redis.StringCmd).Result()
        if err != nil && err != redis.Nil {
            fmt.Printf("Error processing key %s: %v\n", keys[i], err)
            continue
        }
        fmt.Printf("Key: %s, Value: %s\n", keys[i], val)
    }

    return nil
}

// パフォーマンス計測用のベンチマーク
func BenchmarkBatchProcessing(b *testing.B) {
    rdb := redis.NewClient(&redis.Options{
        Addr: "localhost:6379",
    })

    bp := &BatchProcessor{
        rdb: rdb,
        ctx: context.Background(),
    }

    keys := make([]string, 1000)
    for i := range keys {
        keys[i] = fmt.Sprintf("key:%d", i)
    }

    b.Run("Without Pipeline", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            for _, key := range keys {
                rdb.Get(context.Background(), key)
            }
        }
    })

    b.Run("With Pipeline", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            bp.ProcessBatch(keys)
        }
    })
}

メモリ使用量の監視と最適化戦略

メモリ使用量を監視し、最適化するためのユーティリティ関数群:

type MemoryStats struct {
    UsedMemory     int64
    PeakMemory     int64
    FragmentRatio  float64
    EvictedKeys    int64
    ExpiredKeys    int64
}

type MemoryMonitor struct {
    rdb *redis.Client
    ctx context.Context
}

func NewMemoryMonitor(rdb *redis.Client) *MemoryMonitor {
    return &MemoryMonitor{
        rdb: rdb,
        ctx: context.Background(),
    }
}

// メモリ統計情報の取得
func (mm *MemoryMonitor) GetMemoryStats() (*MemoryStats, error) {
    info, err := mm.rdb.Info(mm.ctx, "memory").Result()
    if err != nil {
        return nil, err
    }

    // INFO コマンドの結果をパース
    stats := &MemoryStats{}
    lines := strings.Split(info, "\r\n")
    for _, line := range lines {
        if strings.HasPrefix(line, "used_memory:") {
            stats.UsedMemory, _ = strconv.ParseInt(strings.Split(line, ":")[1], 10, 64)
        }
        if strings.HasPrefix(line, "used_memory_peak:") {
            stats.PeakMemory, _ = strconv.ParseInt(strings.Split(line, ":")[1], 10, 64)
        }
    }

    return stats, nil
}

// メモリ使用量の定期監視
func (mm *MemoryMonitor) StartMonitoring(interval time.Duration, threshold int64) {
    ticker := time.NewTicker(interval)
    go func() {
        for range ticker.C {
            stats, err := mm.GetMemoryStats()
            if err != nil {
                log.Printf("Error getting memory stats: %v", err)
                continue
            }

            // しきい値チェック
            if stats.UsedMemory > threshold {
                log.Printf("WARNING: Memory usage exceeds threshold: %d bytes", stats.UsedMemory)
                // アラート送信やメトリクス記録などの処理を追加
            }
        }
    }()
}

// キーの有効期限設定による自動メモリ解放
func (mm *MemoryMonitor) SetExpirationForPattern(pattern string, ttl time.Duration) error {
    var cursor uint64
    for {
        var keys []string
        var err error
        cursor, keys, err = mm.rdb.Scan(mm.ctx, cursor, pattern, 100).Result()
        if err != nil {
            return err
        }

        // バッチで有効期限を設定
        pipe := mm.rdb.Pipeline()
        for _, key := range keys {
            pipe.Expire(mm.ctx, key, ttl)
        }
        _, err = pipe.Exec(mm.ctx)
        if err != nil {
            return err
        }

        if cursor == 0 {
            break
        }
    }
    return nil
}

これらの最適化テクニックを適切に組み合わせることで、Redisの性能を最大限に引き出すことができます。次のセクションでは、これらの最適化を含めた本番環境での運用ノウハウについて説明します。

本番環境での運用ノウハウ

本番環境でRedisを安全かつ効率的に運用するためのベストプラクティスについて、具体的な実装例とともに解説します。

冗長化構成の実装方法

高可用性を確保するためのRedisセントinel構成の実装例を示します:

type SentinelClient struct {
    rdb    *redis.Client
    ctx    context.Context
    config *SentinelConfig
}

type SentinelConfig struct {
    MasterName     string
    SentinelAddrs  []string
    Password       string
    DB             int
    PoolSize       int
    RouteByLatency bool
    RouteRandomly  bool
}

func NewSentinelClient(config *SentinelConfig) (*SentinelClient, error) {
    // Sentinelクライアントの設定
    rdb := redis.NewFailoverClient(&redis.FailoverOptions{
        MasterName:     config.MasterName,
        SentinelAddrs:  config.SentinelAddrs,
        Password:       config.Password,
        DB:             config.DB,
        PoolSize:       config.PoolSize,
        RouteByLatency: config.RouteByLatency,
        RouteRandomly:  config.RouteRandomly,

        // 耐障害性のための設定
        MaxRetries:      3,
        MinRetryBackoff: time.Millisecond * 100,
        MaxRetryBackoff: time.Second * 2,

        // タイムアウト設定
        DialTimeout:  time.Second * 5,
        ReadTimeout:  time.Second * 3,
        WriteTimeout: time.Second * 3,
    })

    // 接続テスト
    ctx := context.Background()
    if err := rdb.Ping(ctx).Err(); err != nil {
        return nil, fmt.Errorf("failed to connect to Redis: %v", err)
    }

    return &SentinelClient{
        rdb:    rdb,
        ctx:    ctx,
        config: config,
    }, nil
}

// ヘルスチェックの実装
func (sc *SentinelClient) HealthCheck() (*HealthStatus, error) {
    status := &HealthStatus{
        Time: time.Now(),
    }

    // Pingによる疎通確認
    start := time.Now()
    err := sc.rdb.Ping(sc.ctx).Err()
    status.Latency = time.Since(start)

    if err != nil {
        status.Status = "unhealthy"
        status.Error = err.Error()
        return status, err
    }

    // INFO コマンドによる詳細情報の取得
    info, err := sc.rdb.Info(sc.ctx, "replication").Result()
    if err != nil {
        status.Status = "degraded"
        status.Error = fmt.Sprintf("failed to get replication info: %v", err)
        return status, err
    }

    // レプリケーション情報の解析
    status.Role = sc.parseRole(info)
    status.Status = "healthy"

    return status, nil
}

// メトリクス収集の実装
func (sc *SentinelClient) CollectMetrics() (*RedisMetrics, error) {
    metrics := &RedisMetrics{
        Timestamp: time.Now(),
    }

    // メモリ使用量の取得
    memory, err := sc.rdb.Info(sc.ctx, "memory").Result()
    if err == nil {
        metrics.MemoryStats = sc.parseMemoryStats(memory)
    }

    // キー統計の取得
    keyspace, err := sc.rdb.Info(sc.ctx, "keyspace").Result()
    if err == nil {
        metrics.KeyStats = sc.parseKeyspaceStats(keyspace)
    }

    // クライアント接続情報の取得
    clients, err := sc.rdb.Info(sc.ctx, "clients").Result()
    if err == nil {
        metrics.ClientStats = sc.parseClientStats(clients)
    }

    return metrics, nil
}

バックアップと復旧戦略

自動バックアップと復旧手順の実装例:

type BackupManager struct {
    rdb        *redis.Client
    ctx        context.Context
    backupDir  string
    maxBackups int
}

func NewBackupManager(rdb *redis.Client, backupDir string, maxBackups int) *BackupManager {
    return &BackupManager{
        rdb:        rdb,
        ctx:        context.Background(),
        backupDir:  backupDir,
        maxBackups: maxBackups,
    }
}

// バックアップの実行
func (bm *BackupManager) CreateBackup() error {
    // バックアップファイル名の生成
    timestamp := time.Now().Format("20060102150405")
    filename := filepath.Join(bm.backupDir, fmt.Sprintf("redis_backup_%s.rdb", timestamp))

    // SAVE コマンドの実行
    if err := bm.rdb.Save(bm.ctx).Err(); err != nil {
        return fmt.Errorf("failed to create backup: %v", err)
    }

    // バックアップの圧縮
    if err := bm.compressBackup(filename); err != nil {
        return fmt.Errorf("failed to compress backup: %v", err)
    }

    // 古いバックアップの削除
    if err := bm.cleanOldBackups(); err != nil {
        log.Printf("Warning: failed to clean old backups: %v", err)
    }

    return nil
}

// バックアップのスケジュール設定
func (bm *BackupManager) ScheduleBackups(interval time.Duration) {
    ticker := time.NewTicker(interval)
    go func() {
        for range ticker.C {
            if err := bm.CreateBackup(); err != nil {
                log.Printf("Backup failed: %v", err)
            }
        }
    }()
}

// バックアップの復元
func (bm *BackupManager) RestoreFromBackup(backupFile string) error {
    // 復元前の確認
    if err := bm.validateBackup(backupFile); err != nil {
        return fmt.Errorf("backup validation failed: %v", err)
    }

    // 既存データのクリア
    if err := bm.rdb.FlushAll(bm.ctx).Err(); err != nil {
        return fmt.Errorf("failed to clear existing data: %v", err)
    }

    // 復元の実行
    if err := bm.restoreData(backupFile); err != nil {
        return fmt.Errorf("failed to restore data: %v", err)
    }

    return nil
}

モニタリングと監視の設定

包括的な監視システムの実装例:

type MonitoringService struct {
    rdb      *redis.Client
    ctx      context.Context
    metrics  chan Metric
    alerting AlertingConfig
}

type Metric struct {
    Name      string
    Value     float64
    Timestamp time.Time
    Labels    map[string]string
}

func (ms *MonitoringService) StartMonitoring() {
    go ms.collectBasicMetrics()
    go ms.collectPerformanceMetrics()
    go ms.monitorSlowLogs()
    go ms.processMetrics()
}

// 基本メトリクスの収集
func (ms *MonitoringService) collectBasicMetrics() {
    ticker := time.NewTicker(time.Second * 30)
    for range ticker.C {
        info, err := ms.rdb.Info(ms.ctx, "memory", "clients", "stats").Result()
        if err != nil {
            log.Printf("Error collecting basic metrics: %v", err)
            continue
        }

        metrics := ms.parseRedisInfo(info)
        for _, metric := range metrics {
            ms.metrics <- metric
        }
    }
}

// スロークエリの監視
func (ms *MonitoringService) monitorSlowLogs() {
    ticker := time.NewTicker(time.Minute)
    for range ticker.C {
        logs, err := ms.rdb.SlowLogGet(ms.ctx, 10).Result()
        if err != nil {
            log.Printf("Error getting slow logs: %v", err)
            continue
        }

        for _, entry := range logs {
            if entry.Duration > time.Millisecond*100 {
                ms.alertSlowQuery(entry)
            }
        }
    }
}

// メトリクスの処理とアラート
func (ms *MonitoringService) processMetrics() {
    for metric := range ms.metrics {
        // メトリクスの保存
        if err := ms.storeMetric(metric); err != nil {
            log.Printf("Error storing metric: %v", err)
            continue
        }

        // アラートルールのチェック
        if ms.shouldAlert(metric) {
            ms.sendAlert(metric)
        }
    }
}

// アラートの送信
func (ms *MonitoringService) sendAlert(metric Metric) {
    alert := Alert{
        Title:       fmt.Sprintf("Redis Alert: %s threshold exceeded", metric.Name),
        Description: fmt.Sprintf("Current value: %v", metric.Value),
        Severity:    "warning",
        Timestamp:   time.Now(),
    }

    if err := ms.alerting.Send(alert); err != nil {
        log.Printf("Failed to send alert: %v", err)
    }
}

これらの実装例は、本番環境でのRedis運用における基本的な要件をカバーしています。実際の運用では、以下の点に特に注意を払う必要があります:

  1. 監視とアラート
  • メモリ使用量
  • レイテンシ
  • エラーレート
  • 接続数
  1. バックアップ戦略
  • 定期的なバックアップ
  • バックアップの自動化
  • 復旧手順の文書化
  1. スケーリング
  • 負荷監視
  • キャパシティプランニング
  • シャーディング戦略
  1. セキュリティ
  • アクセス制御
  • ネットワークセキュリティ
  • データ暗号化

これらの要素を適切に組み合わせることで、安定した本番環境のRedis運用が可能になります。