有时我们会想在系统启动时做点事情,比如:缓存预热(将热点数据从数据库中加载到内存缓存中)。

不同的语言、框架做这个事情的方式都不一样。

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


一般是在 app 的 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


则是通过在程序的根节点下添加一个新的缓存加载子节点来实现,比较特别。

运行中的 Elixir/Phoenix 程序其实是一个树状节点,每一个节点都是一个进程(BEAM虚拟机的概念,不等于系统进程)。典型的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 回调函数中实现了缓存预热逻辑。