目录
前言
工作中开发过一个上传文件的接口,每个区县都有自己的资源压缩包需要上传到系统,系统接收到压缩包后,需要解压,提取出里面的文件保存到文件服务器中,解析里面的SQLITE文件得到数据保存到数据库中。
由于处理的过程会比较耗时,所以使用了异步处理的方式来优化用户体验,接口接收到文件后快速响应,返回上传成功,异步线程在后台继续执行解析压缩包业务逻辑。为了防止在异步线程处理期间,用户再次上传压缩包,从而导致上传资源数据不一致问题,在异步线程处理期间要获取锁来保证上传资源数据一致。由于项目的架构是微服务架构,所以需要使用分布式锁。
项目中有使用 Redis,所以可以基于 Redis 来实现分布式锁,利用 Redis 的 SetNX便能够简单实现分布式锁了。
SetNX
使用 Spring 框架中的 RedisTemplate 客户端,通过它可以对Redis进行多种操作
pom
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring2.0集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
yml
application.yml
spring: redis: host: localhost password: '' port: 6379 timeout: 10000 lettuce: pool: max-active: 8 max-idle: 8 max-wait: -1 min-idle: 0 server: port: 8888
Controller
以下是一个简单的 RedisTemplate 使用 SetNX 的简单示例
java
@RestController
@RequestMapping("/redis")
public class RedisTemplateController {
@Resource
RedisTemplate<String, String> redisTemplate;
@GetMapping("/setNX")
public String setNX(String key) {
Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", 60, TimeUnit.SECONDS);
if (!Boolean.TRUE.equals(flag)) {
return "请稍后再试";
}
try {
System.out.println("processing");
Thread.sleep(4 * 1000);
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
} finally {
redisTemplate.delete(key);
}
return "成功";
}
}
存在的问题
使用 Redis + RedisTemplate,可以非常简单高效地实现分布式锁,针对绝大部分小系统已经够用了,因为大部分系统终其一生都遇不到那些极端情况,就算遇到了也是重启大法解决一切。。。
存在以下的问题:
- 锁无法续期:如果为锁设置了过期时间,万一超过了过期时间程序还没有执行完,而锁就被释放了,程序就可能出错了。
- 锁永不释放:如果不设置过期时间,那么如果客户端崩溃,那么该分布式锁就永远不会被释放了。
- 不可重入:同一个线程无法重复获取到锁,导致发生死锁。
可重入的SetNX
直接使用 SetNX 会存在以上的问题,不可重入的问题,实际上可以通过线程ID来标识锁,在加锁的时候,将唯一的线程ID也保存进去,这样在判断锁的时候,就可以通过线程ID来判断该分布式锁是不是属于当前线程的了,如果是,则重入数+1
示例代码如下:
java
public class RedisLockUtil {
public static synchronized boolean lock(RedisTemplate<String, Serializable> redisTemplate, String key) {
String currentThreadId = String.valueOf(Thread.currentThread().getId());
String lockValue = (String) redisTemplate.opsForValue().get(key);
if (lockValue == null) {
lockValue = String.format("%s:%s", currentThreadId, 1);
redisTemplate.opsForValue().set(key, lockValue);
return true;
}
String[] parts = lockValue.split(":");
if (parts.length == 2 && parts[0].equals(currentThreadId)) {
int count = Integer.parseInt(parts[1]) + 1;
lockValue = String.format("%s:%s", currentThreadId, count);
redisTemplate.opsForValue().set(key, lockValue, 60, TimeUnit.SECONDS);
return true;
}
return false;
}
public static synchronized void unlock(RedisTemplate<String, Serializable> redisTemplate, String key) {
String currentThreadId = String.valueOf(Thread.currentThread().getId());
String lockValue = (String) redisTemplate.opsForValue().get(key);
if (lockValue != null) {
String[] parts = lockValue.split(":");
if (parts.length == 2 && parts[0].equals(currentThreadId)) {
int count = Integer.parseInt(parts[1]);
if (count > 1) {
lockValue = String.format("%s:%s", currentThreadId, count - 1);
redisTemplate.opsForValue().set(key, lockValue, 60, TimeUnit.SECONDS);
} else {
redisTemplate.delete(key);
}
}
}
}
}
这也只是解决了可重入的问题,还是存在锁续期,锁永远不会被释放问题。
Redisson
解决以上问题,一劳永逸的方式就是直接使用 Redisson ,Redisson 的 lock 通过 Watchdog机制,解决了锁续期的问题。
在没有指定过期时间的前提下,Redisson 客户端实例获取到一个分布式锁,Watchdog 机制基于 Netty 的时间轮启动一个后台任务,定期向Redis发送续期命令,重新设置锁的过期时间,默认续期30秒,每10秒做一次续期。
当分布式锁释放或者 Redisson 客户端关闭时,Watchdog 也停止锁的续期任务。这样就完美地保证程序执行期间获取到的锁不会提前释放。即使客户端崩溃,也不会出现锁永远不会被释放的情况,因为客户端崩溃,Watchdog 也一起停止了续期任务,过了过期时间,锁自己就释放了。
pom
XML
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.11.4</version>
</dependency>
yml
application.yml
spring: redis: redisson: config: "classpath:redisson.yml" server: port: 8888
redisson.yml
{
"singleServerConfig":{
"idleConnectionTimeout":10000,
"pingTimeout":1000,
"connectTimeout":10000,
"timeout":3000,
"retryAttempts":3,
"retryInterval":1500,
"subscriptionsPerConnection":5,
"clientName":null,
"address": "redis://localhost:6379",
"subscriptionConnectionMinimumIdleSize":1,
"subscriptionConnectionPoolSize":50,
"connectionMinimumIdleSize":32,
"connectionPoolSize":64,
"database":0
},
"threads":0,
"nettyThreads":0,
"codec":{
"class":"org.redisson.codec.JsonJacksonCodec"
},
"transportMode":"NIO"
}
Controller
java
@RestController
@RequestMapping("/redisson")
public class RedissonLockController {
@Resource
private RedissonClient redissonClient;
@GetMapping("/lock")
public String reentrantLock(String key) {
RLock reentrantLock = redissonClient.getLock(key);
try {
if (!reentrantLock.tryLock()) {
return "请稍后再试";
}
System.out.println("processing");
Thread.sleep(4 * 1000);
} catch (Exception e) {
reentrantLock.unlock();
} finally {
reentrantLock.unlock();
}
return "成功";
}
}