欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

SpringBoot基于数据库实现简单的分布式锁

程序员文章站 2022-03-20 20:27:15
本文介绍SpringBoot基于数据库实现简单的分布式锁。 1.简介 分布式锁的方式有很多种,通常方案有: 基于mysql数据库 基于redis 基于ZooKeeper 网上的实现方式有很多,本文主要介绍的是如果使用mysql实现简单的分布式锁,加锁流程如下图: 其实大致思想如下: 1.根据一个值来 ......

本文介绍springboot基于数据库实现简单的分布式锁。

1.简介

分布式锁的方式有很多种,通常方案有:

  • 基于mysql数据库
  • 基于redis
  • 基于zookeeper

网上的实现方式有很多,本文主要介绍的是如果使用mysql实现简单的分布式锁,加锁流程如下图:

SpringBoot基于数据库实现简单的分布式锁

其实大致思想如下:

  • 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

分别访问两个端口的项目,如图所示,只有一个请求可以获取锁。

SpringBoot基于数据库实现简单的分布式锁

SpringBoot基于数据库实现简单的分布式锁

4.总结

本案例实现的分布式锁只是一个简单的实现方案,还具备很多问题,不适合生产环境使用。

5.源码地址

源码地址: