【问题标题】:What is the right way to implement sync and async methods in a library?在库中实现同步和异步方法的正确方法是什么?
【发布时间】:2015-05-31 10:06:06
【问题描述】:

我需要创建一个库,在其中我将具有同步和异步功能。

  • executeSynchronous() - 等到我有结果,返回结果。
  • executeAsynchronous() - 立即返回一个 Future,如果需要,可以在其他事情完成后处理它。

我的图书馆的核心逻辑

客户将使用我们的库,他们将通过传递DataKey builder 对象来调用它。然后,我们将使用该DataKey 对象构造一个 URL,并通过执行它对该 URL 进行 HTTP 客户端调用,在我们以 JSON 字符串的形式返回响应后,我们将把该 JSON 字符串原样发送回我们的客户通过创建DataResponse 对象。有些客户会调用executeSynchronous(),有些可能会调用executeAsynchronous() 方法,所以我需要在我的库中分别提供两种方法。

界面:

public interface Client {

    // for synchronous
    public DataResponse executeSynchronous(DataKey key);

    // for asynchronous
    public Future<DataResponse> executeAsynchronous(DataKey key);
}

然后我有我的DataClient,它实现了上面的Client 接口:

public class DataClient implements Client {

    private RestTemplate restTemplate = new RestTemplate();
    private ExecutorService executor = Executors.newFixedThreadPool(10);

    // for synchronous call
    @Override
    public DataResponse executeSynchronous(DataKey key) {
        DataResponse dataResponse = null;
        Future<DataResponse> future = null;

        try {
            future = executeAsynchronous(key);
            dataResponse = future.get(key.getTimeout(), TimeUnit.MILLISECONDS);
        } catch (TimeoutException ex) {
            PotoLogging.logErrors(ex, DataErrorEnum.TIMEOUT_ON_CLIENT, key);
            dataResponse = new DataResponse(null, DataErrorEnum.TIMEOUT_ON_CLIENT, DataStatusEnum.ERROR);
        } catch (Exception ex) {
            PotoLogging.logErrors(ex, DataErrorEnum.CLIENT_ERROR, key);
            dataResponse = new DataResponse(null, DataErrorEnum.CLIENT_ERROR, DataStatusEnum.ERROR);
        }

        return dataResponse;
    }

    //for asynchronous call
    @Override
    public Future<DataResponse> executeAsynchronous(DataKey key) {
        Future<DataResponse> future = null;

        try {
            Task task = new Task(key, restTemplate);
            future = executor.submit(task); 
        } catch (Exception ex) {
            PotoLogging.logErrors(ex, DataErrorEnum.CLIENT_ERROR, key);
        }

        return future;
    }
}

执行实际任务的简单类:

public class Task implements Callable<DataResponse> {

    private DataKey key;
    private RestTemplate restTemplate;

    public Task(DataKey key, RestTemplate restTemplate) {
        this.key = key;
        this.restTemplate = restTemplate;
    }

    @Override
    public DataResponse call() {
        DataResponse dataResponse = null;
        String response = null;

        try {
            String url = createURL();
            response = restTemplate.getForObject(url, String.class);

            // it is a successful response
            dataResponse = new DataResponse(response, DataErrorEnum.NONE, DataStatusEnum.SUCCESS);
        } catch (RestClientException ex) {
            PotoLogging.logErrors(ex, DataErrorEnum.SERVER_DOWN, key);
            dataResponse = new DataResponse(null, DataErrorEnum.SERVER_DOWN, DataStatusEnum.ERROR);
        } catch (Exception ex) {
            PotoLogging.logErrors(ex, DataErrorEnum.CLIENT_ERROR, key);
            dataResponse = new DataResponse(null, DataErrorEnum.CLIENT_ERROR, DataStatusEnum.ERROR);
        }

        return dataResponse;
    }

    // create a URL by using key object
    private String createURL() {
        String url = somecode;
        return url;
    }
}

我们公司的客户将通过在他们的代码库中使用我的工厂来使用我的库,如下所示 -

// if they are calling `executeSynchronous()` method
DataResponse response = DataClientFactory.getInstance().executeSynchronous(dataKey);

// and if they want to call `executeAsynchronous()` method
Future<DataResponse> response = DataClientFactory.getInstance().executeAsynchronous(dataKey);

为我的库实现同步和异步方法的最佳方式是什么?实施sync call as async + waiting 是个坏主意吗?因为我当前的设置每次调用都会消耗线程池中的一个线程?如果是,那么任何人都可以解释为什么这是一个坏主意,并且会有任何性能问题吗?

根据上述条件,您将如何实现同步和异步方法?做这个的最好方式是什么?这个库将在非常重的负载下使用,它必须很快,这意味着无论我的服务器响应什么都需要时间。

我应该在我的代码库中使用AsyncRestTemplate,这将是异步非阻塞架构吗?

【问题讨论】:

    标签: java multithreading performance executorservice resttemplate


    【解决方案1】:

    对于synchronous 调用,在单独的线程中执行绝对不是一个好主意。 在这种情况下,您会产生线程的额外成本和资源以及线程上下文切换的成本。

    如果有很多synchronous 调用,那么您将不必要地阻塞asynchronous 调用的线程,因为您的执行程序是固定大小的线程。在这种情况下,系统的总吞吐量会更少。

    例如: 如果有 10 个客户端分别调用 synchronousasynchronous 调用,则在您的实现中只有线程实际工作。但是,如果您也使用客户端线程,而不是将 synchronous 调用为 asynchronous 并等待,那么将同时处理所有 20 个调用。

    【讨论】:

      【解决方案2】:

      我认为这样更好:

      @Override
      public DataResponse executeSynchronous(DataKey key) {
          Task task = new Task(key, restTemplate);
          return task.call();
      }
      

      它执行相同的工作,清晰,更短,并且没有开销。

      请注意,他还清理了您当前拥有的重复异常处理。

      如果超时是必须,则可以选择使用RestTemplate 类的底层超时,如Spring RestTemplate timeout 中所述

      然后超时将导致您或库客户端可以处理的RestClientException

      【讨论】:

      • 我确实想过。那么我将如何实现超时的东西?我仍然需要这样做,以便在服务器响应时间过长时可以超时。
      • 我在给出答案后注意到超时。我还在没有阅读您的评论的情况下对答案进行了编辑。我不认为(根据您显示的代码)超时应该在 DataKey 上,如果超时是 must 那么这实际上是一个很好的实现.要实现超时,您需要第二个线程来计算时间并终止原来的慢线程。
      • 但是看看:stackoverflow.com/questions/13837012/…。最好正确使用底层库的设置并尽可能简单地编写代码,就像我原来的答案一样(我将对其进行编辑以包含这一点)
      【解决方案3】:

      我不会打扰那个 Task 课程。只需让您的同步方法完成所有工作并从异步方法异步调用它。

      public class DataClient implements Client {
      
          private RestTemplate restTemplate = new RestTemplate();
          private ExecutorService executor = Executors.newFixedThreadPool(10);
      
          // for synchronous call
          @Override
          public DataResponse executeSynchronous(DataKey key) {
              DataResponse dataResponse = null;
              String response = null;
      
              try {
                  String url = createURL();
                  response = restTemplate.getForObject(url, String.class);
      
                  // it is a successful response
                  dataResponse = new DataResponse(response, DataErrorEnum.NONE, DataStatusEnum.SUCCESS);
              } catch (RestClientException ex) {
                  PotoLogging.logErrors(ex, DataErrorEnum.SERVER_DOWN, key);
                  dataResponse = new DataResponse(null, DataErrorEnum.SERVER_DOWN, DataStatusEnum.ERROR);
              } catch (Exception ex) {
                  PotoLogging.logErrors(ex, DataErrorEnum.CLIENT_ERROR, key);
                  dataResponse = new DataResponse(null, DataErrorEnum.CLIENT_ERROR, DataStatusEnum.ERROR);
              }
      
              return dataResponse;
          }
      
          //for asynchronous call
          @Override
          public Future<DataResponse> executeAsynchronous(final DataKey key) {
              return executor.submit(new Callable<DataResponse>() {
                  @Override
                  public DataResponse call() throws Exception {
                      return executeSynchronous(key);
                  }
              });
          }
      }
      

      【讨论】:

      • 我只是这样写了上面的代码,因为我觉得它看起来更漂亮更简单。但是,如果您想要限制对其余服务的同时请求数量,那么您的版本绝对没问题。从您提供的信息中我无法判断哪个会更好。这取决于 REST 服务的工作方式以及库的客户端所做的事情。最好的办法是尝试一个然后另一个并衡量哪个表现更好。我对 AsyncRestTemplate 一无所知,抱歉。
      【解决方案4】:

      上面通过异步执行同步任务的代码与将所有内容都设置为异步相同。如果这是要求,那么我建议您使用 google guava 的 ListenableFuture。我不是拥护者,但它具有管理任务超时的方法、编写良好的回调来处理 onSuccess、onFailure 场景。 https://code.google.com/p/guava-libraries/wiki/ListenableFutureExplained

      【讨论】:

        【解决方案5】:

        如果您在同步操作的情况下(实际上不需要)创建新线程,则会导致性能下降。您基本上是在创建新线程(读作浪费资源),甚至没有从中获得任何好处。 话虽如此,我认为更好的方法是将 HTTP 部分包装在不同的类中。这样,您将在同步和异步情况下重复使用 HTTP 访问代码。

        class HTTPAccess{
            private RestTemplate restTemplate;
            private DataKey key;
        
            public HTTPAccess(DataKey key,RestTemplate restTemplate){
                this.key = key;
                this.restTemplate = restTemplate;
        
            }
        
        
            public DataResponse performRequest() {
                DataResponse dataResponse = null;        
                try {
                    String url = createURL();
                    response = restTemplate.getForObject(url, String.class);
        
                    // it is a successful response
                    dataResponse = new DataResponse(response, DataErrorEnum.NONE, DataStatusEnum.SUCCESS);
                } catch (RestClientException ex) {
                    PotoLogging.logErrors(ex, DataErrorEnum.SERVER_DOWN, key);
                    dataResponse = new DataResponse(null, DataErrorEnum.SERVER_DOWN, DataStatusEnum.ERROR);
                } catch (Exception ex) {
                    PotoLogging.logErrors(ex, DataErrorEnum.CLIENT_ERROR, key);
                    dataResponse = new DataResponse(null, DataErrorEnum.CLIENT_ERROR, DataStatusEnum.ERROR);
                }
        
                return dataResponse;
            }
        
            // create a URL by using key object
            private String createURL() {
                String url = somecode;
                return url;
            }
        
        }
        

        现在对于客户端实现,只需使用这个类。

        public class DataClient implements Client {
        
            private ExecutorService executor = Executors.newFixedThreadPool(10);
            private RestTemplate restTemplate;
            private void initRestClient(DataKey key){
            if(restTemplate == null)
                restTemplate = new RestTemplate(clientHttpRequestFactory(key));
            }
        
            private ClientHttpRequestFactory clientHttpRequestFactory(DataKey key) {
                HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
                factory.setReadTimeout(key.getTimeout());
                factory.setConnectTimeout(key.getTimeout());
                //if you need to set otherparams this is the place we can do it extracting from DataKey obj
                return factory;
            }
        
            // for synchronous call
            @Override
            public DataResponse executeSynchronous(DataKey key) {
                initRestClient(key);
                DataResponse dataResponse = new HTTPAccess(key).performRequest();
                return dataResponse;
            }
        
            //for asynchronous call
            @Override
            public Future<DataResponse> executeAsynchronous(final DataKey key) {
                return executor.submit(new Callable<DataResponse>() {
                    @Override
                    public DataResponse call() throws Exception {
                        return executeSynchronous(key);
                    }
                });
            }
        }
        

        这样,您的 HTTP 实现是完全独立的,将来如果您需要更改接收 DataResponse 的方式(可能来自 DB 调用),那么您只需更改 HTTPAccess 类和其他部分不会受到影响。

        【讨论】:

        • 请注意超时将被捕获为 HTTPAccess 类中的 RestClientException。因此,也许您希望使用更具体的内容来更改我们捕获 RestClientException 的代码以满足您的需求。
        • 感谢您的建议。感谢您的帮助。我几乎没有疑问。使用这种方法,我们不是每次都为每个请求创建RestTemplate 吗?而且我猜,我们正在为每个请求创建工厂,每个工厂都有连接和线程池,我猜是相当重的对象。有没有办法重复使用它们?我想是因为这个原因,我使用 DI 从我的 DataClien 类中传递了 RestTemplate。
        • 是的,您的建议很棒。我们可以重用RestTemplate 对象。已编辑代码以反映更改。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-03-29
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多