🗂

【キャッシュ】GoとRedisでサーバーサイドキャッシュを実装して、処理時間を体感してみた

2024/09/23に公開

はじめに

Redisを使用してサーバーサイドキャッシュを実装し、
データベースからデータを取得する場合と、
サーバーサイドキャッシュを利用してデータを取得する場合の
処理時間を比較していきたいと思います。

まずはRedisを用いて、サーバーサイドキャッシュを実現

Dockerfile
# ベースイメージ
FROM golang:1.19-alpine

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN go build -o main .

CMD ["./main"]
docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - redis
    environment:
      - REDIS_ADDR=redis:6379

  redis:
    image: redis:latest
    ports:
      - "6379:6379"
main.go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	"context"

	"github.com/go-redis/redis/v8"
)

var (
	redisClient *redis.Client
	ctx         = context.Background()
)

var users = map[string]map[string]interface{}{
	"1": {"id": 1, "name": "Alice", "age": 25},
	"2": {"id": 2, "name": "Bob", "age": 30},
	"3": {"id": 3, "name": "Charlie", "age": 35},
}

// Redisに接続する関数
func initRedis() {
	redisAddr := os.Getenv("REDIS_ADDR")
	if redisAddr == "" {
		redisAddr = "localhost:6379"
	}

	redisClient = redis.NewClient(&redis.Options{
		Addr: redisAddr,
	})

	if _, err := redisClient.Ping(ctx).Result(); err != nil {
		log.Fatalf("Redisに接続できません: %v", err)
	}
	fmt.Println("Redisに接続しました")
}

// ユーザーデータを取得するハンドラ
func getUserHandler(w http.ResponseWriter, r *http.Request) {
	userID := r.URL.Query().Get("id")
	if userID == "" {
		http.Error(w, "ユーザーIDを指定してください", http.StatusBadRequest)
		return
	}

	// Redisキャッシュからデータを取得
	cachedUser, err := redisClient.Get(ctx, userID).Result()
	if err == nil {
		fmt.Println("キャッシュからデータを取得しました")
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(cachedUser))
		return
	}

	// キャッシュにデータがない場合はデータベースから取得
	user, exists := users[userID]
	if !exists {
		http.Error(w, "ユーザーが見つかりません", http.StatusNotFound)
		return
	}

	// Redisにキャッシュ(TTLを60秒に設定)
	userJSON, _ := json.Marshal(user)
	redisClient.Set(ctx, userID, userJSON, 60*time.Second)
	fmt.Println("データベースからデータを取得しました")

	w.Header().Set("Content-Type", "application/json")
	w.Write(userJSON)
}

func main() {
	// Redisへの接続を初期化
	initRedis()

	// エンドポイントの設定
	http.HandleFunc("/user", getUserHandler)

	// サーバーの起動
	fmt.Println("サーバーをポート8080で起動します")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

  1. docker-composeでアプリケーションの起動
docker-compose up --build

⬇️実行結果のログ

  1. Redisコンテナに接続し、デモデータがキャッシュされていることの確認
    docker psコマンドで起動中のコンテナを確認

Redisコンテナのidが"c0e52daf0e25"であることがわかったため、docker execでRedisコンテナに接続します。

docker exec -it c0e52daf0e25 redis-cli
  1. ブラウザでURLにアクセスして、データをキャッシュする
    以下のURLにアクセスしてデータを取得できているか確認します。
http://localhost:8080/user?id=1

結果は、

うまくいっています!

  1. Redis CLIでキャッシュデータの確認

リクエストをすると60秒間の間、データがキャッシュされているはずなので、急いでRedisコンテナのRedis CLIで確認。

キャッシュされているキー(ユーザーID)を表示させるコマンド

127.0.0.1:6379> KEYS *

キー(ユーザーID)が1のデータを取得

GET 1

結果は、

KEYS * の結果が(empty array)と表示されていることから、キャッシュデータが期限切れ(TTLが切れている)になってしまったことがわかります。

キャッシュ vs データベース

プロジェクト構成

/go-redis-cache
├── Dockerfile
├── docker-compose.yml
├── main.go
└── go.mod

Dockerの設定

Dockerfile
FROM golang:1.19-alpine

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .

RUN go build -o main .

CMD ["./main"]
docker-compose.yaml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - postgres
      - redis
    environment:
      - REDIS_ADDR=redis:6379
      - POSTGRES_HOST=postgres
      - POSTGRES_PORT=5432
      - POSTGRES_USER=admin
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=userdb

  postgres:
    image: postgres:13
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: password
      POSTGRES_DB: userdb
    volumes:
      - postgres-data:/var/lib/postgresql/data
    ports:
      - "5433:5432"

  pgadmin:
    image: dpage/pgadmin4
    environment:
      PGADMIN_DEFAULT_EMAIL: admin@admin.com
      PGADMIN_DEFAULT_PASSWORD: admin
    ports:
      - "5050:80"
    depends_on:
      - postgres

  redis:
    image: redis:latest
    ports:
      - "6379:6379"

volumes:
  postgres-data:

Golangアプリケーションの設定

以下のコマンドでpgとRedisのドライバをインストール

bash
go get github.com/go-redis/redis/v8
go get github.com/lib/pq
main.go
package main

import (
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
	"time"

	"context"

	"github.com/go-redis/redis/v8"
	_ "github.com/lib/pq"
)

var (
	redisClient *redis.Client
	ctx         = context.Background()
	db          *sql.DB
)

// ユーザーデータ構造体
type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
	Age  int    `json:"age"`
}

// PostgreSQLに接続する関数
func initPostgres() {
	connStr := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
		os.Getenv("POSTGRES_HOST"),
		os.Getenv("POSTGRES_PORT"),
		os.Getenv("POSTGRES_USER"),
		os.Getenv("POSTGRES_PASSWORD"),
		os.Getenv("POSTGRES_DB"),
	)

	var err error
	db, err = sql.Open("postgres", connStr)
	if err != nil {
		log.Fatalf("PostgreSQLへの接続に失敗しました: %v", err)
	}

	if err = db.Ping(); err != nil {
		log.Fatalf("PostgreSQLへの接続テストに失敗しました: %v", err)
	}

	fmt.Println("PostgreSQLに接続しました")
}

// Redisに接続する関数
func initRedis() {
	redisAddr := os.Getenv("REDIS_ADDR")
	if redisAddr == "" {
		redisAddr = "localhost:6379"
	}

	redisClient = redis.NewClient(&redis.Options{
		Addr: redisAddr,
	})

	if _, err := redisClient.Ping(ctx).Result(); err != nil {
		log.Fatalf("Redisに接続できません: %v", err)
	}
	fmt.Println("Redisに接続しました")
}

// データベースからデータを取得する関数
func getUserFromDB(userID string) (*User, error) {
	var user User
	query := "SELECT id, name, age FROM users WHERE id = $1"

	startTime := time.Now() // データベースアクセスの開始時間
	err := db.QueryRow(query, userID).Scan(&user.ID, &user.Name, &user.Age)
	dbDuration := time.Since(startTime) // データベースアクセスの終了時間

	if err != nil {
		return nil, err
	}

	fmt.Printf("データベースからデータを取得しました(処理時間): %v\n", dbDuration)

	return &user, nil
}

// ユーザーデータを取得するハンドラ
func getUserHandler(w http.ResponseWriter, r *http.Request) {
	userID := r.URL.Query().Get("id")
	if userID == "" {
		http.Error(w, "ユーザーIDを指定してください", http.StatusBadRequest)
		return
	}

	// Redisキャッシュからデータを取得
	startTime := time.Now()
	cachedUser, err := redisClient.Get(ctx, userID).Result()

	if err == nil {
		cacheDuration := time.Since(startTime)
		fmt.Printf("キャッシュからデータを取得しました(処理時間): %v\n", cacheDuration)

		// キャッシュからデータを取得した場合のみJSONを返す
		w.Header().Set("Content-Type", "application/json")
		w.Write([]byte(cachedUser))
		return
	}

	// キャッシュにデータがない場合、PostgreSQLから取得
	user, err := getUserFromDB(userID)
	if err != nil {
		http.Error(w, "ユーザーが見つかりません", http.StatusNotFound)
		return
	}

	// データをRedisにキャッシュ(TTLを60秒に設定)
	userJSON, _ := json.Marshal(user)
	redisClient.Set(ctx, userID, userJSON, 60*time.Second)

	// データベースから取得した場合のみデータを返す
	w.Header().Set("Content-Type", "application/json")
	w.Write(userJSON)
}

func main() {
	// PostgreSQLとRedisへの接続を初期化
	initPostgres()
	initRedis()

	// エンドポイントの設定
	http.HandleFunc("/user", getUserHandler)

	// サーバーの起動
	fmt.Println("サーバーをポート8080で起動します")
	log.Fatal(http.ListenAndServe(":8080", nil))
}

Docker Composeの実行

sh
docker-compose up --build

pgAdminを使ってデータベースにアクセス

  1. ブラウザで http://localhost:5050 にアクセスし、pgAdminにログインします。
    • Email: admin@admin.com
    • Password: admin
  2. PostgreSQLサーバーを追加します。
    • Host: postgres
    • Port: 5432
    • Username: admin
    • Password: password
    • Database: userdb

⬇️ログイン画面

⬇️データベースを作成

ユーザーテーブルの作成とデータ挿入

pdAdminでSQLクエリを実行して、ユーザーデータを登録します。

sql
CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  name VARCHAR(50),
  age INT
);

INSERT INTO users (name, age) VALUES
('Alice', 25),
('Bob', 30),
('Charlie', 35);
  1. userdbでクエリツールを開く

  2. SQL文を記述し、実行!

  3. クエリ結果の確認

しっかりテーブルの作成と、データ挿入ができています!

処理速度計測

以下のリンクにブラウザでアクセスし、1回目はデータベースからデータを取得します。
2回目は1回目のリクエスト時にキャッシュされたデータが取得されます。

http://localhost:8080/user?id=1

それぞれの処理速度がログに表示されるので観測してみた。

1回目

処理速度は、710.417μsであることが分かった。

2回目

処理速度は、367.208μsであることから、データベースからの取得に比べ、2倍近く早く取得できることが分かった!

まとめ

以上の結果から、キャッシュという技術によって、データベースから取得するよりも早くデータを取得できることが分かった。

Discussion