首页 > 编程知识 正文

php redis set setnx谈谈Redis中的SetNX

时间:2023-05-06 11:18:11 阅读:265439 作者:398

谈SetNX命令前,先顺带引入下Set命令,由于在Golang开启两个并发协程后,单位时间内读到的有可能是同一个值,因此这对本来就是单线程并发安全的Redis造成了非并发安全的错觉。如下代码所示:

Go

func main() {

config:=kv.Config{

Host: "192.168.0.125",

Port: "6379",

Password: "",

DB: 0,

}

client1:=kv.NewRedisClient(config)

client2:=kv.NewRedisClient(config)

// 开两条协程,并发向 Redis 写入数据

go WriteToRedis(client1)

go WriteToRedis(client2)

// 写入的结果小于 2000

select {

}

}

// WriteToRedis 向 Redis Key 中写入数据

func WriteToRedis(client kv.Client) {

for i:=0;i<1000;i++ {

// 判断 key 是否存在 ,不存在则初始化为1

if !client.Exists("test"){

err:=client.SetString("test","1",time.Hour)

if err!=nil {

panic(err)

}

continue

}

// 存在则先获取

test,err:=client.GetString("test")

if err!=nil {

panic(err)

}

// 转换为整型

num,err:=strconv.Atoi(test)

if err!=nil {

panic(err)

}

num+=1

err=client.SetString("test",strconv.Itoa(num),time.Hour)

if err!=nil {

panic(err)

}

}

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

funcmain(){

config:=kv.Config{

Host:"192.168.0.125",

Port:"6379",

Password:"",

DB:0,

}

client1:=kv.NewRedisClient(config)

client2:=kv.NewRedisClient(config)

// 开两条协程,并发向 Redis 写入数据

goWriteToRedis(client1)

goWriteToRedis(client2)

// 写入的结果小于 2000

select{

}

}

// WriteToRedis 向 Redis Key 中写入数据

funcWriteToRedis(clientkv.Client){

fori:=0;i<1000;i++{

// 判断 key 是否存在 ,不存在则初始化为1

if!client.Exists("test"){

err:=client.SetString("test","1",time.Hour)

iferr!=nil{

panic(err)

}

continue

}

// 存在则先获取

test,err:=client.GetString("test")

iferr!=nil{

panic(err)

}

// 转换为整型

num,err:=strconv.Atoi(test)

iferr!=nil{

panic(err)

}

num+=1

err=client.SetString("test",strconv.Itoa(num),time.Hour)

iferr!=nil{

panic(err)

}

}

}

其实redis本身是并发安全的。只是单位时间有两个协程同时读到了一样的值

在 Redis 里,所谓 SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

当我们在两个终端执行命令:

set lock 123 nx ex 60

如下所示:

有且只有左边的终端执行成功了, setnx 命令是原子性的,因此在并发情况下,如果某个进程或者线程在设置成功了某个值,那么就代表它获取到了锁。并且执行完自己的代码后删除该 KEY 即可。

但这样真的规范吗?

在上面的命令我设置了锁的缓存时间为 60 秒

假设我的A进程获取到了锁并开始执行业务逻辑,但由于业务繁重,执行时长超过了锁设置的缓存时间,那么其实下一个进程 B 早已获取到了该锁。然而我的A进程才执行完就误删了进程B的锁。导致C 进程也获取到了锁。

因此这种方式是不规范的。

因此我们要在创建锁的时候引入一个随机值:

Go

set lock 随机值 nx ex 过期时间秒

1

setlock随机值nxex过期时间秒

当A进程获取到锁的时候顺带设置这个随机值,并且当A进程结束进程后,取出lock这个key中的随机值,看看是否是自己设置的那个,如果是则删除,如果不是就略过。这样就避免了误删的情况。

上一段 PHP 伪代码

Go

$ok = $redis->set($key, $random, array('nx', 'ex' => $ttl));

if ($ok) {

// 具体业务逻辑的一堆代码 ......

// ....... 省略

if ($redis->get($key) == $random) {

$redis->del($key);

}

}

?>

1

2

3

4

5

6

7

8

9

10

11

12

13

$ok=$redis->set($key,$random,array('nx','ex'=>$ttl));

if($ok){

// 具体业务逻辑的一堆代码 ......

// ....... 省略

if($redis->get($key)==$random){

$redis->del($key);

}

}

?>

补充:本文在删除锁的时候,实际上是有问题的,没有考虑到 GC pause 之类的问题造成的影响,比如 A 请求在 DEL 之前卡住了,然后锁过期了,这时候 B 请求又成功获取到了锁,此时 A 请求缓过来了,就会 DEL 掉 B 请求创建的锁,此问题远比想象的要复杂,具体解决方案参见本文最后关于锁的若干个参考链接。

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。