基于redis的分布式锁
一、为什么要做
无疑,关于分布式锁,我们都已比较熟悉,网上有较多的开源解决方案,如redis的redisson,以及zookeeper的curator等,关于这两种分布式锁的使用及原理,后期会写文章介绍。本文主要针对小白,分享一下我学习分布式锁的一些心得,如果是大神请留下您的宝贵意见。
二、俯视代码
关于代码,我写的比较精简,力争在保证功能的情况下让使用上变得更加简单。
1.设计思路
redis锁的核心注意点主要有:
- 设置锁和过期时间是否是原子操作
- 过期时间设置是否合理?太长如果当前实例crash掉影响其他实例获取锁的效率(例如设置一分钟,则其他线程需要等待一分钟之后才能重新获取锁,在这期间无法处理业务),太短的话可能会导致业务还没处理完key就过期,导致锁失效的雪崩效应。
在设计上主要参考了JUC锁的使用模式,实现其Lock接口,在使用上当作ReentranLock使用即可。
2.代码分析
首先看一下锁的主体,首先在过期时间上采取一个比较折中的策略:默认30s,目前直接在代码写死,后期优化成可配置的形式,这样程序宕掉也不至于长时间的不可用;其次,关于业务线程可能阻塞导致的执行时间过长的问题,这边可以看到在lock的时候会启动一个WatchDog线程,此线程的作用是用于监视key的剩余过期时间,发现过小时完成自动续约,以此来保证锁不会被提前释放。
@Component
@Slf4j
public class DefaultLock implements Lock {
public static final String LOCK = "lock";
//默认锁过期时间30秒,后期优化做成可配置
private static long DEFAULT_EXPIRE_TIME = 30L;
@Autowired
private RedisTemplate redisTemplate;
private WatchDog watchDog;
@Override
public void lock() {
//1.这里应对和本地jvm锁一样竞争的场景,如果竞争失败,则自旋
while(!tryLock()){
log.info("线程{}获取分布式锁失败",Thread.currentThread().getName());
}
//2.这里应对有些需要没有获取到锁直接返回失败的场景,后期会做一个策略优化
// Boolean re = redisTemplate.opsForValue().setIfAbsent(LOCK, UUID.randomUUID(), DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
// if(Boolean.FALSE.equals(re)){
// throw new LockCompititionFailException("获取分布式锁失败,当前线程ID:"+Thread.currentThread().getId());
// }
//走到这里说明获取分布式锁成功,开启线程监视key过期时间,防止业务流程还没结束就释放锁的情况
startWatchDog();
}
private void startWatchDog(){
watchDog = new WatchDog(true,redisTemplate);
watchDog.start();
}
@Override
public void lockInterruptibly() throws InterruptedException {
}
@Override
public boolean tryLock() {
return redisTemplate.opsForValue().setIfAbsent(LOCK, UUID.randomUUID(), DEFAULT_EXPIRE_TIME, TimeUnit.SECONDS);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return false;
}
@Override
public void unlock() {
//业务流程结束,释放锁
redisTemplate.delete(LOCK);
//将watchdog线程停止
watchDog.setBusinessDone(false);
}
@Override
public Condition newCondition() {
return null;
}
}
再来看一下WatchDog的实现,比较简单,主要完成续约的问题。
public class WatchDog extends Thread{
//业务流程是否完成
private boolean businessDone;
private RedisTemplate redisTemplate;
public WatchDog(boolean businessDone,RedisTemplate redisTemplate){
this.businessDone = businessDone;
this.redisTemplate = redisTemplate;
}
public boolean isBusinessDone() {
return businessDone;
}
public void setBusinessDone(boolean businessDone) {
this.businessDone = businessDone;
}
@Override
public void run() {
while(businessDone){
//如果锁的剩余过期时间小于10s ,则将其重置
if(redisTemplate.getExpire(DefaultLock.LOCK) < 10L){
redisTemplate.expire(DefaultLock.LOCK,30L, TimeUnit.SECONDS);
}
//为避免空转太频繁,适当让线程sleep
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
三、关于使用
使用上比较简单,用ioc注入DefaultLock实例即可。
public class ZmqTestController {
@Autowired
DefaultLock defaultLock;
//模拟商品数量
private Integer count = 50;
@RequestMapping("/decrease")
@ResponseBody
public String testDistributeLock(){
try {
//加上锁
defaultLock.lock();
if(count < 1){
log.info("库存不足");
return "失败";
}
count--;
log.info("当前线程:{},库存扣减成功,剩余库存:{}",Thread.currentThread().getId(),count);
}catch (Exception e){
log.info(e.getMessage());
}finally {
//释放锁
defaultLock.unlock();
}
return "成功";