## 环境校验(可验证)
java -version
mvn -v
redis-cli PING
确认 Java 21、Maven 3.9+ 与 Redis 可用后进入实战。
## 指标与评分函数(可验证)
score = α * PV + β * UV + γ * dwellSeconds - δ * ageHours
- 建议初始参数:`α=1, β=3, γ=0.02, δ=0.5`,依据压测与线上分布再微调。
## 最小可运行代码(Spring Boot 3)
`pom.xml` 依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
`application.yml`:
spring:
data:
redis:
host: localhost
port: 6379
`HotController.java`:
package ybb.hot;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import java.time.Instant;
import java.util.*;
@RestController
public class HotController {
private final StringRedisTemplate redis;
public HotController(StringRedisTemplate redis) { this.redis = redis; }
@PostMapping("/events/pv")
public Map<String,Object> pv(@RequestParam String articleId) {
redis.opsForValue().increment("ybb:hot:pv:"+articleId);
return Map.of("ok", true);
}
@PostMapping("/events/uv")
public Map<String,Object> uv(@RequestParam String articleId, @RequestParam String userId) {
Boolean added = redis.opsForHash().putIfAbsent("ybb:hot:uv:"+articleId, userId, "1");
if (Boolean.TRUE.equals(added)) {
redis.opsForValue().increment("ybb:hot:uv_count:"+articleId);
}
return Map.of("ok", true);
}
@PostMapping("/events/dwell")
public Map<String,Object> dwell(@RequestParam String articleId, @RequestParam int seconds) {
redis.opsForValue().increment("ybb:hot:dwell:"+articleId, seconds);
return Map.of("ok", true);
}
@PostMapping("/articles")
public Map<String,Object> create(@RequestParam String articleId) {
redis.opsForValue().set("ybb:hot:ts:"+articleId, String.valueOf(Instant.now().getEpochSecond()));
return Map.of("ok", true);
}
@GetMapping("/ranking")
public List<Map<String,Object>> ranking() {
Set<org.springframework.data.redis.connection.RedisZSetCommands.Tuple> tuples =
redis.opsForZSet().reverseRangeWithScores("ybb:hot:zset", 0, 49);
List<Map<String,Object>> list = new ArrayList<>();
if (tuples != null) {
for (var t : tuples) {
list.add(Map.of("id", new String(t.getValue()), "score", t.getScore()));
}
}
return list;
}
}
## 定时重算(时间衰减,可验证)
`HotJob.java`:
package ybb.hot;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import org.springframework.data.redis.core.StringRedisTemplate;
@Component
public class HotJob {
private final StringRedisTemplate redis;
public HotJob(StringRedisTemplate redis) { this.redis = redis; }
@Scheduled(fixedRate = 60000)
public void recompute() {
var conn = redis.getConnectionFactory().getConnection();
var keys = conn.keys("ybb:hot:pv:*".getBytes());
if (keys == null) return;
for (var key : keys) {
String articleId = new String(key).substring("ybb:hot:pv:".length());
double pv = parse(redis.opsForValue().get("ybb:hot:pv:"+articleId));
double uv = parse(redis.opsForValue().get("ybb:hot:uv_count:"+articleId));
double dwell = parse(redis.opsForValue().get("ybb:hot:dwell:"+articleId));
double ts = parse(redis.opsForValue().get("ybb:hot:ts:"+articleId));
double ageHours = ts > 0 ? (System.currentTimeMillis()/1000.0 - ts)/3600.0 : 0;
double score = 1*pv + 3*uv + 0.02*dwell - 0.5*ageHours;
redis.opsForZSet().add("ybb:hot:zset", articleId, score);
}
}
private static double parse(String v) { try { return Double.parseDouble(v); } catch (Exception e) { return 0; } }
}
`YbbHotApplication.java`:
package ybb.hot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class YbbHotApplication {
public static void main(String[] args) { SpringApplication.run(YbbHotApplication.class, args); }
}
## 验证步骤(端到端)
1. 创建文章:`POST /articles?articleId=1001`;检查 `ybb:hot:ts:1001` 存在。
2. 注入事件:对 `PV/UV/dwell` 上报多次;检查对应 Key 增长。
3. 等待 1–2 分钟:访问 `/ranking`,确认分值与排序符合预期。
4. 衰减校验:不再注入事件,观察旧文分值按小时下降,新文更易上榜。
## 压测建议(可验证)
- 对 `/events/*` 与 `/ranking` 进行 2–5 分钟压测,记录 `RPS`、`p95/p99` 与 Redis 命令耗时;调优连接池参数与批量写入策略。
## 注意事项
- 防刷与合规:UV 去重采用匿名标识(不存敏感数据);对异常高频行为限速与封禁。
- 参数调优:依据站点分布回归权重;为高峰期配置更短重算间隔与分类分桶(`ybb:hot:zset:{category}`)。
- 依赖版本:示例基于 Java 21 与 Spring Boot 3;Redis 6.2+。
## 结语
以上实现提供了 Java 生态下热门文章的最小可运行基线,满足可验证、可复现与生产可落地的要求,可依据业务特性迭代权重与窗口策略。

发表评论 取消回复