如何在Phoenix Presence中以常数时间统计在线用户数量?
我正在使用Phoenix搭配LiveView,在某视图中编写了如下代码:
def handle_info(%{event: "presence_diff", payload: _payload}, socket) do # (Payload handling omitted... I just want the count) users_count = map_size(Presence.list("some_topic")) {:noreply, assign(socket, :users_count, users_count)} end
根据map_size的文档说明,该操作是常数时间,这部分没问题,但我对Presence.list的返回值(实际返回Map而非List)存在疑问:每次调用Presence.list时,这个Map是重新创建的,还是只是某种引用?
我担心如果有10000个用户,每次有人加入/离开时,都要为每个用户创建包含10000个元素的Map,这会造成资源浪费。另外Presence模块似乎没有直接获取计数的函数,我查阅了大量源码但未得到明确结论。
注:我知道presence map中每个key对应一个metas数组,但我只关心不同key的数量。
Presence.list的返回值是新创建的Map
Phoenix.Presence的list/1函数每次调用时,都会基于底层存储(比如本地ETS表或Redis)的当前数据生成一个全新的Map实例,不是返回已有Map的引用。这是因为Presence.list需要把底层存储的原始数据(带元信息的条目)转换为用户预期的结构:以用户ID为键、对应元数据数组为值的Map。
如果有10000个在线用户,每次调用Presence.list确实会生成包含10000个键的新Map,频繁调用(比如每次用户进出时)会带来不必要的内存分配和GC开销,你的担忧是合理的。
更高效的用户计数方案
既然你只需要唯一用户的数量,完全没必要每次调用Presence.list。可以利用presence_diff事件的payload来增量更新计数:
presence_diff的payload包含:joins和:leaves两个Map,分别对应新加入和离开的用户。你可以直接用map_size拿到新增/减少的用户数,然后更新当前计数:
def handle_info(%{event: "presence_diff", payload: %{joins: joins, leaves: leaves}}, socket) do count_delta = map_size(joins) - map_size(leaves) new_users_count = socket.assigns.users_count + count_delta {:noreply, assign(socket, :users_count, new_users_count)} end
这种方式完全避开了获取整个在线用户列表,每次操作都是常数时间,哪怕用户量很大也不会有性能问题。
为什么Presence没有直接的计数API
Phoenix.Presence的核心设计是提供灵活的在线状态管理,而计数只是众多衍生需求之一。不同场景下的计数逻辑差异很大(比如是否区分用户元数据、是否按分组统计等),官方没有提供单一的计数函数,而是让开发者基于presence_diff事件或底层存储实现自定义逻辑。
如果需要全局准确的跨节点计数:
- 本地ETS存储:可以用
:ets.select_count直接统计ETS表中的唯一用户数(Phoenix.Presence.Local的ETS表按用户ID分组) - Redis存储:可以用Redis的
HLEN命令(如果Presence用Hash结构存储)获取键的数量
但对大多数实时更新场景来说,用presence_diff增量更新计数是最优解。
内容的提问来源于stack exchange,提问作者user4757175

