排名系统学习

排名榜用于游戏中玩家间的排名和对比,从战力、等级、装备评分、公会、副本通关层数等很多其他模块的数据可以来用进行排行。为了减少代码重复和工作量,所以一个通用排行系统是必须。

排行的抽象

Rank是对所有排名名词的抽象,每个进行排名的角色都拥有Rank

public class Rank {
    // 注意,第一名为零
    public static final NO1 = 0;
    public static final Rank NOT_IN_RANK = new Rank(false, -1);
    // 是否上榜
    private final boolean inRank;

    private final int rank;

    private Rank(boolean inRank, int rank) {
        this.inRank = inRank;
        this.rank = rank;
    }

    public Rank(int rank) {
        this(true, rank);
    }

    // 在榜上玩家处理
    public void ifInRank(Consumer<Integer> consumer) {
        if(this.isInRank()) {
            consumer.accept(rank);
        }
    }

    // 判断排名是否在排行榜上
    public boolean isInRank() {
        return inRank;
    }

    // 获取排名值
    public int getRank() {
        return rank;
    }
}

单个排行榜的信息,多个Rank可以组成RankList,而RankList包含各种类型的排名,并且会持久化的

@Entity
@Cached(type=CacheType.MANUAL)
@InitailConfig(type=InitialType.ALL)
public class RankList extends AbstractEntity<RankPk> {
    // 排名前n名变化时,才通知,包括调出前3名的
    private transient static final int NOTIFY_RANK = 3;
    @Id
    private RankPk key;
    @Lob
    private String rankJson;
    private transient List<RankEntity> rankList;
    private transient Map<Long, RankEntity> rankMap;

    private transient ReentrantWriteLock lock = new ReentrantReadWriteLock();

    // 跨服结算
    private long lastSettleTime;
    // 跨服同步缓存
    private transient AtomicBoolean sync = new AtomiBoolean(false);

    // 记录有改变的排名实体,用于跨服增量同步
    private transient RankUpdateSet changeSet;
    // true为内存排行,不用入库
    private transient boolean memory;

    public static RankList valueOf(RankPk rankPk, boolean memory) {
        RankList vo = new RankList();
        vo.kwy = rankPk;
        //+1是为了排行榜满时,新增上榜数据,在逻辑运算时不用扩容
        vo.rankList = new ArrayList<>(rankPk.getRankType().getMaxRankSize()+1);
        vo.rankMap. = new ConcurrentHashMap<>(rankPk.getRankType().getMAxRankSize()+1);
        if(rankPk.getRankType().isCross()) {
            vo.changeSet = new RankUpdateSet(vo.key);
        }
        vo.memory = memory;
        return vo;
    }

    public static RankList valueOf(RankPk rankPk) {
        return valueOf(rankPk, false);
    }

    public void directInsertRank(RankEntity rankEntity) {
        if(rankMap.put(rankEntity.getUuid(), rankEntity) == null) {
            this.rankList.add(rankEntity.getRank(), rankEntiy);
        }
    }

    public RankEntity removeFromRank(int rank) {
        RankEntity remove = this.rankList.remove(rank);
        this.rankMap.remove(remove.getUuid());
        return remove;
    }

    private void updateRecord(RankEntity rankEntity) {
        if(getId().getRankType().isCross() && ServerType.isCenterServer()) {
            this.changeSet.updateRank(rankEntity.getUuid());
        }
    }

    @Ovveride
    public RankPk getId() {
        return key;
    }    

    @Override
    protected void serialize() {
        rankList.forEach(RankEntity::serialize);
        rankJson = rankJson.object2String(rankList);
    }

    @Override

    public void unSerialize() {

    }

    public boolean isNo1(long uuid) {
        RankEntity rankEntity = rankMap.get(uuid);
        if(rankEntity == null) {
            return false;
        }
        return rankEntity.isNo1();
    }

    public Rank getRank(long uuid) {
        RankList rankEntity = rankMap.get(uuid);
        if(rankEntity == null) {
            return Rank.NOT_IN_RANK;
        } else {
            return new Rank(rankEntity.getRank());
        }
    }

    public RankEntity getRankEntity(long uuid) {
        return rankMap.get(uuid);
    }

    // 清空排行榜
    public void clear() {
        lock.writeLock.lock();
        try {
            rankMap.clear();
            rankList.clear();
        } finally {
            lock.writeLock().unlock();
        }
        RankManager.self().update(this);
    }

    // 清空排行榜,但不更新
    public void clear() {
        lock.writeLock.lock();
        try {
            rankMap.clear();
            rankList.clear();
        } finally {
            lock.writeLock().unlock();
        }
    }

    // 添加数据,不做排序处理
    public void addRankEntity(RankEntity rankEntyry) {
        if(rankMap.put(rankEntity.getUuid(), rankEntity) == null) {
            rankList.add(rankEntity);
        }        
    }

    // 清除过多的数据
    public void removevOverfull(Predicate<RankEntity> predicate) {
        // 可移除玩家数 = 榜单玩家数量-榜单可显示最大值
        int count = rankList.size() - key.getRankType().getLimit();
        if(count <= 0) {
            return;
        }
        lock.writeLock().lock();
        try {
            for(int i=0; i < rankList.size() && c<count; i++) {
                rankEntity = rankList.get(i);
                if(predicae.test(rankEntity)) {
                    removeWithIndex(i);
                    c++;
                }
            }
            // 重新设置排名
            for(int i=0; i < rankList.size(); i++) {
                setNewRank(rankList.get(i),i);
            }
            RankManager.self().update(this);
        } finally {
            lock.writeLock().unlock();
        }
    }

    private void setNewRank(RankEntity rankEntity, int newRank) {
        int oldRank = rankEntity.getRank();
        if(newRank == oldRank) {
            return;
        }
        rankEntity.getRank(newRank);
        rankEntity.setUpdateTime(System.currentTimeMills());
        if(newRank < NOTIFY_RANK || oldRank < NOTIFY_RANK>) {
            try {
                this.key.getRankType().getHandler().onChange(rankEntity.getUuid(), oldRank, rankEntity.getRank());
            } catch(Exception e) {
                logger.error("rank change", e);
            }
        }
    }

    // 移出数据,不做排序处理
    public void removeWithIndex(int index) {
        RankEntity rankEntity = rankList.remove(index);
        if(rankEntity != null) {
            rankMap.remove(rankEntity.getUuid());
        }
        RankManger.self().update(this);
    }

    // 重置排行顺序
    public void sortWithRank() {
        lock.writeLock().lock();
        try {
            Collections.sort(rankList);
            for(int i=0; i< rankList.size(); i++) {
                rankList.get(i).sentRank(i);
            }
        } finally {
            lock.writeLock().unlock();
    }

    // 更新排行榜数据
    public RankEntity update(long uuid, long newValue, Consumer<RankEntity> extraUpdater) {
        lock.writeLock().lock();
        try {
            RankEntity rankEntity = rankMap.get(uuid);
            if(rankEntity == null) {
                // 未上榜的单位
                if(!canIntoRank(newValue)) {
                    return null;
                }
                rankEntity = new RankEntity(key.getRankType(), uuid, rnkList.size(), newValue);
                // 上榜更新
                addRankEntity(rankEntity);
                update(rankEntity, RankWave.UP, extraUpdater, true);
                // 记录新增的排名
                updateRecord(rankEntity);
            } else {
                // 已经在榜上的单位
                long oldValue = rankEntity.getValue();
                if(!key.getRankType().needUpdateWhenChange(oldValue,newValue)) {
                    // 排行值不需要进行变化
                    return null;
                }
                rankEntity.setValue(newValue);
                update(rankEntity, key.getRankType().comprator(oldValue, newValue), extraUpdater, false);
                // 记录新增的排名
                updateRecord(rankEntity);
            }
        } finally {
            lock.writeLock().unlock();
        }
    }

    // 判断是否满足进入排行榜条件
    public boolean canIntoRank(long newValue) {
        return !(isFull() && key.getRankType().comparator(getLastValue(), newVlaue) != RankWave.UP);
    }

    // 更新排行榜
    private void update(RankEntuty updateEntity, RankWave wave, Consumer<RankEntity> extraUpdater, boolean first) {
        if(wave == RankWave.NONE) {
            return;
        }
        int mark = wave.getMark();
        int myOldRank = updateEntity.getRank();
        for(int i=updateEntiy.getRank()+ mark; i<rankList.size() && i>=0; i +=mark) {
            RankEntity exist = rankList.get(i);
            if(key.getRankType().comparator(exist.getValue(), updateEntity.getValue()) != wave) {
                break; // 找到合适的位置了
            }
            // 交换位置
            rankList.set(i - mark, exist);
            setNewRank(exist, i-mark);
            rankList.set(i, updateEntiy);
            updateEntity.setRank(i);
            updateEntiy.setRank(i);
        }
        // 更新额外信息
        if(extraUpdater != null) {
            extraUpdater.accept(updateEntiy);
        }
        if(rankList.size() > key.getRankType().getMaxRankSize) {
            removeWithIndex(rankList.size() -1);
        }
        boolean isRankChange = myOldRank != updateEntity.getRank();
        if(isRankChange) {
            // 名次有变更在记录更新时间
            updateEntity.setUpdateTime(System.currentTimeMills());
        }
        boolean isChange = first || (isRankChange && (myOldRank < NOTIFY_RANK || updateEntity.getRank() < NOTIFY_RANK ));
        if(isChange) {
            this.key.getRankType().getHandler.onChange(updateEntity.getUuid(), myOldRank, updateEntity.getRank());
        }
    }

    // 排行榜最低
    public long getLastValue() {
        lock.readLock.lock();
        try {
            if(rankList.isEmpty()) {
                return 0;
            }
            return rankList.get(rankList.size()-1).getValue();
        } finally {
            lock.readLock().unlock();
        }
    }

    // 排行榜是否已满
    public boolean isFull() {
        lock.readLock().lock();
        try {
            return rankList.size() >= key.getRankType().getMaxRankSize();
        } finally {
            lock.readLock().unlock();
        }
    }

    // 获取分页数据,例如获取前20名(0,20)
    public List<RankEntity> page(int start, int end) {
        // ...
    }

    public List<RankEntity> getAll() {
        // ...
    }

    // 获取榜单第一
    public RankEntity getFirstRankEntity() {
        if(rankList.isEmpty()) {
            return null;
        }
        return rankList.get(0);
    }
    // 移出数据,更新排名
    public void removeWithUuidAndSort(long uuid) {

    }

    // 重置排名
    public void resetRank(int rank) {
        for(int i=rank; i< this.rankList.size(); i++) {
            this.rankList.get(i).setRank(i);
        }
    }
    public void replaceRankList(List<RankEntity> rankList) {
        lock.writeLock().lock();
        try {
            this.rankList = rankList;
            this.rankMap.clear();
            for(RankEntity rankEntiy : this.rankList)) {
                this.ranjMap.puifAbsent(rankEntity.getUuid(), rankEntity);
            }
        } finally {
            lock.writeLock().unlock();
        }
    }

    public boolean isMemory() {
        return memory;
    }

}

排行榜数据波动方式

public enum RankWave {
    // 排名上升
    UP(-1),
    // 排名下降
    DOWN(1),
    // 不变
    NONE(0),

    private int mark;

    RankWave(int mark) {
        this.mark = mark;
    }
    public int getMark() {
        return mark;
    }
}

排行榜数据库联合主键

@Embeddable
public class RankPk implements Serializable, Comparable<RankPk> {

    @Enumerated(EnumType.STRING)
    @Column(columnDefinition = "varchar(32) NOT NULL")
    private RankType rankType;

    private long subKey;

    public RankPk() {

    }

    public static RankPk valueOf(RankType rankType, long subKey) {
        RankRk vo = new RankPk();
        vo.subKey = subkey;
        vo.rankType = rankType;
        return vo;
    }

   // 各种get/set ......
    @Override
    public int compareTo(RankPk p) {
        if(rankType != o.rankType()) {
            return Integer.compare(rankType.oridianl(), o.rankType.ordinal());
        }
        return Long.compare(subKey,o.subKey);
    }

    @Override
    public boolean equals() {
        if(this== o) {
            return true;
        }
        if(o ==null || getClass() != o.getClass()) {
            return false;
        }
        RankRk ranRk = (RankPk) o;
        return rankType == rankPk.rankType && 
        Objects.equals(subKey, rankPk.subKey);
    }

    // hashCode() 与 toString() 方法 .......


排行类型

排行榜类型在枚举里定义,然后也要在RankResource.xlsx里配置相同的排行类型。

@Desc("排行榜类型")
public enum RankType implements EnumReadable,ValueConverter, EnumValue {

    @Desc("战力排行")
    FIGHT_FORCE(1) {
        @Override
        public int getMaxRankSize() {
            // 比排行榜可现实的最大数大0.2,为了在移除流失玩家时做候补
            return (int) (this.getLimit() * 1.2);
        }
    }
    @Desc("等级排行")
    LEVEL(2) {
        @Override
        public int getMaxRankSize() {
            // 比排行榜可显示的最大数大0.2,为了在移除流失玩家时做候补
            return (int) (this.getLimit() * 1.2);
        }
    }

    // ......

    @Desc("跨服争霸全服模式个人英雄塔排行")
    TRANSFER_HERO_TOWER_SER_PER(34,ServerGroupType.godKillTowerServer),
    @Desc("飞升等级榜")
    FLY_LEVEL(38),

    RankType(int value) {
        this(value,null);
    }

    RankType(int value, ServerGroupType serverGroupType) {
        this.value = value;
        this.serverGroupType = serverGroupType;
    }
    // 排行榜可显示的最大数
    public int getLimit() {
        return RankManger.self().getRankResource(this).getRankAmount();
    }
    // 服务器排行的最大数
    public int getMaxRankSize() {
        return this.getLimit();
    }

    // 比较排行榜值的大小,如果有不同的排序方式,可以覆盖该方法
    public RankWave comparator(long oldValue, long newValue) {
        return (oldValue < newValue) ? RankWave.UP : ((oldValue == newValue) ? rankWave.NONE:RankWave.DOWN);
    }

    // 当排行榜内的单位,在数据变化后,检测是否需要更新值,更新后会导致排行变动
    public boolean needUpdateWhenChange(long oldValue, long newValue) {
        return oldValue != newValue;
    }

    @Override
    public int value() {
        return value;
    }

    // 获取排行榜处理器
    public BaseRankHandler getHandler() {
        return BaseRankHandler.get(this);
    }

    public boolean isCross() {
        return serverGroupType != null;
    }

    public ServerGroupType getServerGroupType() {
        return serverGroupType;
    }
}

排行配置

@Resouce
public class RankResource {
    @ResourceId
    public RankType rankType;
    // 排行榜上榜最大人数
    private ubr rankAmount;
    // 客户端显示最大数量。因为发奖原因,可能服务端需要保存的排行榜比客户端需要显示的数量大
    private int showAmount;
    // 达到排行榜第一的通告
    private MessageCode rankUpNumberOne;
    // 排行榜第一登陆的通告
    private MessageCode numberOneLoginOn;


}

排行处理器

角色的游戏数据变化会影响排行榜的变化,所以需要对各种排行类型定义对应的处理器

排行榜处理接口

public abstract class BaseRankHandler {
    // 所有排行榜类型对应处理器接口
    private static EnumMap<RankType, BaseRankHandler> handlerMap = new EnumMap<>(RankType.class);

    public static BaseRankHandler get(RankType type) {
        return handlerMap.get(type);
    }

    public static Collection<BaseRankHandler> getHandlers() {
        return handlerMap.values();
    }

    @PostConstruct
    public void init() {
        RankTpye type = type();
        if(handlerMap.constaisKey(type)) {
            throw new RuntimeException(String.format("RankHandler type[%s]重复!",type));
        }
        handlerMap.put(type, this);
    }

    public RankList getRanKList() {
        return RankManager.self().getRankList(type());
    }

    public RankList getRankList(long subKey) {
        return RankManager.self().getRankList(type(), subKey);
    }

    public Rank getRank(long uuid) {
        return getRankList().getRank(uuid);
    }

    // 更新排行信息
    protected void updateRank(long subKey, long uuid, long newValue, Consumer<RankEntiy> extraUpdater)   {
        getRankList(subKey).update(uuid,newValue, extraUdater);
    }

    // 每一个排行榜类型,都应该有一个`BaseRankHandler`
    public abstract RankType type() ;

    // 获取该排行榜中,与玩家关联的排名实体。以玩家为主体的排行就是玩家本身的排行实体
    // 不以玩家为主体的排行,例如公会排名名,就是玩家所属工会排名
    public abstract RankEntity getRankEntity(long uuid);     

    // 排行主键
    public abstract RankEntity getRankEntity(long uuid);

    // 游戏服发送排行榜信息
    public void sendRankList(long playerId, int start, int terminal) {

    }

    public <T> T getRankListPacket(long playerId, int start, int terminal) {
        return null;
    }

    // 该排名奖励发给哪些玩家
    public List<Long> getRewardPlayerIds(long uuid) {
        return Collection.emptyList()
    }

    // 清空排行榜
    public void clear() {
        RankManager.self().walkRankList(type(), RankList::clear());
    }

    // 名次变化,只有前几名排行变化才触发
    public void onChange(long uuid, int oldRank, int newRank) {

    }

    // 给排行榜内发奖
    public void sendRankReward(long uuid, int rank) {}

}

实现了BaseRankHandler接口的处理器

玩家等级排行榜处理器

@Component
public class PlayerLevelRankHandler extends BasePlayerRankHandler {
    @Overide
    public RankType type() {
        retrun RankType.LEVEL;
    }

    @Override
    public long getRankValue(Player player) {
        return palyer.getLevel();
    }

    // 战力排行
    @ReceiverAnno
    public void updateRank(Player player) {
        // 玩家等级不会降低,不做降低移出排行榜操作,如果前提不成立,需要支持降级导致的出榜逻辑
        if(getRankValue(player) >= RankContants.getInstance().getEnterLevelRankListValue()) {
            super.updateRank(player);
        }
    }
}

总结

添加一个新的排行榜,需要在RankType添加一个新的枚举,在RankResource.xlsx添加对应的排行类型。实现BasePlayerRankHandler为排行榜指定处理器,描述如果获取排行值、如果处理排行的更新。