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