はじめに
読み書き分離、データベース分割、非正規化、NoSQL の採用……これらの拡張手段をすべて導入しても、データレスポンスがますます遅くなる場合、还有什么解決策がありますか?
あります、キャッシュを追加します。キャッシュ層を利用して不均一な負荷とトラフィックピークを吸収します:
Popular items can skew the distribution, causing bottlenecks. Putting a cache in front of a database can help absorb uneven loads and spikes in traffic.
一.どこに追加するか?
理論上、データ層より前の任意の層にキャッシュを追加すればトラフィックを遮断でき、最終的にデータベースに到達する操作リクエストを削減できます:

キャッシュの位置によって 4 種類に分類されます:
-
クライアントキャッシュ:[HTTP キャッシュ](/articles/http キャッシュ/)、ブラウザキャッシュなどを含む
-
Web キャッシュ:例えば CDN、リバースプロキシサービス など
-
データベースキャッシュ:一部のデータベースは組み込みキャッシュサポートを提供しています。例えばクエリキャッシュ(query cache)
データベースの負荷を軽減するために、アプリケーションとデータストレージの間にキーバリューストアをバッファ層として追加します:
A cache is a simple key-value store and it should reside as a buffering layer between your application and your data storage.
メモリ内にキャッシュされたデータで一部のリクエストに応答し、実際にデータベースクエリを実行する必要がないため、データレスポンス速度を向上させます
二.何を保存するか?
一般的な 2 つのキャッシュモードがあります:
-
Cached Database Queries:元のデータベースクエリ結果をキャッシュ
-
Cached Objects:アプリケーション内のデータモデルをキャッシュ。例えば再構成されたデータセット、またはデータモデルクラスインスタンス全体
元のデータベースクエリ結果をキャッシュ
クエリ文から key を生成し、データベースクエリ結果をキャッシュします。例えば:
key = "user.%s" % user_id
user_blob = memcache.get(key)
if user_blob is None:
user = mysql.query("SELECT * FROM users WHERE user_id=\"%s\"", user_id)
if user:
memcache.set(key, json.dumps(user))
return user
else:
return json.loads(user_blob)
このモードの主な欠点はキャッシュの有効期限処理が困難なことです。データと key(つまりクエリ文)の間に明確な関連性がないため、データが変化した後、キャッシュ内のすべての関連エントリを正確に削除するのが困難 です。考えてみてください、1 つのセルが変化した場合、どのクエリ文に影響するでしょうか?
それでも、これは最も一般的なキャッシュモードです。妥協できるからです。例えば:
-
クエリ文と直接関連するデータのみをキャッシュし、ソート、統計、フィルタリングなどの計算結果はすべて保存しない
-
正確さを求めず、影響を受ける可能性のあるすべてのキャッシュエントリを削除する
データオブジェクトをキャッシュ
もう 1 つの考え方は、アプリケーション内のデータモデルオブジェクトをキャッシュすることです。これにより元データとキャッシュの間に論理的な関連性が生まれ、キャッシュ更新の難題を簡単に解決できます
データがどのようにクエリされ、どのように加工変換されても、最終的に得られたデータモデルオブジェクトのみをキャッシュし、元データが変化した場合は、対応するデータオブジェクト全体を削除します
アプリケーションにとって、データオブジェクトは元データよりも管理・保守が容易です。そのため、元データではなくデータオブジェクトをキャッシュすることをお勧めします
三.どのように検索するか?
一般的なキャッシュデータアクセス戦略は 6 種類あります:
-
Cache-aside/Lazy loading:予約キャッシュ
-
Read-through:直読式
-
Write-through:直写式
-
Write-behind/Write-back:裏書き式
-
Write-around:迂回書き込み式
-
Refresh-ahead:更新式
Cache-aside

予約キャッシュモードでは、キャッシュとデータベースの間に直接の関係はありません(キャッシュは脇にあるため Cache-aside と呼ばれます)。アプリケーションが必要なデータをデータベースから読み取り、キャッシュに填充します
データリクエストは優先的にキャッシュを経由し、キャッシュミス時にのみデータベースクエリを実行し、結果をキャッシュします。そのため、キャッシュはオンデマンドです(Lazy loading)。実際にアクセスされたデータのみがキャッシュされます
主な問題は以下の通りです:
-
キャッシュミス時は 3 ステップ必要で、遅延は無視でき���せん(コールドスタートには手動でウォームアップ可能)
-
キャッシュが古くなる可能性があります(一般に TTL を設定して強制的に更新します)
Read-through

直読モードでは、キャッシュがデータベースの前にあり、アプリケーションはデータベースと直接対話せず、キャッシュから直接データを読み取ります
キャッシュミス時は、キャッシュがデータベースクエリを担当し、自分でキャッシュします。予約キャッシュとの唯一の違いはデータベースクエリの作業をキャッシュが担当することで、アプリケーションではありません
Write-through

直読モードに似て、キャッシュもデータベースの前にあり、データは先にキャッシュに書き込まれ、その後データベースに書き込まれます。つまり、すべての書き込み操作は先にキャッシュを経由する必要があります
一般に直読式キャッシュと組み合わせて使用されます。書き込み操作はキャッシュの層が 1 つ増えます(追加の遅延が存在します)が、キャッシュデータの一貫性が保証されます(キャッシュが古くなるのを回避)。この時、キャッシュはデータベースのプロキシのようになり、読み書きともにキャッシュを経由し、キャッシュがデータベースをクエリするか、書き込み操作をデータベースに同期します
Write-behind/Write-back

裏書き式キャッシュは直写式と非常によく似ており、書き込み操作も同様に先にキャッシュを経由しますが、唯一の違いはデータベースへの非同期書き込みで、バッチ処理と書き込み操作の結合を可能にします
同様に直読式キャッシュと組み合わせて使用でき、直写式の書き込み操作のパフォーマンス問題も存在しませんが、最終的一貫性のみを保証します
Write-around
いわゆる迂回書き込み式キャッシュとは書き込み操作がキャッシュを経由せず(迂回し)、アプリケーションが直接データベースに書き込むことで、読み取り操作のみをキャッシュします。予約キャッシュまたは直読キャッシュと組み合わせて使用できます:

Refresh-ahead
事前更新。キャッシュが期限切れになる前に、最近アクセスされたエントリを自動的に更新(再読み込み)します。プリロードによって遅延を削減することもできますが、予測が不正確な場合は逆にパフォーマンスが低下する可能性があります
四.いっぱいになったらどうするか?
もちろん、キャッシュスペースは非常に限られているため、削除戦略(Eviction Policy)も必要です。キャッシュから使用される可能性の低いエントリを削除します。一般的な戦略は以下の通りです:
-
LRU(Least Recently Used):最も一般的な戦略。プログラム実行時の局所性原理に基づき、一定期間内に同じデータにアクセスする確率が高いため、最近使用されていないデータを削除します。例えば航空券の予約で、一定期間内に同じルートを照会する確率が高い
-
LFU(Least Frequently Used):使用頻度に基づき、最も頻繁に使用されないデータを削除します。例えば入力変換の多くは単語頻度で連想します
-
MRU(Most Recently Used):一部のシナリオでは、最近使用したエントリを削除する必要があります。例えば既読、再通知しない、興味なしなど
-
FIFO(First In, First Out):先入れ先出し。最も早くアクセスされたデータを削除
これらの戦略は組み合わせて使用することもできます。例えば LRU + LFU で総合的に考慮します。具体的なシナリオによります
コメントはまだありません