在springboot的日益成熟之下,分布式架构越来越普遍,面临的问题也随之增加,分布式锁就是其中之一,以前我们都是使用synchronized来处理并发请求,虽然也支持分布式,但是总有一下业务不适合,我们首先来看一个例子:秒杀系统
public synchronized void sellProduct(String productId){
//1、查询该商品库存,为0则活动结束
int stockNum = stock.get(productId);
if (stockNum == 0){
throw new SellException(100,"活动结束");
}else {
//2、下单(模拟不同用户id不同)
orders.put(KeyUtil.getUniqueKey(),productId);
//3、减库存
stockNum = stockNum -1 ;
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
stock.put(productId,stockNum);
}
}
现在有A、B、C三种商品在参加秒杀活动,这个时候如果有一个A商品在调用sellProduct方法,那么B和C商品就都需要等待A商品调用这个方法之后释放锁才能执行,同一时刻只能完成一件商品的减库存操作,这样就造成了系统的性能瓶颈,也不符合秒杀系统的设计思想。由于 synchronized 无法做到细粒度的控制,从而引进了分布式锁,分布式锁能够完成 synchronized 无法做到的点。
分布式锁可以使用redis或者其他技术来完成,这里我们记录一下使用redis实现分布式锁。
引入redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
使用
public void sellProduct(String productId){
//加锁
long time = System.currentTimeMillis()+TIMEOUT;
if (!redisLock.lock(productId,String.valueOf(time))){
throw new SellException(1000,"人太多啦,再试试吧!");
}
//1、查询该商品库存,为0则活动结束
int stockNum = stock.get(productId);
if (stockNum == 0){
throw new SellException(100,"活动结束");
}else {
//2、下单(模拟不同用户id不同)
orders.put(KeyUtil.getUniqueKey(),productId);
//3、减库存
stockNum = stockNum -1 ;
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
stock.put(productId,stockNum);
}
//解锁
redisLock.unlock(productId,String.valueOf(time));
}
实现之前首先我们来看下这两个命令的作用:
setnx:如果key不存在就跟set一样的作用,如果key存在则什么都不做
getandset: 返回上一次的value,并设置新的value
/**
* redis分布式锁
*
* @author zhongxiaojian
* @date 2019-07-30
**/
@Component
@Slf4j
public class RedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 加锁
* @param key 主键
* @param value 当前时间+超时时间
* @return
*/
public boolean lock(String key,String value){
Boolean lock = redisTemplate.opsForValue().setIfAbsent(key,value);
if (lock != null && lock){
return true;
}
// currentValue = 1 这两个线程的value都是2 只有其中一个线程能获取锁
String currentValue = redisTemplate.opsForValue().get(key);
//如果锁过期
if (!StringUtils.isEmpty(currentValue)
&& Long.parseLong(currentValue) < System.currentTimeMillis()){
//获取上一个锁的时间
String oldValue = redisTemplate.opsForValue().getAndSet(key,value);
if (oldValue == null || (!StringUtils.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 (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)){
redisTemplate.opsForValue().getOperations().delete(key);
}
}catch (Exception e){
log.error("【redis分布式锁】解锁异常,{}",e.getMessage());
}
}
}
这里我们来解释一下为何在lock方法当中加上 “//如果锁过期” 后面的代码
// currentValue = 1 这两个线程的value都是2 只有其中一个线程能获取锁
String currentValue = redisTemplate.opsForValue().get(key);
//如果锁过期
if (!StringUtils.isEmpty(currentValue)
&& Long.parseLong(currentValue) < System.currentTimeMillis()){
//获取上一个锁的时间
String oldValue = redisTemplate.opsForValue().getAndSet(key,value);
if (oldValue == null || (!StringUtils.isEmpty(oldValue) && oldValue.equals(currentValue))){
return true;
}
}
假如我们不加上这段代码,那么在上面的使用方法sellProduct当中,在加锁之后的业务流程抛出了一个异常,且这个异常我们没有捕获并处理,那么我们接下来的解锁操作是不会执行的,这个时候我们的锁就变成了死锁,这个时候我们就可以使用getandset命令来进行解锁,我们来看一个例子: 以上,就是我们使用redis实现了分布式锁。
假设一个购买B商品的线程发生了死锁,此时currentValue = 1,这个时候购买B商品的两个线程同时调用了lock方法,且value都等于2,同时这两个线程都进入了锁过期的判断"if (!StringUtils.isEmpty(currentValue)&& Long.parseLong(currentValue)