Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I read from Redis inside a MULTI block in Ruby?

I'm encapsulating a complicated set of Redis commands in a MULTI transaction, but the logic in the transaction depends on values already in Redis. But all reads within a transaction seem to return nil

Here's an example that demonstrates the problem:

[Dev]> $redis.set("foo", "bar")
=> "OK"
[Dev]> $redis.multi{ $redis.set("foo", "baz") if $redis.get("foo") == "bar" }
=> ["bar"]
[Dev]> $redis.get("foo")
=> "bar"

Obviously I want the last return value to be 'baz' – how can I achieve this?

like image 742
tws Avatar asked Jul 02 '12 20:07

tws


People also ask

What is Redis multi?

A Redis Transaction is entered using the MULTI command. The command always replies with OK . At this point the user can issue multiple commands. Instead of executing these commands, Redis will queue them. All the commands are executed once EXEC is called.

Is Redis multi Atomic?

Commands included in a transaction block are run sequentially in the order they're queued. Redis transactions are atomic, meaning that either every command in a transaction block is processed (meaning that it's accepted as valid and queued to be executed) or none are.

Does Redis support atomic operation?

Redis features two main mechanisms for executing multiple operations atomically: MULTI / EXEC transactions and Lua scripts.

How are Redis pipelining and transaction different?

Transactions vs Pipeline in Redis. The difference is pipelines are not atomic whereas transactions are atomic, meaning 2 transactions do not run at the same time, whereas multiple pipelines can be executed by Redis-server at the same time in an interleaved fashion.


2 Answers

You cannot, since all commands (including get) are actually executed at exec time. In this situation, the get command only returns a future object, not the actual value.

There are two ways to implement such transaction.

Using a WATCH clause

The watch clause is used to protect against concurrent updates. If the value of the variable is updated between the watch and multi clause, then the commands in the multi block are not applied. It is up to the client to attempt the transaction another time.

loop do
    $redis.watch "foo" 
    val = $redis.get("foo")
    if val == "bar" then
        res = $redis.multi do |r|
            r.set("foo", "baz") 
        end
        break if res
    else
        $redis.unwatch "foo"
        break
    end
end

Here the script is a bit complex because the content of the block can be empty, so there is no easy way to know whether the transaction has been cancelled, or whether it did not take place at all. It is generally easier when the multi block returns results in all cases except if the transaction is cancelled.

Using Lua server-side scripting

With Redis 2.6 or better, Lua scripts can be executed on the server. The execution of the whole script is atomic. It can be easily implemented in Ruby:

cmd = <<EOF
    if redis.call('get',KEYS[1]) == ARGV[1] then
       redis.call('set',KEYS[1],ARGV[2] )
    end
EOF
$redis.eval cmd, 1, "foo", "bar", "baz"

This is typically much simpler than using WATCH clauses.

like image 87
Didier Spezia Avatar answered Oct 21 '22 01:10

Didier Spezia


As Sergio states in his comment, you can't optionally execute a MULTI block like that in Redis. See the documentation on transactions:

Either all of the commands or none are processed.

You can, however, use WATCH to implement optimistic locking using check-and-set (pseudo code):

SET foo bar
WATCH foo
$foo = GET foo
MULTI
if $foo == 'bar'
  SET foo baz
EXEC
GET foo

Using WATCH, the transaction will only be executed if the watched key(s) has not been changed. If the watch key is changed, the EXEC will fail, and you can try again.

Another possibility is using the scripting functionality, but that's only available in the 2.6 release candidate.

like image 21
Linus Thiel Avatar answered Oct 21 '22 00:10

Linus Thiel