Implementation of distributed locking based on SpringBoot and database table records

Posted by LiamBailey on Sat, 08 Jan 2022 13:05:26 +0100

When different threads in the same process operate to share resources, we only need to lock the resources. For example, we can ensure the correctness of the operation by using the tools under JUC. Students unfamiliar with JUC can read the following articles:

However, for high availability, our system is always multi copy and distributed on different machines. The above locking mechanism in the same process will no longer work. In order to ensure the access of multi replica system to shared resources, we introduce distributed lock.

The main implementation methods of distributed lock are as follows:

  • Database based, which is subdivided into database based table records, pessimistic locks and optimistic locks
  • Cache based, such as Redis
  • Zookeeper based

Today, let's demonstrate the simplest distributed locking scheme - distributed locking based on database table records

The main principle is to use the unique index of the database (for students who don't know the index of the database, please refer to my other article mysql index for a brief talk)

For example, there is a table as follows:

CREATE TABLE `test`.`Untitled`  (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'Self incrementing serial number',
  `name` varchar(255) NOT NULL COMMENT 'Lock name',
  `survival_time` int(11) NOT NULL COMMENT 'Survival time, unit ms',
  `create_time` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT 'Creation time',
  `thread_name` varchar(255) NOT NULL COMMENT 'Thread name',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `uk_name`(`name`) USING BTREE
) ENGINE = InnoDB ROW_FORMAT = Dynamic;

The name field is added with a unique index. For multiple new operations with the same name value, the database can only ensure that only one operation is successful, and other operations will be rejected, and a "duplicate key" error will be thrown.

Then, when system 1 is ready to acquire the distributed lock, it tries to insert a record with name="key" into the database. If the insertion is successful, it means that the lock is acquired successfully. If other systems want to obtain distributed locks, they also need to insert records with the same name into the database. Of course, the database will report an error and the insertion failure means that these systems failed to obtain locks. When system 1 wants to release the lock, delete this record. thread_ The name column can be used to ensure that you can only actively release the locks you create.

The distributed lock we want to implement has the following effects:

  1. Obtaining a lock is blocked. It will be blocked until it is obtained
  2. The lock will fail. It will be automatically released after exceeding the lifetime of the lock. This can avoid the problem that some systems cannot actively release locks due to downtime

The approximate flow chart is as follows:

The following dependencies are used:

  • SpringBoot
  • MyBatis-plus
  • Lombok

The project catalogue is:

Dependencies used in pom files:

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.6</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.3.1</version>
        </dependency>

        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-extension</artifactId>
            <version>3.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

Configuration items are:

server:
  port: 9091


spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    username: root
    password: a123

logging:
  level:
    root: info

The entity classes used to map database fields are:

package com.yang.lock1.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 * @author qcy
 * @create 2020/08/25 15:03:47
 */
@Data
@NoArgsConstructor
@TableName(value = "t_lock")
public class Lock {

    /**
     * Self incrementing serial number
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    /**
     * Lock name
     */
    private String name;

    /**
     * Survival time in ms
     */
    private int survivalTime;

    /**
     * Lock creation time
     */
    private Date createTime;

    /**
     * Thread name
     */
    private String ThreadName;
}

Dao layer:

package com.yang.lock1.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yang.lock1.entity.Lock;
import org.apache.ibatis.annotations.Mapper;

/**
 * @author qcy
 * @create 2020/08/25 15:06:24
 */
@Mapper
public interface LockDao extends BaseMapper<Lock> {
}

Service interface layer:

package com.yang.lock1.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.yang.lock1.entity.Lock;

/**
 * @author qcy
 * @create 2020/08/25 15:07:44
 */
public interface LockService extends IService<Lock> {

    /**
     * Blocking access to distributed locks
     *
     * @param name         Lock name
     * @param survivalTime survival time 
     */
    void lock(String name, int survivalTime);

    /**
     * Release lock
     *
     * @param name Lock name
     */
    public void unLock(String name);
}

Service implementation layer:

package com.yang.lock1.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.yang.lock1.dao.LockDao;
import com.yang.lock1.entity.Lock;
import com.yang.lock1.service.LockService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;

import java.util.Date;

/**
 * @author qcy
 * @create 2020/08/25 15:08:25
 */
@Slf4j
@Service
public class LockServiceImpl extends ServiceImpl<LockDao, Lock> implements LockService {

    @Override
    public void lock(String name, int survivalTime) {
        String threadName = "system1-" + Thread.currentThread().getName();
        while (true) {
            Lock lock = this.lambdaQuery().eq(Lock::getName, name).one();
            if (lock == null) {
                //Description no lock
                Lock lk = new Lock();
                lk.setName(name);
                lk.setSurvivalTime(survivalTime);
                lk.setThreadName(threadName);
                try {
                    save(lk);
                    log.info(threadName + "Lock acquisition succeeded");
                    return;
                } catch (DuplicateKeyException e) {
                    //Continue to retry
                    log.info(threadName + "Failed to acquire lock");
                    continue;
                }
            }

            //At this time, there is a lock. Judge whether the lock has expired
            Date now = new Date();
            Date expireDate = new Date(lock.getCreateTime().getTime() + lock.getSurvivalTime());
            if (expireDate.before(now)) {
                //The lock has expired
                boolean result = removeById(lock.getId());
                if (result) {
                    log.info(threadName + "Deleted expired locks");
                }

                //Attempt to acquire lock
                Lock lk = new Lock();
                lk.setName(name);
                lk.setSurvivalTime(survivalTime);
                lk.setThreadName(threadName);
                try {
                    save(lk);
                    log.info(threadName + "Lock acquisition succeeded");
                    return;
                } catch (DuplicateKeyException e) {
                    log.info(threadName + "Failed to acquire lock");
                }
            }
        }

    }

    @Override
    public void unLock(String name) {
        //When releasing a lock, you should note that you can only release the lock you created
        String threadName = "system1-" + Thread.currentThread().getName();
        Lock lock = lambdaQuery().eq(Lock::getName, name).eq(Lock::getThreadName, threadName).one();
        if (lock != null) {
            boolean b = removeById(lock.getId());
            if (b) {
                log.info(threadName + "The lock was released");
            } else {
                log.info(threadName + "Ready to release the lock,But the lock expired,It was forcibly released by other clients");
            }
        } else {
            log.info(threadName + "Ready to release the lock,But the lock expired,It was forcibly released by other clients");
        }
    }

}

The test classes are as follows:

package com.yang.lock1;

import com.yang.lock1.service.LockService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.annotation.Resource;

/**
 * @author qcy
 * @create 2020/08/25 15:10:54
 */
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class Lock1ApplicationTest {

    @Resource
    LockService lockService;

    @Test
    public void testLock() {
        log.info("system1 Ready to acquire lock");
        lockService.lock("key", 6 * 1000);
        try {
            //Time consuming for simulating business
            Thread.sleep(4 * 1000);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lockService.unLock("key");
        }
    }

}

Copy the code and change system1 to system2. Now start both systems at the same time:

The output of system1 is as follows:

The output of system2 is as follows:

At 23.037 seconds, system1 attempts to acquire the lock. At 23.650 seconds, it succeeds and holds the distributed lock. At the 26th second, system2 attempts to acquire a lock and is blocked. At 27.701 seconds, system1 released the lock, system2 obtained the lock at 27.749, and released it at 31 seconds.

Now we change the service time of system1 to 10 seconds, and we can simulate the scenario that system2 releases the lock of system1 timeout.

Start system1 first, then system2

At this time, the output of system1 is as follows:

The output of system2 is as follows:

At 14 seconds, system1 acquired the lock, and then it needed to run for 10 seconds because the business time suddenly exceeded expectations. During this period, the lock created by system1 exceeded its lifetime. At this time, system2 deletes the expired lock at 19 seconds, and then obtains the lock. After 24 seconds, system1 turned back and found that its lock had been released. Finally, system2 released its lock normally.

Topics: Java Database Spring Boot Back-end Distribution