返利app排行榜系统设计:基于大数据计算的实时排名算法实现

返利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)区分。

佣金发放成功后,通过 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开发者团队,转载请注明出处!

相关推荐
C雨后彩虹2 小时前
字符串拼接
java·数据结构·算法·华为·面试
LYFlied2 小时前
【每日算法】LeetCode 279. 完全平方数(动态规划)
前端·算法·leetcode·面试·动态规划
scx201310043 小时前
20251201换根DP总结
算法·动态规划·换根dp
Hello.Reader3 小时前
Flink Table API & SQL Functions 函数类型划分、引用方式与解析优先级
大数据·sql·flink
zd2005723 小时前
STREAMS指南:环境及宿主相关微生物组研究中的技术报告标准
人工智能·python·算法
啊吧怪不啊吧3 小时前
机器学习模型部署全流程实战:从训练完成到上线可用
大数据·人工智能·机器学习
Data_agent3 小时前
京东商品价格历史信息API使用指南
java·大数据·前端·数据库·python
TechNomad3 小时前
排序算法:基数排序算法
算法·排序算法
专业机床数据采集3 小时前
西门子数控数采集变量与说明对照表
大数据·网络·cnc数据采集