Spring Boot高效數(shù)據(jù)聚合之道深入講解
背景
接口開發(fā)是后端開發(fā)中最常見的場(chǎng)景, 可能是RESTFul接口, 也可能是RPC接口. 接口開發(fā)往往是從各處撈出數(shù)據(jù), 然后組裝成結(jié)果, 特別是那些偏業(yè)務(wù)的接口.
例如, 我現(xiàn)在需要實(shí)現(xiàn)一個(gè)接口, 拉取用戶基礎(chǔ)信息+用戶的博客列表+用戶的粉絲數(shù)據(jù)的整合數(shù)據(jù), 假設(shè)已經(jīng)有如下三個(gè)接口可以使用, 分別用來(lái)獲取 用戶基礎(chǔ)信息 ,用戶博客列表, 用戶的粉絲數(shù)據(jù).
用戶基礎(chǔ)信息
@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); } }
用戶的粉絲數(shù)據(jù)
@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; } }
注意, 每一個(gè)方法都sleep了1s以模擬業(yè)務(wù)耗時(shí).
我們需要再封裝一個(gè)接口, 來(lái)拼裝以上三個(gè)接口的數(shù)據(jù).
PS: 這樣的場(chǎng)景實(shí)際在工作中很常見, 而且往往我們需要拼湊的數(shù)據(jù), 是要走網(wǎng)絡(luò)請(qǐng)求調(diào)到第三方去的. 另外可能有人會(huì)想, 為何不分成3個(gè)請(qǐng)求? 實(shí)際為了客戶端網(wǎng)絡(luò)性能考慮, 往往會(huì)在一次網(wǎng)絡(luò)請(qǐng)求中, 盡可能多的傳輸數(shù)據(jù), 當(dāng)然前提是這個(gè)數(shù)據(jù)不能太大, 否則傳輸?shù)暮臅r(shí)會(huì)影響渲染. 許多APP的首頁(yè), 看著復(fù)雜, 實(shí)際也只有一個(gè)接口, 一次性拉下所有數(shù)據(jù), 客戶端開發(fā)也簡(jiǎn)單.
串行實(shí)現(xiàn)
編寫性能優(yōu)良的接口不僅是每一位后端程序員的技術(shù)追求, 也是業(yè)務(wù)的基本訴求. 一般情況下, 為了保證更好的性能, 往往需要編寫更復(fù)雜的代碼實(shí)現(xiàn).
但凡人皆有惰性, 因此, 往往我們會(huì)像下面這樣編寫串行調(diào)用的代碼
@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才能拿到結(jié)果, 且一旦用到某個(gè)接口的數(shù)據(jù), 便需要注入相應(yīng)的service, 復(fù)用麻煩.
并行實(shí)現(xiàn)
有追求的程序員可能立馬會(huì)考慮到, 這幾項(xiàng)數(shù)據(jù)之間并無(wú)強(qiáng)依賴性, 完全可以并行獲取嘛, 通過(guò)異步線程+CountDownLatch+Future實(shí)現(xiàn), 就像下面這樣.
@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; } }
上面的代碼, 將串行調(diào)用改為并行調(diào)用, 在有限并發(fā)級(jí)別下, 能極大提高性能. 但很明顯, 它過(guò)于復(fù)雜, 如果每個(gè)接口都為了并行執(zhí)行都寫這樣一段代碼, 簡(jiǎn)直是噩夢(mèng).
優(yōu)雅的注解實(shí)現(xiàn)
熟悉java的都知道, java有一種非常便利的特性 ~~ 注解. 簡(jiǎn)直是黑魔法. 往往只需要給類或者方法上添加一些注解, 便可以實(shí)現(xiàn)非常復(fù)雜的功能.
有了注解, 再結(jié)合Spring依賴自動(dòng)注入的思想, 那么我們可不可以通過(guò)注解的方式, 自動(dòng)注入依賴, 自動(dòng)并行調(diào)用接口呢? 答案是肯定的.
首先, 我們先定義一個(gè)聚合接口
@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 表示這個(gè)方法是一個(gè)數(shù)據(jù)提供者, 數(shù)據(jù)Id為 userFullData
- @DataConsumer 表示這個(gè)方法的參數(shù), 需要消費(fèi)數(shù)據(jù), 數(shù)據(jù)Id為 user ,posts, followers.
當(dāng)然, 原來(lái)的3個(gè)原子服務(wù) 用戶基礎(chǔ)信息 ,用戶博客列表, 用戶的粉絲數(shù)據(jù), 也分別需要添加一些注解
@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 與前面的含義相同, 表示這個(gè)方法是一個(gè)數(shù)據(jù)提供者
- @InvokeParameter 表示方法執(zhí)行時(shí), 需要手動(dòng)傳入的參數(shù)
這里注意 @InvokeParameter 和 @DataConsumer的區(qū)別, 前者需要用戶在最上層調(diào)用時(shí)手動(dòng)傳參; 而后者, 是由框架自動(dòng)分析依賴, 并異步調(diào)用取得結(jié)果之后注入的.
最后, 僅僅只需要調(diào)用一個(gè)統(tǒng)一的門面(Facade)接口, 傳遞數(shù)據(jù)Id, Invoke Parameters,以及返回值類型. 剩下的并行處理, 依賴分析和注入, 完全由框架自動(dòng)處理.
@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); } }
如何用在你的項(xiàng)目中
上面的功能, 筆者已經(jīng)封裝為一個(gè)spring boot starter, 并發(fā)布到maven中央倉(cāng)庫(kù).
只需在你的項(xiàng)目引入依賴.
<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 實(shí)現(xiàn)聚合查詢
- @DataProvider
- @DataConsumer
- @InvokeParameter
- Spring Bean DataBeanAggregateQueryFacade
注意, @DataConsumer 和 @InvokeParameter 可以混合使用, 可以用在同一個(gè)方法的不同參數(shù)上. 且方法的所有參數(shù)必須有其中一個(gè)注解, 不能有沒有注解的參數(shù).
項(xiàng)目地址和上述示例代碼: https://github.com/lvyahui8/spring-boot-data-aggregator
后期計(jì)劃
后續(xù)筆者將繼續(xù)完善異常處理, 超時(shí)邏輯, 解決命名沖突的問(wèn)題, 并進(jìn)一步提高插件的易用性, 高可用性, 擴(kuò)展性
總結(jié)
以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,謝謝大家對(duì)腳本之家的支持。
相關(guān)文章
Spring Boot中擴(kuò)展XML請(qǐng)求與響應(yīng)的支持詳解
這篇文章主要給大家介紹了關(guān)于Spring Boot中擴(kuò)展XML請(qǐng)求與響應(yīng)的支持的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2018-09-09解決mybatis-plus3.1.1版本使用lambda表達(dá)式查詢報(bào)錯(cuò)的方法
這篇文章主要介紹了解決mybatis-plus3.1.1版本使用lambda表達(dá)式查詢報(bào)錯(cuò)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08Spring?容器初始化?register?與?refresh方法
這篇文章主要介紹了Spring?容器初始化?register?與?refresh方法,文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-07-07Hibernate中實(shí)現(xiàn)增刪改查的步驟詳解
本篇文章主要介紹了Hibernate中實(shí)現(xiàn)增刪改查的步驟與方法,具有很好的參考價(jià)值,下面跟著小編一起來(lái)看下吧2017-02-02SpringBoot實(shí)現(xiàn)接口返回?cái)?shù)據(jù)脫敏的代碼示例
在當(dāng)今的信息化時(shí)代,數(shù)據(jù)安全尤為重要,接口返回?cái)?shù)據(jù)脫敏是一種重要的數(shù)據(jù)保護(hù)手段,可以防止敏感信息通過(guò)接口返回給客戶端,本文旨在探討如何在SpringBoot應(yīng)用程序中實(shí)現(xiàn)接口返回?cái)?shù)據(jù)脫敏,需要的朋友可以參考下2024-07-07SpringMVC @RequestMapping注解作用詳解
通過(guò)@RequestMapping注解可以定義不同的處理器映射規(guī)則,下面這篇文章主要給大家介紹了關(guān)于SpringMVC中@RequestMapping注解用法的相關(guān)資料,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-01-01Java核心編程之文件隨機(jī)讀寫類RandomAccessFile詳解
這篇文章主要為大家詳細(xì)介紹了Java核心編程之文件隨機(jī)讀寫類RandomAccessFile,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08