Here is an example of how to do this with single LUA script, as Redis is signle-threaded you don't need to add any locks or anything else here.
-- This script increments a counter and returns a boolean indicating whether the counter is below a limit. --
-- If the counter is below the limit, the counter is incremented and the new value is returned. --
-- If the counter is above the limit, the counter is not incremented and the current value is returned. --
-- The counter is set to expire after 60 seconds. --
-- ARGV[1] - The key of the counter in Redis --
-- ARGV[2] - The command INCR or DECR --
-- ARGV[3] - Limit to check against --
local counter
local counter_key = ARGV[1]
local command = ARGV[2]
local limit = tonumber(ARGV[3])
local expiration_in_seconds = 60
local success = false
counter = redis.call("GET", counter_key)
if counter == false then
counter = 0
end
if tonumber(counter) < limit and command == "INCR" then
success = true
counter = redis.call("INCR", counter_key)
elseif tonumber(counter) > limit and command == "DECR" then
success = true
counter = redis.call("DECR", counter_key)
end
redis.call("EXPIRE", counter_key, expiration_in_seconds)
return { success, tonumber(counter) }
-- Example of call --
-- EVAL ${LUA_SCRIPT_ABOVE_AS_STRING} 0 test_key INCR 10 --
Example or client in Ruby (you can reimplement in your language of choice):
module RedisHelpers
module CounterWithLimit
LUA_SCRIPT = File.read("#{__dir__}/counter_with_limit.lua").freeze
# NOTE: Redis returns 1 for true and nil for false when LUA script is executed
SUCCESS_VALUE = 1
module_function
def try_incr(key:, limit:, redis: Redis.new)
# NOTE: https://redis.io/commands/eval/
success, counter = redis.call('EVAL', LUA_SCRIPT, 0, key, 'INCR', limit)
{ success: success == SUCCESS_VALUE, counter: }
end
def try_decr(key:, limit: 0, redis: Redis.new)
# NOTE: https://redis.io/commands/eval/
success, counter = redis.call('EVAL', LUA_SCRIPT, 0, key, 'DECR', limit)
{ success: success == SUCCESS_VALUE, counter: }
end
end
end