## 环境校验(可验证)


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 生态下热门文章的最小可运行基线,满足可验证、可复现与生产可落地的要求,可依据业务特性迭代权重与窗口策略。


点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部