Handwritten Redis distributed lock (1)

Handwritten Redis distributed lock (1)

Thinking: Why do you need distributed locks?

? Assuming that in the shopping scene, there are two services that reduce the inventory at the same time, then ordinary synchronize or lock will not work, because synchronize can only guarantee thread safety in one jvm. At this time, the two services are in different jvms. Resource classes (inventory) are shared between different servers, and then synchronize will fail, so distributed locks are needed to solve this problem.

Conditions that distributed locks need to meet

  1. Exclusiveness: Only one thread can hold it at any time.
  2. High availability: 1) Can handle high concurrency scenarios. 2) In the Redis cluster environment, it is not possible to fail to acquire and release locks because a certain node is hung up.
  3. Anti-deadlock: There must be a timeout control mechanism or undo operation, and a comprehensive termination and exit plan.
  4. No random snatching: prevent ostentation, you cannot unlock other people’s locks, and you can release the locks you add yourself.
  5. Reentrancy: If the same thread on the same node acquires the lock, it can also acquire the lock again.

1. Case 1

In the case of using Lock, simulate two services accessing data in Redis at the same time.

  1. Preparation:
    1. Create a new service with port number 7777, where the parent project pom, application.properties, main startup class, controller, service, RedisConfig, and Swagger2Config files are:
    2. Same as 7777, create a new service with port number 8888.
    3. Configure load balancing between 7777 and 8888 on nginx.
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.hyw.redislock</groupId>
    <artifactId>redis_distributed_lock</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>redis_distributed_lock3</module>
        <module>redis_distributed_lock2</module>
    </modules>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.12</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>


    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <lombok.version>1.16.18</lombok.version>
    </properties>



    <dependencies>
        <!--SpringBoot common dependency module-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--SpringBoot and Redis integration dependencies -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>
        <!--swagger2-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>
        <!--Common basic configuration boottest/lombok/hutool-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.8.8</version>
        </dependency>
    </dependencies>

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

</project>
server.port=7777

spring.application.name=redis_distributed_lock2
# =========================== swagger2======================
# http://localhost:7777/swagger-ui.html
swagger2.enabled=true
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

# =========================== redis stand-alone =======================
spring.redis.database=0
spring.redis.host=192.168.80.128
spring.redis.port=6379
spring.redis.password=1234
spring.redis.lettuce.pool.max-active=8
spring.redis.lettuce.pool.max-wait=-1ms
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=0
package com.hyw.redislock;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @auther zzyy
 * @create 2022-10-12 22:20
 */
@SpringBootApplication
public class RedisDistributedLockApp7777
{<!-- -->
    public static void main(String[] args)
    {<!-- -->
        SpringApplication.run(RedisDistributedLockApp7777.class, args);
    }
}
package com.hyw.redislock.controller;


import com.hyw.redislock.service.InventoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Api(tags = "redis distributed lock test")
public class InventoryController
{<!-- -->
    @Autowired
    private InventoryService inventoryService;

    @ApiOperation("Deduct inventory, sell one at a time")
    @GetMapping(value = "/inventory/sale")
        public String sale()
        {<!-- -->
            return inventoryService. sale();
    }
}
package com.hyw.redislock.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Service
@Slf4j
public class InventoryService
{<!-- -->
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;

    private Lock lock = new ReentrantLock();

    public String sale()
    {<!-- -->
        String retMessage = "";
        lock. lock();
        try
        {<!-- -->
            //1 Query inventory information
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 Determine whether the inventory is sufficient
            Integer inventoryNumber = result == null ? 0 : Integer. parseInt(result);
            //3 Deduct inventory
            if(inventoryNumber > 0) {<!-- -->
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "Successfully sold a product, remaining inventory: " + inventoryNumber;
                System.out.println(retMessage);
            }else{<!-- -->
                retMessage = "Product sold out, o(╥﹏╥)o";
            }
        }finally {<!-- -->
            lock. unlock();
        }
        return retMessage + "\t" + "Service port number:" + port;
    }
}
package com.hyw.redislock.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * @auther zzyy
 * @create 2022-07-02 11:25
 */
@Configuration
public class RedisConfig
{<!-- -->
    @Bean
    public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory)
    {<!-- -->
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(lettuceConnectionFactory);
        //Set key serialization method string
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        //Set the serialization method of value json
        redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());

        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }
}
package com.hyw.redislock.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Configuration
@EnableSwagger2
public class Swagger2Config
{<!-- -->
    @Value("${swagger2. enabled}")
    private Boolean enabled;

    @Bean
    public Docket createRestApi() {<!-- -->
        return new Docket(DocumentationType. SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(enabled)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.atguigu.redislock")) //your own package
                .paths(PathSelectors. any())
                .build();
    }
    private ApiInfo apiInfo() {<!-- -->
        return new ApiInfoBuilder()
                .title("springboot uses swagger2 to build api interface documents " + "\t" + DateTimeFormatter.ofPattern("yyyy-MM-dd").format(LocalDateTime.now()))
                .description("springboot + redis integration")
                .version("1.0")
                .termsOfServiceUrl("https://www.baidu.com/")
                .build();
    }

}
  1. Phenomenon: Perform high concurrency test on jmeter: It is found that two commodities are sold many times.
  2. Explanation: In a stand-alone environment, you can use synchronized or Lock to achieve. But in a distributed system, because the competing threads may not be on the same node (in the same jvm), it needs a lock that can be accessed by all processes (such as redis or zookeeper to build), different process jvm The lock at the level does not work, so you can use a third-party component to acquire the lock. If the lock is not acquired, the current thread that wants to run will be blocked.

2. Case 2

? Considering that in a distributed system, the Lock of a single machine is invalid, we use the SETNX command in Redis to implement distributed locks

  1. Idea: The command SET resource-name anystring NX EX max-lock-time is a simple way to implement locking mechanism with Redis. Among them, resource-name is set to "myRedisLock", and anystring is set to UUID + thread ID to uniquely A that identifies the thread holding the lock.

    • EX seconds – Set the specified expire time, in seconds.

    • PX milliseconds – Set the specified expire time, in milliseconds.

    • NX – Only set the key if it does not already exist.

    • XX – Only set the key if it already exists.

    • EX seconds – set the expiration time of the key, in seconds

    • PX milliseconds – set the expiration time of the key, in milliseconds

    • NX – the value of the key will only be set if the key does not exist

    • XX – the value of the key will only be set if the key exists

If the above command returns OK, then the client can acquire the lock (if the above command returns Nil, then the client can try again after a period of time), and the lock can be released by the DEL command. The SETNX command in java is stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS). It means to create a “lock” whose key-value pair is key-uuidValue, and the expiration time is 30s.

After the client locks, if you don’t actively release the lock you created (you can only release the lock you created yourself, in order to don’t grab it randomly: to prevent misleading, you can’t unlock other people’s locks, you can release the lock you added yourself), are automatically released after the expiration time (prevents deadlock).

? Schematic diagram of Zhang Guan Li Dai (process A deletes the lock added by process B, resulting in no lock for process B to delete)

Modify InventoryService to

package com.hyw.redislock.service;

import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

@Service
@Slf4j
public class InventoryService
{<!-- -->
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;

    private Lock lock = new ReentrantLock();

    public String sale()
    {<!-- -->
        String retMessage = "";
        String key = "RedisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();
        // By spinning, try to acquire the lock every 20ms
        while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS))
        {<!-- -->
            // Pause for milliseconds
            try {<!-- --> TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) {<!-- --> e.printStackTrace(); }
        }
        try
        {<!-- -->
            //1 Query inventory information
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 Determine whether the inventory is sufficient
            Integer inventoryNumber = result == null ? 0 : Integer. parseInt(result);
            //3 Deduct inventory
            if(inventoryNumber > 0) {<!-- -->
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "Successfully sold a product, remaining inventory: " + inventoryNumber + "\t" + uuidValue;
                System.out.println(retMessage);
            }else{<!-- -->
                retMessage = "Product sold out, o(╥﹏╥)o";
            }
        }finally {<!-- -->
            // Judging whether locking and unlocking are the same client, the same one is fine, you can only delete your own lock, not delete others' by mistake
            if(stringRedisTemplate.opsForValue().get(key).equalsIgnoreCase(uuidValue)){<!-- -->
                stringRedisTemplate.delete(key);
            }
        }
        return retMessage + "\t" + "Service port number:" + port;
    }
}

3. Case 3

? In Case 2, we seem to have implemented distributed locks, but there is still such a problem: when deleting locks, the if judgment and deletion operations do not have atomicity. In order to solve this problem, we use Lua scripts to write distributed lock judgment + deletion codes to ensure the atomicity of operations.

package com.hyw.redislock.service;

import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @auther zzyy
 * @create 2022-10-22 15:14
 */
@Service
@Slf4j
public class InventoryService
{<!-- -->
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Value("${server.port}")
    private String port;

    private Lock lock = new ReentrantLock();

    public String sale()
    {<!-- -->
        String retMessage = "";
        String key = "zzyyRedisLock";
        String uuidValue = IdUtil.simpleUUID() + ":" + Thread.currentThread().getId();

        while(!stringRedisTemplate.opsForValue().setIfAbsent(key, uuidValue,30L,TimeUnit.SECONDS))
        {<!-- -->
            // Pause for milliseconds
            try {<!-- --> TimeUnit.MILLISECONDS.sleep(20); } catch (InterruptedException e) {<!-- --> e.printStackTrace(); }
        }

        try
        {<!-- -->
            //1 Query inventory information
            String result = stringRedisTemplate.opsForValue().get("inventory001");
            //2 Determine whether the inventory is sufficient
            Integer inventoryNumber = result == null ? 0 : Integer. parseInt(result);
            //3 Deduct inventory
            if(inventoryNumber > 0) {<!-- -->
                stringRedisTemplate.opsForValue().set("inventory001",String.valueOf(--inventoryNumber));
                retMessage = "Successfully sold a product, remaining inventory: " + inventoryNumber + "\t" + uuidValue;
                System.out.println(retMessage);
            }else{<!-- -->
                retMessage = "Product sold out, o(╥﹏╥)o";
            }
        }finally {<!-- -->
            //Merge judgment + delete yourself into a lua script to ensure atomicity
            String luaScript =
                    "if (redis. call('get',KEYS[1]) == ARGV[1]) then " +
                            "return redis. call('del',KEYS[1]) " +
                            "else " +
                            "return 0 " +
                            "end";
            stringRedisTemplate.execute(new DefaultRedisScript<>(luaScript, Boolean.class), Arrays.asList(key), uuidValue);
        }
        return retMessage + "\t" + "Service port number:" + port;
    }
}

Question: What about the reentrancy of the lock? Please see Handwritten Redis Distributed Lock (2).