2023-06-19: Tell me about the implementation of Redis distributed locks?

2023-06-19: Tell me about the implementation of Redis distributed locks?

Answer 2023-06-19:

The simplest implementation of Redis distributed lock

To implement distributed locks, you really need to use mutually exclusive Redis operations. One of the commonly used ways is to use SETNXthe command, which means "SET if Not Exists", that is, the value is set only when the key does not exist, otherwise no operation is performed. In this way, two client processes can execute SETNXcommands to achieve mutual exclusion, thereby achieving the purpose of distributed locks.

Here is an example:

Client 1 applies for locking, and the locking is successful:

SETNX lock_key 1

Client 2 applies for a lock, but because it is at a later time, the lock fails:

SETNX lock_key 1

In this way, you can use Redis's mutual exclusion to implement a simple distributed lock mechanism.

image.png

For clients that have successfully locked, they can perform operations on shared resources, such as modifying a row of data in MySQL or calling API requests.

After the operation is completed, the lock needs to be released in time so that subsequent requests can access shared resources. Releasing the lock is very simple, just use DELthe command to delete the corresponding lock key (key).

Here is an example logic for releasing a lock:

DEL lock_key

By executing the above DELcommand, the lock is successfully released, so that subsequent requests can obtain the lock and execute the logic of operating shared resources.

In this way, by using SETNXa command to lock and then DELa command to release the lock, you can implement a basic distributed locking mechanism.

image.png

However, it has a big problem. After client 1 obtains the lock, if the following scenario occurs, it will cause a "deadlock":

1. The program processing business logic is abnormal, and the lock is not released in time.

2. The process crashes or stops unexpectedly, and the lock cannot be released.

In this case, the client will hold the lock forever, and other clients will not be able to acquire the lock. How to solve this problem?

How to avoid deadlock?

When considering setting a "lease period" for a lock when applying for it, it can be achieved by setting the "expiration time" in Redis. Suppose we assume that the time to operate shared resources will not exceed 10 seconds. When locking, you can set an expiration time of 10 seconds for the key. This can ensure that within a period of time after applying for the lock, if the lock holder does not update the expiration time of the lock within this time, the lock will automatically expire, thereby preventing the lock from being permanently occupied

SETNX lock 1    // 加锁
EXPIRE lock 10  // 10s后自动过期

image.png

In this way, no matter whether the client is abnormal or not, the lock can be "automatically released" after 10s, and other clients can still get the lock.

But now there is still a problem:

The current operation is to execute locking and setting the expiration time as two independent commands. There is a problem that only the first command may be executed but the second command may not be executed in time, causing problems. For example:

  • After the SETNX command is successfully executed, the EXPIRE command fails due to network problems.

  • After the SETNX command is successfully executed, Redis crashes abnormally, resulting in no chance for the EXPIRE command to be executed.

  • After the SETNX command is successfully executed, the client crashes abnormally, which also causes the EXPIRE command to have no chance to execute.

In short, these two commands cannot be guaranteed to be atomic operations (successful together), there is a potential risk that the expiration time setting will fail, and the "deadlock" problem will still occur.

Fortunately, after Redis version 2.6.12, Redis has expanded the parameters of the SET command. Just use this command:

SET lock 1 EX 10 NX

image.png

What if the lock is released by others?

When the above command is executed, each client does not perform strict verification when releasing the lock, and there is a potential risk of releasing other people's locks. To solve this problem, you can set a unique identifier for each client when locking, and compare the identifier when unlocking to verify whether you have the right to release the lock.

For example, it can be your own thread ID, or a UUID (random and unique), here we take UUID as an example:

SET lock $uuid EX 20 NX

After that, when releasing the lock, you must first judge whether the lock is still owned by you. The pseudocode can be written as follows:

if redis.get("lock") == $uuid:
    redis.del("lock")

Here, the two commands GET + DEL are used to release the lock. At this time, we will encounter the atomicity problem we mentioned earlier. Here you can use lua script to solve it.

The Lua script to safely release the lock is as follows:

if redis.call("GET",KEYS[1]) == ARGV[1]
then
    return redis.call("DEL",KEYS[1])
else
    return 0
end

Well, with such optimization all the way, the entire locking and unlocking process will be more "rigorous".

Here we first summarize, based on the distributed lock implemented by Redis, a rigorous process is as follows:

1. Lock

SET lock_key $unique_id EX $expire_time NX

2. Operating shared resources

3. Release the lock: Lua script, first GET to determine whether the lock belongs to itself, and then DEL to release the lock

Go code implements distributed lock

package main

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/go-redis/redis/v8"
	"github.com/google/uuid"
)

const (
	LockTime         = 5 * time.Second
	RS_DISTLOCK_NS   = "tdln:"
	RELEASE_LOCK_LUA = `
        if redis.call('get',KEYS[1])==ARGV[1] then
            return redis.call('del', KEYS[1])
        else
            return 0
        end
    `
)

type RedisDistLock struct {
    
    
	id          string
	lockName    string
	redisClient *redis.Client
	m           sync.Mutex
}

func NewRedisDistLock(redisClient *redis.Client, lockName string) *RedisDistLock {
    
    
	return &RedisDistLock{
    
    
		lockName:    lockName,
		redisClient: redisClient,
	}
}

func (this *RedisDistLock) Lock() {
    
    
	for !this.TryLock() {
    
    
		time.Sleep(100 * time.Millisecond)
	}
}

func (this *RedisDistLock) TryLock() bool {
    
    
	if this.id != "" {
    
    
		// 处于加锁中
		return false
	}
	this.m.Lock()
	defer this.m.Unlock()
	if this.id != "" {
    
    
		// 处于加锁中
		return false
	}
	ctx := context.Background()
	id := uuid.New().String()
	reply := this.redisClient.SetNX(ctx, RS_DISTLOCK_NS+this.lockName, id, LockTime)
	if reply.Err() == nil && reply.Val() {
    
    
		this.id = id
		return true
	}

	return false
}

func (this *RedisDistLock) Unlock() {
    
    
	if this.id == "" {
    
    
		// 未加锁
		panic("解锁失败,因为未加锁")
	}
	this.m.Lock()
	defer this.m.Unlock()
	if this.id == "" {
    
    
		// 未加锁
		panic("解锁失败,因为未加锁")
	}
	ctx := context.Background()
	reply := this.redisClient.Eval(ctx, RELEASE_LOCK_LUA, []string{
    
    RS_DISTLOCK_NS + this.lockName}, this.id)
	if reply.Err() != nil {
    
    
		panic("释放锁失败!")
	} else {
    
    
		this.id = ""
	}
}

func main() {
    
    

	client := redis.NewClient(&redis.Options{
    
    
		Addr: "172.16.11.111:64495",
	})
	const LOCKNAME = "百家号:福大大架构师每日一题"

	lock := NewRedisDistLock(client, LOCKNAME)

	lock.Lock()
	fmt.Println("加锁main")
	ch := make(chan struct{
    
    })
	go func() {
    
    
		lock := NewRedisDistLock(client, LOCKNAME)
		lock.Lock()
		fmt.Println("加锁go程")
		lock.Unlock()
		fmt.Println("解锁go程")
		ch <- struct{
    
    }{
    
    }
	}()
	time.Sleep(time.Second * 2)
	lock.Unlock()
	fmt.Println("解锁main")
	<-ch
}


insert image description here

What should I do if the lock expiration time is not easy to evaluate?

image.png

Looking at the picture above, the expiration time of adding the key is 10s, but after the client C gets the distributed lock, and then executes the business logic for more than 10s, then the problem arises, before the client C releases the lock, in fact, the lock has expired, then both client A and client B can go to get the lock, so the function of distributed lock has been lost! ! !

A relatively simple compromise solution is to "redundant" the expiration time as much as possible to reduce the probability of early expiration of the lock, but this cannot perfectly solve the problem, so what should we do?

Distributed lock joins watchdog

During the locking process, an expiration time can be set, and a daemon thread (also called a "watchdog" thread) can be started to regularly detect the remaining valid time of the lock. If the lock is about to expire, but the shared resource operation has not been completed, the daemon thread can automatically renew the lock and reset the expiration time.

Why use daemon threads:

image.png

red lock in go

package main

import (
	"fmt"
	"time"

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

func main() {
    
    
	client := redis.NewClient(&redis.Options{
    
    
		Addr:     "172.16.11.111:64495",
		Password: "", // 如果有密码,请提供密码
		DB:       0,  // 如果使用不同的数据库,请修改为准确的数据库编号
	})

	pool := goredis.NewPool(client)

	const LOCKNAME = "百家号:福大大架构师每日一题"

	redsync := redsync.New(pool)

	mutex := redsync.NewMutex(LOCKNAME)

	if err := mutex.Lock(); err != nil {
    
    
		fmt.Println("加锁失败:", err)
		return
	}

	fmt.Println("加锁main")

	ch := make(chan struct{
    
    })

	go func() {
    
    
		mutex := redsync.NewMutex(LOCKNAME)

		if err := mutex.Lock(); err != nil {
    
    
			fmt.Println("加锁失败:", err)
			return
		}

		fmt.Println("加锁go程")
		mutex.Unlock()
		fmt.Println("解锁go程")

		ch <- struct{
    
    }{
    
    }
	}()

	time.Sleep(time.Second * 2)
	mutex.Unlock()
	fmt.Println("解锁main")

	<-ch
}

insert image description here

Guess you like

Origin blog.csdn.net/weixin_48502062/article/details/131285813