sql >> Databasteknik >  >> NoSQL >> Redis

Redis fördelat inkrement med låsning

Faktum är att din kod inte är säker runt rollover-gränsen, eftersom du gör en "get", (latency och tänkande), "set" - utan att kontrollera att villkoren i din "get" fortfarande gäller. Om servern är upptagen runt objekt 1000 skulle det vara möjligt att få alla möjliga galna utdata, inklusive saker 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

Alternativ:

  1. använd transaktions- och begränsnings-API:erna för att göra din logik samtidighetssäker
  2. skriv om din logik som ett Lua-skript via ScriptEvaluate

Nu är återupplösningar (per alternativ 1) svåra. Personligen skulle jag använda "2" - förutom att det är enklare att koda och felsöka, betyder det att du bara har 1 tur och retur och operation, i motsats till "get, watch, get, multi, incr/set, exec/ kassera" och en "försök igen från start"-loop för att ta hänsyn till avbrytscenariot. Jag kan försöka skriva det som Lua åt dig om du vill - det bör vara cirka 4 rader.

Här är 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);
}

Obs:om du behöver parametrisera max, skulle du använda:

if result > tonumber(ARGV[1]) then

och:

int result = (int)db.ScriptEvaluate(...,
    new RedisKey[] { key }, new RedisValue[] { max });

(alltså ARGV[1] tar värdet från max )

Det är nödvändigt att förstå den eval /evalsha (vilket är vad ScriptEvaluate samtal) konkurrerar inte med andra serverförfrågningar , så ingenting ändras mellan incr och den möjliga set . Det betyder att vi inte behöver komplex watch etc logik.

Här är samma (tror jag!) 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;
}

Komplicerat, va? Det enkla framgångsfallet här är då:

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*

vilket är... ganska mycket arbete, och involverar en pipeline stall på multiplexern. De mer komplicerade fallen (påståendemisslyckanden, klockmisslyckanden, wrap-arounds) skulle ha något annorlunda utdata, men borde fungera.



  1. Docker-compose - Redis vid 0.0.0.0 istället för 127.0.0.1

  2. Använda Redis Sets

  3. MongoDB :aggregeringsramverk :$matchning mellan fält

  4. Hämta data från samling b som inte finns i samling a i en MongoDB-skalfråga