1. Distributed lock
1.1 what is distributed lock
When we develop a stand-alone application involving concurrent synchronization, we often use synchronized or ReentrantLock to solve the problem of code synchronization between multiple threads. However, when our application works in a distributed cluster, we need a more advanced locking mechanism to deal with the data synchronization between processes across machines, which is distributed locking.
Distributed lock is a way to control synchronous access to shared resources between distributed systems. In distributed systems, it is often necessary to coordinate their actions. If different systems or different hosts of the same system share one or a group of resources, they often need to be mutually exclusive to prevent interference with each other to ensure consistency. In this case, distributed locks need to be used.
Distributed lock can be understood as: controlling the distributed system to operate the shared resources orderly, and ensuring the consistency of data through mutual exclusion.
Some students may ask, can't we solve the problem by using the lock mechanism in Java, such as synchronized and ReentrantLock? Why use distributed locks?
For a simple single project, that is, the runtime program is in the same Java virtual machine, the above Java lock mechanism can indeed solve the problem of multithreading concurrency. For example, the following program code:
public class LockTest implements Runnable { public synchronized void get() { System.out.println("1 thread -->" + Thread.currentThread().getName()); System.out.println("2 thread -->" + Thread.currentThread().getName()); System.out.println("3 thread -->" + Thread.currentThread().getName()); } public void run() { get(); } public static void main(String[] args) { LockTest test = new LockTest(); for (int i = 0; i < 10; i++) { new Thread(test, "thread -" + i).start(); } } }
The operation results are as follows:
1 thread -- > thread - 0 2 thread -- > thread - 0 3 thread -- > thread - 0 1 thread -- > thread - 2 2 thread -- > thread - 2 3 thread -- > thread - 2 1 thread -- > thread - 1 2 thread -- > thread - 1 3 thread -- > thread - 1 1 1 thread -- > thread - 3 2 thread -- > thread - 3 1 thread -- > thread - 4 2 thread -- > thread - 4 3 thread -- > thread - 4
However, in the distributed environment, the program is deployed in cluster mode, as shown in the following figure:
The above cluster deployment method will still cause thread concurrency problems, because synchronized and ReentrantLock are just jvm level locks, and there is no way to control other JVMs. That is, the above two tomcat instances can still be executed concurrently. To solve the concurrency problem in distributed environment, distributed locks must be used.
There are many ways to implement distributed locks, such as database implementation, ZooKeeper implementation, Redis implementation, etc.
1.2 why use distributed locks
In order to illustrate the importance of distributed locks, the following is a case of inventory reduction in an e-commerce project to demonstrate what problems will occur if distributed locks are not used. The code is as follows:
Step 1: import coordinates
<?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> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> <relativePath/> </parent> <groupId>com.test</groupId> <artifactId>lock-test</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!--integrate redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <version>1.4.1.RELEASE</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.3</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> </dependency> </dependencies> </project>
Step 2: configure application YML file
server: port: 8080 spring: redis: host: 68.79.63.42 port: 26379 password: test123
Step 3: write Controller
package com.test.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class StockController { @Autowired private StringRedisTemplate redisTemplate; @GetMapping("/stock") public String stock(){ int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if(stock > 0){ stock --; redisTemplate.opsForValue().set("stock",stock+""); System.out.println("Inventory deduction succeeded, remaining inventory:" + stock); }else { System.out.println("Insufficient inventory!!!"); } return "OK"; } }
Step 4: write the startup class
@SpringBootApplication public class MyApplication { public static void main(String[] args) { SpringApplication.run(MyApplication.class,args); } }
Step 5: set redis
Test method: use jmeter for pressure test, as follows:
Note: Apache JMeter is a Java based stress testing tool developed by Apache organization. It was originally designed for Web application testing, but later extended to other testing fields.
Check the console output and find that thread concurrency has occurred, as follows:
Since the current program is deployed in a Tomcat, that is, the program runs in a jvm, you can synchronize the inventory reduction code, as follows:
Test again (pay attention to recovering the data in redis). At this time, there is no thread concurrency problem. The console output is as follows:
This shows that if the program runs in a jvm, using synchronized can solve the thread concurrency problem.
Next, cluster the program (as shown in the figure below), load it through Nginx, and then test it.
Operation process:
Step 1: configure Nginx
upstream upstream_name{ server 127.0.0.1:8001; server 127.0.0.1:8002; } server { listen 8080; server_name localhost; location / { proxy_pass http://upstream_name; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }
Step 2: modify the application Change the port number in YML to 8001 and 8002 and start the program respectively
Step 3: test again with jemter, and you can see that the concurrency problem occurs again
1.3 characteristics of distributed lock
In the distributed system environment, a method can only be executed by one thread of one machine at the same time
High availability acquire lock and release lock
High performance lock acquisition and release
Reentrant feature
It has a lock failure mechanism to prevent deadlock
It has the non blocking lock feature, that is, if the lock is not obtained, it will directly return to the failure of obtaining the lock
2. Implementation scheme of distributed lock
2.1 database implementation of distributed lock
The core idea of implementing distributed lock based on database is to create a table in the database, which contains fields such as method name, and create a unique index on the method name field. To execute a method, first insert the method name into the table. If the insertion is successful, obtain the lock. After execution, delete the corresponding row data and release the lock. This method is based on the unique index of the database.
The structure of the table is as follows:
The specific implementation process is as follows (based on the previous lock test project):
Step 1: in POM Import maven coordinates from XML
<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency>
Step 2: configure the file application Configuring mybatis plus in YML
server: port: 8002 spring: redis: host: 68.79.63.42 port: 26379 password: test123 application: name: lockTest datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/dlock username: root password: root mybatis-plus: configuration: map-underscore-to-camel-case: false auto-mapping-behavior: full #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl mapper-locations: classpath*:mapper/**/*Mapper.xml
Step 3: create entity class
package com.test.entity; import com.baomidou.mybatisplus.annotation.TableName; import java.io.Serializable; @TableName("mylock") public class MyLock implements Serializable { private int id; private String methodName; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getMethodName() { return methodName; } public void setMethodName(String methodName) { this.methodName = methodName; } }
Step 4: create Mapper interface
package com.test.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.test.entity.MyLock; import org.apache.ibatis.annotations.Mapper; @Mapper public interface MyLockMapper extends BaseMapper<MyLock> { public void deleteByMethodName(String methodName); }
Step 5: create Mapper mapping file mylockmapper. In the resources/mapper directory xml
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.test.mapper.MyLockMapper"> <delete id="deleteByMethodName" parameterType="string"> delete from mylock where methodName = #{value} </delete> </mapper>
Step 6: Transform StockController
@Autowired private MyLockMapper myLockMapper; @GetMapping("/stock") public String stock(){ MyLock entity = new MyLock(); entity.setMethodName("stock"); try { //Insert data. If no exception is thrown, it indicates that the insertion is successful, that is, the lock is obtained myLockMapper.insert(entity); int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if(stock > 0){ stock --; redisTemplate.opsForValue().set("stock",stock+""); System.out.println("Inventory deduction succeeded, remaining inventory:" + stock); }else { System.out.println("Insufficient inventory!!!"); } //Release lock myLockMapper.deleteByMethodName("stock"); }catch (Exception ex){ System.out.println("Cannot perform inventory reduction operation without obtaining lock!!!"); } return "OK"; }
By observing the console output, you can see that the thread concurrency problem has been solved in this way.
Note that although distributed locks can be implemented using the database method, there are still some problems with this implementation method:
1. Because it is implemented based on the database, the availability and performance of the database will directly affect the availability and performance of the distributed lock. Therefore, the database needs dual computer deployment, data synchronization and active / standby switching;
2. It does not have the reentrant feature, because the row data always exists before the same thread releases the lock, and the data cannot be successfully inserted again. Therefore, a new column needs to be added in the table to record the information of the machine and thread currently obtaining the lock. When obtaining the lock again, first query whether the information of the machine and thread in the table is the same as that of the current machine and thread, If it is the same, obtain the lock directly;
3. There is no lock invalidation mechanism, because it is possible that after successfully inserting data, the server goes down, the corresponding data is not deleted, and the lock cannot be obtained after the service is restored. Therefore, a new column needs to be added in the table to record the invalidation time, and there needs to be a regular task to clear these invalid data;
4. It does not have the blocking lock feature, and the failure is directly returned if the lock is not obtained. Therefore, it is necessary to optimize the acquisition logic and cycle for multiple times.
5. In the process of implementation, we will encounter various problems. In order to solve these problems, the implementation method will be more and more complex; Depending on the database requires a certain resource overhead, and the performance needs to be considered.
2.2 ZooKeeper realizes distributed lock
The data storage structure of Zookeeper is like a tree, which is composed of nodes called Znode.
There are four types of nodes in Zookeeper:
1. PERSISTENT node
The default node type. After the client that created the node disconnects from zookeeper, the node still exists
2. PERSISTENT_SEQUENTIAL node
The so-called sequential node means that when creating a node, Zookeeper numbers the name of the node according to the time sequence of creation
3. Temporary node (EPHEMERAL)
In contrast to persistent nodes, temporary nodes are deleted when the client that created the node disconnects from zookeeper
4. Temporary sequential node (EPHEMERAL_SEQUENTIAL)
As the name suggests, temporary sequential node combines the characteristics of temporary node and sequential node: when creating a node, zookeeper numbers the name of the node according to the time sequence of creation; When the client that created the node disconnects from zookeeper, the temporary node is deleted
The principle of Zookeeper implementing distributed lock is based on the temporary sequence node of Zookeeper, as shown in the following figure:
Client a and client B compete for distributed locks, which is actually in / my_ A temporary sequential node is created under the lock node. zk maintains a node serial number internally. The smallest serial number indicates that the corresponding client obtains the lock.
Apache cursor is a relatively complete ZooKeeper client framework. It simplifies the operation of ZooKeeper through a set of encapsulated advanced API s, including the implementation of distributed locks.
The specific operation process is as follows (based on the previous lock test project):
Step 1: in POM Import maven coordinates from XML
<dependency> <groupId>org.apache.zookeeper</groupId> <artifactId>zookeeper</artifactId> <version>3.4.10</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-framework</artifactId> <version>2.12.0</version> </dependency> <dependency> <groupId>org.apache.curator</groupId> <artifactId>curator-recipes</artifactId> <version>2.12.0</version> </dependency>
Step 2: write configuration class
package com.test.config; import org.apache.curator.RetryPolicy; import org.apache.curator.framework.CuratorFramework; import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.retry.ExponentialBackoffRetry; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ZkConfig { @Bean public CuratorFramework curatorFramework(){ RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000,3); CuratorFramework client = CuratorFrameworkFactory.builder() .connectString("localhost:2181") .sessionTimeoutMs(5000) .connectionTimeoutMs(5000) .retryPolicy(retryPolicy) .build(); client.start(); return client; } }
Step 3: Transform StockController
@Autowired private CuratorFramework curatorFramework; @GetMapping("/stock") public String stock() { InterProcessMutex mutex = new InterProcessMutex(curatorFramework,"/mylock"); try { //Attempt to acquire lock boolean locked = mutex.acquire(0, TimeUnit.SECONDS); if(locked){ int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if(stock > 0){ stock --; redisTemplate.opsForValue().set("stock",stock+""); System.out.println("Inventory deduction succeeded, remaining inventory:" + stock); }else { System.out.println("Insufficient inventory!!!"); } //Release lock mutex.release(); }else{ System.out.println("Cannot perform inventory reduction operation without obtaining lock!!!"); } }catch (Exception ex){ System.out.println("Exception occurred!!!"); } return "OK"; }
By observing the console output, you can see that the thread concurrency problem has been solved in this way.
2.3 Redis implements distributed locks
Redis is relatively simple to implement distributed locks, that is, it calls the set command of redis to set the value. If it can be set successfully, it means that the lock is successfully added, that is, the lock is obtained. The set key value is deleted by calling the del command, that is, the lock is released.
2.3. 1 version I
Lock command: set lock_key lock_value NX
Unlock command: del lock_key
Java program:
@GetMapping("/stock") public String stock() { try { //Try locking Boolean locked = redisTemplate.opsForValue().setIfAbsent("mylock", "mylock"); if(locked){//Locking succeeded int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if(stock > 0){ stock --; redisTemplate.opsForValue().set("stock",stock+""); System.out.println("Inventory deduction succeeded, remaining inventory:" + stock); }else { System.out.println("Insufficient inventory!!!"); } //Release lock redisTemplate.delete("mylock"); }else{ System.out.println("Cannot perform inventory reduction operation without obtaining lock!!!"); } }catch (Exception ex){ System.out.println("Exception occurred!!!"); } return "OK"; }
2.3. 2 version II
There is a problem in the implementation of version 1 above, that is, when a thread gets the lock, the program hangs, and there is no time to release the lock, so all subsequent threads can't get the lock. To solve this problem, you can set an expiration time to prevent deadlock.
Lock command: set lock_key lock_value NX PX 5000
Unlock command: del lock_key
Java program:
@GetMapping("/stock") public String stock() { try { //Try locking Boolean locked = redisTemplate.opsForValue().setIfAbsent("mylock", "mylock",5000,TimeUnit.MILLISECONDS); if(locked){//Locking succeeded int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if(stock > 0){ stock --; redisTemplate.opsForValue().set("stock",stock+""); System.out.println("Inventory deduction succeeded, remaining inventory:" + stock); }else { System.out.println("Insufficient inventory!!!"); } //Release lock redisTemplate.delete("mylock"); }else{ System.out.println("Cannot perform inventory reduction operation without obtaining lock!!!"); } }catch (Exception ex){ System.out.println("Exception occurred!!!"); } return "OK"; }
2.3. 3 version III
For the previous version 2, there is another point that needs to be optimized, that is, locking and unlocking must be the same client, so the current thread id can be set when locking, and whether to add the lock for the current thread can be determined when releasing the lock. If so, release the lock again.
Java program:
@GetMapping("/stock") public String stock() { try { String threadId = Thread.currentThread().getId()+""; //Try locking Boolean locked = redisTemplate.opsForValue().setIfAbsent("mylock",threadId,5000,TimeUnit.MILLISECONDS); if(locked){//Locking succeeded int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if(stock > 0){ stock --; redisTemplate.opsForValue().set("stock",stock+""); System.out.println("Inventory deduction succeeded, remaining inventory:" + stock); }else { System.out.println("Insufficient inventory!!!"); } String myValue = redisTemplate.opsForValue().get("mylock"); if(threadId.equals(myValue)){ //Release lock redisTemplate.delete("mylock"); } }else{ System.out.println("Cannot perform inventory reduction operation without obtaining lock!!!"); } }catch (Exception ex){ System.out.println("Exception occurred!!!"); } return "OK"; }
3. Redisson
3.1 introduction to redisson
Redisson is a Java in memory data grid based on Redis (in memory data grid). It makes full use of a series of advantages provided by Redis key database and provides users with a series of common tool classes with distributed characteristics based on the common interfaces in the Java utility toolkit. As a result, the toolkit originally used to coordinate single machine multithreaded concurrent programs has the ability to coordinate distributed multi machine multithreaded concurrent systems, which greatly reduces the cost The difficulty of designing and developing large-scale distributed systems. At the same time, combined with various characteristic distributed services, it further simplifies the cooperation between programs in the distributed environment.
Redisson has built-in Redis based distributed lock implementation, which is our recommended way to use distributed locks.
The maven coordinates of Redisson are as follows:
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.10.1</version> </dependency>
3.2 Redisson distributed lock usage
Step 1: in POM Import maven coordinates of redisson in XML
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.10.1</version> </dependency>
Step 2: write configuration class
package com.test.config; import org.redisson.Redisson; import org.redisson.api.RedissonClient; import org.redisson.config.Config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RedissonConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private String port; @Value("${spring.redis.password}") private String password; @Bean public RedissonClient redissonClient(){ Config config = new Config(); config.useSingleServer().setAddress("redis://" + host + ":" + port); config.useSingleServer().setPassword(password); final RedissonClient client = Redisson.create(config); return client; } }
Step 3: transform the Controller
@Autowired private RedissonClient redissonClient; @GetMapping("/stock") public String stock() { //Obtain the distributed lock object. Note that locking is not successful at this time RLock lock = redissonClient.getLock("mylock"); try { //Try to lock. If the locking is successful, the subsequent program will continue to execute. If the locking is unsuccessful, block and wait lock.lock(5000,TimeUnit.MILLISECONDS); int stock = Integer.parseInt(redisTemplate.opsForValue().get("stock")); if(stock > 0){ stock --; redisTemplate.opsForValue().set("stock",stock+""); System.out.println("Inventory deduction succeeded, remaining inventory:" + stock); }else { System.out.println("Insufficient inventory!!!"); } }catch (Exception ex){ System.out.println("Exception occurred!!!"); }finally { //Unlock lock.unlock(); } return "OK"; }