Redisboard.NET is a optimized .NET library designed to handle leaderboards efficiently using Redis. It provides a simple, yet powerful API to create and manage leaderboards with various ranking systems. The library leverages Redis sorted sets for performance and uses LUA scripts for advanced querying capabilities.
TODO:
✅ Add integration tests
✅ Add benchmarks
❌ Distribute to NuGet
First up, let's get a Redis server ready. Docker makes this super easy.
After you've got Docker set up on your machine, just pop open your terminal and run:
docker run --name my-redis-server -d redis
With your Redis server humming along, it's time to install the Redisboard.NET package with this quick command:
dotnet add package Redisboard.NET
In your project define your leaderboard entity. In should implement the ILeaderboardEntity
// Define a player class implementing ILeaderboardEntity
public class Player : ILeaderboardEntity
{
public string Id { get; set; }
public long Rank {get; set;}
public double Score { get; set; }
}
Start managing your leaderboard with the help of the Leaderboard<Player>
class
const string leaderboardKey = "your_leaderboard_key"
//initialize a new Leaderboard class by passing down IDatabase or IConnectionMultiplexer
var leaderboard = new Leaderboard<Player>(redis)
var players = new[]
{
new Player { Id = "player1", Score = 100 },
new Player { Id = "player2", Score = 150 }
};
// Add players to the leaderboard
await leaderboard.AddEntitiesAsync(leaderboardKey, players);
// Retrieve player and neighbors (offset is default 10)
var result = await leaderboard.GetEntityAndNeighboursAsync(
leaderboardKey, "player1", RankingType.Default);
If you wish to use DI should install the Redisboard.NET.Extensions package and register the Leaderboard in your IServiceCollection
// Add to IServiceCollection
builder.Services.AddLeaderboard<Player>(cfg =>
{
cfg.EndPoints.Add("localhost:6379");
cfg.ClientName = "Development";
cfg.DefaultDatabase = 0;
});
* Config delegate is not required, if you have already registered your IConnectionMultiplexer
or IDatabase
(ref. StackExchange.Redis)
Once registered, your need to inject the ILeaderboard<Player>
interface via the constructor
public class MyService
{
private readonly ILeaderboard<Player> _leaderboard;
public MyService(ILeaderboard<Player> leaderboard)
{
_leaderboard = leaderboard;
}
public async Task AddPlayersAsync(Player[] players)
{
const string leaderboardKey = "your_leaderboard_key"
await _leaderboard.AddEntitiesAsync(leaderboardKey, players);
}
}
This repository also includes a very simple API project, which shows how you can setup the Leaderboard. You can find the project here
The rankings of the players are calculated on the fly when querying the data. Under the hood we are using multiple Redis data structures (Sorted Set and Hash) and LUA scripts for queryin. This provides us with a response time of under 1ms for over 500k players in a leaderboard.
When querying data, you need to specify the RankingType
, which can be one of the following:
Members are ordered by score first, and if there are ties in scores, they are then ordered lexicographically. There is no skipping in the records ranking.
(This is Redis Sorted Set default ranking style.)
Example:
Scores: [{John, 100}, {Micah, 100}, {Alex, 99}, {Tim, 1}]
Ranks: [1, 2, 3, 4]
2. Dense Rank 🥇🥈
Items that compare equally receive the same ranking number, and the next items receive the immediately following ranking number.
Example:
Scores: [100, 80, 50, 50, 40, 10]
Ranks: [1, 2, 3, 3, 4, 5]
3. Standard Competition 🏅
Items that compare equally receive the same ranking number, and then a gap is left in the ranking numbers.
Example:
Scores: [100, 80, 50, 50, 40, 10]
Ranks: [1, 2, 3, 3, 5, 6]
4. Modified Competition 🎖️
Leaves the gaps in the ranking numbers before the sets of equal-ranking items.
Example:
Scores: [100, 80, 50, 50, 40, 10]
Ranks: [1, 2, 4, 4, 5, 6]
These benchmarks have been run (on M3 Macbook Air) over a leaderboard with 500,000 entries of type:
public class Player : ILeaderboardEntity
{
public string Key { get; set; }
public long Rank { get; set; }
public double Score { get; set; }
public string Username { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateTime EntryDate { get; set; }
}
We are benchmarking the most common method - getting a entity and their neighbors with an relevant offset*
*The offset says how many neighbours above and below the targeted entity we should take
Method | Offset | Mean | Error | StdDev | Median | Min | Max | Ratio | Allocated |
---|---|---|---|---|---|---|---|---|---|
GetEntityAndNeighbours_DefaultRanking_500K | 10 | 0.329 ms | 0.010 ms | 0.032 ms | 0.328 ms | 0.257 ms | 0.414 ms | 1.00 | 31.49 KB |
GetEntityAndNeighbours_DenseRanking_500K | 10 | 0.341 ms | 0.009 ms | 0.027 ms | 0.332 ms | 0.280 ms | 0.409 ms | 1.05 | 43 KB |
GetEntityAndNeighbours_Competition_500K | 10 | 0.371 ms | 0.007 ms | 0.017 ms | 0.368 ms | 0.318 ms | 0.405 ms | 1.13 | 46.97 KB |
GetEntityAndNeighbours_DefaultRanking_500K | 20 | 0.410 ms | 0.017 ms | 0.050 ms | 0.411 ms | 0.305 ms | 0.511 ms | 1.00 | 59.84 KB |
GetEntityAndNeighbours_DenseRanking_500K | 20 | 0.509 ms | 0.010 ms | 0.018 ms | 0.515 ms | 0.456 ms | 0.540 ms | 1.40 | 75.99 KB |
GetEntityAndNeighbours_Competition_500K | 20 | 0.544 ms | 0.008 ms | 0.007 ms | 0.543 ms | 0.531 ms | 0.558 ms | 1.50 | 80.09 KB |
GetEntityAndNeighbours_DefaultRanking_500K | 50 | 0.503 ms | 0.006 ms | 0.005 ms | 0.505 ms | 0.494 ms | 0.514 ms | 1.00 | 142.16 KB |
GetEntityAndNeighbours_DenseRanking_500K | 50 | 0.620 ms | 0.010 ms | 0.013 ms | 0.618 ms | 0.593 ms | 0.645 ms | 1.24 | 171.78 KB |
GetEntityAndNeighbours_Competition_500K | 50 | 0.701 ms | 0.010 ms | 0.009 ms | 0.703 ms | 0.676 ms | 0.713 ms | 1.40 | 176.03 KB |