Introduction
This is a continuation of the Scaling Horizontally blog posts that I started earlier (see part 1 and part 2). In those articles I used an MMORPG game as an example program/system to scale horizontally. If you read part 2, you’ll remember my discussion on using Redis to store the active map soldiers. In this post, I’m going to drill down and show how to do it.
Installing Redis for Windows
I have Redis installed on my desktop (and my work laptop) because I work with Redis on many projects. So I’m going to start with how to install the windows Redis and a couple of basic commands. Just in case you’re not as much of a Redis Guru as I am.
- First, go to this GitHub URL and download the zip file of Redis for Windows: Redis Sentinel. This version includes Sentinel, which is a clustering setup. You can use Redis without the cluster.
- Unzip the files into a Redis directory on your hard drive.
- Open a command window (in Admin mode) and navigate to where you unzipped your Redis files (go to the bin/windows directory).
- Then use the following command to run Redis as a service (there are two minus signs in front of the service install instruction): redis-server –service-install
- Next, you’ll need to open the Windows Services and set it to run:
To test your installation, you can navigate to the bin/windows directory and double-click on the file named “redis-cli.exe”. That should start the redis console:Now type in a command like “keys *” you should see this:
Your Redis server is working.
You need two basic commands to get started. First, to clear all keys in all databases of Redis you can use the flush command:
flushall
To see all keys you can use the previous keys command:
keys *
The keys command can work with wild cards. I usually only use the star in combination with a substring. If you have a bunch of keys that are named the way I named them in the previous article (SX50Y50_00001, etc.), then you can do something like this:
keys SX50Y50_*
That will retrieve all keys that start with SX50Y50_. You’ll have to put some data into your Redis server first, but that should be enough Redis knowledge to get you warmed up.
The Redis Connection Manager
The first thing I created was a Redis connection manager. I use the StackExchange.Redis NuGet package to connect to a Redis server from C#. You can create a throw-away project using the code that the StackExchange Redis site shows (see StackExchange Redis Usage Wiki). I’ll warn you that you cannot use the connection manager code as is in a production system. You’ll run out of connection handles. For a production system, you’ll need a connection pool. So here’s the code that I use in such a situation:
public class RedisConnectionManager : IRedisConnectionManager { private static string _redisServerAddress; public RedisConnectionManager(string redisServerAddress) { _redisServerAddress = redisServerAddress; } private static readonly Lazy ConfigOptions = new Lazy(() => { var configOptions = new ConfigurationOptions(); configOptions.EndPoints.Add(_redisServerAddress); configOptions.ClientName = "RedisConnection"; configOptions.ConnectTimeout = 100000; configOptions.SyncTimeout = 100000; configOptions.AbortOnConnectFail = false; return configOptions; }); private readonly Lazy _connectionMux = new Lazy( () => ConnectionMultiplexer.Connect(ConfigOptions.Value)); public IDatabase RedisDatabase => _connectionMux.Value.GetDatabase(0); }
The interface for this class looks like this:
public interface IRedisConnectionManager { IDatabase RedisDatabase { get; } }
Next I created a class to represent the data that will store the soldier information. I only encoded the X,Y coordinates and a property to convert the X,Y coordinates into the first part of a Redis key that will be used to store the data:
public class Soldier { private readonly int _blockSize = 100; public int X { get; set; } public int Y { get; set; } public string BlockAddress { get { var x = (X / _blockSize + 1) * _blockSize; var y = (Y / _blockSize + 1) * _blockSize; return $"SX{x}SY{y}"; } } }
I created some unit tests to make sure the BlockAddress getter was computing the correct block addresses. In my original discussion I used a 50 by 50 block, but after some testing I decided that a 100 by 100 block made more sense. There is a block size parameter to change this, but be aware that it will break the unit tests because they are written to test edge cases for 100 by 100 blocks.
The first “problem” I ran into was that I was off-by-one on my block numbers. As I mentioned in my last article the X50 by Y50 represented coordinates between 0 to 50. That’s not correct. It needs to represent 0 to 49. Otherwise, I could adjust it to 1 to 50, but in the programming world, that would get ugly fast. So I adjusted my algorithm to assume a zero starting number and used 0 through 49. Later I upped it to a range that spans 0 to 99.
My next task was to seed the Redis database with a lot of data. I started with only 500 records and tested my code. Then I tried it with 5000, and finally 50,000. The method I designed to perform this task looks like this:
private static void GenerateData() { for (var i = 0; i < _totalSoldiers; i++) { var value = new Soldier { X = Rand.Next(0, 1000), Y = Rand.Next(0, 1000) }; var uniqueNumber = i.ToString().PadLeft(5, '0'); var key = $"{value.BlockAddress}_{uniqueNumber}"; RedisConnection.RedisDatabase.StringSet(key, JsonConvert.SerializeObject(value)); } }
The map range is from 0 to 999 in both the X and Y direction. Soldier X,Y coordinates are randomly generated and then the data is serialized into a JSON packet and sent to Redis. To read the data back, I had to use a Lua script that I send to Redis to gather all the data in my range and send it back as an array. The Lua script itself, looks like this:
local retArray = {} local keys = (redis.call('keys', ARGV[1])) for i,key in ipairs(keys) do retArray[i] = redis.call('GET', key) i=i+1 end return retArray
The script is sent up to Redis and an argument is assigned before calling the script evaluate redis command. Here’s the basic C# code:
var redisValueArgs = new RedisValue[] { "SX100Y100_*" }; var rowDataFromRedis = RedisConnection.RedisDatabase.ScriptEvaluate(GetListOfSoldiers, null, redisValueArgs); var rowDataAsRedisResult = (RedisResult)rowDataFromRedis; var scriptReturnValueArray = (RedisResult[])rowDataAsRedisResult;
As you can see, I put in a key search of SX100Y100_*. That should get me all the soldiers with X,Y coordinates in the 0-99 by 0-99 block. The Lua script will find all the keys that match the string in ARGV[1] (lua is “1” based, not zero based). Then there is a for loop that works more like a foreach loop. It iterates through the list of keys. This allows the script to “GET” the value for each key. Those values are added to the array. Finally, the array of values is returned all at once.
Next, the C# code will decode the RedisResult set and then cast that as a RedisResult array set. The array consists of JSON data that can be deserialized into Soldier objects.
Results
I introduced a StopWatch to record the execution time on my machine. It’s only a feasibility test. In order to perform a real test, I would need to setup a server (probably virtual) on a host site that I expect to use as my typical site. For this test, the query result was pretty quick:The time is in milliseconds. It took 381 milliseconds to grab 506 soldier records. Don’t forget that this is only the list of soldiers in a block between 0 to 49 by 0 to 49. If you have a larger viewport, you’ll need multiple blocks to fill your viewport. For instance, if you have a viewport of 300 by 200, you’ll need a minimum of 3 by 2 blocks (or a total of 6 blocks). As your viewport shifts by an odd block coordinate, you’ll need an additional layer along the X coordinate and/or the Y coordinate. That means that you could be required to query up to 12 blocks:Now you’re probably thinking that the speed is not so good. There is one thing to remember: One block contained 506 soldiers. So that viewport would contain approximately 3,000 soldiers (assuming they are evenly distributed). I don’t think it would be wise to render 3,000 soldiers on a browser. Not only would it slow down the end-user’s machine, but it would be difficult to see what was going on.
Many games use a technique of grouping soldiers into armies. Each army can contain combinations of different soldier types. The army itself might just have some attributes and quantities of each soldier. This data can be serialized and stored the same as I did with the Soldier object earlier. At this point, I’m not going to get hung up on what the Soldier class really represents. This is just a feasability study to see if we can use Redis to store data that will be moving around a map. So far, it’s a success.
What’s Next?
My next test project will be to test the queuing system to perform a write-behind. I want to know how much activity I can sustain and not fill up the queue. I’ll probably use MS SQL server with a custom written queuing program (or I might just use RabbitMQ). After that, I’ll need to design a front-end with some simple graphics that will display the active viewport. This will be used to test performance for one game player.
Where to Get the Code
You can download the sample code from my GitHub account by clicking here.