如何从List创建许多GenServer进程并存储在其中的地图数据?

时间:2016-07-18 12:01:19

标签: elixir phoenix-framework otp gen-server

在这两种方法中,我坚持如何通过给定的一组id或组映射进程,然后将存储的struct映射到过滤数据。

%{group => [users]}实施。

我意识到群组将与用户相反,因此我创建了一个使用群组名称作为密钥的流程模块。

我担心将来会有很多用户在少数群体中,所以我的问题是如何拆分当前的UserGroupServer模块以保留由组名识别的许多独立进程?我想保留当前模块的功能,在init进程中按组列表,另外我不知道如何映射每个进程以通过user_id获取组?

目前我只通过在子树列表中添加模块在Phoenix lib/myapp.ex中启动一个进程,因此我可以直接在频道中调用UserGroupServer

defmodule UserGroupServer do
  use GenServer

  ## Client API
  def start_link(opts \\ []) do
   GenServer.start_link(__MODULE__, :ok, opts)
  end

  def update_user_groups_state(server, data) do
    {groups, user_id} = data
    GenServer.call(server, {:clean_groups, user_id}, :infinity)
    users = Enum.map(groups, fn(group) ->
      GenServer.call(server, {:add_user_group, group, user_id}, :infinity)
    end)
    Enum.count(Enum.uniq(List.flatten(users)))
  end

  def get_user_groups(server, user_id) do
    GenServer.call(server, {:get_user_groups, user_id})
  end

  def users_count_in_gorup(server, group) do
    GenServer.call(server, {:users_count_in_gorup, group})
  end

  ## Callbacks (Server API)

  def init(_) do
    {:ok, Map.new}
  end

  def handle_call({:clean_groups, user_id}, _from, user_group_dict) do
    user_group_dict = user_group_dict
    |> Enum.map(fn({gr, users}) -> {gr, List.delete(users, user_id)} end)
    |> Enum.into(%{})
    {:reply, user_group_dict, user_group_dict}
  end

  def handle_call({:add_user_group, group, user_id}, _from, user_group_dict) do
    user_group_dict = if Map.has_key?(user_group_dict, group) do
      Map.update!(user_group_dict, group, fn(users) -> [user_id | users] end)
    else
      Map.put(user_group_dict, group, [user_id])
    end
    {:reply, Map.fetch(user_group_dict, group), user_group_dict}
  end
end

试验:

defmodule MyappUserGroupServerTest do
  use ExUnit.Case, async: false

  setup do
    {:ok, server_pid} = UserGroupServer.start_link
    {:ok, server_pid: server_pid}
  end

  test "add users", context do
    c1 = UserGroupServer.update_user_groups_state(context[:server_pid], {[:a, :b, :c], 1})
    assert(1 == c1)
    c2 = UserGroupServer.update_user_groups_state(context[:server_pid], {[:c, :d], 2})
    assert(2 == c2)
    c3 = UserGroupServer.update_user_groups_state(context[:server_pid], {[:x], 2})
    assert(1 == c3)
    c4 = UserGroupServer.update_user_groups_state(context[:server_pid], {[:d], 1})
    assert(1 == c4)
    c5 = UserGroupServer.update_user_groups_state(context[:server_pid], {[:d, :c], 2})
    assert(2 == c5)
  end
end

旧方法%{user => [groups]}

监视器存储分配给user_id的组列表。如何查找给定组中的用户?我是否要创建单独的进程来处理组和用户ID之间的m..n关系?我应该更改以获取每个用户组然后映射它们?

服务器实施:

defmodule Myapp.Monitor do
  use GenServer

  def create(user_id) do
    case GenServer.whereis(ref(user_id)) do
      nil -> Myapp.Supervisor.start_child(user_id)
    end
  end

  def start_link(user_id) do
    GenServer.start_link(__MODULE__, [], name: ref(user_id))
  end

  def set_groups(user_pid, groups) do
    try_call user_pid, {:set_groups, groups}
  end

  def handle_call({:set_groups, groups}, _from, state) do
    { :reply, groups, groups } # reset user groups on each set_group call.
  end

  defp ref(user_id) do
    {:global, {:user, user_id}}
  end

  defp try_call(user_id, call_function) do
    case GenServer.whereis(ref(user_id)) do
      nil -> {:error, :invalid_user}
      user_pid -> GenServer.call(user_pid, call_function)
    end
  end
end

监:

defmodule Myapp.Supervisor do
  use Supervisor

  def start_link do
    Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  def start_child(user_id) do
    Supervisor.start_child(__MODULE__, [user_id])
  end

  def init(:ok) do
    supervise([worker(Myapp.Monitor, [], restart: :temporary)], strategy: :simple_one_for_one)
  end
end

示例:

Monitor.create(5)
Monitor.set_groups(5, ['a', 'b', 'c'])
Monitor.create(6)
Monitor.set_groups(6, ['a', 'b'])
Monitor.set_groups(6, ['a', 'c'])

# Monitor.users_in_gorup('a') # -> 2
# Monitor.users_in_gorup('b') # -> 1
# Monitor.users_in_gorup('c') # -> 2

# or eventually more desired:
# Monitor.unique_users_in_groups(['a', 'b', 'c']) # -> 2
# or return in set_groups unique_users_in_groups result

1 个答案:

答案 0 :(得分:4)

在跳转到进程和gen_servers之前,你总是需要考虑数据结构。

您打算如何添加数据?多常?你打算如何查询?多久一次?

在您的示例中,您提到了三个操作:

  • 为用户设置组(重置所有以前设置的组)
  • 返回群组中的所有用户
  • 以组的形式返回唯一身份用户

使用Elixir中最基本的类型(列表和地图),您可以通过两种方式排列数据:

  • map其中key是user,value是group list(%{user => [groups]}
  • 或反过来(%{group => [users]}

对于这两个实现,您可以评估操作的速度。对于%{user => [groups]}

  • 为用户设置的组为O(1)(只需更新地图中的键)
  • 返回组O(n*m)中的所有用户,其中n是用户数,m是组数(对于所有n用户,您需要检查是否是通过扫描可能的m组名称
  • 在组中
  • 群组中的唯一身份用户与上述+排序和重复数据删除相同

对于使用%{group => [users]}的实施:

  • 为用户设置的组为O(n*m)(如果用户在那里,则需要扫描所有组,删除它,然后仅为新组设置)如果设置组仅将用户添加到新组而不删除它首先,它只是按照与输入中的组数量(而不是所有组)成比例的时间添加用户
  • 返回群组O(1)中的所有用户 - 只需查询地图
  • 与查询+排序和重复数据删除中的组数量成比例

这表明如果您的监视器快速更新并且查询频率较低,则首次实施会更好。如果你不经常更新它,那么第二个会好得多,但是一直在查询它。

在没有任何actor或gen_server的情况下实现其中一个解决方案并且可以告诉它可行之后,您可能希望将pids视为映射键并重写算法。您还可以考虑仅使用一个进程来存储所有数据。这还取决于你的确切问题。祝你好运!