首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >带滤波的大型排行榜排名

带滤波的大型排行榜排名
EN

Stack Overflow用户
提问于 2014-12-30 13:42:36
回答 3查看 3.5K关注 0票数 8

我们正在构建一个大型的多人教育游戏,其中有数以百万计的条目在领头板上(基于聚合的XPs获取)。在游戏结束后,我们需要显示领导板和这个球员/学生是如何排名的。但是,有几个过滤器为这个领导板(全球/按国家,按月/年/今天,按年龄等)可以混合在一起。“给我领队for my Country for the last month”。组合数为20。

我的问题是如何存储这样一个结构,定期更新;重新计算的排名必须在每次游戏后进行。目前,一个典型的完整排行榜上,来自超过150个国家的球员有大约5百万的参赛作品。

  1. 我以前有一个带有3个节点的MySQL集群表(userid、xps、countryid),但是通过XPs排序(无论是在DBMS中还是在需要DB所有数据的应用程序中)都被证明太慢,因为用户数量越来越大(用户超过20K)。这是一个有趣的帖子,但是对于每个查询来说,再重复一次,半秒的时间太长了。
  2. 然后我们使用了REDIS (请参阅这个帖子),但是过滤是这里的问题。我们对前五名和其他人分别使用了不同的列表。前五名立即更新,其余部分则延迟20-30分钟。事实上,我们根据主板的缓存实例(使用真实的XPs,而不是缓存的)对这个用户进行排名,所以这是可以接受的。非五强的实时并不是先决条件。这对于一个全球排名来说是很好的,但是如何根据月份和/或国家和/或年龄来筛选结果。我们需要为每个过滤组合保留一个列表吗?
  3. 我们还测试了Java中的自定义结构(使用它作为Java缓存服务器,功能与REDIS类似),目前仍在试验它。实现我们目标的最佳结构组合是哪一种?最后,我们使用每个过滤组合使用一个列表,例如Map<FilteringCombination, SortedList<User>>,然后对特定密钥的列表进行二进制搜索。这样,一个完成的游戏需要几个插入(比如X ),但它需要X*NumOfPlayers空间,这比保留一个列表要多X倍(不确定这是否适合内存,但我们总是可以通过将组合拆分到不同的服务器来创建集群)。这里有一个关于如何在发生故障时重建缓存的问题,但这是我们可以处理的另一个问题。
  4. 扩展上述方法,如果我们在每个列表中定义得分桶(例如一个桶为0-100xp,另一个为101-1000xp,另一个为1001-10000xp等等),我们可能会稍微提高性能。分桶策略将基于玩家在游戏中的xp分发。诚然,这个发行版在现实世界中是动态的,但我们已经看到,几个月后的变化是很小的,记住XPs总是在增加,但是新的用户也在增加。
  5. 我们还在测试Cassandra的自然排序,方法是使用聚类键和白行功能,尽管我们知道拥有数百万行可能并不容易。

总之,这就是我们需要实现的目标。如果一个用户(让我们命名她的UserX)不包括在Top5列表中,我们需要显示这个用户的排名以及一些周围的玩家(例如上面的2位和下面的2位),如下所示:

代码语言:javascript
运行
复制
    Global TOP 5        My Global Ranking (425)   My Country Ranking     Other Rankings      
1. karen (12000xp)          423. george              1. david    
2. greg (11280xp)           424. nancy               2. donald 
3. philips (10293xp)      **425. UserX**             3. susan
4. jason (9800xp)           426. rebecca           **4. UserX** 
5. barbara (8000xp)         427. james               5. teresa

我研究过许多这样或其他的帖子,但仍然找不到有效更新和过滤大型领导板表的解决方案。您会选择哪一种方案,以及可能的性能改进(空间+内存+(插入/搜索CPU成本))?

EN

回答 3

Stack Overflow用户

发布于 2014-12-31 00:41:35

这是一个非常有趣的问题--谢谢你的发帖。在一般情况下,数据库擅长这种类型的问题,其中有大量的数据需要过滤和搜索。我的第一个猜测是,您没有正确地使用MySQL索引。话虽如此,您显然需要定期在有序列表中找到第n行,这是SQL根本不擅长的事情。

如果您正在寻找某种形式的内存数据库,那么您将需要一些比REDIS更复杂的东西。我建议你看看VoltDB,它速度很快,但不便宜。

如果您想要构建您自己的内存存储,那么您需要计算内存使用量,以确定它是否可行。对于要搜索或筛选的每一行,您将需要一个索引(在这个答案的后面讨论),以及每个用户的记录。然而,即使对于1000万行和20个字段,它仍然小于1Gb内存,这在现代计算机上应该是很好的。

现在是数据结构。我相信你在正确的轨道上使用地图到列表。我不认为列表需要排序-你只需要能够得到一组特定价值的用户。事实上,集合可能更合适(同样值得测试性能)。下面是我的建议:(我刚刚添加了国家和年龄字段-我认为您需要其他字段,但这是一个合理的示例):

代码语言:javascript
运行
复制
enum Country {
    ...
}

class User {
    String givenName;
    String familyName;
    int xp;
    Country country;
    int age;
}

class LeaderBoard {
    Set<User> users;
    Map<Integer, Set<User>> xpIndex;
    Map<Country, Set<User>> countryIndex;
    Map<Integer, Set<User>> ageIndex;
}

当字段发生变化时,需要更新每个索引。例如:

代码语言:javascript
运行
复制
private setUserAge(User user, int age) {
    assert users.contains(user);
    assert ageIndex.get(user.getAge()).contains(user);
    ageIndex.get(user.getAge()).remove(user);
    if (!ageIndex.containsKey(age)) {
        ageIndex.put(age, new TreeSet<>());
    }
    ageIndex.get(age).add(user);
    user.setAge(age);
}

让满足给定组合的所有用户,按等级排列,可以通过以下几种方式实现:

代码语言:javascript
运行
复制
countryIndex.get(Country.Germany).stream()
    .filter(ageIndex.get(20)::contains)
    .sorted(User::compareRank)
    ...

代码语言:javascript
运行
复制
SortedSet<User> germanUsers = new TreeSet<>(User::compareRank);
germanUsers.addAll(countryIndex.get(Country.Germany));
germanUsers.retainAll(ageIndex.get(20));

您需要检查其中哪个更有效--我猜流实现会更高效。而且,它可以很容易地转换为一个paralellStream。

您提到了对更新效率的关注。如果这是一个问题,我会非常惊讶,除非有许多更新一秒钟。通常,使用这些类型的应用程序,您将获得比写入更多的读取。

我认为没有理由像您所建议的那样手动划分索引,除非您将有数亿个条目。最好是用HashMap和TreeMap进行实验,以便具体地实例化索引。

如果需要更好的性能,下一个明显的增强是多线程应用程序。这不应该太复杂,因为您需要同步相对简单的数据结构。当然,在搜索中使用并行流会有所帮助(而且在Java 8中是免费的)。

因此,我的建议是使用这些简单的数据结构,并在尝试任何更复杂的实现之前,使用多线程和调整具体实现(例如哈希函数)来提高性能。

票数 1
EN

Stack Overflow用户

发布于 2014-12-31 18:20:22

虽然我仍然处于基准的中间,但我正在更新目前的发展状况。最好的性能比率出现在使用:

Map<Country, Map<Age, Map <TimingIdentifier, List<User>>>> (列表被排序)

键上的一些注释:我添加了一个名为World的国家,以便有一个完全独立于领导委员会的国家的实例(就好像没有选择国家过滤器一样)。我对年龄(所有年龄)和TimeIdentifier (全时)也是如此.TimeIdentifier键值为全时、月、周、日。

上述内容可以扩展到其他过滤器,因此也可以应用于其他场景。Map<Filter1,Map<Filter2,Map<Filter3,Map<Filter4 ..other Map Keys here..,List<User>>>>

更新:与使用多个Map包装器不同,在带有上述字段的单个Map中用作键的类稍微快一些。当然,我们需要一个类似于多吨的模式来创建所有可用的FilterCombination对象:

代码语言:javascript
运行
复制
class FilterCombination {
    private int CountryId;
    private int AgeId;
    private int TimeId;
    ...
}

然后定义Map<FilterCombination, List<User>> (排序列表)

我可以使用TreeSet,但我没有。为什么?基本上是在寻找订单统计树(参见这里),但似乎没有官方的这里实现(参见这里)。由于List.add(index, Object)效率低下,即O(n),这可能是相对于排序列表的一种方式。LinkedList对.add(index, Object)可能更好,但不幸的是,它在获取k元素(排名为O(n))方面进展缓慢。因此,每一种结构都有其利弊。

目前,我最终使用了一个排序列表。原因是在向排序列表中添加元素时,我使用了稍微修改过的二进制搜索算法(请参阅这里)。上述方法给出了当前用户在插入阶段的排名(不需要额外的搜索查询),即O(logn + n) (二进制搜索索引+List.add(索引,对象))。

还有其他结构比O(logn + n)的插入+排序更好吗?

*当然,如果以后需要查询用户的排名,我将再次根据用户的XP (如您在下面看到的时间戳)进行二进制搜索,而不是Id,因为现在我无法通过列表中的用户Id进行搜索)。

**作为比较国,我使用以下标准

第一: XP点数

在第二次抽签的情况下:上次XP更新的时间戳。

因此,排序列表中的等式极有可能非常少。更重要的是,如果两个使用相同XP的用户被按相反顺序排列的话,我也不会介意(即使用我们数百万个游戏的样本数据,我也发现了很少的领带,不包括我根本不在乎的零XPs )。

XP更新程序需要一些工作和资源。幸运的是,第二个比较标准显著改进了这个列表中的用户搜索(再次进行二进制搜索),因为在更新用户的XPs之前,我必须删除列表中该用户的先前条目.但是我通过她以前的XPs和时间戳来看,所以它是log(n)。

票数 1
EN

Stack Overflow用户

发布于 2017-02-22 19:41:57

最简单的选择是选择Redis的排序集,并使用主从复制。在每个从站上打开RDB,并将RDB文件备份到S3。使用卡夫卡,坚持所有的写作之前,他们去红。这样我们就可以稍后重播丢失的事务了。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/27706573

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档