martes, 30 de julio de 2013

Optimistic Concurrency with REDIS

When using a database there are some situations when you retrieve some values, make some verifications or modifications and then make the update to the database.  This is a typical situation:

values = read_from_database()
if (values.x == A)
    values.x = B
    update_to_database(values)

The problem in this cases is how do you ensure that nobody changed the status from A to C between you read the database and you update it.

One possible solution is using locks in all your code.   This is an easy solution if you have a single instance of your service, but difficult to implement in distributed deployments (although doable even with REDIS [1]).

lock()
values = read_from_database()
if (values.x == A)
   values.x = B
   update_to_database(values)
unlock();

Using locks to solve this concurrency issue is what we call Pesimistic Concurrency, because you assume that you are going to have concurrent modifications and you protect always against them.

Other possible solution is to try to make the modification asking the update to the database to fail if the data changed since the time we read it.   This can be accomplished easily with version numbers or timestamps:

values = read_from_database()
version = values.version
if (values.x == A)
   values.x = B
   values.version ++
   update_to_database_only_if(values, version=version)

Using this strategy to solve this concurrency issue is what we call Optimistic Concurrency, because you assume that you are not going to have concurrent modifications most of the times and the cases it happens you just throw an error and the application should recover from it.

In REDIS we don't have to manually implement this logic and we can make use of WATCH [2] command [2].   Watch allows to mark a specific entry in the database (a key in REDIS) so that if that value changes before we commit the changes we are going to make, we will get an error and the changes will be discarded:

WATCH('customer:27')   // Future EXEC should fail if customer:27 changes
state = HGET('customer:27', 'state')
if state == 'available':
     MULTI()  // Initiate a transaction
     HSET('customer:27', 'state', 'busy')
     EXEC() // Execute the transaction, failing if customer:27 changed since the WATCH command
else:
     UNWATCH()

Very important: You have to manually UNWATCH if you don't make the EXEC or otherwise if the redis connection is reused the watch will be still in place for the next EXEC to be run in that connection.


[1] https://github.com/ServiceStack/ServiceStack.Redis/wiki/RedisLocks
[2] http://redis.io/commands/watch