Skip to content

Commit e75f4b9

Browse files
committed
Replicalock first version.
1 parent 9d30073 commit e75f4b9

File tree

4 files changed

+396
-1
lines changed

4 files changed

+396
-1
lines changed

README.md

+34-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,35 @@
11
# Redis-ReplicaLock
2-
A distributed lock idea implemented by redis and displayed in go language.It is more secure than a simple redis distributed lock.
2+
A distributed lock idea implemented by redis and displayed in go language.It is more secure than a simple redis distributed lock.It is still in the experimental stage.<br>
3+
4+
Welcome to testing ReplicaLock:
5+
```
6+
go get github.com/ncghost1/Redis-ReplicaLock
7+
```
8+
9+
The implementation is almost the same as that of RedissonLock.But ReplicaLock uses the "WAIT" command to wait for all replicas to synchronize.When all replicas have completed writing the lock key, we think we got the lock.<br>
10+
11+
An obvious problem about ReplicaLock: lock acquisition may fail due to network delay between master and slave.<br>
12+
13+
This is to prevent lock loss after failover because the replicas does not complete synchronization with the master node.
14+
15+
A simple example of using ReplicaLock:
16+
```
17+
func main() {
18+
// We use "redigo" to connect to redis
19+
Conn, err := redis.Dial("tcp", ":6379")
20+
if err != nil {
21+
panic(err)
22+
}
23+
Replock, err := ReplicaLock.New(Conn)
24+
if err != nil {
25+
panic(err)
26+
}
27+
28+
// 'WAIT' Command 'timeout' is 1s, ReplicaLock's lease time is 30s.
29+
err = Replock.Lock(1, 30, "s")
30+
if err != nil {
31+
panic(err)
32+
}
33+
Replock.Unlock()
34+
}
35+
```

ReplicaLock.go

+336
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
package ReplicaLock
2+
3+
import (
4+
"errors"
5+
"github.com/gomodule/redigo/redis"
6+
"github.com/petermattis/goid"
7+
uuid "github.com/satori/go.uuid"
8+
"log"
9+
"strconv"
10+
"strings"
11+
"time"
12+
)
13+
14+
const (
15+
INT64_MAX = (1 << 63) - 1
16+
DefaultRawName = "AnyReplicaLock"
17+
Prefix = "ReplicaLock:"
18+
)
19+
20+
var (
21+
internalLeaseTime = int64(30000) // Default is 30000ms,but we never use the default value in the implementation.
22+
renewExpirationOption = false // Default is disabled
23+
)
24+
25+
type ReplicaLock struct {
26+
rawKeyName string // This is to allow the user to set the key name for a more granular locking
27+
conn redis.Conn
28+
}
29+
30+
// New return a ReplicaLock.
31+
func New(Conn redis.Conn) (ReplicaLock, error) {
32+
33+
return ReplicaLock{rawKeyName: "", conn: Conn}, nil
34+
}
35+
36+
// NewWithRawKeyName return a ReplicaLock,
37+
// this allows the user to set the key name for a more granular locking.
38+
func NewWithRawKeyName(Conn redis.Conn, RawKeyName string) (ReplicaLock, error) {
39+
return ReplicaLock{rawKeyName: RawKeyName, conn: Conn}, nil
40+
}
41+
42+
// SetRawKeyName allow users to change the name after creating a lock,
43+
// or to use the same ReplicaLock again, but lock other things.
44+
func (RepLock *ReplicaLock) SetRawKeyName(RawKeyName string) {
45+
RepLock.rawKeyName = RawKeyName
46+
}
47+
48+
// Lock will keep trying to acquire the lock until it succeeds.
49+
// 'timeout' is the maximum time we wait for all replicas to finish synchronizing.
50+
// 'leaseTime' is the existence time of the lock.
51+
// TimeUnit is a unit of time for 'waitTime','timeout','leaseTime'.
52+
// Note that we will convert the input parameters to legal ranges.
53+
func (RepLock *ReplicaLock) Lock(timeout int64, leaseTime int64, TimeUnit string) error {
54+
if leaseTime < 0 {
55+
leaseTime = 0
56+
}
57+
TimeUnit = strings.ToLower(TimeUnit)
58+
if TimeUnit == "s" {
59+
if timeout > INT64_MAX/1000 {
60+
timeout = INT64_MAX
61+
} else {
62+
timeout = timeout * 1000
63+
}
64+
if leaseTime > INT64_MAX/1000 {
65+
leaseTime = INT64_MAX
66+
} else {
67+
leaseTime = leaseTime * 1000
68+
}
69+
} else if TimeUnit == "ms" {
70+
} else {
71+
return errors.New("TimeUnit can only be \"s\" or \"ms\"")
72+
}
73+
ttl, err := RepLock.tryLockInner(timeout, leaseTime, getlockKeyName(RepLock))
74+
if err != nil {
75+
return err
76+
}
77+
if ttl == nil {
78+
return nil
79+
}
80+
for {
81+
ttl, err = RepLock.tryLockInner(timeout, leaseTime, getlockKeyName(RepLock))
82+
if err != nil {
83+
return err
84+
}
85+
if ttl == nil {
86+
break
87+
}
88+
time.Sleep(time.Millisecond)
89+
}
90+
return nil
91+
92+
}
93+
94+
// TryLock attempt to acquire the lock during the 'waitTime' time.
95+
// 'timeout' is the maximum time we wait for all replicas to finish synchronizing.
96+
// 'leaseTime' is the existence time of the lock.
97+
// TimeUnit is a unit of time for 'waitTime','timeout','leaseTime'.
98+
// the bool returned by Trylock tells the user whether the lock was successful(true) or not(false).
99+
// Note that we will convert the input parameters to legal ranges.
100+
func (RepLock *ReplicaLock) TryLock(waitTime int64, timeout int64, leaseTime int64, TimeUnit string) (bool, error) {
101+
startTime := time.Now().UnixMilli()
102+
if waitTime < 0 {
103+
waitTime = 0
104+
}
105+
if timeout < 0 {
106+
timeout = 0
107+
}
108+
if leaseTime < 0 {
109+
leaseTime = 0
110+
}
111+
112+
TimeUnit = strings.ToLower(TimeUnit)
113+
if TimeUnit == "s" {
114+
if waitTime > INT64_MAX/1000 {
115+
waitTime = INT64_MAX
116+
} else {
117+
waitTime = waitTime * 1000
118+
}
119+
if timeout > INT64_MAX/1000 {
120+
timeout = INT64_MAX
121+
} else {
122+
timeout = timeout * 1000
123+
}
124+
if leaseTime > INT64_MAX/1000 {
125+
leaseTime = INT64_MAX
126+
} else {
127+
leaseTime = leaseTime * 1000
128+
}
129+
} else if TimeUnit == "ms" {
130+
} else {
131+
return false, errors.New("TimeUnit can only be \"s\" or \"ms\"")
132+
}
133+
ttl, err := RepLock.tryLockInner(timeout, leaseTime, getlockKeyName(RepLock))
134+
if err != nil {
135+
return false, err
136+
}
137+
if ttl == nil {
138+
return true, nil
139+
}
140+
for {
141+
currentTime := time.Now().UnixMilli()
142+
if currentTime-waitTime >= startTime {
143+
break
144+
}
145+
ttl, err = RepLock.tryLockInner(timeout, leaseTime, getlockKeyName(RepLock))
146+
if err != nil {
147+
return false, err
148+
}
149+
if ttl == nil {
150+
return true, nil
151+
}
152+
time.Sleep(time.Millisecond)
153+
}
154+
return false, nil
155+
}
156+
157+
func (RepLock *ReplicaLock) tryLockInner(waitTime int64, leaseTime int64, lockKeyName string) (interface{}, error) {
158+
NumReplicas, err := getNumReplicas(RepLock)
159+
if err != nil {
160+
return -1, err
161+
}
162+
internalLeaseTime = leaseTime
163+
164+
res, err := RepLock.conn.Do("eval", "if (redis.call('exists', KEYS[1]) == 0) then "+
165+
"redis.call('hincrby', KEYS[1], ARGV[2], 1); "+
166+
"redis.call('pexpire', KEYS[1], ARGV[1]); "+
167+
"return nil; "+
168+
"end; "+
169+
"if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then "+
170+
"redis.call('hincrby', KEYS[1], ARGV[2], 1); "+
171+
"redis.call('pexpire', KEYS[1], ARGV[1]); "+
172+
"return nil; "+
173+
"end; "+
174+
"return redis.call('pttl', KEYS[1]);", 1, getRawName(RepLock), internalLeaseTime, lockKeyName)
175+
if err != nil {
176+
return -1, err
177+
}
178+
replyNum, err := waitforReplicas(RepLock, NumReplicas, waitTime)
179+
if err != nil {
180+
return -1, err
181+
}
182+
if replyNum != NumReplicas {
183+
return -1, nil
184+
}
185+
if renewExpirationOption == true {
186+
go RepLock.renewExpiration(lockKeyName)
187+
}
188+
return res, nil
189+
}
190+
191+
func (RepLock *ReplicaLock) Unlock() {
192+
RepLock.unlockInner()
193+
}
194+
195+
func (RepLock *ReplicaLock) unlockInner() {
196+
RepLock.conn.Do("eval", "if (redis.call('hexists', KEYS[1], ARGV[2]) == 0) then "+
197+
"return nil;"+
198+
"end; "+
199+
"local counter = redis.call('hincrby', KEYS[1], ARGV[2], -1); "+
200+
"if (counter > 0) then "+
201+
"redis.call('pexpire', KEYS[1], ARGV[1]); "+
202+
"return 0; "+
203+
"else "+
204+
"redis.call('del', KEYS[1]); "+
205+
"return 1; "+
206+
"end; "+
207+
"return nil;", 1, getRawName(RepLock), internalLeaseTime, getlockKeyName(RepLock))
208+
}
209+
210+
// ForceUnlock forces the deletion of the key (if the key exists),
211+
// whether it has a lock or not.Please be aware of the risks.
212+
func (RepLock *ReplicaLock) ForceUnlock() bool {
213+
flag, err := RepLock.forceUnlockInner()
214+
if err != nil {
215+
log.Fatalln(err)
216+
}
217+
return flag
218+
}
219+
220+
func (RepLock *ReplicaLock) forceUnlockInner() (bool, error) {
221+
flag := false
222+
raw, err := RepLock.conn.Do("eval",
223+
"if (redis.call('del', KEYS[1]) == 1) then "+
224+
"return 1 "+
225+
"else "+
226+
"return 0 "+
227+
"end", 1, getRawName(RepLock))
228+
if err != nil {
229+
return false, err
230+
}
231+
rawint64, ok := raw.(int64)
232+
if !ok {
233+
return false, errors.New("interface type error")
234+
}
235+
if rawint64 == 1 {
236+
flag = true
237+
}
238+
return flag, err
239+
}
240+
241+
// getRawName Get the name of the key.
242+
// We are using Redis hash to add locks, 'RawName' is the key name of the hash.
243+
func getRawName(RepLock *ReplicaLock) string {
244+
if RepLock.rawKeyName != "" {
245+
return RepLock.rawKeyName
246+
}
247+
return DefaultRawName
248+
}
249+
250+
// lockKeyName format: "Prefix:Client_id:Goroutine_id"
251+
// example : ReplicaLock:3:1
252+
func getlockKeyName(RepLock *ReplicaLock) string {
253+
gid := goid.Get()
254+
gidStr := strconv.FormatInt(gid, 10)
255+
uidStr := uuid.NewV1().String()
256+
return Prefix + uidStr + ":" + gidStr
257+
}
258+
259+
// getNumReplicas get the number of replicas
260+
// The easy way to get the number of replicas for now is to use the 'ROLE' command
261+
// to get the length of the array of replica information
262+
func getNumReplicas(RepLock *ReplicaLock) (int64, error) {
263+
res, err := RepLock.conn.Do("role")
264+
if err != nil {
265+
return 0, err
266+
}
267+
params, ok := res.([]interface{})
268+
if !ok {
269+
return 0, errors.New("interface type error")
270+
}
271+
param, ok := params[2].([]interface{})
272+
if !ok {
273+
return 0, errors.New("interface type error")
274+
}
275+
return int64(len(param)), nil
276+
}
277+
278+
// waitforReplicas execute the 'WAIT' command and try to wait for all replicas
279+
// to finish synchronizing with the master node.
280+
// 'NumReplicas' is the number of replicas we require to be synchronized successfully,
281+
// here we require all replicas to be synchronized successfully to ensure that lock are not lost after failover.
282+
// 'timeout' is the maximum wait time.
283+
func waitforReplicas(RepLock *ReplicaLock, NumReplicas, timeout int64) (int64, error) {
284+
raw, err := RepLock.conn.Do("wait", NumReplicas, timeout)
285+
if err != nil {
286+
return -1, err
287+
}
288+
replyNum, ok := raw.(int64)
289+
if !ok {
290+
return -1, errors.New("interface type error")
291+
}
292+
return replyNum, nil
293+
}
294+
295+
// SetRenewExpirationOption
296+
// Enabled renewExpiration if 'option' is true.
297+
// Disabled renewExpiration if 'option' is true.
298+
func SetRenewExpirationOption(option bool) {
299+
renewExpirationOption = option
300+
}
301+
302+
// renewExpiration renews the lock until it is unlocked.
303+
// The lock will renew once when it reaches one third of the lease time.
304+
func (RepLock *ReplicaLock) renewExpiration(lockKeyName string) {
305+
for {
306+
flag, err := RepLock.renewExpirationInner(lockKeyName)
307+
if err != nil {
308+
log.Fatalln(err)
309+
}
310+
311+
// Already unlocked.
312+
if flag == false {
313+
break
314+
}
315+
time.Sleep(time.Duration(internalLeaseTime/3) * time.Millisecond)
316+
}
317+
}
318+
func (RepLock *ReplicaLock) renewExpirationInner(lockKeyName string) (bool, error) {
319+
flag := false
320+
raw, err := RepLock.conn.Do("eval", "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then "+
321+
"redis.call('pexpire', KEYS[1], ARGV[1]); "+
322+
"return 1; "+
323+
"end; "+
324+
"return 0;", 1, getRawName(RepLock), internalLeaseTime, lockKeyName)
325+
if err != nil {
326+
return false, err
327+
}
328+
rawint64, ok := raw.(int64)
329+
if !ok {
330+
return false, errors.New("interface type error")
331+
}
332+
if rawint64 == 1 {
333+
flag = true
334+
}
335+
return flag, err
336+
}

go.mod

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module github.com/ncghost1/Redis-ReplicaLock
2+
3+
go 1.18
4+
5+
require (
6+
github.com/gomodule/redigo v1.8.8
7+
github.com/petermattis/goid v0.0.0-20220526132513-07eaf5d0b9f4
8+
github.com/satori/go.uuid v1.2.0
9+
)

0 commit comments

Comments
 (0)