Build a real-time leaderboard system with Redis

Tram Ho

Hi guys, it’s fun again. After a while of messing around, I recently started working on the server part. As I see it, for game systems, the rankings are an indispensable part. So today I will share how to create a real-time leaderboard based on my experience. ## 1. Problem Building real-time rankings faces many challenges such as: – Large scale response (up to millions of users) – Calculation on a large number of attributes ( to see ratings based on different sorting conditions) – Provides real-time leaderboard access with high availability – Etc and clouds… Fortunately with the Sorted Sets structure ( ZSETs) that Redis provides, we can easily build a leaderboard easily. (If you don’t know what Redis is, you can go here to find out: https://redis.io/docs/about/) ## 2. Idea I will use 2 Keys: – 1 ZSET key to save the score (score) of the player, ZSET on redis will automatically sort the data by the score. ![](https://images.viblo.asia/34da63eb-e205-4317-9d52-ae71ec4ba539.png) – 1 HSET key to store player name information. ![](https://images.viblo.asia/7ba19774-1972-4e01-82df-6a08c762a5a9.png) When I need to get the leaderboard, I will use ZREVRANGE to get the list of players by sorted scores from ZSET, then map with player names from HSET to return the results. ## 3. Build a simple leaderboard system with Nodejs + Redis In this part, I will create a simple leaderboard template with Nodejs. ### 3.1 Connecting redis To connect Nodejs server, I will use ioredis package (Syntax to download: `npm i ioredis`) “` import Redis from ‘ioredis’ export const redis = new Redis({ host: process .env.REDIS_HOST, port: parseInt(process.env.REDIS_PORT), db: 0 }) // Connected redis.on(‘connect’, function () { console.log(‘connected redis success!!! ‘) }) // Connection failed redis.on(‘error’, function (err) { console.log(‘Connected redis Error ‘ + err) }) “` ### 3.2 Create class **LeaderboardManager* * for leaderboard management “` import { redis } from ‘./redis’ export class LeaderboardManager { private readonly PREFIX = ‘demo:’ private readonly REDIS_KEY = this.PREFIX + ‘leaderboard’ private readonly REDIS_DATA_KEY = this.PREFIX + ‘leaderboard_data’ // Get leaderboard info async getLeaderboard(limit: number) { try { const result = [] // Get sorted member list by score const userRankingSet = await redis.zrevrange(this. REDIS_KEY, 0, limit, ‘WITHSCORES’) // Filter to get list of user scores const topUserScore = userRankingSet.filter((value, index) => { if (index % 2 === 1) return value return false }) // Filter to get list of user id’s const topUserId = userRankingSet .filter((value, index) => { if (index % 2 === 0) return value return false }) // Get list of player names by list id above const listUsername = await redis.hmget(this.REDIS_DATA_KEY , …topUserId) // Map data together for (let i = 0; i < topUserScore.length; i++) { result.push({ id: topUserId[i], ranking: i + 1, username: listUsername[i], point: topUserScore[i] }) } return result } catch (error) { return [] } } // Get the player’s rating by id async getUserRanking(userId: number) { // Get the order of members on zset. Since the order starts at 0, user rank = order + 1 const rankingInRedis = await redis.zrevrank(this.REDIS_KEY, `${userId}`) return rankingInRedis + 1 } // Update any player’s score by id async updateUserPoint(userId: number, point: number, username?: string) { // Update point await redis.zadd(this.REDIS_KEY, point, userId) if (!username) return // If pass username in then update username await redis.hsetnx(this.REDIS_DATA_KEY, `${userId}`, username) } } “` ### 3.3 Create APIs to test “` @Controller(‘api/leaderboard’) export class LeaderboardController { private leaderboardManager = new LeaderboardManager() @Post(‘get’) public async get(req: Request, res: Response, next: NextFunction) { const task = async() => { const data = await this.leaderboardManager .getLeaderboard(10) return new BaseResponse({ data }) } await ErrorHandler.APIReqHandler(task, { req, res, next }) } @Post(‘update’) public async update(req: Request, res: Response, next : NextFunction) { c onst task = async() => { const { id, point, name } = req.body await this.leaderboardManager.updateUserPoint(id, point, name) return new BaseResponse({ message: ‘OK’ }) } await ErrorHandler. APIReqHandler(task, { req, res, next }) } @Post(‘user-ranking’) public async userRanking(req: Request, res: Response, next: NextFunction) { const task = async() => { const { id } = req.body const data = await this.leaderboardManager.getUserRanking(id) return new BaseResponse({ data }) } await ErrorHandler.APIReqHandler(task, { req, res, next }) } } } “` ### 3.4 Results So with just a few lines of code, we have built a real-time leaderboard. ![](https://images.viblo.asia/6d84b9f9-cf8e-471f-83e2-cb1d325e6687.png) ![image.png](https://images.viblo.asia/d5043fd7-c0ee-4633-ba71 -724b4e4db637.png) As you can see, it only takes a few milliseconds for the API to respond, too fast and dangerous isn’t it. ## 4. References: 1. https://redis.com/solutions/use-cases/leaderboards/ 2. https://redis.io/commands/ 3. https://www.npmjs.com /package/ioredis 4. https://gitlab.com/ThanhPham99/sample-redis-leaderboard

Share the news now

Source : Viblo