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
- https://redis.io/commands/eval
来源: https://www.cnblogs.com/weihanli/p/redis-based-cas.html