StackExchange.RedisでRedisを使用しています。ある時点で同じキーの値にアクセスして編集する複数のスレッドがあるので、データの操作を同期する必要があります。
利用可能な関数を見ると、TakeLockとReleaseLockの2つの関数があることがわかります。ただし、これらの関数は、期待される単一のキーがロックされるのではなく、キーと値のパラメーターの両方を取ります。 GitHubのインテリセンのドキュメントとソースには、LockTake関数とLockRelease関数の使用方法や、キーと値のパラメーターに何を渡すかについては説明されていません。
Q:StackExchange.RedisでのLockTakeおよびLockReleaseの正しい使い方は何ですか?
私がやろうとしていることの疑似コードの例:
//Add Items Before Parallel Execution
redis.StringSet("myJSONKey", myJSON);
//Parallel Execution
Parallel.For(0, 100, i =>
{
//Some work here
//....
//Lock
redis.LockTake("myJSONKey");
//Manipulate
var myJSONObject = redis.StringGet("myJSONKey");
myJSONObject.Total++;
Console.WriteLine(myJSONObject.Total);
redis.StringSet("myJSONKey", myNewJSON);
//Unlock
redis.LockRelease("myJSONKey");
//More work here
//...
});
ロックには3つの部分があります。
他の値が頭に浮かばない場合、GUIDは適切な「値」を作成する可能性があります。マシン名(または、同じマシン上で複数のプロセスが競合する可能性がある場合は、マシン名を変更したバージョン)を使用する傾向があります。
また、ロックの取得は投機的であり、ブロッキングではないことに注意してください。 failでロックを取得することは完全に可能です。そのため、これをテストし、おそらく再試行ロジックを追加する必要があります。
典型的な例は次のとおりです。
RedisValue token = Environment.MachineName;
if(db.LockTake(key, token, duration)) {
try {
// you have the lock do work
} finally {
db.LockRelease(key, token);
}
}
作業が長い(特にループ)場合は、途中でLockExtend
呼び出しを途中に追加することをお勧めします。ここでも、成功したかどうかを確認することを忘れないでください(タイムアウトした場合)。
all個々のredisコマンドはアトミックであるため、2つの慎重な操作が競合することを心配する必要がないことにも注意してください。より複雑なマルチオペレーションユニットの場合、transactionsおよびscriptingがオプションです。
コメント付きのロック->取得->変更(必要な場合)->ロック解除アクションのコードの一部があります。
public static T GetCachedAndModifyWithLock<T>(string key, Func<T> retrieveDataFunc, TimeSpan timeExpiration, Func<T, bool> modifyEntityFunc,
TimeSpan? lockTimeout = null, bool isSlidingExpiration=false) where T : class
{
int lockCounter = 0;//for logging in case when too many locks per key
Exception logException = null;
var cache = Connection.GetDatabase();
var lockToken = Guid.NewGuid().ToString(); //unique token for current part of code
var lockName = key + "_lock"; //unique lock name. key-relative.
T tResult = null;
while ( lockCounter < 20)
{
//check for access to cache object, trying to lock it
if (!cache.LockTake(lockName, lockToken, lockTimeout ?? TimeSpan.FromSeconds(10)))
{
lockCounter++;
Thread.Sleep(100); //sleep for 100 milliseconds for next lock try. you can play with that
continue;
}
try
{
RedisValue result = RedisValue.Null;
if (isSlidingExpiration)
{
//in case of sliding expiration - get object with expiry time
var exp = cache.StringGetWithExpiry(key);
//check ttl.
if (exp.Expiry.HasValue && exp.Expiry.Value.TotalSeconds >= 0)
{
//get only if not expired
result = exp.Value;
}
}
else //in absolute expiration case simply get
{
result = cache.StringGet(key);
}
//"REDIS_NULL" is for cases when our retrieveDataFunc function returning null (we cannot store null in redis, but can store pre-defined string :) )
if (result.HasValue && result == "REDIS_NULL") return null;
//in case when cache is epmty
if (!result.HasValue)
{
//retrieving data from caller function (from db from example)
tResult = retrieveDataFunc();
if (tResult != null)
{
//trying to modify that entity. if caller modifyEntityFunc returns true, it means that caller wants to resave modified entity.
if (modifyEntityFunc(tResult))
{
//json serialization
var json = JsonConvert.SerializeObject(tResult);
cache.StringSet(key, json, timeExpiration);
}
}
else
{
//save pre-defined string in case if source-value is null.
cache.StringSet(key, "REDIS_NULL", timeExpiration);
}
}
else
{
//retrieve from cache and serialize to required object
tResult = JsonConvert.DeserializeObject<T>(result);
//trying to modify
if (modifyEntityFunc(tResult))
{
//and save if required
var json = JsonConvert.SerializeObject(tResult);
cache.StringSet(key, json, timeExpiration);
}
}
//refresh exiration in case of sliding expiration flag
if(isSlidingExpiration)
cache.KeyExpire(key, timeExpiration);
}
catch (Exception ex)
{
logException = ex;
}
finally
{
cache.LockRelease(lockName, lockToken);
}
break;
}
if (lockCounter >= 20 || logException!=null)
{
//log it
}
return tResult;
}
と使い方:
public class User
{
public int ViewCount { get; set; }
}
var cachedAndModifiedItem = GetCachedAndModifyWithLock<User>( "MyAwesomeKey", () =>
{
//return from db or kind of that
return new User() { ViewCount = 0 };
}, TimeSpan.FromMinutes(10), user=>
{
if (user.ViewCount< 3)
{
user.ViewCount++;
return true; //save it to cache
}
return false; //do not update it in cache
}, TimeSpan.FromSeconds(10),true);
そのコードは改善できます(たとえば、キャッシュへの呼び出し回数を減らすためにトランザクションを追加できます)が、それが役立つことを嬉しく思います。