SoFunction
Updated on 2025-03-04

Example code for Golang to implement reentrable locks

The project encountered the need and implementation of reentrant locks, and record them in detail.

What is a reentrant lock

The distributed lock we usually talk about generally refers to multiple threads on different servers, only one thread can grab a lock and perform a task. And we use locks to ensure that a task can only be completed by one thread. So we usually use three-stage logic like this:

Lock();
DoJob();
Unlock();

However, since our systems are distributed, this lock is generally not placed in a certain process, and we will borrow third-party storage, such as Redis, to make this distributed lock. But once we use third-party storage, we must face this problem: Can Unlock ensure that it will run?

In addition to program bugs, we also face network instability, processes are killed, servers are down, etc. We cannot guarantee that Unlock will be run.

Then we usually add a timeout time to this lock when locking.

LockByExpire(duration);
DoJob();
Unlock();

This timeout is to allow the Unlock to not be run once an exception occurs, and the lock will be automatically released during the duration time. In redis, we usually use thisset exTo set the lock timeout setting.

But we encountered a problem with this timeout time. How long is the appropriate timeout time set? Of course, the setting takes longer than DoJob. Otherwise, the lock will be released before the task is over, which may still lead to the existence of concurrent tasks.

But in fact, due to network timeout problems, system health problems, etc., we cannot accurately know how long the DoJob function will be executed. So what should I do at this time?

There are two ways:

The first method is that we can make a timeout setting for DoJob. If DoJob can only execute n seconds at most, then the timeout time of my distributed lock is set longer than n seconds. Setting a timeout for a task is possible in many languages. For example, TimeoutContext in golang.

The second method is to set a relatively small timeout for the lock, and then continue to renew the lock. The constant demand for a lock can also be understood as restarting the lock. This kind of lock that can be renewed continuously is called a reentrant lock.

In addition to the main thread, the reentrant lock must have another thread (or Ctrip) that can renew the lock. We call this additional program watchDog (watchdog).

Specific implementation

In Golang, the language level naturally supports coroutines, so this reentrant lock is very easy to implement:

// DistributeLockRedis is a distributed reentrant lock based on redis, automatically renewing the leasetype DistributeLockRedis struct {
	key       string             // The key of the lock	expire    int64              // Lock timeout time	status    bool               // Locking success sign	cancelFun  // Used to cancel automatic renewal of Ctrip	redis            // redis handle}

// Create a okfunc NewDistributeLockRedis(key string, expire int64) *DistributeLockRedis {
	return &DistributeLockRedis{
		 key : key,
		 expire : expire,
	}
}

// TryLock lockfunc (dl *DistributeLockRedis) TryLock() (err error) {
	if err = (); err != nil {
		return err
	}
	ctx, cancelFun := (())
	 = cancelFun
	(ctx) // Create a daemon coroutine to automatically renew the lock	 = true
	return nil
}

// competition lockfunc (dl *DistributeLockRedis) lock() error {
	if res, err := (((), "SET", , 1, "NX", "EX", )); err != nil {
		return err
	} 
	return nil
}


// guard creates a daemon coroutine and automatically renewsfunc (dl *DistributeLockRedis) startWatchDog(ctx ) {
	safeGo(func() error {
		for {
			select {
			// Unlock notification ends			case <-():
				return nil
			default:
				// Otherwise, as long as it starts, it will automatically re-enter (renewal lock)				if  {
					if res, err := (((), "EXPIRE", , )); err != nil {
						return nil
					} 
					// The renewal time is expire/2 seconds					((/2) * )
				}
			}
		}
	})
}

// Unlock releases the lockfunc (dl *DistributeLockRedis) Unlock() (err error) {
	// This reentry lock must be cancelled and executed in the first place	if  != nil {
		() // Release successfully, cancel the reentry lock	}
	var res int
	if  {
		if res, err = (((), "Del", )); err != nil {
			return ("Failed to release the lock")
		}
		if res == 1 {
			 = false
			return nil
		}
	}
	return ("Failed to release the lock")
}

The logic of this code is basically written in the form of comments. The main one is startWatchDog, which re-renews the lock

ctx, cancelFun := (())
 = cancelFun
(ctx) // Create a daemon coroutine to automatically renew the lock = true

First create a cancelContext, and its context function cancelFunc is called to Unlock. Then start a goroutine process to cycle and renew.

This newly started goroutine will only end when the main goroutine process is finished and Unlock is called. Otherwise, the redis expire command will be called once at the expiration time/2 to renew.

As for the external, the following is used

func Foo() error {
  key := foo
  
  // Create reentrant distributed locks	dl := NewDistributeLockRedis(key, 10)
	// Fight for locks	err := ()
	if err != nil {
		// No lock was grabbed		return err
	}
	
	// If you grab the lock, remember to release the lock	defer func() {
		()
	}
	
	// Do real tasks	DoJob()
}

The above is the detailed content of the example code for Golang to implement reentrant locks. For more information about Golang to reentrant locks, please follow my other related articles!