我有一项手术需要完成 N 次,不能再多了。该操作是由许多接收请求的可能并行的进程完成的。进程必须检查计数器是否超过 N,如果没有,则递增计数器并执行操作。我想我可以使用 Redis 计数器来实现这一点。
但是,如果我只是
GET
然后 INCR
一个值,我可能会遇到竞争条件,这将导致操作执行超过 N 次。如何对 Redis 执行某种 test-and-incr
操作?
我知道我可以使用
WATCH
,但这是一个乐观锁。我预计每秒都会发生很多次碰撞。这会导致很多失败。也许我可以用某种外部互斥体包装简单的 GET
和 INCR
。但我不确定它的性能是否足够好。
试试这个
增量
SET my_counter 10
INCR my_counter // Result: 11
INCR my_counter // Result: 12
增加并返回新值
// INCR command increments the value of the key and returns the new value
const incrementedValue = await redis.incr(key);
console.log("Incremented value:", incrementedValue);
这里有一个示例,说明如何使用单个 LUA 脚本执行此操作,因为 Redis 是单线程的,因此您不需要在此处添加任何锁或其他任何内容。
-- 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 --
Ruby 中的示例或客户端(您可以用您选择的语言重新实现):
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