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列表,萬人在線的時候,可能每個列表最大能夠達到萬人的列表,性能可能會降低,後續優化,是否有其他的數據結構可以不用存儲一個大的列表
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章