在elixir中缓存昂贵的计算

时间:2016-02-05 07:43:08

标签: elixir

我在elixir中有一个看起来像这样的Web应用程序

defmodule Test do
  use Plug.Router

  plug :match
  plug :dispatch

  def expensiveComputation() do
     // performs an expensive computation and
     // returns a list
  end

  get "/randomElement" do
    randomElement = expensiveComputation() |> Enum.random
    send_resp(conn, 200, randomElement)
  end

end

每当我向GET发出/randomElement请求时,都会调用expensiveComputationexpensiveComputation函数需要很长时间才能运行,但每次调用它都会返回相同的内容。缓存结果的最简单方法是什么,以便它在启动时只运行一次?

2 个答案:

答案 0 :(得分:6)

您可以使用ETS来缓存昂贵的计算。这是我最近写的东西,它可能不是一个成熟的缓存解决方案,但对我来说效果很好:

defmodule Cache do
  @table __MODULE__

  def start do
    :ets.new @table, [:named_table, read_concurrency: true]
  end

  def fetch(key, expires_in_seconds, fun) do
    case lookup(key) do
      {:hit, value} ->
        value
      :miss ->
        value = fun.()
        put(key, expires_in_seconds, value)
        value
    end
  end

  defp lookup(key) do
    case :ets.lookup(@table, key) do
      [{^key, expires_at, value}] ->
        case now < expires_at do
          true -> {:hit, value}
          false -> :miss
        end
      _ ->
        :miss
    end
  end

  defp put(key, expires_in_seconds, value) do
    expires_at = now + expires_in_seconds
    :ets.insert(@table, {key, expires_at, value})
  end

  defp now do
    :erlang.system_time(:seconds)
  end
end

首先,你需要在某处调用Cache.start,因此将创建ETS表(例如在你的应用程序的start函数中)。然后你可以像这样使用它:

value = Cache.fetch cache_key, expires_in_seconds, fn ->
  # expensive computation
end

例如:

Enum.each 1..100000, fn _ ->
  message = Cache.fetch :slow_hello_world, 1, fn ->
    :timer.sleep(1000) # expensive computation
    "Hello, world at #{inspect :calendar.local_time}!"
  end
  IO.puts message
end

答案 1 :(得分:5)

在Elixir中,当你想要状态时,你几乎总是需要一个进程来保持这种状态。 Agent模块特别适合您想要的操作 - 只需包装一些值并访问它。这样的事情应该有效:

defmodule Cache do
  def start_link do
    initial_state = expensive_computation
    Agent.start_link(fn -> initial_state end, name: __MODULE__)
  end

  def get(f \\ &(&1)) do
    Agent.get(__MODULE__, f)
  end

  defp expensive_computation do
    # ...
  end
end

然后,您可以正常将Cache插入监管树,当您需要Cache.get的结果时,只需expensive_computation

请注意,在启动监督树的过程中,这将在启动时运行expensive_computation。如果非常昂贵 - 大约10秒或更长时间 - 您可能希望将计算移至Agent进程:

def start_link do
  Agent.start_link(fn -> expensive_computation end, name: __MODULE__)
end

在这种情况下,您需要处理缓存为空的情况,而在第一个示例中,阻止启动直到expensive_computation完成。您可以根据启动顺序中的Cache放置工作人员来使用它。