基於 Redis 實現 CAS 操作
阿新 • • 發佈:2020-03-08
# 基於 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