为什么要异步调用:
1:在做网络调用时大部分时间都是阻塞,我们程序需要阻塞等到响应时才会继续往下执行,而阻塞多了就意味着时间被消耗了2:如果说在阻塞的时候做其它的事情,或者将阻塞的时间切换到另外的线程去,那么这样我们程序的执行时间将变短
3:dubbo的客户端的异步调用能够免费的为你提供异步的功能
那么就由我们来写一个demo来学习一下这个dubbo的异步调用吧:
1:你所需要的前置知识 : 1:dubbo 2:CompletableFuture的使用或者Mono的使用 3:简单的lamada的知识框架:spring-cloud-alibaba
额外环境: nacos
github源码:https://github.com/WanT0011/sping-cloud-alibaba-async-demo
流程:
项目结构为一个provider,一个consumer
我们将使用一个模拟的流程,来进行一次异步化调用降低我们接口的响应时间
流程说明:
模拟一个检查用户账户余额的操作
1:调用商店服务 获取商店的折扣信息
2: 调用商品服务获取商品的价格和折扣信息
3: 调用用户服务获取用户账户余额
4: 调用折扣服务获取所有折扣信息,选用最低折扣并计算商品真实价格
5: 和用户的账户余额做对比,返回接果
其中,1、2、5是无前后关系的,4必须在1和2步骤后才可以执行。
对象说明:
User–用户:id、name、money(账户余额)
Store–商店:id、productIdList(商品id列表)、discountIdList(折扣id列表)
Product–商品:id、storeId(商店id)、name、price(价格)、discountIdList(折扣id列表)
Discount–折扣:id、discount(折扣)、ttl(是否超时)
服务层说明:InMemoryUserServiceImpl、InMemoryStoreServiceImpl、InMemoryDisCountServiceImpl三个服务内部各有一个或多个map,用于模拟数据库,都提供了根据id查看的方法,初始化时会初始化一些测试数据进行,
在api的模块中:
由于项目是中期改造过来的,所以我们采用接口的默认方法去封装异步,这里可以参考dubbo官网说明:
那么我们的服务端就是这样准备好了,在对每一个方法进行一个模拟延迟,每个接口都需要1s的访问时间,接下来来写客户端:
首先按照预先流程,写一个同步逻辑:
/**
* 使用同步做调用
* @param uid 用户id
* @param pid 商品id
* @param sid 商店id
* @return
*/
@ApiOperation(value = "使用同步做调用")
@ExecTimeLog
@GetMapping("/sync/checkUserAccountPrice/{uid}/{pid}/{sid}")
public CheckRespDTO syncCheckUserAccountPrice(@PathVariable("uid") Integer uid
,@PathVariable("pid") Integer pid,@PathVariable("sid") Integer sid){
// 查询店铺的信息,找不到就拿第一个商店
Store store = Optional.ofNullable(storeService.listById(sid))
.orElseGet(() -> storeService.listById(1));
// 获取商品信息 ,找不到就拿第一个商品信息
Product product = Optional.ofNullable(storeService.listProductById(pid))
.orElseGet(() -> storeService.listProductById(1));
// 获取所有的折扣
List<Integer> discountIds = Optional.ofNullable(store)
.map(Store::getDiscountIdList)
.map(list -> {
list.addAll(Optional.ofNullable(product).map(Product::getDiscountIdList).orElse(Collections.emptyList()));
return list;
})
.orElseGet(() -> Optional.of(product).map(Product::getDiscountIdList).orElse(null));
// 找出最优折扣,没有折扣就使用 Discount.NO_DISCOUNT
Discount discount = Optional.ofNullable(discountIds)
.map(disCountService::listById)
.map(discounts -> discounts
.stream()
.filter(discount1 -> !discount1.getTtl())
.reduce((v1, v2) -> {
if (v1.getDiscount() > v2.getDiscount()) {
return v2;
} else {
return v1;
}
}).orElse(null))
.orElse(Discount.NO_DISCOUNT);
// 计算金额
Long price = discount.getDiscount() * product.getPrice();
// 获取用户账户余额
User info = userService.info(uid);
/** 设置返回值 */
CheckRespDTO checkRespDTO = new CheckRespDTO();
checkRespDTO.setCheckPass(price < info.getMoney());
checkRespDTO.setDiscount(discount);
checkRespDTO.setPrice(product.getPrice());
return checkRespDTO;
}
执行一下查看时间:
执行10次,每次花费2s多,那么我们根据业务特性进行异步化处理成一下结构:
/**
* 使用dubbo异步的方式
* @param uid 用户id
* @param pid 商品id
* @param sid 商店id
* @return
*/
@ApiOperation(value = "使用异步做调用")
@ExecTimeLog
@GetMapping("/async/checkUserAccountPrice/{uid}/{pid}/{sid}")
public CheckRespDTO asyncCheckUserAccountPrice(@PathVariable("uid") Integer uid
,@PathVariable("pid") Integer pid,@PathVariable("sid") Integer sid) throws Exception {
CheckRespDTO checkRespDTO = new CheckRespDTO();
CompletableFuture<CheckRespDTO> future =
// 根据商店id获取商店信息
storeService.listById(sid, false)
.thenCombine(
//同时我们再去获取商品信息
storeService.listProductById(pid, false)
// 将获取的商店和商品结果整合
, (store, product) -> {
checkRespDTO.setPrice(product.getPrice());
// 将商店和商品的 折扣 id整合
List<Integer> discountIds = Optional.ofNullable(store)
.map(Store::getDiscountIdList)
.map(list -> {
list.addAll(Optional.ofNullable(product).map(Product::getDiscountIdList).orElse(Collections.emptyList()));
return list;
})
.orElseGet(() -> Optional.of(product).map(Product::getDiscountIdList).orElse(null));
// 返回折扣服务进行查询所有的折扣信息
return disCountService.listById(discountIds);
})
.thenCombine(
// 查询用户信息用户
userService.info(uid, false)
// 折扣信息和用户信息都查询到了,进行整合
, (discountList, user) -> {
// 选出最优折扣
Discount discount = Optional.ofNullable(discountList)
.map(discounts -> discounts
.stream()
.filter(discount1 -> !discount1.getTtl())
.reduce((v1, v2) -> {
if (v1.getDiscount() > v2.getDiscount()) {
return v2;
} else {
return v1;
}
}).orElse(null))
.orElse(Discount.NO_DISCOUNT);
// 计算金额
Long price = discount.getDiscount() * checkRespDTO.getPrice();
//初始化返回值
checkRespDTO.setCheckPass(price < user.getMoney());
checkRespDTO.setDiscount(discount);
return checkRespDTO;
});
return future.get(3000, TimeUnit.MILLISECONDS);
}
简单解读一下,这里就是异步获取商店和商品信息,得到结果后在调用折扣服务获取折扣信息,获取到一个未来的结果,同时又异步获取用户信息,和未来的折扣信息整合,进行计算得到最终的校验结果,最后我们等待这个结果3s钟,返回结果。
执行一下:
和同步的比较节约了一倍的时间,
使用dubbo原生的可能在某些地方会比较的生硬,这里我们可以引入reactor的Mono或者flux:
/**
* 使用mono异步的方式
* @param uid 用户id
* @param pid 商品id
* @param sid 商店id
* @return
*/
@ApiOperation(value = "使用mono异步做调用")
@ExecTimeLog
@GetMapping("/async/mono/checkUserAccountPrice/{uid}/{pid}/{sid}")
public CheckRespDTO asyncByReactorCheckUserAccountPrice(@PathVariable("uid") Integer uid
,@PathVariable("pid") Integer pid,@PathVariable("sid") Integer sid) {
CheckRespDTO checkRespDTO = new CheckRespDTO();
Tuple2<Discount, User> res = Mono.zip(
Mono.zip(
// 获取店铺信息
Mono.fromFuture(Optional.ofNullable(storeService.listById(sid, false)).orElseGet(()
-> storeService.listById(1, false)))
// 获取商品信息
, Mono.fromFuture(Optional.ofNullable(storeService.listProductById(pid, false)).orElseGet(()
-> storeService.listProductById(1, false)))
// 选出最优的折扣
, (store, product) -> {
checkRespDTO.setPrice(product.getPrice());
// 获取所有的折扣,将店铺和商品的折扣整合
List<Integer> discountIds = Optional.ofNullable(store)
.map(Store::getDiscountIdList)
.map(list -> {
list.addAll(Optional.ofNullable(product)
.map(Product::getDiscountIdList)
.orElse(Collections.emptyList()));
return list;
})
.orElseGet(() -> Optional.ofNullable(product)
.map(Product::getDiscountIdList)
.orElse(null));
// 根据id调用接口获取折扣选出最优
return Optional.ofNullable(discountIds)
.map(disCountService::listById)
.map(discounts -> discounts
.stream()
.filter(discount1 -> !discount1.getTtl())
.reduce((v1, v2) -> {
if (v2.getDiscount() >= v1.getDiscount()) {
return v1;
} else {
return v2;
}
}).orElse(null))
.orElse(Discount.NO_DISCOUNT);
})
// 获取用户信息
, Mono.fromFuture(userService.info(uid, false))
//等待响应
).block(Duration.ofSeconds(3));
/** 设置返回值 */
checkRespDTO.setDiscount(res.getT1());
checkRespDTO.setCheckPass(checkRespDTO.getPrice() * res.getT1().getDiscount() < res.getT2().getMoney());
return checkRespDTO;
}
这里说一下为什么使用block而不是subscribe,因为如果使用的是subscribe的话再返回的时候可能结果暂时还没有计算出来,执行一下看一下结果:
执行10次可以看到和dubbo原生的差不多,这里多了一部分的时间消耗,可能是执行的次数太少或者是使用Mono存在线程切换导致的,如果感觉使用原生的异步不太顺手的话可以考虑使用Mono进行异步化,
总结:
dubbo支持异步api,我们可以再客户端这边做一些异步的操作将我们的接口的响应时间缩短,同时充分利用多核和cpu的速度。