本文介紹SpringBoot基於資料庫實現簡單的分散式鎖。
1.簡介
分散式鎖的方式有很多種,通常方案有:
- 基於mysql資料庫
- 基於redis
- 基於ZooKeeper
網上的實現方式有很多,本文主要介紹的是如果使用mysql實現簡單的分散式鎖,加鎖流程如下圖:
其實大致思想如下:
- 1.根據一個值來獲取鎖(也就是我這裡的tag),如果當前不存在鎖,那麼在資料庫插入一條記錄,然後進行處理業務,當結束,釋放鎖(刪除鎖)。
- 2.如果存在鎖,判斷鎖是否過期,如果過期則更新鎖的有效期,然後繼續處理業務,當結束時,釋放鎖。如果沒有過期,那麼獲取鎖失敗,退出。
2.資料庫設計
2.1 資料表介紹
資料庫表是由JPA自動生成的,稍後會對實體進行介紹,內容如下:
CREATE TABLE `lock_info` (
`id` bigint(20) NOT NULL,
`expiration_time` datetime NOT NULL,
`status` int(11) NOT NULL,
`tag` varchar(255) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tag` (`tag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
其中:
- id:主鍵
- tag:鎖的標示,以訂單為例,可以鎖訂單id
- expiration_time:過期時間
- status:鎖狀態,0,未鎖,1,已經上鎖
3.實現
本文使用SpringBoot 2.0.3.RELEASE,MySQL 8.0.16,ORM層使用的JPA。
3.1 pom
新建專案,在專案中加入jpa和mysql依賴,完整內容如下:
<?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.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.dalaoyang</groupId>
<artifactId>springboot2_distributed_lock_mysql</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot2_distributed_lock_mysql</name>
<description>springboot2_distributed_lock_mysql</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.22</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.2 配置檔案
配置檔案配置了一下資料庫資訊和jpa的基本配置,如下:
server.port=20001
##資料庫配置
##資料庫地址
spring.datasource.url=jdbc:mysql://localhost:3306/lock?characterEncoding=utf8&useSSL=false
##資料庫使用者名稱
spring.datasource.username=root
##資料庫密碼
spring.datasource.password=12345678
##資料庫驅動
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
##validate 載入hibernate時,驗證建立資料庫表結構
##create 每次載入hibernate,重新建立資料庫表結構,這就是導致資料庫表資料丟失的原因。
##create-drop 載入hibernate時建立,退出是刪除表結構
##update 載入hibernate自動更新資料庫結構
##validate 啟動時驗證表的結構,不會建立表
##none 啟動時不做任何操作
spring.jpa.hibernate.ddl-auto=update
##控制檯列印sql
spring.jpa.show-sql=true
##設定innodb
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
3.3 實體類
實體類如下,這裡給tag欄位設定了唯一索引,防止重複插入相同的資料:
package com.dalaoyang.entity;
import lombok.Data;
import javax.persistence.*;
import java.util.Date;
@Data
@Entity
@Table(name = "LockInfo",
uniqueConstraints={@UniqueConstraint(columnNames={"tag"},name = "uk_tag")})
public class Lock {
public final static Integer LOCKED_STATUS = 1;
public final static Integer UNLOCKED_STATUS = 0;
/**
* 主鍵id
*/
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
/**
* 鎖的標示,以訂單為例,可以鎖訂單id
*/
@Column(nullable = false)
private String tag;
/**
* 過期時間
*/
@Column(nullable = false)
private Date expirationTime;
/**
* 鎖狀態,0,未鎖,1,已經上鎖
*/
@Column(nullable = false)
private Integer status;
public Lock(String tag, Date expirationTime, Integer status) {
this.tag = tag;
this.expirationTime = expirationTime;
this.status = status;
}
public Lock() {
}
}
3.4 repository
repository層只新增了兩個簡單的方法,根據tag查詢鎖和根據tag刪除鎖的操作,內容如下:
package com.dalaoyang.repository;
import com.dalaoyang.entity.Lock;
import org.springframework.data.jpa.repository.JpaRepository;
public interface LockRepository extends JpaRepository<Lock, Long> {
Lock findByTag(String tag);
void deleteByTag(String tag);
}
3.5 service
service介面定義了兩個方法,獲取鎖和釋放鎖,內容如下:
package com.dalaoyang.service;
public interface LockService {
/**
* 嘗試獲取鎖
* @param tag 鎖的鍵
* @param expiredSeconds 鎖的過期時間(單位:秒),預設10s
* @return
*/
boolean tryLock(String tag, Integer expiredSeconds);
/**
* 釋放鎖
* @param tag 鎖的鍵
*/
void unlock(String tag);
}
實現類對上面方法進行了實現,其內容與上述流程圖中一致,這裡不在做介紹,完整內容如下:
package com.dalaoyang.service.impl;
import com.dalaoyang.entity.Lock;
import com.dalaoyang.repository.LockRepository;
import com.dalaoyang.service.LockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.Calendar;
import java.util.Date;
import java.util.Objects;
@Service
public class LockServiceImpl implements LockService {
private final Integer DEFAULT_EXPIRED_SECONDS = 10;
@Autowired
private LockRepository lockRepository;
@Override
@Transactional(rollbackFor = Throwable.class)
public boolean tryLock(String tag, Integer expiredSeconds) {
if (StringUtils.isEmpty(tag)) {
throw new NullPointerException();
}
Lock lock = lockRepository.findByTag(tag);
if (Objects.isNull(lock)) {
lockRepository.save(new Lock(tag, this.addSeconds(new Date(), expiredSeconds), Lock.LOCKED_STATUS));
return true;
} else {
Date expiredTime = lock.getExpirationTime();
Date now = new Date();
if (expiredTime.before(now)) {
lock.setExpirationTime(this.addSeconds(now, expiredSeconds));
lockRepository.save(lock);
return true;
}
}
return false;
}
@Override
@Transactional(rollbackFor = Throwable.class)
public void unlock(String tag) {
if (StringUtils.isEmpty(tag)) {
throw new NullPointerException();
}
lockRepository.deleteByTag(tag);
}
private Date addSeconds(Date date, Integer seconds) {
if (Objects.isNull(seconds)){
seconds = DEFAULT_EXPIRED_SECONDS;
}
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.add(Calendar.SECOND, seconds);
return calendar.getTime();
}
}
3.6 測試類
建立了一個測試的controller進行測試,裡面寫了一個test方法,方法在獲取鎖的時候會sleep 2秒,便於我們進行測試。完整內容如下:
package com.dalaoyang.controller;
import com.dalaoyang.service.LockService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
private LockService lockService;
@GetMapping("/tryLock")
public Boolean tryLock(String tag, Integer expiredSeconds) {
return lockService.tryLock(tag, expiredSeconds);
}
@GetMapping("/unlock")
public Boolean unlock(String tag) {
lockService.unlock(tag);
return true;
}
@GetMapping("/test")
public String test(String tag, Integer expiredSeconds) {
if (lockService.tryLock(tag, expiredSeconds)) {
try {
//do something
//這裡使用睡眠兩秒,方便觀察獲取不到鎖的情況
Thread.sleep(2000);
} catch (Exception e) {
} finally {
lockService.unlock(tag);
}
return "獲取鎖成功,tag是:" + tag;
}
return "當前tag:" + tag + "已經存在鎖,請稍後重試!";
}
}
3.測試
專案使用maven打包,分別使用兩個埠啟動,分別是20000和20001。
java -jar springboot2_distributed_lock_mysql-0.0.1-SNAPSHOT.jar --server.port=20001
java -jar springboot2_distributed_lock_mysql-0.0.1-SNAPSHOT.jar --server.port=20000
分別訪問兩個埠的專案,如圖所示,只有一個請求可以獲取鎖。
4.總結
本案例實現的分散式鎖只是一個簡單的實現方案,還具備很多問題,不適合生產環境使用。
5.原始碼地址
原始碼地址:https://gitee.com/dalaoyang/springboot_learn/tree/master/springboot2_distributed_lock_mysql