SpringBoot基于数据库实现简单的分布式锁
程序员文章站
2022-03-20 20:27:15
本文介绍SpringBoot基于数据库实现简单的分布式锁。 1.简介 分布式锁的方式有很多种,通常方案有: 基于mysql数据库 基于redis 基于ZooKeeper 网上的实现方式有很多,本文主要介绍的是如果使用mysql实现简单的分布式锁,加锁流程如下图: 其实大致思想如下: 1.根据一个值来 ......
本文介绍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.源码地址
源码地址:
上一篇: gRPC 本地服务搭建
下一篇: Matlab建造者模式