一、什么是分布式锁?
线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该代码段。线程锁只在同一JVM中有效果,因为线程锁的实现在根本上是依靠线程之间共享内存实现的,比如synchronized是共享对象头,显示锁Lock是共享某个变量(state)
进程锁:为了控制同一操作系统中多个进程访问某个共享资源,因为进程具有独立性,各个进程无法访问其他进程的资源,因此无法通过synchronized等线程锁实现进程锁。
分布式锁:当多个进程不在同一个系统中,用分布式锁控制多个进程对资源的访问。
二、分布式锁的特性?
- 互斥性: 任意时刻,只有一个客户端能持有锁。
- 锁超时释放:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。
- 可重入性:一个线程如果获取了锁之后,可以再次对其请求加锁。
- 高性能和高可用:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。
- 安全性:锁只能被持有的客户端删除,不能被其他客户端删除
三、基于redis实现分布式锁
1.Redis分布式锁方案一:SETNX + EXPIRE
即先用setnx来抢锁,如果抢到之后,再用expire给锁设置一个过期时间,防止锁忘记释放
// 获取锁 基于 setnx 和 expire 此方法不会保证原子性 可以使用lua脚本(redis 又演变出 set加过期时间的方式)
public boolean getLockNx(Jedis jedis, String lockeKey, String requestId, Long expireTime) {
Long setnx = jedis.setnx(lockeKey, requestId);
if (Objects.equals(setnx, 1)) {
jedis.expire(lockeKey, new Long(expireTime).intValue());
return true;
}
return false;
}
2.Redis分布式锁方案二:SET的扩展命令(SET EX PX NX)
// 获取锁, 设置超时时间,单位为毫秒 此方法目前可以满足大多数需求
public boolean getLock(Jedis jedis, String lockKey, String requestId, Long expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
存在问题:
- 问题一:「锁过期释放了,业务还没执行完」。假设线程a获取锁成功,一直在执行临界区的代码。但是100s过去后,它还没执行完。但是,这时候锁已经过期了,此时线程b又请求过来。显然线程b就可以获得锁成功,也开始执行临界区的代码。那么问题就来了,临界区的业务代码都不是严格串行执行的啦。
- 问题二:「锁被别的线程误删」。假设线程a执行完后,去释放锁。但是它不知道当前的锁可能是线程b持有的(线程a去释放锁时,有可能过期时间已经到了,此时线程b进来占有了锁)。那线程a就把线程b的锁释放掉了,但是线程b临界区业务代码可能都还没执行完呢
3.Redis分布式锁方案三:分布式锁的释放
//释放锁 存在问题是会误删他人的锁
public boolean releaseLock(Jedis jedis1, String key, String requestId) {
try {
String result = jedis1.get(key);
if (Objects.equals(result, requestId)) {
// lockkey锁失效,下一步删除的就是别人的锁
jedis1.del(key);
return true;
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
jedis1.close();
}
return false;
}
/**
* 释放分布式锁 基于lua脚本释放 保证了原子性 和释放锁是符合自己的 解决并发问题
*
* @param jedis Redis客户端
* @param lockKey 锁
* @param requestId 请求标识
* @return 是否释放成功
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
4.Redis分布式锁方案四:Redisson框架
方案3还是可能存在「锁过期释放,业务没执行完」的问题。有些h会认为,稍微把锁过期时间设置长一些就可以啦。其实我们设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放
开源框架Redisson解决了这个问题。我们一起来看下Redisson底层原理图吧: 只要线程一加锁成功,就会启动一个watch dog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了「锁过期释放,业务没执行完」问题
//redlock 实现分布式锁 最终方案
public boolean getRedLock(RedissonClient redisClient){
RLock lock = redisClient.getLock("REDLOCK_KEY");
try {
boolean flag = lock.tryLock();
if (flag) {
System.out.println("加锁成功");
}
} catch (Exception ex){
} finally {
lock.unlock();
}
return false;
}
|