使用 SETNX 的单实例 Redis 锁
Single-instance Redis lock with SETNX
我需要从应用程序客户端连接到单个 Redis 实例。
由于客户端将在 Kubernetes 中进行复制,我正在研究有关锁的 Redis 文档以防止客户端副本之间的竞争。
在谷歌搜索和阅读之后,我关注了这两个资源:
- 此处描述的
SETNX
命令:https://redis.io/commands/setnx
- 此处描述的 Redlock 算法:https://redis.io/topics/distlock
有趣的是 SETNX
文档明确建议不要使用 SETNX
来实现锁,声明它基本上已经过时了:
The following pattern is discouraged in favor of the Redlock algorithm [...]
We document the old pattern anyway because certain existing implementations link to this page as a reference.
然而,Redlock 算法是专门为 分布式 锁量身定制的,因此当一个人试图锁定多个 Redis 实例时 - 它们实际上指的是多个 masters.
更进一步,库 redsync (golang) 声明 New
函数如下:
func New(pools []Pool) *Redsync {
return &Redsync{
pools: pools,
}
}
它看起来毫无疑问是为支持 Redis 集群上的锁定而设计的。
在我的用例中,我将只连接到一个 Redis 实例。
也许我可以只使用 redsync 包并传递一个长度为 1 的切片,但对我来说 SETNX
模式在单个 Redis 实例上同样可以正常工作。
我没看错吗?
谢谢
是的,Redlock 算法确实是为分布式 Redis 系统设计的,如果您使用的是单个实例,那么使用 SET and SETNX 文档中描述的更简单的锁定方法应该没问题.
然而,更重要的一点是:您可能不需要使用锁来避免多个 Redis 客户端之间的冲突。 Redis 锁通常用于保护某些 外部 分布式资源(请参阅我的回答 了解更多信息)。在 Redis 本身中,通常不需要锁;由于 Redis 的 single-threaded 特性,许多命令已经是原子的,您可以使用事务或 Lua 脚本来组合任意复杂的原子操作。
所以我的建议是设计您的客户端代码以使用原子性来避免冲突,而不是尝试使用锁(分布式或其他方式)。
我想我可以post一个答案供参考。按照 Kevin 的建议,我最终使用 Lua 脚本来确保原子性。
这是实现 (Go) 的样子:
// import "github.com/gomodule/redigo/redis"
type redisService struct {
pool *redis.Pool
lastLogin *redis.Script // Lua script initialized into this field
}
// Constructing a redis client
func NewRedisService(config *config.RedisClientConfig) RedisService {
return &redisService{
pool: &redis.Pool{
MaxIdle: 10,
IdleTimeout: 120 * time.Second,
Dial: func() (redis.Conn, error) {
return redis.Dial("tcp", config.BaseURL)
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
if time.Since(t) < time.Minute {
return nil
}
_, err := c.Do("PING")
return err
},
},
// initialize Lua script object
// lastLoginLuaScript is a Go const with the script literal
lastLogin: redis.NewScript(1, lastLoginLuaScript),
}
}
Lua 脚本(注释解释了它的作用):
--[[
Check if key exists, if it exists, update the value without changing the remaining TTL.
If it doesn't exist, create it.
Script params
KEYS[1] = the account id used as key
ARGV[1] = the key TTL in seconds
ARGV[2] = the value
]]--
local errorKeyExpired = 'KEXP'
local statusKeyUpdated = 'KUPD'
local statusKeyCreated = 'KCRE'
if redis.call('exists', KEYS[1]) == 1 then
local ttl = redis.call('ttl', KEYS[1])
if ttl < 0 then --[[ no expiry ]]--
redis.call('setex', KEYS[1], ARGV[1], ARGV[2])
return redis.status_reply(statusKeyCreated)
end
if ttl == 0 then --[[ expired ]]--
return redis.error_reply(errorKeyExpired)
end
redis.call('setex', KEYS[1], ttl, ARGV[2])
return redis.status_reply(statusKeyUpdated)
else
redis.call('setex', KEYS[1], ARGV[1], ARGV[2])
return redis.status_reply(statusKeyCreated)
end
用法:
func (rs *redisService) UpsertLastLoginTime(key string, ttl uint, value int64) (bool, error) {
conn := rs.pool.Get()
defer conn.Close()
// call Do on the script object
resp, err := rs.lastLogin.Do(conn, key, ttl, value)
switch resp {
case statusKeyCreated:
return true, nil
case statusKeyUpdated:
return false, nil
case errorKeyExpired:
return false, ErrKeyExpired
default:
return false, errors.Wrap(err, "script execution error")
}
}
我需要从应用程序客户端连接到单个 Redis 实例。
由于客户端将在 Kubernetes 中进行复制,我正在研究有关锁的 Redis 文档以防止客户端副本之间的竞争。
在谷歌搜索和阅读之后,我关注了这两个资源:
- 此处描述的
SETNX
命令:https://redis.io/commands/setnx - 此处描述的 Redlock 算法:https://redis.io/topics/distlock
有趣的是 SETNX
文档明确建议不要使用 SETNX
来实现锁,声明它基本上已经过时了:
The following pattern is discouraged in favor of the Redlock algorithm [...]
We document the old pattern anyway because certain existing implementations link to this page as a reference.
然而,Redlock 算法是专门为 分布式 锁量身定制的,因此当一个人试图锁定多个 Redis 实例时 - 它们实际上指的是多个 masters.
更进一步,库 redsync (golang) 声明 New
函数如下:
func New(pools []Pool) *Redsync {
return &Redsync{
pools: pools,
}
}
它看起来毫无疑问是为支持 Redis 集群上的锁定而设计的。
在我的用例中,我将只连接到一个 Redis 实例。
也许我可以只使用 redsync 包并传递一个长度为 1 的切片,但对我来说 SETNX
模式在单个 Redis 实例上同样可以正常工作。
我没看错吗? 谢谢
是的,Redlock 算法确实是为分布式 Redis 系统设计的,如果您使用的是单个实例,那么使用 SET and SETNX 文档中描述的更简单的锁定方法应该没问题.
然而,更重要的一点是:您可能不需要使用锁来避免多个 Redis 客户端之间的冲突。 Redis 锁通常用于保护某些 外部 分布式资源(请参阅我的回答
所以我的建议是设计您的客户端代码以使用原子性来避免冲突,而不是尝试使用锁(分布式或其他方式)。
我想我可以post一个答案供参考。按照 Kevin 的建议,我最终使用 Lua 脚本来确保原子性。
这是实现 (Go) 的样子:
// import "github.com/gomodule/redigo/redis"
type redisService struct {
pool *redis.Pool
lastLogin *redis.Script // Lua script initialized into this field
}
// Constructing a redis client
func NewRedisService(config *config.RedisClientConfig) RedisService {
return &redisService{
pool: &redis.Pool{
MaxIdle: 10,
IdleTimeout: 120 * time.Second,
Dial: func() (redis.Conn, error) {
return redis.Dial("tcp", config.BaseURL)
},
TestOnBorrow: func(c redis.Conn, t time.Time) error {
if time.Since(t) < time.Minute {
return nil
}
_, err := c.Do("PING")
return err
},
},
// initialize Lua script object
// lastLoginLuaScript is a Go const with the script literal
lastLogin: redis.NewScript(1, lastLoginLuaScript),
}
}
Lua 脚本(注释解释了它的作用):
--[[
Check if key exists, if it exists, update the value without changing the remaining TTL.
If it doesn't exist, create it.
Script params
KEYS[1] = the account id used as key
ARGV[1] = the key TTL in seconds
ARGV[2] = the value
]]--
local errorKeyExpired = 'KEXP'
local statusKeyUpdated = 'KUPD'
local statusKeyCreated = 'KCRE'
if redis.call('exists', KEYS[1]) == 1 then
local ttl = redis.call('ttl', KEYS[1])
if ttl < 0 then --[[ no expiry ]]--
redis.call('setex', KEYS[1], ARGV[1], ARGV[2])
return redis.status_reply(statusKeyCreated)
end
if ttl == 0 then --[[ expired ]]--
return redis.error_reply(errorKeyExpired)
end
redis.call('setex', KEYS[1], ttl, ARGV[2])
return redis.status_reply(statusKeyUpdated)
else
redis.call('setex', KEYS[1], ARGV[1], ARGV[2])
return redis.status_reply(statusKeyCreated)
end
用法:
func (rs *redisService) UpsertLastLoginTime(key string, ttl uint, value int64) (bool, error) {
conn := rs.pool.Get()
defer conn.Close()
// call Do on the script object
resp, err := rs.lastLogin.Do(conn, key, ttl, value)
switch resp {
case statusKeyCreated:
return true, nil
case statusKeyUpdated:
return false, nil
case errorKeyExpired:
return false, ErrKeyExpired
default:
return false, errors.Wrap(err, "script execution error")
}
}