システムの起動時に何か処理を挟みたいときがあります。たとえばホットデータをデータベースから読み込み、メモリキャッシュをウォームアップするような場合です。
言語やフレームワークによって実現方法はさまざまです。
Java:Spring Boot
一般的には implements ApplicationListener<ContextRefreshedEvent> を実装し、onApplicationEvent コールバックで処理を行います。
// https://juejin.cn/post/7459021463329865728
@Component
public class CachePrewarmListener implements ApplicationListener<ContextRefreshedEvent> {
@Autowired
private CacheService cacheService; // これはキャッシュサービスの例
@Override
public void onApplicationEvent(ContextRefreshedEvent event) {
// キャッシュのウォームアップ処理
System.out.println("キャッシュのウォームアップを開始...");
List<String> hotData = cacheService.getHotDataFromDatabase(); // データベースからホットデータを取得
for (String data : hotData) {
cacheService.putIntoCache(data); // データをキャッシュへ書き込む
}
System.out.println("キャッシュのウォームアップが完了しました!");
}
}
Node.js:Fastify
アプリの ready コールバックで処理するのが一般的です。
app.ready(async (err) => {
if (err) throw err
app.log.info('キャッシュのウォームアップを開始...')
const hotData = await cacheService.getHotDataFromDatabase()
for (const data of hotData) {
await cacheService.putIntoCache(data)
}
app.log.info('キャッシュのウォームアップが完了しました!')
})
Elixir:Phoenix
Phoenix では、ルートノードの子プロセスとしてキャッシュローダーを追加する形で実現します。少し変わっています。
実行中の Elixir/Phoenix アプリケーションは木構造のノードで構成され、それぞれがプロセス(BEAM 仮想マシン上の概念であり、OS プロセスとは異なる)です。典型的な Phoenix アプリでは、Application という名前のルートノード配下に、テレメトリー、メトリクス収集、データベース統合、ルーティング、ノード自動検出などの子ノードがぶら下がっています。
これまでにもキャッシュ用ライブラリやジョブキュー用ライブラリを子ノードとして追加してきましたが、今回さらにキャッシュのウォームアップ担当である Dokuya.Translation.DictionaryLoader を追加しました。
@impl true
def start(_type, _args) do
children = [
DokuyaWeb.Telemetry,
Dokuya.Repo,
{DNSCluster, query: Application.get_env(:dokuya, :dns_cluster_query) || :ignore},
{Oban, Application.fetch_env!(:dokuya, Oban)},
{Phoenix.PubSub, name: Dokuya.PubSub},
DokuyaWeb.Endpoint,
{Cachex, [:translation_dictionary_cache]},
Dokuya.Translation.DictionaryLoader
]
opts = [strategy: :one_for_one, name: Dokuya.Supervisor]
Supervisor.start_link(children, opts)
end
ここで登録するモジュールはいずれも GenServer である必要があります。そのため Dokuya.Translation.DictionaryLoader は次のように実装しました。
defmodule Dokuya.Translation.DictionaryLoader do
use GenServer
require Logger
alias Dokuya.Translation.Dictionary
def start_link(_opts) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@impl true
def init(init_arg) do
Logger.info("キャッシュのウォームアップを開始")
dictionaries =
Dictionary.load_all()
|> Enum.map(fn dict ->
{dict.text, dict}
end)
Cachex.put_many!(:translation_dictionary_cache, dictionaries)
Logger.info("辞書データをメモリへロードしました")
{:ok, init_arg}
end
end
ロジック自体はシンプルで、ほぼボイラープレートです。init コールバック内でウォームアップ処理を行っています。