从0到1认识Redis分布式锁
今天来说说Redis分布式锁。
在说Redis分布式锁之前你首先得明白什么是分布式。
在我看来服务部署就两种形式,一种是单体应用,一种是分布式架构。
那么什么叫单体应用呢? 举个简单的例子,比如你的网段ip是 192.168.xxx.xxx,你只有一个服务,就部署在这一台ip上,那么我认为这种就是单体应用。
那么什么又叫分布式架构? 你可以这样理解,比如你的应用最开始上市平平无奇,没有什么访问量,那么单体应用看起来并不会什么问题,完全够用嘛。假如说某一天,你的app火了,成千上万甚至几十万的用户访问到你的系统,那么所有的用户请求全部用一台服务器去承载这是会有大问题的。会造成系统拥堵,cpu过高,系统响应慢,频繁fgc,甚至一台机器可能扛不住,直接挂了。那你的整个系统就瘫痪了。带给用户的体验就是一卡一卡的,然后突然就宕机了。
那么这时候完蛋了,一个大好的赚钱机会就被你单体架构部署给弄丢了。
所以这个时候,我们往往会去采取分布式架构部署服务,在所有的应用前通过一层服务(服务你可以理解为网关或者nginx等等一系列可以分发请求的服务)去分发流量,给系统一个良好并且稳定的工作环境,增加系统的可用性以及稳定性。
在跟你聊什么是分布式锁之前,我们先来看看这样的一种场景。
假如你有一个商品服务,你把他部署了分布式架构,部署了两台,然后通过nginx来分发流量。
然后商品服务里面有一个接口是获取库存接口,伪代码如下
@RestController
public class MarketController {
@Resource
private MarketService marketService;
public String getMarket(String userId){
//获取库存 默认100个
int num = marketService.getMarketNum();
System.out.println("当前库存:" + num + ",用户userId拿到了第" + num +"号商品");
num--;
//重新修改库存
marketService.setMarketNum(num);
return "购买成功!";
}
}
假如对这段操作我们没有任何的上锁操作,那么就会出现超卖的问题。
什么是超卖?
就比如张三和李四同时在抢商品,当前商品是85,当张三拿到了85号商品,系统还没有进行减库存的操作时,李四这时候也进入了这个接口,因为数据库还没被更改,那么李四也拿到了85号商品,这就出现了超卖现象。同一件商品被卖给了两位顾客,你觉得合适吗? 顾客可能心里美滋滋,但是对于系统而言就是资损了。我们肯定不能允许这种问题存在。
好,那么这个时候你已经意识到了要上锁来防止问题的出现了。
那要用什么锁呢? 可能有同学会想到,哎用那个synchronized去锁啊,锁住这一整个方法不就可以了吗?
比如这样
@RestController
public class MarketController {
@Resource
private MarketService marketService;
public String getMarket(String userId){
synchronized (this) {
//获取库存 默认100个
int num = marketService.getMarketNum();
System.out.println("当前库存:" + num + ",用户userId拿到了第" + num + "号商品");
num--;
//重新修改库存
marketService.setMarketNum(num);
}
return "购买成功!";
}
}
如果你是单体架构,那么我认为这堂课我们已经可以下课了
开个玩笑,开个玩笑 ->_ ->
其实还没有结束,你是单体应用的话,确实这样锁住防止了并发,但是其实这把锁是一把悲观锁,效率不高。
而且,如果你是分布式架构,这样就有问题,你只能锁住当前这台虚拟机部署的应用的这个库存方法,当两台服务同时出现用户进行库存操作的时候,其实还是会出现超卖的问题。归根结底,就是多把锁不同步,如果我们能够用一把锁去控制,那这样是不是就不会出现并发的问题了?比如这样
@RestController
public class MarketController {
@Resource
private MarketService marketService;
@Resource
private StringRedisTemplate stringRedisTemplate;
public String getMarket(String userId){
//设置分布式锁
Boolean ifAbsent = stringRedisTemplate.opsForValue()
.setIfAbsent("userId", "product-01", 30, TimeUnit.SECONDS);
if(!ifAbsent){
return "当前系统正在处理,请稍后再试!";
}
//获取库存 默认100个
int num = marketService.getMarketNum();
System.out.println("当前库存:" + num + ",用户userId拿到了第" + num + "号商品");
num--;
//重新修改库存
marketService.setMarketNum(num);
//移除分布式锁
stringRedisTemplate.delete("userId");
return "购买成功!";
}
}
这种样子会不会有问题呢?
其实是有的,假如服务在设置完redis锁之后,到修改库存的那一步,突然程序发生了异常。会发生什么?
程序会回滚,但是redis的操作已经把这把锁给设置上去了。那这把分布式锁就成了一把死锁了,就有大问题了,同学。
好,又有一个同学说,那可以try-catch起来,用finally去删除 不就好了吗,不管发生啥异常,我最后都会要删除掉这把分布式锁,就像这样
@RestController
public class MarketController {
@Resource
private MarketService marketService;
@Resource
private StringRedisTemplate stringRedisTemplate;
public String getMarket(String userId){
try {
//设置分布式锁
Boolean ifAbsent = stringRedisTemplate.opsForValue()
.setIfAbsent("userId", "product-01", 30, TimeUnit.SECONDS);
if(!ifAbsent){
return "当前系统正在处理,请稍后再试!";
}
//获取库存 默认100个
int num = marketService.getMarketNum();
System.out.println("当前库存:" + num + ",用户userId拿到了第" + num + "号商品");
num--;
//重新修改库存
marketService.setMarketNum(num);
} catch (Exception e) {
//异常处理
} finally {
//移除分布式锁
stringRedisTemplate.delete("userId");
}
return "购买成功!";
}
}
我们再来看看这样的结构会不会有问题,
try-catch确实可以解决所有异常,甚至是错误,但是当设置完redis锁之后,到修改库存的那一步,你机器突然不争气的宕机了呢?
那么死锁的问题还是出现了。
怎么解决?
可能有同学就说了,设置过期时间啊,过期时间搞一个不就完事了吗? 你再怎么宕机异常,我这把锁我总归不会成为死锁,就像这样
@RestController
public class MarketController {
@Resource
private MarketService marketService;
@Resource
private StringRedisTemplate stringRedisTemplate;
public String getMarket(String userId){
try {
//设置分布式锁
Boolean ifAbsent = stringRedisTemplate.opsForValue()
.setIfAbsent("userId", "product-01", 30, TimeUnit.SECONDS);
if(!ifAbsent){
return "当前系统正在处理,请稍后再试!";
}
//获取库存 默认100个
int num = marketService.getMarketNum();
System.out.println("当前库存:" + num + ",用户userId拿到了第" + num + "号商品");
num--;
//重新修改库存
marketService.setMarketNum(num);
} catch (Exception e) {
//异常处理
} finally {
//移除分布式锁
stringRedisTemplate.delete("userId");
}
return "购买成功!";
}
}
现在我们再来仔细看看这个程序,不论你宕机还是异常这把锁都会自动过期,不会死锁,程序正常的时候也能去锁住这个接口,防止并发问题导致的超卖现象。看上去好像是没什么问题了。
你再仔细看看,真的没问题了吗?
再仔细看看
再看看
我来举个栗子
如果我们设置的redis超时时间是30秒。
第一个线程在执行修改库存时,这个时候数据库可能卡了,导致线程1整个时间超过了30秒,这个时候还没能够执行完,因为这个时候,分布式锁已经失效了,这个时候第二个线程就可以进来了,那么这个时候,超卖问题是不是又发生了?显然是发生了。
这个时候,可能又有同学会说,时间拉长啊,再拉个几倍呢?
其实不然,不论你时间怎么调其实都会有问题
如果你调小,那有可能就是我上面描述的这种问题,锁过期了但是第一个线程还没能够把程序执行完毕,其他线程就可以进来,这样就会有问题
如果你调大,那有可能发生异常,你重启服务,这个功能要等个好几十秒甚至几分钟才能用,这样也不友好。所以总的来说,不能通过依赖这个过期时间去解决这个问题
虽然这个过期时间其实是可以解决百分之九十九以上的问题的。并且如果你的系统对这个功能点的并发要求不是那么高,其实做到这样子就差不多了。
如果你对一致性有很高的要求,也不想有这种因为时间而导致的并发问题,其实还有一种解决方案
想象一下,如果有另外一个线程,可以在主线程执行这段代码的过程中不断的去看这把锁有没有过期,如果锁快要过期了,就给锁延长一下存在的时间,其实说白了也就是锁续命
通过这种续命的方式,在你的主线程执行完之前都保留有这把分布式锁,这样就可以防止过期时间导致的锁过期的问题了。
那么如何去做呢? 其实市面上已经有比较成熟的框架了。
比如说 Redisson
那么Redisson是怎么去实现这一过程的呢? 我们一起根据源码看一看
本文地址:https://blog.csdn.net/qq_42865087/article/details/110531761
上一篇: Redis深度探险知识总结
下一篇: 到是给我上天啊