消息隊列異步請求,客戶端同步收到結果

如何設計一個接口,使用消息隊列異步請求,但是客戶端同步收到結果

  異步處理,同步返回?爲什麼會有這樣一個需求?既然接口要求同步返回,那麼直接阻塞就好了,要什麼異步消息同步返回?高併發保護系統的手段是緩存、限流、降級。限流有許多的手段,想令牌桶、漏桶算法按數量限流,也有使用消息隊列,排隊限流的。至於使用消息隊列的好處就不多說了,這裏主要將如何實現這個需求,有一個系統比較的不穩定,但是沒人維護,又不能替換它,只能在他的上層加一層來保護她,可以限流處理,也可以用mq讓它以他的最大處理能力處理。說白了這東西就是一個緩衝系統,可替代性高,存粹的技術型應用,由於新鮮所以我覺得可以一試;

選型

  首先我們來選型,分析需求:使用消息隊列異步請求,那麼選型消息隊列: zeromq、rabbitmq、activemq、kafka、rocketmq等等,消息隊列很多如果沒有什麼要求,那麼都可以選,但是首先我們需要考慮實現問題呢,使用的mq是否支持。我們需要可以排隊,那麼zeromq就不能選了,activemq有較小概率丟失消息,一般我不太愛用這個。好了我們實現這個需求不需要什麼複雜的功能,那麼剩下的都是可以選的,接下來就是考慮架設成本和易用性的問題。rabbitmq的時效性非常的好,但是吞吐量不及kafka和rocketmq,而且隔熱你用的較少;所以一般來說我習慣在kafka和rocketmq中選擇。rocketmq綜合性能比較好,而且有很多的功能(消息提交重新消費、延時消費等),做支付金融首選rocketmq,但是我們這裏不需要用到這些,所以這裏用了kafka。
  有了異步處理消息的mq,我們還需要一個保存mq處理完的返回值隊列,能讓阻塞的線程獲取到。因爲要分佈式的,所以這個隊列不能是java中的數據,所以這裏使用redis保存mq處理完的數據。

架構

  接下來我們先構造系統,首先我們有一個web服務,用來接收http的請求,接受請求後發送mq處理,然後阻塞當前處理的線程,等待mq處理完成,從redis的隊列中取出數據,現在還差一個mq的接收方,實現一個server服務,接受mq消息並處理,然後將數據放入redis,並且通知web的這個線程消息已經處理完畢,讓web這個阻塞的線程取出redis中處理完成的數據。至於通知需要廣播通知,因爲分佈式的話這個處理請求的線程會在任意一臺web服務中,至於這個通知我們可以用redis的發佈訂閱功能來實現;
  整體我們就有2個服務,一個web,一個server,之間通過mq通信,redis共享數據,redis發佈訂閱同步狀態喚醒線程。

實現

  首先是web端的實現,簡單的springboot項目加上web依賴,這裏不贅述,這裏我們模擬場景:我們需要去一個三方系統獲取用戶信息,通過後臺http調用獲取他的用戶信息,用戶要麼輸入手機,用戶名或郵箱和密碼(加密的);

 

@RestController
@RequestMapping("/async")
public class AsyncController {

    @Autowired
    private AsyncRequestExecutorService asyncService;

    /**
     * json處理工具 這裏用的gson 也可以fasejson或其他
     */
    private Gson gson = new Gson();

    @RequestMapping(value = "/userInfo", method = RequstMethod.POST)
    public String getUserInfo(UserInfoQueryDTO userInfoQuery) {
        //正常的話我們調用一個service就得到返回值就同步返回了,客戶端就能獲取到相關信息;但是這裏要異步處理,同步返回,我們準備一個異步處理的線程池來處理;先看AsyncRequestExecutorService;
        try{
            Future<String> result asyncService.doRequest(UUID.randomUUID().toString(), gson.toJson(userInfoQuery));
            // 這裏需要設置超時時間, 保證能給客戶端一個反應(這裏就實現了阻塞)
            return result.get(5, TimeUnit.SECONDS);
        } catch(Exception e) {
            //這裏要麼超時要麼失敗處理
        }

    }


}

@Data
public class UserInfoQueryDTO {

    private String mobile;

    private Strig userName;

    private String email;

    private String password;
}

@Service
public class AsyncRequestExecutorService {

    /**
     * 線程名稱方便定位問題
     */
    private String final threadName = "ASYNC-THREAD-";

    /**
     * 這就是kafkaspringboot的簡單集成使用kafkaTemplate的發送消息這裏不詳述
     */
    @Autowired
    private KafkaMessagePublisher messagePublisher;

    /**
     * redis的集成,保存mq處理完成後的結果集,這裏用的是set數據結構,因爲這裏請求時一次性的 返回就移除pop()方法正好滿足, 而且請求不重複
     */
    @Autowired;
    private ResponseRedisCache responseCache;

    /**
     * 線程池
     */
    private ExecutorService executorService;

    public AsyncRequestExecutorService() {
        this.executorService = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors() * 8,
                200,
                3000,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(),
                new ThreadFactory() {
                    private AtomicInteger count = new AtomicInteger(0);
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, THREAD_NAME + count.incrementAndGet());
                    }
        });
    }

    /**
     * 實現異步處理的關鍵
     * @param requestId 當前請求的id 可以用uuid
     * @param message 當前參數(json格式)
     * @return 返回一個Future用來阻塞
     *
     */
    public Future<String> doRequest(String requestId, String message) {
        // 我們收到消息後發送mq處理(這裏一定要將requestId一起處理,方便server處理完放入redis後的存取)
        messagePublisher.send(requestId, message);
        // 返回一個線程處理
        return executorService.submit(() -> {
            //一個靜態map保存當前線程(注意使用ConcurrentHashMap)
            GlobalThreadMap.parkThreadMap.put(requestId, Thread.currentThread());
            // 立即阻塞,應爲它不會馬上處理完成的(最多阻塞5s)
            LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(5));
            // 之後是線程被喚醒的處理
            // 首先從靜態map中刪除當前請求的線程
            GlobalThreadMap.parkThreadMap.remove(requestId);
            // 返回從redis中取出的結果,是null也直接返回(因爲5s的阻塞時間,過了5s還沒處理完就需要響應客戶端了,但是這時redis還沒有數據);
            return responseCache.pop(requestId);
        })    
    }

}

以上是web端的處理,是關鍵部分,接下來是server端的處理,比較簡單,就簡單敘述下:
在web中向kafka中推送了一條獲取用戶信息的消息,接下來就只要處理一下步驟:
srver端消費消息
反序列化
http調用第三方,同步獲取返回結果(這裏注意配置http調用的超時時間和異常處理)
將http的返回結果用消息中的requestId作爲key寫入redis
最後通過發佈訂閱返回requestId處理完成的消息

到這裏這條請求的處理又回到了web端:
web端收到了redis的發佈訂閱消息,從GlobalThreadMap中用發佈訂閱的requestId(也就是一開始的UUID生成的id)取出被park的線程執行unpark喚醒,之後result.get(5, TimeUnit.SECONDS)就能獲取從redis中取出的數據完成一次請求處理;

總結

  1. 使用kafka異步處理請求,注意消費端消費請求的時候用多線程處理。
  2. 使用Future阻塞處理的線程達到同步返回的目的。
  3. 使用redis保存處理結果按請求id取出;發佈訂閱通知阻塞的線程處理完成(否則就要輪詢隊列來判斷是否完成,浪費cpu)。
  4. 其中使用到的kafka和redis都是springboot的整合,這個又大量教程。
  5. 其中的線程數等優化還需結合實際,也許不會比直接限流強多少。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章