1. 程式人生 > >基於 Redis 實現 CAS 操作

基於 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 ``` lua 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 ``` lua 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` 提供了幾個方便使用的擴充套件方法,實現如下: ``` csharp 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 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 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); } ``` ## 實際使用 使用可以參考下面的測試程式碼: ``` csharp [Fact] public void StringCompareAndExchangeTest() { var key = "test:String:cas"; var redis = DependencyResolver.Current .GetRequiredService() .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() .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 - [https://redis.io/commands/eval](https://redis.io/commands/eval) - [https://redisbook.readthedocs.io/en/latest/feature/scripting.html](https://redisbook.readthedocs.io/en/latest/feature/scripting.html) - [https://github.com/WeihanLi/WeihanLi.Redis/blob/dev/src/WeihanLi.Redis/RedisExtensions.cs](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](https://github.com/WeihanLi/WeihanLi.Redis/blob/dev/test/WeihanLi.Redis.UnitTest/RedisExtensionsT