当前位置: 移动技术网 > IT编程>开发语言>Java > Spring Boot高效数据聚合之道深入讲解

Spring Boot高效数据聚合之道深入讲解

2019年07月19日  | 移动技术网IT编程  | 我要评论

春暖花开公告,天地人伤感音乐网,武德奉文思

背景

接口开发是后端开发中最常见的场景, 可能是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;
 }
}

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

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

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

串行实现

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

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

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

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

并行实现

有追求的程序员可能立马会考虑到, 这几项数据之间并无强依赖性, 完全可以并行获取嘛, 通过异步线程+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依赖自动注入的思想, 那么我们可不可以通过注解的方式, 自动注入依赖, 自动并行调用接口呢? 答案是肯定的.

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

@component
public class useraggregate {
 @dataprovider(id="userfulldata")
 public user userfulldata(@dataconsumer(id = "user") user user,
        @dataconsumer(id = "posts") list<post> posts,
        @dataconsumer(id = "followers") list<user> followers) {
  user.setfollowers(followers);
  user.setposts(posts);
  return user;
 }
}

其中

  • @dataprovider 表示这个方法是一个数据提供者, 数据id为 userfulldata
  • @dataconsumer 表示这个方法的参数, 需要消费数据, 数据id为 user ,posts, followers.

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

@service
public class userserviceimpl implements userservice {
 @dataprovider(id = "user")
 @override
 public user get(@invokeparameter("userid") long id) {
@service
public class postserviceimpl implements postservice {
 @dataprovider(id = "posts")
 @override
 public list<post> getposts(@invokeparameter("userid") long userid) {
@service
public class followserviceimpl implements followservice {
 @dataprovider(id = "followers")
 @override
 public list<user> getfollowers(@invokeparameter("userid") long userid) {

其中

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

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

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

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

如何用在你的项目中

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

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

<dependency>
 <groupid>io.github.lvyahui8</groupid>
 <artifactid>spring-boot-data-aggregator-example</artifactid>
 <version>1.0.1</version>
</dependency>

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

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

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

  • @dataprovider
  • @dataconsumer
  • @invokeparameter
  • spring bean databeanaggregatequeryfacade

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

项目地址和上述示例代码:

后期计划

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

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对移动技术网的支持。

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网