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:
- Obtaining a lock is blocked. It will be blocked until it is obtained
- 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.