基於 Redis 實現 CAS 操作


基於 Redis 實現 CAS 操作

Intro

在 .NET 里並發情況下我們可以使用 Interlocked.CompareExchange 來實現 CAS (Compare And Swap) 操作,在分布式的情景下很多時候我們都會使用 Redis ,最近在改之前做的一個微信小游戲項目,之前是單機運行的,有些數據存儲是基於內存的,直接基於對象操作的,最近要改成支持分布式的,於是引入了 redis,原本基於內存的數據就要遷移到 redis 中存儲,原來的代碼里有一些地方使用了 Interlocked.CompareExchange 來實現 CAS 操作,遷移到 redis 中之后也需要類似的功能,於是就想基於 redis 實現 CAS 操作。

CAS

CAS (Compare And Swap) 通常可以使用在並發操作中更新某一個對象的值,CAS 是無鎖操作,CAS 相當於是一種樂觀鎖,而直接加鎖相當於是悲觀鎖,所以相對來說 CAS 操作 是會比直接加鎖更加高效的。

Redis Lua

redis 從 2.6.0 版本開始支持 Lua 腳本,Lua 腳本的執行是原子性的,所以我們在實現基於 redis 的分布式鎖釋放鎖的時候或者下面要介紹的實現CAS 操作的,要執行多個操作但是希望操作是原子操作的時候就可以借助 Lua 腳本來實現(也可以使用事務來做)

基於 Redis Lua 實現 CAS

String CAS Lua Script:

KEYS[1] 對應要操作的String 類型的 redis 緩存的 key,ARGV[1]對應要比較的值,值相同則更新成 ARGV[2],並返回 1,否則返回 0

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] 對應要操作的 Hash 類型的 redis 緩存的 key,ARGV[1] 對應 Hash 的 field,ARGV[2]對應要比較的值,值相同則更新成 ARGV[3],並返回 1,否則返回 0

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

基於 StackExchange.Redis 的實現

為了方便使用,基於 IDatabase 提供了幾個方便使用的擴展方法,實現如下:

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);
}

實際使用

使用可以參考下面的測試代碼:

[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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM