De fato, seu código não está seguro em torno do limite de rollover, porque você está fazendo um "get", (latência e pensamento), "set" - sem verificar se as condições em seu "get" ainda se aplicam. Se o servidor estiver ocupado em torno do item 1000, seria possível obter todos os tipos de saídas malucas, incluindo coisas como:
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
Opções:
- use as APIs de transação e restrição para tornar sua lógica segura para simultaneidade
- reescreva sua lógica como um script Lua via
ScriptEvaluate
Agora, as transações redis (por opção 1) são difíceis. Pessoalmente, eu usaria "2" - além de ser mais simples de codificar e depurar, significa que você só tem 1 ida e volta e operação, ao contrário de "get, watch, get, multi, incr/set, exec/ descarte" e um loop "retry from start" para considerar o cenário de aborto. Posso tentar escrevê-lo como Lua para você, se quiser - deve ter cerca de 4 linhas.
Aqui está a implementação de 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 você precisar parametrizar o max, você usaria:
if result > tonumber(ARGV[1]) then
e:
int result = (int)db.ScriptEvaluate(...,
new RedisKey[] { key }, new RedisValue[] { max });
(então
ARGV[1]
pega o valor de max
) É necessário entender que
eval
/evalsha
(que é o que ScriptEvaluate
chamadas) não estão competindo com outras solicitações do servidor , então nada muda entre o incr
e o possível set
. Isso significa que não precisamos de watch
complexos etc lógica. Aqui está o mesmo (eu acho!) por meio da API de transação / restrição:
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;
}
Complicado, hein? O caso de sucesso simples aqui está então:
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*
que é... um pouco de trabalho, e envolve uma parada de pipeline no multiplexador. Os casos mais complicados (falhas de declaração, falhas de observação, wrap-arounds) teriam uma saída ligeiramente diferente, mas deveriam funcionar.