Rzeczywiście, twój kod nie jest bezpieczny wokół granicy najazdu, ponieważ wykonujesz „pobieranie” (opóźnienie i myślenie), „ustawianie” — bez sprawdzania, czy warunki w twoim „pobierz” nadal mają zastosowanie. Jeśli serwer jest zajęty w okolicach pozycji 1000, możliwe byłoby uzyskanie różnego rodzaju zwariowanych danych wyjściowych, w tym rzeczy takich jak:
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
Opcje:
- użyj interfejsów API transakcji i ograniczeń, aby zapewnić bezpieczną współbieżność logiki
- przepisz swoją logikę jako skrypt Lua za pomocą
ScriptEvaluate
Teraz transakcje redis (na opcję 1) są trudne. Osobiście użyłbym "2" - oprócz tego, że jest prostsze w kodowaniu i debugowaniu, oznacza to, że masz tylko 1 podróż w obie strony i operację, w przeciwieństwie do "get, watch, get, multi, incr/set, exec/ odrzuć” i pętlę „ponów próbę od początku”, aby uwzględnić scenariusz przerwania. Mogę spróbować napisać to jako Lua, jeśli chcesz - powinno to być około 4 linijek.
Oto implementacja 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);
}
Uwaga:jeśli potrzebujesz sparametryzować maksimum, użyjesz:
if result > tonumber(ARGV[1]) then
i:
int result = (int)db.ScriptEvaluate(...,
new RedisKey[] { key }, new RedisValue[] { max });
(więc ARGV[1]
pobiera wartość z max
)
Należy zrozumieć, że eval
/evalsha
(co jest tym, co ScriptEvaluate
połączeń) nie konkurują z innymi żądaniami serwera , więc nic się nie zmienia między incr
i możliwy set
. Oznacza to, że nie potrzebujemy skomplikowanego watch
logika itp.
Oto to samo (chyba!) za pośrednictwem interfejsu API transakcji / ograniczeń:
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;
}
Skomplikowane, co? Prosty przypadek sukcesu oto więc:
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*
co jest… dość pracochłonne i wiąże się z zatrzymaniem potoku na multiplekserze. Bardziej skomplikowane przypadki (niepowodzenia asercji, awarie zegarków, zawijanie) miałyby nieco inne wyniki, ale powinny działać.