1. redis分布式锁主要是由以下4个命令实现的:
a. setnx:是set if not exists的缩写,也就是当该key在redis中不存在的时候才放入redis中,这个步骤分为两步:首先判断该key是否存在,若不存在,则放入;这两步操作是个原子操作,所以这个命令能够实现锁的效果;
b. getset:这个命令先根据key执行get操作,再执行set操作,这个命令的作用是:先获取原来的旧值,再设置一个新值;这个命令也具有原子性;
c. expire:设置键的有效期;
d. del:根据key,删除对应的value;
2. Redis分布式锁流程图--基础版(不防死锁)
这个版本的redis分布式锁是有缺陷的,考虑一个这样的应用场景:
当业务量增加到一定程度后,一台服务器无法应付这么大的业务量了,需要再启动一台服务器,用两台服务器同时运行;假设我们使用tomcat运行项目的war包,用nginx做负载均衡;这两个war包里都有定时任务,根据业务需要,只能在一台服务器上执行定时任务,这时候就用到redis分布式锁了。即在同一时刻,哪个tomcat获取到redis锁,哪个tomcat就执行定时任务,另一个tomcat则提前执行结束。假设tomcat-A在执行完setnx后,获取到redis锁,在即将执行expire(lockkey)时,tomcat-A突然宕机了,那么后果就是该lockkey在redis中将永久存在;在下一个时间点,要执行该定时任务时,任何一个tomcat都无法执行,因为这个lockkey在redis中永久存在了。
3. Redis分布式锁--优化版(双重防死锁)--setnx命令和expire分开执行
上述图片对应代码如下:
优化版本的redis分布式锁与基础版的redis分布式锁相比较而言,lockKey对应的value是currentTime + timeOut--当前时间戳 + 超时时间(这个超时时间要比定时任务执行的时间略长)
假设我们用5个tomcat才能支撑现在的业务量,每个tomcat上运行的功能代码都是一样的,每个tomcat都可以运行这个定时任务,但同一时间点只能有一个tomcat执行该定时任务;
//每分钟执行一次
@Scheduled(cron="0 */1 * * * ?")
public static void borrowcash() throws InterruptedException {
logger.info("当前tomcat = 【" + PropertiesUtil.getProperty("tomcat_platform") + "】,执行定时任务");
//lockTimeout:这个时间要比定时任务执行时间要长一些,因为在setnx命令后,若是没有获取到锁,
//那么在第一个else分支下,我们会判断这个lockTimeout是否超时,若是超时了,那么就认为这个锁失效了
long lockTimeout = 50l;
Long setnxResult = RedisShardedPoolUtil.setnx(BORROWCASH_LOCK,String.valueOf(System.currentTimeMillis()+lockTimeout));
if(setnxResult != null && setnxResult.intValue() == 1){
/*同一时间点只有一个tomcat能获取到锁,从而进入到这里---执行业务逻辑,其余4个tomcat只能走else分支*/
sendMsgToUps(BORROWCASH_LOCK);
}else{
/*其余未获取到锁的4个tomcat都执行这个else分支下的代码*/
//这4个tomcat先根据key取出旧值,然后根据当前时间戳,看是否可以重置并获取到锁
String oldLockValue = RedisShardedPoolUtil.get(BORROWCASH_LOCK);
if(oldLockValue != null && System.currentTimeMillis() > Long.parseLong(oldLockValue)){
//能进入到这里,说明这把锁已经失效了,既然失效了,这个锁为什么还没有被释放掉:
//a. 其中一个有可能的原因是:获取锁的tomcat正在执行该定时任务,实际完成定时任务所用的时间比预设的lockTimeout的值要长;
//这里使用getset命令重新设置key的时间戳,并获取到旧值
/*两个疑问:
* 1. 为什么使用getset命令,而不是使用两个命令get--获取旧值和set命令--设置新值?
* 因为getset命令在执行的过程中是个原子操作,而get和set这两个命令连在一起使用时,不是原子操作。
* 2. 不使用getset命令,而使用get命令,会出现什么后果?
* 同一时刻,只有一个tomcat获取到锁,那么其余4个tomcat都会执行get操作,它们
* 获取到的值是一样的,接下来如果满足这个条件
* (newLockValue!= null && StringUtils.equals(oldLockValue ,newLockValue)),
* 那么这4个tomcat将都会执行该定时任务,这样一来,就失去了分布式锁的意义;如果不
* 满足这个条件,这4个tomcat都不执行该定时任务,而获取到锁的tomcat在执行完setnx
* 后,突然宕机了,那么此时,该定时任务在这个时间点将不会被执行,即5个tomcat在这
* 一时间点都不会执行该定时任务;在下一时间点,这5个tomcat都会执行该定时任务;
* 为了防止这种情况的发生,所以使用getset命令设置新值,而不使用get命令只获取旧值;
*/
String newLockValue= RedisShardedPoolUtil.getSet(BORROWCASH_LOCK,String.valueOf(System.currentTimeMillis()+lockTimeout));
if(newLockValue!= null && StringUtils.equals(oldLockValue ,newLockValue)){
//真正获取到锁
/*
* 当这个条件newLockValue!= null && StringUtils.equals(oldLockValue ,newLockValue)满足时,即新值和旧值相等,
* 则说明这个锁并没有被其它的tomcat操作,那么此时就可以执行该定时任务了;
* 假设获取到锁的tomcat刚执行完setnx后,就宕机了,那么其余的4个tomcat就算同时
* 执行getset命令,也没关系,因为redis是单线程的,这4个tomcat的请求会串行执
* 行;总有一个tomcat获取到的新值和旧值相等,那么该tomcat就会执行该定时任务;
*/
sendMsgToUps(BORROWCASH_LOCK);
}else{
System.out.println("当前tomcat = 【" + PropertiesUtil.getProperty("tomcat_platform") + "】没有获取到分布式锁 = 【" + BORROWCASH_LOCK + "】");
}
}else{
//a. 当旧值不存在时(要么是key过期了--redis自动将该key删除了,要么是定时任务执行完了,执行定时任务的tomcat使用del命令把该key给删除了),这时候获取到锁的tomcat很可能已经执行完该定时任务了,此时执行到这里的tomcat就执行结束了;
//b. 当旧值存在,且当前时间戳小于旧值时,说明该锁还在有效期内,这时候获取到锁的tomcat很可能正在执行该定时任务,此时执行到这里的tomcat也执行结束了;
System.out.println("当前tomcat = 【" + PropertiesUtil.getProperty("tomcat_platform") + "】没有获取到分布式锁 = 【" + BORROWCASH_LOCK + "】");
}
}
System.out.println("给用户打款结束");
}
private static void sendMsgToUps(String lockName) throws InterruptedException {
RedisShardedPoolUtil.expire(lockName,5);//有效期50秒,防止死锁
System.out.println("当前tomcat = 【" + PropertiesUtil.getProperty("tomcat_platform") + "】获取 = 【" + BORROWCASH_LOCK + "】");
//模拟处理业务所花的时间 2019-06-06
Thread.sleep(3000);
RedisShardedPoolUtil.del(BORROWCASH_LOCK);
System.out.println("当前tomcat = 【" + PropertiesUtil.getProperty("tomcat_platform") + "】释放 = 【" + BORROWCASH_LOCK + "】");
System.out.println("当前tomcat = 【" + PropertiesUtil.getProperty("tomcat_platform") + "】===============================");
}
4. Redis分布式锁--优化版
与版本3相比,有一下改进
4.1 setnx命令和expire命令作为一个原子操作执行
解决的问题:防死锁时,代码逻辑更加简单;
4.2 每个分布式锁,由当前线程加锁,同时也由当前线程根据value的值(value的值不再是时间戳,而是本线程自己的标识,比如是UUID)解锁;
解决的问题:
a. 在异常情况下,执行业务逻辑的时间超过了key的有效期;
b. 当前线程加的锁,由其它线程释放了;
这两个问题都会导致线程安全的问题;
上述图片对应代码如下:
// 分布式锁和Synchronized关键字、ReentrantLock显示锁相同点和不同点:
// 相同点:都是为了保证线程安全;
// 不同点:
// a.适用场景不一样:
// 分布式锁一般情况下是在不同的进程之间来保证线程安全;
// Synchronized关键字和ReentrantLock显示锁是在同一个进程内来保证线程安全;
// b.释放锁的方式不一样:
// b.1 使用redis作为分布式锁,释放锁有两种方式:
// 设置key的有效期,过期自动失效;
// 由加锁成功的线程来执行del(key)命令释放锁;
// b.2 使用Synchronized关键字,jvm自动释放锁;
// b.3 使用ReentrantLock显示锁,由加锁线程来释放锁;
// 使用Redis作为分布式锁,有两个需要注意的点:
// a. 如何避免死锁;
// a1. 业务代码没有执行完之前,服务器宕机,由key的有效期释放锁;
// a2. 业务代码执行完之后,由加锁成功的线程来释放锁;
// b. 如何做到真正的线程安全:
// b1. 在同一个时间点,只能由一个线程来执行代码块中的业务逻辑代码--由setnx来保证;
// b2. 要保证释放锁的操作,只能由加锁成功的线程释放(在不宕机的情况下,不应该由key自动失效来释放锁);
// b.2.1 防止key失效的解决方案:在执行业务代码时,另起一个线程,动态设置key的有效期,防止出现业务代码还没执行完毕,key失效了,后果是:多个线程同时同步代码块,造成线程不安全;
// b.2.2 保证每个分布式锁只能由自己的线程来释放的解决方案:每个线程加锁时,value中存储的是自己的标识(UUID生成的字符串),在执行完业务逻辑后,删除分布式锁时,要判断value值是否是自己的;
//当前线程或进程竞争锁时,设置自己的唯一标识---其目的是,当前线程只能释放自己加的锁;
String value = UUID.randomUUID().toString();
try {
//多线程或多进程竞争分布式锁
boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(Const.TESTKEY, value, Const.TIMEOUT, TimeUnit.SECONDS);
if (false == flag) {
//获取锁失败时,直接返回
return Const.ERRORSTR;
}
// 获取锁成功后,执行业务逻辑;
// 同时另起一个线程B,只要主线程A没有执行结束,
// 则在线程B中,将当前分布式锁的有效时间重置--其目的是防止分布式锁的有效期过了,而业务代码还没执行完毕;
} catch (Exception e) {
e.printStackTrace();
} finally {
//只能释放本线程加的锁
if (value.equals(stringRedisTemplate.opsForValue().get(Const.TESTKEY))) {
stringRedisTemplate.delete(Const.TESTKEY);
}
}
5. 由Redission开源框架实现分布式锁
RLock lock = redissonClient.getLock(Const.TESTKEY);
boolean getLock = false;
try {
if(getLock = lock.tryLock(0,5, TimeUnit.SECONDS)){
log.info("Redisson获取到分布式锁:{},ThreadName:{}",Const.TESTKEY,Thread.currentThread().getName());
//扣减库存
subStore();
}else{
log.info("Redisson没有获取到分布式锁:{},ThreadName:{}",Const.TESTKEY,Thread.currentThread().getName());
}
} catch (Exception e) {
e.printStackTrace();
log.error("Redisson分布式锁获取异常",e);
} finally {
//如果该线程没有加锁成功,则直接返回
if(false == getLock){
return Const.ERRORSTR;
}
lock.unlock();
log.info("Redisson分布式锁释放锁");
}
版本5的实现方案只是把版本4的逻辑给封装了一下。