はじめに
Webアプリケーションや基幹システムにおいて、データベースへのアクセス負荷をどう軽減するかは、パフォーマンス設計上の重要なテーマです。特に、参照系(Read)のトラフィックが書き込み系(Write)を大きく上回るシステムでは、インメモリキャッシュの導入が有効な選択肢となります。
今回は Google Cloud のマネージド型インメモリデータベースサービスである Memorystore を用いた、典型的なキャッシュ戦略「Cache Aside」のハンズオンを整理してみたいと思います。
なお、Google Cloud を活用したシステム開発については、Udemy にて関連コースをリリースしておりますので、ご興味のある方はご覧ください。
1. 代表的なキャッシュ戦略
Memorystore(Redis / Memcached)を使った代表的なキャッシュ戦略には、主に以下のパターンが存在します。
- Cache Aside(キャッシュアサイド / Lazy Loading):アプリケーションがキャッシュとDBの両方を直接制御するパターン。最も一般的。
- Write Through:データ書き込み時に、DBとキャッシュを同時に更新するパターン。読み取り時のキャッシュミスを減らせる反面、書き込みレイテンシは増加する。
- Write Behind(Write Back):書き込みをまずキャッシュに対して行い、非同期で DB に反映するパターン。書き込み性能は高いが、キャッシュ障害時のデータロストリスクがある。
- Read Through:アプリケーションはキャッシュにのみアクセスし、キャッシュミス時はキャッシュ自身が DB からデータを読み込むパターン。
今回はこの中で最も汎用的に用いられる Cache Aside を、ハンズオンで実装していきます。
2. Cache Aside の動作原理
今回構築するアーキテクチャは以下の構成です。
- アプリ層:Compute Engine(VM)
- キャッシュ層:Memorystore for Redis
- データベース層:Cloud SQL(MySQL)
Cache Aside における データ読み取り(Read) の流れは以下の通りです。
- アプリケーションは、まずキャッシュ(Memorystore)にデータが存在するか確認する。
- キャッシュヒットの場合:キャッシュからデータを取得してクライアントに返す。インメモリ参照のため、ミリ秒未満の高速な応答が可能。
- キャッシュミスの場合:データベース(Cloud SQL)から直接データを取得する。取得したデータは次回参照に備えてキャッシュに保存(SET)したうえで、クライアントに返す。
データ書き込み(Write) の流れは以下の通りです。
- アプリケーションはデータベースを直接更新する。必要に応じて、該当キーをキャッシュから削除(無効化)する。
このパターンの特徴は、 「キャッシュはあくまで補助的な存在」 であり、キャッシュが落ちてもアプリケーションはDB直アクセスで動作継続できる点です。可用性と実装シンプルさのバランスが良いため、多くのシステムで採用されています。
3. ハンズオン:Compute Engine + Memorystore + Cloud SQL
3-1. 事前準備(初回のみ)
リージョンとプロジェクトIDを変数化し、必要な API を有効化します。(東京リージョンを想定しています)
REGION=asia-northeast1
ZONE=asia-northeast1-a
PROJECT_ID=$(gcloud config get-value project)
gcloud services enable \
compute.googleapis.com \
redis.googleapis.com \
sqladmin.googleapis.com \
servicenetworking.googleapis.com
3-2. VPC とプライベートサービス接続の設定
Memorystore と Cloud SQL(プライベートIP)は、デフォルト VPC とプライベートサービス接続経由でアクセスする構成とします。
# プライベートサービス接続用のIP範囲を予約
gcloud compute addresses create google-managed-services-default \
--global \
--purpose=VPC_PEERING \
--prefix-length=16 \
--network=default
# プライベートサービス接続を確立
gcloud services vpc-peerings connect \
--service=servicenetworking.googleapis.com \
--ranges=google-managed-services-default \
--network=default
3-3. Cloud SQL(MySQL)インスタンスの作成
データベース本体となる Cloud SQL を作成します。検証用に最小スペックで構築します。
gcloud sql instances create cache-demo-db \
--database-version=MYSQL_8_0 \
--tier=db-f1-micro \
--region=$REGION \
--network=default \
--no-assign-ip
# データベースとユーザー作成
gcloud sql databases create appdb --instance=cache-demo-db
gcloud sql users create appuser --instance=cache-demo-db --password='ChangeMe123!'
# プライベートIPを確認
DB_HOST=$(gcloud sql instances describe cache-demo-db \
--format="value(ipAddresses[0].ipAddress)")
echo $DB_HOST
3-4. Memorystore for Redis インスタンスの作成
キャッシュ層となる Memorystore(Redis)を作成します。
gcloud redis instances create cache-demo-redis \
--size=1 \
--region=$REGION \
--redis-version=redis_7_0 \
--tier=basic
# Redis のホストIPを確認
REDIS_HOST=$(gcloud redis instances describe cache-demo-redis \
--region=$REGION --format="value(host)")
echo $REDIS_HOST
3-5. アプリ層となる Compute Engine VM の作成
アプリケーションを動作させる VM を作成します。
gcloud compute instances create cache-demo-app \
--zone=$ZONE \
--machine-type=e2-small \
--image-family=debian-12 \
--image-project=debian-cloud \
--scopes=cloud-platform
# SSH 接続
gcloud compute ssh cache-demo-app --zone=$ZONE
3-6. サンプルアプリの作成(Cache Aside の実装)
VM 内で、Cache Aside パターンを実装した Python アプリを作成します。
sudo apt-get update && sudo apt-get install -y python3-pip
pip3 install --break-system-packages redis pymysql
# 接続情報の環境変数を設定(事前に控えた値を使用)
export REDIS_HOST=<3-4で確認したIP>
export DB_HOST=<3-3で確認したIP>
cat > ~/app.py <<'EOF'
import os, time, json
import redis, pymysql
r = redis.Redis(host=os.environ["REDIS_HOST"], port=6379, decode_responses=True)
db = pymysql.connect(
host=os.environ["DB_HOST"], user="appuser", password="ChangeMe123!",
database="appdb", cursorclass=pymysql.cursors.DictCursor
)
# 初期データ投入(初回のみ)
with db.cursor() as cur:
cur.execute("CREATE TABLE IF NOT EXISTS products (id INT PRIMARY KEY, name VARCHAR(100), price INT)")
cur.execute("INSERT IGNORE INTO products VALUES (1,'Widget',1200),(2,'Gadget',3400)")
db.commit()
def get_product(pid):
key = f"product:{pid}"
# 1. キャッシュ確認
cached = r.get(key)
if cached:
print(f"[CACHE HIT] {key}")
return json.loads(cached)
# 2. キャッシュミス → DB から取得
print(f"[CACHE MISS] {key} → query DB")
with db.cursor() as cur:
cur.execute("SELECT * FROM products WHERE id=%s", (pid,))
row = cur.fetchone()
# 3. 次回のためにキャッシュへ保存(TTL=60秒)
if row:
r.setex(key, 60, json.dumps(row))
return row
# 動作確認:同じキーを2回取得
t1 = time.time(); print(get_product(1)); print(f"1st: {(time.time()-t1)*1000:.2f}ms")
t2 = time.time(); print(get_product(1)); print(f"2nd: {(time.time()-t2)*1000:.2f}ms")
EOF
python3 ~/app.py
3-7. 動作確認
実行すると、以下のような出力が得られるはずです。
[CACHE MISS] product:1 → query DB
{'id': 1, 'name': 'Widget', 'price': 1200}
1st: 15.32ms
[CACHE HIT] product:1
{'id': 1, 'name': 'Widget', 'price': 1200}
2nd: 0.84ms
1回目のアクセスはキャッシュミスのため Cloud SQL へクエリが飛びますが、2回目以降は Memorystore からの応答となり、レスポンスタイムが 1桁から2桁オーダーで高速化 されていることが確認できるかと思います。
3-8. 書き込み時のキャッシュ無効化
Cache Aside における書き込み処理は、DB 更新後に該当キャッシュを削除(無効化)する実装が一般的です。
def update_product_price(pid, new_price):
with db.cursor() as cur:
cur.execute("UPDATE products SET price=%s WHERE id=%s", (new_price, pid))
db.commit()
# キャッシュを無効化(次回読み取り時に再ロード)
r.delete(f"product:{pid}")
これにより、次回の読み取り時にキャッシュミスが発生し、最新のDB値が再びキャッシュに乗る、というサイクルが回ります。
4. おわりに
Cache Aside は、実装がシンプルで導入障壁が低く、かつアプリケーション側でキャッシュ制御を完全に握れるため、最初に検討すべきキャッシュ戦略 と言えます。
一方で、
- TTL(有効期限)の設計
- キャッシュとDBの整合性(特に書き込み頻度が高いデータ)
- キャッシュスタンピード(同時大量ミス時の DB 過負荷)への対策
など、本番運用に向けて考慮すべきポイントは多数存在します。Memorystore はマネージドサービスとして可用性やパッチ運用の負荷を大幅に下げてくれますので、まずは Cache Aside から始め、要件に応じて Write Through 等の戦略も組み合わせていくのが現実的なアプローチかと思います。
Google Cloud 上でパフォーマンス改善を検討されている方は、Memorystore の導入を検証されることをお勧めします。
プロフィール
[Maruchin Tech]
AWS、Google Cloud、製造業・SCM DXを専門とするUdemy講師・技術顧問。
新卒で日産自動車に入社。その後アクセンチュア、NTTデータを経て独立。
大手製造業の基幹システム刷新やDXプロジェクトにおいて、要件定義からアーキテクチャ設計までをリード。
現在は「現場で本当に使える技術と視点」をテーマに、エンジニア教育に従事。