第十八节 SpringBoot使用Redis实现分布式锁
一、共享资源问题
我们知道,在如果希望多个线程获取同一个共享资源,在Java里面一般使用Synchronized锁ReentrantLock锁来解决。对于单节点的项目,所有的线程都在同一个JVM进程里面,使用Java语言提供的锁机制可以起到对共享资源进行同步的作用。
那么,我现在遇到的问题是:SpringBoot项目通常都是分布式部署,一般会部署好几个节点。这几个节点之间通常都会出现争夺共享资源的情况。因此,必须使用分布式锁来解决多个节点之间资源共享的问题。比如,这几天,我就遇到一个非常现实的问题。
这个问题是:我现在的应用里有一个定时任务,而应用又部署在了公司的三台服务器上。这就会出现什么情况呢?就是在同一时刻,这个定时任务执行了三次。每个服务器里面的应用都是独立的JVM进程,它们之间没有联系,因此每个节点都理所当然的在固定时刻指定了定时任务。如何解决的呢?这里必须借助中间件----Redis缓存中间件。
二、Redis实现分布式锁
当前普遍的实现思路为:SET my_key my_value NX PX milliseconds。即为每个要执行的任务,添加一个键值对构成的锁。NX表示只有当键key不存在的时候才会设置key的值,PX表示设置键key的过期时间,单位是毫秒。
转为代码思路或许清晰一点:
(1)执行某任务之前,在Redis服务器里面检查是否已经有此任务对应的taskId。
如果没有,将taskId添加进Redis服务器。
如果有,说明当前任务已有其它服务器在执行,因此放弃执行。
(2)任务执行完毕或出现异常,释放同步锁,即清空Redis缓存中此taskId。
好,有了大牛们的思路,我们开始撸码,否则一切都是空谈。
先写一个最简单的定时任务。我们假设上面的定时任务分配了一个taskId,规定每30秒执行一次。
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@EnableScheduling
@Component
public class Task1111 {
@Scheduled(cron = "*/5 * * * * ?")
public void schedule() {
System.out.println("定时任务执行");
}
}
好,根据之前的经验,在执行这个定时任务之前,先判断taskId在不在Redis缓存里面。
如果Redis中存在taskId,那么说明有其它的节点正在处理这个定时任务。
--所以当前节点放弃执行定时任务。
如果Redis中不存在taskId,说明没有任何节点正在执行这个定时任务。
--那么当前节点就执行这个任务。执行任务后释放同步锁即可。
下面代码中的RedisService相关的代码未给出,但是见方法名可知意思。具体代码可以下载源码查看。
package com.zhoutianyu.learnspringboot.task;
import com.zhoutianyu.learnspringboot.redis.RedisService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@EnableScheduling
@Component
public class Task2222 {
private static final Logger LOGGER = LoggerFactory.getLogger(Task2222.class);
@Autowired
private RedisService redisService;
private static final String TASK_ID = "1";
private static final String VALUE = "value";
private static final Long TIME_OUT = 60L;
//each 30s execute
@Scheduled(cron = "*/30 * * * * ?")
public void schedule() {
final byte[] key = TASK_ID.getBytes();
try {
Boolean isExist = redisService.exists(key);
if (isExist) {
LOGGER.error("已经存在任务,放弃执行");
return;
}
//add lock , time:60s
redisService.setEx(key, VALUE, TIME_OUT);
System.out.println("定时任务执行");
//release lock
redisService.del(key);
} catch (Exception e) {
redisService.del(key);
}
}
}
好了,上面的程序基本上就是按照这个思路来的。执行定时任务之前判断key是否存在就可以了。
但是,还有两个问题。第一个问题,在实际过程中,真实的业务可能会处理很久,如果我们强行规定缓存存活60秒。那么60s后,我们的锁过期了。
假设我们当前是A节点,A节点处理任务超过了60s,但是这个锁已经过期了。
B节点在Redis中没有发现这个锁,则判断没有任何其它节点拿到了锁,那么就顺理成章拿到锁并开始执行任务。
A节点在业务执行完毕后,执行释放锁的代码,实际上可能释放了B的同步锁。
卧槽,这不行啊!第二个问题,如果Redis服务器炸了怎么办。
解决第一个问题的办法:增加随机值。在释放锁的时候,判断Redis中的随机数是不是当前节点生成的。
---如果不是当前节点生成的,那么就不要删除了,小心删除了别的节点的同步锁。
因此,代码可以优化为:
package com.zhoutianyu.learnspringboot.task;
import com.zhoutianyu.learnspringboot.redis.RedisService;
import org.apache.commons.lang3.RandomStringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@EnableScheduling
@Component
public class Task3333 {
private static final Logger LOGGER = LoggerFactory.getLogger(Task3333.class);
@Autowired
private RedisService redisService;
private static final String TASK_ID = "1";
private static final Long TIME_OUT = 60L;
//each 30s execute
@Scheduled(cron = "*/30 * * * * ?")
public void schedule() {
final byte[] key = TASK_ID.getBytes();
try {
Boolean isExist = redisService.exists(key);
if (isExist) {
LOGGER.error("已经存在任务,放弃执行");
return;
}
//add lock , time:60s
String value = randomString();
redisService.setEx(key, value, TIME_OUT);
System.out.println("定时任务执行");
//模拟40s的业务时间
Thread.sleep(40);
//release lock
if (value.equals(redisService.get(key))) {
redisService.del(key);
}
} catch (Exception e) {
redisService.del(key);
}
}
private String randomString() {
return RandomStringUtils.randomAlphanumeric(10);
}
}
至于第二个问题,Redis服务器宕机的问题,解决办法就是搭建Redis集群,保证Redis服务器的高可用。
综上所述,今天提供的Redis实现的分布式锁,实际上也只能适用于使用单个Redis服务器的应用,距离真正的应用环境还有一定的差距。实际上,我现在公司里使用分布式锁,它的是基于Zookeepers集群的分布式锁,共3个Zookeepers服务器同时工作。但是,我们不能否定Redis缓存实现分布式锁的价值,如果搭建出Redis集群的话,也能够实现有相同功能的高效的分布式锁。当然,这么高深的技术,博主现在肯定还不会啦(づ╥﹏╥)づ
三、源码下载
本章节项目源码:点我下载源代码
如果本系列文章对你有帮助,不妨请我喝瓶可乐吧!
你的打赏是对我最好的支持!