本文目录导读:

- 核心思路:设计一个“热度分数”公式
- 方案一:数据库实时计算(简单,适合小流量)
- 方案二:预计算 + 定时任务(推荐用于中等流量)
- 方案三:分级 + Redis Zset 缓存(适合高并发、大型网站)
- 方案四:算法升级(应对刷分与长尾效应)
- 总结建议
在PHP项目中实现资讯热门排序,通常需要综合考量时间衰减、用户互动(如阅读、点赞、评论、分享) 等多个维度,而不是单纯依赖某一项指标(比如只看阅读量)。
以下是几种从简单到复杂、从理论到代码实现的完整方案:
核心思路:设计一个“热度分数”公式
热门排序的本质是计算一个 Hot Score(热度分数),然后按此分数降序排列。
最经典的简化公式是借鉴 Hacker News 或 Reddit 的算法:
-
Hacker News 风格(侧重爆发力,防旧文霸榜):
Score = (P - 1) / (T + 2)^GP= 点赞/投票数(或其他互动值)T= 发布时间距离现在的小时数G= 重力因子(1.5~1.8),值越大,老文章衰减越快
-
Reddit 风格(兼顾热度和时效):
Score = log10( max(|upvotes - downvotes|, 1) ) + (sign * seconds / 45000)核心思想:互动总量的对数 + 时间戳的贡献。
-
我们常用的简单加权模型(推荐项目初期使用):
Hot_Score = A * 阅读量 + B * 点赞数 + C * 评论数 + D * 分享数 + E * 收藏数 - F * 发布时间衰减A, B, C, D, E是权重(分享比点赞重要,权重高)。F是时间衰减系数,确保老文章的热度下降越快。
数据库实时计算(简单,适合小流量)
直接在 MySQL 查询时,用 SQL 计算热度分。
数据表设计 (articles) 需要包含文章的元数据和时间戳。
CREATE TABLE articles (
id INT PRIMARY KEY AUTO_INCREMENT,VARCHAR(255),
content TEXT,
created_at DATETIME,
view_count INT DEFAULT 0,
like_count INT DEFAULT 0,
comment_count INT DEFAULT 0,
share_count INT DEFAULT 0,
-- 可选:预计算的时间衰减因子字段
weight DECIMAL(10,4) DEFAULT 0.0
);
PHP 查询代码(实时计算)
<?php
define('HOUR_WEIGHT', 0.5); // 每过一小时,热度衰减的系数
define('LIKE_WEIGHT', 2.0);
define('COMMENT_WEIGHT', 3.0);
define('VIEW_WEIGHT', 0.1);
define('SHARE_WEIGHT', 5.0);
function getHotNews($db, $limit = 20) {
// 使用 TIMESTAMPDIFF 计算小时差,实现时间衰减
$sql = "
SELECT id, title,
(
(view_count * :view_w) +
(like_count * :like_w) +
(comment_count * :comment_w) +
(share_count * :share_w)
) -
(TIMESTAMPDIFF(HOUR, created_at, NOW()) * :hour_w)
AS hot_score
FROM articles
WHERE status = 1
ORDER BY hot_score DESC
LIMIT :limit
";
$stmt = $db->prepare($sql);
$stmt->bindValue(':view_w', VIEW_WEIGHT, PDO::PARAM_STR);
$stmt->bindValue(':like_w', LIKE_WEIGHT, PDO::PARAM_STR);
$stmt->bindValue(':comment_w', COMMENT_WEIGHT, PDO::PARAM_STR);
$stmt->bindValue(':share_w', SHARE_WEIGHT, PDO::PARAM_STR);
$stmt->bindValue(':hour_w', HOUR_WEIGHT, PDO::PARAM_STR);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 调用示例
// $hotNews = getHotNews($pdo);
?>
缺点:
- 每次查询都要全表扫描计算
TIMESTAMPDIFF,数据量大(>10万)时性能极差。 - 无法处理复杂的时间衰减曲线(如指数衰减)。
预计算 + 定时任务(推荐用于中等流量)
原理:使用 Cron 或 Laravel Task Scheduling,每隔 5~10 分钟计算一次所有文章的热度分,并直接写入数据库的单独字段,查询时只需按该字段排序。
数据表增加字段
ALTER TABLE articles ADD COLUMN hot_score DECIMAL(14,4) DEFAULT 0.0 INDEX;
定时任务脚本 (cron_hot_score.php)
<?php
// 伪代码逻辑
function recalculateAllHotScores($db) {
$sql = "UPDATE articles
SET hot_score =
(view_count * 0.1 +
like_count * 2.0 +
comment_count * 3.0 +
share_count * 5.0)
-
(TIMESTAMPDIFF(HOUR, created_at, NOW()) * 0.5)
WHERE status = 1";
// 如果数据量大,建议分批更新,避免锁表
// 或者使用 MySQL 事件调度器
$db->exec($sql);
}
// 设置定时任务:每5分钟执行一次
// crontab: */5 * * * * /usr/bin/php /var/www/project/cron_hot_score.php
查询:
SELECT id, title, hot_score FROM articles WHERE status = 1 ORDER BY hot_score DESC LIMIT 20;
这个查询因为用的是 INDEX,排序会非常快,即便有百万数据。
优点:
- 查询极快,排序利用索引。
- 逻辑清晰,易于调整权重。
分级 + Redis Zset 缓存(适合高并发、大型网站)
原理:将“计算”和“存储”完全交给内存数据库 Redis,用于处理高频查询,MySQL 只作为持久化存储。
存储结构
- 使用 Redis 的 Sorted Set(有序集合)。
- Key:
hot_news - Member: 文章ID (
article:123) - Score: 热度分数(浮点数)
PHP 逻辑(用户互动时触发)
<?php
// 当用户阅读文章时,增加热度
function incrementView($redis, $articleId, $weight) {
$score = $redis->zIncrBy('hot_news', $weight, "article:$articleId");
return $score;
}
// 当用户点赞/评论时,增加更多
function likeArticle($redis, $articleId) {
$redis->zIncrBy('hot_news', 2.0, "article:$articleId"); // 点赞权重高
}
// 新文章发布时,先加入,并给予基础热度
function publishArticle($redis, $articleId) {
$redis->zAdd('hot_news', time(), "article:$articleId");
}
时间衰减处理
Redis 本身不做复杂衰减,我们可以借助定时任务,或者查询时结合文章发布时间:
方案 A(推荐):在获取排序时,同步计算衰减(需要 Redis + MySQL 联动)
function getHotNews($redis, $db) {
// 1. 从 Redis Zset 获取前50个文章ID
$articleIds = $redis->zRevRange('hot_news', 0, 49);
// 2. 从 MySQL 批量获取这些文章的时间和其他信息
$ids = implode(',', array_map(fn($id) => str_replace('article:', '', $id), $articleIds));
$sql = "SELECT id, created_at FROM articles WHERE id IN ($ids)";
// ... 执行查询获取文章的发布时间
// 3. 在 PHP 中进行二次排序,应用时间衰减(分数 = Redis分数 - 小时数 * 0.5)
// 使用 usort 根据新分数排序
usort($articles, function($a, $b) {
// 计算衰减后的分数
$scoreA = $redisScore - (time() - strtotime($a['created_at'])) / 3600 * 0.5;
$scoreB = $redisScore - (time() - strtotime($b['created_at'])) / 3600 * 0.5;
return $scoreB <=> $scoreA;
});
return $articles;
}
方案 B(更简单):直接用 Redis 的 Key 时间戳
如果文章发布时间已知,可以直接用 (当前时间戳 - 创建时间戳) / 3600 作为衰减,在上面的 PHP 排序中完成。
算法升级(应对刷分与长尾效应)
如果你的项目有“刷分”风险,或者希望“优秀的老文章”偶尔也能被推荐,可以考虑:
- 对数函数:为了降低刷阅读量的影响,可以用
log10(view_count + 1)来代替原始阅读量,1000次阅读和10000次阅读的差距被缩小。 - 威尔逊区间 (Wilson Lower Bound):防止“点赞极少但满星”的文章排到第一,常用于评分排序(如知乎、豆瓣)。
- 牛顿冷却定律:更拟真的时间衰减,
1 / (1 + e^(-\alpha * (t - t0)))。
总结建议
| 阶段 | 推荐方案 | 原因 |
|---|---|---|
| 小项目/轻量级 | 方案一(数据库实时计算) | 简单,无需额外组件,数据量<10万可用。 |
| 中等规模项目 | 方案二(定时任务 + 预存字段) | 性能好,容易维护,MySQL 足以应对。 |
| 高并发/大型网站 | 方案三(Redis Zset) | 查询性能极优,可以实时更新热度,但需要维护 Redis。 |
| 算法要求高/防刷 | 方案二/三 + 对数/时间衰减优化 | 引入 log10 和指数衰减。 |
最后的关键建议:不要过度设计,先从最简单的加权公式开始,上线后观察数据,再根据用户反馈和业务需求(今天发布的新文” vs “一周前的优质文”的占比),逐步调整权重和衰减曲线。