基于redis实现分布式锁
“ 在上一篇文章中介绍了动态配置定时任务,其中的原理跟spring 定时任务注解@Scheduled一样的,都是通过线程池和定义执行时间来控制。来思考一个问题,如果我们的定时任务在分布式微服务里面呢?在分布式微服务里面一个微服务肯定可以有多个实例的,在上一篇文章当中配置的定时任务就会有可能存在多个,显然定时任务被多次执行并不是我们想要的结果,这个时候我们的分布式锁机制就出现了!”
(分布式锁有很多实现方式,以前我们都是使用synchronized来处理并发请求,虽然也支持分布式,但是总有一些业务不适合,比如秒杀系统的多个商品同时开启秒杀,同一时刻只能完成一件商品的减库存操作,这样就造成了系统的性能瓶颈,也不符合秒杀系统的设计思想。由于 synchronized 无法做到细粒度的控制,从而引进了分布式锁,分布式锁能够完成 synchronized 无法做到的点。下面我们要介绍的是基于redis的实现方式)。
01
—
引入redis依赖
引入springboot官方的redis依赖。
引入一个hutool工具包的依赖,功能很全的一个java工具包,强烈推荐使用。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.3.2</version>
</dependency>
02
—
基于redis实现
怎么使用redis实现呢,先来看下redis的两个命令。
setnx:如果key不存在就跟set一样的作用,如果key存在则什么都不做
getandset:返回上一次的value,并设置新的value
import cn.hutool.core.util.StrUtil;
import cn.hutool.log.StaticLog;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
/**
* redis分布式锁
*
* @author zhongxiaojian
* @date 2020/4/17
**/
public class LockUtil {
private StringRedisTemplate redisTemplate;
/**
* 加锁
*
* @param key 主键
* @param value 当前时间+超时时间
* @return true or false
*/
public boolean lock(String key, String value) {
Boolean lock = redisTemplate.opsForValue().setIfAbsent(key, value);
if (lock != null && lock) {
return true;
}
String currentValue = redisTemplate.opsForValue().get(key);
//如果锁过期
if (!StrUtil.isEmpty(currentValue)
&& Long.parseLong(currentValue) < System.currentTimeMillis()) {
String oldValue = redisTemplate.opsForValue().getAndSet(key, value);
if (StrUtil.isBlank(oldValue) || (!StrUtil.isEmpty(oldValue) && oldValue.equals(currentValue))) {
return true;
}
}
return false;
}
/**
* 解锁
*
* @param key 主键
* @param value 当前时间+超时时间
*/
public void unlock(String key, String value) {
try {
String currentValue = redisTemplate.opsForValue().get(key);
if (!StrUtil.isEmpty(currentValue) && currentValue.equals(value)) {
redisTemplate.opsForValue().getOperations().delete(key);
}
} catch (Exception e) {
StaticLog.error("redis分布式锁解锁异常,{}", e.getMessage());
}
}
}
在上一篇文章当中的代码中使用
public class ScheduleTask implements Runnable {
private static final int TIMEOUT = 30000;
private String id;
private TaskService service;
private String keyword;
private LockUtil lockUtil;
public String getId() {
return id;
}
/**
* @param id 任务ID
* @param service 业务类
* @param keyword 关键字参数
*/
public ScheduleTask(String id, TaskService service,LockUtil lockUtil, String keyword) {
this.id = id;
this.service = service;
this.lockUtil = lockUtil;
this.keyword = keyword;
}
public void run() {
String currentTime = DateUtil.now();
long time = System.currentTimeMillis() + TIMEOUT;
if (lockUtil.lock(id, String.valueOf(time))) {
System.out.println("ScheduleTask start taskId: " + this.id + " time: " + currentTime);
try {
service.work(keyword);
} catch (Exception e) {
StaticLog.error(e.getMessage());
} finally {
lockUtil.unlock(id, String.valueOf(time));
}
}
}
}
03
—
秒杀系统下的应用
这里我们来解释一下为何在lock方法当中加上 “//如果锁过期” 后面的代码,我们以商品秒杀系统举例比较好理解。
假如我们不加上这段代码,在加锁之后的业务流程抛出了一个异常,且这个异常我们没有捕获并处理,那么我们接下来的解锁操作是不会执行的,这个时候我们的锁就变成了死锁,我们就可以使用getandset命令来进行解锁,举个🌰:
现有B商品在参加秒杀活动,假设一个购买B商品的线程发生了死锁,此时currentValue = 1,这个时候购买B商品的两个线程同时调用了lock方法,且value都等于2,同时这两个线程都进入了锁过期的判断"if (!StringUtils.isEmpty(currentValue)&& Long.parseLong(currentValue) <System.currentTimeMillis())",由于redis是单线程的,所以这两个购买B商品的线程也就是说只会有一个线程先执行getandset命令,我们假设第一个先执行线程为B1,第二个执行的线程为B2,那么B1线程执行getandset命令之后的变量:currentValue =1 、oldValue =1,后面的判断"if (oldValue == null || (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)))"就会等于true,B1线程成功获取到了锁;B2跟着马上执行getandset,这个时候的B2的变量:currentValue =1 、oldValue =2,此时后面的判断"if (oldValue == null || (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue)))"就会等于false了,B2线程获取不到锁,所以最终购买B商品的这线程只会有一个线程成功获得锁。
以上,就是我们使用redis实现了分布式锁。
如果你觉得小编写的对你有用的话就扶贫一下吧,哈哈哈