欢迎访问Spring Cloud中国社区

《重新定义Spring Cloud实战》由Spring Cloud中国社区倾力打造,基于Spring Cloud的Finchley.RELEASE版本,本书内容宽度足够广、深度足够深,而且立足于生产实践,直接从生产实践出发,包含大量生产实践的配置。欢迎加微信Software_King进群答疑,国内谁在使用Spring Cloud?欢迎登记

Spring Boot 高效数据聚合之道

lvyahui8 · 5月前 · 2384 ·

项目地址和示例代码: https://github.com/lvyahui8/spring-boot-data-aggregator

背景

接口开发是后端开发中最常见的场景, 可能是RESTFul接口, 也可能是RPC接口. 接口开发往往是从各处捞出数据, 然后组装成结果, 特别是那些偏业务的接口.

例如, 我现在需要实现一个接口, 拉取用户基础信息+用户的博客列表+用户的粉丝数据的整合数据, 假设已经有如下三个接口可以使用, 分别用来获取 用户基础信息 ,用户博客列表, 用户的粉丝数据.

用户基础信息

  1. @Service
  2. public class UserServiceImpl implements UserService {
  3. @Override
  4. public User get(Long id) {
  5. try {Thread.sleep(1000L);} catch (InterruptedException e) {}
  6. /* mock a user*/
  7. User user = new User();
  8. user.setId(id);
  9. user.setEmail("lvyahui8@gmail.com");
  10. user.setUsername("lvyahui8");
  11. return user;
  12. }
  13. }

用户博客列表

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

用户的粉丝数据

  1. @Service
  2. public class FollowServiceImpl implements FollowService {
  3. @Override
  4. public List<User> getFollowers(Long userId) {
  5. try { Thread.sleep(1000L); } catch (InterruptedException e) {}
  6. int size = 10;
  7. List<User> users = new ArrayList<>(size);
  8. for(int i = 0 ; i < size; i++) {
  9. User user = new User();
  10. user.setUsername("name"+i);
  11. user.setEmail("email"+i+"@fox.com");
  12. user.setId((long) i);
  13. users.add(user);
  14. };
  15. return users;
  16. }
  17. }

注意, 每一个方法都sleep了1s以模拟业务耗时.

我们需要再封装一个接口, 来拼装以上三个接口的数据.

PS: 这样的场景实际在工作中很常见, 而且往往我们需要拼凑的数据, 是要走网络请求调到第三方去的. 另外可能有人会想, 为何不分成3个请求? 实际为了客户端网络性能考虑, 往往会在一次网络请求中, 尽可能多的传输数据, 当然前提是这个数据不能太大, 否则传输的耗时会影响渲染. 许多APP的首页, 看着复杂, 实际也只有一个接口, 一次性拉下所有数据, 客户端开发也简单.

串行实现

编写性能优良的接口不仅是每一位后端程序员的技术追求, 也是业务的基本诉求. 一般情况下, 为了保证更好的性能, 往往需要编写更复杂的代码实现.

但凡人皆有惰性, 因此, 往往我们会像下面这样编写串行调用的代码

  1. @Component
  2. public class UserQueryFacade {
  3. @Autowired
  4. private FollowService followService;
  5. @Autowired
  6. private PostService postService;
  7. @Autowired
  8. private UserService userService;
  9. public User getUserData(Long userId) {
  10. User user = userService.get(userId);
  11. user.setPosts(postService.getPosts(userId));
  12. user.setFollowers(followService.getFollowers(userId));
  13. return user;
  14. }
  15. }

很明显, 上面的代码, 效率低下, 起码要3s才能拿到结果, 且一旦用到某个接口的数据, 便需要注入相应的service, 复用麻烦.

并行实现

有追求的程序员可能立马会考虑到, 这几项数据之间并无强依赖性, 完全可以并行获取嘛, 通过异步线程+CountDownLatch+Future实现, 就像下面这样.

  1. @Component
  2. public class UserQueryFacade {
  3. @Autowired
  4. private FollowService followService;
  5. @Autowired
  6. private PostService postService;
  7. @Autowired
  8. private UserService userService;
  9. public User getUserDataByParallel(Long userId) throws InterruptedException, ExecutionException {
  10. ExecutorService executorService = Executors.newFixedThreadPool(3);
  11. CountDownLatch countDownLatch = new CountDownLatch(3);
  12. Future<User> userFuture = executorService.submit(() -> {
  13. try{
  14. return userService.get(userId);
  15. }finally {
  16. countDownLatch.countDown();
  17. }
  18. });
  19. Future<List<Post>> postsFuture = executorService.submit(() -> {
  20. try{
  21. return postService.getPosts(userId);
  22. }finally {
  23. countDownLatch.countDown();
  24. }
  25. });
  26. Future<List<User>> followersFuture = executorService.submit(() -> {
  27. try{
  28. return followService.getFollowers(userId);
  29. }finally {
  30. countDownLatch.countDown();
  31. }
  32. });
  33. countDownLatch.await();
  34. User user = userFuture.get();
  35. user.setFollowers(followersFuture.get());
  36. user.setPosts(postsFuture.get());
  37. return user;
  38. }
  39. }

上面的代码, 将串行调用改为并行调用, 在有限并发级别下, 能极大提高性能. 但很明显, 它过于复杂, 如果每个接口都为了并行执行都写这样一段代码, 简直是噩梦.

优雅的注解实现

熟悉java的都知道, java有一种非常便利的特性 ~~ 注解. 简直是黑魔法. 往往只需要给类或者方法上添加一些注解, 便可以实现非常复杂的功能.

有了注解, 再结合Spring依赖自动注入的思想, 那么我们可不可以通过注解的方式, 自动注入依赖, 自动并行调用接口呢? 答案是肯定的.

首先, 我们先定义一个聚合接口

  1. @Component
  2. public class UserAggregate {
  3. @DataProvider(id="userFullData")
  4. public User userFullData(@DataConsumer(id = "user") User user,
  5. @DataConsumer(id = "posts") List<Post> posts,
  6. @DataConsumer(id = "followers") List<User> followers) {
  7. user.setFollowers(followers);
  8. user.setPosts(posts);
  9. return user;
  10. }
  11. }

其中

  • @DataProvider 表示这个方法是一个数据提供者, 数据Id为 userFullData

  • @DataConsumer 表示这个方法的参数, 需要消费数据, 数据Id为 user ,posts, followers.

当然, 原来的3个原子服务 用户基础信息 ,用户博客列表, 用户的粉丝数据, 也分别需要添加一些注解

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

其中

  • @DataProvider 与前面的含义相同, 表示这个方法是一个数据提供者
  • @InvokeParameter 表示方法执行时, 需要手动传入的参数

这里注意 @InvokeParameter@DataConsumer的区别, 前者需要用户在最上层调用时手动传参; 而后者, 是由框架自动分析依赖, 并异步调用取得结果之后注入的.

最后, 仅仅只需要调用一个统一的门面(Facade)接口, 传递数据Id, Invoke Parameters,以及返回值类型. 剩下的并行处理, 依赖分析和注入, 完全由框架自动处理.

  1. @Component
  2. public class UserQueryFacade {
  3. @Autowired
  4. private DataBeanAggregateQueryFacade dataBeanAggregateQueryFacade;
  5. public User getUserFinal(Long userId) throws InterruptedException,
  6. IllegalAccessException, InvocationTargetException {
  7. return dataBeanAggregateQueryFacade.get("userFullData",
  8. Collections.singletonMap("userId", userId), User.class);
  9. }
  10. }

如何用在你的项目中

上面的功能, 笔者已经封装为一个spring boot starter, 并发布到maven中央仓库.

只需在你的项目引入依赖.

  1. <dependency>
  2. <groupId>io.github.lvyahui8</groupId>
  3. <artifactId>spring-boot-data-aggregator-starter</artifactId>
  4. <version>1.0.1</version>
  5. </dependency>

并在 application.properties 文件中声明注解的扫描路径.

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

之后, 就可以使用如下注解和 Spring Bean 实现聚合查询

注意, @DataConsumer@InvokeParameter 可以混合使用, 可以用在同一个方法的不同参数上. 且方法的所有参数必须有其中一个注解, 不能有没有注解的参数.

项目地址和上述示例代码: https://github.com/lvyahui8/spring-boot-data-aggregator

后期计划

后续笔者将继续完善异常处理, 超时逻辑, 解决命名冲突的问题, 并进一步提高插件的易用性, 高可用性, 扩展性