I’m working on a system that consumes a bunch of readings from a sensor and I thought how nice it would be if I could get a rolling average into Redis. As I’m going to be consuming quite a few of these pieces of data I’d rather not fetch the current value from redis, add to it and send it back. I was thinking about just storing the aggregate value and a counter in a hash set in Redis and then dividing one by the other when I needed the true value. You can set multiple hash values at the same time with HMSET and you can increment a value using a float inside a hash using HINCRBYFLOAT but there is no way to combine the two. I was complaining that there is no HMINCRBYFLOAT command in Redis on twitter when Itamar Haber suggested writing my own in Lua.
I did not know it but apparently you can write your own functions that plug into Redis and can become commands. Nifty! I managed to dig up a quick Lua tutorial that listed the syntax and I got to work. Instead of a HMINCRBYFLOAT function I thought I could just shift the entire rolling average into Redis.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local currentval = redis.call('get', ARGV[1]) | |
local currentcount = redis.call('get', ARGV[1] .. '.count') | |
redis.call('incr', ARGV[1] .. '.count') | |
currentval = (currentval * (currentcount/(currentcount + 1))) + (ARGV[2]/(currentcount+1)) | |
redis.call('set', ARGV[1], currentval) | |
return currentval |
This script gets the current value of the field as well as a counter of the number of records that have been entered into this field. By convention I’m calling this counter key.count. I increment the counter and use it to weight the old and new values.
I’m using the excellent StackExchange.Redis client so to test out my function I created a test that looked like
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Fact] | |
public void EvalTest() | |
{ | |
var redisConnection = GetRedisConnection(); | |
redisConnection.StringSet("foo", 77); | |
redisConnection.StringSet("foo.count", 1); | |
var result = redisConnection.ScriptEvaluate(@"local currentval = redis.call('get', ARGV[1]) | |
local currentcount = redis.call('get', ARGV[1] .. '.count') | |
redis.call('incr', ARGV[1] .. '.count') | |
currentval = (currentval * (currentcount/(currentcount + 1))) + (ARGV[2]/(currentcount+1)) | |
redis.call('set', ARGV[1], currentval) | |
return currentval", null, new RedisValue[] { "foo", 11 }, CommandFlags.None); | |
((int)result).Should().Be.EqualTo(44); | |
} |
The test passed perfectly even against the Redis hosted on Azure. This script saves me a database trip for every value I take in from the sensors. On an average day this could reduce the number of requests to Redis by a couple of million.
The script is a bit large to transmit to the server each time. However if you take the SHA1 digest of the script and pass that in instead then Redis will use the cached version of the script that matches the given SHA1. You can calculate the SHA1 like so
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public byte[] GetScriptHash(string script) | |
{ | |
SHA1 sha = new SHA1CryptoServiceProvider(); | |
return sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes(script)); | |
} |
The full test looks like
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[Fact] | |
public void EvalTest() | |
{ | |
var redisConnection = GetRedisConnection(); | |
redisConnection.StringSet("foo", 77); | |
redisConnection.StringSet("foo.count", 1); | |
string script = @"local currentval = redis.call('get', ARGV[1]) | |
local currentcount = redis.call('get', ARGV[1] .. '.count') | |
redis.call('incr', ARGV[1] .. '.count') | |
currentval = (currentval * (currentcount/(currentcount + 1))) + (ARGV[2]/(currentcount+1)) | |
redis.call('set', ARGV[1], currentval) | |
return currentval"; | |
var result = redisConnection.ScriptEvaluate(script, null, new RedisValue[] { "foo", 11 }, CommandFlags.None); | |
((int)result).Should().Be.EqualTo(44); | |
result = redisConnection.ScriptEvaluate(GetScriptHash(script), null, new RedisValue[] { "foo", 11 }, CommandFlags.None); | |
((int)result).Should().Be.EqualTo(33); | |
} |
Great post! I maintain a library of useful Lua scripts for use with Redis. Would you mind if I added your script? This sort of example is very helpful.
Go for it. It occurs to me, however, that this script isn’t thread safe. I’ll have to revisit it in a future post.
The lua script needn’t be threadsafe, as it is executed as a single (atomic) Redis operation. Redis is itself single-threaded.
Oh no way, awesome. That sure saves me figuring out locking. Thanks for the tip.
I made a couple of small changes to properly use the KEYS array so this script will be Redis Cluster compatible – check it out: https://github.com/stvp/lualibrary/blob/master/runningavg.html
Also online here: http://www.redisgreen.net/library/runningavg.html