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
- Exclusiveness: Only one thread can hold it at any time.
- 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.
- Anti-deadlock: There must be a timeout control mechanism or undo operation, and a comprehensive termination and exit plan.
- No random snatching: prevent ostentation, you cannot unlock other people’s locks, and you can release the locks you add yourself.
- 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.
- Preparation:
- 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:
- Same as 7777, create a new service with port number 8888.
- 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(); } }
- Phenomenon: Perform high concurrency test on jmeter: It is found that two commodities are sold many times.
- 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
-
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"
, andanystring
is set toUUID + 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).