谈SetNX命令前,先顺带引入下Set命令,由于在Golang开启两个并发协程后,单位时间内读到的有可能是同一个值,因此这对本来就是单线程并发安全的Redis造成了非并发安全的错觉。如下代码所示:
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 |
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) } } } |
其实redis本身是并发安全的。只是单位时间有两个协程同时读到了一样的值
下面引入SETNX
在 Redis 里,所谓 SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。
当我们在两个终端执行命令:
set lock 123 nx ex 60
如下所示:

有且只有左边的终端执行成功了, setnx 命令是原子性的,因此在并发情况下,如果某个进程或者线程在设置成功了某个值,那么就代表它获取到了锁。并且执行完自己的代码后删除该 KEY 即可。
但这样真的规范吗?
在上面的命令我设置了锁的缓存时间为 60 秒
假设我的A进程获取到了锁并开始执行业务逻辑,但由于业务繁重,执行时长超过了锁设置的缓存时间,那么其实下一个进程 B 早已获取到了该锁。然而我的A进程才执行完就误删了进程B的锁。导致C 进程也获取到了锁。
因此这种方式是不规范的。
因此我们要在创建锁的时候引入一个随机值:
1 |
set lock 随机值 nx ex 过期时间秒 |
当A进程获取到锁的时候顺带设置这个随机值,并且当A进程结束进程后,取出lock这个key中的随机值,看看是否是自己设置的那个,如果是则删除,如果不是就略过。这样就避免了误删的情况。
上一段 PHP 伪代码
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php $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 请求创建的锁,此问题远比想象的要复杂,具体解决方案参见本文最后关于锁的若干个参考链接。
如此基本实现了单机锁,假如要实现分布锁,请参考:Distributed locks with Redis,不过分布式锁需要注意的地方更多:How to do distributed locking,Is Redlock safe。此外,还有中文版:基于Redis的分布式锁到底安全吗(上/下)。
文章评论(0)