暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

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

amazingdotnet 2021-08-28
1263

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<boolStringSetWhenValueChangedAsync(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<boolHashSetWhenValueChangedAsync(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 操作


文章转载自amazingdotnet,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论