什么是信号枪?
信号枪(Semaphore),有时被称为信号灯,是在多线程环境下使用的一种设施,能保证一个共同资源在某一段时间内只有有限数的线程能够进行访问的机制
如何实现?
基本思路:
1用zset有序集合来存储信息,键为随机生成的Id,值为当前的时间戳,
这样zset会根据时间戳进行排序
2每次尝试获取信号量之前,要先对集合进行过滤,删除已经过期的信号量
3插入信号量到集合之后,要获取当前的信号量的排名在集合中的索引位置,只有足够低的情况下(即要小于设置的临界值,如小于5),才能表示获取成功,否则就删除集合中该信号量元素
代码:
public String acquire_semaphore(int limit,long time_out,String listName){
String id= UUID.randomUUID().toString();
redisTemplate.setEnableTransactionSupport(true);
long now=System.currentTimeMillis();
redisTemplate.multi();
redisTemplate.opsForZSet().removeRangeByScore(listName,0,now-time_out);//先过滤元素
redisTemplate.opsForZSet().add(listName,id,now);
redisTemplate.opsForZSet().rank(listName,id);
if((int)redisTemplate.exec().get(2)<limit){
//排名足够小,才能获取信号量
return id;
}else{
redisTemplate.opsForZSet().remove(listName,id);
//否则就移出
return null;
}
}
不足之处:
该程序运行虽然很快,但是无法保证并发情况下,如A,B两个线程同时获取了信号量,则A,B在zset中的排名是相同的,这就造成了不公平情况的发生,这种情况也是并发环境下绝对会出现的情况,所以应该进行改进:
改进版本:公平信号量
如何实现公平信号量?
具体思路:
现在想要做的,是又想用时间戳来进行过期处理,又想要公平,所以就换个想法,用一个计数器count来表示身份,每次尝试获取信号量时,让count自增获取count的值,然后插入表中,但是在插入之前还要进行如下操作:
1用两张表来存储数据,一张是信号量持有者表,另一张是超时有序集合
2每次存储前,先对超时有序集合进行类似上述的过滤
3用zset的zinterStroe来获取两张表的交集
最后,执行插入,获取排名,查看是否排名是否够低
代码如下:
public static String fair_semaphore(String listName,int limit,long time_out){
String id=UUID.randomUUID().toString();
String czset=listName+":owner";
String ctr=listName+":counter";
long now=System.currentTimeMillis();
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.multi();
redisTemplate.opsForZSet().removeRangeByScore(listName,0,now-time_out);//第二步
redisTemplate.opsForZSet().intersectAndStore(listName,czset,czset);
//第三步
redisTemplate.opsForValue().increment(ctr);
//自增
int count=(int)redisTemplate.exec().get(3);
redisTemplate.multi();
redisTemplate.opsForZSet().add(listName,id,now);
redisTemplate.opsForZSet().add(czset,id,count);
redisTemplate.opsForZSet().rank(czset,id);
if((int)redisTemplate.exec().get(2)<limit){
return id;
//获取排名,查看排名
}else {
//不够低,没有获取,就删除
redisTemplate.multi();
redisTemplate.opsForZSet().remove(listName,id);
redisTemplate.opsForZSet().remove(czset,id);
redisTemplate.exec();
return null;
}
}
信号量刷新机制:
有时候,我们不希望信号量只能使用一定的时间,而是希望可以在使用的时候可以不断的刷新他的使用时间。这个时候我们可以这样:
//信号量刷新:
public boolean fresh_semaphore(String listName,String id){
if(redisTemplate.opsForZSet().add(listName,id,System.currentTimeMillis())){
//如果还能加入,说明信号量在之前就已经过期了,那么当然不能继续使用,就删除始放
release_semaphore(listName,id);
return false;
}
return true;
}
上述版本存在的问题:
如果a,b两个线程争夺信号量,假设a先进行自增操作,但是b比a先插入有序集合,之后a再插入,排名比B低,又夺走了b的信号量,而B只有再尝试始放信号或者刷新信号量时才会察觉这一点
解决办法:加锁处理
//带有锁的信号量
public String semaphore_with_lock(String listName,int limit,long time_out) throws InterruptedException {
String id=acquire_lock_withTimeOut(10,"semphoreLock");
if(id!=null){
try{
return fair_semaphore(listName,limit,time_out);
}finally {
release_semaphore(listName,id);
}
}
return null;
}
这样就能保证在同一时刻只有一个能进行自增操作