测试数据
- 结论:可以看出使用计数排序,可以极大的提高排序的效率,相比全排序提高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列表,万人在线的时候,可能每个列表最大能够达到万人的列表,性能可能会降低,后续优化,是否有其他的数据结构可以不用存储一个大的列表