Redis
 sql >> Database >  >> NoSQL >> Redis

Incremento distribuito Redis con bloccaggio

In effetti, il tuo codice non è sicuro attorno al limite di rollover, perché stai facendo un "get", (latenza e pensiero), "set" - senza verificare che le condizioni nel tuo "get" siano ancora valide. Se il server è occupato intorno all'elemento 1000, sarebbe possibile ottenere tutti i tipi di output pazzi, inclusi cose come:

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

Opzioni:

  1. utilizza le API di transazione e vincolo per rendere la tua logica sicura per la concorrenza
  2. riscrivi la tua logica come uno script Lua tramite ScriptEvaluate

Ora, le transazioni redis (per opzione 1) sono difficili. Personalmente, userei "2" - oltre ad essere più semplice da codificare ed eseguire il debug, significa che hai solo 1 andata e ritorno e operazione, al contrario di "get, watch, get, multi, incr/set, exec/ scartare" e un ciclo "riprova dall'inizio" per tenere conto dello scenario di interruzione. Se vuoi, posso provare a scriverlo come Lua:dovrebbe essere di circa 4 righe.

Ecco l'implementazione 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);
}

Nota:se hai bisogno di parametrizzare il massimo, dovresti usare:

if result > tonumber(ARGV[1]) then

e:

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

(quindi ARGV[1] prende il valore da max )

È necessario comprendere che eval /evalsha (che è ciò che ScriptEvaluate chiamate) non sono in competizione con altre richieste del server , quindi non cambia nulla tra incr e l'eventuale set . Ciò significa che non abbiamo bisogno di complessi watch ecc logica.

Ecco lo stesso (credo!) tramite l'API di transazione/vincolo:

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

Complicato, eh? Il semplice caso di successo ecco allora:

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*

che è... un bel po' di lavoro, e comporta uno stallo della pipeline sul multiplexer. I casi più complicati (asserzioni fallite, watch fallite, wrap-around) avrebbero un output leggermente diverso, ma dovrebbero funzionare.