Redis 使用 Lua 脚本进行原子操作

Posted dotNET跨平台

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis 使用 Lua 脚本进行原子操作相关的知识,希望对你有一定的参考价值。

Redis 使用 Lua 脚本进行原子操作

Intro

之前写过一篇文章也是 Redis 使用 LUA 脚本实现分布式的 CAS 操作,可以参考:基于 Redis 实现 CAS 操作

最近使用 Redis 的时候有一个需求,只有值发生变化的时候才更新,如果要更新的值和现在的值是一样的就不用更新,有点类似于 SET NX,只是 SET NX 只有值不存在的时候才会 SET,我的需求则是要检查要 SET 的值和 Redis 里的值,如果不一样就 SET,一样就直接返回

Implement

我实现了针对 StringHash 的 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

  • 基于 Redis 实现 CAS 操作

以上是关于Redis 使用 Lua 脚本进行原子操作的主要内容,如果未能解决你的问题,请参考以下文章

Redis_07_Lua脚本实现多条Redis命令原子性

Redis_05_Lua脚本实现多条Redis命令原子性

一网打尽Redis Lua脚本并发原子组合操作

Redis+Lua实现限流

Redis中使用Lua

使用Lua脚本通过原子减防止超卖