[May 1 Creation] Realize distributed lock based on mysql relational type

Estimated time to read this article: 15 minutes

Before looking at the specific technology stack: springboot mysql nginx (just understand)

Table of contents

0. write in front

1. Let’s talk about inventory reduction

1.1. Environment preparation

  1.2. Simple implementation of inventory reduction

 1.3. Demonstration of oversold phenomenon

1.4. Demonstration of jvm lock problem 

1.4.2. Principle

1.5. Multi-service problem 

1.5.1. Install and configure nginx

1.5.2. Stress testing

 1.6. mysql lock demo

1.6.1. mysql pessimistic lock

1.6.2. mysql optimistic lock 

 1.6.3. mysql lock defect

 2. Realize distributed lock based on mysql

2.1. Basic ideas 

2.2. Code implementation

2.3. Defects and solutions 


0. write in front

In multi-threaded high-concurrency scenarios, in order to ensure the thread safety of resources, jdk provides us with synchronized keywords and
ReentrantLock reentrant locks, but they can only guarantee thread safety within one jvm. At present, when distributed clusters, microservices, and cloud native are rampant, how to ensure the thread safety of different processes, services, and machines, jdk does not provide us with existing solutions. At this point, we have to implement it manually with the help of related technologies. There are currently three mainstream implementation methods:
1. Based on mysql relational implementation
2. Based on redis non-relational data implementation
3. Based on zookeeper implementation

This article mainly explains the realization of distributed lock based on mysql relational type

1. Let’s talk about inventory reduction

The inventory is prone to oversold when the concurrency is large. Once the oversold phenomenon occurs, there will be a situation where more orders are sold and the goods cannot be delivered.

Scenario:
        When the stock balance of product S is 5, users A and B purchase a product S at the same time. At this time, the query inventory is 5. If the stock is sufficient, the stock will be reduced: User A: update db_stock set stock = stock - 1
where id = 1
User B: update db_stock set stock = stock - 1 where id = 1
In the case of concurrency, the updated result may be 4, but the actual final stock should be 3

1.1. Environment preparation

In order to simulate specific scenarios, we need to prepare the development environment

First, you need to prepare a table in the mysql database:

CREATE TABLE `db_stock` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`product_code` varchar(255) DEFAULT NULL COMMENT '商品编号',
`stock_code` varchar(255) DEFAULT NULL COMMENT '仓库编号',
`count` int(11) DEFAULT NULL COMMENT '库存量',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

 The data in the table is as follows:

 Create a distributed lock demo project:

 Create the following tools directory structure:

 pom dependency file:


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

 application.yml configuration file:

server:
  port: 6000
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://172.16.116.100:3306/test
    username: root
    password: root

DistributedLockApplication startup class:

@SpringBootApplication
@MapperScan("com.atguigu.distributedlock.mapper")

public class DistributedLockApplication {
    public static void main(String[] args) {
        SpringApplication.run(DistributedLockApplication.class, args);
   }
}

Stock entity class:

@Data
@TableName("db_stock")
public class Stock {
   @TableId
   private Long id;
   private String productCode;
   private String stockCode;
   private Integer count;
}

StockMapper interface:

public interface StockMapper extends BaseMapper<Stock> {
}

  1.2. Simple implementation of inventory reduction

Next, let's practice the code

StockController:

@RestController
public class StockController {
    @Autowired
    private StockService stockService;
    @GetMapping("check/lock")
    public String checkAndLock(){
        this.stockService.checkAndLock();
        return "验库存并锁库存成功!";
    }
}

StockService:

@Service
public class StockService {
    @Autowired
    private StockMapper stockMapper;

    public void checkAndLock() {
// 先查询库存是否充足
        Stock stock = this.stockMapper.selectById(1L);
// 再减库存
        if (stock != null && stock.getCount() > 0) {
            stock.setCount(stock.getCount() - 1);
            this.stockMapper.updateById(stock);
        }
    }
}

test:

 

 Check out the database:

When visiting one by one in the browser, the inventory is reduced by 1 for each visit, and there is no problem.

 1.3. Demonstration of oversold phenomenon

Next, we use the jmeter stress test tool to test under high concurrency and add a thread group: 100 concurrent cycles 50 times, that is, 5000 requests.

 

 Add HTTP Request to the thread group:

Fill in the test interface path as follows:

Then select the test report you want, for example, select the aggregation report here:

Start the test and view the stress test report:

Test results: The total number of requests is 5000, the average request time is 202ms, the median (50%) request is completed within 173ms, 90% of the requests are completed within 344ms, the minimum time-consuming is 12ms, the maximum time-consuming is 1125ms, the error rate 0%, an average of 473.8 times per second.

View the remaining inventory of the mysql database: there are 4870

If there are still people who place orders at this time, there will be an oversold phenomenon (others buy successfully, but there is no goods to send).

1.4. Demonstration of jvm lock problem 

Try using jvm lock (synchronized keyword or ReetrantLock):

 Restart the tomcat service and use the jmeter stress test again, the effect is as follows:

View the mysql database:

 There is no oversold phenomenon, a perfect solution.  

1.4.2. Principle

After adding the synchronized keyword, StockService has an object lock. Due to the addition of an exclusive exclusive lock, only one request can obtain the lock at the same time, and the inventory is reduced. At this time, all requests will only be executed one-by-one, and overselling will not occur.

1.5. Multi-service problem 

 There is indeed no problem with using jvm locks in the case of a single project and a single service, but what happens in a cluster situation? Next, start multiple services and use nginx load balancing, the structure is as follows:

Start three services (port numbers are 8000 8100 8200), as follows:

1.5.1. Install and configure nginx

Install nginx based on:

# 拉取镜像

docker pull nginx:latest

# 创建nginx对应资源、日志及配置目录

mkdir -p /opt/nginx/logs /opt/nginx/conf /opt/nginx/html

# 先在conf目录下创建nginx.conf文件,配置内容参照下方

# 再运行容器

docker run -d -p 80:80 --name nginx -v /opt/nginx/html:/usr/share/nginx/html 

-v /opt/nginx/conf/nginx.conf:/etc/nginx/nginx.conf -v 
/opt/nginx/logs:/var/log/nginx nginx
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    #include /etc/nginx/conf.d/*.conf;
	
	upstream distributed {
		server 172.16.116.1:8000;
		server 172.16.116.1:8100;
		server 172.16.116.1:8200;
	}
	
	server {
		listen       80;
        server_name  172.16.116.100;
		location / {
			proxy_pass http://distributed;
		}
	}
	
}

 Test in browser: 172.16.116.100 is my nginx server address

 After testing, everything is normal when accessing services through nginx.

1.5.2. Stress testing

 Note: first restore the database inventory to 5000.

Referring to the previous test case, create a new test group: the parameters are the same as before

Configure the address of nginx and the access path of the service as follows:

 Test results: performance is only slightly improved.

 The database inventory remaining is as follows:

 There is another concurrency problem, that is, an oversold phenomenon.

 1.6. mysql lock demo

In addition to using jvm locks, you can also use data locks: pessimistic locks or optimistic locks

Pessimistic lock: Lock those rows when reading data, and other updates to these rows need to wait until the end of the pessimistic lock to continue. Optimistic: No lock when reading data, check whether the data has been updated when updating, if so, cancel the current update, generally we will choose optimistic lock when the waiting time of pessimistic lock is too long and unacceptable.

1.6.1. mysql pessimistic lock

In MySQL's InnoDB, the default Tansaction isolation level is REPEATABLE READ (rereadable)

There are two main types of read locks in SELECT:

  • SELECT ... LOCK IN SHARE MODE (shared lock)
  • SELECT ... FOR UPDATE (pessimistic locking)

These two methods must wait for other transaction data to be submitted (Commit) before executing when SELECTing to the same data table during the transaction (Transaction). The main difference is that LOCK IN SHARE MODE can easily cause deadlock when one transaction wants to Update the same form. Simply put, if you want to UPDATE the same form after SELECT, it is best to use SELECT ... FOR UPDATE.

Code implementation to transform StockService:

Define the selectStockForUpdate method in StockeMapper:

public interface StockMapper extends BaseMapper<Stock> {
    public Stock selectStockForUpdate(Long id);
}

Define the corresponding configuration in StockMapper.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.atguigu.distributedlock.mapper.StockMapper">
    <select id="selectStockForUpdate" 
resultType="com.atguigu.distributedlock.pojo.Stock">
       select * from db_stock where id = #{id} for update
    </select>
</mapper>

pressure test

Note: Before testing, you need to change the inventory to 5000. The pressure test data is as follows: much higher performance than jvm, nearly 1 times lower than lock-free

mysql database library:

1.6.2. mysql optimistic lock 

Optimistic Locking (Optimistic Locking) Compared with pessimistic locking, optimistic locking assumes that data will not cause conflicts under normal circumstances, so when the data is submitted for update, it will formally detect whether the data conflicts or not. If a conflict is found , try again. So how do we implement optimistic locking?

It is implemented using the data version (Version) recording mechanism, which is the most commonly used implementation of optimistic locking. This is generally achieved by adding a numeric "version" field to the database table. When reading data, read the value of the version field together. Every time the data is updated, the version value is incremented by one. When we submit an update, we judge that the current version information of the corresponding record in the database table is compared with the version value extracted for the first time. If the current version number of the database table is equal to the version value extracted for the first time, it is updated.

Add a version field to the db_stock table:

 Correspondingly, you also need to add a version attribute to the Stock entity class. Omit here.

Code

public void checkAndLock() {
    // 先查询库存是否充足
    Stock stock = this.stockMapper.selectById(1L);
    // 再减库存
    if (stock != null && stock.getCount() > 0){
        // 获取版本号
        Long version = stock.getVersion();
        stock.setCount(stock.getCount() - 1);
        // 每次更新 版本号 + 1
        stock.setVersion(stock.getVersion() + 1);
        // 更新之前先判断是否是之前查询的那个版本,如果不是重试
        if (this.stockMapper.update(stock, new UpdateWrapper<Stock>
().eq("id", stock.getId()).eq("version", version)) == 0) {
            checkAndLock();
       }
   }
}

 After restarting, the results of using the jmeter stress test tool are as follows:

Modify the test parameters as follows:

 The test results are as follows:

It shows that the greater the amount of concurrency, the lower the performance of optimistic locking (because a large number of retries are required); the smaller the amount of concurrency, the higher the performance.

 1.6.3. mysql lock defect

In the case of database clusters, database locks will become invalid, and many database cluster middleware do not support pessimistic locks at all. For example: mycat, in the scenario of read-write separation, optimistic locking may be unreliable. This lock strongly depends on the availability of the database. The database is a single point. Once the database is down, the business system will be unavailable.

 2. Realize distributed lock based on mysql

 Whether it is a jvm lock or a mysql lock, in order to ensure the concurrent safety of threads, a pessimistic exclusive exclusive lock is provided. Therefore, exclusiveness is also a basic requirement of distributed locks. It can be realized by using the characteristic that the unique key index cannot be inserted repeatedly. The design table is as follows:

CREATE TABLE `db_lock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `lock_name` varchar(50) NOT NULL COMMENT '锁名',
  `class_name` varchar(100) DEFAULT NULL COMMENT '类名',
  `method_name` varchar(50) DEFAULT NULL COMMENT '方法名',
  `server_name` varchar(50) DEFAULT NULL COMMENT '服务器ip',
  `thread_name` varchar(50) DEFAULT NULL COMMENT '线程名',
  `create_time` timestamp NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP 

COMMENT '获取锁时间',
  `desc` varchar(100) DEFAULT NULL COMMENT '描述',
  PRIMARY KEY (`id`),
  UNIQUE KEY `idx_unique` (`lock_name`)
) ENGINE=InnoDB AUTO_INCREMENT=1332899824461455363 DEFAULT CHARSET=utf8;

Lock entity class:  

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("db_lock")

public class Lock {
    private Long id;
    private String lockName;
    private String className;
    private String methodName;
    private String serverName;
    private String threadName;
    private Date createTime;
    private String desc;
}

LockMapper interface:

public interface LockMapper extends BaseMapper<Lock> {
}

2.1. Basic ideas 

The synchronized keyword and the ReetrantLock lock are both exclusive and exclusive locks, that is, when multiple threads compete for a resource, only one thread can seize the resource at the same time, and other threads can only block and wait until the thread that owns the resource releases the resource.

  1. Threads simultaneously acquire locks (insert)
  2. The acquisition is successful, the business logic is executed, and the execution is completed to release the lock (delete)
  3. Other threads wait to retry

2.2. Code implementation

Retrofit StockService:

@Service

public class StockService {
    @Autowired
    private StockMapper stockMapper;
    @Autowired
    private LockMapper lockMapper;
    /**
     * 数据库分布式锁
     */
    public void checkAndLock() {
        // 加锁
        Lock lock = new Lock(null, "lock", this.getClass().getName(), new 
Date(), null);
        try {
            this.lockMapper.insert(lock);
       } catch (Exception ex) {
            // 获取锁失败,则重试
            try {
                Thread.sleep(50);
                this.checkAndLock();
           } catch (InterruptedException e) {
                e.printStackTrace();
           }
       }
        // 先查询库存是否充足
        Stock stock = this.stockMapper.selectById(1L);
        // 再减库存
        if (stock != null && stock.getCount() > 0){
            stock.setCount(stock.getCount() - 1);
            this.stockMapper.updateById(stock);
       }
        // 释放锁
        this.lockMapper.deleteById(lock.getId());
   }
}

Lock:  

// 加锁
Lock lock = new Lock(null, "lock", this.getClass().getName(), new Date(), null);
try {
    this.lockMapper.insert(lock);
} catch (Exception ex) {
    // 获取锁失败,则重试
    try {
        Thread.sleep(50);
        this.checkAndLock();
   } catch (InterruptedException e) {
        e.printStackTrace();
   }
}

unlock:

// 释放锁
this.lockMapper.deleteById(lock.getId());

Using Jmeter stress test results:

 It can be seen that the performance is touching. The inventory balance of the mysql database is 0, which can ensure thread safety. 

2.3. Defects and solutions 

1. This lock strongly depends on the availability of the database. The database is a single point. Once the database is down, the business system will be unavailable.

Solution: Build a master and backup for the lock database

2. This lock has no expiration time. Once the unlock operation fails, the lock record will remain in the database, and other threads will no longer be able to obtain the lock.

Solution: Just do a timed task to clean up the overtime data in the database at regular intervals.

3. This lock is non-reentrant, and the same thread cannot acquire the lock again until the lock is released. Because the data in the data already exists.

Solution: Record the host information and thread information that acquired the lock. If the same thread wants to acquire the lock, re-entry directly.

4. Due to the performance of the database, the concurrency capability is limited.

Solution: Unable to resolve.

Guess you like

Origin blog.csdn.net/m0_62436868/article/details/130439348