谈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 请求创建的锁,此问题远比想象的要复杂,具体解决方案参见本文最后关于锁的若干个参考链接。
53429709