Faktisk er din kode ikke sikker omkring rollover-grænsen, fordi du laver en "get", (latency og tænkning), "set" - uden at kontrollere, at betingelserne i din "get" stadig gælder. Hvis serveren er optaget omkring punkt 1000, ville det være muligt at få alle mulige skøre output, inklusive ting som:
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
Valgmuligheder:
- brug transaktions- og begrænsnings-API'erne til at gøre din logik samtidig sikker
- omskriv din logik som et Lua-script via
ScriptEvaluate
Nu er gendis-transaktioner (pr. mulighed 1) svære. Personligt ville jeg bruge "2" - udover at det er nemmere at kode og fejlfinde, betyder det, at du kun har 1 rundtur og betjening, i modsætning til "get, watch, get, multi, incr/set, exec/ kassere", og en "forsøg igen fra start"-løkke for at tage højde for afbrydelsesscenariet. Jeg kan prøve at skrive det som Lua for dig, hvis du vil - det skal være omkring 4 linjer.
Her er Lua-implementeringen:
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);
}
Bemærk:Hvis du skal indstille maks., skal du bruge:
if result > tonumber(ARGV[1]) then
og:
int result = (int)db.ScriptEvaluate(...,
new RedisKey[] { key }, new RedisValue[] { max });
(altså ARGV[1]
tager værdien fra max
)
Det er nødvendigt at forstå den eval
/evalsha
(hvilket er hvad ScriptEvaluate
opkald) konkurrerer ikke med andre serveranmodninger , så intet ændrer sig mellem incr
og det mulige set
. Det betyder, at vi ikke har brug for kompleks watch
osv. logik.
Her er det samme (tror jeg!) via transaktions-/begrænsnings-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;
}
Kompliceret, ikke? Den enkle successag her er så:
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*
hvilket er... en del arbejde, og involverer en pipeline stall på multiplexeren. De mere komplicerede sager (påstandsfejl, urfejl, omslutninger) ville have lidt anderledes output, men burde fungere.