Spring Boot基于数据库如何实现简单的分布式锁

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 (本地下载)

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对我们的支持。

(0)

相关推荐

  • springboot+redis分布式锁实现模拟抢单

    本篇内容主要讲解的是redis分布式锁,这个在各大厂面试几乎都是必备的,下面结合模拟抢单的场景来使用她:本篇不涉及到的redis环境搭建,快速搭建个人测试环境,这里建议使用docker:本篇内容节点如下: jedis的nx生成锁 如何删除锁 模拟抢单动作(10w个人开抢) jedis的nx生成锁 对于java中想操作redis,好的方式是使用jedis,首先pom中引入依赖: <dependency> <groupId>redis.clients</groupId> &

  • 浅谈Java(SpringBoot)基于zookeeper的分布式锁实现

    通过zookeeper实现分布式锁 1.创建zookeeper的client 首先通过CuratorFrameworkFactory创建一个连接zookeeper的连接CuratorFramework client public class CuratorFactoryBean implements FactoryBean<CuratorFramework>, InitializingBean, DisposableBean { private static final Logger LOGG

  • Spring Boot基于数据库如何实现简单的分布式锁

    1.简介 分布式锁的方式有很多种,通常方案有: 基于mysql数据库 基于redis 基于ZooKeeper 网上的实现方式有很多,本文主要介绍的是如果使用mysql实现简单的分布式锁,加锁流程如下图: 其实大致思想如下: 1.根据一个值来获取锁(也就是我这里的tag),如果当前不存在锁,那么在数据库插入一条记录,然后进行处理业务,当结束,释放锁(删除锁). 2.如果存在锁,判断锁是否过期,如果过期则更新锁的有效期,然后继续处理业务,当结束时,释放锁.如果没有过期,那么获取锁失败,退出. 2.数

  • Spring boot基于JPA访问MySQL数据库的实现

    本文展示如何通过JPA访问MySQL数据库. JPA全称Java Persistence API,即Java持久化API,它为Java开发人员提供了一种对象/关系映射工具来管理Java应用中的关系数据,结合其他ORM的使用,能达到简化开发流程的目的,使开发者能够专注于实现自己的业务逻辑上. Spring boot结合Jpa 能够简化创建 JPA 数据访问层和跨存储的持久层功能,用户的持久层Dao接口只需要继承定义好的接口,无需再写实现类,就可以实现对象的CRUD操作以及分页排序等功能. 环境要求

  • spring boot基于DRUID实现数据源监控过程解析

    这篇文章主要介绍了spring boot基于DRUID实现数据源监控过程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 随着需求和技术的日益革新,spring boot框架是越来越流行,她也越来越多地出现在我们的项目中,当然最主要的原因还是因为spring boot构建项目实在是太爽了,构建方便,开发简单,而且效率高.今天我们并不是来专门学习spring boot项目的,我们要讲的是数据源的加密和监控,监控到好说,就是不监控也没什么问题,但

  • Spring Boot 集成Elasticsearch模块实现简单查询功能

    目录 背景 系统集成 引入jar包 application.yml文件中添加ES配置 创建文档实体 接口实现 具体实现 基础查询 新增文档 请求参数 Controller实现 返回结果 修改文档 通过id查询文档信息 Controller实现 删除文档 Controller实现 分页查询 Controller实现 返回结果 模糊查询 Controller实现 范围查询 Controller实现 总结 背景 项目中我们经常会用搜索功能,普通的搜索我们可以用一个SQL的like也能实现匹配,但是搜索

  • spring boot与kafka集成的简单实例

    本文介绍了spring boot与kafka集成的简单实例,分享给大家,具体如下: 引入相关依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.

  • 聊聊使用RedisTemplat实现简单的分布式锁的问题

    目录 不使用redisson框架实现Redis分布式锁 1.在SpringBootTest中编写测试模块 1.1:使用占位符加锁: 1.2:使用占位符设置有效时间解决死锁问题: 1.3:使用lua脚本解决线程不安全问题: 1.3.1:编写lua脚本 1.3.2:修改ReidsConfig类 1.3.3:编写测试模块 不使用redisson框架实现Redis分布式锁 准备工作: 导入依赖 <dependency> <groupId>org.springframework.boot&l

  • Spring Boot 基于注解的 Redis 缓存使用详解

    看文本之前,请先确定你看过上一篇文章<Spring Boot Redis 集成配置>并保证 Redis 集成后正常可用,因为本文是基于上文继续增加的代码. 一.创建 Caching 配置类 RedisKeys.Java package com.shanhy.example.redis; import java.util.HashMap; import java.util.Map; import javax.annotation.PostConstruct; import org.springf

  • Spring boot基于ScheduledFuture实现定时任务

    一. 背景 接上一篇,完成存储过程的动态生成后,需要构建定时任务执行存储过程 二. 环境 1.此随笔内容基于spring boot项目 2.数据库为mysql 5.7.9版本 3.jdk 版本为1.8 三. 内容 1.定义接口和接口参数bean: 1)在上一篇博客bean 的基础上把接口配置参数bean修改一下,添加一个配置参数值和排序字段:在添加一个监测项的bean,想查看其他的bean信息,请移步 @Entity @Table(name="monitor_warn_item") p

  • Spring boot实现数据库读写分离的方法

    背景 数据库配置主从之后,如何在代码层面实现读写分离? 用户自定义设置数据库路由 Spring boot提供了AbstractRoutingDataSource根据用户定义的规则选择当前的数据库,这样我们可以在执行查询之前,设置读取从库,在执行完成后,恢复到主库. 实现可动态路由的数据源,在每次数据库查询操作前执行 ReadWriteSplitRoutingDataSource.java import org.springframework.jdbc.datasource.lookup.Abst

  • Spring Boot Mysql 数据库操作示例

    本文默认你的开发环境.数据库已经安装好 想用使用数据库.我们需要现在pom文件中添加相应的依赖 pom.xml <?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:sc

随机推荐