I used distributed locks in my company’s project, but I knew how to use them but didn’t understand the rules

So write an article about it

Usage scenario: transaction services, using redis distributed lock, to prevent repeated submission of orders, oversold problems

What conditions a distributed lock should have

  • In distributed systems, a method can only be executed by one thread on one machine at a time
  • Highly available lock acquisition and lock release
  • High-performance lock acquisition and lock release
  • Reentrant feature (reentrant, used concurrently by more than one task without worrying about data errors)
  • Lock failure mechanism to prevent deadlocks
  • It has the non-blocking lock feature, that is, if the lock is not obtained, the system returns a failure to obtain the lock

The implementation of distributed lock

  1. Database based optimistic/pessimistic locking
  2. Redis Distributed Locks (this article) : LeveragesetnxCommand. This command is atomic and can be executed only when the key does not existset, which means the thread has acquired the lock
  3. Zookeeper Distributed lock: Uses the sequential temporary nodes of Zookeeper to implement distributed locks and wait queues. Zookeeper is designed to implement distributed lock services
  4. Memcached:addCommand. This command is atomic and can be executed only when the key does not existadd, which means that the thread has acquired the lock

How does Redis implement locking?

In Redis, there is a command that implements the lock

SETNX key value
Copy the code

This command sets the value of the key to value if and only if the key does not exist. If the given key already exists, SETNX does nothing. If the setting is successful, return 1. Setting failed, return 0

This is the logic behind using Redis to implement locks

Thread 1 lock acquisition -- > setnx lockKey lockValue -- > 1 lock acquisition success thread 2 lock acquisition -- > setnx lockKey lockValue -- > 0 lock acquisition failure Thread 1 unlocked the lock -- > Thread 2 acquired the lock -- > setnx lockKey lockValue -- > 1 acquired the lock successfullyCopy the code

Next we will implement redis distributed lock based on Springboot

1. The introduction ofredis, for springmvc,lombokRely on

<? xml version="1.0" encoding="UTF-8"? > <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0. 0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId>  <version>2.4. 0</version> <relativePath/> <! -- lookup parent from repository --> </parent> <groupId>cn.miao.redis</groupId> <artifactId>springboot-caffeine-demo</artifactId> <version>0.01.-SNAPSHOT</version>
    <name>springboot-redis-lock-demo</name>
    <description>Demo project for Redis Distribute Lock</description>

    <properties>
        <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <! --redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>2.14..RELEASE</version> </dependency> <! --springMvc--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId>  <version>2.33..RELEASE</version> </dependency> <! -- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.1812.</version>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
Copy the code

2. New RedisDistributedLock. Java and write lock unlock logic

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.RedisStringCommands;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.types.Expiration;

import java.nio.charset.StandardCharsets;

/ * * *@authorMiao * Redis Locking tool class */
@Slf4j
public class RedisDistributedLock {

    /** * Timeout duration */
    private static final long TIMEOUT_MILLIS = 15000;

    /** * Number of retries */
    private static final int RETRY_TIMES = 10;

    /*** * Sleep time */
    private static final long SLEEP_MILLIS = 500;

    /** * Lua script used to lock ** Deprecated because the new Redis lock operation is atomic */
    private static final String LOCK_LUA =
            "if redis.call(\"setnx\",KEYS[1],ARGV[1]) == 1 " +
                    "then " +
                    " return redis.call('expire',KEYS[1],ARGV[2]) " +
                    "else " +
                    " return 0 " +
                    "end";

    If redis. Get (KEYS[1]) == ARGV[1], then redis delete KEYS[1] * otherwise return 0 * KEYS[1], ARGV[1] is an argument, KEYS[1] is used to pass key values in Redis * ARGV[1] is used to pass value values in Redis */
    private static final String UNLOCK_LUA =
            "if redis.call(\"get\",KEYS[1]) == ARGV[1] "
                    + "then "
                    + " return redis.call(\"del\",KEYS[1]) "
                    + "else "
                    + " return 0 "
                    + "end ";

    /** * Check if redisKey is locked **@param redisKey redisKey
     * @param template template
     * @return Boolean
     */
    public static Boolean isLock(String redisKey, String value, RedisTemplate<Object, Object> template) {

        return lock(redisKey, value, template, RETRY_TIMES);
    }

    private static Boolean lock(String redisKey,
                                String value,
                                RedisTemplate<Object, Object> template,
                                int retryTimes) {

        boolean result = lockKey(redisKey, value, template);

        while(! (result) && retryTimes-- >0) {
            try {

                log.debug("lock failed, retrying... {}", retryTimes);
                Thread.sleep(RedisDistributedLock.SLEEP_MILLIS);
            } catch (InterruptedException e) {

                return false;
            }
            result = lockKey(redisKey, value, template);
        }

        return result;
    }


    private static Boolean lockKey(final String key,
                                   final String value,
                                   RedisTemplate<Object, Object> template) {
        try {

            RedisCallback<Boolean> callback = (connection) -> connection.set(
                    key.getBytes(StandardCharsets.UTF_8),
                    value.getBytes(StandardCharsets.UTF_8),
                    Expiration.milliseconds(RedisDistributedLock.TIMEOUT_MILLIS),
                    RedisStringCommands.SetOption.SET_IF_ABSENT
            );

            return template.execute(callback);
        } catch (Exception e) {

            log.info("lock key fail because of ", e);
        }

        return false;
    }


    /** * Release the distributed lock resource **@param redisKey key
     * @param value    value
     * @param template redis
     * @return Boolean
     */
    public static Boolean releaseLock(String redisKey, String value, RedisTemplate
       
         template)
       ,> {
        try {
            RedisCallback<Boolean> callback = (connection) -> connection.eval(
                    UNLOCK_LUA.getBytes(),
                    ReturnType.BOOLEAN,
                    1,
                    redisKey.getBytes(StandardCharsets.UTF_8),
                    value.getBytes(StandardCharsets.UTF_8)
            );

            return template.execute(callback);
        } catch (Exception e) {

            log.info("release lock fail because of ", e);
        }

        return false; }}Copy the code

Supplement:

1. spring-data-redisStringRedisTemplaandRedisTemplateTwo, but I chose itRedisTemplate“Because he is more versatile. The difference is: You can use StringRedisTemplate if you are storing string data in your Redis database or if you are accessing string data, but if your data is a complex object type and you don’t want to do any conversion to retrieve it, Pulling an object directly from Redis is a better option to use the RedisTemplate.
2. Lua script is selected because script execution is atomic and no client can operate during script execution. Therefore, Lua script is used to release locks.
The latest version of Redis locks to ensure the atomicity of redis value and automatic expiration time, using no Lua script

3. Create a test classTestController

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/ * * *@author miao
 */
@RestController
@Slf4j
public class TestController {

    @Resource
    private RedisTemplate<Object, Object> redisTemplate;

    @PostMapping("/order")
    public String createOrder(a) throws InterruptedException {

        log.info("Start creating order");

        Boolean isLock = RedisDistributedLock.isLock("testLock"."456789", redisTemplate);

        if(! isLock) { log.info("The lock is occupied.");
            return "fail";
        } else {
            / /... Processing logic
        }

        Thread.sleep(10000);
        // Remember to release the lock, otherwise there will be a problem
        RedisDistributedLock.releaseLock("testLock"."456789", redisTemplate);

        return "success"; }}Copy the code

4. Test using Postman

5. Disadvantages of Redis distributed lock

Above we are talking about redis, which is a single point case. The situation is different in the Redis Sentinel cluster. In the Redis Sentinel cluster, we have multiple Redis with a master-slave relationship among them, such as one master and two slave. The data corresponding to our set command is written to the master and then synchronized to the slave. When we apply for a lock, the command setnx mykey myValue is used. In the Redis Sentinel cluster, this command falls on the primary library first. If the primary database is down and the data is not synchronized to the secondary database, sentinel will elect one of the secondary databases as the primary database. If setnx mykey hisValue is executed by another client, it will also succeed, that is, it will also get the lock. This means that at this point, two clients have acquired the lock. This is not desirable, although the record of this happening is small and only occurs during a master/slave failover. It is tolerated by most systems in most cases, but not all systems.

6. Optimization of Redis distributed lock

In order to solve the defect in the case of failover, Antirez invented the Redlock algorithm. Using the Redlock algorithm, multiple Redis instances are required. When locking, it will send the setex mykey myValue command to half of the nodes. Then the lock is successful. To release the lock, issue the del command to all nodes. This is a mechanism based on which [most agree]. Those who are interested can consult relevant information. For practical use, we can choose from existing open source implementations such as Redlock-py in Python and Redisson Redlock in Java.

Redlock does solve the “wonky situation” mentioned above. But as it solves the problem, it comes at a cost. You need multiple instances of Redis, you need to introduce new library code and you need to tweak it, and it hurts performance. Therefore, as expected, there is no “perfect solution”, we need to be able to solve the problem according to the actual situation and conditions.

So far, I’ve covered the issues with redis distributed locking in general (I’ll keep updating if I have any new insights).


** Distributed lock comparison **

Database distributed lock implementation

Disadvantages:

  1. Db operation performance is poor, and there is a risk of locking tables
  2. If a non-blocking operation fails, polling is required, occupying CPU resources
  3. If you do not commit or poll for a long time, connection resources may be occupied

Redis distributed lock implementation

Disadvantages:

  1. The expiration time of lock deletion failure cannot be controlled
  2. If the operation fails, polling is required, occupying CPU resources

ZK distributed lock implementation

Disadvantages:

  1. The performance is not as good as the Redis implementation, mainly because write operations (acquiring locks and releasing locks) need to be performed on the Leader and then synchronized to the follower.

conclusion

From the perspective of ease of understanding (from low to high) Database > Redis >Zookeeper From the perspective of implementation complexity (from low to high) Zookeeper >= Redis > Database From the perspective of performance (from high to low) Cache >Zookeeper >= Database From the perspective of reliability (from highest to lowest) Zookeeper > Redis > database