Redis distributed locks and solutions to common problems

Redis is an in-memory data structure storage system that can be used as a database, cache, and message broker. Due to its high performance and flexible data structure, Redis is widely used in various scenarios, including implementing distributed locks.

Distributed lock is a technology that implements mutually exclusive access in a distributed system. In many practical application scenarios, we need to ensure that certain operations can only be executed by one node at the same time, such as updating shared resources, processing task queues, etc. At this time, we need to use distributed locks.

Redis provides a simple and effective way to implement distributed locks. The basic idea is to use Redis's SETNX command, which sets the value when the key does not exist and does nothing if the key already exists. Through this atomic operation, we can achieve mutually exclusive access between multiple nodes.

However, although the implementation of Redis distributed locks is relatively simple, many issues need to be considered in actual use, such as lock timeout and renewal issues, lock fairness issues, network partition issues, etc. In the following articles, we will detail these problems and their solutions.



1. Introduction to Redis distributed lock
1.1. About distributed locks

In a distributed system, when a thread reads and modifies data, because reading and updating and saving is not an atomic operation, it is easy to encounter concurrency problems during concurrency, which in turn leads to incorrect data. This scenario is very common, such as e-commerce flash sale activities and inventory quantity updates. If it is a stand-alone application, it can be avoided by directly using local locks. If it is a distributed application, local locks are not useful, and distributed locks need to be introduced to solve the problem.

Generally speaking, there are the following ways to implement distributed locks:

  1. Using MySQL: This method is to create a unique index table in the database, and then acquire the lock by inserting a piece of data. If the insertion is successful, the lock acquisition is successful, otherwise the lock acquisition fails. The operation of releasing the lock is to delete this piece of data. The advantage of this method is that it is simple to implement, but the disadvantage is that the performance is lower because it involves database operations.
  2. Using ZooKeeper: ZooKeeper provides a native distributed lock implementation. The basic idea is to create a temporary ordered node, and then determine whether it has the smallest sequence number among all child nodes. If so, acquire the lock successfully. Otherwise, listen to the node with a smaller sequence number than its own, and try to acquire the lock again when the node is deleted. The advantage of this method is that it can ensure fairness, but the disadvantage is that the implementation is more complicated.
  3. Using Redis: This method is implemented through the SETNX command of Redis. This command can set the value when the key does not exist, and does nothing if the key already exists. Through this atomic operation, we can achieve mutually exclusive access between multiple nodes. The advantages of this method are high performance and simple implementation. The disadvantage is that it needs to deal with lock timeout and renewal issues.
1.2. Overview of Redis distributed locks

In Redis, we can use SETNXthe command to implement distributed locks. Here are the specific steps:

  1. Locking: The client uses SETNX key valuethe command to try to set a key, where keyis the name of the lock and valueis a unique identifier (such as UUID) used to identify the locked client. If the key does not exist, SETNXthe command will set the value of the key and return 1, indicating that the lock is successful; if the key already exists, the SETNXcommand will not change the value of the key and return 0, indicating that the lock failed.

image-20230916113717663

  1. Perform business operations: After successfully acquiring the lock, the client can perform business operations that need protection.
  2. Unlocking: After completing the business operation, the client needs to release the lock so that other clients can acquire the lock. In order to ensure that only the locked client can unlock, the client needs to first obtain the value of the lock (i.e. unique identifier), and then compare the value of the lock with its own unique identifier to see if they are the same. If they are the same, use the command to delete the key to release the lock DEL key. .

2. Problems and solutions of Redis distributed locks
2.1. Lock timeout mechanism

The following is a basic Redis distributed lock usage process:

  1. Client A sends a SETNX lock.key command. If 1 is returned, then Client A obtains the lock.
  2. After client A completes execution, it releases the lock through the DEL lock.key command.

However, there is a problem with this most basic lock, that is, if client A cannot send the DEL command to release the lock for some reason (such as a crash or network problem) after completion of execution, other clients will never be able to obtain it. Lock. In order to solve this problem, we need to introduce a lock timeout mechanism.

image-20230916130718524

The following is a usage process of Redis distributed lock with timeout mechanism:

  1. Client A sends a SETNX lock.key command. If 1 is returned, then Client A obtains the lock.
  2. Client A sets the lock timeout through the EXPIRE lock.key timeout command.
  3. After client A completes execution, it releases the lock through the DEL lock.key command.

This way, even if client A cannot release the lock after execution, other clients can obtain the lock after the lock times out.

2.2. Lock renewal mechanism

However, there is a problem with this lock with a timeout mechanism, that is, if client A is still executing when the lock is about to time out, the lock may be acquired by other clients, resulting in multiple clients holding the lock at the same time. . In order to solve this problem, we need to introduce a lock renewal mechanism.

image-20230916130800910

The following is a usage process of Redis distributed lock with renewal mechanism:

  1. Client A sends a SETNX lock.key command. If 1 is returned, then Client A obtains the lock.
  2. Client A sets the lock timeout through the EXPIRE lock.key timeout command.
  3. During execution, client A periodically renews the lock through the EXPIRE lock.key timeout command.
  4. After client A completes execution, it releases the lock through the DEL lock.key command.

In this way, even if the execution time of client A exceeds the initial timeout period, the mutual exclusivity of the lock can be guaranteed through the renewal mechanism.

2.3. Problem of accidentally deleting locks

Introducing the lock renewal mechanism can solve the problem of early lock expiration, but it does not solve the problem of other thread locks being deleted when unlocking. This is because, even with the renewal mechanism, there is still a situation where thread A is still executing business logic when the lock is about to expire. At this time, the lock expires, thread B acquires the lock, and then thread A finishes executing the business logic. Try to delete the lock, but the lock of thread B is deleted.

In order to solve this problem, we can use the Lua script function of Redis to encapsulate these three operations in a Lua script, and then use the EVALcommand to execute the Lua script. Since Redis executes all commands sequentially in a single thread, EVALcommands can ensure that operations in Lua scripts are atomic.

The following is an example of unlocking using Lua script:

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

In this Lua script, we first use getthe command to get the value of the lock, and then compare the value of the lock with the client's unique identifier. If they are the same, use the delcommand to delete the lock.

The client can execute this Lua script using the following command:

EVAL script 1 key value

Among them, scriptis the content of the Lua script, keyis the name of the lock, valueand is the unique identifier of the client.

2.4. Split-brain problem and Redlock

In a Redis cluster, if the master node dies before the synchronization lock is transferred to the slave node, the slave node may mistakenly believe that the lock does not exist after being upgraded to the master node, allowing other clients to obtain the lock, which results in the same lock. The problem of being held by multiple clients at the same time.

To solve this problem, we can use the RedLock algorithm. RedLock is a distributed lock implementation algorithm officially recommended by Redis. Its basic idea is to try to acquire locks on multiple independent Redis nodes at the same time. Only when most Redis nodes successfully acquire the lock, the entire operation is considered successful. .

The following are the basic steps of the RedLock algorithm:

  1. Get the current time in milliseconds.
  2. Attempts are made to acquire locks on all Redis nodes in sequence, with a fixed timeout for each attempt. If acquisition of the lock fails, return immediately and no longer try other nodes.
  3. If the locks of most Redis nodes are successfully acquired and the total time to acquire the locks is less than the validity period of the locks, then the entire operation is successful.
  4. If the total time to acquire the lock is greater than the lock validity period, or the locks of most Redis nodes are not successfully acquired, then the locks are released on all Redis nodes.
  5. If the entire operation is successful, the validity period of the lock is the original validity period minus the total time to acquire the lock.

The above are the basic steps of the RedLock algorithm. By trying to acquire locks on multiple independent Redis nodes at the same time, the RedLock algorithm can solve the problem of lock loss caused by the failure of the master node to a certain extent. However, it should be noted that the RedLock algorithm does not fully guarantee the security of the lock, because in the case of network partitions or node time out-of-synchronization, the same lock may still be held by multiple clients at the same time. Therefore, when using the RedLock algorithm, detailed design and testing are required based on the actual situation.

2.5. Fairness Issues

In addition, lock fairness may become an issue in the implementation of Redis distributed locks. The so-called fairness means that when multiple clients request locks at the same time, the locks should be allocated in the order of requests. However, due to network latency and the single-threaded model of Redis, Redis distributed locks cannot guarantee fairness. Specifically, when multiple clients request locks at the same time, these requests may arrive at Redis at different times due to network delays, and Redis will allocate locks in the order in which the requests arrive, which may be different from the client's request order. In addition, even if multiple requests arrive at Redis at the same time, due to Redis's single-threaded model, Redis can only process these requests in sequence, and the processing order may be different from the client's request order.

Therefore, if your application requires fair distributed locks, you may need to use other distributed lock implementations, such as those based on ZooKeeper. ZooKeeper's distributed lock ensures the fairness of the lock by creating sequential temporary nodes under the locked node and comparing whether the own node is the smallest node to determine whether the lock has been obtained.


3. Implementation of Redis distributed lock under Java
3.1. Jedis implementation

In Java, we can use Redis client libraries such as Jedis or Lettuce to implement Redis distributed locks. Here is a basic implementation example:

import redis.clients.jedis.Jedis;

public class RedisLock {
    
    
    private Jedis jedis;
    private String lockKey;
    private String lockValue;
    private int expireTime;
    private boolean locked = false;

    public RedisLock(Jedis jedis, String lockKey, int expireTime) {
    
    
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.expireTime = expireTime;
        this.lockValue = Thread.currentThread().getId() + "-" + System.nanoTime();
    }

    public boolean lock() {
    
    
        long startTime = System.currentTimeMillis();
        while (true) {
    
    
            String result = jedis.set(lockKey, lockValue, "NX", "PX", expireTime);
            if ("OK".equals(result)) {
    
    
                locked = true;
                return true;
            }
            // 如果没有获取到锁,需要稍微等待一下再尝试
            try {
    
    
                Thread.sleep(100);
            } catch (InterruptedException e) {
    
    
                throw new RuntimeException(e);
            }
            // 如果尝试获取锁超过了expireTime,那么返回失败
            if (System.currentTimeMillis() - startTime > expireTime) {
    
    
                return false;
            }
        }
    }

    public void unlock() {
    
    
        if (!locked) {
    
    
            return;
        }
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        jedis.eval(script, 1, lockKey, lockValue);
    }
}

In this example, we use the and options setof the command to implement lock acquisition and timeout settings, and use Lua scripts to implement safe unlocking operations. We also use a while loop to continuously try to acquire the lock until the lock is successfully acquired or the try time expires.NXPX

However, this example does not implement a lock renewal mechanism. In order to implement the renewal mechanism, we need to regularly check the remaining time of the lock in another thread. If the remaining time is insufficient, then we need to use the expirecommand to reset the lock timeout. This requires more complex code to implement, such as using Java's ScheduledExecutorService to perform renewal operations periodically.

3.2. SpringBoot implementation

In Spring Boot, we can use Redisson, the Redis client library, to implement Redis distributed locks. Redisson provides a rich set of distributed services, including distributed locks, distributed collections, distributed queues, etc., and Redisson has built-in lock timeout and renewal mechanisms, and solves the problem of accidentally deleting locks.

The following is a basic example of using Redisson to implement Redis distributed locks:

import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.util.concurrent.TimeUnit;

@Component
public class RedissonDistributedLocker {
    
    

    private RedissonClient redissonClient;

    @PostConstruct
    public void init() {
    
    
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        redissonClient = Redisson.create(config);
    }

    public void lock(String lockKey) {
    
    
        RLock lock = redissonClient.getLock(lockKey);
        // Wait for 100 seconds and automatically unlock it after 10 seconds
        lock.lock(10, TimeUnit.SECONDS);
    }

    public void unlock(String lockKey) {
    
    
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }
}

In this example, we first initcreate a RedissonClient instance in the method, then lockobtain an RLock object in the method, and call its lockmethod to acquire the lock. In unlockthe method, we also obtain an RLock object and call its unlockmethod to release the lock.

It should be noted that Redisson's lockmethod will automatically renew. As long as the thread holding the lock is still running, the lock will be renewed until the thread ends or the unlockmethod is explicitly called. Therefore, we do not need to implement the renewal mechanism manually. In addition, Redisson's unlockmethod will check whether the current thread holds the lock. Only the thread holding the lock can release the lock, which solves the problem of accidentally deleting the lock.

Guess you like

Origin blog.csdn.net/weixin_45187434/article/details/132916333