Redis distributed lock


First, the difference between distributed lock and other locks

Lock: I understand that a lock is equivalent to placing a (uniquely identifiable) tag on a resource that can be seen by multiple preemptors. If the resource has this flag (lock), it indicates that the resource is temporarily preempted and needs to wait for no flag (lock release) to use the resource

Thread locks: Lock methods and code blocks. If multiple threads want to access a shared resource, only one thread is allowed to access the resource. After the resource is idle, it will be occupied by other threads. For example, the GO language sync package provides Mutex and RWMutex locks for concurrent processing of multiple Goroutines or threads reading and writing the same variable. For multiple threads, memory is shared in the process, so the set tag is in memory

Process lock: When multiple processes access a critical resource, we need to think about which write location can be accessed by multiple processes. This is where we can lock the resource. Think back to interprocess communication in operating systems to solve the problem of preemption of critical resources, most commonly using semaphores.

P – V operation

P checks the size of the semaphore. If it is less than 0, it will block and return to the waiting queue. Otherwise apply for resources, semaphore -1

V releases resources, semaphore +1, and if there is a waiting process, the process will wake up

In GO we use fileLock to solve the problem of multiple processes reading and writing a file. When a file is preempted, the file is locked and cannot be accessed by any other process except the current one. You can see here that syscall system semaphores are used

/ / lock
func (l *FileLock) Lock(a) error {
	f, err := os.OpenFile(l.filePath, os.O_CREATE|os.O_RDONLY, 0666)
	iferr ! =nil {
		return err
	}
	l.f = f
	err = syscall.Flock(int(f.Fd()), syscall.LOCK_EX)
	iferr ! =nil {
		return err
	}
	return nil
}

/ / releases the lock
func (l *FileLock) Unlock(a) error {
	defer l.f.Close()
	return syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN)
}
Copy the code

Distributed locking: to access a critical resource by multiple machines, the resource must be marked (locked) and accessible to each machine. Often used in go language is based on Redis cache to achieve, based on Zookeeper coordination system to achieve, and based on ETCD and so on. At present redis should be compared with the implementation is relatively simple, this paper mainly introduces the principle of redis distributed lock implementation

Second, the principle of Redis distributed lock

1. Test scenarios and results

First you can see the redis lock I tested, where 2 machines (188,173) read and write to CSV files in NFS.

Scenario 1: Garbled characters appear when file locks and thread locks are used (as shown in the figure), indicating that process locks/thread locks cannot meet the required functions.

Scenario 2: Redis distributed lock is used. Each node starts 10 coroutines, and each coroutine reads and writes 5K data without garbled characters. The actual execution can be seen in the figure below, where each node’s Goroutine waits for the locked resource, methodically reading and writing files.

2. Redis lock code implementation (Golang)

1 > deployment redis

Here to wall crack recommended Docker deployment, source deployment encountered a lot of pits.

#Pull the mirror
docker pull redis 
#Look at mirror
docker images 
#Mount the run
docker run -p 6379:6379 --name myredis -v /usr/local/docker/redis.conf:/etc/redis/redis.conf -v /usr/local/docker/data:/data -d redis redis-server /etc/redis/redis.conf --appendonly yes
Copy the code

Note here that the redis configuration file must be modified to enable multiple nodes to access redis resources

Redis. Conf modifies several variables

Bind 127.0.0.1 # comment out this section, which restricts redis to only locally accessible. -mode no # default yes, enable the protected mode, restrict local access daemonize no # default no, It can run in the background, except for the kill process. If you change it to yes, you will fail to start Redis in configuration file modeCopy the code

2> Connect to the Redis server

// Use the redis package
"github.com/go-redis/redis"

Addr :127.0.0.1
// Remote access addr: IP address of redis deployment machine
func connRedis(addr, password string) *redis.Client {
	conf := redis.Options{
		Addr:     addr,
		Password: password,
	}
	return redis.NewClient(&conf) / / call the library
}
Copy the code

3 > lock

func (r *redisClient) lock(value string) (error, bool) {
    // Key points SetNX(atomic operation): key,V, expiration time
	ret := r.SetNX("mylock", value, time.Minute*10)
	iferr := ret.Err(); err ! =nil {
		fmt.Printf("set value %s error: %v\n", value, err)
		return err, false
	}
	return nil, ret.Val()
}
Copy the code

Here we use set ex nx, where the k-v value and expiration time are atomic, and the set succeeds only if the KEY does not exist.

The following figure shows the process of locking

4 > unlock

func (r *redisClient) unlock(a) bool {
    // Delete the information corresponding to K value
	ret := r.Del("mylock")
	iferr := ret.Err(); err ! =nil {
		fmt.Println("unlock error: ", err)
		return false
	}
	return true
}
Copy the code

5> Take a break before preempting

At first, the preemption will rest for 1s(configure yourself) and then continue to preempt

func (r *redisClient) retryLock(goroutineId int) bool {
	ok := false
	for! ok { err, t := r.getExTime()// Get the expiration time
		iferr ! =nil {
			return false
		}
		if t > 0 {
			fmt.Print(time.Now().Format("The 2006-01-02 15:04:05"))
			fmt.Printf("The coroutine %d lock was preempted, try again after %f seconds... \n", goroutineId, (t / 600).Seconds())
			time.Sleep(t / 600) // Sleep for 1s, because the expiration time is set to 10min
		}
		err, ok = r.lock("mylock") // Get the lock again
		iferr ! =nil {
			return false}}return ok
}
Copy the code

Three, encountered pit

1, 2 machines, one machine didn’t take the other machine, can only wait. Before releasing the lock, the lock resource expires, resulting in a deadlock, and the system forcibly ends the process. This is clearly not what we expected

Solution: In the preceding scenario, I set the lock resource to 10s, because the amount of read and write data is large, the lock time is easy to expire. So I changed it to 1 minute to solve the problem. Therefore, the expiration time of lock resources must meet service requirements. However, if the setting is too large, when a machine dies, the lock release time is too long, and other nodes wait too long, resulting in a waste of resources. Therefore, we should be careful to set the expiration time of the lock resource