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:
- utilizza le API di transazione e vincolo per rendere la tua logica sicura per la concorrenza
- 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.