Building Leaderboards with Redis

Using Sorted Sets for Leaderboards

Welcome to the next exciting part of our Redis-based backend system project. In this unit, we will focus on building leaderboard functionality using Redis sorted sets. Building a leaderboard is a popular use case for many applications, such as games and competitive platforms. You've already connected to Redis and stored basic values; now we'll build on that foundation.

What You'll Build

Let's briefly review what we'll focus on in this unit. Our main tasks will be:

  1. Adding user scores to a leaderboard: We will store user scores using Redis sorted sets.
  2. Retrieving the leaderboard: We will fetch and display the top users and their scores.
  3. Getting a user's rank and score: We will retrieve the ranking and score of a specific user.

Section 1: Set Up Redis, the Request, and the Response

We start by creating the Asio event loop, opening a Redis connection, and preparing the request and response objects. The request will hold all Redis commands we want to send, and the response<...> type describes the result type for each command in the same order.

#include <boost/redis.hpp>
#include <boost/redis/src.hpp>
#include <boost/asio/io_context.hpp>
#include <boost/asio/detached.hpp>
#include <boost/asio/consign.hpp>
#include <iostream>
#include <memory>
#include <vector>
#include <string>
#include <cstdint>

namespace net = boost::asio;
using boost::redis::connection;
using boost::redis::request;
using boost::redis::response;
using boost::redis::config;
using boost::redis::logger;

int main() {
    // Create the io_context (event loop) for asynchronous operations
    net::io_context ioc;
    
    // Create a shared connection to Redis
    auto conn = std::make_shared<connection>(ioc);

    // Start the Redis connection asynchronously (connects to localhost:6379 by default)
    conn->async_run(
        config{},
        logger{logger::level::disabled},
        net::consign(net::detached, conn)
    );

    // Create a request object to hold all our commands
    // We'll pipeline multiple commands together for efficiency
    auto req = std::make_shared<request>();
    
    // Define the response type - each element corresponds to one command's result
    using resp_type = response<
        std::string,               // Result from SETEX (usually "OK")
        std::int64_t,              // Result from ZADD user1
        std::int64_t,              // Result from ZADD user2
        std::int64_t,              // Result from ZADD user3
        std::vector<std::string>,  // Result from ZREVRANGE (members and scores)
        std::int64_t,              // Result from ZREVRANK (rank)
        std::string                // Result from ZSCORE (score as string)
    >;
    auto resp = std::make_shared<resp_type>();

In this section, the main idea is that Boost.Redis lets us describe the expected reply types ahead of time. That is why response<...> contains one entry per Redis command.

A few useful points here:

  • Pipelining commands: instead of sending commands one by one, we collect them into a single request. Redis can process them together, which reduces network round-trips.
  • SETEX returns a string, usually "OK", so we use std::string.
  • Each ZADD returns an integer telling us how many new members were added, so we use std::int64_t.
  • ZREVRANGE ... WITHSCORES returns a list of strings, so we use std::vector<std::string>.
  • ZREVRANK returns a numeric rank, so we use std::int64_t.
  • ZSCORE returns the member's score as a string, so we use std::string.
Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal