笔记篇-分布式锁的实现

image.png

什么是分布式锁

分布式锁是为了解决在不同机器上的应用仍能保证资源访问的有序性。当多个进程不在同一个系统中,就需要用分布式锁控制多个进程对资源的访问。

实现方式

目前实现的方式有基于MySQL、基于Redis、基于Zookeeper

基于MySQL

基于表记录实现

通过对数据库表设计时对其做唯一约束,需要加锁进行添加记录,释放锁将记录进行删除。

1
2
3
4
5
6
7
CREATE TABLE `database_lock` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`resource` int NOT NULL COMMENT '锁定的资源',
`description` varchar(1024) NOT NULL DEFAULT "" COMMENT '描述',
PRIMARY KEY (`id`),
UNIQUE KEY `uiq_idx_resource` (`resource`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';

需要获取锁时对数据表进行插入数据

1
INSERT INTO database_lock(resource, description) VALUES (1, 'lock');

释放锁时将记录进行删除

1
DELETE FROM database_lock WHERE resource=1;

基于乐观锁实现

乐观锁的思想就是获取锁的时候并不进行判断,而是在更新的时候对其获取时的版本号进行对比,如果一致则进行更新。

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `optimistic_lock` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`resource` int NOT NULL COMMENT '锁定的资源',
`version` int NOT NULL COMMENT '版本信息',
`created_at` datetime COMMENT '创建时间',
`updated_at` datetime COMMENT '更新时间',
`deleted_at` datetime COMMENT '删除时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uiq_idx_resource` (`resource`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据库分布式锁表';
1
INSERT INTO optimistic_lock(resource, version, created_at, updated_at) VALUES(20, 1, CURTIME(), CURTIME());

STEP1 - 获取资源: SELECT resource, version FROM optimistic_lock WHERE id = 1
STEP2 - 执行业务逻辑
STEP3 - 更新资源:UPDATE optimistic_lock SET resource = resource -1, version = version + 1 WHERE id = 1 AND version = oldVersion

基于悲观锁实现

关闭事务自动提交

1
2
mysql> SET AUTOCOMMIT = 0;
Query OK, 0 rows affected (0.00 sec)

STEP1 - 获取锁:SELECT * FROM database_lock WHERE id = 1 FOR UPDATE;
STEP2 - 执行业务逻辑。
STEP3 - 释放锁:COMMIT。

1
SELECT * FROM database_lock WHERE description='lock' FOR UPDATE;

基于Redis实现

原理

加锁实际上就是在redis中,给Key键设置一个值,为避免死锁,并给定一个过期时间。

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。

语法 SET lock_key random_value NX PX 5000

random_value 是客户端生成的唯一的字符串。
NX 代表只在键不存在时,才对键进行设置操作。
PX 5000 设置键的过期时间为5000毫秒。

举个栗子:``

  • 获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
  • 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。
  • 释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

基于SETNX实现

1.引入依赖

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.6.0</version>
</dependency>

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
public class SetNxLock {

private Jedis jedis = null;

@PostConstruct
public void init(){
jedis = new Jedis("47.106.210.183",6379);
}

/**
* 获取lock
* @param clientId
* @param lockName
* @param expireTime
* @return
*/
public boolean tryLock(String clientId,String lockName,long expireTime) {
Long setNx = jedis.setnx(lockName,clientId);
if (setNx==1){
//如果程序在这里出现问题将无法设置过期时间
jedis.expire(lockName,expireTime);
return true;
}
return false;
}
/**
* 解锁
* @param lockName
* @return
*/
public boolean unLock(String clientId,String lockName) {
String lock = jedis.get(lockName);
if (lock.equals(clientId)) {
Long setNx = jedis.del(lockName);
return setNx==1;
}
return false;
}
}

基于INCR实现

1.这种加锁的思路是, key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作进行加一。然后其它用户在执行 INCR 操作进行加一时,如果返回的数大于 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
36
public class IncrLock {

private Jedis jedis = null;

public IncrLock(){
jedis = new Jedis("127.0.0.1",6379);
}

private Map<String,String> map = new HashMap<>();
/**
* 获取lock
* @param lockName
* @return
*/
public boolean tryLock(String clientId,String lockName) {
Long incrLock = jedis.incr(lockName);
if(incrLock == 1){
//记录是哪个client获取锁
map.put(lockName,clientId);
return true;
}
return false;
}
/**
* 解锁
* @param lockName
* @return
*/
public boolean unLock(String clientId,String lockName) {
if (!clientId.equals(map.get(lockName))) {
return false;
}
jedis.del(lockName);
return true;
}
}

基于Redisson实现

Redisson在基于NIO的Netty框架上,充分的利用了Redis键值数据库提供的一系列优势,在Java实用工具包中常用接口的基础上,为使用者提供了一系列具有分布式特性的常用工具类。

1.引入依赖

1
2
3
4
5
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.10.1</version>
</dependency>

2.实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {

Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
config.useSingleServer().setPassword("redis1234");

final RedissonClient client = Redisson.create(config);
RLock lock = client.getLock("lock1");

try{
lock.lock();
}finally{
lock.unlock();
}
}

基于Zookeeper实现

原理

ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下:

(1)创建一个目录lock;
(2)线程A想获取锁就在lock目录下创建临时顺序节点;
(3)获取lock目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁;
(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点;
(5)线程A处理完,删除自己的节点,线程B监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。

Curator介绍

Curator是一个zookeeper的开源客户端,也提供了分布式锁的实现。他的使用方式也比较简单:

导入依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.0.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.7</version>
</dependency>

获取锁

1
2
3
4
5
6
7
8
InterProcessMutex lock = new InterProcessMutex(client, lockPath);
if ( lock.acquire(maxWait, waitUnit) ) {
try {
// do some work inside of the critical section here
} finally {
lock.release();
}
}

参考资料

curator 官网

三种实现分布式锁的方式

分布式锁用 Redis 还是 Zookeeper

基于zookeeper实现分布式锁

怎样实现redis分布式锁


笔记篇-分布式锁的实现
https://mikeygithub.github.io/2021/04/04/yuque/笔记篇-分布式锁的实现/
作者
Mikey
发布于
2021年4月4日
许可协议