Spring Boot基于數(shù)據(jù)庫(kù)如何實(shí)現(xiàn)簡(jiǎn)單的分布式鎖
1.簡(jiǎn)介
分布式鎖的方式有很多種,通常方案有:
- 基于mysql數(shù)據(jù)庫(kù)
- 基于redis
- 基于ZooKeeper
網(wǎng)上的實(shí)現(xiàn)方式有很多,本文主要介紹的是如果使用mysql實(shí)現(xiàn)簡(jiǎn)單的分布式鎖,加鎖流程如下圖:

其實(shí)大致思想如下:
1.根據(jù)一個(gè)值來獲取鎖(也就是我這里的tag),如果當(dāng)前不存在鎖,那么在數(shù)據(jù)庫(kù)插入一條記錄,然后進(jìn)行處理業(yè)務(wù),當(dāng)結(jié)束,釋放鎖(刪除鎖)。
2.如果存在鎖,判斷鎖是否過期,如果過期則更新鎖的有效期,然后繼續(xù)處理業(yè)務(wù),當(dāng)結(jié)束時(shí),釋放鎖。如果沒有過期,那么獲取鎖失敗,退出。
2.數(shù)據(jù)庫(kù)設(shè)計(jì)
2.1 數(shù)據(jù)表介紹
數(shù)據(jù)庫(kù)表是由JPA自動(dòng)生成的,稍后會(huì)對(duì)實(shí)體進(jìn)行介紹,內(nèi)容如下:
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:鎖的標(biāo)示,以訂單為例,可以鎖訂單id
- expiration_time:過期時(shí)間
- status:鎖狀態(tài),0,未鎖,1,已經(jīng)上鎖
3.實(shí)現(xiàn)
本文使用SpringBoot 2.0.3.RELEASE,MySQL 8.0.16,ORM層使用的JPA。
3.1 pom
新建項(xiàng)目,在項(xiàng)目中加入jpa和mysql依賴,完整內(nèi)容如下:
<?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 配置文件
配置文件配置了一下數(shù)據(jù)庫(kù)信息和jpa的基本配置,如下:
server.port=20001 ##數(shù)據(jù)庫(kù)配置 ##數(shù)據(jù)庫(kù)地址 spring.datasource.url=jdbc:mysql://localhost:3306/lock?characterEncoding=utf8&useSSL=false ##數(shù)據(jù)庫(kù)用戶名 spring.datasource.username=root ##數(shù)據(jù)庫(kù)密碼 spring.datasource.password=12345678 ##數(shù)據(jù)庫(kù)驅(qū)動(dòng) spring.datasource.driver-class-name=com.mysql.jdbc.Driver ##validate 加載hibernate時(shí),驗(yàn)證創(chuàng)建數(shù)據(jù)庫(kù)表結(jié)構(gòu) ##create 每次加載hibernate,重新創(chuàng)建數(shù)據(jù)庫(kù)表結(jié)構(gòu),這就是導(dǎo)致數(shù)據(jù)庫(kù)表數(shù)據(jù)丟失的原因。 ##create-drop 加載hibernate時(shí)創(chuàng)建,退出是刪除表結(jié)構(gòu) ##update 加載hibernate自動(dòng)更新數(shù)據(jù)庫(kù)結(jié)構(gòu) ##validate 啟動(dòng)時(shí)驗(yàn)證表的結(jié)構(gòu),不會(huì)創(chuàng)建表 ##none 啟動(dòng)時(shí)不做任何操作 spring.jpa.hibernate.ddl-auto=update ##控制臺(tái)打印sql spring.jpa.show-sql=true ##設(shè)置innodb spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
3.3 實(shí)體類
實(shí)體類如下,這里給tag字段設(shè)置了唯一索引,防止重復(fù)插入相同的數(shù)據(jù):
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;
/**
* 鎖的標(biāo)示,以訂單為例,可以鎖訂單id
*/
@Column(nullable = false)
private String tag;
/**
* 過期時(shí)間
*/
@Column(nullable = false)
private Date expirationTime;
/**
* 鎖狀態(tài),0,未鎖,1,已經(jīng)上鎖
*/
@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層只添加了兩個(gè)簡(jiǎn)單的方法,根據(jù)tag查找鎖和根據(jù)tag刪除鎖的操作,內(nèi)容如下:
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接口定義了兩個(gè)方法,獲取鎖和釋放鎖,內(nèi)容如下:
package com.dalaoyang.service;
public interface LockService {
/**
* 嘗試獲取鎖
* @param tag 鎖的鍵
* @param expiredSeconds 鎖的過期時(shí)間(單位:秒),默認(rèn)10s
* @return
*/
boolean tryLock(String tag, Integer expiredSeconds);
/**
* 釋放鎖
* @param tag 鎖的鍵
*/
void unlock(String tag);
}
實(shí)現(xiàn)類對(duì)上面方法進(jìn)行了實(shí)現(xiàn),其內(nèi)容與上述流程圖中一致,這里不在做介紹,完整內(nèi)容如下:
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 測(cè)試類
創(chuàng)建了一個(gè)測(cè)試的controller進(jìn)行測(cè)試,里面寫了一個(gè)test方法,方法在獲取鎖的時(shí)候會(huì)sleep 2秒,便于我們進(jìn)行測(cè)試。完整內(nèi)容如下:
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 "當(dāng)前tag:" + tag + "已經(jīng)存在鎖,請(qǐng)稍后重試!";
}
}
3.測(cè)試
項(xiàng)目使用maven打包,分別使用兩個(gè)端口啟動(dòng),分別是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
分別訪問兩個(gè)端口的項(xiàng)目,如圖所示,只有一個(gè)請(qǐng)求可以獲取鎖。


4.總結(jié)
本案例實(shí)現(xiàn)的分布式鎖只是一個(gè)簡(jiǎn)單的實(shí)現(xiàn)方案,還具備很多問題,不適合生產(chǎn)環(huán)境使用。
5.源碼地址
源碼地址:https://gitee.com/dalaoyang/springboot_learn/tree/master/springboot2_distributed_lock_mysql (本地下載)
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
在Spring-Boot中如何使用@Value注解注入集合類
這篇文章主要介紹了在Spring-Boot中如何使用@Value注解注入集合類的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08
Java貪心算法之Prime算法原理與實(shí)現(xiàn)方法詳解
這篇文章主要介紹了Java貪心算法之Prime算法原理與實(shí)現(xiàn)方法,簡(jiǎn)單描述了Prime算法的概念、原理、實(shí)現(xiàn)與使用技巧,需要的朋友可以參考下2017-09-09
mybatis 如何利用resultMap復(fù)雜類型list映射
這篇文章主要介紹了mybatis 如何利用resultMap復(fù)雜類型list映射的操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-07-07
Java實(shí)現(xiàn)的按照順時(shí)針或逆時(shí)針方向輸出一個(gè)數(shù)字矩陣功能示例
這篇文章主要介紹了Java實(shí)現(xiàn)的按照順時(shí)針或逆時(shí)針方向輸出一個(gè)數(shù)字矩陣功能,涉及java基于數(shù)組遍歷、運(yùn)算的矩陣操作技巧,需要的朋友可以參考下2018-01-01
JAVA基于SnakeYAML實(shí)現(xiàn)解析與序列化YAML
這篇文章主要介紹了JAVA基于SnakeYAML實(shí)現(xiàn)解析與序列化YAML,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2019-12-12
Java使用HttpClient實(shí)現(xiàn)Post請(qǐng)求實(shí)例
本篇文章主要介紹了Java使用HttpClient實(shí)現(xiàn)Post請(qǐng)求實(shí)例,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-02-02

