Implementation method and implementation principle of Redis distributed lock

Insert image description here

Hello everyone, I am Nezha.

The company wants to recruit a back-end programmer with 5 years of development experience. After reading many resumes, I found a common problem. Most of them have never used distributed locks. Is this normal?

Below is the personal skill package of a friend who has already joined the company. At first glance, it looks okay and I have never used distributed locks.

During the lunch break, I chatted with her. She used to work in a company that dealt with Japan.

  1. Demand is determined by the product;
  2. System design and detailed design are done by PM;
  3. The interface documents and database design documents are provided by Japanese companies;
  4. The most exaggerated thing is that even the color of the button is marked in the document...
  5. All she needs to do is to code and test according to the interface document and it will be ok.

Some interviewees have worked for 5 years, working on their own products and only having two projects. They have been fixing bugs, adding requirements, and doing operation and maintenance over and over again. The technology stack is very old, and it is still SSM. It has been transformed recently and finally used SpringBoot...
Microservices, message middleware, and distributed locks have not been used at all...

There are also interviewees who work in a big factory in XX, and have done C# for one year and Go for one year. At first glance, their resumes look very gorgeous and they are proficient in three languages . However, in fact, they are all in the entry-level stage and have no competitiveness at all.

On a whim, I have said too much, so let’s get back to the point. I will summarize an article on distributed locks, enrich my resume, improve my interview level, add some talking points to myself, and become an interview expert in seconds. BAT is not a dream.

1. The importance and challenges of distributed locks

1.1 Concurrency issues in distributed systems

In modern distributed systems, various concurrency problems often occur due to multiple nodes operating shared resources at the same time. These problems include race conditions, data inconsistencies, deadlocks, etc., which pose challenges to the stability and reliability of the system. Let’s dive into some concurrency issues that arise in distributed systems:

race condition

A race condition occurs when multiple processes or threads create uncertainty in the order of execution, causing the program's behavior to become unpredictable. In a distributed system, multiple nodes access shared resources at the same time. Without a suitable synchronization mechanism, race conditions may occur. For example, in an e-commerce platform, multiple users try to purchase a limited product at the same time. If there is no good synchronization mechanism, it may lead to oversold problems.

Data is inconsistent

In a distributed system, data consistency may be affected due to splitting and copying of data. Multiple nodes modify the same data at the same time. Without appropriate synchronization measures, data inconsistency may result. For example, in a social network application, users modify their personal information on different nodes. If there is no synchronization mechanism, data may be inconsistent and affect the user experience.

deadlock

Deadlock is when multiple processes or threads fall into an infinite waiting state because they are waiting for each other to release resources. In a distributed system, multiple nodes may compete for resources at the same time. Without a good coordination mechanism, deadlock may occur. For example, multiple nodes try to acquire a set of distributed locks at the same time, but due to improper order, it may cause a deadlock problem. .

2. Basic principles and implementation methods of distributed locks

2.1 Basic concepts of distributed locks

Distributed locks are a key concept in distributed systems and are used to solve concurrency problems that may arise when multiple nodes access shared resources at the same time. The following are some basic concepts of distributed locks:

  • Lock : A lock is a synchronization mechanism used to ensure that only one node (process or thread) can access shared resources at any time. Locks prevent race conditions and data inconsistencies.

  • Shared Resource : Shared resources are data, files, services, etc. that multiple nodes need to access or modify. In a distributed system, multiple nodes may try to access these shared resources simultaneously, causing problems.

  • Lock status : Locks usually have two states, namely locked state and unlocked state. In the locked state, only the node holding the lock can access the shared resource, and other nodes are blocked. In the unlocked state, any node can attempt to acquire the lock.

  • Race Condition : Race condition refers to the uncertainty in the execution order of multiple nodes, causing the behavior of the program to become unpredictable. In a distributed system, race conditions may cause multiple nodes to access shared resources at the same time, destroying the consistency of the system.

  • Data Inconsistency : Data inconsistency means that multiple nodes modify the same data, but due to the lack of synchronization mechanism, the data may be in an inconsistent state. This can cause errors or unexpected behavior in the application.

  • Deadlock : Deadlock is a state in which multiple nodes are stuck in an infinite waiting state because they are waiting for each other to release resources. In a distributed system, multiple nodes may compete for resources at the same time. Without a good coordination mechanism, deadlock may occur.

The basic goal of distributed locks is to solve these problems and ensure that multiple nodes can operate safely and orderly when accessing shared resources, thereby maintaining data consistency and system stability.

2.2 Database-based distributed lock

Principle and implementation

A common distributed lock implementation is database-based. In this approach, each node first attempts to insert a record with a unique constraint in the database before accessing the shared resource. If the insertion is successful, it means that the node successfully acquired the lock; otherwise, it means that the lock has been occupied by other nodes.

The principle of database distributed lock is relatively simple, but there are some issues that need to be considered in its implementation. Here are some key points:

  • Unique Constraint : The unique constraint in the database ensures that only one node can successfully insert the lock record. This can be achieved through the table structure of the database, ensuring that the key to the lock record is unique.

  • Timeout : In order to avoid the node crashing after acquiring the lock and causing the lock to be unable to be released, it is usually necessary to set the timeout of the lock. If the node does not complete the operation within the timeout period, the lock will be automatically released and other nodes can acquire the lock.

  • Transaction : The database transaction mechanism can ensure data consistency. During the process of acquiring and releasing locks, transactions can be used to wrap the operations to ensure that the operations are atomic.

In the figure, node A tries to insert a lock record in the database. If the insertion is successful, it means that node A has acquired the lock and can perform operations. After the operation is completed, node A releases the lock. If the insertion fails, it means that the lock is already occupied by other nodes, and node A needs to handle the lock contention situation.

Advantages and Disadvantages

advantage shortcoming
The implementation is relatively simple and does not require the introduction of additional components. The performance is relatively poor and the IO overhead of the database is large.
You can use the database's transaction mechanism to ensure data consistency. Deadlocks are prone to occur and require careful design.
It is not suitable for high concurrency scenarios and may become a bottleneck of the system.

This table provides a concise summary of the advantages and disadvantages of database-based distributed locks.

2.3 Cache-based distributed lock

Principle and implementation

Another common and more efficient way to implement distributed locks is based on a caching system, such as Redis. In this way, each node tries to set a key with an expiration time in the cache. If the setting is successful, it means that the lock is acquired; otherwise, it means that the lock is already occupied by other nodes.

Cache-based distributed locks are usually implemented using atomic operations to ensure that lock acquisition is safe in a concurrent environment. Redis provides something like SETNX(SET if Not

eXists) command to implement this atomic operation. In addition, we can also set an expiration time for the lock to prevent the node from crashing after acquiring the lock and causing the lock to never be released.

In the above figure, node A attempts to set a lock key with an expiration time in the cache system. If the setting is successful, it means that node A has acquired the lock and can perform operations. After the operation is completed, node A releases the lock key. If the setting fails, it means that the lock is already occupied by other nodes, and node A needs to handle the lock contention situation.

Advantages and Disadvantages

advantage shortcoming
The performance is high, the cache system usually operates in memory, and the IO overhead is small. There may be problems such as cache invalidation and node crashes, which require additional processing.
You can use cached atomic operations to ensure the safety of acquiring locks. The need to rely on an external caching system introduces system complexity.
It is suitable for high concurrency scenarios and is not likely to become the bottleneck of the system.

Through database-based and cache-based distributed lock implementations, we can better understand the basic principles of distributed locks and their respective advantages and disadvantages. Based on actual application scenarios and performance requirements, it is very important to choose an appropriate distributed lock implementation.

3. Implementation and use of Redis distributed lock

3.1 Use the SETNX command to implement distributed locks

In Redis, you can use SETNXthe (SET if Not eXists) command to implement basic distributed locks. SETNXThe command will try to set a key-value pair in the cache. If the key does not exist, the setting is successful and 1 is returned; if the key already exists, the setting fails and 0 is returned. Through this mechanism, we can use it SETNXto create distributed locks.

SETNXThe following is an example of Java code that uses commands to implement distributed locks:

import redis.clients.jedis.Jedis;

public class DistributedLockExample {
    
    

    private Jedis jedis;

    public DistributedLockExample() {
    
    
        jedis = new Jedis("localhost", 6379);
    }

    public boolean acquireLock(String lockKey, String requestId, int expireTime) {
    
    
        Long result = jedis.setnx(lockKey, requestId);
        if (result == 1) {
    
    
            jedis.expire(lockKey, expireTime);
            return true;
        }
        return false;
    }

    public void releaseLock(String lockKey, String requestId) {
    
    
        String storedRequestId = jedis.get(lockKey);
        if (storedRequestId != null && storedRequestId.equals(requestId)) {
    
    
            jedis.del(lockKey);
        }
    }

    public static void main(String[] args) {
    
    
        DistributedLockExample lockExample = new DistributedLockExample();

        String lockKey = "resource:lock";
        String requestId = "request123";
        int expireTime = 60; // 锁的过期时间

        if (lockExample.acquireLock(lockKey, requestId, expireTime)) {
    
    
            try {
    
    
                // 执行需要加锁的操作
                System.out.println("Lock acquired. Performing critical section.");
                Thread.sleep(1000); // 模拟操作耗时
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                lockExample.releaseLock(lockKey, requestId);
                System.out.println("Lock released.");
            }
        } else {
    
    
            System.out.println("Failed to acquire lock.");
        }
    }
}

3.2 Setting timeout and preventing deadlock

In the above code, we expireset an expiration time for the lock by using a command to prevent the node from crashing or exiting abnormally after acquiring the lock, causing the lock to never be released. Setting a reasonable expiration time can avoid resource waste caused by locks being occupied for a long time.

3.3 Lock reentrancy and thread safety

One issue that needs to be considered with distributed locks is reentrancy , that is, whether the same thread can acquire the same lock multiple times without being blocked. Normally, distributed locks are not reentrant, because each time a lock is acquired, a new identity (such as requestId) is generated, which will not be the same as the previous identity.

In order to solve the problem of reentrancy, we can introduce a counter to record the number of times a certain thread acquires the lock. When the thread tries to acquire the lock again, it will only actually acquire the lock if the counter is 0, otherwise it will only increment the counter. When the lock is released, the counter is decremented until it reaches 0 before the lock is actually released.

It should be noted that in order to ensure the thread safety of distributed locks, we should use thread local variables for storage requestIdto prevent interference between different threads.

4. Advanced applications and performance considerations of distributed locks

4.1 Selection of lock granularity

In the application of distributed locks, it is very important to choose the appropriate lock granularity. The choice of lock granularity will directly affect the performance and concurrency of the system. Generally speaking, lock granularity can be divided into coarse-grained locks and fine-grained locks.

  • Coarse-grained locking: Locking a larger range of code blocks may reduce concurrency, but reduces locking overhead. It is suitable for scenarios that do not have high requirements on data consistency but low concurrency performance requirements.
  • Fine-grained locking: Locking a smaller range of code blocks improves concurrency performance, but may increase lock overhead. It is suitable for scenarios that require high data consistency but high concurrency performance.

When selecting lock granularity, you need to weigh it based on specific business scenarios and performance requirements to avoid excessive locking or insufficient locking.

4.2 Multi-Redis instance lock based on RedLock

The RedLock algorithm is an algorithm that implements distributed locks on multiple Redis instances to improve the reliability of locks. Since a single Redis instance may cause distributed lock failure due to failure or network problems, by using multiple Redis instances, we can reduce the probability of lock failure.

The basic idea of ​​the RedLock algorithm is to create the same lock on multiple Redis instances and use SETNXcommands to try to acquire the lock. When acquiring a lock, you also need to check the timestamps of most Redis instances to ensure that the timestamps of the locks on multiple instances are consistent. Only when the timestamps of most instances are consistent, the lock acquisition is considered successful.

The following is a Java code example of a distributed lock based on RedLock:

import redis.clients.jedis.Jedis;

public class RedLockExample {
    
    

    private static final int QUORUM = 3;
    private static final int LOCK_TIMEOUT = 500;

    private Jedis[] jedisInstances;

    public RedLockExample() {
    
    
        jedisInstances = new Jedis[]{
    
    
            new Jedis("localhost", 6379),
            new Jedis("localhost", 6380),
            new Jedis("localhost", 6381)
        };
    }

    public boolean acquireLock(String lockKey, String requestId) {
    
    
        int votes = 0;
        long start = System.currentTimeMillis();
        
        while ((System.currentTimeMillis() - start) < LOCK_TIMEOUT) {
    
    
            for (Jedis jedis : jedisInstances) {
    
    
                if (jedis.setnx(lockKey, requestId) == 1) {
    
    
                    jedis.expire(lockKey, LOCK_TIMEOUT / 1000); // 设置锁的超时时间
                    votes++;
                }
            }
            
            if (votes >= QUORUM) {
    
    
                return true;
            } else {
    
    
                // 未获取到足够的票数,释放已获得的锁
                for (Jedis jedis : jedisInstances) {
    
    
                    if (jedis.get(lockKey).equals(requestId)) {
    
    
                        jedis.del(lockKey);
                    }
                }
            }
            
            try {
    
    
                Thread.sleep(50); // 等待一段时间后重试
            } catch (InterruptedException e) {
    
    
                Thread.currentThread().interrupt();
            }
        }
        
        return false;
    }

    public void releaseLock(String lockKey, String requestId) {
    
    
        for (Jedis jedis : jedisInstances) {
    
    
            if (jedis.get(lockKey).equals(requestId)) {
    
    
                jedis.del(lockKey);
            }
        }
    }

    public static void main(String[] args) {
    
    
        RedLockExample redLockExample = new RedLockExample();
        
        String lockKey = "resource:lock";
        String requestId = "request123";

        if (redLockExample.acquireLock(lockKey, requestId)) {
    
    
            try {
    
    
                // 执行需要加锁的操作
                System.out.println("Lock acquired. Performing critical section.");
                Thread.sleep(1000); // 模拟操作耗时
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            } finally {
    
    
                redLockExample.releaseLock(lockKey, requestId);
                System.out.println("Lock released.");
            }
        } else {
    
    
            System.out.println("Failed to acquire lock.");
        }
    }
}

4.3 Performance considerations of distributed locks

The introduction of distributed locks will increase the complexity and performance overhead of the system, so when using distributed locks, you need to consider its impact on system performance.

Some performance optimization methods include:

  • Reduce lock holding time: Try to shorten the locking time in the code block to reduce lock competition and blocking.
  • Use fine-grained locks: avoid locking too many resources at one time, try to choose the appropriate lock granularity, and reduce the lock granularity.
  • Choose a high-performance lock implementation: For example, cache-based distributed locks are usually more performant than database locks.
  • Set the lock timeout reasonably: avoid long-term lock occupation, resulting in waste of resources.
  • Consider concurrency and performance requirements: Reasonably design lock strategies and solutions based on the concurrency and performance requirements of the system.

Advanced applications of distributed locks require the selection of appropriate strategies based on actual conditions to ensure system performance and consistency. When considering performance optimization, factors such as lock granularity, concurrency, and reliability need to be comprehensively considered.

5. Comparison of common concurrency problems and distributed lock solutions

5.1 Data consistency issues in high concurrency scenarios

In high-concurrency scenarios, data consistency is a common problem. Multiple concurrent requests modify the same data at the same time, which may lead to data inconsistency. Distributed lock is one of the effective solutions to solve this problem. Compared with other solutions, it has the following advantages:

  • Atomicity guarantee: Distributed locks can guarantee the atomicity of a set of operations, thereby ensuring that only one of multiple operations can be executed at the same time, avoiding concurrency conflicts.
  • Simple and easy to use: The use of distributed locks is relatively simple. Through the operations of locking and releasing locks, the consistency of data can be effectively guaranteed.
  • Widely applicable: Distributed locks are suitable for different data storage systems, such as relational databases, NoSQL databases and cache systems.

In contrast, other solutions may require more complex logic and additional processing, such as using optimistic locking, pessimistic locking, distributed transactions, etc. Although these solutions are effective in some scenarios, distributed locks, as a general solution, can provide simple and reliable data consistency guarantees in most cases.

5.2 Unique constraints and distributed locks

Uniqueness constraints are another common concurrency problem that involves ensuring that certain operations can only be performed once, avoiding duplicate operations. For example, in a distributed environment, we may need to ensure that only one user can create a certain resource, or that only one task can be executed.

Distributed locks can well solve the problem of unique constraints. When a request attempts to acquire a distributed lock, if the acquisition is successful, it means that the request has obtained execution rights and can perform operations that require unique constraints. Failure of other requests to acquire the lock means that there is already a request performing the same operation, thus avoiding repeated operations.

Compared with other solutions, the implementation of distributed locks is relatively simple and does not require modifying the data table structure or adding additional constraints. Other solutions may involve database uniqueness constraints, queue consumer deduplication, etc., which may require more processing and adjustments.

6. Best practices and precautions

6.1 Best practices for distributed locks

Distributed locks are a powerful tool, but there are some best practices that need to be followed when using them to ensure system reliability and performance. Here are some key best practices:

Choose the right scene

Distributed locks are suitable for scenarios where data consistency and concurrency control need to be ensured, but not all situations require the use of distributed locks. In the design, business needs should be carefully evaluated, and appropriate scenarios should be selected to use distributed locks to avoid unnecessary complexity.

Example :

Scenarios suitable for using distributed locks include: order payment, inventory deduction and other operations that require strong consistency and avoid concurrency problems.

Counterexample :

For read-only operations or data-insensitive operations, distributed locks may not be needed to avoid introducing unnecessary complexity.

Lock granularity selection

When using distributed locks, choosing the appropriate lock granularity is crucial. A lock granularity that is too large may cause performance degradation, while a lock granularity that is too small may increase the risk of lock contention. The appropriate lock granularity needs to be selected based on the business scenario and data model.

Example :

In the order system, if you need to operate multiple orders at the same time, you can set the lock granularity to the granularity of each order instead of the granularity of the entire system.

Counterexample :

If the lock granularity is too large, such as locking at the entire system level, concurrency performance may decrease.

Set a reasonable timeout

Setting reasonable timeouts for locks is an important step in preventing deadlocks and resource waste. A too long timeout may cause the lock to be occupied for a long time, while a too short timeout may cause the lock to be released frequently, increasing the possibility of lock contention.

Example :

If the normal execution time of an operation does not exceed 5 seconds, you can set the lock timeout to 10 seconds to ensure that the lock can be released under normal circumstances.

Counterexample :

Setting a timeout that is too long may cause the lock to be occupied for a long time, resulting in a waste of resources.

6.2 Avoid common pitfalls and mistakes

There are also some common pitfalls and mistakes you need to be aware of when using distributed locks to avoid introducing more problems:

Release lock repeatedly

When releasing the lock, ensure that only requests to acquire the lock can perform the release operation. Releasing locks repeatedly may cause other requests to acquire locks that they should not acquire.

Example :

// 错误的释放锁方式
if (storedRequestId != null) {
    
    
    jedis.del(lockKey);
}

Positive example :

// 正确的释放锁方式
if (storedRequestId != null && storedRequestId.equals(requestId)) {
    
    
    jedis.del(lockKey);
}

Lock reentrancy

When implementing distributed locks, it is necessary to consider the reentrancy of the lock. After a request acquires the lock, it may request the lock again in the same thread. It is necessary to ensure that the lock is reentrant during implementation.

Example :

// 错误的可重入性处理
if (lockExample.acquireLock(lockKey, requestId, expireTime)) {
    
    
    // 执行操作
    lockExample.acquireLock(lockKey, requestId, expireTime); // 错误:再次获取锁
    lockExample.releaseLock(lockKey, requestId);
}

Positive example :

// 正确的可重入性处理
if (lockExample.acquireLock(lockKey, requestId, expireTime)) {
    
    
    try {
    
    
        // 执行操作
    } finally {
    
    
        lockExample.releaseLock(lockKey, requestId);
    }
}

Correct release of locks

Ensure that the lock release operation is performed at the correct location to avoid entering the next stage without the lock being released, resulting in data inconsistency.

Example :

// 错误的锁释放位置
if (lockExample.acquireLock(lockKey, requestId, expireTime)) {
    
    
    // 执行操作
    lockExample.releaseLock(lockKey, requestId); // 错误:锁未释放就执行了下一步操作
    // 执行下一步操作
}

Positive example :

// 正确的锁释放位置
if (lockExample.acquireLock(lockKey, requestId, expireTime)) {
    
    
    try {
    
    
        // 执行操作
    } finally {
    
    
        lockExample.releaseLock(lockKey, requestId);
    }
}

Locks should not be abused

Although distributed locks can solve concurrency problems, excessive use of locks may reduce system performance. When using distributed locks, there is a trade-off between performance and consistency.

Example :

Distributed locks should not be added to every operation of the entire system to avoid too frequent lock competition and performance problems.

Positive example :

Only add distributed locks to necessary operations to ensure consistency while minimizing lock contention.

By following the above best practices and avoiding common pitfalls and mistakes, you can better use distributed locks to achieve data consistency and concurrency control, ensuring the reliability and performance of distributed systems.

Redis catches everything in one go

Brother, is the ranking list of King of Glory implemented through Redis?

Detailed explanation of Redis transactions and pipelines

Local cache, Redis data caching strategy

If you don’t know Redis in 2023, you will be eliminated.

Illustrating Redis, talking about Redis persistence, RDB snapshots and AOF logs

Redis single-threaded or multi-threaded? IO multiplexing principle

Why is the maximum number of slots in the Redis cluster 16384?

What exactly are Redis cache penetration, breakdown, and avalanche? 7 pictures to tell you

How to implement Redis distributed lock

Redis distributed cache, flash sale

The principle and application scenarios of Redis Bloom filter to solve cache penetration

Guess you like

Origin blog.csdn.net/guorui_java/article/details/132958806