Erlang ordered and key-value data struct
我想实现一个实时的分数排名(排序)。我希望我能尽快得到每个球员的分数(关键值)。
这里,玩家ID是关键,得分是价值。
我尝试使用有序集合类型ets来存储所有玩家的分数列表,但是有序集合的顺序后面的键不是值。
erlang/otp是否有其他数据结构可以解决我的问题?
我理解的是,您需要维护一个您想要执行的配对(键、分数)列表:
- 经常更新分数,
- 经常显示按分数排序的列表的完整或部分视图
我建议您将数据存储到两个不同的ETS中:
- 第一个快速访问密钥的方法是将密钥存储在第一个字段中,将分数存储在第二个字段中。
- 第二个是一个有序的集合,其中存储一个元组得分,key作为key,没有值。这应确保每个记录A的唯一性,并保持按分数排序的列表。
当需要显示分数时,排序集就足够了。
当您需要更新分数时,您应该使用ETS获取键的上一个分数值,删除记录prevscore、key并在有序集合中插入new score、key,然后简单地用新值更新第一个ETS。
我在1000 000个项目列表上测试了这个解决方案,在我的笔记本电脑上更新1分平均需要3微秒(Windows XP、Core i5、2Go RAM、所有磁盘已满,许多应用程序在后台运行)。我使用的代码:
注意:我使用私有表和单个服务器来访问它们,以保证两个表的一致性,因此并发进程可以访问服务器(命名的score),而不会发生冲突,请求将按到达服务器的顺序执行。可以用2个接收块优先回答任何GET(n)请求。
这是我的家用电脑(Ubuntu 12.04,8GB DDR,AMD Phenom II X6)的测试结果…
[编辑]为了同步,我修改了update/2函数,所以度量现在很重要,结果更容易理解。对于小于10000条记录的表,ETS管理和消息传递的开销似乎占优势。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 | -module (score). -export ([start/0]). -export ([update/2,delete/1,get/1,stop/0]). score ! {update,M,S,self()}, receive ok -> ok end. delete(M) -> score ! {delete,M}. get(N) -> score ! {getbest,N,self()}, receive {ok,L} -> L after 5000 -> timeout end. stop() -> score ! stop. start() -> P = spawn(fun() -> initscore() end), register(score,P). initscore() -> ets:new(score,[ordered_set,private,named_table]), ets:new(member,[set,private,named_table]), loop(). loop() -> receive {getbest,N,Pid} when is_integer(N), N > 0 -> Pid ! {ok,lists:reverse(getbest(N))}, loop(); {update,M,S,P} -> update_member(M,S), P ! ok, loop(); {delete,M} -> delete_member(M), loop(); stop -> ok end. update_member(M,S) -> case ets:lookup(member,M) of [] -> ok; [{M,S1}] -> ets:delete(score,{S1,M}) end, ets:insert(score,{{S,M}}), ets:insert(member,{M,S}). delete_member(M) -> case ets:lookup(member,M) of [] -> ok; [{M,S}] -> ets:delete(score,{S,M}), ets:delete(member,M) end. getbest(N) -> K= ets:last(score), getbest(N-1,K,[]). getbest(_N,'$end_of_table',L) -> L; getbest(0,{S,M},L) -> [{M,S}|L]; getbest(N,K={S,M},L) -> K1 = ets:prev(score,K), getbest(N-1,K1,[{M,S}|L]). |
试验规范:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | -module (test_score). -compile([export_all]). init(N) -> score:start(), random:seed(erlang:now()), init(N,10*N). stop() -> score:stop(). init(0,_) -> ok; init(N,M) -> score:update(N,random:uniform(M)), init(N-1,M). test_update(N,M) -> test_update(N,M,0). test_update(0,_,T) -> T; test_update(N,M,T) -> test_update(N-1,M,T+update(random:uniform(M),random:uniform(10*M))). update(K,V) -> {R,_} = timer:tc(score,update,[K,V]), R. |
有三种解决方案:
ETS有序集
带辅助索引的只读内存记忆表
尼夫
1)按顺序设置,ETS表中的记录应为得分,玩家,而不是玩家,得分,以便按得分排序。要想得到一个球员的分数,只需使用match。虽然match需要扫描整个表,但它仍然很快。
分析:假设有10个玩家,将10个记录插入到ets表中,然后ets:match_object(table,"",playerid)。每场比赛需要0.7到1.1毫秒,这在大多数情况下已经足够好了。(CPU:I5 750)
2)mnesia表,使其仅为RAM,并对玩家ID使用辅助索引:
1 | mnesia:create_table(user, [{type, ordered_set}, {attributes, record_info(fields, user)}, {index, [playerid]}]) |
使用mnesia:read-in-mnesia:transaction的平均获取时间为0.09ms。但是,插入10K记录的速度比其ETS对应记录慢180倍(2820毫秒对15毫秒)。
如果ETS和Mnesia都不满足您的性能要求,则使用C和NIF是另一种选择。但我个人认为,优化在这里是不值得的,除非它真的是你的瓶颈。
我不会挑剔Erlang选择如何排序数据:它优化自己或快速查找。但是,您可以在列表中读取ETS表,并使用
它仍然很快:只要您有足够的表和列表,ETS表和列表就驻留在内存中。但是,我会把订购的_集丢弃,你会失去固定的访问时间。
从ETS手册:
This module is an interface to the Erlang built-in term storage BIFs.
These provide the ability to store very large quantities of data in an
Erlang runtime system, and to have constant access time to the data.
(In the case of ordered_set, see below, access time is proportional to
the logarithm of the number of objects stored).
不要忘记一些基于磁盘的备份、DETS或记忆空间(如果数据值得保存的话)。