CAS operation based on Redis

Intro

We can use Interlocked.CompareExchange to implement CAS (Compare And Swap) operation in the concurrent case of. NET. In the distributed case, we will use redis many times Recently, a wechat game project was made before the change. Before the change, it was run by a single computer. Some data stores were based on memory and directly based on object operation. Recently, it was changed to support distributed, so redis was introduced. The original memory based data would be migrated to redis for storage. In the original code, Interlocked.CompareExchange was used in some places To implement CAS operations, similar functions are required after migration to redis, so we want to implement CAS operations based on redis.

CAS

CAS (Compare And Swap) can usually be used to update the value of an object in concurrent operations. CAS is a lock free operation. CAS is an optimistic lock, while direct locking is a pessimistic lock. Therefore, CAS operation is more efficient than direct locking.

Redis Lua

Redis supports Lua script since version 2.6.0. The execution of lua script is atomic. Therefore, when implementing redis based distributed lock release lock or CAS operation to be described below, we need to perform multiple operations, but when we want the operation to be atomic, we can implement it with Lua script (or transaction)

CAS implementation based on Redis Lua

String CAS Lua Script:

KEYS[1] corresponds to the key of redis cache of String type to be operated, and ARGV[1] corresponds to the value to be compared. If the values are the same, they will be updated to ARGV[2], and 1 will be returned, otherwise 0 will be returned

if redis.call(""get"", KEYS[1]) == ARGV[1] then
    redis.call(""set"", KEYS[1], ARGV[2])
    return 1
else
    return 0
end

Hash CAS Lua Script:

KEYS[1] corresponds to the key of redis cache of Hash type to be operated, ARGV[1] corresponds to the field of Hash, ARGV[2] corresponds to the value to be compared, if the values are the same, it will be updated to ARGV[3], and 1 will be returned, otherwise 0 will be returned

if redis.call(""hget"", KEYS[1], ARGV[1]) == ARGV[2] then
    redis.call(""hset"", KEYS[1], ARGV[1], ARGV[3])
    return 1
else
    return 0
end

Implementation based on StackExchange.Redis

In order to facilitate the use, several convenient extension methods are provided based on IDatabase. The implementation is as follows:

public static bool StringCompareAndExchange(this IDatabase db, RedisKey key, RedisValue newValue, RedisValue originValue)
{
    return (int)db.ScriptEvaluate(StringCasLuaScript, new[] { key }, new[] { originValue, newValue }) == 1;
}

public static async Task<bool> StringCompareAndExchangeAsync(this IDatabase db, RedisKey key, RedisValue newValue, RedisValue originValue)
{
    return await db.ScriptEvaluateAsync(StringCasLuaScript, new[] { key }, new[] { originValue, newValue })
        .ContinueWith(r => (int)r.Result == 1);
}

public static bool HashCompareAndExchange(this IDatabase db, RedisKey key, RedisValue field, RedisValue newValue, RedisValue originValue)
{
    return (int)db.ScriptEvaluate(HashCasLuaScript, new[] { key }, new[] { field, originValue, newValue }) == 1;
}

public static async Task<bool> HashCompareAndExchangeAsync(this IDatabase db, RedisKey key, RedisValue field, RedisValue newValue, RedisValue originValue)
{
    return await db.ScriptEvaluateAsync(HashCasLuaScript, new[] { key }, new[] { field, originValue, newValue })
        .ContinueWith(r => (int)r.Result == 1);
}

Actual use

You can refer to the following test code for use:

[Fact]
public void StringCompareAndExchangeTest()
{
    var key = "test:String:cas";
    var redis = DependencyResolver.Current
        .GetRequiredService<IConnectionMultiplexer>()
        .GetDatabase();
    redis.StringSet(key, 1);

    // set to 3 if now is 2
    Assert.False(redis.StringCompareAndExchange(key, 3, 2));
    Assert.Equal(1, redis.StringGet(key));

    // set to 4 if now is 1
    Assert.True(redis.StringCompareAndExchange(key, 4, 1));
    Assert.Equal(4, redis.StringGet(key));

    redis.KeyDelete(key);
}

[Fact]
public void HashCompareAndExchangeTest()
{
    var key = "test:Hash:cas";
    var field = "testField";

    var redis = DependencyResolver.Current
        .GetRequiredService<IConnectionMultiplexer>()
        .GetDatabase();
    redis.HashSet(key, field, 1);

    // set to 3 if now is 2
    Assert.False(redis.HashCompareAndExchange(key, field, 3, 2));
    Assert.Equal(1, redis.HashGet(key, field));

    // set to 4 if now is 1
    Assert.True(redis.HashCompareAndExchange(key, field, 4, 1));
    Assert.Equal(4, redis.HashGet(key, field));

    redis.KeyDelete(key);
}

References

Tags: C# Redis github

Posted on Sat, 07 Mar 2020 13:54:28 -0500 by sqishy