web-dev-qa-db-ja.com

StackExchange.Redis-LockTake / LockReleaseの使用

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
        //...
    });
27
lolcodez

ロックには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がオプションです。

42
Marc Gravell

コメント付きのロック->取得->変更(必要な場合)->ロック解除アクションのコードの一部があります。

    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);

そのコードは改善できます(たとえば、キャッシュへの呼び出し回数を減らすためにトランザクションを追加できます)が、それが役立つことを嬉しく思います。

3
Nigrimmist