The if modified since type of pattern for retrieving data from cache can save significant network bandwidth and compute cycles.

The data modification time reference should be from the client side and be sent as part of the request and not assumed on server side based on a non acknowledgement type of method like simply last time the client connected to the server.

This kind of pattern is possible on redis server side leveraging lua on redis, with very less overhead in terms of latency.

We can store the last modification time of a key in a hash and retrieve the value only if it is newer than the time client sent.

The following Lua snippets store/retrieve based on the modification time of key in a hash called MY_KEYSTIME.

Get based on modification time

local keys_mtime_hset = "MY_KEYSMTIME"
local key = KEYS[1]
local mtime = tonumber(ARGV[1])

local key_mtime = redis.call('HGET', keys_mtime_hset, key)
key_mtime = tonumber(key_mtime)
-- if missing key in the hash set return the value of the key.
-- or key mtime > mtime 
if not key_mtime or key_mtime > mtime then
    return redis.call('GET', key)
end

return nil 

Set and update modification time

local keys_mtime_hset = "MY_KEYSMTIME"
local key = KEYS[1]
local value = ARGV[1]
local mtime = tonumber(ARGV[2])

redis.call('SET', key, value)
redis.call('HSET', keys_mtime_hset, key, mtime)

Client sends the modification time while retrieving and setting the key. Redis Lua scripts are atomic.

The following python code illustrates a full working example.

mtime_get="""
local keys_mtime_hset = "MY_KEYSMTIME"
local key = KEYS[1]
local mtime = tonumber(ARGV[1])

local key_mtime = redis.call('HGET', keys_mtime_hset, key)
key_mtime = tonumber(key_mtime)
-- if missing key in the hash set return the value of the key.
-- or key mtime > mtime 
if not key_mtime or key_mtime > mtime then
    return redis.call('GET', key)
end

return nil    
"""

mtime_set="""
local keys_mtime_hset = "MY_KEYSMTIME"
local key = KEYS[1]
local value = ARGV[1]
local mtime = tonumber(ARGV[2])

redis.call('SET', key, value)
redis.call('HSET', keys_mtime_hset, key, mtime)
"""

m1 = int(time.time())
r = redis.Redis(host='localhost', port=6379)
MTGET=r.register_script(mtime_get)
MTSET=r.register_script(mtime_set)

t_k = "Hello"
t_v = "World"

r.delete(t_k)

print(MTGET(keys=[t_k], args=[m1]))

print(MTSET(keys=[t_k], args=[t_v, m1]))

print(r.get(t_k))

print(MTGET(keys=[t_k], args=[m1 - 100]))

print(MTGET(keys=[t_k], args=[m1 + 100]))

Running it

None
None
b'World'
b'World'
None

How much overhead will this incur ? A simple benchmark shows not a lot, since HGET is an O(1) operation.

REPEAT=10000
NUMBER=3

g = timeit.Timer("r.get(t_k)",globals=globals())
timings_get = g.repeat(repeat=REPEAT, number=NUMBER)

g = timeit.Timer("MTGET(keys=[t_k], args=[m1 - 100])",globals=globals())
timings_hits_mtget = g.repeat(repeat=REPEAT, number=NUMBER)

g = timeit.Timer("MTGET(keys=[t_k], args=[m1 + 100])",globals=globals())
timings_miss_mtget = g.repeat(repeat=REPEAT, number=NUMBER)

Running and plotting the timings from this benchmark, x axis is time taken for calls.

redis_mtget_timings

As expected there is a slight overhead, with plain get being the fastest and modification time based get the slowest. See this jupyter notebook for full working example and chart source.

HN Dicussion.