1、redis中,setnx 和 set nx 以及普通set的区别

0 阅读3分钟

初次见到这道题题时候感觉很奇怪,心里默想这两家伙不是一样的吗,但是既然这样问,以及有这样的题目,那就让我们来了解一下他吧。

寻找了答案之后是这样回答的

在 Redis 中,SET NX 和 SETNX 的功能是相同的,都用于在键不存在时设置键值对,但它们存在以下细微区别:

1、命令格式

  • SETNX key value
    这是一个原子命令,格式固定为 SETNX key value,不可添加其他参数。
  • SET key value NX
    这是 SET 命令的扩展用法,通过添加 NX 选项来实现与 SETNX 相同的效果。SET 命令还支持其他选项(如 EXPXXX 等),使其功能更灵活。

2、原子性

两者都是原子操作,在分布式环境中不会出现竞态条件。例如:

SETNX mykey "value" # 原子操作,等价于: 
SET mykey "value" NX # 同样是原子操作

3、返回值

  • SETNX

    • 成功(键不存在):返回 1
    • 失败(键已存在):返回 0
  • SET key value NX

    • 成功(键不存在):返回 OK
    • 失败(键已存在):返回 nil

4、兼容性

  • SETNX 是 Redis 早期版本就支持的命令,所有 Redis 版本都兼容。
  • SET ... NX 是在 Redis 2.6.12 版本后引入的扩展语法,老版本可能不支持。

5、github.com/redis/go-redis/v9包小探索

在此包中只有SETNX方法,但是当我们进入源码中可以发现SETNX将redis中得set nx 和 setnx 合二为一了:

// SetNX Redis `SET key value [expiration] NX` command.
//
// Zero expiration means the key has no expiration time.
// KeepTTL is a Redis KEEPTTL option to keep existing TTL, it requires your redis-server version >= 6.0,
// otherwise you will receive an error: (error) ERR syntax error.
func (c cmdable) SetNX(ctx context.Context, key string, value interface{}, expiration time.Duration) *BoolCmd {
    var cmd *BoolCmd
    switch expiration {
    case 0:
       // Use old `SETNX` to support old Redis versions.
       cmd = NewBoolCmd(ctx, "setnx", key, value)
    case KeepTTL:
       cmd = NewBoolCmd(ctx, "set", key, value, "keepttl", "nx")
    default:
       if usePrecise(expiration) {
          cmd = NewBoolCmd(ctx, "set", key, value, "px", formatMs(ctx, expiration), "nx")
       } else {
          cmd = NewBoolCmd(ctx, "set", key, value, "ex", formatSec(ctx, expiration), "nx")
       }
    }

    _ = c(ctx, cmd)
    return cmd
}

const KeepTTL = -1

func usePrecise(dur time.Duration) bool {
	return dur < time.Second || dur%time.Second != 0
}

1、过期时间为 0 :永远有效

2、过期时间为 -1:可用于续期

分布式锁续期:在延长锁的持有时间时不改变锁的值。

业务场景

  • 当持有锁的业务执行失败需要retry时候,在释放锁和重新加锁时候,在检查锁归属和延长时间之间,锁可能已过期并被其他客户端获取(竞态条件)。使用它可以原子性续期
// 原子操作:仅当锁存在且值未变时延长锁时间
ok, _ := client.Set(ctx, "lock:resource", "my-value", "NX", "KEEPTTL").Result()
if ok {
    // 锁续期成功,继续执行临界区代码
} else {
    // 锁已过期或被其他客户端获取,需重新竞争锁
}

3、自定义过期时间:default分支,区分了毫秒级和秒级的过期时间。

6、多一嘴 过期时间为 -1

在普通的set中:过期时间为 -1时候、键存在可以用于刷新值而不改变过期时间。(用户信息刷新),不存在则创建永久的key

// 原子操作:仅当锁存在时续期(等价于原代码中的 SetNX + KeepTTL) 
client.Set(ctx, "lock:resource", "my-value", "NX", redis.KeepTTL).Result()

为了避免误产生永久key

// Lua 脚本:仅当键存在时更新值并保留 TTL 
script := ` if redis.call("EXISTS", KEYS[1]) == 1 then return redis.call("SET", KEYS[1], ARGV[1], "KEEPTTL") else return 0 end ` 
result, _ := client.Eval(ctx, script, []string{"user:1"}, "new-value").Result() 
if result == "OK" { // 更新成功 } else { 
// 键不存在,需创建 
    client.Set(ctx, "user:1", "new-value", 60*time.Second) }

总结

在使用分布式锁中,要注意时间的设置,否则可能会造成死锁。