排行榜设计-高并发场景下的最佳实践

项目背景

AI电力交易竞赛平台,需要为参与交易竞赛的团队设计一个的排行榜,按照不同的赛道进行排名,价格预测赛道按照多个准确率排名,交易赛道按照收益进行排名。具体需求如下:

  • 实时性:市场边界变化时,排行榜要立即更新。
  • 高并发:支持所有参赛团队同时查询排行榜(100个参赛团队)。
  • 排名稳定性:排名计算准确,且能应对短时间内的大量更新。

设计思路

在设计排行榜系统时,关键要点在于明确实时性需求以及选择合适的数据存储和更新策略,这直接影响到系统的性能和用户体验。

实时性与存储时机

排行榜按实时性可分为实时排行榜和非实时排行榜,二者主要区别在于存储更新的时机:

  1. 实时排行榜:当市场边界或相关数据发生变化时,排行榜需立即更新。这要求系统能实时捕捉数据变动,并迅速在存储中反映出来,以确保用户随时获取到最新的排名信息。例如,在在线竞技游戏中,玩家的实时积分一旦改变,游戏的排行榜就应即刻更新。
  2. 非实时排行榜:允许在数据发生变化后延后更新。常见做法是借助定时任务框架,如 xxl - job 或 powerjob,按设定的时间间隔(如每小时、每天等)执行统计任务。任务执行时,从数据源查询相关数据进行统计计算,完成后将结果存储到相应的存储介质中。例如,电商平台的月度商品销售排行榜,可通过每天凌晨执行的定时任务,统计前一天的销售数据来更新排行榜。

数据存储与更新方案

在排行榜设计中,如何高效存储和更新排行榜数据是核心问题,常见方案如下:

  1. 数据库方案

    • 原理 :传统关系型数据库(如 MySQL)利用排序和索引功能实现排行榜。例如,假设数据库中有subject_info表,create_by字段为用户唯一标识,通过select count(1), create_by from subject_info group by create_by limit 0, 5;语句,可对用户进行分组统计,并获取排名前 5 的用户。
    • 优势:数据存储具有完整性和一致性保障,数据持久化能力强,适用于数据准确性要求极高且数据量、并发量较小的场景。
    • 劣势:在高并发场景下,数据库承受的查询压力巨大,若索引使用不当,易产生慢 SQL,导致查询效率低下,难以满足实时性需求。
    • 优化措施:可在数据库层面添加缓存,以减轻数据库查询压力,提升响应速度,但会引入一定的延时性。
  2. 缓存方案

    • 原理 :以 Redis 为代表的内存数据库,利用其有序集合(Sorted Set)存储排行榜数据。将排行榜项目作为 Sorted Set 元素,对应分数作为排序依据。如游戏排行榜,玩家 ID 为元素,游戏得分是分数,使用ZADD命令更新分数,Redis 自动重新排序,通过ZRANGEZREVRANGE命令查询排名。
    • 优势:基于内存操作,性能卓越,查询速度极快,能满足高并发和实时性要求,且减少数据库负载。
    • 劣势:需额外搭建和维护 Redis 环境,增加系统复杂性,使用不当易出现大 key 问题,影响性能和稳定性,且数据存在内存丢失风险(可通过持久化缓解)。
  3. 混合方案

    • 原理:结合数据库和缓存。数据库用于数据持久化存储,保障数据安全可靠;Redis 负责实时计算和快速查询。例如,新数据先写入数据库,再同步到 Redis 更新排行榜,查询时直接从 Redis 获取。
    • 优势:兼顾数据持久化和高效查询,既能满足实时性需求,又能保证数据完整性,适用于对实时性和数据可靠性都有较高要求的场景。
    • 劣势:系统架构相对复杂,需要协调数据库和缓存之间的数据同步,增加了开发和维护成本。

方案选择

综合考虑系统的实时性和高并发需求,推荐以 Redis 作为排行榜的核心存储,并配合 MySQL 进行数据持久化。Redis 的有序集合为排行榜排序和排名操作提供高效实现,外部交互直接访问缓存,可大幅提升系统性能,同时 MySQL 确保数据安全存储,满足大多数场景下的需求。

具体实现

数据结构设计

在Redis中,我们可以使用Sorted Set来实现排行榜。Sorted Set是一个带有分数的集合,集合中的每个元素都有一个唯一的值和一个关联的分数。我们可以利用分数进行排序,从而实现排行榜的功能。

以现货增益排名举例:每个参赛团队都有一个唯一的ID和一个现货增益值(spotGain),可以设计以下结构:

  • Key: spot:rank:gain(排行榜的唯一标识)
  • Member: teamId(参赛团队ID)
  • Score: spotGain(现货增益值)

实现基本操作

(1)新增/更新参赛团队增益值

java 复制代码
String rankKey = 'spot:rank:gain';
Long teamId = 2L;
double spotGain = 65.23;

// 更新现货增益值
redis.zadd(rankKey, spotGain, String.valueOf(teamId));

当团队的增益值发生变化时,可以调用zadd方法更新排行榜。如果团队已经存在于排行榜中,Redis会自动更新其分数。

(2)查询团队排名

java 复制代码
// 获取参赛团队排名,Redis返回的排名是从0开始的
Long rank = redis.zrevrank(rankKey, String.valueOf(teamId));
if (rank != null) {
    log.info("参赛团队[{0}]的排名是 {2}", teamId, rank + 1);
} else {
    log.info("参赛团队[{0}]没有排名", teamId);
}

使用zrevrank方法可以获取参赛团队的排名,注意这里是倒序排列,即分数高的排在前面。

(3)获取排行榜前N名

java 复制代码
// 获取前20名排名
Set<String> topRank = redis.zrevrange(rankKey, 0, 19);

通过zrevrange方法可以获取排行榜的前N名参赛团队及其对应的分数。

持久化与数据恢复

虽然Redis提供了高效的排行榜操作,但它毕竟是内存数据库,断电或服务器故障时可能导致数据丢失。为此,我们需要考虑数据的持久化问题。

1. 数据持久化

我们可以定期将Redis中的排行榜数据同步到MySQL中,确保数据的持久性。可以使用以下方式:

  • 定时备份:通过定时任务将Redis中的排行榜数据导出,并写入MySQL。
  • 更新时同步:每当参数团队增益值发生变化时,同时更新Redis和MySQL。

2. 数据恢复

当Redis服务器重启或数据丢失时,可以从MySQL中恢复排行榜数据。这样即使Redis中的数据丢失,我们也可以通过从MySQL恢复来保障排行榜的正常运行。

应对高并发与性能优化

在高并发场景下,我们需要考虑Redis的性能优化,以确保排行榜系统的稳定性和高效性。

  • 使用集群: 当单台Redis服务器无法支撑高并发请求时,可以考虑使用Redis Cluster,将数据分布到多个节点中,提高系统的可扩展性。
  • 限流与降级: 在高峰期,可以对排行榜的查询和更新操作进行限流,避免Redis服务器被过度消耗。同时,也可以考虑在必要时进行功能降级,例如延迟更新排行榜或限制查询频率。

Redis Zset多字段排序方案设计

场景:价格预测排名,根据综合准确率排名,如果相等,再用价差方向准确率排名。

思路:使用Redis zset数据结构,主要考虑将score字段进行分段,例如,完整的score:1000020000,综合准确率:10000,价差方向准确率:20000,分段去维护两个数据,整体作为score去查询

java 复制代码
Double score = RedisUtil.zScore("teamId", "2");
//获取第一段排序
int scorePart1 = (int) ((score / 1000000000) * 10000);
//获取第二段排序
int scorePart2 = (int) (score % 100000);