项目背景
AI电力交易竞赛平台,需要为参与交易竞赛的团队设计一个的排行榜,按照不同的赛道进行排名,价格预测赛道按照多个准确率排名,交易赛道按照收益进行排名。具体需求如下:
- 实时性:市场边界变化时,排行榜要立即更新。
- 高并发:支持所有参赛团队同时查询排行榜(100个参赛团队)。
- 排名稳定性:排名计算准确,且能应对短时间内的大量更新。
设计思路
在设计排行榜系统时,关键要点在于明确实时性需求以及选择合适的数据存储和更新策略,这直接影响到系统的性能和用户体验。
实时性与存储时机
排行榜按实时性可分为实时排行榜和非实时排行榜,二者主要区别在于存储更新的时机:
- 实时排行榜:当市场边界或相关数据发生变化时,排行榜需立即更新。这要求系统能实时捕捉数据变动,并迅速在存储中反映出来,以确保用户随时获取到最新的排名信息。例如,在在线竞技游戏中,玩家的实时积分一旦改变,游戏的排行榜就应即刻更新。
- 非实时排行榜:允许在数据发生变化后延后更新。常见做法是借助定时任务框架,如 xxl - job 或 powerjob,按设定的时间间隔(如每小时、每天等)执行统计任务。任务执行时,从数据源查询相关数据进行统计计算,完成后将结果存储到相应的存储介质中。例如,电商平台的月度商品销售排行榜,可通过每天凌晨执行的定时任务,统计前一天的销售数据来更新排行榜。
数据存储与更新方案
在排行榜设计中,如何高效存储和更新排行榜数据是核心问题,常见方案如下:
-
数据库方案
- 原理 :传统关系型数据库(如 MySQL)利用排序和索引功能实现排行榜。例如,假设数据库中有
subject_info
表,create_by
字段为用户唯一标识,通过select count(1), create_by from subject_info group by create_by limit 0, 5;
语句,可对用户进行分组统计,并获取排名前 5 的用户。 - 优势:数据存储具有完整性和一致性保障,数据持久化能力强,适用于数据准确性要求极高且数据量、并发量较小的场景。
- 劣势:在高并发场景下,数据库承受的查询压力巨大,若索引使用不当,易产生慢 SQL,导致查询效率低下,难以满足实时性需求。
- 优化措施:可在数据库层面添加缓存,以减轻数据库查询压力,提升响应速度,但会引入一定的延时性。
- 原理 :传统关系型数据库(如 MySQL)利用排序和索引功能实现排行榜。例如,假设数据库中有
-
缓存方案:
- 原理 :以 Redis 为代表的内存数据库,利用其有序集合(Sorted Set)存储排行榜数据。将排行榜项目作为 Sorted Set 元素,对应分数作为排序依据。如游戏排行榜,玩家 ID 为元素,游戏得分是分数,使用
ZADD
命令更新分数,Redis 自动重新排序,通过ZRANGE
或ZREVRANGE
命令查询排名。 - 优势:基于内存操作,性能卓越,查询速度极快,能满足高并发和实时性要求,且减少数据库负载。
- 劣势:需额外搭建和维护 Redis 环境,增加系统复杂性,使用不当易出现大 key 问题,影响性能和稳定性,且数据存在内存丢失风险(可通过持久化缓解)。
- 原理 :以 Redis 为代表的内存数据库,利用其有序集合(Sorted Set)存储排行榜数据。将排行榜项目作为 Sorted Set 元素,对应分数作为排序依据。如游戏排行榜,玩家 ID 为元素,游戏得分是分数,使用
-
混合方案
- 原理:结合数据库和缓存。数据库用于数据持久化存储,保障数据安全可靠;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);