Created
November 6, 2012 18:21
-
-
Save andybons/4026525 to your computer and use it in GitHub Desktop.
A lock service written in Go using Redis
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"errors" | |
"github.com/garyburd/redigo/redis" | |
"log" | |
"math/rand" | |
"sync" | |
"time" | |
) | |
// This Redis lock service is based on the algorithm described at | |
// (http://redis.io/commands/setnx) | |
// | |
// * C1 sends SETNX lock.foo in order to acquire the lock | |
// * The crashed client C2 still holds it, so Redis will reply with 0 to C1. | |
// * C1 sends GET lock.foo to check if the lock expired. If it is not, it | |
// will sleep for some time and retry from the start. | |
// * Instead, if the lock is expired because the Unix time at lock.foo is older | |
// than the current Unix time, C1 tries to perform: | |
// GETSET lock.foo <current Unix timestamp + lock timeout> | |
// * Because of the GETSET semantic, C1 can check if the old value stored at | |
// key is still an expired timestamp. If it is, the lock was acquired. | |
// * If another client, for instance C3, was faster than C1 and acquired the | |
// lock with the GETSET operation, the C1 GETSET operation will return a non | |
// expired timestamp. C1 will simply restart from the first step. Note that | |
// even if C1 set the key a bit a few seconds in the future this is not a problem. | |
// * A client holding a lock should always check the timeout didn't expire | |
// before unlocking the key with DEL because client failures can be complex, | |
// not just crashing but also blocking a lot of time against some operations | |
// and trying to issue DEL after a lot of time (when the LOCK is already held | |
// by another client). | |
var conn redis.Conn | |
var m sync.Mutex | |
// SafeDo wraps calls to the non-threadsafe Do command in a mutex. | |
func SafeDo(cmd string, args ...interface{}) (interface{}, error) { | |
m.Lock() | |
result, err := conn.Do(cmd, args...) | |
m.Unlock() | |
return result, err | |
} | |
func Lock(key string) error { | |
for { | |
timeout := time.Now().Add(30 * time.Second).Unix() | |
// Sends SETNX lock.foo in order to acquire the lock. | |
response, err := SafeDo("SETNX", key, timeout) | |
if err != nil { | |
return err | |
} | |
if status, _ := response.(int64); status == 1 { | |
// The lock was obtained. | |
break | |
} | |
// The lock is taken. Check if it has expired. | |
response, err = SafeDo("GET", key) | |
if err != nil { | |
return err | |
} | |
now := time.Now().Unix() | |
if expires, ok := response.(int64); ok && now > expires { | |
newExpires := time.Now().Add(30 * time.Second).Unix() | |
response, err := SafeDo("GETSET", key, newExpires) | |
if err != nil { | |
return err | |
} | |
if oldExpires, ok := response.(int64); ok && now > oldExpires { | |
break | |
} | |
} | |
time.Sleep(10 * time.Millisecond) | |
} | |
return nil | |
} | |
func Unlock(key string) error { | |
now := time.Now().Unix() | |
response, err := SafeDo("GET", key) | |
if err != nil { | |
return err | |
} | |
if expires, ok := response.(int64); ok && now > expires { | |
// The lock has already expired. A subsequent Lock attempt will succeed. | |
return nil | |
} | |
response, err = SafeDo("DEL", key) | |
status, _ := response.(int64) | |
if err != nil { | |
return err | |
} | |
if status != 1 { | |
return errors.New("Response status for DEL was not 1.") | |
} | |
return nil | |
} | |
func main() { | |
log.Println("Starting...") | |
var err error | |
if conn, err = redis.Dial("tcp", "localhost:6379"); err != nil { | |
panic(err) | |
} | |
defer conn.Close() | |
SafeDo("DEL", "key.foo") | |
var wg sync.WaitGroup | |
wg.Add(2) | |
go func() { | |
defer wg.Done() | |
for i := 0; i < 20; i++ { | |
if err := Lock("key.foo"); err != nil { | |
log.Printf("Walter Lock: got error %q\n", err.Error()) | |
} | |
log.Printf("Walter: %d...", i) | |
time.Sleep(time.Duration(rand.Int63n(500)) * time.Millisecond) | |
if err := Unlock("key.foo"); err != nil { | |
log.Printf("Walter Unlock: got error %q", err.Error()) | |
} | |
time.Sleep(time.Duration(rand.Int63n(50)) * time.Millisecond) | |
} | |
}() | |
go func() { | |
defer wg.Done() | |
for i := 0; i < 20; i++ { | |
if err := Lock("key.foo"); err != nil { | |
log.Printf("Clarence Lock: got error %q\n", err.Error()) | |
} | |
log.Printf("Clarence: %d...", i) | |
time.Sleep(time.Duration(rand.Int63n(300)) * time.Millisecond) | |
if err := Unlock("key.foo"); err != nil { | |
log.Printf("Clarence Unlock: got error %q", err.Error()) | |
} | |
time.Sleep(time.Duration(rand.Int63n(50)) * time.Millisecond) | |
} | |
}() | |
wg.Wait() | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment