Способ Spring Boot для эффективной агрегации данных

Spring Boot

Адрес проекта и пример кода:GitHub.com/Green Buds8/Билеты…

задний план

Разработка интерфейса — наиболее распространенный сценарий в бэкенд-разработке, это может быть интерфейс RESTFul или интерфейс RPC.Разработка интерфейса часто заключается в извлечении данных из разных мест, а затем в сборе их в результаты, особенно в тех интерфейсах, которые в большей степени ориентированы на бизнес.

какУдобная и быстрая разработка высокопроизводительных интерфейсов, это вопрос, который необходимо рассмотреть.

Например, теперь мне нужно реализовать интерфейс, который вытягивает用户基础信息+用户的博客列表+用户的粉丝数据, предполагается, что доступны следующие три интерфейса, которые используются для получения用户基础信息 ,用户博客列表, 用户的粉丝数据.

Основная информация о пользователе

@Service
public class UserServiceImpl implements UserService {
    @Override
    public User get(Long id) {
        try {Thread.sleep(1000L);} catch (InterruptedException e) {}
        /* mock a user*/
        User user = new User();
        user.setId(id);
        user.setEmail("lvyahui8@gmail.com");
        user.setUsername("lvyahui8");
        return user;
    }
}

Список блогов пользователей

@Service
public class PostServiceImpl implements PostService {
    @Override
    public List<Post> getPosts(Long userId) {
        try { Thread.sleep(1000L); } catch (InterruptedException e) {}
        Post post = new Post();
        post.setTitle("spring data aggregate example");
        post.setContent("No active profile set, falling back to default profiles");
        return Collections.singletonList(post);
    }
}

Данные фанатов пользователя

@Service
public class FollowServiceImpl implements FollowService {
    @Override
    public List<User> getFollowers(Long userId) {
        try { Thread.sleep(1000L); } catch (InterruptedException e) {}
        int size = 10;
        List<User> users = new ArrayList<>(size);
        for(int i = 0 ; i < size; i++) {
            User user = new User();
            user.setUsername("name"+i);
            user.setEmail("email"+i+"@fox.com");
            user.setId((long) i);
            users.add(user);
        };
        return users;
    }
}

Обратите внимание, что каждый метод спит в течение 1 с, чтобы имитировать рабочее время.

Нам нужно инкапсулировать еще один интерфейс, чтобы собрать данные трех вышеуказанных интерфейсов.

PS: Такие сценарии на самом деле очень распространены в работе, и часто данные, которые нам нужно собрать, передаются третьей стороне через сетевой запрос.Кроме того, некоторые люди могут подумать, а почему бы не разделить их на 3 запроса? клиентская сеть Из соображений производительности часто необходимо передавать как можно больше данных в одном сетевом запросе.Конечно, предпосылка заключается в том, что данные не могут быть слишком большими, иначе длительная передача повлияет на рендеринг. многие приложения кажутся сложными, но на самом деле имеют только один интерфейс. Вытягивайте все данные за один раз, и разработка клиента также проста.

Серийная реализация

Написание интерфейса с хорошей производительностью — это не только техническое занятие каждого back-end программиста, но и основное требование бизнеса.В общем, чтобы обеспечить лучшую производительность, часто приходится писать более сложную реализацию кода.

Но все ленивы, поэтому мы часто пишем код для последовательных вызовов, подобный следующему.

@Component
public class UserQueryFacade {
    @Autowired
    private FollowService followService;
    @Autowired
    private PostService postService;
    @Autowired
    private UserService userService;
    
    public User getUserData(Long userId) {
        User user = userService.get(userId);
        user.setPosts(postService.getPosts(userId));
        user.setFollowers(followService.getFollowers(userId));
        return user;
    }
}

Очевидно, приведенный выше код неэффективен, и для получения результата требуется не менее 3 с., и как только используются данные определенного интерфейса, необходимо внедрить соответствующую службу, что затруднительно для повторного использования.

Параллельная реализация

Начинающие программисты могут сразу подумать, что этоМежду несколькими элементами данных нет сильной зависимости, и их можно получать параллельноНу, это реализовано асинхронным потоком + CountDownLatch + Future, как показано ниже.

@Component
public class UserQueryFacade {
    @Autowired
    private FollowService followService;
    @Autowired
    private PostService postService;
    @Autowired
    private UserService userService;
    
    public User getUserDataByParallel(Long userId) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        CountDownLatch countDownLatch = new CountDownLatch(3);
        Future<User> userFuture = executorService.submit(() -> {
            try{
                return userService.get(userId);
            }finally {
                countDownLatch.countDown();
            }
        });
        Future<List<Post>> postsFuture = executorService.submit(() -> {
            try{
                return postService.getPosts(userId);
            }finally {
                countDownLatch.countDown();
            }
        });
        Future<List<User>> followersFuture = executorService.submit(() -> {
            try{
                return followService.getFollowers(userId);
            }finally {
                countDownLatch.countDown();
            }
        });
        countDownLatch.await();
        User user = userFuture.get();
        user.setFollowers(followersFuture.get());
        user.setPosts(postsFuture.get());
        return user;
    }
}

код выше,Замена последовательных вызовов на параллельные вызовы может значительно повысить производительность при ограниченных уровнях параллелизма.Но очевидно, что это слишком сложно, если каждый интерфейс пишет такой кусок кода для параллельного выполнения, это кошмар.

Элегантная реализация аннотации

Те, кто знаком с java, знают, что в java есть очень удобная фича ~~ аннотации.Это просто черная магия.Часто нужно всего лишь добавить какие-то аннотации к классам или методам для достижения очень сложных функций.

С аннотациями в сочетании с идеей автоматического внедрения зависимостей Spring, можем ли мы автоматически внедрять зависимости и автоматически вызывать интерфейсы параллельно через аннотации?Ответ — да.

Прежде всего, определим интерфейс агрегата (конечно, мы не можем определить агрегатный класс, весь код также можно написать в исходном классе Service)

@Component
public class UserAggregate {
    @DataProvider("userFullData")
    public User userFullData(@DataConsumer("user") User user,
                             @DataConsumer("posts") List<Post> posts,
                             @DataConsumer("followers") List<User> followers) {
        user.setFollowers(followers);
        user.setPosts(posts);
        return user;
    }
}

в

  • @DataProviderУказывает, что этот метод является поставщиком данных, а идентификатор данныхuserFullData

  • @DataConsumerУказывает параметры этого метода, который должен потреблять данные, и идентификатор данных соответственно.user ,posts, followers.

Конечно, оригинальные 3 атомных сервиса用户基础信息 ,用户博客列表, 用户的粉丝数据, также необходимо добавить некоторые аннотации

@Service
public class UserServiceImpl implements UserService {
    @DataProvider("user")
    @Override
    public User get(@InvokeParameter("userId") Long id) {
@Service
public class PostServiceImpl implements PostService {
    @DataProvider("posts")
    @Override
    public List<Post> getPosts(@InvokeParameter("userId") Long userId) {
@Service
public class FollowServiceImpl implements FollowService {
    @DataProvider("followers")
    @Override
    public List<User> getFollowers(@InvokeParameter("userId") Long userId) {

в

  • @DataProviderИмеет то же значение, что и раньше, указывая на то, что этот метод является поставщиком данных.
  • @InvokeParameterУказывает, что при выполнении метода необходимоПараметры переданы вручную

Обратите внимание здесь@InvokeParameterа также@DataConsumerРазница между первым требует, чтобы пользователь вручную передавал параметры при вызове верхнего уровня; во втором фреймворк автоматически анализирует зависимости и вводит результаты после асинхронных вызовов.

Наконец, просто вызовите интерфейс унифицированного фасада (Facade), передайте идентификатор данных, параметры вызова и тип возвращаемого значения.Остальная параллельная обработка, анализ зависимостей и внедрение полностью выполняются фреймворком автоматически.

@Component
public class UserQueryFacade {
    @Autowired
    private DataBeanAggregateQueryFacade dataBeanAggregateQueryFacade;

    public User getUserFinal(Long userId) throws InterruptedException, 
    			IllegalAccessException, InvocationTargetException {
        return dataBeanAggregateQueryFacade.get("userFullData",
                Collections.singletonMap("userId", userId), User.class);
    }
}

Как использовать в своем проекте

Вышеупомянутые функции автор инкапсулировал в виде весеннего загрузочного стартера и выпустил на центральный склад maven.

Просто импортируйте зависимость в свой проект.

<dependency>
  <groupId>io.github.lvyahui8</groupId>
  <artifactId>spring-boot-data-aggregator-starter</artifactId>
  <version>1.0.2</version>
</dependency>

И вapplication.propertiesПуть сканирования для объявления аннотаций в файле.

# 替换成你需要扫描注解的包
io.github.lvyahui8.spring.base-packages=io.github.lvyahui8.spring.example

После этого вы можете использовать следующие аннотации и Spring Beans для реализации агрегированных запросов.

  • @DataProvider
  • @DataConsumer
  • @InvokeParameter
  • Spring Bean DataBeanAggregateQueryFacade

Уведомление,@DataConsumerа также@InvokeParameterЕго можно смешивать и использовать на разных параметрах одного и того же метода, причем все параметры метода должны иметь одну из аннотаций и не могут иметь параметров без аннотаций.

Адрес проекта и приведенный выше пример кода:GitHub.com/Green Buds8/Билеты…, Спасибо, что дали звезду, добро пожаловать на совместное участие в улучшении

характеристика

  • Получить зависимости асинхронно

    все@DataConsumerОпределенные зависимости будут получены асинхронно.Когда все зависимости в параметрах метода провайдера будут получены, метод провайдера будет выполнен

  • Неограниченное вложение

    Зависимости поддерживают глубокую вложенность, пример выше имеет только один уровень

  • Обработка исключений

    В настоящее время поддерживает два метода обработки: игнорировать или завершать

    Игнорировать означает, что когда метод поставщика выполняется, он игнорирует выброшенное исключение и возвращает нулевое значение; Прекращение означает, что как только метод поставщика выдает исключение, оно будет вызываться уровень за уровнем для прекращения последующей обработки.

    Конфигурация поддерживает уровень потребителя или глобальный, приоритет: уровень потребителя > глобальный

  • кэш запросов

    Во время жизненного цикла запроса, который вызывает метод запроса Фасада,Результаты вызова метода можно использовать повторно. Пока сигнатура метода и параметры непротиворечивы, метод по умолчанию является идемпотентным, а кэшированный результат запроса будет использоваться напрямую.Но это не абсолютно, учитывая особенности многопоточности, кэш может иногда не использоваться.

  • Контроль времени ожидания(будет реализовано)

Более поздний план

В последующем автор продолжит улучшать #обработку тайм-аута, #кеш, #улучшать пропускную способность и дополнительно улучшать простоту использования, высокую доступность и масштабируемость плагинов.


Сканируйте код и подписывайтесь на мой официальный аккаунт