Implement game leaderboard using Redis
This is the Third Post in The Redis Series.
Part One: Install Redis inside Ubuntu VM
Part Two: Redis Persistence by Example
Part Three: Implement game leaderboard using Redis š
Part Four: Implement Job Queue using Redis
Part Five: Building REST API backed by Redis
Part Six: Building Chat Service in Golang and Websockets backed by Redis
Part Seven: Redis Cluster configurations by example
Part Eight: Redis Geospatial by example
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.
Letās know about the Sorted Set data structure in Redis
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:
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.
Working with redis-cli
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 ZRANGE
command 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 WITHSCORES
returns the elements along with their scores.
We could use ZREVRANGE
to get the elements sorted by scores.
And we can verify that we have 3 elements using ZCARD myss
command.
Implementing the game scoring in go
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
Comparing Performance:
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 fsycn
policy 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
fsync
which is every second, we got a performance comparable with when we using RDB only(snapshotting).
Resources: