1.简介
在单机场景中,解决资源互斥访问的话,可以使用synchronized或者ReetrantLock去进行实现;但是,随着分布式的架构出现,那么,就需用通过分布式锁来控制分布式系统之间共享资源的访问了;
分布式锁大概可以通过三种方式:
- 数据库实现分布式锁
- zk实现分布式锁
- redis实现分布式锁 我这篇文章主要是讲解一下redis实现分布式锁;
2.实现
在redis中,有一个语句 set key value [EX seconds|PX milliseconds] [NX|XX]
- EX seconds: 将键的过期时间设置为seconds秒
- PX milliseconds: 将键的过期时间设置为milliseconds毫秒
- NX 只有键不存在时,才能对键进行设置
- XX 只有键已经存在时,才对键进行设置
所以,当key不存在时,才能对键进行设置,当key存在时,不对key进行设置;这个功能很符合我们分布式锁的定义;
set "key" 100 ex 100 nx
OK
set "key" 100 ex 100 nx
(nil)
在这部分的实现,有很多大佬已经实现过了;大佬文章我自己在学习的过程中,也是查看该大佬的文章来进行实现的;
2.1 Redis实现分布式锁问题
我们都知道,我们使用原子性的Redis语句set key value NX
,让多个线程想要获取锁的时候,只有一个线程获取到;但是,如果,该获取到锁的线程发生异常,或者业务处理完毕没有即使释放锁,会导致其他线程会一直尝试获取锁阻塞;但是,这个时候,又引入了一个新的问题,超时时间设定多久合适呢?超时时间太长的话,别的线程会进行阻塞等待;超时时间太短的话,可能当前线程还没有执行完毕,这个资源就被别的线程获取访问了。
2.2 解决方案
由于,这个业务处理的时间,受各个因素所导致,比如说:网络、并发量、当前系统IO CPU的占用情况等因素影响,需要进行动态的设置;所以,我采用一种Future / Callable的方式来进行动态增加key值的存活时间;
流程如下:
- 1.前端页面传过来id时,通过,AOP获取到该参数
- 2.根据获取的参数,去获取对应的分布式锁,如果,没有获取到就直接报错(可以做相应更改)
- 3.把对应Controller层的方法,提交到Callable中进行执行,获取对应的Futrue;
- 4.获取到Futurn之后,每一定时间调用一次isDone(),如果,执行完毕了,就释放锁;如果,还没执行完毕,就增加当前id的存活时间; 相关代码如下:
/**
* @description: AOP拦截器
* @author: lgb
* @create: 2019/05/17 20:54
*/
@Aspect
@Component
public class LockMethodAspect {
@Autowired
private RedisLockHelper redisLockHelper;
@Autowired
private JedisUtil jedisUtil;
private Logger logger = LoggerFactory.getLogger(LockMethodAspect.class);
@Around("@annotation(com.redis.lock.annotation.RedisLock)")
public Object around(ProceedingJoinPoint joinPoint) {
Jedis jedis = jedisUtil.getJedis();
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
RedisLock redisLock = method.getAnnotation(RedisLock.class);
String value = UUID.randomUUID().toString();
Object[] args = joinPoint.getArgs();
String key = redisLock.key();
if (args != null && args.length > 0) {
Object arg = args[0];
if (arg != null) {
key = (String) arg;
}
}
try {
final boolean islock = redisLockHelper.lock(jedis, key, value, redisLock.expire(), redisLock.timeUnit());
logger.info("isLock : {}", islock);
if (!islock) {
logger.error("获取锁失败");
throw new RuntimeException("获取锁失败");
}
try {
logger.info("获取到锁:" + key);
String result = "";
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, 1, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(1));
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
try {
return (String) joinPoint.proceed();
} catch (Throwable throwable) {
logger.warn("业务抛出了异常");
throwable.printStackTrace();
}
return null;
}
};
Future<String> future = threadPoolExecutor.submit(callable);
while (true) {
Thread.sleep(1000);
if (future.isDone()) {
result = future.get();
break;
} else {
jedis.expire(key, 1);
}
}
return result;
}
catch (Throwable throwable) {
throw new RuntimeException("系统异常");
}
} finally {
logger.info("释放锁");
redisLockHelper.unlock(jedis, key, value);
jedis.close();
}
}
}
2.3 相关测试
Postman 同时输入以下网址进行测试:
http://localhost:8080/index?id=11
返回index
http://localhost:8080/index?id=11
"timestamp": "2021-05-17T12:56:40.038+0000",
"status": 500,
"error": "Internal Server Error",
"message": "获取锁失败",
"path": "/index"
http://localhost:8080/index?id=12
index
两个线程id = 11去访问,会失败;一个线程id = 12去访问,就成功了,在一定时间内,只能有一个id去访问后端数据; 符合预期
gitte相关代码