Redis
 sql >> Datenbank >  >> NoSQL >> Redis

Redis verteiltes Inkrement mit Sperren

In der Tat ist Ihr Code um die Rollover-Grenze herum nicht sicher, da Sie ein "Get", (Latenz und Denken), "Setzen" durchführen - ohne zu prüfen, ob die Bedingungen in Ihrem "Get" noch gelten. Wenn der Server um Punkt 1000 herum ausgelastet ist, wäre es möglich, alle möglichen verrückten Ausgaben zu erhalten, einschließlich Dinge wie:

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

Optionen:

  1. verwenden Sie die Transaktions- und Einschränkungs-APIs, um Ihre Logik parallelitätssicher zu machen
  2. schreiben Sie Ihre Logik als Lua-Skript über ScriptEvaluate um

Jetzt sind Redis-Transaktionen (gemäß Option 1) schwierig. Persönlich würde ich "2" verwenden - abgesehen davon, dass es einfacher zu codieren und zu debuggen ist, bedeutet dies, dass Sie nur einen Roundtrip und eine Operation haben, im Gegensatz zu "get, watch, get, multi, incr/set, exec/ discard" und eine "retry from start"-Schleife, um das Abbruchszenario zu berücksichtigen. Ich kann versuchen, es für dich als Lua zu schreiben, wenn du möchtest - es sollte ungefähr 4 Zeilen lang sein.

Hier ist die Lua-Implementierung:

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

Hinweis:Wenn Sie das Maximum parametrisieren müssen, verwenden Sie:

if result > tonumber(ARGV[1]) then

und:

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

(also ARGV[1] nimmt den Wert von max )

Es ist notwendig, diesen eval zu verstehen /evalsha (was ScriptEvaluate ist Anrufe) konkurrieren nicht mit anderen Serveranfragen , also ändert sich nichts zwischen den incr und den möglichen set . Das bedeutet, dass wir keine komplexe watch benötigen usw. Logik.

Hier ist dasselbe (glaube ich!) über die Transaktions-/Einschränkungs-API:

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

Kompliziert, oder? Der einfache Erfolgsfall hier steht dann:

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*

das ist ... ziemlich viel Arbeit und beinhaltet einen Pipeline-Stall auf dem Multiplexer. Die komplizierteren Fälle (Assertionsfehler, Überwachungsfehler, Wrap-Arounds) würden eine etwas andere Ausgabe haben, sollten aber funktionieren.