Redis 使用 Lua 脚本进行原子操作
Intro
之前写过一篇文章也是 Redis 使用 LUA 脚本实现分布式的 CAS 操作,可以参考:基于 Redis 实现 CAS 操作
最近使用 Redis 的时候有一个需求,只有值发生变化的时候才更新,如果要更新的值和现在的值是一样的就不用更新,有点类似于 SET NX
,只是 SET NX
只有值不存在的时候才会 SET
,我的需求则是要检查要 SET
的值和 Redis 里的值,如果不一样就 SET
,一样就直接返回
Implement
我实现了针对 String
和 Hash
的 SET 检查,核心就是我们的 Lua 脚本
实现代码如下:
对于 Hash 会多一个参数 —— hash field name, 对于 string 则直接是 value 了,就会比 hash 少一个参数
private const string HashSetWhenValueChangedLuaScript = @"
if redis.call(""HGET"", KEYS[1], ARGV[1]) == ARGV[2] then
return 0
else
redis.call(""HSET"", KEYS[1], ARGV[1], ARGV[2])
return 1
end
";
private const string StringSetWhenValueChangedLuaScript = @"
if redis.call(""GET"", KEYS[1]) == ARGV[1] then
return 0
else
redis.call(""SET"", KEYS[1], ARGV[1])
return 1
end
";
实现起来也比较简单,就是先取一下 Redis 中的数据,如果和输入的值是一样就返回 0,不一样则更新值,然后返回 1
StackExchange.Redis
使用 API
在 StackExchange.Redis
中可以使用 ScriptEvaluate
/ScriptEvaluateAsync
来执行 Lua 脚本,为了方便使用我把他们封装成了扩展方法,实现如下:
public static bool StringSetWhenValueChanged(this IDatabase db, RedisKey key, RedisValue value)
{
return (int)db.ScriptEvaluate(StringSetWhenValueChangedLuaScript, new[] { key }, new[] { value }) == 1;
}
public static async Task<bool> StringSetWhenValueChangedAsync(this IDatabase db, RedisKey key, RedisValue value)
{
return await db.ScriptEvaluateAsync(StringSetWhenValueChangedLuaScript, new[] { key }, new[] { value })
.ContinueWith(r => (int)r.Result == 1);
}
public static bool HashSetWhenValueChanged(this IDatabase db, RedisKey key, RedisValue field, RedisValue value)
{
return (int)db.ScriptEvaluate(HashSetWhenValueChangedLuaScript, new[] { key }, new[] { field, value }) == 1;
}
public static async Task<bool> HashSetWhenValueChangedAsync(this IDatabase db, RedisKey key, RedisValue field, RedisValue value)
{
return await db.ScriptEvaluateAsync(HashSetWhenValueChangedLuaScript, new[] { key }, new[] { field, value }).ContinueWith(r => (int)r.Result == 1);
}
Sample
使用示例可以参考下面的测试用例:
[Fact]
public void StringSetWhenValueChangedTest()
{
var key = $"{nameof(StringSetWhenValueChangedTest)}";
var redis = DependencyResolver.Current
.GetRequiredService<IConnectionMultiplexer>()
.GetDatabase();
redis.StringSet(key, 1);
// update to 1 if now is not 1
Assert.False(redis.StringSetWhenValueChanged(key, 1));
Assert.Equal(1, redis.StringGet(key));
// update to 2 if now is not 2
Assert.True(redis.StringSetWhenValueChanged(key, 2));
Assert.Equal(2, redis.StringGet(key));
}
[Fact]
public void HashSetWhenValueChangedTest()
{
var key = $"{nameof(HashSetWhenValueChangedTest)}";
var field = "testField";
var redis = DependencyResolver.Current
.GetRequiredService<IConnectionMultiplexer>()
.GetDatabase();
redis.HashSet(key, field, 1);
Assert.False(redis.HashSetWhenValueChanged(key, field, 1));
Assert.Equal(1, redis.HashGet(key, field));
Assert.True(redis.HashSetWhenValueChanged(key, field, 2));
Assert.Equal(2, redis.HashGet(key, field));
}
More
在使用 Lua 脚本的时候,如果要使用不等于的逻辑需要小心一些,和其他语言不同,需要使用 ~=
而非 !=
来表示不等
References
https://github.com/WeihanLi/WeihanLi.Redis/blob/dev/src/WeihanLi.Redis/RedisExtensions.cs
https://github.com/WeihanLi/WeihanLi.Redis/blob/dev/test/WeihanLi.Redis.UnitTest/RedisExtensionsTest.cs