如何设计一个高性能排行榜系统?
本文最后更新于 720 天前,其中的信息可能已经有所发展或是发生改变。

1、业务背景

##

1.1 排行榜系统是什么?

排行榜系统是一种用于记录和展示某种排名或评分的系统。日常生活中常见的游戏玩家排名、音乐排行榜、电影票房排行榜等都属于排行榜系统。

排行榜系统天然具有聚焦用户注意力的特点,比如说新闻类的热点排行榜,可以引导用户点击新闻,提高用户活跃度与新闻曝光率,对于某些互联网公司,排行榜系统是一个门面系统,承载着核心流量入口的作用。

1.2 排行榜系统的功能点

##

1、业务背景

##

1.1 排行榜系统是什么?

排行榜系统是一种用于记录和展示某种排名或评分的系统。日常生活中常见的游戏玩家排名、音乐排行榜、电影票房排行榜等都属于排行榜系统。

排行榜系统天然具有聚焦用户注意力的特点,比如说新闻类的热点排行榜,可以引导用户点击新闻,提高用户活跃度与新闻曝光率,对于某些互联网公司,排行榜系统是一个门面系统,承载着核心流量入口的作用。

1.2 排行榜系统的功能

##

2、技术方案

制作设计:

1,数据库查询设计(masql order by)

优化:1.支持高性能,2,海量数

2,内存堆排序

优化:1.支持分布式,2,支持持久化

3,redis(Zset集合:names: {muxue-8,dog-10}

方案一:masql order by

架构与设计要贴合业务。

如果数据量比较小、业务场景也比较简单时,可以直接使用 MySQL 的 ORDER BY 子句。

step 1: 新建一张玩家积分表, id、name、score 分别表示 主键id 、玩家名称、分数。

 CREATE TABLE leaderboard (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键id',
  name VARCHAR(50) NOT NULL COMMENT '玩家名称',
  score INT NOT NULL COMMENT '分数',
  PRIMARY KEY (`id`)
 ) COMMENT '排行榜';

step 2:增加新的用户和积分

INSERT INTO leaderboard (name, score)
VALUES
('p1', 1),
('p2', 2),
('p3', 3),
('p4', 4),
('p5', 5),
('p6', 6),
('p7', 7),
('p8', 8),
('p9', 9),
('p10', 10),
('p11', 11),
('p12', 1);

step 3:

此时已经满足一个排行榜系统的基本需求了

百(千)万级别数据压测

查询前 100 名:

SELECT name, score
FROM leaderboard
ORDER BY score DESC
LIMIT 100;

本地测试结果:

优化一下,在 score 上加上索引:

ALTER TABLE leaderboard ADD INDEX idx_score (score);

缺陷与不足:

mysql是一个关系型数据库,对于大规模的排行榜,特别是在高并发环境下频繁的更新和查询操作可能会产生性能影响

在竞争激烈的排行榜中,如果多个用户同时提交,可能会导致数据不一致问题,数据的并发更新导致竞态条件和冲突,需要采取措施来确保数据一致性和

数据查询慢怎么办?

用缓存:提前把数据取出来保存好(通常保存到读写更快的介质,比如内存),就可以更快地读写

缓存预热

问题:第一个用户进来的时候很慢(加入第一个是老板),也一定程度上保存数据库

预热缓存的有缺点:

1.解决上面的问题,可以让用户始终很快

缺点:

1.增加开发成本(你要额外的开发,设计)

2.预热的开机和时间如果错了,有可能你换成的数据不对或者太老

3.需要占用空间

模拟第一个人进入的时候推荐

方案2:内存堆排序(不推荐)

利用java本地jvm内存运算进行排序

缺陷与不足

内存占用大:优先队列需要在内存中维护排行榜数据结构,对于大规模排行榜,内存占用会很大

数据持久化:如果程序异常,排行榜数据会丢失

实效性不一致:java队列基于堆实现的,有插入顺序元素排列问题,需要考虑并发和数据更新同步问题!

方案三:Redis zset

出于性能、自排序、持久化、可扩展性上的考虑,可以使用 Redis 的 zset 来实现排行榜。

step1 :新增玩家和积分信息

将玩家的分数和名称作为有序集合中的成员,分数作为排序依据。使用Redis的ZADD命令将玩家的分数和名称添加到有序集合中:

 ZADD leaderboard <score> <name>

其中,leaderboard为有序集合的键名,<score>是玩家的分数,<name>是玩家的名称。

step2:查找排行榜数据

获取排行榜的前几名。使用Redis的ZREVRANGE命令可以获取有序集合中按分数从高到低排名的成员:

 ZREVRANGE leaderboard 0 <count - 1> WITHSCORES

其中,leaderboard为有序集合的键名,<count>是需要获取的排行榜数量。该命令将返回排行榜前<count>名的玩家名称和对应的分数。

千万数据性能压测:

用 python 写入千万数据:

import redis

# Redis连接信息
redis_host = 'localhost'
redis_port = 6379

# 生成玩家名称
def generate_player_name(index):
    return "p" + str(index)

# 连接到Redis
r = redis.Redis(host=redis_host, port=redis_port)

# 批量插入数据
for i in range(1, 10000001):
    player_name = generate_player_name(i)
    score = i
    r.zadd('leaderboard', {player_name: score})

print("数据插入完成。")

Redis 的 zset 集合了 MySQL 的 ORDER BY 和 内存堆的优点,在大流量、高并发的场景下,推荐优先选取 Redis 的 zset 实现排行榜系统。

设计缓存key

redis 内存不能无限增加,一定设置过期时间!!

java里实现方式

spring Data Redis(推荐)

spring Data :通用的数据访问架构,定义一组增删改查的接口

mysql,redis,jpa

spring-data-redis

Lettuce

高阶的操作redis的java客户端

异步,连接池

jedis

独立于spring操作java客户端

redisson

分布式操作redis的java客户端,让你像在本地集合一样操作redis(分布式redis网格)

对比

1.如果你用的是Spring,并且没有过多的定制化要求,可以用Spring Data Redis,最方便

2.如果你用的不是Spring,并且追求简单,并且没有过高的性能要求可以使用jedis+jedis Pool

3.如果你的项目不是Spring,并且追求高性能,高定制化,可以使用Lettuce,支持异步,连接池


如果你的项目是分布式的,需要用到一些分布式的特性(如分布式锁分布式集合)推荐使用redissio

怎么缓存预热?

1.定时

2.模拟触发(手动触发)

实现

用定时任务,刷新所有用户的排行列表

注意点:

1.缓存预热的意义(新增少,总用户多)

2.缓存的空间不能太大,要有预留给其他缓存空间

3.缓存的数据周期(每天一次)

分析有缺点的时候,要打开思路,从整个项目从0到1的线路上去分析

定时任务实现

1.Spring Scheduler(spring boot 默认整合了)

2.Quartz(独立于Spring存在的定时任务框架)

3.xxl-job之类的分布式任务调度平台

第一种方式:

1.主类开启@EnableScheduling

2.给要定时执行的方法添加@Scheduling注解,指定cron表达式或者执行频率

不要去背cron表达式!!!

控制定时任务的执行

为啥?

1.浪费资源,想象10000台服务器同时“打鸣”

2.脏数据,比如重复插入

要控制定时任务在同一时间只有一个服务器能执行

怎么做?

1.分离定时任务程序很主程序只有1服务器执行定时任务。成本太大

2.写死配置,每个服务器都执行定时任务,但是只有ip符合配置的服务器才真实执行业务逻辑,其他直接返回。成本最低;但是我们ip可能是不固定的,把ip写的太死了

3.动态配置,配置是可以轻松的,很方便地更新的(代码无需重启),但是只有ip符合配置的服务器才真实执行业务逻辑

  • 数据库
  • reids
  • 配置中心(Nacos,Apollo,Spring Cloud Config)

问题服务器多了ip不可控

4.分布式锁,只有抢到锁才能执行真实任务。坏处:增加成本,好处:不用手动配置多少个服务器都一样

上一篇
下一篇