Go-Redis 实现分布式锁(WatchDog机制)

一、分布式锁基础知识

锁,代表该变量在同一时刻只能有一个线程拥有,以便保护共享数据。分布式锁也是锁的一种,用于解决在分布式系统中对共享资源的访问。

二、分布式锁常见场景

  1. 库存扣减
  2. 缓存击穿、缓存雪崩
  3. 在高并发场景下阻止流量打到后端

三、Go 结合 redis 实现带看门狗机制的分布式锁

  1. 使用 lua 脚本确保解锁和续期的原子性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const (
// 解锁用的 lua 脚本,确保解锁的原子性
unlockScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
// 看门狗用于自动续期的 lua 脚本
watchDogScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("expire", KEYS[1], ARGV[2])
else
return 0
end
`
)
  1. redis 锁结构体
1
2
3
4
5
6
7
8
9
10
type RedisLock struct {
client *redis.Client // redis客户端
ctx context.Context // 上下文
cancel context.CancelFunc // 取消上下文函数
key string
val string
status bool // 是否上锁的标志
expiration time.Duration // 过期时间
waitTime time.Duration // 等待时间
}

其中 client 保存 redis 客户端,cancel 为取消上下文的函数,用于在解锁的时候通知看门狗协程退出,避免锁一直不被释放,status 代表是否上锁的标志,expiration 代表单次锁的有效期,waitTime 代表上锁超时时间,当上锁超时的时候直接报上锁失败的错误

  1. 上锁的方法:因为使用了看门狗机制,所以在上锁之后,需要启动一个 watchDog 协程来进行锁续期,来处理上锁后业务执行超时的时候,锁到期自动释放的情况。
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
// 尝试上锁
func (lock *RedisLock) TryLock() (bool, error) {
result, err := lock.Lock()
if err != nil {
return false, err
}
if result {
lock.status = true
go lock.WatchDog()
}
return result, nil
}

// 上锁
func (lock *RedisLock) Lock() (bool, error) {
now := time.Now()
for time.Since(now) < lock.waitTime {
result, err := lock.client.SetNX(lock.ctx, lock.key, lock.val, lock.expiration).Result()
fmt.Println(result, err)
if err != nil {
return false, err
}
if result {
lock.status = true
return true, nil
}
time.Sleep(time.Second)
}
// 锁等待超时,上锁失败
return false, nil
}

看门狗协程:单独起一个协程实时监听锁是否超时(通过定时器实现)并监听退出信号,在上锁时间为过期时间的一半的时候主动为锁续期

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 看门狗 - 锁续期
func (lock *RedisLock) WatchDog() {
loopTime := lock.expiration / 2 // 续期间隔为过期时间的一半
expiredTicker := time.NewTicker(loopTime)
defer expiredTicker.Stop()

for {
select {
case <-lock.ctx.Done():
// 接收到取消信号,退出
return
case <-expiredTicker.C:
// 尝试续期
_, err := lock.client.Eval(lock.ctx, watchDogScript, []string{lock.key}, lock.val, int(lock.expiration/time.Second)).Result()
if err != nil {
log.Println("Failed to extend lock:", err)
return
}
}
}
}
  1. 解锁:使用 lua 脚本解锁,确保原子性,并在解锁成功后通知看门狗协程退出,避免锁无法释放
1
2
3
4
5
6
7
8
9
10
11
12
13
// 解锁
func (lock *RedisLock) UnLock() (bool, error) {
if lock.status {
result, err := lock.client.Eval(lock.ctx, unlockScript, []string{lock.key}, lock.val).Result()
if err != nil {
return false, err
}
lock.status = false
lock.cancel() // 取消上下文,停止看门狗
return result.(int64) == 1, nil
}
return true, nil
}

完整的代码如下:

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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package main

import (
"context"
"github.com/go-redis/redis/v8"
"github.com/google/uuid"
"log"
"time"
)

const (
// 解锁用的 lua 脚本,确保解锁的原子性
unlockScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
// 看门狗用于自动续期的 lua 脚本
watchDogScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("expire", KEYS[1], ARGV[2])
else
return 0
end
`
)

type RedisLock struct {
client *redis.Client // redis客户端
ctx context.Context // 上下文
cancel context.CancelFunc // 取消上下文函数
key string
val string
status bool // 是否上锁的标志
expiration time.Duration // 过期时间
waitTime time.Duration // 等待时间
}

func NewRedisLock(ctx context.Context, client *redis.Client, key string, expiration time.Duration) *RedisLock {
ctx, cancel := context.WithCancel(ctx)
return &RedisLock{
ctx: ctx,
client: client,
cancel: cancel,
key: key,
val: time.Now().Format("20060102150405") + "_" + uuid.New().String(), // 生成唯一标识符
expiration: expiration,
waitTime: time.Second * 5,
}
}

// 尝试上锁
func (lock *RedisLock) TryLock() (bool, error) {
result, err := lock.Lock()
if err != nil {
return false, err
}
if result {
lock.status = true
go lock.WatchDog()
}
return result, nil
}

// 上锁
func (lock *RedisLock) Lock() (bool, error) {
now := time.Now()
for time.Since(now) < lock.waitTime {
result, err := lock.client.SetNX(lock.ctx, lock.key, lock.val, lock.expiration).Result()
if err != nil {
return false, err
}
if result {
lock.status = true
return true, nil
}
time.Sleep(time.Second)
}
// 锁等待超时,上锁失败
return false, nil
}

// 解锁
func (lock *RedisLock) UnLock() (bool, error) {
if lock.status {
result, err := lock.client.Eval(lock.ctx, unlockScript, []string{lock.key}, lock.val).Result()
if err != nil {
return false, err
}
lock.status = false
lock.cancel() // 取消上下文,停止看门狗
return result.(int64) == 1, nil
}
return true, nil
}

// 看门狗 - 锁续期
func (lock *RedisLock) WatchDog() {
loopTime := lock.expiration / 2 // 续期间隔为过期时间的一半
expiredTicker := time.NewTicker(loopTime)
defer expiredTicker.Stop()

for {
select {
case <-lock.ctx.Done():
// 接收到取消信号,退出
return
case <-expiredTicker.C:
// 尝试续期
log.Println("Extending lock...")
_, err := lock.client.Eval(lock.ctx, watchDogScript, []string{lock.key}, lock.val, int(lock.expiration/time.Second)).Result()
if err != nil {
log.Println("Failed to extend lock:", err)
return
}
}
}
}

func main() {
// 连接 redis
client := redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "", // 无密码
DB: 0, // 默认数据库
})
defer client.Close()

// 创建分布式锁
ctx := context.Background()
lock := NewRedisLock(ctx, client, "my_lock", time.Second*10)

// 加锁
ok, err := lock.TryLock()
if err != nil {
log.Fatalf("Failed to acquire lock: %v", err)
}
if !ok {
log.Println("Failed to acquire lock: lock is already held")
return
}
log.Println("Lock acquired")

// 模拟业务逻辑
time.Sleep(9 * time.Second)

// 解锁
ok, err = lock.UnLock()
if err != nil {
log.Fatalf("Failed to release lock: %v", err)
}
if !ok {
log.Println("Failed to release lock: lock is not held")
}
log.Println("Lock released")

}

四、验证

运行代码,设置锁超时时间为 10S,模拟的业务逻辑运行时间为 9S,9S 后自动解锁,业务逻辑执行到第 5S 的时候,看门狗协程自动续期

控制台输出的日志如下:

1
2
3
4
5
6
7
8
9
10
GOROOT=/usr/local/go #gosetup
GOPATH=/Users/wu/go #gosetup
/usr/local/go/bin/go build -o /Users/wu/Library/Caches/JetBrains/GoLand2023.1/tmp/GoLand/___go_build_learn_redis_lock_main learn/redis-lock/main #gosetup
/Users/wu/Library/Caches/JetBrains/GoLand2023.1/tmp/GoLand/___go_build_learn_redis_lock_main
2025/05/15 15:10:18 Lock acquired
2025/05/15 15:10:23 Extending lock...
2025/05/15 15:10:27 Lock released

Process finished with the exit code 0


Go-Redis 实现分布式锁(WatchDog机制)
https://wuwanhao.github.io/2025/05/15/Go-Redis 实现分布式锁(WatchDog机制)/
作者
Wuuu
发布于
2025年5月15日
许可协议