11 07 2019

一、背景

公司有个小程序商城的项目,后台管理系统已经完成了,眼看就要开始做小程序项目,作为公司最尽(shuai)职(qi)的程序员,感觉这个订单和支付模块肯定会分到我手上,于是我便开始未雨绸缪。

了解到下单有扣库存的操作,当访问量比较大的时候容易出现库存超卖的问题,毕竟都是和钱相关了,一不小心就会被祭天了(非酋脸),为了保证数据最终一致性,感觉公司小程序行锁足以应付了,

但是重任肯定不能让DB独自承受,帮他分担下吧,于是我就引入了redis分布式锁(单机环境)。


二、分布式锁


2.1 什么是分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,

那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。 (直接上百度词条) 


2.2 分布式锁的实现方式

1、数据库锁
2、基于Redis的分布式锁
3、基于ZooKeeper的分布式锁
4、Memcached(add命令)
5、。。。。。。


2.3 分布式锁应该是怎样的

1互斥性:可以保证在分布式部署的应用集群中,同一个方法在同一时间只能被一台机器上的一个线程执行(我们这里是单机模式,保证同一个方法在同一时间只能一个线程执行就行了)。
2、无死锁:即便持有锁的客户端崩溃或者其他意外事件,锁仍然可以被获取(可重入)。
3、容错:只要大部分Redis节点都活着,客户端就可以获取和释放锁(单机模式,暂不考虑)
4、解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了


2.4 最开始使用的reids分布式锁

先看下流程图


由流程图可知,我们是通过几个具有原子性的redis命令来实现redis分布式锁的,相关操作如下:

1、通过setnx命令将锁的key(唯一)和现在时间+过期时间加入到redis中,

如果成功将会返回1,失败则返回0(setnx命令:如果key不存在进行set操作,如果不存在不做任何操作)。

2、如果返回1,即获取锁成功,就通过expire命令设置key-value的有效期(防死锁),然后执行业务操作,操作执行完毕之后手动删除释放锁流程就结束了。

3、如果返回0,先通过get命令通过key获取时间戳,判断时间戳,看是否可以重置获取到锁,如果lockValue不是空,并且当前时间大于锁的有效期,说明之前的lock的时间已超时, 超时  的话说明可以获取锁,然后我们通过getSet命令set一个新的锁,如果getset命令返回的时间戳和为空或者和之前通过key获取的时间戳相等,说明获取锁成功, 如果lockValue不是空,并且当前时间小于锁的有效期,说明锁还没过期,不做任何操作。(getset命令:set操作成功之后返回之前的历史数据

接下来我们看看代码

//防死锁分布式锁
public <T extends BaseResponse> T test() {
//自行设计key,需要唯一
Long id = Long.valueOf(241);
//锁5秒有效期
long lockTimeout = Long.parseLong("5000");
//这个时间如何用呢,看下面。和时间戳结合起来用。
Long setnxResult = RedisPoolUtil.setnx(String.valueOf(id), String.valueOf(System.currentTimeMillis()+lockTimeout));
if(setnxResult != null && setnxResult.intValue() == 1){
//如果返回值是1,代表设置成功,获取锁
Boolean b = this.saveLock(2,id);
if(b){
return ResponseUtils.success();
}
}else{
//如果setnxResult==null 或 setnxResult.intValue() ==0 即 != 1的时候
//未获取到锁,继续判断,判断时间戳,看是否可以重置获取到锁
String lockValueStr = RedisPoolUtil.get(String.valueOf(id));

//如果lockValue不是空,并且当前时间大于锁的有效期,说明之前的lock的时间已超时,执行getset命令.
if(lockValueStr != null && System.currentTimeMillis() > Long.parseLong(lockValueStr)){
String getSetResult = RedisPoolUtil.getSet(String.valueOf(id),String.valueOf(System.currentTimeMillis()+lockTimeout));
//再次用当前时间戳getset,
//返回给定 key 的旧值。 ->旧值判断,是否可以获取锁
// 当 key 没有旧值时,即 key 不存在时,返回 nil 。 ->获取锁
//这里我们set了一个新的value值,获取旧的值。
if(getSetResult == null || (getSetResult !=null && org.apache.commons.lang.StringUtils.equals(lockValueStr,getSetResult))){
//获取到锁
Boolean b = this.saveLock(2,id);
if(b){
return ResponseUtils.success();
}
}else{
log.info("没有获得分布式锁:{}",id);
}
}else{
log.info("没有获得分布式锁:{}",id);
}
}
return ResponseUtils.failure("false");
}


解锁代码

/**
* 解锁方法
* @param num 业务操作的数量
* @param id 锁的key
* @return
*/
public Boolean saveLock(int num,Long id){
//expire命令用于给该锁设定一个过期时间,用于防止线程crash,导致锁一直有效,从而导致死锁。
//有效期5秒,防死锁
RedisPoolUtil.expire(String.valueOf(id),5);
log.info("获取{},ThreadName:{}",String.valueOf(id),Thread.currentThread().getName());
//执行业务操作
Boolean b = this.saveInv(num,id);
//释放锁
RedisPoolUtil.del(String.valueOf(id));
log.info("释放{},ThreadName:{}",String.valueOf(id),Thread.currentThread().getName());
log.info("=============================");
return b;
}

这是我第一版的代码,现在网上大部分的博客也是相似的代码,后续和朋友讨论得知这个代码有个很大的BUG,当业务的执行时间大于我们设置的过期时间,由于锁已经过期就会自动放开,从而导致数据出现问题,当时我给出的解决方案是加个行锁,虽然可以解决问题,但是始终太过笨重,也增加了不必要的资源浪费。那这个问题如何解决呢?


三、redisson 实现分布式锁

3.1 Redisson分布式锁的底层原理



3.2 Redisson分布式锁的使用

我们可以看看redisson官方项目 地址  ,从官方的文档中了解到,redisson增加了看门口狗机制,会每隔一定时间去check执行情况从而进行续期操作(业务代码还在执行的话,增加过期时间)。


看看我们写的代码

public <T extends BaseResponse> T test() {
Long id = Long.valueOf(241);
RLock lock = redissonManager.getRedisson().getLock(String.valueOf(id));
boolean getLock = false;
try {
try {
//trylock增加锁,等待0秒并在50秒后自动解锁
if (getLock = lock.tryLock(0, 50, TimeUnit.SECONDS)) {
log.info("===获取{},ThreadName:{}", String.valueOf(id), Thread.currentThread().getName());
//执行业务操作
Boolean b = this.saveInv(2, id);
if (b) {
return ResponseUtils.success();
}
} else {
log.info("===没有获得分布式锁:{}", String.valueOf(id));
}
} catch (InterruptedException e) {
e.printStackTrace();
}
} finally {
if (!getLock) {
return null;
}
log.info("===释放分布式锁:{}", String.valueOf(id));
//解锁
lock.unlock();
}
}

这就通过redisson解决了我们的问题,这个问题也是面试经常问的(Redis分布式锁的原理以及如何续期)。

以上都是本人的一些看法,如有问题欢迎评论区探讨,图片来源于网络,侵删。


如果文章对你有用请评论或点个赞,顶上去让更多人看到,少踩坑,谢谢(疯狂暗示)  

延伸阅读
  1. TCP的三次握手与四次挥手(详解+动图)
  2. 服务注册和发现 Spring Cloud Eureka 初体验
发表评论