Sebastian Nowak
Sebastian Nowak

Reputation: 5717

Redis as LRU cache race condition

Imagine following scenario:

  1. Client #1 requests object A from Redis
  2. Redis answers with null
  3. Client #1 requests object A from database
  4. Database returns object A
  5. Client #1 stores object A in Redis

Everything works fine. Let's add another client between 4 and 5:

  1. Client #1 requests object A from Redis
  2. Redis answers with null
  3. Client #1 requests object A from database
  4. Database returns object A

    • Client #2 requests object A from Redis
    • Redis answers with null
    • Client #2 requests object A from dtabase
    • Database returns object A
    • Client #2 stores object A in Redis
    • Client #2 updates object A, both in database and Redis
  5. Client #1 stores object A in Redis

Now object A is up to date in database, but outdated in Redis. Are there any patterns to prevent such behaviour? Obviously locking the database while waiting for Redis to store its copy isn't a solution.

I should also note I'm using NodeJS which maintains a fixed size connection pool to Redis and requests are processed asynchronously, so we can't assume that the order of queries from different clients won't be mixed in a single connection.

Upvotes: 1

Views: 1166

Answers (3)

Lunan
Lunan

Reputation: 183

If your data are not changed often (non-volatile), as in Historical data from yesterday or a month ago, you can safely ignore the race condition, because no matter who got the last commit, data is still the same across clients.

For time sensitive data, especially transaction data, you can separate the cache based on user id by using user id (Client#1, Client#2) as the additional Key. This will eliminate race condition between clients without resorting to locking and transaction synchronization with drawback of higher memory usage.

This method can be combined with the first method e.g. if the data is about user specific data, you can use user id key, and on the other hand if the data contains shared/grouping information like menu access based on role, you can use the group id instead.

Upvotes: 1

omerfarukdemir
omerfarukdemir

Reputation: 158

If there is not an update operation on object (5th step), you should use SETNX to store it. So you don't store outdated object.

Upvotes: 0

Aaron Dufour
Aaron Dufour

Reputation: 17505

You're looking for Redis transactions. The key is the WATCH command. You call WATCH on object A before the initial read, and set the object inside a MULTI. Now your two scenarios look like:

  1. Client #1 WATCHes object A in Redis
  2. Client #1 requests object A from Redis
  3. Redis answers with null
  4. Client #1 requests object A from database
  5. Database returns object A
  6. Client #1 stores object A in Redis in MULTI/EXEC block

and

  1. Client #1 WATCHes object A in Redis
  2. Client #1 requests object A from Redis
  3. Redis answers with null
  4. Client #1 requests object A from database
  5. Database returns object A

    • Client #2 requests object A from Redis
    • Redis answers with null
    • Client #2 requests object A from dtabase
    • Database returns object A
    • Client #2 stores object A in Redis
    • Client #2 updates object A, both in database and Redis
  6. Client #1 stores object A in Redis in MULTI/EXEC block, but it fails because object A changed since the WATCH

After failure, Client #1 could retry, but it's probably better to just ignore the failure, since it appears that you're just using it as a cache.

Client #2, of course, should have also done the WATCH/MULTI/EXEC pattern.

Upvotes: 0

Related Questions