Redis and Distributed Locking

Ishita
4 min readJun 14, 2023

--

Distributed Locks

There are several resources in a system that should not be accessed or updated simultaneously by multiple processes or applications for the operation to succeed. For example, a shared file stored in a shared directory should not be simultaneously updated by multiple users or the users will not be able to see their changes in the correct format. Therefore, exclusive access to such a shared resource by a user must be ensured. This exclusiveness of access is called mutual exclusion. Same logic applies to shared resources in the system. Shared database is one of the common examples where we need to maintain this mutual exclusion to write the data in database for maintaining Consistency. We can use distributed locking for mutually exclusive access to such resources.

Locks are one of the mechanisms used to provide mutually exclusive access to a resource. For example, lock can be used to:

  • Provide exclusive access to an entire database, a specific database table or a specific row in a database table
  • Lock updating files from a shared file system

Any shared resource where updates can take place can be the target for a lock. Locks are usually used on operations that mutate state of the object and not on read operations on the object.

Redis for Distributed Locks

  1. Performance​: Redis runs in-memory, which enables low-latency and high throughput. Running in-memory means requests for data do not require a trip to disk. This leads to an order of magnitude more operations and faster response times. Redis is one of the only databases that supports millions of operations per second.
  2. Easy Scalability: Redis can be easily scaled horizontally which makes it a good fit for distributed systems.

There are a number of libraries and blog posts describing how to implement a distributed lock with Redis, but every library uses a different approach. In this blog, I will use RedSync, Go implementation of RedLock algorithm, to create locks on Redis data store.

Redis Lock Example using RedSync

Pre-requisites:

  1. Download and Install Redis on your local machine: You can download Redis from the official Redis website.
  2. Install RedSync: You can install RedSync using the below go command:
$ go get github.com/go-redsync/redsync/v4

To implement Redis Lock we will start by implementing the connection pool to the Redis instance. RedSync uses this connection pool to communicate with Redis instances.

func getRedSyncInstance() {
client := goredislib.NewClient(&goredislib.Options{
Addr: "localhost:6379",
})
pool := goredis.NewPool(client)

rs := redsync.New(pool)
}

Now, we can call the create a mutex with the same name for all instances wanting the same lock on shared Redis data. We execute mutex.Lock() to acquire the lock on the objects. Once the lock is acquired successfully, no other process can access this object till the lock acquired by the current process is released.

To release the mutex lock, we can call the function mutex.Unlock(). This function will release the lock for the other processes to acquire the lock.

rs := getRedSyncInstance()

// Use the same name of the Mutex for all instances wanting the same lock.
mutexname := "my-global-mutex"
mutex := rs.NewMutex(mutexname)

// Obtain a lock for our given mutex. After this is successful, no one else
// can obtain the same lock (the same mutex name) until we unlock it.
if err := mutex.Lock(); err != nil {
panic(err)
}

fmt.Println("Lock Acquired Successfully!!")

// Release the lock so other processes or threads can obtain a lock.
if ok, err := mutex.Unlock(); !ok || err != nil {
panic("unlock failed")
}

fmt.Println("Lock Released Successfully!!")

The complete code can be seen below:

package main

import (
"fmt"

"github.com/go-redsync/redsync/v4"
"github.com/go-redsync/redsync/v4/redis/goredis/v9"
goredislib "github.com/redis/go-redis/v9"
)

func getRedSyncInstance() *redsync.Redsync {
client := goredislib.NewClient(&goredislib.Options{
Addr: "localhost:6379",
})
pool := goredis.NewPool(client)

rs := redsync.New(pool)

return rs
}

func main() {

rs := getRedSyncInstance()

// Use the same name of the Mutex for all instances wanting the same lock.
mutexname := "my-global-mutex"
mutex := rs.NewMutex(mutexname)

// Obtain a lock for our given mutex. After this is successful, no one else
// can obtain the same lock (the same mutex name) until we unlock it.
if err := mutex.Lock(); err != nil {
panic(err)
}

fmt.Println("Lock Acquired Successfully!!")

// Release the lock so other processes or threads can obtain a lock.
if ok, err := mutex.Unlock(); !ok || err != nil {
panic("unlock failed")
}

fmt.Println("Lock Released Successfully!!")
}

If a lock fails to release for some uncertain reasons, the RedLock algorithm automatically releases the lock after a certain specified time. This expiration time prevents locks from being held indefinitely in case of a crash or an error. However, it’s essential to choose an appropriate expiration time that balances the risk of a lock being held for too long and the possibility of a lock being released prematurely while a critical section is still in progress.

--

--