Spring Boot 高效數據聚合之道

項目地址和示例代碼: github.com/lvyahui8/sp…java

背景

接口開發是後端開發中最多見的場景, 多是RESTFul接口, 也多是RPC接口. 接口開發每每是從各處撈出數據, 而後組裝成結果, 特別是那些偏業務的接口.git

如何方便快速的開發高性能的接口, 是一個必須思考的問題.程序員

例如, 我如今須要實現一個接口, 拉取用戶基礎信息+用戶的博客列表+用戶的粉絲數據的整合數據, 假設已經有以下三個接口可使用, 分別用來獲取 用戶基礎信息 ,用戶博客列表, 用戶的粉絲數據.github

用戶基礎信息spring

@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依賴自動注入的思想, 那麼咱們可不能夠經過註解的方式, 自動注入依賴, 自動並行調用接口呢? 答案是確定的.

首先, 咱們先定義一個聚合接口 (固然也能夠不定義聚合類, 全部代碼寫在原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 表示這個方法是一個數據提供者, 數據Id爲 userFullData

  • @DataConsumer 表示這個方法的參數, 須要消費數據, 數據Id分別爲 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)接口, 傳遞數據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-starter</artifactId>
  <version>1.0.2</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 能夠混合使用, 能夠用在同一個方法的不一樣參數上. 且方法的全部參數必須有其中一個註解, 不能有沒有註解的參數.

項目地址和上述示例代碼: github.com/lvyahui8/sp…. 感謝給予star, 歡迎一塊兒參與完善

特性

  • 異步獲取依賴

    全部 @DataConsumer 定義的依賴將異步獲取. 當provider方法參數中的全部依賴獲取完成, 才執行provider方法

  • 不限級嵌套

    依賴關係支持深層嵌套. 上面的示例只有一層

  • 異常處理

    目前支持兩種處理方式: 忽略or終止

    忽略是指provider方法在執行時, 忽略拋出的異常並return null值; 終止是指一旦有一個provider方法拋出了異常, 將逐級向上拋出, 終止後續處理.

    配置支持consumer級或者全局, 優先級 : consumer級 > 全局

  • 查詢緩存

    在調用Facade的query方法的一次查詢生命週期內, 方法調用結果可能複用, 只要方法簽名以及傳參一致, 則默認方法是冪等的, 將直接使用緩存的查詢結果. 但這個不是絕對的, 考慮到多線程的特性, 可能有時候不會使用緩存

  • 超時控制 (待實現)

後期計劃

後續筆者將繼續完善 #超時處理,#緩存,#提升吞吐量 並進一步提升插件的易用性, 高可用性, 擴展性


掃碼關注本人公衆號

相關文章
相關標籤/搜索