Erlang 服务器万人同服排行榜设计优化

测试数据

  • 结论:可以看出使用计数排序,可以极大的提高排序的效率,相比全排序提高3-4倍的效率

测试环境

  • erlang23.3

性能优缺点分析

  • 计数排序的好处是当数据变化时,只需要对增加的分片进行排序,减少排序量
  • 性能弱点:分片保存的是玩家的唯一id列表,当数量足够大时,删除分片数据是一个性能弱点

代码实现

%%%-------------------------------------------------------------------
%%% @doc
%%% @end
%%%-------------------------------------------------------------------
-module(test_rank).
-include_lib("stdlib/include/ms_transform.hrl").

%% API
-export([put_rank_value/2, rank_sort/0,init_data/0]).

-export([test_insert/1]).

%% 采用计数排序法
%% 对固定值的序列很有好处,如vip排行榜,充值排行榜,积分类的排行榜等等
%% 数据结构{rank_value =>{num, list(玩家id,可以不排序,客户端需要的时候再排序)}}
%% 使用ets是因为玩家进程需要读取排行榜数据
%% 而数据的排序是在排行榜管理器进行的

-define(ETS_RANK_SECTION, ets_rank_section).
-define(ETS_RANK_PLAYER, ets_rank_player).

-define(PROCESS_CACHE_PLAYER_SCORE, cache_player_score).
-define(PROCESS_CHANGE_SECTION, change_section).
-define(PROCESS_SECTION_LIST, section_list).
-define(PROCESS_PLAYER_LIST, player_list).
-define(PROCESS_CHANGE_PLAYER_LIST, change_player_list).


-record(r_rank_player, {
    id = 0,       %% 玩家id
    section_id = 0,  %% 区id(排行值)
    time = 0,     %% 时间
    extra = []
}).

-record(r_rank_section, {
    section_id = 0, %% 排行数据
    count = 0,
    rank_player_list = []		%% 排行榜 [玩家id]
}).

init_data() ->
    ets:new(?ETS_RANK_SECTION, [{keypos, #r_rank_section.section_id}, named_table, set, public]),
    ets:new(?ETS_RANK_PLAYER, [{keypos, #r_rank_player.id}, named_table, set, public]),
    ok.

test_insert(Count) ->
    
    F = fun(_Id) ->
            put_rank_value(rand:uniform(50000), #{rank_value => rand:uniform(600),rank_time => rand:uniform(10000)})
%%            put_rank_value(random:uniform(100000000), #{rank_value => random:uniform(10),rank_time => random:uniform(10000)})
        end,
    lists:foreach(F, lists:seq(1,Count)),
    
    {TimerUpdate, _} = timer:tc(fun rank_sort/0),
    io:format("timer update = ~p ~n", [TimerUpdate]),
    
    {Time, _} = timer:tc(fun sort_rank_section/0),
    {TimeAll, _} = timer:tc(fun all_sort/0),
    io:format("rank_sort time = ~p; all time = ~p ~n", [Time, TimeAll]),
    
%%    io:format("player_list = ~p ~n", [ets:tab2list(?ETS_RANK_PLAYER)]),
    io:format("player_list length= ~p ~n", [length(ets:tab2list(?ETS_RANK_PLAYER))]),
    io:format("section length= ~p ~n", [length(ets:tab2list(?ETS_RANK_SECTION))]),
    io:format("change player_list length= ~p ~n", [length(get(?PROCESS_CHANGE_PLAYER_LIST))]),
    
    clear_cache(),
    
    ok.

clear_cache() ->
    
    put(?PROCESS_CACHE_PLAYER_SCORE, #{}),
    put(?PROCESS_CHANGE_SECTION, #{}),
    
    put(?PROCESS_CHANGE_PLAYER_LIST, []),
    
    ok.

all_sort() ->
    case get(?PROCESS_PLAYER_LIST) of
        undefined ->
            [];
        PlayerList ->
            PlayerList_1 = lists:sort(fun sort_player/2, PlayerList),
            put(?PROCESS_PLAYER_LIST, PlayerList_1)
    end,
    ok .

%% 将玩家数据保存到maps数据结构中
%% 次数采用maps的方式在于,可扩展长度,并且数据的随时变更需要快速重新写入
%% value:#{rank_val
put_rank_value(PlayerId,  Value) ->
    case get(?PROCESS_CACHE_PLAYER_SCORE) of
        undefined ->
           put(?PROCESS_CACHE_PLAYER_SCORE, maps:put(PlayerId, Value,#{}));
        Maps ->
            put(?PROCESS_CACHE_PLAYER_SCORE, maps:put(PlayerId, Value, Maps))
    end.

%% 对排行数据进行切片处理,按照关键数据进行排序
rank_sort() ->
    case get(?PROCESS_CACHE_PLAYER_SCORE) of
        undefined ->
            skip;
        Maps ->
            Iterator = maps:iterator(Maps),
            update_rank_interator(Iterator)
    end,
    
    ok.

update_rank_interator(Iterator) ->
    case maps:next(Iterator) of
        {K, V, Iterator_1} ->
            update_rank_info(K,V),
            update_rank_interator(Iterator_1);
        none ->
            ok
    end.


sort_rank_section() ->
    case get(?PROCESS_CHANGE_SECTION) of
        undefined ->
            skip;
        ChangeSectionMaps ->
            MapSectionList = maps:keys(ChangeSectionMaps),
%%             对每个改变的片段进行排序,
            F =
                fun(SectionId) ->
                    case ets:lookup(?ETS_RANK_SECTION, SectionId) of
                        [] ->
                            skip;
                        [#r_rank_section{rank_player_list = RankPlayerList}] ->
%%                            根据其他的条件确定排序
                            RankPlayerList_1 = lists:sort(fun sort_player_time/2, RankPlayerList),
                            ets:update_element(?ETS_RANK_SECTION, SectionId,[{#r_rank_section.rank_player_list,RankPlayerList_1}])
                    end
                end,
            lists:foreach(F, MapSectionList)
    end.

sort_player_time(PlayerA, PlayerB) ->
    [#r_rank_player{time = TimeA}] = ets:lookup(?ETS_RANK_PLAYER,PlayerA),
    [#r_rank_player{time = TimeB}] = ets:lookup(?ETS_RANK_PLAYER, PlayerB),
    TimeA < TimeB.

sort_player(PlayerA, PlayerB) ->
    
    [#r_rank_player{time = TimeA, section_id = SectionIdA}] = ets:lookup(?ETS_RANK_PLAYER,PlayerA),
    [#r_rank_player{time = TimeB, section_id = SectionIdB}] = ets:lookup(?ETS_RANK_PLAYER, PlayerB),
    case SectionIdA =:= SectionIdB of
        true ->
            TimeA < TimeB;
        false ->
            SectionIdA > SectionIdB
    end.
    
    

%% 设置玩家的每个片段
update_rank_info(PlayerId, #{rank_value := RankValue}=RankInfo) ->
    case get_rank_section(PlayerId) of
        0 ->
            update_player_section(PlayerId, RankInfo);
        RankValue ->
            skip;
        OldSectionId ->
            delete_player_section(PlayerId, OldSectionId),
            update_player_section(PlayerId, RankInfo)
    end,
    
    set_change_section(RankValue),
    
    ok.

set_change_section(RankValue) ->
    
    case get(?PROCESS_CHANGE_SECTION) of
        undefined ->
            put(?PROCESS_CHANGE_SECTION, maps:put(RankValue,1, #{}));
        Maps ->
            put(?PROCESS_CHANGE_SECTION, maps:put(RankValue, 1, Maps))
    end,
    
    ok.

update_player_section(PlayerId, #{rank_value :=SectionId}=RankInfo) ->
    
    case ets:lookup(?ETS_RANK_SECTION, SectionId) of
        [] ->
            update_player_rank_info(PlayerId, RankInfo),
            
            ets:insert(?ETS_RANK_SECTION, #r_rank_section{section_id = SectionId, count = 1, rank_player_list = [PlayerId]}),
            
            add_section_list(SectionId);
        
        [#r_rank_section{count = Count, rank_player_list = RankPlayerList}=RankSection|_] ->
            
            update_player_rank_info(PlayerId, RankInfo),
    
            RankSection_1= RankSection#r_rank_section{count = Count+1, rank_player_list = [PlayerId|RankPlayerList]},
            ets:insert(?ETS_RANK_SECTION, RankSection_1)
    end.

update_player_rank_info(PlayerId, #{rank_value :=SectionId,rank_time := Time}) ->
    
    case get(?PROCESS_CHANGE_PLAYER_LIST) of
        undefined ->
            put(?PROCESS_CHANGE_PLAYER_LIST, [PlayerId]);
        ChangeList ->
            put(?PROCESS_CHANGE_PLAYER_LIST, [PlayerId|ChangeList])
    end,
    
    case ets:lookup(?ETS_RANK_PLAYER, PlayerId) of
        [] ->
            case get(?PROCESS_PLAYER_LIST) of
                undefined ->
                    put(?PROCESS_PLAYER_LIST, [PlayerId]);
                PlayerList ->
                    put(?PROCESS_PLAYER_LIST, [PlayerId|PlayerList])
            end,
            
            ets:insert(?ETS_RANK_PLAYER, #r_rank_player{id = PlayerId,section_id = SectionId,time = Time});
        [#r_rank_player{}] ->
            ets:update_element(?ETS_RANK_PLAYER, PlayerId,
                [{#r_rank_player.section_id,SectionId},{#r_rank_player.time,Time}])
    end.

add_section_list(SectionId) ->
    
    case get(?PROCESS_SECTION_LIST) of
        undefined ->
            put(?PROCESS_SECTION_LIST, [SectionId]);
        SectionList ->
            SectionList_1 = lists:sort([SectionId|SectionList]),
            put(?PROCESS_SECTION_LIST, lists:reverse(SectionList_1))
    end,
    
    ok.

delete_player_section(PlayerId, SectionId) ->
    
    case ets:lookup(?ETS_RANK_SECTION, SectionId) of
        [] ->
            skip;
        [#r_rank_section{count = Count, rank_player_list = RankPlayerList}=RankSection] ->
            RankPlayerList_1 = lists:delete(PlayerId, RankPlayerList),
            case RankPlayerList_1 of
                [] ->
                    ets:delete(?ETS_RANK_SECTION, SectionId),
                    delete_section_list(SectionId);
                [_|_] ->
                    RankSection_1 = RankSection#r_rank_section{count = Count-1,rank_player_list = lists:delete(PlayerId,RankPlayerList)},
                    ets:insert(?ETS_RANK_SECTION, RankSection_1)
            end
    end,
    
    ok.

delete_section_list(SectionId) ->
    
    case get(?PROCESS_SECTION_LIST) of
        undefined ->
            skip;
        SectionList ->
            SectionList_1 = lists:delete(SectionId, SectionList),
            put(?PROCESS_SECTION_LIST, SectionList_1)
    end,
    
    ok.

get_rank_section(PlayerId) ->
%%    io:format("rank section id = ~p, val = ~p~n", [PlayerId, ets:lookup(?ETS_RANK_PLAYER, PlayerId)]),
    case ets:lookup(?ETS_RANK_PLAYER, PlayerId) of
        [] ->
            0;
        [#r_rank_player{section_id = SectionId}] ->
            SectionId
    end.

后续说明

  • 每一个分片的玩家列表保存的是玩家的id列表,万人在线的时候,可能每个列表最大能够达到万人的列表,性能可能会降低,后续优化,是否有其他的数据结构可以不用存储一个大的列表
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章