Simplified distributed locking implementation using Redis

GoDoc

分布式锁使用示例:

 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
import (
  "context"
  "fmt"
  "log"
  "time"

  "github.com/bsm/redislock"
  "github.com/redis/go-redis/v9"
)

func main() {
	// Connect to redis.
	client := redis.NewClient(&redis.Options{
		Network:	"tcp",
		Addr:		"127.0.0.1:6379",
	})
	defer client.Close()

	// Create a new lock client.
	locker := redislock.New(client)

	ctx := context.Background()

	// Try to obtain lock.
	lock, err := locker.Obtain(ctx, "my-key", 100*time.Millisecond, nil)
	if err == redislock.ErrNotObtained {
		fmt.Println("Could not obtain lock!")
	} else if err != nil {
		log.Fatalln(err)
	}

	// Don't forget to defer Release.
	defer lock.Release(ctx)
	fmt.Println("I have a lock!")

	// Sleep and check the remaining TTL.
	time.Sleep(50 * time.Millisecond)
	if ttl, err := lock.TTL(ctx); err != nil {
		log.Fatalln(err)
	} else if ttl > 0 {
		fmt.Println("Yay, I still have my lock!")
	}

	// Extend my lock.
	if err := lock.Refresh(ctx, 100*time.Millisecond, nil); err != nil {
		log.Fatalln(err)
	}

	// Sleep a little longer, then check.
	time.Sleep(100 * time.Millisecond)
	if ttl, err := lock.TTL(ctx); err != nil {
		log.Fatalln(err)
	} else if ttl == 0 {
		fmt.Println("Now, my lock has expired!")
	}

}

获取锁

  1. 生成一个Token, 作为标识符, 添加metadata信息, 辅助后期debug

  2. 设置ttl, 重试策略

  • 最大重试次数

  • 指数退避算法

  • 自定义间隔重试

策略实现:

https://pkg.go.dev/github.com/bsm/redislock#RetryStrategy

检查是否已经设置了最大超时时间, 如果没有设置, 默认使用ttl作为超时时间

不断尝试获取锁, 如果没有获取, 根据重试策略直接进行重试, 或超时返回

设置锁

使用obtain.lua进行尝试加锁, 使用 msetnx 和 mset 操作

获取TTL

使用pttl.lua进行最小ttl的获取

刷新TTL

使用refresh.lua更新所有的key的ttl

释放

调用release.lua删除所有占用的key

  • 注意 Redis: msetnx

    当且仅当给定的所有键都不存在时, 为所有的键设定值

    只要有一个键存在, 则拒绝所有操作, 并返回 0

    即: 要么全部设置, 要么全部不设置

Redislock源码分析

重试策略

  1. 最大重试 LimitRetry

最大重试次数示例,重试3次,每次间隔100ms

1
2
3
4
5
6
7
// Retry every 100ms, for up-to 3x
backoff := redislock.LimitRetry(redislock.LinearBackoff(100*time.Millisecond), 3)

// Obtain lock with retry
lock, err := locker.Obtain(ctx, "my-key", time.Second, &redislock.Options{
	RetryStrategy: backoff,
})
  1. 指数退避算法 ExponentialBackoff

ExponentialBackoff strategy is an optimization strategy with a retry time of 2**n milliseconds (n means number of times). You can set a minimum and maximum value, the recommended minimum value is not less than 16ms.

1
2
3
4
5
6
7
// Retry every 2**4ms, for up-to a 2**10ms
backoff := redislock.ExponentialBackoff(16 * time.Millisecond, 1024 * time.Millisecond)

// Obtain lock with retry
lock, err := locker.Obtain(ctx, "my-key", time.Second, &redislock.Options{
	RetryStrategy: backoff,
})
  1. 自定义间隔重试 LinearBackoff

LinearBackoff allows retries regularly with customized intervals

自定义重试1分钟,重试间隔500ms

1
2
3
4
5
6
7
8
9
// Retry every 500ms, for up-to a minute
backoff := redislock.LinearBackoff(500 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(time.Minute))
defer cancel()

// Obtain lock with retry + custom deadline
lock, err := locker.Obtain(ctx, "my-key", time.Second, &redislock.Options{
	RetryStrategy: backoff,
})

NoRetry acquire the lock only once.

1
2
3
4
// NoRetry acquire the lock only once.
func NoRetry() RetryStrategy {
	return linearBackoff(0)
}