SpringCloud-4-应用通信

1.HTTP vs RPC

  1. 应用(微服务)间通讯方式主要有两种:HTTP和RPC
  2. 两种方式的主角:RPC–Dubbo HTTP–SpringCloud
  3. Dubbo定位始终是一个RPC远程调用框架,而SpringCloud是微服务下的一站式解决方式
  4. SpringCloud微服务下服务调用使用的是:HTTP Restful,HTTP Restful本身轻量、适应性强、可以很容易跨语言跨平台。
  5. SpringCloud中服务间两种restful调用方式:1. RestTemplate 2.Feign

2.RestTemplate的三种使用方式

  1. 1.RestTemplate是一个HTTP客户端,类似于HttpClient,功能差不多,但是用法上更加简单。

    2.1 RestTemplate例子

  2. 1.使用 “订单服务->商品服务”为例
  3. 2.订单服务调用商品服务,我们把商品服务当做server端,订单服务当做client端
  4. 3.为了不影响之前的代码逻辑,我们新建单独的包为例子。
  5. 4.product端新建ServerController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author yxm
* @date 2019/4/20 0:16:16
*/
@RestController
public class ServerController {
@GetMapping("/msg")
public String msg(){
return "this is product server mmsg";
}
}
```
5. 5.order服务中新建ClientController
RestTemplate第一种方式

/**

  • @author yxm
  • @date 2019/4/20 0:18:18
    */
    @RestController
    @Slf4j
    public class ClientController {

    @GetMapping(“/getProductMsg”)
    public String getProductMsg(){

    //1.RestTemplate第一种方式 
    RestTemplate restTemplate = new RestTemplate();
    String response = restTemplate.getForObject("http://localhost:8080/msg", String.class);
    log.info("response{}",response);
    return response;
    

    }
    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    由于product服务已经启用了8080端口,我们用-D指令在order服务中用-Dserver.port=8081,不建议直接写到配置文件。
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/84.png)
    ![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/85.png)
    以上是第一种调用方式。缺点:1.url为固定写死的,上线时候部署多台服务器,有时候连部署到哪台服务器都不知道。2.对方可能启动了多个实例,如果写死到一台实现不了负载均衡,其他服务器就不能访问了。
    6. 6.RestTemplate第二种方式

@RestController
@Slf4j
public class ClientController {

@Autowired
private LoadBalancerClient loadBalancerClient;

@GetMapping("/getProductMsg")
public String getProductMsg(){

    //2.第二种方式:通过LoadBalancerClient获取服务名,ip和port
    /**
     * SpringCloud提供了LoadBalancerClient,将其注入到Spring中
     */
    RestTemplate restTemplate = new RestTemplate();
    ServiceInstance product = loadBalancerClient.choose("PRODUCT");
    String url = String.format("http://%s:%s", product.getHost(), product.getPort());
    String response = restTemplate.getForObject(url, String.class);
    log.info("response{}",response);
    return response;
}

}

1
2
3
4
5
6
第二种方式每次都需要那么写(创建对象,获取ip,获取port,在调用获取),还是比较麻烦
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/85.png)
7. 7.第三种方式

@Component
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}

@RestController
@Slf4j
public class ClientController {
@Autowired
private RestTemplate restTemplate;

@GetMapping("/getProductMsg")
public String getProductMsg(){
    //3.第三种方式:通过RestTemplate的配置加LoadBalancerClient注解
    String response = restTemplate.getForObject("http://PRODUCT/msg", String.class);
    log.info("response{}",response);
    return response;
}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
![](https://raw.githubusercontent.com/startshineye/img/master/2019/04/85.png)
#### 3.负载均衡器Ribbion
1. 1.前面我们在说Eureka时候,谈到了服务端发现和客户端发现。Eureka属于客户端发现的方式,他的负载均衡是软负载,也就是客户端会向服务器(例如:EurekaServer)拉取已经注册的可用服务信息,然后根据负载均衡策略直接命中哪台服务器、发送请求。这整个过程都是在客户端完成的,并不需要服务端参数。SpringCloud中客户端负载均衡就是Ribbion 。他是基于NetflexRibbion实现的。通过SpringCloud的封装可以轻松的实现面向服务的restful模板请求自动转化成客户端负载均衡服务调用。
一下组件都使用到了Ribbon:
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/9.png)
2. 2.SpringCloud在结合了Ribbon的负载均衡实现中,封装增加了HttpClient和OkHttp两种请求端实现,默认使用了Ribbon对Eureka服务发现的负载均衡client。
3. 3.在上一节小结中,我们介绍了RestTemplate的三种实现方式。其中通过添加@LoadBalanced注解或者 直接写代码时候使用LoadBalancerClient,其实用到的就是Ribbon的组件
4. 4.添加@LoadBalanced注解后,Ribbon会通过LoadBalancerClient自动的帮助你基于某种规则,比如:随机简单的轮训去连接目标服务,从而很容易使用Ribbon实现自定义的负载均衡算法。
5. 5.Ribbon实现负载均衡核心有3点:1.服务发现(也就是发现依赖服务的列表,也就是依据服务的名字,把该服务下的实例全都找出来) 2.服务选择规则(依据规则策略:如何从多个服务中选择一个有效的服务) 3.服务监听(监测失效服务做到高效剔除)
6. 6.主要组件是:ServerList、IRule、ServerListFilter。总体流程是:首先通过ServerList获取所有的可用服务列表。然后通过ServerListFilter过滤掉一部分地址,最后剩下的地址中通过IRule选择一个实例作为最终目标结果。
#### 4 追踪源码自定义负载均衡策略
#### 5 Feign的使用
##### 5.1 Feign的基本使用
1. 1.本节使用Feign实现应用间通信。
2. 2.现在我们使用了Feign就不用使用restTemplate了,我们现在在ClientController中将restTemplate相关删除,并将RestTemplateConfig删除
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/10.png)
3. 3.添加依赖:


org.springframework.cloud
spring-cloud-starter-feign

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/11.png)
4. 4.启动类中添加注解
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/12.png)
5. 5.定义好feign要调用的Server端接口
建立client包,定义一个ProductClient接口(里面的方法就是调用product服务的方法),此client代表order服务是一个相对于product的客户端服务。
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/13.png)
product中ServerController里面调用的方法:
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/14.png)
6. 6.此接口定义完之后,在Controller里面定义,然后接口请求:http://localhost:8081/getProductMsg
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/15.png)
##### 5.2 Feign的总结
1. 声明式REST客户端(伪RPC)
2. 采用了基于接口的注解(定义一个接口,然后在其上加注解)
3. 内部使用了Ribbon作为负载均衡
#### 6 Feign获取商品列表
在Oder服务的OrderServiceImpl中的create里面1、2、3是我们还没有做的,这节我们需要通过Feign获取商品列表
##### 6.1 Product服务中添加获取商品列表
###### 1.Product-->dao
ProductInfoRepository:

public interface ProductInfoRepository extends JpaRepository{
List findByProductStatus(Integer productStatus);
List findByProductIdIn(List productIdList);
}

1
2
3
4
###### 2.Product-->service
ProductService:

public interface ProductService {
/**

 * 查询所有在架商品列表
 */
List<ProductInfo> findUpAll();
/**
 * 查询商品列表
 */
List<ProductInfo> findList(List<String> productIdList);

}

1
2
3
ProductServiceImpl:

@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductInfoRepository repository;
@Override
public List findUpAll() {
return repository.findByProductStatus(ProductStatusEnum.UP.getCode());
}
@Override
public List findList(List productIdList) {
return repository.findByProductIdIn(productIdList);
}
}

1
2
3
###### 3.Product-->controller
ProductController:

@RestController
@RequestMapping(“/product”)
public class ProductController {
@Autowired
private ProductService productService;
@Autowired
private ProductCategoryService productCategoryService;
/**

 * 获取商品列表(给订单服务使用)
 * @param productIdList
 * @return
 */
@GetMapping("/listForOrder")
public List<ProductInfo> listForOrder(@RequestBody List<String> productIdList){
    return productService.findList(productIdList);
}

}

1
2
3
4
5
6
##### 6.2 Order服务中调用商品列表
在client的ProductClient添加调用product的方法
###### 1.Order-->ProductClient

@FeignClient(name = “product”)
public interface ProductClient {
@GetMapping(“/listForOrder”)
List listForOrder(@RequestBody List productIdList);
}

1
2
###### 2.Order-->ClientController

@RestController
@Slf4j
public class ClientController {
@Autowired
private ProductClient productClient;
@GetMapping(“/getProductMsg”)
public String getProductMsg(){
String msg = productClient.productMsg();
log.info(“getProductMsg()”,msg);
return msg;
}
@GetMapping(“/getProductList”)
public String getProductList(){
List productInfos = productClient.listForOrder(Arrays.asList(“157875196366160022”));
return “ok”;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
###### 3.Order-->请求测试
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/16.png)
#### 7.扣库存(Feign)
我们已经获取了商品列表,现在开始在Product微服务实现扣库存
![](https://raw.githubusercontent.com/startshineye/img/master/2019/05/17.png)
从创建订单接口我们知道,前端会传过来商品的信息和购买数量:我们在DTO中构造一个对象:CartDTO用来传送上面的对象
1. 1.Product-->CartDTO对象
新建DTO包,新建CartDTO类:

public class CartDTO {
/**

  • 商品id
    /
    private String productId;
    /*
    • 商品数量
      */
      private Integer productQuantity;
      public CartDTO(){
      }
      public CartDTO(String productId,Integer productQuantity){
      this.productId = productId;
      this.productQuantity = productQuantity;
      }
      }
      1
      2
      3
      2. 2.Product-->Exception自定义异常

public class ProductException extends RuntimeException {
private Integer code;
public ProductException(Integer code,String message){
super(message);
this.code =code;
}
public ProductException(ResultEnum resultEnum){
super(resultEnum.getMessage());
this.code = resultEnum.getCode();
}
}

1
2
3
4
3. 3.Product-->返回枚举

public enum ResultEnum {
PRODUCT_NOT_EXIST(1,”商品不存在”),
PRODUCT_STOCK_ERROE(2,”商品库存错误” );
private Integer code;
private String message;
ResultEnum(Integer code, String message) {
this.code = code;
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
4. 4.Product-->Service里面新建方法
```
@Override
@Transactional(rollbackFor = Exception.class)
public void decreaseStock(List<CartDTO> cartDTOList) {
/**
* 遍历:查看是否存在
*/
for (CartDTO cartDTO:cartDTOList){
Optional<ProductInfo> productInfoOptional = repository.findById(cartDTO.getProductId());
//商品不存在
if(!productInfoOptional.isPresent()){
throw new ProductException(ResultEnum.PRODUCT_NOT_EXIST);
}
//商品存在-库存错误
ProductInfo productInfo = productInfoOptional.get();
int result = productInfo.getProductStock() - cartDTO.getProductQuantity();
if(result<0){
throw new ProductException(ResultEnum.PRODUCT_STOCK_ERROE);
}
//保存
productInfo.setProductStock(result);
repository.save(productInfo);
}
}

  1. 5.Product–>controller
1
2
3
4
5
6
7
8
9
10
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping("/decreaseStock")
public void decreaseStock(@RequestBody List<CartDTO> cartDTOList){
productService.decreaseStock(cartDTOList);
}
}
  1. 6.在Order服务中client里面的ProductClient添加:减库存方法
1
2
3
4
5
6
7
@FeignClient(name = "product")
public interface ProductClient {
@GetMapping("/msg")
String productMsg();
@PostMapping("/product/decreaseStock")
void decreaseStock(@RequestBody List<CartDTO> cartDTOList);
}
  1. 7.Order服务中:ClientController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@Slf4j
public class ClientController {
@Autowired
private ProductClient productClient;
@GetMapping("/getProductMsg")
public String getProductMsg(){
String msg = productClient.productMsg();
log.info("getProductMsg()",msg);
return msg;
}
@GetMapping("/getProductList")
public String getProductList(){
List<ProductInfo> productInfos = productClient.listForOrder(Arrays.asList("157875196366160022"));
return productInfos.toString();
}
@GetMapping("/productDecreaseStock")
public String productDecreaseStock(){
productClient.decreaseStock(Arrays.asList(new CartDTO("157875196366160022",1)));
return "ok";
}
}
  1. 8.测试

    库存减少了

8.整合接口打通下单流程(Feign)

之前我们已经完善了,商品的查询,扣库存,那么我们这一节就要完成整套业务的打通。我们之前在Order服务中,创建订单时候,里面还有需要实现的如下几步:

  1. 1.现在我们补充完毕:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private OrderMasterRepository orderMasterRepository;
@Autowired
private OrderDetailRepository orderDetailRepository;
@Autowired
private ProductClient productClient;
@Override
public OrderDTO create(OrderDTO orderDTO) {
String orderId = KeyUtil.genUniqueKey();
//1.TODO 查询商品信息(调用商品服务)
List<String> productIdList = orderDTO.getOrderDetailList().stream().map(OrderDetail::getProductId).collect(Collectors.toList());
List<ProductInfo> productInfoList = productClient.listForOrder(productIdList);
//2.TODO 计算订单总价
//定义总价
BigDecimal orderAmout = new BigDecimal(BigInteger.ZERO);
for(OrderDetail orderDetail:orderDTO.getOrderDetailList()){
//总价=(单价*数量)+总价
Integer productQuantity = orderDetail.getProductQuantity();
for(ProductInfo productInfo:productInfoList){
if(productInfo.getProductId().equals(orderDetail.getProductId())){
orderAmout = productInfo.getProductPrice().multiply(new BigDecimal(productQuantity)).add(orderAmout);
//订单详情赋值
BeanUtils.copyProperties(productInfo,orderDetail);
orderDetail.setOrderId(orderId);
orderDetail.setDetailId(KeyUtil.genUniqueKey());
//订单详情入库
orderDetailRepository.save(orderDetail);
}
}
}
//3.TODO 扣除库存(调用商品服务)
List<CartDTO> cartDTOList = orderDTO.getOrderDetailList().stream()
.map(e -> new CartDTO(e.getProductId(), e.getProductQuantity()))
.collect(Collectors.toList());
productClient.decreaseStock(cartDTOList);
//4. 订单入库
OrderMaster orderMaster = new OrderMaster();
//先设置主键 会copy到orderMaster
orderDTO.setOrderId(orderId);
BeanUtils.copyProperties(orderDTO,orderMaster);
orderMaster.setOrderAmount(orderAmout);
orderMaster.setOrderStatus(OrderStatusEnum.NEW.getCode());
orderMaster.setPayStatus(PayStatusEnum.WAIT.getCode());
orderMasterRepository.save(orderMaster);
return orderDTO;
}
}
  1. 2.测试

9.项目改造成多模块

多模块改造参考:https://blog.csdn.net/yangshangwei/article/details/88809468

9.1 项目缺陷

虽然我们之前已经完成了下单并扣除库存,虽然通信是完成了,但是有些地方做的不好。有如下问题:
1.在Product微服务中,获取商品列表(给订单服务使用)

返回的ProductInfo是数据库实体:

基本上,我们不会把自己数据库映射的表对象暴露给外部的。
2.在订单服务和商品服务之间都有共有的对象:CartDTO,ProductInfo.重复定义。对象属于哪一个服务就在那一个服务中定义。

3.在Order(订单)服务里,定义了ProductClient接口使用Feign主键调用,我们在项目中可能一个服务就是很多人来做,各个服务费透明的,所以我们在Oder服务中不能把Product的服务代码请求路径等写到Order服务中.

9.2 Product多模块拆分
9.2.1模块之间职责

针对解决以上情况,我们把项目分成3个模块:product-server、prodect-client、product-common

  1. 1.product-server:存放所有业务逻辑:包括,controller、service
  2. 2.prodect-client:对外暴露的接口,商品模块对外暴露的接口:商品列表和扣库存。
  3. 3.product-common:公用的对象:既会被内部模块调用,也会被外部模块调用。
    9.2.2模块之间依赖关系
  4. 1.product-common是公用的对象,所以product-server会依赖product-common,返回的商品对象prodect-client也会依赖product-common

  5. 2.product商品多模块划分可以参考:https://blog.csdn.net/qq_29479041/article/details/84230669

  6. 3.在本地打包
    1
    mvn -Dmaven.test.skip=true -U clean install

使用以上命令,会将对应模块打包并安装到本地maven仓库

9.3 Order多模块拆分
  1. 1.以此类推,Order服务也分为3个模块,但是order-client目前是没有代码:其目前不需要给外提供服务 order-common目前也没有代码:目前没有需求
  2. 2.在订单服务需要注意的是:订单服务调用商品服务,那么需要在订单服务里面的启动类中添加扫描到商品服务的路径:@EnableFeignClients(basePackages = “com.yxm.product.client”)
  3. 3.最外层pom文件引入product-client的jar包
  4. 4.是管理jar包版本,不会下载对应依赖
9.5 测试

启动注册中心,通过product-server和order-server中的main函数启动俩微服务

下单

1
2
select * from order_master a where a.order_id = '1558247336963513928'
select * from order_detail a where a.order_id = '1558247336963513928'

源码:github地址:https://github.com/startshineye/SpringCloud_Shell/tree/develop_multimodulw

10.同步或者异步

  1. 1.当前订单服务和商品服务,两个服务之间的通讯机制是同步的。订单会调用商品服务的扣库存接口。微服务中除了同步,有很多时候会集成到异步的场景下,通过队列和订阅主题,实现消息的 发布和订阅,一个微服务可以是消息的发布者,通过异步的方式发送到队列和订阅主题下,作为消费者的微服务,可以从队列或者主题中获取消息。通过消息中间件。把服务间的直接调用解耦。
  2. 比如以下图中:用户登录的时候,用户服务需要调用短信服务发短信。要给用户加积分,需要调用积分服务。还可能有其他服务,如果都采用同步的机制,服务间耦合过大,用户登录成功,需要向多个服务同步响应后才会成功,就会造成不好的用户体验。这个时候,我们通过消息队列可以实现很好的异步调用。
  3. 3.再比如:订单服务在口库存前会调用查询商品服务,之后再调用减库存的接口来扣库存。我们对其改造:商品服务在更改库存的时候、发布库存变化的消息、订单服务来订阅这个消息、可以获取到商品的部分信息,比如:可购买的商品个数、商品id。订单服务在下单的时候不必同步的去查询数据确定商品的库存信息 而是查询自己服务中数据 然后在扣库存时候订单服务发布一个扣库存的消息、商品服务订阅这个消息。拿到消息后,减少本库存消息的库存量。
  4. 4.消息中间件:目前常见的消息队列为:RabbitMQ、Kafka、ActiveMQ,我们现在使用RabbitMQ

11.RabbitMQ的安装

进入下载网址:https://www.rabbitmq.com/download.html
我们使用docker安装RabbitMQ

我们使用带有管理界面的mq:3.8.0-beta.4-management

https://blog.csdn.net/antma/article/details/81334932
https://www.jianshu.com/p/f3e49b495d74

毕业于<br>相信技术可以改变人与人之间的生活<br>码农一枚