Redis
 sql >> Base de Dados >  >> NoSQL >> Redis

Redis incremento distribuído com bloqueio


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:
  1. use as APIs de transação e restrição para tornar sua lógica segura para simultaneidade
  2. 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.