StackExchange.Utilisation De Redis-LockTake / LockRelease

J'utilise Redis avec StackExchange.Redis. J'ai plusieurs threads qui vont à un certain point d'accès et éditer la valeur de la même clé, donc je dois synchroniser la manipulation des données.

en regardant les fonctions disponibles, je vois qu'il y a deux fonctions, TakeLock et ReleaseLock. Cependant, ces fonctions prennent à la fois une clé et un paramètre de valeur plutôt que la clé unique attendue à verrouiller. La documentation d'intellisene et la source sur GitHub n'expliquent pas comment pour utiliser les fonctions LockTake et LockRelease ou quoi passer pour la clé et les paramètres de valeur.

Q: Quelle est l'utilisation correcte de LockTake et LockRelease dans StackExchange?Redis?

pseudo-code exemple de ce que j'ai l'intention de faire:

//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
        //...
    });
18
demandé sur lolcodez 2014-08-05 00:41:37

2 réponses

il y a 3 parties à une serrure:

  • la clé (le nom unique de la serrure dans la base de données)
  • la valeur (l'appelant défini jeton qui peut être utilisé à la fois pour indiquer qui est "propriétaire" de la serrure, et de vérifier que la libération et l'extension de la serrure est effectué correctement)
  • la durée (un verrou est intentionnellement une durée finie chose)

si aucune autre valeur ne vient à l'esprit, un guide pourrait faire une "valeur"appropriée. Nous avons tendance à utiliser nom de la machine (ou version mungée du nom de la machine si plusieurs procédés peuvent être en concurrence sur la même machine).

aussi, notez que prendre une serrure est spéculative, pas blocage. Il est tout à fait possible que vous fail pour obtenir la serrure, et donc vous pouvez avoir besoin de tester pour cela et peut-être ajouter de la logique de réessaiement.

Un exemple typique pourrait être:

RedisValue token = Environment.MachineName;
if(db.LockTake(key, token, duration)) {
    try {
        // you have the lock do work
    } finally {
        db.LockRelease(key, token);
    }
}

Note que si le travail est long (une boucle, particulier), vous souhaiterez peut-être ajouter quelques LockExtend appels au milieu de nouveau à se rappeler de vérifier pour la réussite (dans le cas où il a expiré).

Notez aussi que les commandes individuelles redis sont atomiques, donc vous n'avez pas à vous soucier de deux opérations discrètes en compétition. Pour les unités multi-opérations plus complexes, transactions et script sont des options.

34
répondu Marc Gravell 2014-08-05 11:53:12

il y a ma partie de code pour lock->get->modify(si nécessaire)->unlock actions with comments.

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

et d'utilisation :

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

ce code peut être amélioré (par exemple, vous pouvez ajouter des transactions pour moins de count call à cache et etc), mais je suis content qu'il vous sera utile.

2
répondu Nigrimmist 2015-09-29 19:42:55