forked from syafdia/go-exercise
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathredlock.go
137 lines (110 loc) · 2.57 KB
/
redlock.go
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
package demoredlock
import (
"context"
"errors"
"math/rand"
"time"
redis "github.com/go-redis/redis/v8"
)
var (
ErrAcquireResource = errors.New("locker: failed acquiring resource")
ErrReleaseResource = errors.New("locker: failed releasing resource")
)
var scriptLock = `
if redis.call("EXISTS", KEYS[1]) == 1 then
return 0
end
return redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2])
`
var scriptUnlock = `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
`
type Locker interface {
Lock(ctx context.Context) error
Unlock(ctx context.Context) error
}
type DLM struct {
redisClients []*redis.Client
quorum int
expiration time.Duration
drift time.Duration
}
func NewDLM(redisClients []*redis.Client, expiration time.Duration, drift time.Duration) *DLM {
return &DLM{
redisClients: redisClients,
expiration: expiration,
drift: drift,
quorum: len(redisClients)/2 + 1,
}
}
func (dlm *DLM) NewLocker(name string) Locker {
return newLocker(dlm, name)
}
type locker struct {
redisClients []*redis.Client
expiration time.Duration
drift time.Duration
quorum int
name string
value string
}
func newLocker(dlm *DLM, name string) Locker {
return &locker{
redisClients: dlm.redisClients,
quorum: dlm.quorum,
name: name,
value: generateRandomString(),
expiration: dlm.expiration,
drift: dlm.drift,
}
}
func (l *locker) Lock(ctx context.Context) error {
totalSuccess := 0
for _, rc := range l.redisClients {
start := time.Now()
status, err := rc.Eval(ctx, scriptLock, []string{l.name}, l.value, l.expiration.Milliseconds()).Result()
if err != nil {
return err
}
ok := status == "OK"
now := time.Now()
isTimeValid := (l.expiration - (now.Sub(start) - l.drift)) > 0
if ok && isTimeValid {
totalSuccess++
}
}
if totalSuccess < l.quorum {
l.Unlock(ctx)
return ErrAcquireResource
}
return nil
}
func (l *locker) Unlock(ctx context.Context) error {
totalSuccess := 0
for _, rc := range l.redisClients {
status, err := rc.Eval(ctx, scriptUnlock, []string{l.name}, l.value).Result()
if err != nil {
return err
}
if status != int64(0) {
totalSuccess++
}
}
if totalSuccess < l.quorum {
return ErrReleaseResource
}
return nil
}
func generateRandomString() string {
var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
b := make([]rune,
time.Now().Unix()%64)
for i := range b {
b[i] = letterRunes[rand.Intn(len(letterRunes))]
}
return string(b)
}