4

I'm trying to use StackExchange.Redis to implement transactional Add Or Update logic. I've looked at the https://stackexchange.github.io/StackExchange.Redis/Transactions.html example on GitHub but this refers to an "add if not exists" process, which is really only part of the story.

My requirement is it "Add if cache item does not exist and update if it has not changed" (i.e. optimistic concurrency). This example helped https://github.com/StackExchange/StackExchange.Redis/issues/885 but it only uses one condition, as does the example for transaction on Github.

My tests of a redis transaction suggests that you cannot add multiple conditions, therefore this approach seems to work:

            var committed = await _redisDatabase.StringSetAsync(key, value, expiry, When.NotExists, flags);
            if (committed)
            {
                return committed;
            }

            var txn = _redisDatabase.CreateTransaction();

            txn.AddCondition(Condition.StringEqual(key, value));
            txn.StringSetAsync(key, value, expiry, when, flags);

            committed = await txn.ExecuteAsync();

            return committed;

i.e. assume it does not exist, attempt update if it does... The When.NotExists always needs to be called as the second section could fail because the object has changed or that it doesn't exist.. so you need the first call so that you know the second has failed due to stale stale.

This makes a SETNX on the first call and then the additional WATCH, GET, MULTI and SET commands....is there anyway to combine this into one Redis transaction?

1 Answer 1

1

Frankly, you should probably prefer Lua (EVAL/EVALSHA) to transactions (MULTI/EXEC). This is pretty easy to use too - for example, here's one that only ever increments a hash by key (never decrements), or creates new hash entries if it is missing:

var keys = new RedisKey[] { key };
var values = new RedisValue[] { subscriptionId, lastUpdate };

_redis.GetDatabase(db).ScriptEvaluate(@"
if redis.call('hsetnx', KEYS[1], ARGV[1], ARGV[2]) == 0 then
  local old = redis.call('hget', KEYS[1], ARGV[1])
  if tonumber(old) < tonumber(ARGV[2]) then
    redis.call('hset', KEYS[1], ARGV[1], ARGV[2])
  end
end", keys, values);

there is also an alternative API that does the same with some helpers that make it look more like SQL, but I prefer the above version with KEYS and ARGV, as I can test it directly via redis-cli.

Your version could do something similar, but testing the old expected value in Lua before calling set.

Just to explain: the 'hsetnx' here sets the hash value if it doesn't already exist; if that fails (== 0), then we fetch the existing value, compare it, and finally call hset if needed.

For more information: see the EVAL documentation. The ScriptEvaluate API can handle return semantics too, if you need to get values back.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.