201905W21 Share


又遇到一个使用分布式锁的情况, 但是没有用 Redis 或者 Zookeeper, 而是直接依赖 MySQL 给实现了。

其实用 MySQL 做分布式锁不是很适合, 但是在原理上非常容易理解。

我们只需要在数据库里创建一张表, 然后对这个表写数据, 多个实例中, 成功写入的那一个, 便拿到了锁, 可以进行下一步逻辑。

这里我们不考虑任何的可重入性、 超时、 阻塞等, 仅仅区满足分布式锁的互斥性, 其他的特性我们可以在下一次来实现。

下面开始我们的实现, 使用 SpringBoot 和 MySQL。

  1. 首先, 需要在 maven 引入依赖
1
2
3
4
5
6
7
8
9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
  1. 创建 Event 的 model 和 dao
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
import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity(name = "event")
public class Event {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;

private String name;

private Date startTime;

private Date finishTime;

public int getId() {
return id;
}

public Event setId(int id) {
this.id = id;
return this;
}

public String getName() {
return name;
}

public Event setName(String name) {
this.name = name;
return this;
}

public Date getStartTime() {
return startTime;
}

public Event setStartTime(Date startTime) {
this.startTime = startTime;
return this;
}

public Date getFinishTime() {
return finishTime;
}

public Event setFinishTime(Date finishTime) {
this.finishTime = finishTime;
return this;
}

}
1
2
3
4
5
6
7
8
import indi.yanss.dls.model.Event;
import org.springframework.data.jpa.repository.JpaRepository;

public interface EventRepository extends JpaRepository<Event, Long> {

Event findByName(String name);

}
  1. 创建锁的获取和释放方法
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
import java.util.Date;

import indi.yanss.dls.model.Event;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class EventLock {

@Autowired
private EventRepository eventRepository;

public Event acquire(String name) {
Event event = eventRepository.findByName(name);

if (event == null) {
// 没有事件,注册
event = new Event().setName(name).setStartTime(new Date());
eventRepository.save(event);
} else if (event.getFinishTime() == null) {
// 事件在运行
throw new RuntimeException("Lock has been acquired.");
} else if (event.getFinishTime().before(new Date())) {
// 上一个事件已经完成,开始新事件
int id = event.getId();
event = new Event().setId(id).setName(name).setStartTime(new Date());
eventRepository.save(event);
}
return event;
}

public void release(Event event) {
eventRepository.save(event.setFinishTime(new Date()));
}
}
  1. 创建 Schedule 任务
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
import java.util.Date;

import indi.yanss.dls.model.Event;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class EventLock {

@Autowired
private EventRepository eventRepository;

public Event acquire(String name) {
Event event = eventRepository.findByName(name);

if (event == null) {
// 没有事件,注册
event = new Event().setName(name).setStartTime(new Date());
eventRepository.save(event);
} else if (event.getFinishTime() == null) {
// 事件在运行
throw new RuntimeException("Lock has been acquired.");
} else if (event.getFinishTime().before(new Date())) {
// 上一个事件已经完成,开始新事件
int id = event.getId();
event = new Event().setId(id).setName(name).setStartTime(new Date());
eventRepository.save(event);
}
return event;
}

public void release(Event event) {
eventRepository.save(event.setFinishTime(new Date()));
}
}
  1. 启动 MySQL, 配置连接。 这里不用手动创建表, JPA 会在实例启动时自动帮我们创建和 Event model 对应的表。
1
2
3
4
# 需要启动mysql, 创建数据库demo
spring.datasource.url = jdbc:mysql://localhost:3306/demo
spring.datasource.username = root
spring.datasource.password = 123456
  1. 启动多个实例, 用不同端口即可。

多实例配置

  1. Done! 观察控制台输出吧。