本文目录导读:

- 方案一:基于MySQL数据库(适用于中小型项目,数据量<百万级)
- 方案二:基于Redis有序集合(适用于高并发、实时性要求高的项目)
- 方案三:MySQL + Redis 混合方案(推荐)
- 方案四:亿级数据量下的高级方案
- 常见问题与注意事项
- 总结:你应该选哪种?
在PHP项目中实现用户排行榜,通常需要结合 数据库(如MySQL、Redis)和 缓存策略 来保证性能和实时性,下面我为你梳理一个完整的实现方案,涵盖从简单到复杂的多种场景。
基于MySQL数据库(适用于中小型项目,数据量<百万级)
这是最直接的方式,利用SQL的ORDER BY和LIMIT分页。
数据库设计
假设用户表users已有积分字段score。
-- 用户表(简化) CREATE TABLE `users` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL, `score` INT DEFAULT 0, `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB;
PHP查询排行榜(按分数降序)
<?php
function getLeaderboard($page = 1, $limit = 20) {
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$offset = ($page - 1) * $limit;
$stmt = $pdo->prepare("
SELECT id, username, score,
RANK() OVER (ORDER BY score DESC) AS `rank`
FROM users
ORDER BY score DESC
LIMIT :limit OFFSET :offset
");
$stmt->bindParam(':limit', $limit, PDO::PARAM_INT);
$stmt->bindParam(':offset', $offset, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 获取当前用户排名
function getUserRank($userId) {
$pdo = new PDO('mysql:host=localhost;dbname=test', 'root', '');
$stmt = $pdo->prepare("
SELECT COUNT(*) + 1 AS `rank`
FROM users
WHERE score > (SELECT score FROM users WHERE id = :id)
");
$stmt->execute([':id' => $userId]);
return $stmt->fetchColumn();
}
?>
优点:实现简单,数据一致性好。
缺点:当用户量大(>10万)时,ORDER BY score DESC 排序会变慢,尤其是分页靠后的查询。
优化MySQL方案
- 加索引:给
score字段加索引ALTER TABLE users ADD INDEX idx_score (score DESC);。 - 使用缓存:将排行榜结果缓存到Redis或Memcached中,设置过期时间(如1分钟)。
- 定时计算:如果是日榜/周榜,可以提前用定时任务生成一个快照表。
基于Redis有序集合(适用于高并发、实时性要求高的项目)
Redis的 Sorted Set(有序集合) 是天然为排行榜设计的,具有原子性增减和极快的排序能力。
数据结构
- Key:
leaderboard:{type}:{date}(leaderboard:global:20231027) - 成员(Member):用户ID(如
user:123) - 分值(Score):用户的积分
PHP操作示例(使用Redis扩展)
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
// 添加/更新用户分数(原子操作)
function updateScore($userId, $score) {
global $redis;
$redis->zAdd('leaderboard:global', $score, "user:$userId");
}
// 增加用户分数(例如完成一次任务加10分)
function incrementScore($userId, $increment = 10) {
global $redis;
$redis->zIncrBy('leaderboard:global', $increment, "user:$userId");
}
// 获取排行榜前100名(带分数和排名)
function getTopRank($rankStart = 0, $rankEnd = 99) {
global $redis;
// 获取成员和分数(降序)
$list = $redis->zRevRange('leaderboard:global', $rankStart, $rankEnd, true);
$rank = $rankStart + 1;
$result = [];
foreach ($list as $member => $score) {
$userId = str_replace('user:', '', $member);
$result[] = [
'user_id' => $userId,
'score' => $score,
'rank' => $rank
];
$rank++;
}
return $result;
}
// 获取某个用户的排名(从0开始,需要+1)
function getUserRank($userId) {
global $redis;
$rank = $redis->zRevRank('leaderboard:global', "user:$userId");
return $rank === false ? null : $rank + 1; // 不存在返回null
}
// 获取用户的分数
function getUserScore($userId) {
global $redis;
return $redis->zScore('leaderboard:global', "user:$userId");
}
?>
关键点说明
- 升降序:
zRevRange取最高分在前(降序),zRange取最低分在前(升序)。 - 数据持久化:Redis数据在重启后会丢失(如果未配置AOF/RDB),建议定期将Redis排行榜数据同步到MySQL等持久化数据库,作为后备。
- 维度扩展:日榜可以用
leaderboard:20231027,周榜用leaderboard:week:2023W43,全服榜用leaderboard:global。
MySQL + Redis 混合方案(推荐)
结合持久化与性能,用 MySQL存底、Redis做缓存。
更新策略
- 用户积分变动时,同时更新MySQL和Redis。
- 或者:先写MySQL(通过消息队列如RabbitMQ),再由消费者更新Redis。
排行榜读取
- 优先读取Redis中的排行榜数据(速度快)。
- 如果Redis中没有数据,回源到MySQL计算并回填Redis缓存。
定期同步
- 用定时任务(crontab)每5分钟将MySQL中的积分变化批量同步到Redis。
- 或者直接在Redis中做持久化配置(AOF+RDB)。
亿级数据量下的高级方案
当用户规模达到千万甚至亿级时,需要考虑分片和近似排名。
- 数据分片:按用户ID哈希分到多个Redis实例(一致性哈希)。
- 近似计算:使用Redis的
ZREVRANK虽然快,但单个Key容量有限,可以使用分桶(Bucket)策略,比如每1000分一个桶,先统计桶内排名,再精确命中。 - 外部排序:利用MapReduce或Spark等大数据组件,定期计算全量排名,存入HBase或分布式数据库中。
常见问题与注意事项
| 问题 | 解决方案 |
|---|---|
| 排名并列(同分) | 默认情况下,同分者按用户ID字母顺序排,可以自定义规则:例如用 score * 10000000000 + unix_timestamp 作为Redis分数,让先达到该分数的用户排前面。 |
| 实时性与性能 | 不必实时显示所有人排名,用缓存+过期时间(如5秒),用户查看自己排名时直接用Redis O(log N)查询。 |
| 防作弊 | 在服务端校验积分增加逻辑,避免直接操作数据库或Redis,限制单位时间内的积分变动频次。 |
| 用户数据变更 | 改名、删除用户后,需同步更新排行榜,Redis中使用用户ID而非用户名作为成员。 |
你应该选哪种?
- 个人博客/小团队(用户 < 1万):直接使用 MySQL + RANK() 函数,简单够用。
- 中型游戏/社区(用户 1万 - 100万):使用 Redis Sorted Set,性能和实时性都好。
- 大型项目(用户 > 100万):采用 MySQL + Redis 混合 或 分布式方案,并加入缓存过期、分页优化等策略。
如果需要我提供具体的代码框架或某个方案的完整示例,请告诉我你的数据规模和应用场景,我可以帮你进一步细化。