Java并發編程入門(十一)限流場景和Spring限流器實現

限流場景一般基于硬件資源的使用負載,包括CPU,內存,IO。例如某個報表服務需要消耗大量內存,如果并發數增加就會拖慢整個應用,甚至內存溢出導致應用掛掉。

限流適用于會動態增加的資源,已經池化的資源不一定需要限流,例如數據庫連接池,它是已經確定的資源,池的大小固定(即使可以動態伸縮池大小),這種場景下并不需要通過限流來實現,只要能做到如果池內鏈接已經使用完,則無法再獲取新的連接則可。

因此,使用限流的前提是:

1.防止資源使用過載產生不良影響。

2.使用的資源會動態增加,例如一個站點的請求。

二、Spring中實現限流

I、限流需求

1.只針對Controller限流

2.根據url請求路徑限流

3.可根據正則表達式匹配url來限流 4.可定義多個限流規則,每個規則的最大流量不同

II、相關類結構

Java并發編程入門(十一)限流場景和Spring限流器實現

1.CurrentLimiteAspect是一個攔截器,在controller執行前后執行后攔截

2.CurrentLimiter是限流器,可以添加限流規則,根據限流規則獲取流量通行證,釋放流量通行證;如果獲取通行證失敗則拋出異常。

3.LimiteRule是限流規則,限流規則可設置匹配url的正則表達式和最大流量值,同時獲取該規則的流量通信證和釋放流量通信證。

4.AcquireResult是獲取流量通信證的結果,結果有3種:獲取成功,獲取失敗,不需要獲取。

5.Application是Spring的啟動類,簡單起見,在啟動類種添加限流規則。

III、Show me code

1.AcquireResult.java

public class AcquireResult {

    /** 獲取通行證成功 */
    public static final int ACQUIRE_SUCCESS = 0;

    /** 獲取通行證失敗 */
    public static final int ACQUIRE_FAILED = 1;

    /** 不需要獲取通行證 */
    public static final int ACQUIRE_NONEED = 2;

    /** 獲取通行證結果 */
    private int result;

    /** 可用通行證數量 */
    private int availablePermits;

    public int getResult() {
        return result;
    }

    public void setResult(int result) {
        this.result = result;
    }

    public int getAvailablePermits() {
        return availablePermits;
    }

    public void setAvailablePermits(int availablePermits) {
        this.availablePermits = availablePermits;
    }
}
復制代碼

2.LimiteRule.java

/**
 * @ClassName LimiteRule
 * @Description TODO
 * @Author 鏗然一葉
 * @Date 2019/10/4 20:18
 * @Version 1.0
 * javashizhan.com
 **/
public class LimiteRule {

    /** 信號量 */
    private final Semaphore sema;

    /** 請求URL匹配規則 */
    private final String pattern;

    /** 最大并發數 */
    private final int maxConcurrent;

    public LimiteRule(String pattern, int maxConcurrent) {
        this.sema = new Semaphore(maxConcurrent);
        this.pattern = pattern;
        this.maxConcurrent = maxConcurrent;
    }

    /**
     * 獲取通行證
     * @param urlPath 請求Url
     * @return 0-獲取成功,1-沒有獲取到通行證,2-不需要獲取通行證
     */
    public synchronized AcquireResult tryAcquire(String urlPath) {

        AcquireResult acquireResult = new AcquireResult();
        acquireResult.setAvailablePermits(this.sema.availablePermits());

        try {
            //Url請求匹配規則則獲取通行證
            if (Pattern.matches(pattern, urlPath)) {

                boolean acquire = this.sema.tryAcquire(50, TimeUnit.MILLISECONDS);

                if (acquire) {
                    acquireResult.setResult(AcquireResult.ACQUIRE_SUCCESS);
                    print(urlPath);
                } else {
                    acquireResult.setResult(AcquireResult.ACQUIRE_FAILED);
                }
            } else {
                acquireResult.setResult(AcquireResult.ACQUIRE_NONEED);
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        return acquireResult;
    }

    /**
     * 釋放通行證
     */
    public synchronized void release() {
        this.sema.release();
        print(null);
    }

    /**
     * 得到最大并發數
     * @return
     */
    public int getMaxConcurrent() {
        return this.maxConcurrent;
    }

    /**
     * 得到匹配表達式
     * @return
     */
    public String getPattern() {
        return this.pattern;
    }

    /**
     * 打印日志
     * @param urlPath
     */
    private void print(String urlPath) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("Pattern: ").append(pattern).append(", ");
        if (null != urlPath) {
            buffer.append("urlPath: ").append(urlPath).append(", ");
        }
        buffer.append("Available Permits:").append(this.sema.availablePermits());
        System.out.println(buffer.toString());
    }

}
復制代碼

3.CurrentLimiter.java

/**
 * @ClassName CurrentLimiter
 * @Description TODO
 * @Author 鏗然一葉
 * @Date 2019/10/4 20:18
 * @Version 1.0
 * javashizhan.com
 **/
public class CurrentLimiter {

    /** 本地線程變量,存儲一次請求獲取到的通行證,和其他并發請求隔離開,在controller執行完后釋放本次請求獲得的通行證 */
    private static ThreadLocal<Vector<LimiteRule>> localAcquiredLimiteRules = new ThreadLocal<Vector<LimiteRule>>();

    /** 所有限流規則 */
    private static Vector<LimiteRule> allLimiteRules = new Vector<LimiteRule>();

    /** 私有構造器,避免實例化 */
    private CurrentLimiter() {}

    /**
     * 添加限流規則,在spring啟動時添加,不需要加,如果在運行中動態添加,需要加鎖
     * @param rule
     */
    public static void addRule(LimiteRule rule) {
        printRule(rule);
        allLimiteRules.add(rule);
    }

    /**
     * 獲取流量通信證,所有流量規則都要獲取后才能通過,如果一個不能獲取則拋出異常
     * 多線程并發,需要加鎖
     * @param urlPath
     */
    public synchronized static void tryAcquire(String urlPath) throws Exception {
        //有限流規則則處理
        if (allLimiteRules.size() > 0) {

            //能獲取到通行證的流量規則要保存下來,在Controller執行完后要釋放
            Vector<LimiteRule> acquiredLimitRules = new Vector<LimiteRule>();

            for(LimiteRule rule:allLimiteRules) {
                //獲取通行證
                AcquireResult acquireResult = rule.tryAcquire(urlPath);

                if (acquireResult.getResult() == AcquireResult.ACQUIRE_SUCCESS) {
                    acquiredLimitRules.add(rule);
                    //獲取到通行證的流量規則添加到本地線程變量
                    localAcquiredLimiteRules.set(acquiredLimitRules);

                } else if (acquireResult.getResult() == AcquireResult.ACQUIRE_FAILED) {
                    //如果獲取不到通行證則拋出異常
                    StringBuffer buffer = new StringBuffer();
                    buffer.append("The request [").append(urlPath).append("] exceeds maximum traffic limit, the limit is ").append(rule.getMaxConcurrent())
                            .append(", available permit is").append(acquireResult.getAvailablePermits()).append(".");

                    System.out.println(buffer);
                    throw new Exception(buffer.toString());

                } else {
                    StringBuffer buffer = new StringBuffer();
                    buffer.append("This path does not match the limit rule, path is [").append(urlPath)
                            .append("], pattern is [").append(rule.getPattern()).append("].");
                    System.out.println(buffer.toString());
                }
            }
        }
    }

    /**
     * 釋放獲取到的通行證。在controller執行完后掉調用(拋出異常也需要調用)
     */
    public synchronized static void release() {
        Vector<LimiteRule> acquiredLimitRules = localAcquiredLimiteRules.get();
        if (null != acquiredLimitRules && acquiredLimitRules.size() > 0) {
            acquiredLimitRules.forEach(rule->{
                rule.release();
            });
        }

        //destory本地線程變量,避免內存泄漏
        localAcquiredLimiteRules.remove();
    }

    /**
     * 打印限流規則信息
     * @param rule
     */
    private static void printRule(LimiteRule rule) {
        StringBuffer buffer = new StringBuffer();
        buffer.append("Add Limit Rule, Max Concurrent: ").append(rule.getMaxConcurrent())
                .append(", Pattern: ").append(rule.getPattern());
        System.out.println(buffer.toString());
    }
}
復制代碼

4.CurrentLimiteAspect.java

/**
 * @ClassName CurrentLimiteAspect
 * @Description TODO
 * @Author 鏗然一葉
 * @Date 2019/10/4 20:15
 * @Version 1.0
 * javashizhan.com
 **/
@Aspect
@Component
public class CurrentLimiteAspect {

    /**
     * 攔截controller,自行修改路徑
     */
    @Pointcut("execution(* com.javashizhan.controller..*(..))")
    public void controller() { }

    @Before("controller()")
    public void controller(JoinPoint point) throws Exception {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        //獲取通行證,urlPath的格式如:/limit
        CurrentLimiter.tryAcquire(request.getRequestURI());
    }

    /**
     * controller執行完后調用,即使controller拋出異常這個攔截方法也會被調用
     * @param joinPoint
     */
    @After("controller()")
    public void after(JoinPoint joinPoint) {
        //釋放獲取到的通行證
        CurrentLimiter.release();
    }
}
復制代碼

5.Application.java

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).run(args);

        //添加限流規則
        LimiteRule rule = new LimiteRule("/limit", 4);
        CurrentLimiter.addRule(rule);
    }
}
復制代碼

IV、驗證

測試驗證碰到的兩個坑:

1.人工通過瀏覽器刷新請求發現controller是串行的

2.通過postman設置了并發測試也還是串行的,即便設置了并發數,如下圖:

Java并發編程入門(十一)限流場景和Spring限流器實現

百度無果,只能自行寫代碼驗證了,代碼如下:

/**
 * @ClassName TestClient
 * @Description TODO
 * @Author 鏗然一葉
 * @Date 2019/10/5 0:51
 * @Version 1.0
 * javashizhan.com
 **/
public class CurrentLimiteTest {

    public static void main(String[] args) {
        final String limitUrlPath = "http://localhost:8080/limit";
        final String noLimitUrlPath = "http://localhost:8080/nolimit";

        //限流測試
        test(limitUrlPath);

        //休眠一會,等上一批線程執行完,方便查看日志
        sleep(5000);

        //不限流測試
        test(noLimitUrlPath);

    }

    private static void test(String urlPath) {
        Thread[] requesters = new Thread[10];

        for (int i = 0; i < requesters.length; i++) {
            requesters[i] = new Thread(new Requester(urlPath));
            requesters[i].start();
        }
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class Requester implements Runnable {

    private final String urlPath;
    private final RestTemplate restTemplate = new RestTemplate();

    public Requester(String urlPath) {
        this.urlPath = urlPath;
    }

    @Override
    public void run() {
        String response = restTemplate.getForEntity(urlPath, String.class).getBody();
        System.out.println("response: " + response);
    }
}
復制代碼

輸出日志如下:

Pattern: /limit, urlPath: /limit, Available Permits:3
Pattern: /limit, urlPath: /limit, Available Permits:2
Pattern: /limit, urlPath: /limit, Available Permits:1
Pattern: /limit, urlPath: /limit, Available Permits:0
The request [/limit] exceeds maximum traffic limit, the limit is 4, available permit is0.
The request [/limit] exceeds maximum traffic limit, the limit is 4, available permit is0.
The request [/limit] exceeds maximum traffic limit, the limit is 4, available permit is0.
The request [/limit] exceeds maximum traffic limit, the limit is 4, available permit is0.
The request [/limit] exceeds maximum traffic limit, the limit is 4, available permit is0.
The request [/limit] exceeds maximum traffic limit, the limit is 4, available permit is0.
Pattern: /limit, Available Permits:1
Pattern: /limit, Available Permits:2
Pattern: /limit, Available Permits:3
Pattern: /limit, Available Permits:4
This path does not match the limit rule, path is [/nolimit] pattern is [/limit].
This path does not match the limit rule, path is [/nolimit] pattern is [/limit].
This path does not match the limit rule, path is [/nolimit] pattern is [/limit].
This path does not match the limit rule, path is [/nolimit] pattern is [/limit].
This path does not match the limit rule, path is [/nolimit] pattern is [/limit].
This path does not match the limit rule, path is [/nolimit] pattern is [/limit].
This path does not match the limit rule, path is [/nolimit] pattern is [/limit].
This path does not match the limit rule, path is [/nolimit] pattern is [/limit].
This path does not match the limit rule, path is [/nolimit] pattern is [/limit].
This path does not match the limit rule, path is [/nolimit] pattern is [/limit].
復制代碼

可以看到日志輸出信息為:

1.第1個測試url最大并發為4,一次10個并發請求,有4個獲取通行證后,剩余6個獲取通行證失敗。

2.獲取到通行證的4個請求在controller執行完后釋放了通行證。

3.第2個測試url沒有限制并發,10個請求均執行成功。

至此,限流器驗證成功。

end.

相關閱讀:

Java并發編程(一)知識地圖

Java并發編程(二)原子性

Java并發編程(三)可見性

Java并發編程(四)有序性

Java并發編程(五)創建線程方式概覽

Java并發編程入門(六)synchronized用法

Java并發編程入門(七)輕松理解wait和notify以及使用場景

Java并發編程入門(八)線程生命周期

Java并發編程入門(九)死鎖和死鎖定位

Java并發編程入門(十)鎖優化

Java并發編程入門(十二)生產者和消費者模式-代碼模板

Java并發編程入門(十三)讀寫鎖和緩存模板

原文 

https://juejin.im/post/5d9792e8518825157267f6c3

本站部分文章源于互聯網,本著傳播知識、有益學習和研究的目的進行的轉載,為網友免費提供。如有著作權人或出版方提出異議,本站將立即刪除。如果您對文章轉載有任何疑問請告之我們,以便我們及時糾正。

PS:推薦一個微信公眾號: askHarries 或者qq群:474807195,里面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高并發、高性能、分布式、微服務架構的原理,JVM性能優化這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多

轉載請注明原文出處:Harries Blog? » Java并發編程入門(十一)限流場景和Spring限流器實現

贊 (0)
分享到:更多 ()

評論 0

  • 昵稱 (必填)
  • 郵箱 (必填)
  • 網址
手机彩票计划软件超稳