(五十一) springcloud+springcloud+vue+uniapp分佈式微服務電商 商城之跟我學習Apollo服務端設計原理(源碼解析)

本節主要對 Apollo 服務端設計原理進行解析。

1. 配置發佈後的實時推送設計

配置中心最重要的一個特性就是實時推送,正因爲有這個特性,我們纔可以依賴配置中心做很多事情。如圖 1 所示。

圖 1 簡要描述了配置發佈的大致過程。

  • 用戶在 Portal 中進行配置的編輯和發佈。
  • Portal 會調用 Admin Service 提供的接口進行發佈操作。
  • Admin Service 收到請求後,發送 ReleaseMessage 給各個 Config Service,通知 Config Service 配置發生變化。
  • Config Service 收到 ReleaseMessage 後,通知對應的客戶端,基於 Http 長連接實現。

2. 發送 ReleaseMessage 的實現方式

ReleaseMessage 消息是通過 Mysql 實現了一個簡單的消息隊列。之所以沒有采用消息中間件,是爲了讓 Apollo 在部署的時候儘量簡單,儘可能減少外部依賴,如圖 2 所示。

圖 2 簡要描述了發送 ReleaseMessage 的大致過程:

  • Admin Service 在配置發佈後會往 ReleaseMessage 表插入一條消息記錄。
  • Config Service 會啓動一個線程定時掃描 ReleaseMessage 表,來查看是否有新的消息記錄。
  • Config Service 發現有新的消息記錄,就會通知到所有的消息監聽器。
  • 消息監聽器得到配置發佈的信息後,就會通知對應的客戶端。

3. Config Service 通知客戶端的實現方式

通知採用基於 Http 長連接實現,主要分爲下面幾個步驟:

  • 客戶端會發起一個 Http 請求到 Config Service 的 notifications/v2 接口。
  • notifications/v2 接口通過 Spring DeferredResult 把請求掛起,不會立即返回。
  • 如果在 60s 內沒有該客戶端關心的配置發佈,那麼會返回 Http 狀態碼 304 給客戶端。
  • 如果發現配置有修改,則會調用 DeferredResult 的 setResult 方法,傳入有配置變化的 namespace 信息,同時該請求會立即返回。
  • 客戶端從返回的結果中獲取到配置變化的 namespace 後,會立即請求 Config Service 獲取該 namespace 的最新配置。

4. 源碼解析實時推送設計

Apollo 推送涉及的代碼比較多,本教程就不做詳細分析了,筆者把推送這裏的代碼稍微簡化了下,給大家進行講解,這樣理解起來會更容易。

當然,這些代碼比較簡單,很多細節就不做考慮了,只是爲了能夠讓大家明白 Apollo 推送的核心原理。

發送 ReleaseMessage 的邏輯我們就寫一個簡單的接口,用隊列存儲,測試的時候就調用這個接口模擬配置有更新,發送 ReleaseMessage 消息。具體代碼如下所示。

@RestController
public class NotificationControllerV2 implements ReleaseMessageListener {

    // 模擬配置更新, 向其中插入數據表示有更新
    public static Queue<String> queue = new LinkedBlockingDeque<>();

    @GetMapping("/addMsg")
    public String addMsg() {
        queue.add("xxx");
        return "success";
    }
}

消息發送之後,根據前面講過的 Config Service 會啓動一個線程定時掃描 ReleaseMessage 表,查看是否有新的消息記錄,然後取通知客戶端,在這裏我們也會啓動一個線程去掃描,具體代碼如下所示。

@Component
public class ReleaseMessageScanner implements InitializingBean {

    @Autowired
    private NotificationControllerV2 configController;

    @Override
    public void afterPropertiesSet() throws Exception {
        // 定時任務從數據庫掃描有沒有新的配置發佈
        new Thread(() -> {
            for (;;) {
                String result = NotificationControllerV2.queue.poll();
                if (result != null) {
                    ReleaseMessage message = new ReleaseMessage();
                    message.setMessage(result);
                    configController.handleMessage(message);
                }
            }
        }).start();
        ;
    }
}

循環讀取 NotificationControllerV2 中的隊列,如果有消息的話就構造一個 Release-Message 的對象,然後調用 NotificationControllerV2 中的 handleMessage() 方法進行消息的處理。

ReleaseMessage 就一個字段,模擬消息內容,具體代碼如下所示。

public class ReleaseMessage {
    private String message;
    public void setMessage(String message) {
        this.message = message;
    }
    public String getMessage() {
        return message;
    }
}

接下來,我們來看 handleMessage 做了哪些工作。

NotificationControllerV2 實現了 ReleaseMessageListener 接口,ReleaseMessageListener 中定義了 handleMessage() 方法,具體代碼如下所示。

public interface ReleaseMessageListener {
    void handleMessage(ReleaseMessage message);
}

handleMessage 就是當配置發生變化的時候,發送通知的消息監聽器。消息監聽器在得到配置發佈的信息後,會通知對應的客戶端,具體代碼如下所示。

@RestController
public class NotificationControllerV2 implements ReleaseMessageListener {
    private final Multimap<String, DeferredResultWrapper> deferredResults = Multimaps
            .synchronizedSetMultimap(HashMultimap.create());
    @Override
    public void handleMessage(ReleaseMessage message) {
        System.err.println("handleMessage:" + message);
        List<DeferredResultWrapper> results = Lists.newArrayList(deferredResults.get("xxxx"));
        for (DeferredResultWrapper deferredResultWrapper : results) {
            List<ApolloConfigNotification> list = new ArrayList<>();
            list.add(new ApolloConfigNotification("application", 1));
            deferredResultWrapper.setResult(list);
        }
    }
}

Apollo 的實時推送是基於 Spring DeferredResult 實現的,在 handleMessage() 方法中可以看到是通過 deferredResults 獲取 DeferredResult,deferredResults 就是第一行的 Multimap,Key 其實就是消息內容,Value 就是 DeferredResult 的業務包裝類 DeferredResultWrapper,我們來看下 DeferredResultWrapper 的代碼,代碼如下所示。

public class DeferredResultWrapper {
    private static final long TIMEOUT = 60 * 1000;// 60 seconds
    private static final ResponseEntity<List<ApolloConfigNotification>> NOT_MODIFIED_RESPONSE_LIST = new ResponseEntity<>(
            HttpStatus.NOT_MODIFIED);
    private DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> result;
    public DeferredResultWrapper() {
        result = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE_LIST);
    }
    public void onTimeout(Runnable timeoutCallback) {
        result.onTimeout(timeoutCallback);
    }
    public void onCompletion(Runnable completionCallback) {
        result.onCompletion(completionCallback);
    }
    public void setResult(ApolloConfigNotification notification) {
        setResult(Lists.newArrayList(notification));
    }
    public void setResult(List<ApolloConfigNotification> notifications) {
        result.setResult(new ResponseEntity<>(notifications, HttpStatus.OK));
    }
    public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> getResult() {
        return result;
    }
}

通過 setResult() 方法設置返回結果給客戶端,以上就是當配置發生變化,然後通過消息監聽器通知客戶端的原理,那麼客戶端是在什麼時候接入的呢?具體代碼如下。

@RestController
public class NotificationControllerV2 implements ReleaseMessageListener {
    // 模擬配置更新, 向其中插入數據表示有更新
    public static Queue<String> queue = new LinkedBlockingDeque<>();
    private final Multimap<String, DeferredResultWrapper> deferredResults = Multimaps
            .synchronizedSetMultimap(HashMultimap.create());
    @GetMapping("/getConfig")
    public DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> getConfig() {
        DeferredResultWrapper deferredResultWrapper = new DeferredResultWrapper();
        List<ApolloConfigNotification> newNotifications = getApolloConfigNotifications();
        if (!CollectionUtils.isEmpty(newNotifications)) {
            deferredResultWrapper.setResult(newNotifications);
        } else {
            deferredResultWrapper.onTimeout(() -> {
                System.err.println("onTimeout");
            });
            deferredResultWrapper.onCompletion(() -> {
                System.err.println("onCompletion");
            });
            deferredResults.put("xxxx", deferredResultWrapper);
        }
        return deferredResultWrapper.getResult();
    }
    private List<ApolloConfigNotification> getApolloConfigNotifications() {
        List<ApolloConfigNotification> list = new ArrayList<>();
        String result = queue.poll();
        if (result != null) {
            list.add(new ApolloConfigNotification("application", 1));
        }
        return list;
    }
}

NotificationControllerV2 中提供了一個 /getConfig 的接口,客戶端在啓動的時候會調用這個接口,這個時候會執行 getApolloConfigNotifications() 方法去獲取有沒有配置的變更信息,如果有的話證明配置修改過,直接就通過 deferredResultWrapper.setResult(newNotifications) 返回結果給客戶端,客戶端收到結果後重新拉取配置的信息覆蓋本地的配置。

如果 getApolloConfigNotifications() 方法沒有返回配置修改的信息,則證明配置沒有發生修改,那就將 DeferredResultWrapper 對象添加到 deferredResults 中,等待後續配置發生變化時消息監聽器進行通知。

同時這個請求就會掛起,不會立即返回,掛起是通過 DeferredResultWrapper 中的下面這部分代碼實現的,具體代碼如下所示。

private static final long TIMEOUT = 60 * 1000; // 60 seconds
private static final ResponseEntity<List<ApolloConfigNotification>> NOT_MODIFIED_RESPONSE_LIST = new ResponseEntity<>(
        HttpStatus.NOT_MODIFIED);
private DeferredResult<ResponseEntity<List<ApolloConfigNotification>>> result;
public DeferredResultWrapper() {
  result = new DeferredResult<>(TIMEOUT, NOT_MODIFIED_RESPONSE_LIST);
}

在創建 DeferredResult 對象的時候指定了超時的時間和超時後返回的響應碼,如果 60s 內沒有消息監聽器進行通知,那麼這個請求就會超時,超時後客戶端收到的響應碼就是 304。

整個 Config Service 的流程就走完了,接下來我們來看一下客戶端是怎麼實現的,我們簡單地寫一個測試類模擬客戶端註冊,具體代碼如下所示。

public class ClientTest {
    public static void main(String[] args) {
        reg();
    }
    private static void reg() {
        System.err.println("註冊");
        String result = request("http://localhost:8081/getConfig");
        if (result != null) {
            // 配置有更新, 重新拉取配置
            // ......
        }
        // 重新註冊
        reg();
    }
    private static String request(String url) {
        HttpURLConnection connection = null;
        BufferedReader reader = null;
        try {
            URL getUrl = new URL(url);
            connection = (HttpURLConnection) getUrl.openConnection();
            connection.setReadTimeout(90000);
            connection.setConnectTimeout(3000);
            connection.setRequestMethod("GET");
            connection.setRequestProperty("Accept-Charset", "utf-8");
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("Charset", "UTF-8");
            System.out.println(connection.getResponseCode());
            if (200 == connection.getResponseCode()) {
                reader = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
                StringBuilder result = new StringBuilder();
                String line = null;
                while ((line = reader.readLine()) != null) {
                    result.append(line);
                }
                System.out.println("結果 " + result);
                return result.toString();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
        return null;
    }
}

首先啓動 /getConfig 接口所在的服務,然後啓動客戶端,然後客戶端就會發起註冊請求,如果有修改直接獲取到結果,則進行配置的更新操作。如果無修改,請求會掛起,這裏客戶端設置的讀取超時時間是 90s,大於服務端的 60s 超時時間。

每次收到結果後,無論是有修改還是無修改,都必須重新進行註冊,通過這樣的方式就可以達到配置實時推送的效果。

我們可以調用之前寫的 /addMsg 接口來模擬配置發生變化,調用之後客戶端就能馬上得到返回結果。

推薦電子商務源碼

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