redis 实现分布式锁
环境准备
- SETNX(SET if Not eXists 如果不存在,则 SET)命令
setnx key value,只有当key不存在的情况下,将key设置为value;若key存在,不做任何操作,结果成功返回1,失败返回0 - SETEX 命令
setex key seconds value:将key值设置为value,并将设置key的生存周期 - SET 命令
SET {key} {value} [ex seconds] [px milliseconds] [nx|xx] - ex seconds 设置秒级别的过期时间 - px milliseconds 设置毫秒级别的过期时间 - nx 键必须不存在,才可以设置成功,用于添加 - xx 键必须存在,才可以设置成功,用于更新
- docker 启动一个 redis 实例
docker run --name redis1 -p 6679:6379 -d redis redis-server --appendonly yes
redis 是单实例
实现分布式锁需要的很早期命令SETNX
,这个命令表示 SET If Not eXists,即如果 key 不存在,才会设置它的值,否则什么也不做。
两个客户端进程可以执行这个命令,达到互斥,就可以实现一个分布式锁。
启动一个redis-cli1
# redis-cli -h 127.0.0.1 -p 6679
127.0.0.1:6679> SETNX lock 1
(integer) 1
再启动另一个redis-cli2:
redis-cli -h 127.0.0.1 -p 6679
127.0.0.1:6679> SETNX lock 1
(integer) 0
127.0.0.1:6679> get lock
"1"
redis-cli1 使用del 释放锁
redis-cli2 获取锁:
上面的实现已经基本实现了互斥的功能,但还不够,试想:假设当客户端1拿到锁后,进程挂了,那么就会导致这个客户端一直占用这个锁,其他客户端也就永远拿不到这个锁了。
那想说,这样也好办,再使用命令给这个锁设置一个过期时间:
127.0.0.1:6679> setex lock 10 1
OK
127.0.0.1:6679> ttl lock
(integer) 6
这样,无论客户端是否异常,这个锁都可以在一定时间后被自动释放,其它客户端依旧可以拿到锁。
但就是如此,还是会遇到2个问题:
- 锁过期时间怎么确定?
假设客户端 1 操作共享资源需要耗时很长,那可能还没有工作做完,就导致锁被自动释放。
这个问题,目前没有好的解决办法,只好业务层去评估合适的过期时间。 - 释放别人的锁
锁过期发生后,锁被客户端2持有,这时客户端 1 释放了客户端 2 持有的锁
这种问题本质在于每个客户端在释放锁时,并没有检查这把锁是否还归自己持有,所以就会发生释放别人锁的风险。为了解决这一问题,客户端在加锁时,可以设置一个唯一标识 进去,可以是自己的线程 ID,也可以是一个 UUID。
Demo:
package main
import (
"fmt"
"github.com/go-redis/redis"
"sync"
"time"
)
var lockKey = "counter_lock"
var counterKey = "counter"
func incr(grNum int) {
client := redis.NewClient(&redis.Options{
Addr: "192.168.48.139:6679",
Password: "",
DB: 0,
})
// lock
resp := client.SetNX(lockKey, 1, time.Second*5)
lockSuccess, err := resp.Result()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(grNum, "lock result: ", lockSuccess)
if !lockSuccess {
return
}
// counter++
getResp := client.Get(counterKey)
cntValue, err := getResp.Int64()
if err == nil {
cntValue++
resp := client.Set(counterKey, cntValue, 0)
_, err := resp.Result()
if err != nil {
fmt.Println(grNum, "set value error", err.Error())
}
} else {
fmt.Println(grNum, "get value error", err.Error())
}
fmt.Println(grNum, "current counter is ", cntValue)
// unlock
delResp := client.Del(lockKey)
unlockSuccess, err := delResp.Result()
if err == nil && unlockSuccess > 0 {
fmt.Println(grNum, "unlock success")
} else {
fmt.Println(grNum, "unlock failed", err)
}
}
func main() {
client := redis.NewClient(&redis.Options{
Addr: "192.168.48.139:6679",
Password: "",
DB: 0,
})
resp := client.Set(counterKey, 0, 0)
_, err := resp.Result()
if err != nil {
fmt.Println("set value error", err.Error())
return
}
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
time.Sleep(time.Microsecond * 100)
wg.Add(1)
go func(grNum int) {
defer wg.Done()
incr(grNum)
}(i)
}
wg.Wait()
}
输出:

redis 是多实例
上面分析的场景都是,锁在单个Redis 实例中可能产生的问题,而实际在使用 Redis 时,一般会采用 主从集群 + 哨兵的模式部署,用来保证可用性。
那当主从发生切换时,这个分布式锁会依旧安全吗?
想想这样的场景:客户端 1 在主库上执行 SET 命令,加锁成功,此时,主库异常宕机,SET
命令还未同步到从库上(主从复制是异步的)从库被哨兵提升为新主库,这时锁在新的主库上就丢失了。
为了解决这一问题
TODO
zookeeper 实现分布式锁
TODO
zookeeper 的锁基于Redis 锁不同之处在于Lock成功之前会一直阻塞,这与单价的 mutex.Lock 行为相似。
这种分布式阻塞锁适合分布式任务调度场景,但不适合高频次持锁时间段的抢锁场景。
etcd 实现分布式锁
TODO
总结:如何挑选合适的分布式锁?
在业务规模不大,QPS很小的情况下,使用哪种分布式锁都差不多,如果公司内已有可以使用的zookeeper,etcd 或者 redis 集群,那就尽量在不引入新技术栈的情况下满足业务需求。
参考资料
1、
网友评论