En effet, votre code n'est pas sûr autour de la limite de survol, car vous faites un "get", (latence et réflexion), "set" - sans vérifier que les conditions de votre "get" s'appliquent toujours. Si le serveur est occupé autour de l'élément 1000, il serait possible d'obtenir toutes sortes de sorties folles, y compris des choses comme :
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
Choix :
- utilisez les API de transaction et de contrainte pour rendre votre logique sécurisée pour la concurrence
- réécrivez votre logique en tant que script Lua via
ScriptEvaluate
Désormais, les transactions Redis (selon l'option 1) sont difficiles. Personnellement, j'utiliserais "2" - en plus d'être plus simple à coder et à déboguer, cela signifie que vous n'avez qu'un aller-retour et une opération, par opposition à "get, watch, get, multi, incr/set, exec/ jeter », et une boucle « réessayer depuis le début » pour tenir compte du scénario d'abandon. Je peux essayer de l'écrire en Lua si vous le souhaitez - cela devrait faire environ 4 lignes.
Voici l'implémentation 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);
}
Remarque :si vous devez paramétrer le maximum, vous utiliserez :
if result > tonumber(ARGV[1]) then
et :
int result = (int)db.ScriptEvaluate(...,
new RedisKey[] { key }, new RedisValue[] { max });
(donc ARGV[1]
prend la valeur de max
)
Il faut comprendre que eval
/evalsha
(c'est ce que ScriptEvaluate
appels) ne sont pas en concurrence avec d'autres demandes de serveur , donc rien ne change entre le incr
et l'éventuel set
. Cela signifie que nous n'avons pas besoin de watch
complexes logique etc.
Voici la même chose (je pense !) via l'API de transaction/contrainte :
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;
}
Compliqué, hein ? Le cas de réussite simple voici donc :
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*
ce qui est... un peu de travail, et implique un décrochage du pipeline sur le multiplexeur. Les cas les plus compliqués (échecs d'assertion, échecs de surveillance, bouclage) auraient une sortie légèrement différente, mais devraient fonctionner.