SpringBoot基於數據庫實現簡單的分布式鎖


本文介紹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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM