Redis场景实战:各种排行榜与评分系统都给你写好了

797 阅读23分钟

Redis场景实战:各种排行榜与评分系统

虽然我没上过掘金的排行榜,但是不妨碍我自己实现一下。Redis是一种高性能的内存数据库,广泛应用于各种高实时性需求的场景。特别是在排行榜和评分系统中,Redis的有序集合(ZSet)提供了强大的支持。本文将详细介绍如何使用封装好的Redis工具类来实现这些功能,并展示其在实际应用中的重要性。

基础封装在@使用 Node.js、ioredis 和 TS 封装 Redis 操作.md一文中

git仓库@TempRedisTools

使用有序集合(ZSet)实现排行榜

有序集合(ZSet)的基本概念

有序集合(ZSet)是Redis中的一种数据结构,它不仅存储了元素的值,还为每个元素关联了一个分数(score)。有序集合中的元素是按照分数进行排序的。

定义

有序集合是一个包含唯一字符串成员的集合,每个成员都有一个与之相关的分数。成员在集合中的排序是基于分数的,从最低到最高。

常见操作命令
  • ZADD:向有序集合添加一个或多个成员,或者更新已存在成员的分数。
  • ZREM:移除有序集合中的一个或多个成员。
  • ZRANGE:返回有序集合中指定区间内的成员。
  • ZRANGEBYSCORE:返回有序集合中指定分数范围内的成员。
  • ZSCORE:返回有序集合中指定成员的分数。

实现排行榜

排行榜的基本需求和设计

一个典型的排行榜需要支持以下基本功能:

  • 添加用户及其分数
  • 删除用户
  • 查询用户的排名及分数
  • 获取前N名用户

封装Leaderboard类

为了更好地管理排行榜,我们可以封装一个Leaderboard类。以下是实现代码:

import { Redis as RedisClient } from "ioredis";
import { CacheSortedSet } from "../TSRedisCacheKit/SortedSet";

export class Leaderboard {
  private redis: RedisClient;
  private leaderboard: CacheSortedSet;

  /**
   * 构造函数
   * @param {RedisClient} redis - Redis客户端实例
   * @param {string} leaderboardKey - 排行榜的键
   */
  constructor(redis: RedisClient, leaderboardKey: string) {
    this.redis = redis;
    this.leaderboard = new CacheSortedSet(
      leaderboardKey,
      { appName: "app", funcName: "leaderboard" },
      redis
    );
  }

  /**
   * 添加用户及其分数
   * @param {string} userId - 用户ID
   * @param {number} score - 用户分数
   */
  async addUser(userId: string, score: number) {
    await this.leaderboard.add(score, userId);
  }

  /**
   * 删除用户
   * @param {string} userId - 用户ID
   */
  async removeUser(userId: string) {
    await this.leaderboard.remove(userId);
  }

  /**
   * 获取用户排名及分数
   * @param {string} userId - 用户ID
   * @returns {Promise<{rank: number, score: number}>} 用户的排名和分数
   */
  async getUserRank(userId: string) {
    const rank = await this.redis.zrevrank(
      this.leaderboard.createKey(),
      userId
    );
    const score = await this.leaderboard.score(userId);
    return { rank, score };
  }

  /**
   * 获取前N名用户
   * @param {number} n - 要获取的用户数量
   * @returns {Promise<Array<{member: string, score: number}>>} 前N名用户及其分数
   */
  async getTopUsers(n: number) {
    return await this.leaderboard.rangeWithScores(0, n - 1);
  }

  /**
   * 实时更新用户分数
   * @param {string} userId - 用户ID
   * @param {number} score - 新增的分数
   */
  async updateScore(userId: string, score: number) {
    await this.leaderboard.incrementBy(userId, score);
  }
}

实践案例

游戏积分排行榜

在游戏中,玩家的积分是动态变化的,通过有序集合可以方便地管理和查询玩家的积分排名。以下是一个示例代码,展示如何使用封装的 Leaderboard 类来实现游戏积分排行榜:

import { Redis as RedisClient } from "ioredis";
import { Leaderboard } from './leaderboard'; 

(async () => {
  const redis = new RedisClient({ host: 'localhost', port: 6379 });
  const leaderboard = new Leaderboard(redis, 'game:leaderboard');

  // 添加玩家及其积分
  await leaderboard.addUser('player1', 100);
  await leaderboard.addUser('player2', 200);
  await leaderboard.addUser('player3', 150);

  // 获取前3名玩家
  console.log(await leaderboard.getTopUsers(3));

  // 获取指定玩家的排名及积分
  console.log(await leaderboard.getUserRank('player1'));

  // 实时更新玩家积分
  await leaderboard.updateScore('player1', 10);

  // 获取更新后的前3名玩家
  console.log(await leaderboard.getTopUsers(3));

  redis.quit();
})();

在这个示例中,我们创建了一个游戏积分排行榜,并添加了三名玩家及其积分。通过 getTopUsers 方法,我们可以获取前3名玩家的排名和积分。通过 getUserRank 方法,我们可以获取指定玩家的排名和积分。通过 updateScore 方法,我们可以实时更新玩家的积分,并再次获取更新后的前3名玩家。

销售排行榜

在电商平台上,可以使用有序集合来管理商品的销售数据,并生成实时的销售排行榜。以下是一个示例代码,展示如何使用封装的 Leaderboard 类来实现销售排行榜:

import { Redis as RedisClient } from "ioredis";
import { Leaderboard } from './leaderboard';

(async () => {
  const redis = new RedisClient({ host: 'localhost', port: 6379 });
  const leaderboard = new Leaderboard(redis, 'sales:leaderboard');

  // 添加商品及其销售数据
  await leaderboard.addUser('product1', 500);
  await leaderboard.addUser('product2', 800);
  await leaderboard.addUser('product3', 600);

  // 获取前3名商品
  console.log(await leaderboard.getTopUsers(3));

  // 获取指定商品的排名及销售数据
  console.log(await leaderboard.getUserRank('product1'));

  // 实时更新商品销售数据
  await leaderboard.updateScore('product1', 100);

  // 获取更新后的前3名商品
  console.log(await leaderboard.getTopUsers(3));

  redis.quit();
})();

在这个示例中,我们创建了一个销售排行榜,并添加了三件商品及其销售数据。通过 getTopUsers 方法,我们可以获取前3名商品的排名和销售数据。通过 getUserRank 方法,我们可以获取指定商品的排名和销售数据。通过 updateScore 方法,我们可以实时更新商品的销售数据,并再次获取更新后的前3名商品。

通过这些示例,我们可以看到如何使用封装好的 Leaderboard 类来实现动态更新的游戏积分排行榜和销售排行榜。这种方法不仅高效,而且易于扩展和维护。希望这些示例代码能为您在实际开发中提供有价值的参考。

评分系统的设计与实现

评分系统的基本概念

评分系统广泛应用于电影、产品等各种评价场景中。通过评分系统,可以对用户的评价进行统计和分析。

评分的定义和用途

评分系统的核心是对项目(如电影、产品)进行评分,并根据评分进行排序和查询。

常见操作命令
  • ZINCRBY:增加有序集合中成员的分数。
  • ZSCORE:返回有序集合中指定成员的分数。
  • ZRANK:返回有序集合中指定成员的排名(从低到高)。
  • ZREVRANK:返回有序集合中指定成员的排名(从高到低)。

封装RatingSystem类

为了更好地管理评分系统,我们可以封装一个RatingSystem类。以下是实现代码:

import { Redis as RedisClient } from "ioredis";
import { CacheSortedSet } from "../TSRedisCacheKit/SortedSet";

export class RatingSystem {
  private redis: RedisClient;
  private rating: CacheSortedSet;

  /**
   * 构造函数
   * @param {RedisClient} redis - Redis客户端实例
   * @param {string} ratingKey - 评分系统的键
   */
  constructor(redis: RedisClient, ratingKey: string) {
    this.redis = redis;
    this.rating = new CacheSortedSet(
      ratingKey,
      { appName: "app", funcName: "rating" },
      redis
    );
  }

  /**
   * 添加评分
   * @param {string} itemId - 项目ID
   * @param {number} score - 评分
   */
  async addRating(itemId: string, score: number) {
    await this.rating.incrementBy(itemId, score);
  }

  /**
   * 获取评分
   * @param {string} itemId - 项目ID
   * @returns {Promise<number>} 评分
   */
  async getRating(itemId: string) {
    return await this.rating.score(itemId);
  }

  /**
   * 获取排名
   * @param {string} itemId - 项目ID
   * @returns {Promise<number | null>} 排名,若没有找到则返回null
   */
  async getRatingRank(itemId: string) {
    const rank = await this.redis.zrevrank(this.rating.createKey(), itemId);
    return rank;
  }

  /**
   * 实时更新评分
   * @param {string} itemId - 项目ID
   * @param {number} score - 新增的评分
   */
  async updateRating(itemId: string, score: number) {
    await this.rating.incrementBy(itemId, score);
  }
}

实践案例

电影评分系统

在电影评分系统中,用户可以对电影进行评分,通过有序集合可以方便地管理和查询电影的评分及排名。以下是一个示例代码,展示如何使用封装的 RatingSystem 类来实现电影评分系统:

import { Redis as RedisClient } from "ioredis";
import { RatingSystem } from './ratingSystem'; 

(async () => {
  const redis = new RedisClient({ host: 'localhost', port: 6379 });
  const ratingSystem = new RatingSystem(redis, 'movie:ratings');

  // 添加电影评分
  await ratingSystem.addRating('movie1', 5);
  await ratingSystem.addRating('movie2', 3);
  await ratingSystem.addRating('movie3', 4);

  // 获取指定电影的评分
  console.log(await ratingSystem.getRating('movie1'));

  // 获取指定电影的排名
  console.log(await ratingSystem.getRatingRank('movie1'));

  // 实时更新电影评分
  await ratingSystem.updateRating('movie1', 2);

  // 获取更新后的电影评分
  console.log(await ratingSystem.getRating('movie1'));

  redis.quit();
})();

在这个示例中,我们创建了一个电影评分系统,并添加了三部电影及其评分。通过 getRating 方法,我们可以获取指定电影的评分。通过 getRatingRank 方法,我们可以获取指定电影的排名。通过 updateRating 方法,我们可以实时更新电影的评分,并再次获取更新后的电影评分。

产品评分系统

在电商平台上,可以使用有序集合来管理产品的评分数据,并生成实时的产品评分排行榜。以下是一个示例代码,展示如何使用封装的 RatingSystem 类来实现产品评分系统:

import { Redis as RedisClient } from "ioredis";
import { RatingSystem } from './ratingSystem'; 

(async () => {
  const redis = new RedisClient({ host: 'localhost', port: 6379 });
  const ratingSystem = new RatingSystem(redis, 'product:ratings');

  // 添加产品评分
  await ratingSystem.addRating('product1', 4.5);
  await ratingSystem.addRating('product2', 3.8);
  await ratingSystem.addRating('product3', 4.0);

  // 获取指定产品的评分
  console.log(await ratingSystem.getRating('product1'));

  // 获取指定产品的排名
  console.log(await ratingSystem.getRatingRank('product1'));

  // 实时更新产品评分
  await ratingSystem.updateRating('product1', 0.5);

  // 获取更新后的产品评分
  console.log(await ratingSystem.getRating('product1'));

  redis.quit();
})();

在这个示例中,我们创建了一个产品评分系统,并添加了三款产品及其评分。通过 getRating 方法,我们可以获取指定产品的评分。通过 getRatingRank 方法,我们可以获取指定产品的排名。通过 updateRating 方法,我们可以实时更新产品的评分,并再次获取更新后的产品评分。

通过这些示例,我们可以看到如何使用封装好的 RatingSystem 类来实现动态更新的电影评分系统和产品评分系统。这种方法不仅高效,而且易于扩展和维护。希望这些示例代码能为您在实际开发中提供有价值的参考。

动态更新的排行榜和评分系统

动态更新排行榜

实现实时更新排行榜数据的需求

在一些实时性要求较高的场景中,如竞赛排名、实时积分榜,需要对排行榜进行实时更新。

实践代码

以下是如何使用封装的Leaderboard类来实现动态更新排行榜的示例代码:

import { Redis as RedisClient } from "ioredis";
import { Leaderboard } from './leaderboard'; 

(async () => {
  const redis = new RedisClient({ host: 'localhost', port: 6379 });
  const leaderboard = new Leaderboard(redis, 'game:leaderboard');
  await leaderboard.addUser('user1', 100);
  await leaderboard.addUser('user2', 200);
  await leaderboard.addUser('user3', 150);
  console.log(await leaderboard.getTopUsers(3)); // 获取前3名用户
  console.log(await leaderboard.getUserRank('user1')); // 获取user1的排名及分数
  await leaderboard.updateScore('user1', 10); // 实时更新user1的分数
  console.log(await leaderboard.getTopUsers(3)); // 获取前3名用户
  redis.quit();
})();

动态更新评分系统

实现实时更新评分数据的需求

在一些实时性要求较高的场景中,如实时电影评分、实时产品评分,需要对评分系统进行实时更新。

实践代码

以下是如何使用封装的RatingSystem类来实现动态更新评分系统的示例代码:

import { Redis as RedisClient } from "ioredis";
import { RatingSystem } from './ratingSystem'; 

(async () => {
  const redis = new RedisClient({ host: 'localhost', port: 6379 });
  const ratingSystem = new RatingSystem(redis, 'movie:ratings');
  await ratingSystem.addRating('movie1', 5);
  await ratingSystem.addRating('movie2', 3);
  await ratingSystem.addRating('movie1', 2); // 更新movie1的评分
  console.log(await ratingSystem.getRating('movie1')); // 获取movie1的评分
  console.log(await ratingSystem.getRatingRank('movie1')); // 获取movie1的排名
  await ratingSystem.updateRating('movie1', 1); // 实时更新movie1的评分
  console.log(await ratingSystem.getRating('movie1')); // 获取movie1的评分
  redis.quit();
})();

高级应用

多维度排行榜

实现基于多个维度的排行榜

在一些复杂场景中,需要基于多个维度进行排序和查询。

实践代码

以下是如何使用封装的Leaderboard类来实现多维度排行榜的示例代码:

import { Redis as RedisClient } from "ioredis";
import { Leaderboard } from './leaderboard'; // 假设封装类存于 ./leaderboard

const multiDimensionalKey = 'game:multi_dimensional_leaderboard';

class MultiDimensionalLeaderboard {
  private redis: RedisClient;
  private leaderboards: { [dimension: string]: Leaderboard };

  constructor(redis: RedisClient) {
    this.redis = redis;
    this.leaderboards = {};
  }

  // 添加综合评分
  async addCompositeScore(userId: string, score: number, dimension: string) {
    if (!this.leaderboards[dimension]) {
      this.leaderboards[dimension] = new Leaderboard(this.redis, `${multiDimensionalKey}:${dimension}`);
    }
    await this.leaderboards[dimension].addUser(userId, score);
  }

  // 获取综合排名
  async getCompositeRank(userId: string, dimension: string) {
    if (!this.leaderboards[dimension]) {
      this.leaderboards[dimension] = new Leaderboard(this.redis, `${multiDimensionalKey}:${dimension}`);
    }
    return await this.leaderboards[dimension].getUserRank(userId);
  }
}

// 示例:添加、查询综合评分数据
(async () => {
  const redis = new RedisClient({ host: 'localhost', port: 6379 });
  const leaderboard = new MultiDimensionalLeaderboard(redis);

  await leaderboard.addCompositeScore('user1', 100, 'level');
  await leaderboard.addCompositeScore('user1', 50, 'speed');
  console.log(await leaderboard.getCompositeRank('user1', 'level')); // 获取user1在level维度的排名
  redis.quit();
})();
主要逻辑
  1. 定义常量

    • 定义一个基础键前缀 multiDimensionalKey,用于标识多维度排行榜的键。
  2. 定义 MultiDimensionalLeaderboard

    • 包含一个 Redis 客户端实例和一个存储不同维度的排行榜实例的对象。
    • 提供了两个主要方法:
      • addCompositeScore:向指定维度的排行榜中添加用户的分数。
      • getCompositeRank:获取用户在指定维度的排行榜中的排名。
  3. 示例代码

    • 创建 Redis 客户端实例,连接到本地 Redis 服务器。
    • 创建 MultiDimensionalLeaderboard 实例。
    • levelspeed 维度的排行榜中添加用户 user1 的分数。
    • 获取并打印用户 user1level 维度中的排名。
    • 关闭 Redis 连接。
代码执行流程
  1. 初始化

    • 创建 Redis 客户端和 MultiDimensionalLeaderboard 实例。
  2. 添加分数

    • 检查指定维度的排行榜实例是否存在,不存在则创建。
    • 向指定维度的排行榜中添加用户及其分数。
  3. 获取排名

    • 检查指定维度的排行榜实例是否存在,不存在则创建。
    • 获取并返回用户在该维度排行榜中的排名。

通过这些步骤,代码实现了一个多维度的排行榜系统,可以管理和查询用户在不同维度上的表现。

实践案例:自走棋某一局的棋子贡献榜单

在自走棋游戏中,每个棋子在一局中的表现可以通过多个维度来衡量,如击杀数、助攻数和伤害输出等。我们可以使用 Leaderboard 类来实现这些维度的排行榜,并根据这些维度生成综合评分。

代码实现
import { Redis as RedisClient } from "ioredis";
import { Leaderboard } from './leaderboard'; // 假设封装类存于 ./leaderboard

const multiDimensionalKey = 'autochess:match:contribution';

class ChessPieceContributionLeaderboard {
  private redis: RedisClient;
  private leaderboards: { [dimension: string]: Leaderboard };

  constructor(redis: RedisClient) {
    this.redis = redis;
    this.leaderboards = {};
  }

  private getLeaderboardKey(baseKey: string, dimension: string) {
    return `${baseKey}:${dimension}`;
  }

  private getLeaderboard(baseKey: string, dimension: string) {
    const key = this.getLeaderboardKey(baseKey, dimension);
    if (!this.leaderboards[dimension]) {
      this.leaderboards[dimension] = new Leaderboard(this.redis, key);
    }
    return this.leaderboards[dimension];
  }

  // 添加棋子的击杀数
  async addKills(pieceId: string, kills: number, matchId: string) {
    const leaderboard = this.getLeaderboard(`${multiDimensionalKey}:${matchId}`, 'kills');
    await leaderboard.addUser(pieceId, kills);
  }

  // 添加棋子的助攻数
  async addAssists(pieceId: string, assists: number, matchId: string) {
    const leaderboard = this.getLeaderboard(`${multiDimensionalKey}:${matchId}`, 'assists');
    await leaderboard.addUser(pieceId, assists);
  }

  // 添加棋子的伤害输出
  async addDamage(pieceId: string, damage: number, matchId: string) {
    const leaderboard = this.getLeaderboard(`${multiDimensionalKey}:${matchId}`, 'damage');
    await leaderboard.addUser(pieceId, damage);
  }

  // 获取棋子在某一维度的排名
  async getRank(pieceId: string, dimension: string, matchId: string) {
    const leaderboard = this.getLeaderboard(`${multiDimensionalKey}:${matchId}`, dimension);
    return await leaderboard.getUserRank(pieceId);
  }

  // 获取棋子在某一维度的分数
  async getScore(pieceId: string, dimension: string, matchId: string) {
    const leaderboard = this.getLeaderboard(`${multiDimensionalKey}:${matchId}`, dimension);
    return await leaderboard.getUserRank(pieceId);
  }

  // 获取棋子的综合排名(简单加权平均)
  async getCompositeRank(pieceId: string, matchId: string) {
    const kills = await this.getScore(pieceId, 'kills', matchId);
    const assists = await this.getScore(pieceId, 'assists', matchId);
    const damage = await this.getScore(pieceId, 'damage', matchId);

    // 计算综合评分,这里简单地将三个维度的分数加权平均
    const compositeScore = (kills * 0.5) + (assists * 0.3) + (damage * 0.2);

    return compositeScore;
  }
}

// 示例:添加、查询棋子贡献数据
(async () => {
  const redis = new RedisClient({ host: 'localhost', port: 6379 });
  const leaderboard = new ChessPieceContributionLeaderboard(redis);

  const matchId = 'match1';

  // 添加棋子贡献数据
  await leaderboard.addKills('piece1', 10, matchId);
  await leaderboard.addAssists('piece1', 5, matchId);
  await leaderboard.addDamage('piece1', 2000, matchId);

  await leaderboard.addKills('piece2', 8, matchId);
  await leaderboard.addAssists('piece2', 7, matchId);
  await leaderboard.addDamage('piece2', 1800, matchId);

  // 获取棋子在各维度的排名
  console.log(await leaderboard.getRank('piece1', 'kills', matchId)); // 获取piece1的击杀排名
  console.log(await leaderboard.getRank('piece1', 'assists', matchId)); // 获取piece1的助攻排名
  console.log(await leaderboard.getRank('piece1', 'damage', matchId)); // 获取piece1的伤害排名

  // 获取棋子的综合排名(综合评分)
  console.log(await leaderboard.getCompositeRank('piece1', matchId)); // 获取piece1的综合评分
  console.log(await leaderboard.getCompositeRank('piece2', matchId)); // 获取piece2的综合评分

  redis.quit();
})();
代码说明
  1. ChessPieceContributionLeaderboard

    • 封装了处理棋子贡献数据的逻辑,包括添加击杀数、助攻数和伤害输出,以及获取这些维度的排名和分数。
    • 提供了一个方法 getCompositeRank 来计算棋子的综合评分,这里简单地将三个维度的分数加权平均。
  2. 示例代码

    • 创建了一个 ChessPieceContributionLeaderboard 实例,并添加了两枚棋子的贡献数据。
    • 获取了棋子在各维度的排名和综合评分。

通过这个示例,我们可以看到如何使用封装好的 Leaderboard 类来实现自走棋某一局的棋子贡献榜单。这种方法不仅高效,而且易于扩展和维护。希望这些示例代码能为您在实际开发中提供有价值的参考。

时间范围排行榜

实现基于时间范围的排行榜

在一些应用场景中,需要基于时间范围(如每日、每周、每月)进行排序和查询。

实践代码

以下是如何使用封装的Leaderboard类来实现时间范围排行榜的示例代码:

import { Redis as RedisClient } from "ioredis";
import { Leaderboard } from './leaderboard'; 

const dailyKey = 'game:daily_leaderboard';
const weeklyKey = 'game:weekly_leaderboard';
const monthlyKey = 'game:monthly_leaderboard';

class TimeBasedLeaderboard {
  private redis: RedisClient;
  private leaderboards: { [key: string]: Leaderboard };

  constructor(redis: RedisClient) {
    this.redis = redis;
    this.leaderboards = {};
  }

  private getLeaderboardKey(baseKey: string, period: string) {
    return `${baseKey}:${period}`;
  }

  private getLeaderboard(baseKey: string, period: string) {
    const key = this.getLeaderboardKey(baseKey, period);
    if (!this.leaderboards[key]) {
      this.leaderboards[key] = new Leaderboard(this.redis, key);
    }
    return this.leaderboards[key];
  }

  // 添加每日积分
  async addDailyScore(userId: string, score: number, date: string) {
    const leaderboard = this.getLeaderboard(dailyKey, date);
    await leaderboard.addUser(userId, score);
  }

  // 获取每日排名
  async getDailyRank(userId: string, date: string) {
    const leaderboard = this.getLeaderboard(dailyKey, date);
    return await leaderboard.getUserRank(userId);
  }

  // 添加每周销售数据
  async addWeeklySales(itemId: string, sales: number, week: string) {
    const leaderboard = this.getLeaderboard(weeklyKey, week);
    await leaderboard.addUser(itemId, sales);
  }

  // 获取每周排名
  async getWeeklyRank(itemId: string, week: string) {
    const leaderboard = this.getLeaderboard(weeklyKey, week);
    return await leaderboard.getUserRank(itemId);
  }

  // 添加每月销售数据
  async addMonthlySales(itemId: string, sales: number, month: string) {
    const leaderboard = this.getLeaderboard(monthlyKey, month);
    await leaderboard.addUser(itemId, sales);
  }

  // 获取每月排名
  async getMonthlyRank(itemId: string, month: string) {
    const leaderboard = this.getLeaderboard(monthlyKey, month);
    return await leaderboard.getUserRank(itemId);
  }
}

// 示例:添加、查询每日积分数据
(async () => {
  const redis = new RedisClient({ host: 'localhost', port: 6379 });
  const leaderboard = new TimeBasedLeaderboard(redis);

  // 添加每日积分
  await leaderboard.addDailyScore('user1', 100, '2024-07-22');
  await leaderboard.addDailyScore('user2', 200, '2024-07-22');
  console.log(await leaderboard.getDailyRank('user1', '2024-07-22')); // 获取user1在2024-07-22的排名及分数

  // 添加每周销售数据
  await leaderboard.addWeeklySales('item1', 500, '2024-W30');
  await leaderboard.addWeeklySales('item2', 800, '2024-W30');
  console.log(await leaderboard.getWeeklyRank('item1', '2024-W30')); // 获取item1在2024-W30的排名及销售数据

  // 添加每月销售数据
  await leaderboard.addMonthlySales('item1', 1500, '2024-07');
  await leaderboard.addMonthlySales('item2', 1800, '2024-07');
  console.log(await leaderboard.getMonthlyRank('item1', '2024-07')); // 获取item1在2024-07的排名及销售数据

  redis.quit();
})();

通过Redis的有序集合(ZSet),我们可以方便地管理和查询每日、每周、每月的积分和销售数据,并生成相应的排行榜。这种方法不仅高效,而且易于扩展和维护。希望以上示例代码能为您在实际开发中提供有价值的参考。

综合示例:掘金文章综合榜单

image.png

需求分析

  1. 综合文章榜单

    • 需要一个综合评分系统,综合多维度数据生成评分。
    • 支持添加和更新用户的积分和评分。
    • 支持获取用户的综合排名和前 N 名用户的综合评分。
  2. 维度

    • 积分维度:如阅读量、点赞数、评论数等。
    • 评分维度:如用户评分、专家评分等。
  3. 功能

    • 添加和更新用户的积分和评分。
    • 获取用户的综合排名。
    • 获取前 N 名用户的综合排名。

设计实现

  1. CompositeLeaderboard 类

    • 管理多个维度的排行榜和评分系统。
    • 提供方法添加和更新用户的积分和评分。
    • 提供方法获取用户的综合排名和前 N 名用户的综合评分。
  2. 综合评分计算

    • 可以根据不同维度的权重计算综合评分。
    • 综合评分计算公式:综合评分 = 积分 * 权重1 + 评分 * 权重2

代码实现

import { Redis as RedisClient } from "ioredis";
import { Leaderboard } from "./Leaderboard";
import { RatingSystem } from "./RatingSystem";

/**
 * 综合排行榜类
 */
export class CompositeLeaderboard {
  private redis: RedisClient;
  private leaderboards: Record<string, Leaderboard>;
  private ratingSystems: Record<string, RatingSystem>;
  private compositeKey: string;

  /**
   * 构造函数
   * @param {RedisClient} redis - Redis客户端实例
   * @param {string} compositeKey - 综合排行榜的键
   */
  constructor(redis: RedisClient, compositeKey: string) {
    this.redis = redis;
    this.leaderboards = {};
    this.ratingSystems = {};
    this.compositeKey = compositeKey;
  }

  /**
   * 获取排行榜键
   * @param {string} baseKey - 基础键
   * @param {string} period - 时间段
   * @returns {string} 排行榜键
   */
  private getLeaderboardKey(baseKey: string, period: string) {
    return `${baseKey}:${period}`;
  }

  /**
   * 获取排行榜实例
   * @param {string} baseKey - 基础键
   * @param {string} period - 时间段
   * @returns {Leaderboard} 排行榜实例
   */
  private getLeaderboard(baseKey: string, period: string) {
    const key = this.getLeaderboardKey(baseKey, period);
    if (!this.leaderboards[key]) {
      this.leaderboards[key] = new Leaderboard(this.redis, key);
    }
    return this.leaderboards[key];
  }

  /**
   * 获取评分系统键
   * @param {string} baseKey - 基础键
   * @param {string} period - 时间段
   * @returns {string} 评分系统键
   */
  private getRatingSystemKey(baseKey: string, period: string) {
    return `${baseKey}:${period}`;
  }

  /**
   * 获取评分系统实例
   * @param {string} baseKey - 基础键
   * @param {string} period - 时间段
   * @returns {RatingSystem} 评分系统实例
   */
  private getRatingSystem(baseKey: string, period: string) {
    const key = this.getRatingSystemKey(baseKey, period);
    if (!this.ratingSystems[key]) {
      this.ratingSystems[key] = new RatingSystem(this.redis, key);
    }
    return this.ratingSystems[key];
  }

  /**
   * 添加用户积分
   * @param {string} userId - 用户ID
   * @param {number} score - 积分
   * @param {string} date - 日期
   */
  async addScore(userId: string, score: number, date: string) {
    const leaderboard = this.getLeaderboard(this.compositeKey, date);
    await leaderboard.addUser(userId, score);
  }

  /**
   * 添加用户评分
   * @param {string} itemId - 项目ID
   * @param {number} score - 评分
   * @param {string} date - 日期
   */
  async addRating(itemId: string, score: number, date: string) {
    const ratingSystem = this.getRatingSystem(this.compositeKey, date);
    await ratingSystem.addRating(itemId, score);
  }

  /**
   * 获取用户综合排名及评分
   * @param {string} userId - 用户ID
   * @param {string} date - 日期
   * @returns {Promise<{rank: number, score: number}>} 用户的综合排名及评分
   */
  async getCompositeRank(
    userId: string,
    date: string
  ): Promise<{ rank: number; score: number }> {
    const leaderboard = this.getLeaderboard(this.compositeKey, date);
    const ratingSystem = this.getRatingSystem(this.compositeKey, date);

    const userRank = await leaderboard.getUserRank(userId);
    const userRating = await ratingSystem.getRating(userId);

    // 检查 userRank 和 userRating 是否为 null
    const rankScore = userRank?.score ?? 0;
    const ratingScore = userRating ?? 0;

    // 综合评分计算方式:积分 * 权重1 + 评分 * 权重2
    const compositeScore = rankScore * 0.7 + ratingScore * 0.3;
    return { rank: userRank?.rank ?? -1, score: compositeScore };
  }

  /**
   * 获取前N名用户及其综合评分
   * @param {number} n - 用户数量
   * @param {string} date - 日期
   * @returns {Promise<Array<{member: string, score: number}>>} 前N名用户及其综合评分
   */
  async getTopCompositeUsers(
    n: number,
    date: string
  ): Promise<{ member: string; score: number }[]> {
    const leaderboard = this.getLeaderboard(this.compositeKey, date);
    const topUsers = await leaderboard.getTopUsers(n);

    const compositeScores = await Promise.all(
      topUsers.map(async (user) => {
        const rating = await this.getRatingSystem(
          this.compositeKey,
          date
        ).getRating(user.value);
        const ratingScore = rating ?? 0;
        return {
          member: user.value,
          score: user.score * 0.7 + ratingScore * 0.3, // 综合评分计算方式
        };
      })
    );

    compositeScores.sort((a, b) => b.score - a.score); // 按综合评分排序
    return compositeScores;
  }
}

// 示例:添加、查询综合文章榜单
(async () => {
  const redis = new RedisClient({ host: "localhost", port: 6379 });
  const compositeLeaderboard = new CompositeLeaderboard(
    redis,
    "composite:leaderboard"
  );

  // 添加每日积分和评分
  const articles = [
    {
      userId: "user1",
      score: 5000,
      rating: 167,
      name: "Web前端浅谈ArkTS组件开发",
    },
    {
      userId: "user2",
      score: 5200,
      rating: 96,
      name: "哔哩哔哩APP的 AGP8 升级之旅",
    },
    {
      userId: "user3",
      score: 3200,
      rating: 115,
      name: "我写了一个ESLint插件,解决了团队棘手问题",
    },
    {
      userId: "user4",
      score: 2500,
      rating: 117,
      name: "这一年我优化了一个46万行的超级系统",
    },
    {
      userId: "user5",
      score: 1500,
      rating: 40,
      name: "使用Hutool要注意了!升级到6.0后你调用的所有方法都将报错",
    },
    {
      userId: "user6",
      score: 1500,
      rating: 24,
      name: "现在前端组长都是这样做 Code Review",
    },
    {
      userId: "user7",
      score: 1300,
      rating: 25,
      name: "别再用雪花算法生成ID了!试试这个吧",
    },
    {
      userId: "user8",
      score: 1000,
      rating: 34,
      name: "📚 Vue3 多组件的 N 种编写方式",
    },
    {
      userId: "user9",
      score: 763,
      rating: 46,
      name: "vue的组件化开发?一个小demo就带你掌握!",
    },
    {
      userId: "user10",
      score: 983,
      rating: 34,
      name: "Flutter 组件集录 | 后悔药 UndoHistory",
    },
  ];

  for (const article of articles) {
    await compositeLeaderboard.addScore(
      article.userId,
      article.score,
      "2024-07-22"
    );
    await compositeLeaderboard.addRating(
      article.userId,
      article.rating,
      "2024-07-22"
    );
  }

  // 获取2024-07-22的前10名用户及其综合评分
  const topUsers = await compositeLeaderboard.getTopCompositeUsers(
    10,
    "2024-07-22"
  );

  // 模拟获取用户详细信息的函数
  const getUserDetails = async (userId: string) => {
    const userDetails: Record<string, { name: string; avatar: string }> = {
      user1: { name: "JS老狗", avatar: "avatar1.png" },
      user2: { name: "程序员厉飞雨", avatar: "avatar2.png" },
      user3: { name: "颜海镜", avatar: "avatar3.png" },
      user4: { name: "河畔一角", avatar: "avatar4.png" },
      user5: { name: "赵侠客", avatar: "avatar5.png" },
      user6: { name: "大麦大麦", avatar: "avatar6.png" },
      user7: { name: "码财同行", avatar: "avatar7.png" },
      user8: { name: "pany", avatar: "avatar8.png" },
      user9: { name: "今天一定晴q", avatar: "avatar9.png" },
      user10: { name: "张风捷特烈", avatar: "avatar10.png" },
    };
    return userDetails[userId];
  };

  // 打印综合文章榜单
  console.log("掘金文章榜 · 综合");
  for (let i = 0; i < topUsers.length; i++) {
    const user = topUsers[i];
    const userDetails = await getUserDetails(user.member);
    const article = articles.find((article) => article.userId === user.member);
    console.log(
      `${i + 1} ${userDetails.name} ${userDetails.avatar} ${
        article?.name || "未知文章"
      } ${user.score} 热度`
    );
  }

  redis.quit();
})();

关键注释

1. CompositeLeaderboard 类
  • 管理多个维度的排行榜和评分系统

    • CompositeLeaderboard 类通过 leaderboardsratingSystems 属性分别管理多个排行榜和评分系统实例。
    • 这些实例是通过 getLeaderboardgetRatingSystem 方法动态创建和缓存的。
  • 提供方法 addScoreaddRating 添加用户的积分和评分

    • addScore(userId: string, score: number, date: string) 方法用于在指定日期为用户添加积分。
    • addRating(itemId: string, score: number, date: string) 方法用于在指定日期为项目(如文章)添加评分。
  • 提供方法 getCompositeRank 获取用户的综合排名

    • getCompositeRank(userId: string, date: string) 方法计算并返回用户在指定日期的综合排名和综合评分。
  • 提供方法 getTopCompositeUsers 获取前 N 名用户及其综合评分

    • getTopCompositeUsers(n: number, date: string) 方法返回指定日期前 N 名用户及其综合评分,并按综合评分排序。
2. 综合评分计算
  • 综合评分 = 积分 * 0.7 + 评分 * 0.3
    • 综合评分的计算公式是通过给积分和评分赋予不同的权重来实现的。
    • 当前的权重设置为积分占 70%,评分占 30%。这个比例可以根据具体需求进行调整。
3. 示例代码
  • 创建 Redis 客户端和 CompositeLeaderboard 实例

    • 示例代码通过 new RedisClient({ host: "localhost", port: 6379 }) 创建 Redis 客户端实例。
    • 然后通过 new CompositeLeaderboard(redis, "composite:leaderboard") 创建 CompositeLeaderboard 实例。
  • 添加用户的每日积分和评分

    • 示例代码遍历 articles 数组,分别调用 addScoreaddRating 方法为每篇文章添加积分和评分。
  • 获取用户在指定日期的综合排名及分数

    • 示例代码通过调用 getCompositeRank 方法获取用户在指定日期的综合排名和综合评分。
  • 获取指定日期的前 N 名用户及其综合评分

    • 示例代码通过调用 getTopCompositeUsers 方法获取指定日期前 10 名用户及其综合评分,并打印结果。

通过这些步骤,代码实现了一个类似掘金综合文章榜单的系统,可以根据多维度数据生成综合评分,并生成榜单。这个解释是准确的,并且涵盖了代码的核心功能和实现细节。

运行效果

掘金文章榜 · 综合

1 程序员厉飞雨 avatar2.png 哔哩哔哩APP的 AGP8 升级之旅 3672.3999999999996 热度

2 JS老狗 avatar1.png Web前端浅谈ArkTS组件开发 3554.6 热度

3 颜海镜 avatar3.png 我写了一个ESLint插件,解决了团队棘手问题 2277.2 热度

4 河畔一角 avatar4.png 这一年我优化了一个46万行的超级系统 1785.1 热度

5 赵侠客 avatar5.png 使用Hutool要注意了!升级到6.0后你调用的所有方法都将报错 1062 热度

6 大麦大麦 avatar6.png 现在前端组长都是这样做 Code Review 1057.2 热度

7 码财同行 avatar7.png 别再用雪花算法生成ID了!试试这个吧 917.4999999999999 热度

8 pany avatar8.png 📚 Vue3 多组件的 N 种编写方式 710.2 热度

9 张风捷特烈 avatar10.png Flutter 组件集录 | 后悔药 UndoHistory 698.3 热度

10 今天一定晴q avatar9.png vue的组件化开发?一个小demo就带你掌握! 547.9 热度

总结

Redis在排行榜和评分系统中的应用非常广泛,通过有序集合(ZSet)可以方便地实现各种排序和查询需求。本文详细介绍了如何使用封装好的Redis工具类来实现这些功能,并展示了其在实际应用中的重要性和实用性。希望本文能为开发者提供有价值的参考和帮助。