背景
在实际开发中会碰到一个接口特别耗时,需要进行大量的数据处理和计算。如果不限制接口的请求调用,将会有大量的请求阻塞,导致服务器崩溃。如何解决这种问题呢?下面我们一起看下如何通过Semaphore来实现接口的访问限制。
简介
Semaphore(信号量)是java.util.current包中的一个对象。简单来讲就是通过锁的方式去获取信号量,如果获取到则执行,否则不执行
主要应用在控制并发量,一段时间内只允许特定的线程去执行,甚至可以控制并发量为1,也就是同步执行某一个方法
主要方法
-
Semaphore semaphore = new Semaphore(1); 通过这个方式创建信号量对象,初始化参数是许可总数
-
availablePermits 获取当前存在的许可
-
acquire 争取信号,该方式是先通过自旋锁的方式获取信息,直到permits小于0,然后再通过阻塞的方式去等待的获取许可
-
tryAcquire 非阻塞的方式获取信号量,通过自旋锁的方式获取,如果获取失败直接返回
-
tryAcquire(long timeout, TimeUnit unit) 再特定的实际内阻塞,获取信号量,获取失败返回false
-
release 释放许可,需要注意的是该方法可以传一个参数,释放许可的量,这个方法会扩充许可量,最后在try finally中执行,然后判断是否成功获取许可后再释放许可
代码分析和解读
public static void main(String[] args) { Semaphore semaphore = new Semaphore(1); for (int i = 0; i < 100; i++) { MyThread thread = new MyThread(semaphore); thread.setName(i + 1+""); thread.start(); } } static class MyThread extends Thread { Semaphore semaphore; public MyThread(Semaphore semaphore){ this.semaphore = semaphore; } @Override public void run() { boolean acquire = false; try { int i = semaphore.availablePermits(); String name = Thread.currentThread().getName(); if (i < 1) { System.out.println(String.format("线程%s没获取到:%s", name, i)); return; } System.out.println(String.format("线程%s开始争取信号!!:%s", name, i)); Thread.sleep(1000); // if(Integer.valueOf(name)>50){ // throw new RuntimeException("当前信号量:"+semaphore.availablePermits()); // } acquire = semaphore.tryAcquire(); if (acquire) { System.out.println(Thread.currentThread().getName()+"争取到信号:开始执行"+semaphore.availablePermits()); Thread.sleep(5000); } System.out.println(Thread.currentThread().getName() + " " + semaphore.getQueueLength()); } catch (InterruptedException e) { e.printStackTrace(); } finally { if(acquire){ semaphore.release(1); } } } } }
tryAcquire 适合用在一个程序的执行时间特别长
假设在一个非常大的并发的情况下,有大量线程读取availablePermits是大于0的,这个适合都会通过tryAcquire 或acquire方法去获取许可;如果使用acquire去获取,假设这个后续的执行线程需要占用这个许可大量的时间,这就导致其他线程在阻塞状态,这会导致资源得不到释放,高并发情况下会导致内存飙升,甚至僚机
acquire适合用在执行时间特别短,或需要某一个方法有比较高的成功率
acquire方法在获取许可的时候,先是通过自旋的方式获取,然后再是阻塞等待,直到前面的线程执行完成释放许可,在执行时间特别短的程序可以使用这种方式,会有一个比较高的几率获取到许可,然后成功执行程序。如果用tryAcquire的方式,则会出现大量无效的请求
release方法注意 事项
需要确定释放获取到许可了再在finally中释放许可,否则碰到异常的情况,acquire方法没获取到许可,在finally中释放了许可,会导致许可量大大增
生产实践
该模块已经运用到华宇的在线Excel平台中,用来限制数据清洗等耗时的接口请求。
代码展示
这里是通过AOP和Semaphore来限制接口的请求
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SysLimitAp { String value() default ""; SysSemaphoreLimitAspect.ModuleSemaphoreEnum semaphore(); }
package com.huayu.sys.core.aop; import com.huayu.sys.common.BaseResult; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.concurrent.Semaphore; @Aspect @Component @Slf4j public class SysSemaphoreLimitAspect { @Pointcut("@annotation(com.huayu.sys.core.aop.SysLimitAp)") public void logPointCut() { } public final static String SERVICE_BUSY_RESPONSE = "正在处理服务请求,请稍后再试"; @Around("logPointCut()") public Object around(ProceedingJoinPoint point) throws Throwable { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); SysLimitAp annotation = method.getAnnotation(SysLimitAp.class); if (annotation == null) { throw new IllegalArgumentException("annotation不能为空"); } ModuleSemaphoreEnum semaphoreEnum = annotation.semaphore(); if (semaphoreEnum == null) { throw new IllegalArgumentException("semaphoreEnum参数不能为空"); } Semaphore semaphore = semaphoreEnum.semaphore; int availablePermits=semaphore.availablePermits(); if(availablePermits<1){ return BaseResult.initBaseResult(SERVICE_BUSY_RESPONSE); } boolean hasAcquired = false; try { hasAcquired = semaphore.tryAcquire(1); if(hasAcquired){ Object result = point.proceed(); return result; }else{ return BaseResult.initBaseResult(SERVICE_BUSY_RESPONSE); } } finally { if (hasAcquired) { semaphore.release(1); } } } /** * 实例枚举 */ public enum ModuleSemaphoreEnum { SEMAPHORE_APPLY_SNAPSHOT(1), SEMAPHORE_PUSH_REPORT(1), SEMAPHORE_PUSH_EXPAND_REPORT(1); public int permits; public Semaphore semaphore; ModuleSemaphoreEnum(int permits){ this.permits = permits; this.semaphore = new Semaphore(permits); } } }
具体使用方式:
@SysLimitAp(semaphore = SysSemaphoreLimitAspect.ModuleSemaphoreEnum.SEMAPHORE_PUSH_REPORT)
在接口上使用该注解,我这里使用的是信号量是1,同一时间只允许1个线程去调用该接口。
如果第一次请求没结束,再次访问该接口,可以给前端返回【服务器繁忙】等提示信息。