TA

TA

キャッシュの更新

多くの人がキャッシュデータの更新コードを書くとき、まずキャッシュを削除し、その後データベースを更新し、続く操作でデータを再びキャッシュにロードするということを見かけます。しかし、この論理は間違っています。想像してみてください、二つの並行操作があり、一つは更新操作、もう一つはクエリ操作です。更新操作がキャッシュを削除した後、クエリ操作はキャッシュにヒットせず、古いデータを読み出してキャッシュに置き、その後更新操作がデータベースを更新しました。したがって、キャッシュ内のデータは依然として古いデータであり、キャッシュ内のデータは汚れてしまい、ずっとそのまま汚れた状態が続きます。

なぜこんなに多くの人がこの論理を使っているのか分かりません。私が微博にこの投稿をした後、多くの人が非常に複雑で奇妙な提案をしてくれました。そこで、いくつかのキャッシュ更新のデザインパターンについて書こうと思います(私たちにもう少しルーチンを持たせましょう)。

ここでは、キャッシュの更新とデータの更新が一つのトランザクションであるか、失敗の可能性があるかについては議論しません。まず、データベースの更新とキャッシュの更新が成功する場合を仮定します(まず成功するコードロジックを正しく書きましょう)。

キャッシュ更新のデザインパターンには四つの種類があります:Cache Aside、Read Through、Write Through、Write Behind Caching。これからこの四つのパターンを一つずつ見ていきましょう。

Cache Aside Pattern#

これは最も一般的なパターンです。具体的な論理は以下の通りです:

無効化:アプリケーションはまずキャッシュからデータを取得し、得られなければデータベースからデータを取得し、成功したらキャッシュに格納します。
ヒット:アプリケーションはキャッシュからデータを取得し、取得できたら返します。
更新:まずデータをデータベースに保存し、成功したらキャッシュを無効化します。

Cache-Aside-Design-Pattern-Flow-Diagram-e1470471723210.png
Updating-Data-using-the-Cache-Aside-Pattern-Flow-Diagram-1-e1470471761402.png

注意点として、私たちの更新はまずデータベースを更新し、成功したらキャッシュを無効化します。この方法では、記事の冒頭で言及された問題は解決できるのでしょうか?少し考えてみましょう。

一つはクエリ操作、もう一つは更新操作の並行処理です。まず、キャッシュデータを削除する操作はなく、データベース内のデータを先に更新します。この時、キャッシュは依然として有効です。したがって、並行するクエリ操作は更新されていないデータを取得しますが、更新操作はすぐにキャッシュを無効化し、その後のクエリ操作はデータをデータベースから引き出します。これにより、記事の冒頭での論理のような問題は発生せず、後続のクエリ操作は常に古いデータを取得することはありません。

これは標準的なデザインパターンであり、Facebook の論文《Scaling Memcache at Facebook》でもこの戦略が使用されています。なぜデータベースを書き終えた後にキャッシュを更新しないのでしょうか?Quora のこの質問《Why does Facebook use delete to remove the key-value pair in Memcached instead of updating the Memcached during write request to the backend?》を見てみると、主に二つの並行書き操作が汚れたデータを引き起こすことを恐れているからです。

では、Cache Aside では並行問題が発生しないのでしょうか?そうではありません。例えば、一つは読み取り操作ですが、キャッシュにヒットせず、データベースからデータを取得します。この時、書き込み操作が来て、データベースを書き終えた後にキャッシュを無効化し、その後の読み取り操作が古いデータをキャッシュに戻すことになります。これにより、汚れたデータが発生します。

しかし、このケースは理論的には発生する可能性がありますが、実際にはその確率は非常に低いかもしれません。なぜなら、この条件はキャッシュが無効化される読み取り時に発生し、並行して書き込み操作がある必要があります。実際には、データベースの書き込み操作は読み取り操作よりもはるかに遅く、テーブルをロックする必要があります。また、読み取り操作は書き込み操作の前にデータベース操作に入る必要があり、書き込み操作の後にキャッシュを更新する必要があります。これらの条件がすべて満たされる確率は基本的に大きくありません。

したがって、Quora の回答にあるように、強い一貫性を保証するためには 2PC または Paxos プロトコルを使用するか、並行時の汚れたデータの確率を必死に下げる必要があります。Facebook はこの確率を下げる方法を使用しています。なぜなら、2PC は遅すぎ、Paxos は複雑すぎるからです。もちろん、キャッシュには期限を設定するのが最善です。

Read/Write Through Pattern#

上記の Cache Aside パターンでは、アプリケーションコードが二つのデータストレージを維持する必要があります。一つはキャッシュ(Cache)、もう一つはデータベース(Repository)です。したがって、アプリケーションは少し冗長です。しかし、Read/Write Through パターンでは、データベース(Repository)の更新操作をキャッシュ自体が代理するため、アプリケーション層にとっては非常にシンプルになります。これは、アプリケーションがバックエンドを単一のストレージと見なすことができ、ストレージ自体がキャッシュを維持することを意味します。

Read Through
Read Through パターンは、クエリ操作中にキャッシュを更新することを意味します。つまり、キャッシュが無効化されたとき(期限切れまたは LRU 置換)、Cache Aside は呼び出し側がデータをキャッシュにロードする責任を負いますが、Read Through はキャッシュサービス自体がデータをロードするため、アプリケーション側には透明です。

Write Through
Write Through パターンは Read Through に似ていますが、データを更新する際に発生します。データが更新されるとき、キャッシュにヒットしなければ、直接データベースを更新し、返します。キャッシュにヒットした場合は、キャッシュを更新し、その後キャッシュ自体がデータベースを更新します(これは同期操作です)。

下の図は Wikipedia のキャッシュ項目からのものです。ここでのメモリは、私たちの例でのデータベースと理解できます。

460px-Write-through_with_no-write-allocation.svg_.png

Write Behind Caching Pattern#

Write Behind は Write Back とも呼ばれます。Linux オペレーティングシステムのカーネルを理解している人は、write back に非常に慣れているでしょう。これは Linux ファイルシステムのページキャッシュのアルゴリズムではありませんか?はい、基礎はすべて共通しています。したがって、基礎は非常に重要です。私は基礎が重要であることを何度も言ってきました。

Write Back パターンは、データを更新する際に、キャッシュのみを更新し、データベースは更新しないというものです。そして、私たちのキャッシュは非同期にデータベースをバッチ更新します。この設計の利点は、データの I/O 操作が非常に高速になることです(なぜならメモリを直接操作するからです)。非同期であるため、write back は同じデータに対する複数の操作を統合することもできるため、パフォーマンスの向上は非常に顕著です。

しかし、これに伴う問題は、データが強い一貫性を持たず、失われる可能性があることです(私たちは Unix/Linux の異常終了がデータ損失を引き起こすことを知っていますが、それが原因です)。ソフトウェア設計において、私たちは基本的に欠陥のない設計を作ることは不可能です。アルゴリズム設計における時間と空間のトレードオフと同じように、時には強い一貫性と高性能、高可用性と高信頼性が対立することがあります。ソフトウェア設計は常にトレードオフです。

さらに、Write Back の実装ロジックは比較的複雑です。なぜなら、どのデータが更新されたかを追跡し、永続層にフラッシュする必要があるからです。オペレーティングシステムの write back は、キャッシュが無効化される必要があるときにのみ、実際に永続化されます。例えば、メモリが不足した場合やプロセスが終了した場合など、これをレイジー書き込みと呼びます。

Wikipedia には write back のフローチャートがあります。基本的な論理は以下の通りです:

Write-back_with_write-allocation.png

さらに少しお話しします#

1)上記のデザインパターンは、実際にはソフトウェアアーキテクチャにおける MySQL データベースと Memcache/Redis の更新戦略ではありません。これらはコンピュータアーキテクチャの設計に関するものであり、CPU のキャッシュ、ハードディスクファイルシステムのキャッシュ、ハードディスク上のキャッシュ、データベース内のキャッシュなどが含まれます。基本的に、これらのキャッシュ更新のデザインパターンは非常に古く、長い時間の試練を経た戦略です。したがって、これはエンジニアリングにおけるいわゆるベストプラクティスであり、従えば良いのです。

2)時には、マクロなシステムアーキテクチャを考える人は非常に経験豊富であると考えがちですが、実際にはマクロなシステムアーキテクチャの多くの設計は、これらのミクロなものから派生しています。例えば、クラウドコンピューティングにおける多くの仮想化技術の原理は、従来の仮想メモリと非常に似ていませんか?Unix の I/O モデルも、アーキテクチャの同期非同期モデルに拡大され、Unix が発明したパイプはデータストリーム計算アーキテクチャではありませんか?TCP のいくつかの設計も異なるシステム間の通信に使用されており、これらのミクロなレベルを注意深く見ると、多くの設計が非常に巧妙であることに気づくでしょう…… ですので、ここで私の明確な意見を述べさせてください —— アーキテクチャをうまく作りたいのであれば、まずコンピュータアーキテクチャや多くの古い基礎技術をしっかり理解する必要があります。

3)ソフトウェア開発や設計において、事前に既存の設計や考え方を参考にし、関連するガイドライン、ベストプラクティス、またはデザインパターンを確認し、既存のものを十分に理解した上で、再発明するかどうかを決定することを強くお勧めします。決して、似て非なるものを想像して、当たり前のようにソフトウェア設計を行わないでください。

4)上記では、キャッシュ(Cache)と永続層(Repository)の全体的なトランザクションの問題を考慮していません。例えば、キャッシュの更新が成功し、データベースの更新が失敗した場合はどうしますか?またはその逆。これに関して、強い一貫性が必要な場合は、「二段階コミットプロトコル」を使用する必要があります ——prepare、commit/rollback。例えば、Java 7 の XAResource や MySQL 5.7 の XA トランザクション、一部のキャッシュは XA をサポートしています。例えば EhCache です。もちろん、XA のような強い一貫性の手法はパフォーマンスの低下を引き起こすことがあります。

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。