Java 案例:统计长短传比例分布
业务背景
在短视频/直播/内容平台中,"长短传"通常指:

- 短传:用户上传的短内容(如短视频、短文本、小文件)
- 长传:用户上传的长内容(如长视频、长文本、大文件)
统计长短传比例分布,是内容运营、风控、存储规划的重要指标。
需求定义
时长**区分(也可以按文件大小、文本长度):
| 类型 | 时长范围 |
|---|---|
| 短传 | < 60 秒 |
| 中传 | 60 ~ 300 秒 |
| 长传 | > 300 秒 |
统计目标:
- 各类型数量、占比
- 按天/按用户/按渠道的分布
- 输出比例结果
数据模型
@Data
@AllArgsConstructor
public class UploadRecord {
private String id;
private String userId;
private String channel; // 渠道:app/web/mini
private long durationSec; // 时长(秒)
private LocalDate uploadDate;
private long fileSize; // 字节
}
核心统计代码
1 枚举定义
public enum UploadType {
SHORT("短传"),
MEDIUM("中传"),
LONG("长传");
private final String desc;
UploadType(String desc) { this.desc = desc; }
public String getDesc() { return desc; }
public static UploadType of(long durationSec) {
if (durationSec < 60) return SHORT;
if (durationSec <= 300) return MEDIUM;
return LONG;
}
}
2 统计结果对象
@Data
public class RatioStat {
private UploadType type;
private long count;
private double ratio; // 占比 0~1
public String getRatioPercent() {
return String.format("%.2f%%", ratio * 100);
}
}
3 基础统计(Stream 实现)
public class UploadStatService {
/**
* 统计长短传比例分布
*/
public List<RatioStat> statRatio(List<UploadRecord> records) {
if (records == null || records.isEmpty()) {
return Collections.emptyList();
}
Map<UploadType, Long> countMap = records.stream()
.collect(Collectors.groupingBy(
r -> UploadType.of(r.getDurationSec()),
Collectors.counting()
));
long total = records.size();
return Arrays.stream(UploadType.values())
.map(type -> {
long cnt = countMap.getOrDefault(type, 0L);
RatioStat stat = new RatioStat();
stat.setType(type);
stat.setCount(cnt);
stat.setRatio(total == 0 ? 0 : (double) cnt / total);
return stat;
})
.collect(Collectors.toList());
}
}
4 按渠道分组统计
public Map<String, List<RatioStat>> statRatioByChannel(List<UploadRecord> records) {
Map<String, List<UploadRecord>> byChannel = records.stream()
.collect(Collectors.groupingBy(UploadRecord::getChannel));
Map<String, List<RatioStat>> result = new LinkedHashMap<>();
byChannel.forEach((channel, list) ->
result.put(channel, statRatio(list))
);
return result;
}
5 按天统计分布
public Map<LocalDate, List<RatioStat>> statRatioByDay(List<UploadRecord> records) {
Map<LocalDate, List<UploadRecord>> byDay = records.stream()
.collect(Collectors.groupingBy(UploadRecord::getUploadDate));
Map<LocalDate, List<RatioStat>> result = new TreeMap<>();
byDay.forEach((day, list) -> result.put(day, statRatio(list)));
return result;
}
完整示例
public class Main {
public static void main(String[] args) {
List<UploadRecord> records = List.of(
new UploadRecord("1","u1","app", 30, LocalDate.of(2024,1,1), 1024),
new UploadRecord("2","u1","app", 45, LocalDate.of(2024,1,1), 2048),
new UploadRecord("3","u2","web", 120, LocalDate.of(2024,1,1), 5000),
new UploadRecord("4","u3","app", 600, LocalDate.of(2024,1,2), 10000),
new UploadRecord("5","u4","web", 900, LocalDate.of(2024,1,2), 20000),
new UploadRecord("6","u5","app", 20, LocalDate.of(2024,1,2), 512)
);
UploadStatService service = new UploadStatService();
// 全局比例
System.out.println("==== 全局比例 ====");
service.statRatio(records).forEach(s ->
System.out.printf("%s: 数量=%d, 占比=%s%n",
s.getType().getDesc(), s.getCount(), s.getRatioPercent()));
// 按渠道
System.out.println("\n==== 按渠道 ====");
service.statRatioByChannel(records).forEach((ch, stats) -> {
System.out.println("渠道: " + ch);
stats.forEach(s ->
System.out.printf(" %s: %s%n", s.getType().getDesc(), s.getRatioPercent()));
});
// 按天
System.out.println("\n==== 按天 ====");
service.statRatioByDay(records).forEach((day, stats) -> {
System.out.println("日期: " + day);
stats.forEach(s ->
System.out.printf(" %s: %s%n", s.getType().getDesc(), s.getRatioPercent()));
});
}
}
输出示例:
==== 全局比例 ====
短传: 数量=3, 占比=50.00%
中传: 数量=1, 占比=16.67%
长传: 数量=2, 占比=33.33%
==== 按渠道 ====
渠道: app
短传: 66.67%
中传: 0.00%
长传: 33.33%
渠道: web
短传: 0.00%
中传: 50.00%
长传: 50.00%
大数据量优化方案
当记录量达到百万级,建议:
1 使用并行流
Map<UploadType, Long> countMap = records.parallelStream()
.collect(Collectors.groupingByConcurrent(
r -> UploadType.of(r.getDurationSec()),
Collectors.counting()
));
2 单次遍历(避免多次 Stream)
public List<RatioStat> statRatioFast(List<UploadRecord> records) {
long[] counts = new long[3]; // SHORT, MEDIUM, LONG
for (UploadRecord r : records) {
counts[UploadType.of(r.getDurationSec()).ordinal()]++;
}
long total = records.size();
List<RatioStat> result = new ArrayList<>(3);
for (UploadType type : UploadType.values()) {
RatioStat s = new RatioStat();
s.setType(type);
s.setCount(counts[type.ordinal()]);
s.setRatio(total == 0 ? 0 : (double) counts[type.ordinal()] / total);
result.add(s);
}
return result;
}
3 数据库聚合(推荐生产用)
直接 SQL 聚合,避免全量拉取:
SELECT
CASE
WHEN duration_sec < 60 THEN 'SHORT'
WHEN duration_sec <= 300 THEN 'MEDIUM'
ELSE 'LONG'
END AS type,
COUNT(*) AS cnt,
ROUND(COUNT(*) * 100.0 / SUM(COUNT(*)) OVER (), 2) AS ratio_pct
FROM upload_record
WHERE upload_date = '2024-01-01'
GROUP BY type;
Java 侧只需映射结果:
@Mapper
public interface UploadMapper {
@Select("... 上面 SQL ...")
List<RatioStat> statRatioByDate(@Param("date") LocalDate date);
}
4 缓存 + 定时计算
@Scheduled(cron = "0 5 * * * ?") // 每小时5分
public void refreshRatioCache() {
LocalDate today = LocalDate.now();
Map<String, List<RatioStat>> result = statRatioByDayForDate(today);
redisTemplate.opsForValue().set("upload:ratio:" + today, result, 2, TimeUnit.HOURS);
}
扩展建议
| 场景 | 扩展方式 |
|---|---|
| 按用户分层 | groupingBy(UploadRecord::getUserId) 后再统计 |
| 加权比例 | 按播放量/曝光量加权,而非条数 |
| 风控指标 | 长传占比突增可触发告警 |
| 分位数分析 | 用 long[] durationArr + 排序求 P50/P90/P99 |
| 实时统计 | Flink/Spark Streaming 按窗口聚合 |
- 核心逻辑:
分类枚举 + groupingBy + 计算占比 - 小数据:Stream 一行搞定
- 大数据:单次遍历或 SQL 聚合
- 业务分层:全局 → 渠道 → 天 → 用户,逐层下钻
- 注意:占比要保证分母一致,空数据要防除零
如果你的"长短传"是按文件大小或文本长度区分,只需把 UploadType.of() 里的判断条件替换即可,其余统计逻辑完全复用。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播