Implement game leaderboard using Redis

Photo by Jorge Franganillo on Unsplash

In this post, we will talk about the Sorted Set and some of its operations, then work with sorted sets in the redis-cli client and then switch to golang and implement a simple scoring using Redis. In the end we will compare the performance of running against Redis in RDB and AOF storage modes.

Sorted Set is like a Set data structure in which it contains non-repeating elements. however, it is different in that, each member is associated with a score.

Sorted set commands are stared with the Z character:

Redis Sorted Set Commands (Click to enlarge)

For the sake of this example, we need to focus on the following commands:

ZADD: Adds all the specified members with the specified scores to the sorted set stored at key.

ZCARD: Get number of members in a sorted set (cardinality)

ZINCRBY: Increments the score of a member in the sorted set stored by value.

ZSCORE: Return the score associated with a given member in a sorted set.

ZRANGE: Returns the specified range of elements in the sorted set stored and take WITHSCORES to return scores as well.

ZREVRANGE: similar to ZRANGE but return elements sorted by score.

You can read more about other sorted set commands in the documentation.

Let’s try sorted set operations in redis-cli. First, make sure you have installed Redis as specified in the first post in this series.

Now let’s connect to the VM using vermin, you might need to start the VM if it is not started:

$ vermin ps -a
VM NAME IMAGE CPUS MEM DISK TAGS
vm_01 ubuntu/focal 1 1024 2.8GB redis

Then start the VM if it is not running:

$ vermin start vm_01

Now, let’s ssh into the VM and start redis-cli command:

$ vermin ssh vm_01
✔ Establishing connection
Welcome to Ubuntu 20.04 LTS (GNU/Linux 5.4.0-26-generic x86_64)Last ...
...
vermin@verminbox:~$ redis-cli
127.0.0.1:6379>

Let’s work with the sorted set commands as follows:

127.0.0.1:6379> zadd myss 10 Java 20 Redis 30 GO
(integer) 3

This command added 3 elements to the sorted set myss, “Java” with score 10, then “Redis” with score 20 and finally “GO” with score 30.

Now let’s increment the score of Redis by 1:

127.0.0.1:6379> ZINCRBY myss 1 Redis
"21"

Now let’s show the sorted set elements along with its score:

127.0.0.1:6379> zrange myss 0 -1 WITHSCORES
1) "Java"
2) "10"
3) "Redis"
4) "21"
5) "GO"
6) "30"

Note, the ZRANGEcommand takes the first parameter the key, then the start index, and then the stop index (0 and -1 where -1 means the latest index), and finally WITHSCORESreturns the elements along with their scores.

We could use ZREVRANGEto get the elements sorted by scores.

And we can verify that we have 3 elements using ZCARD myss command.

Next, we will implement simple game scoring using Golang and go-redis library.

I run the application on my macOS desktop however I am running Redis inside VM, so I’ve to do port forwarding so I can access the port from my local macOS desktop. In a new Terminal write:

$ vermin port vm_01 6379Connected. Press CTRL+C anytime to stop

I am using Intellij/Golang but any IDE can be used.

Here’s the source code:

package main

import (
"fmt"
"github.com/go-redis/redis/v7"
"log"
"math/rand"
"time"
)

func init() {
rand.Seed(time.Now().Unix())
}

const key = "players"

func
main() {
c := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
})
c.Del(key) // remove the key from redis for clean start

for i := 0; i < 10000; i++ {
player := getWinnerPlayer()

err := c.ZIncr(key, &redis.Z{
Score: 1,
Member: player,
}).Err()

if err != nil {
log.Fatal(err)
}
}

result, _ := c.ZRevRangeWithScores(key, 0, -1).Result()
fmt.Println(result)

}

func getWinnerPlayer() string {
players := []string{"Mohammad", "Ali", "John", "Abdullah", "Farida"}
return players[rand.Intn(len(players))]
}

First, create a client for Redis. Then we call getWinnerPlayer() function that simulates a winner player among the 5 players we have.

Then we call ZIncr to increment the score of the selected player by 1. And at the end, we call ZRevRangeWithScores to get a map of the score and associated element sorted by score. We do this 10000 times.

I got the following result. however, you might get a different result:

$ go build; time ./redis-go
[{2033 John} {2030 Ali} {2002 Abdullah} {1981 Mohammad} {1954 Farida}]
./redis-go 0.32s user 0.41s system 15% cpu 4.570 total

This part we will compare the performance of RDB vs AOF, so it is good to go and read the second part of this series here before continuing to make some context.

By default Redis is running in RDB mode(Snapshotting), so the result of running 10000 queries was around 4.5 seconds as shown above.

Now Let’s change the configuration to use AOF and change fsycnpolicy to always (so every time a new command is appended to the log)

127.0.0.1:6379> config set appendonly yes
OK
127.0.0.1:6379> config set appendfsync always
OK
127.0.0.1:6379> config rewrite
OK

Now let’s run the previous program and compare the results:

go build; time ./redis-go
[{2089 Mohammad} {2001 John} {1986 Farida} {1968 Ali} {1956 Abdullah}]
./redis-go 0.33s user 0.41s system 6% cpu 11.174 total

Ohh, it is around 11 seconds, which is about 3 times slower than running with RDB only enabled.

If we enable AOF with the default fsyncwhich is every second, we got a performance comparable with when we using RDB only(snapshotting).

Software Developer/Architect