【キャッシュ】GoとRedisでサーバーサイドキャッシュを実装して、処理時間を体感してみた
はじめに
Redisを使用してサーバーサイドキャッシュを実装し、
データベースからデータを取得する場合と、
サーバーサイドキャッシュを利用してデータを取得する場合の
処理時間を比較していきたいと思います。
まずはRedisを用いて、サーバーサイドキャッシュを実現
# ベースイメージ
FROM golang:1.19-alpine
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main .
CMD ["./main"]
version: '3.8'
services:
app:
build: .
ports:
- "8080:8080"
depends_on:
- redis
environment:
- REDIS_ADDR=redis:6379
redis:
image: redis:latest
ports:
- "6379:6379"
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))
}
- docker-composeでアプリケーションの起動
docker-compose up --build
⬇️実行結果のログ
- Redisコンテナに接続し、デモデータがキャッシュされていることの確認
docker psコマンドで起動中のコンテナを確認
Redisコンテナのidが"c0e52daf0e25"であることがわかったため、docker execでRedisコンテナに接続します。
docker exec -it c0e52daf0e25 redis-cli
- ブラウザでURLにアクセスして、データをキャッシュする
以下のURLにアクセスしてデータを取得できているか確認します。
http://localhost:8080/user?id=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の設定
FROM golang:1.19-alpine
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o main .
CMD ["./main"]
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のドライバをインストール
go get github.com/go-redis/redis/v8
go get github.com/lib/pq
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の実行
docker-compose up --build
pgAdminを使ってデータベースにアクセス
- ブラウザで http://localhost:5050 にアクセスし、pgAdminにログインします。
• Email: admin@admin.com
• Password: admin - PostgreSQLサーバーを追加します。
• Host: postgres
• Port: 5432
• Username: admin
• Password: password
• Database: userdb
⬇️ログイン画面
⬇️データベースを作成
ユーザーテーブルの作成とデータ挿入
pdAdminで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);
-
userdbでクエリツールを開く
-
SQL文を記述し、実行!
-
クエリ結果の確認
しっかりテーブルの作成と、データ挿入ができています!
処理速度計測
以下のリンクにブラウザでアクセスし、1回目はデータベースからデータを取得します。
2回目は1回目のリクエスト時にキャッシュされたデータが取得されます。
http://localhost:8080/user?id=1
それぞれの処理速度がログに表示されるので観測してみた。
1回目
処理速度は、710.417μsであることが分かった。
2回目
処理速度は、367.208μsであることから、データベースからの取得に比べ、2倍近く早く取得できることが分かった!
まとめ
以上の結果から、キャッシュという技術によって、データベースから取得するよりも早くデータを取得できることが分かった。
Discussion