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 ex
To 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!