微服务框架Spring Cloud介绍 Part4: 使用Eureka, Ribbon, Feign实现REST服务客户端
2016-10-09 10:46
1571 查看
原文地址:http://skaka.me/blog/2016/08/25/springcloud4/
在上一篇文章中我们开发了一个用户注册服务.
这篇文章我将介绍如何开发mysteam订单服务中的下单功能, 下单功能会涉及服务之间的交互与事件的处理, 并且我会对开发过程中用到的框架和类库进行简单地讲解. 开始写代码之前, 我们先来看看下单的处理流程:
其中1,2,3,4,11步的黑色箭头代表是同步操作, 5,6,7,8,9,10步是异步操作. 下单接口接收要订购的产品ID, 数量和要使用的优惠券ID, 然后调用产品服务的接口查询产品信息, 调用优惠券接口校验优惠券是否有效, 以及调用账户接口判断账户金额是否足够(mysteam是一个虚拟物品商城, 采用先充值后购买的形式). 如果这些校验都成功, 订单服务会发送账户扣款事件和优惠券使用事件到MQ, 账户服务和优惠券服务会从MQ读取事件进行处理,
如果处理成功, 订单服务将能接收到结果, 并且将订单状态置为
如果处理失败或超时, 订单状态会被置为
流程清楚了, 现在我们来看代码. 订单类是
订单类和前面的用户类类似, 其中有两个字段需要注意一下:
OrderStatus是一个枚举, 表示订单状态. OrderItem是订单项.
DAO层基本没有实际的代码, 就不贴了.
下单的业务逻辑都在service内, 打开
找到
代码略长, 我略过了其中一部分代码.
1.根据订购的产品ID, 向产品服务查询产品信息, 并计算订单金额.
2.如果有使用优惠券, 向优惠券服务查询优惠券是否有效(请求REST接口).
3.根据订单金额, 向账户服务查询用户余额是否足够(请求REST接口).
4.如果上述步骤都成功完成, 发送账户扣款事件以及优惠券使用事件(如果有优惠券), 并注册回调方法等待事件结果.
如果事件处理成功会调用
处理失败会调用
这个方法只是将订单状态置为
流程就完成了.
只不过是将订单状态改为
看到这里, 先不去管服务调用的实现细节, 细心的你可能会产生一些疑问:
1.第4步为什么要使用事件的形式去扣款和处理优惠券, 不能和前面的查询操作一样使用REST接口来处理吗?
2.为什么要先查询用户余额是否足够, 再发送扣款事件, 直接发送扣款事件不好吗? 如果查询余额返回成功之后, 其他业务修改了余额, 处理扣款事件的时候余额不足怎么办?
3.第4步同时发送了扣款事件和优惠券使用事件, 如果扣款成功了, 但是优惠券使用失败了怎么处理?
其实这些问题都指向同一个问题域: 分布式事务. 分布式事务是开发微服务首先要解决的问题. 分布式事务是一个很大的话题, 这里我只简单介绍一下eBay的Dan Pritchard提出的BASE原则:
基本可用(Basically Available)
软状态(Soft state)
最终一致(Eventually consistent)
BASE其实和传统数据库的ACID是两个不同的思想, 以我们上面的订单系统为例. 订单服务向账户服务发送扣款事件, 账户服务接收到事件并且处理成功, 但是还没有将处理结果发送到订单服务, 这时候系统数据就处于短暂的不一致状态: 用户的账户余额已经被扣减掉了, 但是订单状态还是
过了一段时间, 订单服务获取到扣款事件的处理结果并且将订单状态置为
这个时候系统才达到最终一致的状态. 这种事务处理方法并不是适用于所有业务, 如果需要强一致性, 还是得使用2PC或者3PC来完成.
了解了mysteam的事务处理原则, 我们回头看看刚才提出的3个问题:
1.mysteam是使用事件的方式来进行事务处理的, REST接口一般只用来实现查询或者其他不需要事务的操作. 所以只要涉及到数据修改, 一般都通过事件来完成.
2.发送扣款之前先查询余额是为了减少不必要的事件操作, 因为如果事件处理失败会涉及到事件撤销, 是比较耗时的操作, 先进行余额查询, 余额不足直接流程就中止了. 根据我们的经验, 一般来说查询余额成功后续扣款失败的几率比较小, 所以收益大于付出. 3.这涉及到事件的撤销处理. 在mysteam的订单服务中, 如果接收到了扣款成功和优惠券使用失败这两个事件结果, 订单服务会启动事件撤销流程, 向账户服务发送扣款撤销事件, 并且将订单状态置为下单失败.
综上, mysteam的事务处理遵循BASE, 实现方式是使用事件. 关于事务的其他细节以及事件如何实现, 我后面会用单独的文章来介绍. 这里我们先回到本篇的主题, 如何调用REST接口. 在这里, 我先简单介绍一下Eureka, Ribbon和Feign这三个组件.
Eureka:
服务注册中心. 我们的REST服务在启动的时候会将自己的地址注册到Eureka, 其他需要该服务的应用会请求Eureka进行服务寻址, 得到目标服务的ip地址之后就会使用该地址直连目标服务.
Ribbon:
客户端负载均衡类库. 当客户端请求的目标服务存在多个实例时, Ribbon会将请求分散到各个实例. 一般会结合Eureka一起使用.
Feign:
HTTP客户端类库. 我们使用Feign提供的注解编写HTTP接口的客户端代码非常简单, 只需要声明一个Java接口加上少量注解就完成了.
接下来我们看代码实例. 以账户服务的接口为例, 之前我们在
打开
代码如下:
AccountClient是一个加了Feign注解的接口:
id, 这个service id就是我们在YAML配置文件中配的
比如
我们请求的REST接口需要一个url路径参数userId, 以及一个查询参数balance. 我们在代码中不需要直接调用Ribbon的代码, Feign会帮我们处理好一切. 根据我们的
Feign会在Spring容器启动之后, 将生成的代理类注入
所以我们不需要写HTTP调用的实现代码就能完成REST接口的调用.
到这里下单的逻辑就完成了. 我们知道在分布式环境下, 服务之间的依赖都是脆弱而且不稳定的, 极有可能因为一个服务实例的延迟或宕机造成所有服务不可用. 所以mysteam中引入了hystrix. 细心的同学可能已经在
下篇文章我将介绍hystrix的基本用法, 以及如何使用hystrix board和turbine来监控hystrix服务.
在上一篇文章中我们开发了一个用户注册服务.
这篇文章我将介绍如何开发mysteam订单服务中的下单功能, 下单功能会涉及服务之间的交互与事件的处理, 并且我会对开发过程中用到的框架和类库进行简单地讲解. 开始写代码之前, 我们先来看看下单的处理流程:
其中1,2,3,4,11步的黑色箭头代表是同步操作, 5,6,7,8,9,10步是异步操作. 下单接口接收要订购的产品ID, 数量和要使用的优惠券ID, 然后调用产品服务的接口查询产品信息, 调用优惠券接口校验优惠券是否有效, 以及调用账户接口判断账户金额是否足够(mysteam是一个虚拟物品商城, 采用先充值后购买的形式). 如果这些校验都成功, 订单服务会发送账户扣款事件和优惠券使用事件到MQ, 账户服务和优惠券服务会从MQ读取事件进行处理,
如果处理成功, 订单服务将能接收到结果, 并且将订单状态置为
下单成功,
如果处理失败或超时, 订单状态会被置为
下单失败.
1. 实现Model
流程清楚了, 现在我们来看代码. 订单类是$YOUR_PATH/mysteam/order/core/src/main/java/com/akkafun/order/domain/Order.java,
订单类和前面的用户类类似, 其中有两个字段需要注意一下:
1 2 3 4 5 6 | @Column @Enumerated(value = EnumType.STRING) private OrderStatus status; @OneToMany(fetch = FetchType.LAZY, mappedBy = "order") private List<OrderItem> orderItemList = new ArrayList<>(0); |
2. 实现DAO
DAO层基本没有实际的代码, 就不贴了.
3. 实现Service
下单的业务逻辑都在service内, 打开$YOUR_PATH/mysteam/order/core/src/main/java/com/akkafun/order/service/OrderService.java,
找到
placeOrder方法:
1 2 3 4 5 67 | /** * 下订单 * * @param placeOrderDto * @return */ @Transactional public Order placeOrder(PlaceOrderDto placeOrderDto) { //... //#1 //查询产品信息 List<Long> productIds = placeOrderDto.getPlaceOrderItemList().stream() .map(PlaceOrderItemDto::getProductId) .collect(Collectors.toList()); List<ProductDto> productDtoList = productGateway.findProducts(productIds); //... //#2 //查询优惠券信息 List<OrderCoupon> orderCouponList = new ArrayList<>(); Set<Long> couponIdSet = new HashSet<>(placeOrderDto.getCouponIdList()); if (!couponIdSet.isEmpty()) { //#2 List<CouponDto> couponDtoList = couponGateway.findCoupons(new ArrayList<>(couponIdSet)); orderCouponList = couponDtoList.stream().map(couponDto -> { OrderCoupon orderCoupon = new OrderCoupon(); orderCoupon.setCouponAmount(couponDto.getAmount()); orderCoupon.setCouponCode(couponDto.getCode()); orderCoupon.setCouponId(couponDto.getId()); return orderCoupon; }).collect(Collectors.toList()); } //#3 //计算订单金额 long couponAmount = orderCouponList.stream().mapToLong(OrderCoupon::getCouponAmount).sum(); order.setPayAmount(order.calcPayAmount(order.getTotalAmount(), couponAmount)); //检验账户余额是否足够 if (order.getPayAmount() > 0L) { boolean balanceEnough = accountGateway.isBalanceEnough(placeOrderDto.getUserId(), order.getPayAmount()); if(!balanceEnough) { throw new AppBusinessException(CommonErrorCode.BAD_REQUEST, "下单失败, 账户余额不足"); } } //... //#4 eventBus.ask( AskParameterBuilder.askOptional(askReduceBalance, askUseCoupon) .callbackClass(OrderCreateCallback.class) .addParam("orderId", String.valueOf(order.getId())) .build() ); return order; } |
placeOrder方法主要做的事情有:
1.根据订购的产品ID, 向产品服务查询产品信息, 并计算订单金额.
2.如果有使用优惠券, 向优惠券服务查询优惠券是否有效(请求REST接口).
3.根据订单金额, 向账户服务查询用户余额是否足够(请求REST接口).
4.如果上述步骤都成功完成, 发送账户扣款事件以及优惠券使用事件(如果有优惠券), 并注册回调方法等待事件结果.
如果事件处理成功会调用
markCreateSuccess方法,
处理失败会调用
markCreateFail,
markCreateSuccess方法的代码如下:
1 2 3 4 5 67 | @Transactional public void markCreateSuccess(Long orderId) { Order order = checkOrderBeforeMarkSuccessOrFail(orderId); order.setStatus(OrderStatus.CREATED); orderRepository.save(order); } |
下单成功,
流程就完成了.
markCreateFail处理过程类似,
只不过是将订单状态改为
下单失败.
看到这里, 先不去管服务调用的实现细节, 细心的你可能会产生一些疑问:
1.第4步为什么要使用事件的形式去扣款和处理优惠券, 不能和前面的查询操作一样使用REST接口来处理吗?
2.为什么要先查询用户余额是否足够, 再发送扣款事件, 直接发送扣款事件不好吗? 如果查询余额返回成功之后, 其他业务修改了余额, 处理扣款事件的时候余额不足怎么办?
3.第4步同时发送了扣款事件和优惠券使用事件, 如果扣款成功了, 但是优惠券使用失败了怎么处理?
其实这些问题都指向同一个问题域: 分布式事务. 分布式事务是开发微服务首先要解决的问题. 分布式事务是一个很大的话题, 这里我只简单介绍一下eBay的Dan Pritchard提出的BASE原则:
基本可用(Basically Available)
软状态(Soft state)
最终一致(Eventually consistent)
BASE其实和传统数据库的ACID是两个不同的思想, 以我们上面的订单系统为例. 订单服务向账户服务发送扣款事件, 账户服务接收到事件并且处理成功, 但是还没有将处理结果发送到订单服务, 这时候系统数据就处于短暂的不一致状态: 用户的账户余额已经被扣减掉了, 但是订单状态还是
正在下单.
过了一段时间, 订单服务获取到扣款事件的处理结果并且将订单状态置为
下单成功.
这个时候系统才达到最终一致的状态. 这种事务处理方法并不是适用于所有业务, 如果需要强一致性, 还是得使用2PC或者3PC来完成.
了解了mysteam的事务处理原则, 我们回头看看刚才提出的3个问题:
1.mysteam是使用事件的方式来进行事务处理的, REST接口一般只用来实现查询或者其他不需要事务的操作. 所以只要涉及到数据修改, 一般都通过事件来完成.
2.发送扣款之前先查询余额是为了减少不必要的事件操作, 因为如果事件处理失败会涉及到事件撤销, 是比较耗时的操作, 先进行余额查询, 余额不足直接流程就中止了. 根据我们的经验, 一般来说查询余额成功后续扣款失败的几率比较小, 所以收益大于付出. 3.这涉及到事件的撤销处理. 在mysteam的订单服务中, 如果接收到了扣款成功和优惠券使用失败这两个事件结果, 订单服务会启动事件撤销流程, 向账户服务发送扣款撤销事件, 并且将订单状态置为下单失败.
综上, mysteam的事务处理遵循BASE, 实现方式是使用事件. 关于事务的其他细节以及事件如何实现, 我后面会用单独的文章来介绍. 这里我们先回到本篇的主题, 如何调用REST接口. 在这里, 我先简单介绍一下Eureka, Ribbon和Feign这三个组件.
Eureka:
服务注册中心. 我们的REST服务在启动的时候会将自己的地址注册到Eureka, 其他需要该服务的应用会请求Eureka进行服务寻址, 得到目标服务的ip地址之后就会使用该地址直连目标服务.
Ribbon:
客户端负载均衡类库. 当客户端请求的目标服务存在多个实例时, Ribbon会将请求分散到各个实例. 一般会结合Eureka一起使用.
Feign:
HTTP客户端类库. 我们使用Feign提供的注解编写HTTP接口的客户端代码非常简单, 只需要声明一个Java接口加上少量注解就完成了.
接下来我们看代码实例. 以账户服务的接口为例, 之前我们在
placeOrder方法内查询账户余额的代码如下:
1 | boolean balanceEnough = accountGateway.isBalanceEnough(placeOrderDto.getUserId(), order.getPayAmount()); |
$YOUR_PATH/mysteam/order/core/src/main/java/com/akkafun/order/service/gateway/AccountGateway.java,
代码如下:
1 2 3 4 5 67 | @Service public class AccountGateway { protected Logger logger = LoggerFactory.getLogger(AccountGateway.class); @Autowired AccountClient accountClient; @HystrixCommand(ignoreExceptions = RemoteCallException.class) public boolean isBalanceEnough(Long userId, Long amount) { return accountClient.checkEnoughBalance(userId, amount).isSuccess(); } } |
1 2 3 4 5 67 | @FeignClient(AccountUrl.SERVICE_HOSTNAME) public interface AccountClient { @RequestMapping(method = RequestMethod.GET, value = AccountUrl.CHECK_ENOUGH_BALANCE) BooleanWrapper checkEnoughBalance(@PathVariable("userId") Long userId, @RequestParam("balance") Long balance); } |
@FeignClient注解需要声明一个service
id, 这个service id就是我们在YAML配置文件中配的
spring.application.name的值,
比如
account.yml中的
spring.application.name值是
account.
我们请求的REST接口需要一个url路径参数userId, 以及一个查询参数balance. 我们在代码中不需要直接调用Ribbon的代码, Feign会帮我们处理好一切. 根据我们的
AccountClient接口声明,
Feign会在Spring容器启动之后, 将生成的代理类注入
AccountGateway,
所以我们不需要写HTTP调用的实现代码就能完成REST接口的调用.
到这里下单的逻辑就完成了. 我们知道在分布式环境下, 服务之间的依赖都是脆弱而且不稳定的, 极有可能因为一个服务实例的延迟或宕机造成所有服务不可用. 所以mysteam中引入了hystrix. 细心的同学可能已经在
AccountGateway中发现
@HystrixCommand注解了,
下篇文章我将介绍hystrix的基本用法, 以及如何使用hystrix board和turbine来监控hystrix服务.
相关文章推荐
- 服务注册发现Eureka之三:Spring Cloud Ribbon实现客户端负载均衡(客户端负载均衡Ribbon之三:使用Ribbon实现客户端的均衡负载)
- 使用Eureka, Ribbon, Feign实现REST服务客户端
- 微服务框架Spring Cloud介绍 Part1: 使用事件和消息队列实现分布式事务
- 微服务框架Spring Cloud介绍 Part1: 使用事件和消息队列实现分布式事务
- SpringCloud 查找调用REST服务使用RestTemplate(ribbon负载)或feign模式 教程源码 火推
- 微服务框架Spring Cloud介绍 Part1: 使用事件和消息队列实现分布式事务
- Spring Cloud版——电影售票系统<三>使用Feign实现声明式REST调用
- 第二章 使用SpringCloud框架实现一个微服务
- Spring Cloud(三)服务提供者 Eureka + 服务消费者(rest + Ribbon)
- springcloud使用ribbon实现客户端负载均衡
- 使用Spring Cloud Feign作为HTTP客户端调用远程HTTP服务
- 使用Spring Cloud Feign作为HTTP客户端调用远程HTTP服务
- spring cloud中使用Ribbon实现客户端的软负载均衡
- 使用Spring Cloud Feign作为HTTP客户端调用远程HTTP服务
- Spring Cloud 入门教程(六): 用声明式REST客户端Feign调用远端HTTP服务
- 疯狂Spring Cloud连载(10)REST客户端Feign介绍
- 使用Spring Cloud Feign作为HTTP客户端调用远程HTTP服务
- Spring Cloud+Eureka+Ribbon实现客户端负载均衡
- springcloud微服务实战:Eureka+Zuul+Feign/Ribbon+Hystrix Turbine+SpringConfig+sleuth+zipkin
- 疯狂Spring Cloud连载(10)——Rest客户端Feign介绍