Distributed lock based on MySQL database exclusive lock (for update)

1. Overview of the lock

  Locking is a very important knowledge point in Java development. Lock or mutual exclusion (mutex) is a synchronization mechanism used to control the access rights of each thread to resources in a multi-threaded environment. Locks are designed to enforce mutual exclusion and concurrency control strategies.

1.1, single application lock

  The lock in the JDK can only be effective in a JVM process, we call this kind of lock a single application lock. Common locks in JAVA are: synchronized, ReentrantLock, ReadWriteLock, etc.

1.2, the limitations of a single application lock

  Monolithic application locks are not problematic in traditional single-application services, but in the current high-concurrency cluster scenario, problems will occur, as shown in the following figure:
Insert picture description here
  In the above figure, each Tomcat is a JVM. The two Tomcats provide the same service, and the single application lock in each Tomcat service will only take effect in its own application, so if two Tomcat services compete for a resource at the same time, problems may occur .

1.3, distributed lock

  In view of the limitations of single application locks, how do we solve this problem? The answer is: with the help of third-party components to realize distributed locks, multiple services can realize cross-JVM and cross-process distributed locks through third-party components.

  Common distributed lock schemes are:

  1. Database-based distributed lock
  2. Redis-based distributed lock
  3. Distributed lock based on Zookeeper
the way advantage Disadvantage
database Simple to implement and easy to understand Pressure on the database
Redis Easy to understand Implement by yourself, more complicated, does not support blocking
Zookeeper Support blocking Need to use Zookeeper, to achieve complex
Curator (Zookeeper client) Based on Zookeeper implementation, provides a lock method Rely on Zookeeper, strong consistency
Redisson (Redis client) Based on Redis, provides a lock method, which can be blocked

2. Realize distributed lock based on MySQL exclusive lock (for update)

2.1, exclusive lock (for update)

  for update is a row-level lock, also called exclusive lock. Once a user has imposed a row-level lock on a row, the user can query or update the locked data row. Other users can only query but cannot update the locked data row.

  Row locks are always exclusive locks. Only when the following conditions occur, the lock will be released: 1. Execute the COMMIT statement; 2. Exit the database (LOG OFF); 3. Stop the program.

2.2. Implementation principle

  Introduce the MySQL database as a third-party component to implement distributed locks, create a data table to record distributed locks (business modules can be distinguished), and then where you need to use distributed locks, obtain the corresponding business modules through select……for update If the lock record is successfully acquired, the record row is locked, and other threads will have to wait. When the thread execution ends, the lock will be released, and other threads can acquire the lock and continue execution.

2.3. Implement distributed locks
2.3.1, create a table

  In fact, the distributed lock based on exclusive lock (for update) only needs two fields: Id and module_code.

CREATE TABLE `t_sys_distributedlock` (
`id`  int(11) NOT NULL AUTO_INCREMENT ,
`module_code`  varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
`module_name`  varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
`expiration_time`  datetime NOT NULL ,
`creater`  varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
`create_time`  datetime NOT NULL ,
`modifier`  varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL ,
`modify_time`  datetime NOT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8 COLLATE=utf8_general_ci
AUTO_INCREMENT=1
ROW_FORMAT=DYNAMIC
;
2.3.2, build the project

  This is mainly based on Spring Boot, Mybatis, MySQL, etc. to build projects. Among them, the pom file depends on the following:

<dependency>
   <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
 </dependency>
 <dependency>
     <groupId>org.mybatis.spring.boot</groupId>
     <artifactId>mybatis-spring-boot-starter</artifactId>
     <version>2.1.4</version>
 </dependency>

 <dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
     <scope>runtime</scope>
 </dependency>
 <!--集成druid连接池-->
 <dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>druid</artifactId>
     <version>1.1.11</version>
 </dependency>

  The application.properties file is configured as follows:

server.port=8080

#数据源配置
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/db_admin?useSSL=true&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

#mybatis配置
mybatis.mapper-locations=classpath:mappings/**/*Mapper.xml
mybatis.type-aliases-package=com.qriver.distributedlock

#日志
logging.level.com.qriver.distributedlock=debug

  Finally, the default startup class of SpringBoot is as follows:

@SpringBootApplication
public class QriverDistributedLockApplication {
    
    

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

}
2.3.3、DistributedLock

  The entity class corresponding to the distributed lock.

/**
 * 分布式锁 实体类
 */
public class DistributedLock implements Serializable {
    
    

    private int id;

    private String moduleCode;

    private String moduleName;

    private Date expirationTime;

    private String creater;

    private Date createTime;

    private String modifier;

    private Date modifyTime;
	
	//省略getter or setter 方法
 }
2.3.4, mapper file

  Here mainly provides the select element with id=getDistributedLock. Exclusive lock (for update) is used in this statement.

<?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.qriver.distributedlock.mapper.DistributedLockMapper">

       <select id="getDistributedLock" resultType="com.qriver.distributedlock.entity.DistributedLock">
            SELECT
                T.*
            FROM
                t_sys_distributedlock T
            <where>
                 <if test="moduleCode != null">
                    AND T.module_code = #{moduleCode}
                 </if>
            </where>

            for update
       </select>

</mapper>
2.3.5, DistributedLockMapper interface

  A getDistributedLock method is defined in DistributedLockMapper, which determines whether the row lock of the database is acquired according to whether the returned result is empty.

@Mapper
public interface DistributedLockMapper {
    
    

    public List<DistributedLock> getDistributedLock(DistributedLock lock);

}
2.3.6、DistributedLockService

  Provides a method to acquire a lock, by judging whether the return value is empty, and then converting it to a boolean value of whether the lock is acquired.

@Service
public class DistributedLockService {
    
    

    @Autowired
    private DistributedLockMapper distributedLockMapper;

    public boolean tryLock(String code){
    
    
        DistributedLock distributedLock = new DistributedLock();
        distributedLock.setModuleCode(code);
        List<DistributedLock> list = distributedLockMapper.getDistributedLock(distributedLock);
        if(list != null && list.size() > 0){
    
    
            return true;
        }
        return false;
    }

}
2.3.7 Application of distributed locks

  A DemoController class is implemented here to apply distributed locks. The implementation is as follows:

@RestController
public class DemoController {
    
    

    private Logger logger = LoggerFactory.getLogger(DemoController.class);

    @Autowired
    private DistributedLockService distributedLockService;

    @RequestMapping("mysqlLock")
    @Transactional(rollbackFor = Exception.class)
    public String testLock() throws Exception {
    
    
        logger.debug("进入testLock()方法;");
        if(distributedLockService.tryLock("order")){
    
    
            logger.debug("获取到分布式锁;");
            Thread.sleep(30 * 1000);
        }else{
    
    
            logger.debug("获取分布式锁失败;");
            throw new Exception("获取分布式锁失败;");
        }
        logger.debug("执行完成;");
        return "返回结果";
    }

}

  In the above method, the @Transactional annotation is added to ensure that the distributedLockService.tryLock("order") statement will not submit the data corresponding to the method until the method is executed, otherwise it will directly commit and the distributed lock will become invalid.

2.3.8, test

  Start two instances with ports 8080 and 8081 respectively. For the startup method, please refer to "How to Start Multiple Project Instances for One Project in IntelliJ Idea" . Then, visit http://localhost:8081/mysqlLock and http://localhost:8080/mysqlLock respectively in the browser.

  8081 service instance log:

2021-01-17 21:18:17.317 DEBUG 28768 --- [nio-8081-exec-2] c.q.d.controller.DemoController          : 进入testLock()方法;
2021-01-17 21:18:17.319 DEBUG 28768 --- [nio-8081-exec-2] c.q.d.m.D.getDistributedLock             : ==>  Preparing: SELECT T.* FROM t_sys_distributedlock T WHERE T.module_code = ? for update
2021-01-17 21:18:17.320 DEBUG 28768 --- [nio-8081-exec-2] c.q.d.m.D.getDistributedLock             : ==> Parameters: order(String)
2021-01-17 21:18:17.323 DEBUG 28768 --- [nio-8081-exec-2] c.q.d.m.D.getDistributedLock             : <==      Total: 1
2021-01-17 21:18:17.323 DEBUG 28768 --- [nio-8081-exec-2] c.q.d.controller.DemoController          : 获取到分布式锁;
2021-01-17 21:18:47.323 DEBUG 28768 --- [nio-8081-exec-2] c.q.d.controller.DemoController          : 执行完成;

  8080 service instance log:

2021-01-17 21:18:18.913 DEBUG 47436 --- [nio-8080-exec-2] c.q.d.controller.DemoController          : 进入testLock()方法;
2021-01-17 21:18:18.913 DEBUG 47436 --- [nio-8080-exec-2] c.q.d.m.D.getDistributedLock             : ==>  Preparing: SELECT T.* FROM t_sys_distributedlock T WHERE T.module_code = ? for update
2021-01-17 21:18:18.914 DEBUG 47436 --- [nio-8080-exec-2] c.q.d.m.D.getDistributedLock             : ==> Parameters: order(String)
2021-01-17 21:18:47.327 DEBUG 47436 --- [nio-8080-exec-2] c.q.d.m.D.getDistributedLock             : <==      Total: 1
2021-01-17 21:18:47.327 DEBUG 47436 --- [nio-8080-exec-2] c.q.d.controller.DemoController          : 获取到分布式锁;
2021-01-17 21:19:17.328 DEBUG 47436 --- [nio-8080-exec-2] c.q.d.controller.DemoController          : 执行完成;

  When accessing the above two addresses, the first one acquires the lock first, and then executes the business logic. When the business execution is completed (30s), the second service acquires the lock and then continues to execute the business logic.

Guess you like

Origin blog.csdn.net/hou_ge/article/details/112754366