为了账号安全,请及时绑定邮箱和手机立即绑定

使用SETNX的单实例Redis锁

使用SETNX的单实例Redis锁

Go
慕尼黑5688855 2023-06-26 15:35:22
我需要从应用程序客户端连接到单个 Redis 实例。由于客户端将在 Kubernetes 中进行复制,因此我正在研究有关锁的 Redis 文档,以防止客户端副本之间的竞争。有趣的是,SETNX文档明确建议不要使用SETNX锁来实现,并指出它基本上已经过时了:为了支持 Redlock 算法,不鼓励使用以下模式 [...]无论如何,我们都会记录旧模式,因为某些现有实现链接到此页面作为参考。然而,Redlock 算法是专门为分布式锁量身定制的,因此当一个人试图锁定多个 Redis 实例时,它们实际上是指多个主实例。更进一步,库redsync (golang) 声明该New函数如下:func New(pools []Pool) *Redsync {    return &Redsync{        pools: pools,    }}它看起来毫无疑问是为了支持 Redis 集群上的锁定而设计的。在我的用例中,我将仅连接到一个 Redis 实例。也许我可以只使用 redsync 包并传递长度为 1 的切片,但对我来说,该SETNX模式似乎可以在单个 Redis 实例上同样正常工作。我的看法正确吗?谢谢
查看完整描述

2 回答

?
慕无忌1623718

TA贡献1744条经验 获得超4个赞

是的,Redlock 算法确实是为分布式 Redis 系统设计的,如果您使用单个实例,那么使用 SET 和 SETNX 文档中描述的更简单的锁定方法应该没问题。

然而,更重要的一点是:您可能不需要使用锁来避免多个 Redis 客户端之间的冲突。Redis 锁通常用于保护某些外部分布式资源(有关此问题的更多信息,请参阅我的回答。Redis 本身通常不需要锁;由于 Redis 的单线程特性,许多命令已经是原子的,您可以使用事务或 Lua 脚本来组成任意复杂的原子操作。

因此,我的建议是设计您的客户端代码以使用原子性来避免冲突,而不是尝试使用锁(分布式锁或其他锁)。


查看完整回答
反对 回复 2023-06-26
?
慕仙森

TA贡献1827条经验 获得超7个赞

实现 (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")

    }

}


查看完整回答
反对 回复 2023-06-26
  • 2 回答
  • 0 关注
  • 121 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信