使用 SETNX 的单实例 Redis 锁

Single-instance Redis lock with SETNX

我需要从应用程序客户端连接到单个 Redis 实例。

由于客户端将在 Kubernetes 中进行复制,我正在研究有关锁的 Redis 文档以防止客户端副本之间的竞争。

在谷歌搜索和阅读之后,我关注了这两个资源:

有趣的是 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")
    }
}