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

释放双眼,带上耳机,听听看~!

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

1.简介

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

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

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

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

其实大致思想如下:

  • 1.根据一个值来获取锁(也就是我这里的tag),如果当前不存在锁,那么在数据库插入一条记录,然后进行处理业务,当结束,释放锁(删除锁)。
  • 2.如果存在锁,判断锁是否过期,如果过期则更新锁的有效期,然后继续处理业务,当结束时,释放锁。如果没有过期,那么获取锁失败,退出。

2.数据库设计

2.1 数据表介绍

数据库表是由JPA自动生成的,稍后会对实体进行介绍,内容如下:


1
2
3
4
5
6
7
8
9
10
1CREATE TABLE `lock_info` (
2  `id` bigint(20) NOT NULL,
3  `expiration_time` datetime NOT NULL,
4  `status` int(11) NOT NULL,
5  `tag` varchar(255) NOT NULL,
6  PRIMARY KEY (`id`),
7  UNIQUE KEY `uk_tag` (`tag`)
8) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
9
10

其中:

  • 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依赖,完整内容如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4   <modelVersion>4.0.0</modelVersion>
5   <parent>
6       <groupId>org.springframework.boot</groupId>
7       <artifactId>spring-boot-starter-parent</artifactId>
8       <version>2.0.3.RELEASE</version>
9       <relativePath/> <!-- lookup parent from repository -->
10  </parent>
11  <groupId>com.dalaoyang</groupId>
12  <artifactId>springboot2_distributed_lock_mysql</artifactId>
13  <version>0.0.1-SNAPSHOT</version>
14  <name>springboot2_distributed_lock_mysql</name>
15  <description>springboot2_distributed_lock_mysql</description>
16
17  <properties>
18      <java.version>1.8</java.version>
19  </properties>
20
21  <dependencies>
22      <dependency>
23          <groupId>org.springframework.boot</groupId>
24          <artifactId>spring-boot-starter-web</artifactId>
25      </dependency>
26      <dependency>
27          <groupId>org.springframework.boot</groupId>
28          <artifactId>spring-boot-starter-data-jpa</artifactId>
29      </dependency>
30
31      <dependency>
32          <groupId>mysql</groupId>
33          <artifactId>mysql-connector-java</artifactId>
34          <scope>runtime</scope>
35      </dependency>
36      <dependency>
37          <groupId>org.springframework.boot</groupId>
38          <artifactId>spring-boot-starter-test</artifactId>
39          <scope>test</scope>
40      </dependency>
41
42      <dependency>
43          <groupId>org.projectlombok</groupId>
44          <artifactId>lombok</artifactId>
45          <version>1.16.22</version>
46          <scope>provided</scope>
47      </dependency>
48  </dependencies>
49
50  <build>
51      <plugins>
52          <plugin>
53              <groupId>org.springframework.boot</groupId>
54              <artifactId>spring-boot-maven-plugin</artifactId>
55          </plugin>
56      </plugins>
57  </build>
58
59</project>
60
61

3.2 配置文件

配置文件配置了一下数据库信息和jpa的基本配置,如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
1server.port=20001
2
3
4##数据库配置
5##数据库地址
6spring.datasource.url=jdbc:mysql://localhost:3306/lock?characterEncoding=utf8&useSSL=false
7##数据库用户名
8spring.datasource.username=root
9##数据库密码
10spring.datasource.password=12345678
11##数据库驱动
12spring.datasource.driver-class-name=com.mysql.jdbc.Driver
13
14
15##validate  加载hibernate时,验证创建数据库表结构
16##create   每次加载hibernate,重新创建数据库表结构,这就是导致数据库表数据丢失的原因。
17##create-drop        加载hibernate时创建,退出是删除表结构
18##update                 加载hibernate自动更新数据库结构
19##validate 启动时验证表的结构,不会创建表
20##none  启动时不做任何操作
21spring.jpa.hibernate.ddl-auto=update
22
23##控制台打印sql
24spring.jpa.show-sql=true
25##设置innodb
26spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
27
28
29

3.3 实体类

实体类如下,这里给tag字段设置了唯一索引,防止重复插入相同的数据:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
1package com.dalaoyang.entity;
2
3
4import lombok.Data;
5import javax.persistence.*;
6import java.util.Date;
7
8@Data
9@Entity
10@Table(name = "LockInfo",
11        uniqueConstraints={@UniqueConstraint(columnNames={"tag"},name = "uk_tag")})
12public class Lock {
13
14    public final static Integer LOCKED_STATUS = 1;
15    public final static Integer UNLOCKED_STATUS = 0;
16
17    /**
18     * 主键id
19     */
20    @Id
21    @GeneratedValue(strategy = GenerationType.AUTO)
22    private Long id;
23
24    /**
25     * 锁的标示,以订单为例,可以锁订单id
26     */
27    @Column(nullable = false)
28    private String tag;
29
30    /**
31     * 过期时间
32     */
33    @Column(nullable = false)
34    private Date expirationTime;
35
36    /**
37     * 锁状态,0,未锁,1,已经上锁
38     */
39    @Column(nullable = false)
40    private Integer status;
41
42    public Lock(String tag, Date expirationTime, Integer status) {
43        this.tag = tag;
44        this.expirationTime = expirationTime;
45        this.status = status;
46    }
47
48    public Lock() {
49    }
50}
51
52

3.4 repository

repository层只添加了两个简单的方法,根据tag查找锁和根据tag删除锁的操作,内容如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1package com.dalaoyang.repository;
2
3import com.dalaoyang.entity.Lock;
4import org.springframework.data.jpa.repository.JpaRepository;
5
6
7public interface LockRepository extends JpaRepository<Lock, Long> {
8
9    Lock findByTag(String tag);
10
11    void deleteByTag(String tag);
12}
13
14
15

3.5 service

service接口定义了两个方法,获取锁和释放锁,内容如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1package com.dalaoyang.service;
2
3
4public interface LockService {
5
6    /**
7     * 尝试获取锁
8     * @param tag 锁的键
9     * @param expiredSeconds 锁的过期时间(单位:秒),默认10s
10     * @return
11     */
12    boolean tryLock(String tag, Integer expiredSeconds);
13
14    /**
15     * 释放锁
16     * @param tag 锁的键
17     */
18    void unlock(String tag);
19}
20
21
22

实现类对上面方法进行了实现,其内容与上述流程图中一致,这里不在做介绍,完整内容如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
1package com.dalaoyang.service.impl;
2
3import com.dalaoyang.entity.Lock;
4import com.dalaoyang.repository.LockRepository;
5import com.dalaoyang.service.LockService;
6import org.springframework.beans.factory.annotation.Autowired;
7import org.springframework.stereotype.Service;
8import org.springframework.transaction.annotation.Propagation;
9import org.springframework.transaction.annotation.Transactional;
10import org.springframework.util.StringUtils;
11
12import java.util.Calendar;
13import java.util.Date;
14import java.util.Objects;
15
16
17@Service
18public class LockServiceImpl implements LockService {
19
20    private final Integer DEFAULT_EXPIRED_SECONDS = 10;
21
22    @Autowired
23    private LockRepository lockRepository;
24
25    @Override
26    @Transactional(rollbackFor = Throwable.class)
27    public boolean tryLock(String tag, Integer expiredSeconds) {
28        if (StringUtils.isEmpty(tag)) {
29            throw new NullPointerException();
30        }
31        Lock lock = lockRepository.findByTag(tag);
32        if (Objects.isNull(lock)) {
33            lockRepository.save(new Lock(tag, this.addSeconds(new Date(), expiredSeconds), Lock.LOCKED_STATUS));
34            return true;
35        } else {
36            Date expiredTime = lock.getExpirationTime();
37            Date now = new Date();
38            if (expiredTime.before(now)) {
39                lock.setExpirationTime(this.addSeconds(now, expiredSeconds));
40                lockRepository.save(lock);
41                return true;
42            }
43        }
44        return false;
45    }
46
47    @Override
48    @Transactional(rollbackFor = Throwable.class)
49    public void unlock(String tag) {
50        if (StringUtils.isEmpty(tag)) {
51            throw new NullPointerException();
52        }
53        lockRepository.deleteByTag(tag);
54    }
55
56    private Date addSeconds(Date date, Integer seconds) {
57        if (Objects.isNull(seconds)){
58            seconds = DEFAULT_EXPIRED_SECONDS;
59        }
60        Calendar calendar = Calendar.getInstance();
61        calendar.setTime(date);
62        calendar.add(Calendar.SECOND, seconds);
63        return calendar.getTime();
64    }
65}
66
67
68

3.6 测试类

创建了一个测试的controller进行测试,里面写了一个test方法,方法在获取锁的时候会sleep 2秒,便于我们进行测试。完整内容如下:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
1package com.dalaoyang.controller;
2
3import com.dalaoyang.service.LockService;
4import org.springframework.beans.factory.annotation.Autowired;
5import org.springframework.web.bind.annotation.GetMapping;
6import org.springframework.web.bind.annotation.RestController;
7
8
9@RestController
10public class TestController {
11
12    @Autowired
13    private LockService lockService;
14
15    @GetMapping("/tryLock")
16    public Boolean tryLock(String tag, Integer expiredSeconds) {
17        return lockService.tryLock(tag, expiredSeconds);
18    }
19
20    @GetMapping("/unlock")
21    public Boolean unlock(String tag) {
22        lockService.unlock(tag);
23        return true;
24    }
25
26    @GetMapping("/test")
27    public String test(String tag, Integer expiredSeconds) {
28        if (lockService.tryLock(tag, expiredSeconds)) {
29            try {
30                //do something
31                //这里使用睡眠两秒,方便观察获取不到锁的情况
32                Thread.sleep(2000);
33            } catch (Exception e) {
34
35            } finally {
36                lockService.unlock(tag);
37            }
38            return "获取锁成功,tag是:" + tag;
39        }
40        return "当前tag:" + tag + "已经存在锁,请稍后重试!";
41    }
42}
43
44
45

3.测试

项目使用maven打包,分别使用两个端口启动,分别是20000和20001。


1
2
3
1java -jar springboot2_distributed_lock_mysql-0.0.1-SNAPSHOT.jar --server.port=20001
2
3

1
2
3
1java -jar springboot2_distributed_lock_mysql-0.0.1-SNAPSHOT.jar --server.port=20000
2
3

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

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

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

4.总结

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

5.源码地址

源码地址:https://gitee.com/dalaoyang/springboot_learn/tree/master/springboot2_distributed_lock_mysql

给TA打赏
共{{data.count}}人
人已打赏
安全网络

CDN安全市场到2022年价值76.3亿美元

2018-2-1 18:02:50

安全运维

Elasticsearch性能优化实战指南

2021-12-11 11:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索