Redis
 sql >> Base de données >  >> NoSQL >> Redis

Incrément distribué Redis avec verrouillage

En effet, votre code n'est pas sûr autour de la limite de survol, car vous faites un "get", (latence et réflexion), "set" - sans vérifier que les conditions de votre "get" s'appliquent toujours. Si le serveur est occupé autour de l'élément 1000, il serait possible d'obtenir toutes sortes de sorties folles, y compris des choses comme :

1
2
...
999
1000 // when "get" returns 998, so you do an incr
1001 // ditto
1002 // ditto
0 // when "get" returns 999 or above, so you do a set
0 // ditto
0 // ditto
1

Choix :

  1. utilisez les API de transaction et de contrainte pour rendre votre logique sécurisée pour la concurrence
  2. réécrivez votre logique en tant que script Lua via ScriptEvaluate

Désormais, les transactions Redis (selon l'option 1) sont difficiles. Personnellement, j'utiliserais "2" - en plus d'être plus simple à coder et à déboguer, cela signifie que vous n'avez qu'un aller-retour et une opération, par opposition à "get, watch, get, multi, incr/set, exec/ jeter », et une boucle « réessayer depuis le début » pour tenir compte du scénario d'abandon. Je peux essayer de l'écrire en Lua si vous le souhaitez - cela devrait faire environ 4 lignes.

Voici l'implémentation Lua :

string key = ...
for(int i = 0; i < 2000; i++) // just a test loop for me; you'd only do it once etc
{
    int result = (int) db.ScriptEvaluate(@"
local result = redis.call('incr', KEYS[1])
if result > 999 then
    result = 0
    redis.call('set', KEYS[1], result)
end
return result", new RedisKey[] { key });
    Console.WriteLine(result);
}

Remarque :si vous devez paramétrer le maximum, vous utiliserez :

if result > tonumber(ARGV[1]) then

et :

int result = (int)db.ScriptEvaluate(...,
    new RedisKey[] { key }, new RedisValue[] { max });

(donc ARGV[1] prend la valeur de max )

Il faut comprendre que eval /evalsha (c'est ce que ScriptEvaluate appels) ne sont pas en concurrence avec d'autres demandes de serveur , donc rien ne change entre le incr et l'éventuel set . Cela signifie que nous n'avons pas besoin de watch complexes logique etc.

Voici la même chose (je pense !) via l'API de transaction/contrainte :

static int IncrementAndLoopToZero(IDatabase db, RedisKey key, int max)
{
    int result;
    bool success;
    do
    {
        RedisValue current = db.StringGet(key);
        var tran = db.CreateTransaction();
        // assert hasn't changed - note this handles "not exists" correctly
        tran.AddCondition(Condition.StringEqual(key, current));
        if(((int)current) > max)
        {
            result = 0;
            tran.StringSetAsync(key, result, flags: CommandFlags.FireAndForget);
        }
        else
        {
            result = ((int)current) + 1;
            tran.StringIncrementAsync(key, flags: CommandFlags.FireAndForget);
        }
        success = tran.Execute(); // if assertion fails, returns false and aborts
    } while (!success); // and if it aborts, we need to redo
    return result;
}

Compliqué, hein ? Le cas de réussite simple voici donc :

GET {key}    # get the current value
WATCH {key}  # assertion stating that {key} should be guarded
GET {key}    # used by the assertion to check the value
MULTI        # begin a block
INCR {key}   # increment {key}
EXEC         # execute the block *if WATCH is happy*

ce qui est... un peu de travail, et implique un décrochage du pipeline sur le multiplexeur. Les cas les plus compliqués (échecs d'assertion, échecs de surveillance, bouclage) auraient une sortie légèrement différente, mais devraient fonctionner.