用MySQL实现排行榜

问题的引出

开发产品时(特别是在游戏中)时常会出现诸如“实时计算用户积分并且显示排名”的需求。 针对这类需求,往往会有多种多样的实现方式,接下来谈谈机种常见的解决方式。

问题的一般分类及对策

这些东西一般需求方是不会提出来的。 如果自己不主动挖掘,往往会在第一时间就错过最好的解决方案。

个人认为数据的变动特点是影响这类问题解决方案最重要的要素。
其分类和解决思路如下:

排行榜类问题的基本特征

1. 统一积分排名模式

类似于Game Center的模式。 每一个玩家有一个积分的属性。 在这个属性上建索引,计算得到玩家排名。

SELECT	count(*) + 1 AS rank
  FROM	user_ranking
  WHERE	pt > ?

如果用户数量在10万量级以内,那么这个方法应该是毫无压力。 但是很显然的,这个查询的响应时间会随着用户量的增长急剧增长。

针对这种情况,一个很简单的优化方法是使用覆盖索引。 注意mysql explain之后是否出现了”using index”。 对于百万数据量级的查询,使用覆盖索引可以将响应时间控制在平均300ms左右。 但是对于排名比较靠后,分数比较少的查询而言,最开始的几次查询效率会接近于全表count,响应时间超过1s,实际上不是很理想。 时间代价上虽然说不上很理想,但是配合缓存和对于精度的模糊处理仍然可以应对大部分此类需求。

2. 分段积分排名模式

针对第一种积分榜最常采用的优化方法就是分段排名。 顾名思义就是将用户划分为数段,在段内对用户进行排名。 在需要的情况下组合成各段的总排名。

SELECT	count(*) + 1 AS rank
  FROM	user_ranking
  WHERE pt >= 1000
	AND	pt <  2000
	AND pt > ?

这样做的好处是极大的缩减了需要count的记录条数,缩短响应时间。 坏处是需要额外维护一张表,记录每个区段各有多少用户。 如果用户的跨区间变动比较频繁的话维护的开销会比较大。

并且区间的划分也是一个需要考虑的问题。 一般而言,根据80/20原则,分数比较低的区段需要多划分以平衡每个区段的用户量,比如:

>10000pt 每10000pt一段
>1000pt  每1000pt一段
>100pt   每100pt一段
<=100pt  独立一段

当然,实际的分段策略要根据用户的分布而定,需要逐步调整。

可以用MySQL的Partition功能对于这种方法稍作改进,但是无法在时间上取得更好的效果。 在200~300ms的量级,使用partition是可以一定程度改善最坏情况,但是没有办法让查询变得更快。

3. 交换排名模式

交换排名模式仅适用于排名由强弱关系而非具体分数决定的场合。 用户的强弱由用户间的胜败关系决定。 排名低的用户可以向排名高的用户挑战,如果胜则交换排名。

4. 混合排名模式

可以理解为前两种的混合体。 动态的将用户分为若干动态组,用户在小组之间的迁移只能够通过交换实现。 小组之内用户的位次由积分决定。例如:

user_id win_num pt
12345	4 		350
12346	4 		353
02345	3 		153
02344	3 		253
00344	1 		53

上边的这个例子中,胜利场数即为分组的基准。 3胜组的用户只可以向4胜组用户挑战,胜则交换所在小组。 这样在查询的时候首先根据胜利场数对用户进行划分, 然后在对各组之内的用户计算其组内排名。

同样是混合排名方法,我们可以将上边的做法反过来。 在组内用胜败关系决定位次,用积分决定分到哪个组。

总结