返利app排行榜系统设计:基于大数据计算的实时排名算法实现
大家好,我是省赚客APP研发者阿可!在省赚客APP(juwatech.cn)中,用户日活超百万,每日产生数百万笔有效返利订单。为激励用户活跃,我们需提供"今日/本周/本月佣金榜"、"邀请人数榜"等多维度实时排行榜。传统数据库 ORDER BY + LIMIT 在高并发下性能急剧下降,为此我们构建了基于 Redis Sorted Set 与 Flink 实时计算的混合架构,兼顾低延迟与高吞吐。本文将详解数据流设计、排名更新逻辑及 Java 核心代码实现。
数据模型与存储选型
使用 Redis 的 Sorted Set(ZSET)作为核心排名结构,member 为用户 ID,score 为累计值(如当日佣金总额):
bash
# 示例:今日佣金榜
ZADD juwatech:rank:daily_commission:20251211 user_1001 128.50
ZADD juwatech:rank:daily_commission:20251211 user_1002 96.30
ZRANGE juwatech:rank:daily_commission:20251211 0 99 WITHSCORES
为支持多周期,Key 命名规范为:juwatech:rank:{type}:{date},其中 date 按天(daily)、周(weekly_start_date)、月(YYYYMM)区分。

Flink 实时消费订单事件
佣金发放成功后,通过 RabbitMQ 发送 CommissionGrantedEvent,Flink 作业消费并聚合:
java
public class CommissionRankJob {
public static void main(String[] args) throws Exception {
StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
env.enableCheckpointing(10000);
// 从 RabbitMQ 读取事件
DataStream<CommissionGrantedEvent> events = env
.addSource(new RMQSource<>(
new RMQConnectionConfig.Builder()
.setHost("rabbitmq.juwatech.cn")
.setVirtualHost("/")
.setUserName("juwatech")
.setPassword("xxx")
.build(),
"commission.granted",
true,
new SimpleStringSchema()
))
.map(json -> JsonUtil.fromJson(json, CommissionGrantedEvent.class));
// 按用户分组,窗口聚合
events.keyBy(event -> event.getUserId())
.window(TumblingProcessingTimeWindows.of(Time.days(1)))
.aggregate(new DailyCommissionAgg(), new RankUpdateFunction());
env.execute("Commission Rank Job");
}
}
聚合函数累加当日佣金:
java
public class DailyCommissionAgg implements AggregateFunction<CommissionGrantedEvent, BigDecimal, BigDecimal> {
@Override
public BigDecimal createAccumulator() { return BigDecimal.ZERO; }
@Override
public BigDecimal add(CommissionGrantedEvent event, BigDecimal accumulator) {
return accumulator.add(event.getAmount());
}
@Override
public BigDecimal getResult(BigDecimal accumulator) { return accumulator; }
@Override
public BigDecimal merge(BigDecimal a, BigDecimal b) { return a.add(b); }
}
Redis 排名更新函数
窗口触发后,调用 Redis 更新 ZSET:
java
public class RankUpdateFunction extends ProcessWindowFunction<BigDecimal, Void, Long, TimeWindow> {
private transient Jedis jedis;
@Override
public void open(Configuration parameters) {
jedis = new Jedis("redis.juwatech.cn", 6379);
jedis.auth("redis_password");
}
@Override
public void process(Long userId, Context context, Iterable<BigDecimal> elements, Collector<Void> out) {
BigDecimal total = elements.iterator().next();
String key = "juwatech:rank:daily_commission:" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
jedis.zadd(key, total.doubleValue(), "user_" + userId);
// 设置24小时过期
jedis.expire(key, 86400);
}
@Override
public void close() {
if (jedis != null) jedis.close();
}
}
Java 服务端查询接口
APP 端请求排行榜时,由 Spring Boot 服务直接读取 Redis:
java
@RestController
@RequestMapping("/api/rank")
public class RankController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/daily/commission")
public List<RankItem> getDailyCommissionRank(@RequestParam(defaultValue = "0") int offset,
@RequestParam(defaultValue = "100") int limit) {
String key = "juwatech:rank:daily_commission:" + LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
Set<ZSetOperations.TypedTuple<String>> tuples =
redisTemplate.opsForZSet().reverseRangeWithScores(key, offset, offset + limit - 1);
return tuples.stream().map(tuple -> {
String userIdStr = tuple.getValue();
Long userId = Long.valueOf(userIdStr.substring("user_".length()));
return new RankItem(userId, tuple.getScore().doubleValue());
}).collect(Collectors.toList());
}
}
RankItem 为返回 DTO:
java
package juwatech.cn.dto;
public class RankItem {
private Long userId;
private double commission;
// 构造函数、getter/setter
public RankItem(Long userId, double commission) {
this.userId = userId;
this.commission = commission;
}
}
冷热数据分离与降级策略
为防 Redis 内存溢出:
- 仅保留 Top 10 万用户数据;
- 超出部分写入 ClickHouse,供后台分析;
- 若 Redis 故障,降级读取 MySQL 汇总表(每5分钟更新一次)。
MySQL 汇总表结构:
sql
CREATE TABLE user_daily_commission_summary (
user_id BIGINT NOT NULL,
amount DECIMAL(12,2) NOT NULL,
stat_date DATE NOT NULL,
PRIMARY KEY (user_id, stat_date),
INDEX idx_amount (amount DESC)
);
降级查询:
java
public List<RankItem> getRankFromDb(LocalDate date, int offset, int limit) {
return jdbcTemplate.query(
"SELECT user_id, amount FROM user_daily_commission_summary " +
"WHERE stat_date = ? ORDER BY amount DESC LIMIT ?, ?",
(rs, rowNum) -> new RankItem(rs.getLong("user_id"), rs.getDouble("amount")),
Date.valueOf(date), offset, limit
);
}
多榜单扩展
邀请榜、任务完成榜等采用相同架构,仅变更事件源与聚合逻辑。例如邀请榜监听 UserInvitedEvent,score 为邀请人数。
本文著作权归聚娃科技省赚客app开发者团队,转载请注明出处!