(51) design principle of Apollo server of springcloud+springboot+uniapp+vue b2b2c distributed micro service E-commerce mall (source code analysis)

Posted by Kold on Thu, 03 Mar 2022 15:05:19 +0100

This section mainly analyzes the design principle of Apollo server.

  1. Configure real-time push design after release
    The most important feature of the configuration center is real-time push. Because of this feature, we can rely on the configuration center to do many things. As shown in Figure 1.

    Figure 1 briefly describes the general process of configuration publishing.

Users can edit and publish configuration in Portal.
Portal will call the interface provided by Admin Service to publish.
After receiving the request, the Admin Service sends a ReleaseMessage to each Config Service to notify the Config Service that the configuration has changed.
After receiving the ReleaseMessage, the Config Service notifies the corresponding client, which is implemented based on the Http long connection.
2. Implementation method of sending ReleaseMessage
ReleaseMessage message is a simple message queue implemented through Mysql. The reason why message oriented middleware is not adopted is to make the deployment of Apollo as simple as possible and reduce external dependencies as much as possible, as shown in Figure 2.

Figure 2 briefly describes the general process of sending ReleaseMessage:

After configuration publishing, the Admin Service will insert a message record into the ReleaseMessage table.
Config Service will start a thread to regularly scan the ReleaseMessage table to see if there are new message records.
If the Config Service finds a new message record, it will notify all message listeners.
After the message listener obtains the information published by the configuration, it will notify the corresponding client.
3. Implementation of config service notification client
The notification is implemented based on Http long connection, which is mainly divided into the following steps:

The client will initiate an Http request to the notifications/v2 interface of Config Service.
The notifications/v2 interface suspends the request through Spring DeferredResult and will not return immediately.
If the configuration concerned by the client is not released within 60s, the Http status code 304 will be returned to the client.
If the configuration is modified, the setResult method of DeferredResult will be called to pass in the namespace information with configuration changes, and the request will be returned immediately.
After the client obtains the namespace with changed configuration from the returned results, it will immediately request Config Service to obtain the latest configuration of the namespace.
4. Source code analysis and real-time push design
Apollo push involves a lot of code, so this tutorial will not do a detailed analysis. The author simplifies the code here and explains it to you, so it will be easier to understand.

Of course, these codes are relatively simple, and many details will not be considered, just to let you understand the core principle of Apollo push.

For the logic of sending ReleaseMessage, we will write a simple interface, store it in queue, call this interface when testing, simulate the update of configuration and send ReleaseMessage message. The specific code is as follows.

@RestController
public class NotificationControllerV2 implements ReleaseMessageListener {

    // Simulate configuration update, and insert data into it to indicate that there is an update
    public static Queue<String> queue = new LinkedBlockingDeque<>();

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

After the message is sent, according to the above-mentioned Config Service will start a thread to regularly scan the ReleaseMessage table to check whether there are new message records, and then take the notification client. Here, we will also start a thread to scan. The specific code is as follows.

@Component
public class ReleaseMessageScanner implements InitializingBean {

    @Autowired
    private NotificationControllerV2 configController;

    @Override
    public void afterPropertiesSet() throws Exception {
        // The scheduled task scans the database for new configuration releases
        new Thread(() -> {
            for (;;) {
                String result = NotificationControllerV2.queue.poll();
                if (result != null) {
                    ReleaseMessage message = new ReleaseMessage();
                    message.setMessage(result);
                    configController.handleMessage(message);
                }
            }
        }).start();
        ;
    }
}

Loop read the queue in NotificationControllerV2, if there is a message, then construct a Release-Message object, then call the handleMessage() method in NotificationControllerV2 to process the message.

ReleaseMessage is a field that simulates the message content. The specific code is as follows.

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

Next, let's look at what handleMessage does.

NotificationControllerV2 implements the ReleaseMessageListener interface. The ReleaseMessageListener defines the handleMessage() method. The specific code is shown below.

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

handleMessage is a message listener that sends a notification when the configuration changes. After receiving the information published by the configuration, the message listener will notify the corresponding client. The specific code is as follows.

@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);
        }
    }
}

The real-time push of Apollo is implemented based on Spring DeferredResult. In the handleMessage() method, we can see that deferredResults is obtained through deferredResults. deferredResults is the Multimap in the first line, Key is actually the message content, and Value is the business packaging class DeferredResultWrapper of DeferredResult, Let's take a look at the code of DeferredResultWrapper, as shown below.

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;
    }
}

Set the return result to the client through the setResult() method. The above is the principle of notifying the client through the message listener when the configuration changes. When does the client access? The specific code is as follows.

@RestController
public class NotificationControllerV2 implements ReleaseMessageListener {
    // Simulate configuration update, and insert data into it to indicate that there is an update
    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 provides a / getConfig interface. The client will call this interface when starting. At this time, it will execute the getapollo confignotifications () method to obtain the information about whether the configuration has been changed. If so, it can be proved that the configuration has been modified directly through the deferredresultwrapper Setresult (New notifications) returns the result to the client. After receiving the result, the client pulls the configuration information again to overwrite the local configuration.

If the getapollo confignotifications () method does not return the information of configuration modification, it proves that the configuration has not been modified. Then add the DeferredResultWrapper object to deferredResults and wait for the message listener to notify when the subsequent configuration changes.

At the same time, the request will be suspended and will not be returned immediately. The suspension is realized through the following code in DeferredResultWrapper. The specific code is as follows.

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);
}

When creating the DeferredResult object, the timeout time and the response code returned after timeout are specified. If there is no message listener to notify within 60s, the request will timeout. After timeout, the response code received by the client is 304.

The whole process of Config Service is finished. Next, let's see how the client is implemented. We simply write a test class to simulate client registration. The specific code is as follows.

public class ClientTest {
    public static void main(String[] args) {
        reg();
    }
    private static void reg() {
        System.err.println("register");
        String result = request("http://localhost:8081/getConfig");
        if (result != null) {
            // If the configuration is updated, pull the configuration again
            // ......
        }
        // Re register
        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 " + result);
                return result.toString();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (connection != null) {
                connection.disconnect();
            }
        }
        return null;
    }
}

First start the service where the / getConfig interface is located, and then start the client, and then the client will initiate a registration request. If the result is directly obtained through modification, the configuration will be updated. If there is no modification, the request will be suspended. Here, the read timeout set by the client is 90s, which is greater than the 60s timeout set by the server.

After receiving the results every time, whether there is modification or no modification, you must register again. In this way, you can achieve the effect of configuring real-time push.

We can call the / addMsg interface written before to simulate the change of configuration. After calling, the client can get the return result immediately.

Recommended e-commerce source code