信号量
什么是信号量
信号量(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。
如何使用redis实现信号量
- 实现一个简单的信号量
我们使用一个zset(SIGNAL_LOCK_TIMESET_KEY)来保存对某个资源的信号量。使用不同的uuid作为value来标识使用者,使用时间来作为score来进行排序。
(1)将过期的信号量清除掉
jedis.zremrangeByScore(LockUtils.signalLockTimeKey(lockName),0,getEndDate(timeUnit,timeOut).getTime());
(2)将新的信号量插入
jedis.zadd(LockUtils.signalLockTimeKey(lockName),new Date().getTime(),uuid);
(3)获取信号量所在的排名
if(jedis.zrank(LockUtils.signalLockTimeKey(lockName),uuid) < limit){
return uuid;
}
(4)如果信号量不在限定的排名,那么移除该信号量
//移除过期的判断
jedis.zrem(LockUtils.signalLockTimeKey(lockName),uuid);
但是这样会有一个问题,如果在分布式系统中有A、B两个用户分别在A、B两台机器上争夺信号量(假设只能有一个人获得到)。B机器和A机器在获取时间的时候是完全相同的,这个时候score就一样,那么不论A用户先获取到信号量还是B用户先获取到信号量,后插入的那个用户因为score和前一个用户相同,那么总会排在前一个用户前面。导致系统就会有两个人获取到信号量。如果在高并发情况下,我们完全不能掌握有多少个用户可以获取到信号量。
2. 实现一个公平的信号量
我们使用三个KEY,一个与前一个相同ZSET(SIGNAL_LOCK_TIMESET_KEY,用来存储key的时间,用来过期),第二个ZSET(SIGNAL_LOCK_SORTED_KEY,value为uuid,score则为一个自增长的值),第三个String(SIGNAL_LOCK_INCR_KEY,一个自增长的key)。自增长的key可以为不同的用户提供不同的排序值,这样就会保证所有的用户都是有序的。
(1)首先移除已经过期的key
//移除已经过期的标记
jedis.zremrangeByScore(LockUtils.signalLockTimeKey(lockName),0,getEndDate(timeUnit,timeOut).getTime());
ZParams zParams = new ZParams();
zParams.weightsByDouble(1,0);
zParams.aggregate(ZParams.Aggregate.SUM);
jedis.zinterstore(LockUtils.signalLockSortedKey(lockName),zParams,LockUtils.signalLockSortedKey(lockName),LockUtils.signalLockTimeKey(lockName));
不仅仅是要将过期的key从SIGNAL_LOCK_TIMESET_KEY中移除,还需要将没有过期时间的key从SIGNAL_LOCK_SORTED_KEY中移除。
(2)从自增长的key中获取最新的值
Long incrNum = jedis.incr(LockUtils.signalLockIncrKey(lockName));
(3)分别向SIGNAL_LOCK_TIMESET_KEY和SIGNAL_LOCK_SORTED_KEY的key下新增最新的uuid
jedis.zadd(LockUtils.signalLockSortedKey(lockName),incrNum,uuid);
jedis.zadd(LockUtils.signalLockTimeKey(lockName),new Date().getTime(),uuid);
(4)判断用户是否拿到了信号量
if(jedis.zrank(LockUtils.signalLockTimeKey(lockName),uuid) < limit){
return uuid;
}
这个时候也有一个问题,因为自增长的key大家都可以获取。A、B两个用户分别获取到1、2两个值,但是B先将值插入到SIGNAL_LOCK_SORTED_KEY中,A后插入。这个时候B获取到的是位置1,而A获取到的也是位置1,导致两个用户都获取到了信号量。这个时候想到了什么?直接将自增长的key加锁。
贴一下完整的代码
/**
* 获取信号量的锁
* 1、移除掉已经过期的标记
* 2、将新的标记加入到队列中
* 3、获取新标记所在的位置,如果在允许的范围,那么返回标记,否则移除标记
* @param lockName 要锁的资源名称
* @param timeUnit 时间的单位 (默认为分钟)
* @param timeOut 锁的超时时间
* @param limit 可以获取到锁的资源数量
* @return
*/
public static String lock(Jedis jedis, final String lockName, TimeUnit timeUnit,int timeOut, int limit){
AssertLock.isTrue(!StringUtils.isEmpty(lockName),"lockName不能为空");
AssertLock.isTrue(null != timeUnit,"timeUnit不能为空");
AssertLock.isTrue(0 < timeOut,"timeOut必须大于0");
AssertLock.isTrue(0 < limit,"limit必须大于0");
//获取自增长信息
String uuid = null;
try {
uuid = CommonLock.lock(jedis,LockUtils.signalLockIncrKey(lockName));
if(StringUtils.isEmpty(uuid)){
// logger.info("获取自增长锁{}失败",LockUtils.signalLockIncrKey(lockName));
return uuid;
}
return semaLock(uuid,jedis,lockName,timeUnit,timeOut,limit);
}catch (Exception e) {
logger.error("获取信号量锁失败",e);
}finally{
if(!StringUtils.isEmpty(uuid)){
CommonLock.unLock(jedis,LockUtils.signalLockIncrKey(lockName),uuid);
}
}
return null;
}
/**
*
* @param jedis
* @param lockName
* @param timeUnit
* @param timeOut
* @param limit
* @return
*/
private static String semaLock(String uuid,Jedis jedis,String lockName,TimeUnit timeUnit,int timeOut,int limit) {
try {
//移除已经过期的标记
jedis.zremrangeByScore(LockUtils.signalLockTimeKey(lockName),0,getEndDate(timeUnit,timeOut).getTime());
ZParams zParams = new ZParams();
zParams.weightsByDouble(1,0);
zParams.aggregate(ZParams.Aggregate.SUM);
jedis.zinterstore(LockUtils.signalLockSortedKey(lockName),zParams,LockUtils.signalLockSortedKey(lockName),LockUtils.signalLockTimeKey(lockName));
//将新标记添加到队列中
Long incrNum = jedis.incr(LockUtils.signalLockIncrKey(lockName));
jedis.zadd(LockUtils.signalLockSortedKey(lockName),incrNum,uuid);
jedis.zadd(LockUtils.signalLockTimeKey(lockName),new Date().getTime(),uuid);
if(jedis.zrank(LockUtils.signalLockTimeKey(lockName),uuid) < limit){
return uuid;
}
unlock(jedis,lockName,uuid);
}catch (Exception e){
logger.error("获取信号量锁失败", e);
//如果没有获取到信号量,那么需要移除信号量信息
unlock(jedis,lockName,uuid);
}
return null;
}
/**
* 解锁
* @param jedis
* @param lockName
* @param uuid
*/
public static void unlock(Jedis jedis,String lockName,String uuid){
//移除过期的判断
jedis.zrem(LockUtils.signalLockTimeKey(lockName),uuid);
//移除排序的判断
jedis.zrem(LockUtils.signalLockSortedKey(lockName),uuid);
}
刷新信号量
有时候,我们不希望信号量只能使用一定的时间,而是希望可以在使用的时候可以不断的刷新他的使用时间。这个时候我们可以这样。
/**
* 刷新锁
* @param jedis
* @param lockName
* @param uuid
*/
public static String refreshLock(Jedis jedis,String lockName,String uuid){
if(1 == jedis.zadd(LockUtils.signalLockTimeKey(lockName),new Date().getTime(),uuid)){
//如果是新增的,那么证明信号量已经过期了
unlock(jedis,lockName,uuid);
return null;
}
return uuid;
}