Redis的分布式锁 - Alias的博客

Redis的分布式锁

集群下的线程并发问题

我们在使用synchronized进行加锁时,由于每一个JVM有一个锁监视器,这就能够保证每次只有一个线程能获取这把锁。但是若在集群环境下(假设两台),那就会有两个JVM,也就会有两个锁监视器,就会有多个线程获取到锁,这样就没办法实现多JVM之间的互斥,就会产生线程并发问题

分布式锁

实现一个在多个JVM外部的同一个锁监视器

分布式锁:满足分布式系统下或集群模式下多进程可见并且互斥的锁

基于Redis的分布式锁

获取锁:

​ 互斥:确保只能有一个线程获取锁

​ 非阻塞:尝试一次,成功返回true,失败返回false

​ 设置锁和设置超时时间必须保证原子性

1
SET lock thread1 NX EX 10

释放锁:

​ 手动释放

​ 超时释放:获取锁时添加一个超时时间

1
DEL key

简易版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface ILock {

/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true代表获取锁成功; false代表获取锁失败
*/
boolean tryLock(long timeoutSec);

/**
* 释放锁
*/
void unlock();
}
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
public class SimpleRedisLock implements ILock {

private String name;
private StringRedisTemplate stringRedisTemplate;

public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}

private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
UNLOCK_SCRIPT.setResultType(Long.class);
}

@Override
public boolean tryLock(long timeoutSec) {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(success);
}
@Override
public void unlock() {
// 获取线程标示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁中的标示
String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断标示是否一致
if(threadId.equals(id)) {
// 释放锁
stringRedisTemplate.delete(KEY_PREFIX + name);
}
}
}

此时所存在的问题:

Redis的Lua脚本

Redis提供了Lua脚本功能,在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性。

业务的lua脚本:

1
2
3
4
5
6
-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) == ARGV[1]) then
-- 释放锁 del key
return redis.call('del', KEYS[1])
end
return 0

基于Redis的分布式锁实现思路:

1、利用set nx ex 获取锁,并设置过期时间,保存线程标示

2、释放锁时先判断线程标示是否与自己一致,一致则删除锁

特性:

1、利用set nx 满足互斥性

2、利用set ex 保证故障时锁依然能释放,避免死锁,提高安全性

3、利用Redis集群保证高可用和高并发

Redisson

基于setnx实现的分布式锁存在下面问题

Redisson是一个在Redis的基础上实现的java驻内存数据网络。他不仅提供了一系列的分布式的Java常用对象,还提供了许多分布式服务,其中就包含了各种分布式锁的实现。

官网:https://redisson.org

入门:

导入依赖

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

配置客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {

@Bean
public RedissonClient redissonClient(){
// 配置
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.150.101:6379").setPassword("123321");
// 创建RedissonClient对象
return Redisson.create(config);
}
}

使用分布式锁:

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
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Slf4j
@SpringBootTest
class RedissonTest {

@Resource
private RedissonClient redissonClient;

private RLock lock;

@BeforeEach
void setUp() {
lock = redissonClient.getLock("order");
}

@Test
void method1() throws InterruptedException {
// 尝试获取锁
boolean isLock = lock.tryLock(1L, TimeUnit.SECONDS);
if (!isLock) {
log.error("获取锁失败 .... 1");
return;
}
try {
log.info("获取锁成功 .... 1");
method2();
log.info("开始执行业务 ... 1");
} finally {
log.warn("准备释放锁 .... 1");
lock.unlock();
}
}
void method2() {
// 尝试获取锁
boolean isLock = lock.tryLock();
if (!isLock) {
log.error("获取锁失败 .... 2");
return;
}
try {
log.info("获取锁成功 .... 2");
log.info("开始执行业务 ... 2");
} finally {
log.warn("准备释放锁 .... 2");
lock.unlock();
}
}
}

Redisson分布式锁原理:

可重入:利用hash结构记录线程id和重入次数

可重试:利用信号量和PubSub功能实现等待、唤醒,获取锁失败的重试机制

超时续约:利用watchDog,每隔一段时间(releaseTime/3),重置超时时间

总结:

1、不可重入Redis分布式锁:

原理:利用setnx的互斥性;利用ex避免死锁;释放锁时判断线程标示

缺陷:不可重入、无法重试、锁超时失效

2、可重入的Redis分布式锁:

原理:利用hash结构,记录线程标示和重入次数;利用watchDog延续锁时间;利用信号量控制锁重试等待

缺陷:redis宕机引起锁失效问题

3、Redisson的multiLock:

原理:多个独立的Redis节点,必须在所有节点都获取重入锁才算获取锁成功

缺陷:运维成本高、实现复杂

评论