測試數據
- 結論:可以看出使用計數排序,可以極大的提高排序的效率,相比全排序提高3-4倍的效率
測試環境
性能優缺點分析
- 計數排序的好處是當數據變化時,只需要對增加的分片進行排序,減少排序量
- 性能弱點:分片保存的是玩家的唯一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列表,萬人在線的時候,可能每個列表最大能夠達到萬人的列表,性能可能會降低,後續優化,是否有其他的數據結構可以不用存儲一個大的列表