Erlang有序和键值数据结构

时间:2014-01-29 03:43:36

标签: data-structures erlang key-value

我想实现实时分数排名(有序)。我希望我能快速得到每个球员的得分(键值)。

这里player_id是关键,得分是值。

我尝试使用有序集类型ETS来存储所有玩家得分的列表,但是在键不是值之后的有序集合命令。

Erlang / OTP是否有其他数据结构可以解决我的问题?

3 个答案:

答案 0 :(得分:6)

我的理解是你需要维护一个你想要执行的对(Key,Score)列表:

  • 频繁更新分数,
  • 经常显示按分数排序的列表的完整或部分视图

我建议您将数据存储到两个不同的位置:

  • 快速访问密钥的第一个是将密钥存储在第一个字段中,将分数存储在第二个字段中的集合。
  • 第二个是有序集,您将元组{Score,Key}存储为键,没有值。这应该保证每个记录的唯一性,维护按分数排序的列表。

当您需要显示分数时,有序集就足够了。

当您需要更新分数时,您应该使用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管理和消息传递的开销是优势的。 enter image description here

-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,你将失去不变的访问时间

From the ETS manual

  

此模块是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)

  1. match_object比匹配稍快;
  2. 使用匹配规范选择比匹配慢,可能是因为这里的选择非常简单,因为fun2ms的开销超过了它的增益。请注意,选择通常比匹配更受欢迎。
  3. 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可能是另一种选择。但我个人认为过度优化在这里是不值得的,除非它确实是你的瓶颈。