核心特性要求
-
互斥性:同一时刻只有一个客户端能持有锁
-
可重入性:同一客户端可多次获取同一把锁
-
锁超时:避免死锁,持有锁的客户端崩溃后能自动释放
-
高可用:锁服务本身需要高可用,避免单点故障
-
高性能:获取和释放锁的操作应高效
-
阻塞/非阻塞:支持尝试获取锁失败后的处理策略
1、Redis分布式锁流程图(二个要点:①超时解锁 ②获得锁的线程唯一标识,用以谁的锁谁来解锁)

2、Redis分布式锁算法:
①加锁
a:锁的唯一标识(设置随机值作为锁的持有人,只有锁的持有人才可以解锁)
b:锁的超时时间
加锁指令:jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime)
lockKey:key值
requestID:value值,即锁的唯一标识,只有该requestId才可以解锁对应锁
set_if_not_exist:当key不存在时进行set操作,如果key存在则不做任何操作
set_with_expire_time:过期时间key
expireTime:具体过期时间值(一般是通过压测得到一个经验值)
总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。
心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑
②解锁
a:检查是否持有锁
b:持有锁条件下删除锁
采用Lua脚本进行删锁操作

3、redis代码:
3.1redis加锁代码
redis是线程安全,redis server中指令操作是原子的
public class RedisTool { private static final String LOCK_SUCCESS = "OK"; private static final String SET_IF_NOT_EXIST = "NX"; private static final String SET_WITH_EXPIRE_TIME = "PX"; /** * 尝试获取分布式锁 * @param jedis Redis客户端 * @param lockKey 锁 * @param requestId 请求标识 * @param expireTime 超期时间 * @return 是否获取成功 */ public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int 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; } }
上述代码执行有两个结果:①锁不存在,进行加锁操作 ②存在锁,不做任何操作
代码中满足三个条件:①requestID唯一锁持有人标识 ,满足谁的锁谁去解锁
②expireTime 设置超时时间,如果超时自动解锁
③nx 保证key存在则不进行任何操作
3.2redis解锁代码
jedis.eval()是用于redis执行Lua脚本的接口
public class RedisTool { private static final Long RELEASE_SUCCESS = 1L; /** * 释放分布式锁 * @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 (RELEASE_SUCCESS.equals(result)) { return true; } return false; } }
我们将Lua代码传到jedis.eval()方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。
Lua脚本功能:获取key对应的value,然后和requestID对比是否一样,如果一样则解锁,返回true,如果不相等则返回false
4、主节点在同步锁到子节点前挂掉处理方案:redLock


另外,如果客户端A在master1、2、3获取到锁后,master1宕机,此时客户端B又过来请求锁,在master1、4、5获得锁,这种情况下A、B均获得锁了,解决方案是:master1宕机后不会立马重启,而是延迟重启,在延迟重启的时间段内,保证客户端A便执行完代码
5 Redisson 实现可重入锁和锁续期
Redisson原理图

第三节中redis实现分布式锁中可能会出现如下问题:
- 1 假如我们对某个 key 进行了加锁,如果 该key 对应的锁还没有释放的话,在使用相同的key去加锁,大概率是会失败的 -- 可重入锁
privateint expireTime = 1000; public void fun(int level,String lockKey,String requestId){ try{ String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime); if ("OK".equals(result)) { if(level<=10){ this.fun(++level,lockKey,requestId); } else { return; } } return; } finally { unlock(lockKey,requestId); } }
- 2 如果线程A执行任务需要
10s,锁的时间是5s,也就是当锁的过期时间设置的过短,在任务还没执行成功的时候就释放了锁,此时,线程B就会加锁成功,等线程A执行任务执行完成之后,执行释放锁的操作,此时,就把线程B的锁给释放了 -- 锁续期 - 3 对于大量写入的业务场景,使用普通的分布式锁就可以实现我们的需求。但是对于写入操作少的,有大量读取操作的业务场景,直接使用普通的redis锁就会浪费性能了。所以对于锁的优化来说,我们就可以从业务场景,读写锁来区分锁的颗粒度 -- 读写锁
针对以上几点redis分布式锁无法实现,可以使用Redission来实现
Redission分布式锁基本代码如下:
// 1.构造redisson实现分布式锁必要的Config Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:5379").setPassword("123456").setDatabase(0); // 2.构造RedissonClient RedissonClient redissonClient = Redisson.create(config); // 3.获取锁对象实例(无法保证是按线程的顺序获取到) RLock rLock = redissonClient.getLock(lockKey); try { /** * 4.尝试获取锁 * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败 * leaseTime 锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完) */ boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS); if (res) { //成功获得锁,在这里处理业务 } } catch (Exception e) { throw new RuntimeException("aquire lock fail"); }finally{ //5 无论如何, 最后都要解锁 rLock.unlock(); }
基于Redission,接下来详细介绍下 加锁、可重入锁、锁续期、锁释放
加锁
KEYS[1] 锁key
ARG[1] 过期时间
ARG[2] key对应value,即requestId
if (redis.call('exists', KEYS[1]) == 0) //1. exists 查看对应key是否存在。为0表示当前key不存在没线程抢占锁 then redis.call('hset', KEYS[1], ARGV[2], 1); //hset为hash表中字段赋值。设置重入锁--重入锁初始化值为1 redis.call('pexpire', KEYS[1], ARGV[1]); //设置过期时间 return nil; end;
//接下来if then操作就是重入锁的核心部分 if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) // 2 进入该部分说明有线程抢占锁成功,接下来判断是否是同一个线程;hexist用于查redis hash表中字段是否存在该表达式即为redis.get(requiestId)是存在的 then redis.call('hincrby', KEYS[1], ARGV[2], 1); //hincrby 为hash表中字段指定增量值; redis.call('pexpire', KEYS[1], ARGV[1]); //设置超时时间 return nil; end;
//前面两个if都没有进,说明当前key存在,但不是这个requestId,锁被抢占,直接返回对应requesti的的过期时间
//redis.call('pttl', key)是 Redis Lua 脚本中用于获取键剩余生存时间的命令
return redis.call('pttl', KEYS[1]);

锁订阅
对于那些获取锁失败的线程会自旋订阅锁释放事件,当锁被其它资源占用时,当前线程通过 Redis 的 channel 订阅锁的释放事件,一旦锁释放会发消息通知待等待的线程进行竞争
解锁

-- 若锁不存在:则直接广播解锁消息,并返回1 if (redis.call('exists', KEYS[1]) == 0) then redis.call('publish', KEYS[2], ARGV[1]); //锁不存在将解锁消息发布出去,这对应上一步中锁订阅subscribe return 1; end; -- 若锁存在,但唯一标识不匹配:则表明锁被其他线程占用,当前线程不允许解锁其他线程持有的锁 if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil; end; -- 若锁存在,且唯一标识匹配:则先将锁重入计数减1 local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then -- 锁重入计数减1后还大于0:表明当前线程持有的锁还有重入,不能进行锁删除操作,但可以友好地帮忙设置下过期时期 redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else -- 锁重入计数已为0:间接表明锁已释放了。直接删除掉锁,并广播解锁消息,去唤醒那些争抢过锁但还处于阻塞中的线程 redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;

锁续期
针对锁过期时间<业务执行时间问题,需要对锁续期。获取到锁之后,Redisson watchDog自动的开启一个定时任务,每隔 10s 中自动刷新一次过期时间
具体参考 https://blog.csdn.net/weixin_44550507/article/details/145212564 锁续期
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return1; end; return0;
读写锁
读锁
RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock rLock = readWriteLock.readLock();
try {
rLock.lock();
//业务操作
} catch (Exception e) {
log.error(e);
} finally {
rLock.unlock();
}
写锁
RReadWriteLock readWriteLock = redisson.getReadWriteLock("readWriteLock");
RLock rLock = readWriteLock.writeLock();
try {
rLock.lock();
//业务操作
} catch (InterruptedException e) {
log.error(e);
} finally {
rLock.unlock();
}
读写锁特点
- 读锁与读锁不互斥,可共享
- 读锁与写锁互斥
- 写锁与写锁互斥
参考: https://blog.csdn.net/weixin_44550507/article/details/145212564
https://mp.weixin.qq.com/s/j69OLgLIo6R2VI80alJF0Q
https://blog.csdn.net/zhou920786312/article/details/137179599
浙公网安备 33010602011771号