多執行緒在微服務API統計和健康檢查中的使用

w39發表於2021-09-09

API統計

在服務呼叫的時候,統計每個介面的呼叫次數,從而做到對介面的限流或統計。

在下面的程式碼中,使用了多執行緒的方式進行統計,主要使用瞭如下概念

  • 執行緒池 Executor

  • ConcurrentHashMap

  • CountDownLatch

其中列舉了四種實現方式

  • 1 使用ConcurrentHashMap統計:不過該方法存在問題,統計的increase不是執行緒安全的,所以得到的結果不對

  • 2 使用CAS理念對ConcurrentHashMap進行改進,從而解決自增方法increase的問題

  • 3 使用Google的AtomicLongMap,原理同CAS一致,程式碼量小,比較優雅

  • 4 對HashMap加鎖ReentrantReadWriteLock

本文程式碼示例:

使用ConcurrentHashMap統計

package concurrent;import java.util.Map;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;/**
 * Java 併發實踐- ConcurrentHashMap 與 CAS
 * API呼叫次數統計
 * 涉及概念: 多執行緒/執行緒池/ConcurrentHashMap/CountDownLatch
 * @author billjiang 
 * @createTime 2017-08-04
 */public class CounterDemo {    private final Map<String, Long> urlCounter = new ConcurrentHashMap<>();    /**
     * 介面呼叫次數,此方法存在問題,ConcurrentHashMap的原子方法是同步的,但increase方法沒有同步
     * @param url
     * @return
     */
    public long increase(String url) {
        Long oldValue=urlCounter.get(url);
        Long newValue=(oldValue==null)?1l:oldValue+1;
        urlCounter.put(url,newValue);        return newValue;
    }    //獲取呼叫次數
    public long getCount(String url){        return urlCounter.get(url);
    }    public static void main(String[] args) {
        ExecutorService executorService= Executors.newFixedThreadPool(10);        final CounterDemo counterDemo=new CounterDemo();        int callTime=100000;        final String url="";
        CountDownLatch countDownLatch=new CountDownLatch(callTime);        //模擬併發情況下的介面呼叫統計
        for (int i = 0; i < callTime; i++) {
            executorService.execute(new Runnable() {                @Override
                public void run() {
                    counterDemo.increase2(url);
                    countDownLatch.countDown();
                }
            });
        }        try{
            countDownLatch.await();
        }catch (InterruptedException e){
            e.printStackTrace();
        }

        executorService.shutdown();        //等待所有執行緒統計完成後輸出呼叫次數
        System.out.println("呼叫次數:"+counterDemo.getCount(url));

    }
}

圖片描述

ConcurrentHashMap

從結果上看,使用ConcurrentHashMap存在問題,沒有輸出預期結果,這是因為ConcurrentHashMap雖然是執行緒安全的,不過它的執行緒安全指的是getput等原子方法。而方法increase卻不是執行緒安全的,當然可以透過對increase方法加鎖(使用synchonized關鍵字),不過synchonized是悲觀鎖,其他執行緒要掛起等待,影響效能。可以使用類似樂觀鎖CAS對increase改進。

使用CAS對increase方法改進

關於CAS,可參考這篇文章:

改進後的increase方法如下:

  /**
     * CAS 樂觀鎖/自旋
     * @param url
     * @return
     */
    public long increase2(String url){
        Long oldValue,newValue;        while(true){
            oldValue=urlCounter.get(url);            if(oldValue==null){
                newValue=1l;                //初始化成功,退出迴圈
                if(urlCounter.putIfAbsent(url,1l)==null)                    break;                //如果初始化失敗,說明其他執行緒已經初始化了
            }else{
                newValue=oldValue+1;                //+1成功,退出迴圈
                if(urlCounter.replace(url,oldValue,newValue)){                    break;                    //如果+1失敗,則說明其他執行緒已經修改過了舊值
                }
            }
        }        return newValue;
    }

不過還有更簡單的方法,就是使用AtomicLongMap

使用Google的AtomicLongMap

AtomicLongMap<String> urlCounter3 = AtomicLongMap.create(); //執行緒安全,支援併發public long increase3(String url){     return urlCounter3.incrementAndGet(url);
}

傳統做法,對HashMap加鎖

 Map<String, Integer> map = new HashMap<String, Integer>(); //執行緒不安全
 ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); //為map2增加併發鎖

 public long increase4(String url){    //對map2新增寫鎖,可以解決執行緒併發問題
        lock.writeLock().lock();    try{        if(map.containsKey(key)){            map.put(key, map.get(key)+1);
        }else{            map.put(key, 1);
        }
    }catch(Exception ex){
        ex.printStackTrace();
    }finally{
        lock.writeLock().unlock();
    }
 }

上文中提到的CountDownLatch的概念可參考:

圖片描述

CountDownLatch

健康檢查

場景:服務註冊中心需要定時對服務提供者進行心跳檢測,即定時呼叫服務提供者的特定藉口,如果返回正常狀態嗎,則認為服務正常,否則,認為服務提供者異常,在註冊中心顯示為Down狀態,如Consul的服務健康檢查機制與之類似。

下面使用CountDownLatch和執行緒池模擬這種實現。

思路

首先定義一個應用程式啟動類,它開始時啟動了n個執行緒類,這些執行緒將檢查外部系統並通知閉鎖,並且啟動類一直在閉鎖上等待著。一旦驗證和檢查了所有外部服務,那麼啟動類恢復執行。

實現

BaseHealthChecker:基礎健康檢查類,實現Runable介面,包含CountDownLatch, ServiceName(服務名稱),ServiceUp(服務狀態),其中verifyService 為具體繼承該類的子類要實現的方法。

package concurrent.health;import java.util.concurrent.CountDownLatch;public abstract class BaseHealthChecker implements Runnable {    private CountDownLatch countDownLatch;    private String serviceName;    private boolean serviceUp;    public BaseHealthChecker(String serviceName,CountDownLatch countDownLatch){        super();        this.serviceName=serviceName;        this.countDownLatch=countDownLatch;        this.serviceUp=false;
    }    @Override
    public void run() {        try{
            verifySerivce();
            serviceUp=true;
        }catch (Throwable t){
            t.printStackTrace(System.err);
            serviceUp=false;
        }finally {            if(countDownLatch!=null)
                countDownLatch.countDown();
        }

    }    public String getServiceName() {        return serviceName;
    }    public boolean isServiceUp() {        return serviceUp;
    }    //this method need to be implemented by all specific service checker
    public abstract void verifySerivce();

}

DatabaseHealthChecker: 資料庫健康檢查類

package concurrent.health;import java.util.concurrent.CountDownLatch;public class DataBaseHealthChecker extends BaseHealthChecker {    public DataBaseHealthChecker(CountDownLatch countDownLatch) {        super("database service", countDownLatch);
    }    @Override
    public void verifySerivce() {
        System.out.println("Checking " + this.getServiceName());        try {
            Thread.sleep(7000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.getServiceName() + " is UP");
    }
}

FileHealthChecker:檔案服務健康檢查(UserHealthChecker類似)

package concurrent.health;import java.util.concurrent.CountDownLatch;public class FileHealthChecker extends BaseHealthChecker {    public FileHealthChecker(CountDownLatch countDownLatch) {        super("file service", countDownLatch);
    }    @Override
    public void verifySerivce() {
        System.out.println("Checking " + this.getServiceName());        try {
            Thread.sleep(7000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(this.getServiceName() + " is UP");
    }
}

ApplicationStartupUtil:服務註冊中心呼叫發起方的主類,在系統啟動的時候發起健康檢測請求。

package concurrent.health;import java.util.ArrayList;import java.util.List;import java.util.concurrent.CountDownLatch;import java.util.concurrent.ExecutorService;import java.util.concurrent.Executors;public class ApplicationStartupUtil {    //list of service checker
    private static List<BaseHealthChecker> checkers;    //this latch will be used to wait on
    private static CountDownLatch countDownLatch;    //singleton
    private ApplicationStartupUtil() {

    }    private static ApplicationStartupUtil applicationStartupUtil = new ApplicationStartupUtil();    public static ApplicationStartupUtil getInstance() {        return applicationStartupUtil;
    }    public static boolean checkExternalServices() throws InterruptedException {        //init the latch with the number of service checks
        countDownLatch = new CountDownLatch(3);        //add all service checks into the list
        checkers = new ArrayList<>();
        checkers.add(new DataBaseHealthChecker(countDownLatch));
        checkers.add(new UserHealthChecker(countDownLatch));
        checkers.add(new FileHealthChecker(countDownLatch));        //start service checks using executor framework
        ExecutorService executor = Executors.newFixedThreadPool(checkers.size());        for (BaseHealthChecker checker : checkers) {
            executor.execute(checker);
        }        //now wait all services checked
        countDownLatch.await();        //service checkers are finished and now proceed startup
        for (BaseHealthChecker checker : checkers) {            if (!checker.isServiceUp()) {                return false;
            }
        }        return true;


    }
}

測試

測試方法

package concurrent.health;public class TestMain {    public static void main(String[] args) {        boolean result = false;        try {
            result = ApplicationStartupUtil.checkExternalServices();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        System.out.println("External services validation completed !! Result was :: " + result);
    }

}

結果

Checking database service
Checking file service
Checking user service
database service is UP
user service is UP
file service is UP
External services validation completed !! Result was :: true



作者:billJiang
連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/854/viewspace-2820477/,如需轉載,請註明出處,否則將追究法律責任。

相關文章