Skip to content

Instantly share code, notes, and snippets.

@andybons
Created November 6, 2012 18:21
Show Gist options
  • Save andybons/4026525 to your computer and use it in GitHub Desktop.
Save andybons/4026525 to your computer and use it in GitHub Desktop.
A lock service written in Go using Redis
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