我想实现实时分数排名(有序)。我希望我能快速得到每个球员的得分(键值)。
这里player_id是关键,得分是值。
我尝试使用有序集类型ETS来存储所有玩家得分的列表,但是在键不是值之后的有序集合命令。
Erlang / OTP是否有其他数据结构可以解决我的问题?
答案 0 :(得分:6)
我的理解是你需要维护一个你想要执行的对(Key,Score)列表:
我建议您将数据存储到两个不同的位置:
当您需要显示分数时,有序集就足够了。
当您需要更新分数时,您应该使用ets获取Key的先前分数的值,删除记录{PrevScore,Key}并在有序集中插入{NewScore,Key}并简单地更新第一个具有新价值的东西。
我在1 000 000项目列表上测试了这个解决方案,我的笔记本电脑(Windows XP,核心i5,2Gom,所有磁盘已满,许多应用程序在后台运行)的1分数更新平均需要3μs。我使用的代码:
注意我使用私有表和单个服务器来访问它们以保证2个表的一致性,因此并发进程可以访问服务器(命名得分)而不会发生冲突将按照它们到达服务器的顺序执行。有可能优先回答任何带有2个接收块的get(N)请求。
这是我家用电脑上的测试结果(ubuntu 12.04,8gb ddr,AMD phenom II X6)......
[edit] 我修改了update / 2函数以便同步,所以度量现在很重要,结果更容易理解。看来,对于小于10000条记录的表,ets管理和消息传递的开销是优势的。
-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]).
和测试代码:
-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.
答案 1 :(得分:3)
我不会挑剔erlang选择订购数据的方式:它优化自己或快速查找。但是,您可以在列表中读取ETS表,并使用lists:sort/2
对数据进行排序。
List = ets:tab2list(EtsTable),
lists:sort(SortFun,List).
它仍然很快:ETS表和列表驻留在内存中,只要你有足够的。但是,我会转储ordered_set,你将失去不变的访问时间
此模块是Erlang内置术语存储BIF的接口。 它们提供了存储大量数据的能力 Erlang运行时系统,并具有对数据的持续访问时间。 (在ordered_set 的情况下,请参见下文,访问时间与之成正比 存储的对象数量的对数)。
不要忘记某种形式的基于磁盘的备份,dets或mnesia(如果数据值得保留)。
答案 2 :(得分:3)
有三种解决方案:
ets ordered-set
具有二级索引的仅RAM的mnesia表
NIF
1)有序集,ets表中的记录应为{score,player_id},而不是{player_id,score},以便按分数排序。要获得玩家的分数,只需使用匹配。虽然匹配需要扫描整个表格,但它仍然很快。
剖析:假设有10k玩家,将10k记录插入ets表,然后ets:match_object(Table,{'_',PlayerID})。每场比赛需要0.7到1.1毫秒,这在大多数情况下都足够好。 (CPU:i5 750)
2)mnesia表,使其仅限RAM并使用player_id的二级索引:
mnesia:create_table(user, [{type, ordered_set}, {attributes, record_info(fields, user)}, {index, [playerid]}])
使用mnesia的平均获取时间:读入mnesia:transaction为0.09ms。但是,插入10k记录的速度比其对应的速度快180倍(2820ms vs 15ms)。
如果ets和mnesia都不满足你的性能要求,那么使用n和C可能是另一种选择。但我个人认为过度优化在这里是不值得的,除非它确实是你的瓶颈。