Je code is inderdaad niet veilig rond de rollover-grens, omdat je een "get", (latentie en denken), "set" doet - zonder te controleren of de voorwaarden in je "get" nog steeds van toepassing zijn. Als de server bezig is rond item 1000, zou het mogelijk zijn om allerlei gekke outputs te krijgen, waaronder dingen als:
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
Opties:
- gebruik de transactie- en beperkings-API's om uw logica gelijktijdig veilig te maken
- herschrijf je logica als een Lua-script via
ScriptEvaluate
Nu zijn redis-transacties (per optie 1) moeilijk. Persoonlijk zou ik "2" gebruiken - naast dat het eenvoudiger is om te coderen en te debuggen, betekent dit dat je maar 1 retour en bewerking hebt, in tegenstelling tot "get, watch, get, multi, incr/set, exec/ weggooien" en een "opnieuw proberen vanaf start"-lus om rekening te houden met het afbreekscenario. Ik kan proberen het als Lua voor je te schrijven als je wilt - het zou ongeveer 4 regels moeten zijn.
Hier is de Lua-implementatie:
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);
}
Opmerking:als u de max moet parametriseren, gebruikt u:
if result > tonumber(ARGV[1]) then
en:
int result = (int)db.ScriptEvaluate(...,
new RedisKey[] { key }, new RedisValue[] { max });
(dus ARGV[1]
neemt de waarde van max
)
Het is noodzakelijk om te begrijpen dat eval
/evalsha
(dat is wat ScriptEvaluate
oproepen) concurreren niet met andere serververzoeken , dus er verandert niets tussen de incr
en de mogelijke set
. Dit betekent dat we geen complexe watch
nodig hebben enz. logica.
Hier is hetzelfde (denk ik!) via de transactie-/beperkings-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;
}
Ingewikkeld, hè? De eenvoudige succescase hier is dan:
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*
dat is... nogal wat werk, en er is een pijpleidingstop op de multiplexer nodig. De meer gecompliceerde gevallen (bevestigingsfouten, horlogefouten, wrap-arounds) zouden een iets andere output hebben, maar zouden moeten werken.