微服务实战系列之SpringCloud Alibaba学习(二)

微服务实战系列之SpringCloud Alibaba:

微服务实战系列之SpringCloud Alibaba学习(一)中,学习了搭建三大微服务并完成交互开发与测试。

但在微服务实现过程中的会遇到一些问题:

  1. 就是将用户微服务所在的IP和端口,以及商品微服务所在的IP和端口硬编码到订单微服务的代码中了。这样的做法存在着非常多的问题。这就需要用到服务治理,来实现微服务之间的动态注册与发现。相应的就引出了使用Nacos来实现服务的注册与发现功能。可以参考:SpringCloud架构之Nacos配置注册中心

  2. 但是还存在一个很明显的问题,那就是如果用户微服务和商品微服务在服务器上部署多份的话,之前的程序无法实现服务调用的负载均衡功能。这就需要使用Ribbon来实现服务调用的负载均衡功能。可以参考:SpringCloud架构之Ribbon负载均衡

  3. 现在系统中存在着一个很明显的问题,那就是如果系统的并发量上来后,系统并没有容错的能力,这可能会导致系统不可用或者直接宕机,所以,我们的系统需要支持容错的能力。可以参考:

1. Nacos实现服务治理

1.1 硬编码的问题

如果将用户微服务和商品微服务所在的IP地址和端口号硬编码到订单微服务中,会存在非常多的问题,其中,最明显的问题有三个,如下所示。

(1)如果用户微服务和商品微服务的IP地址或者端口号发生了变化,则订单微服务将变得不可用,需要对同步修改订单微服务中调用用户微服务和商品微服务的IP地址和端口号。

(2)如果系统中提供了多个用户微服务和商品微服务,则无法实现微服务的负载均衡功能。

(3)如果系统需要支持更高的并发,需要部署更多的用户微服务和商品微服务以及订单微服务,如果将用户微服务和商品微服务的IP地址和端口硬编码到订单微服务,则后续的维护会变得异常复杂。

所以,在微服务开发的过程中,需要引入服务治理功能,实现微服务之间的动态注册与发现。

1.2 服务治理

如果系统采用了微服务的架构模式,随着微服务数量的不断增多,服务之间的调用关系会变得纵横交错,以纯人工手动的方式来管理这些微服务以及微服务之间的调用关系是及其复杂的,也是极度不可取的。

所以,需要引入服务治理的功能。服务治理也是在微服务架构模式下的一种最核心和最基本的模块,主要用来实现各个微服务的自动注册与发现。

引入服务治理后,微服务项目总体上可以分为三个大的模块:服务提供者、服务消费者和注册中心,三者的关系如下图所示。

在这里插入图片描述
图片来源:冰河技术,下同。

(1)服务提供者会将自身提供的服务注册到注册中心,并向注册中心发送心跳信息来证明自己还存活,其中,心跳信息中就会包含服务提供者自身提供的服务信息。

(2)注册中心会存储服务提供者上报的信息,并通过服务提供者发送的心跳来更新服务提供者最后的存活时间,如果超过一段时间没有收到服务提供者上报的心跳信息,则注册中心会认为服务提供者不可用,会将对应的服务提供者从服务列表中剔除。

(3)服务消费者会向注册中心订阅自身监听的服务,注册中心会保存服务消费者的信息,也会向服务消费者推送服务提供者的信息。

(4)服务消费者从注册中心获取到服务提供者的信息时,会直接调用服务提供者的接口来实现远程调用。

需要注意的是:服务消费者一般会从注册中心中获取到所有服务提供者的信息,根据具体情况实现对具体服务提供者的实例进行访问。

1.3 注册中心

从上面的分析可以看出,微服务实现服务治理的关键就是引入了注册中心,它是微服务架构模式下一个非常重要的组件,主要实现了服务注册与发现,服务配置和服务的健康检测等功能。

1.3.1 服务注册与发现

(1)服务注册:注册中心提供保存服务提供者和服务消费者的相关信息。

(2)服务发现:也可以理解为服务订阅,服务调用者也就是服务消费者,向注册中心订阅服务提供者的信息,注册中心会向服务消费者推送服务提供者的信息。

1.3.2 服务配置

(1)配置订阅:服务的提供者和消费者都可以向注册中心订阅微服务相关的配置信息。

(2)配置下发:注册中心能够将微服务相关的配置信息主动推送给服务的提供者和消费者。

1.3.3 服务健康检测

注册中心会定期检测存储的服务列表中服务提供者的健康状况,例如服务提供者超过一定的时间没有上报心跳信息,则注册中心会认为对应的服务提供者不可用,就会将服务提供者踢出服务列表。

1.3.4 常见的注册中心

能够实现注册中心功能的组件有很多,但是常用的组件大概包含:Zookeeper、Eureka、Consul、Etcd、Nacos等。这里,就给大家简单介绍下这些能够实现注册中心功能的框架或组件。

(1)Zookeeper

接触过分布式或者大数据开发的小伙伴应该都知道,Zookeeper是Apache Hadoop的一个子项目,它是一个分布式服务治理框架,主要用来解决应用开发中遇到的一些数据管理问题,例如:分布式集群管理、元数据管理、分布式配置管理、状态同步和统一命名管理等。在高并发环境下,也可以通过Zookeeper实现分布式锁功能。

(2)Eureka

Eureka是Netflix开源的SpringCloud中支持服务注册与发现的组件,但是后来闭源了。

(3)Consul

Consul 是 HashiCorp 公司推出的开源产品,用于实现分布式系统的服务发现、服务隔离、服务配置,这些功能中的每一个都可以根据需要单独使用,也可以同时使用所有功能。

(4)Etcd

etcd 是一个高度一致的分布式键值存储,它提供了一种可靠的方式来存储需要由分布式系统或机器集群访问的数据。它可以优雅地处理网络分区期间的领导者选举,即使在领导者节点中也可以容忍机器故障。

(5)Nacos

这里,我们重点说下Nacos。Nacos是阿里巴巴开源的一款更易于构建云原生应用的支持动态服务发现、配置管理和服务管理的平台,其提供了一组简单易用的特性集,能够快速实现动态服务发现、服务配置、服务元数据及流量管理,主要如下所示。

  • 服务注册:Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如IP地址、端口等信 息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内存Map中。
  • 服务心跳:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。
  • 服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)
  • 服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请求给Nacos Server,获取上面注册的服务清 单,并且缓存在Nacos Client本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地存。
  • 服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。

这里,我们选用的注册中心就是阿里巴巴开源的Nacos。

1.4 搭建Nacos环境
1.4.1 Nacos环境配置与启动

(1)https://github.com/alibaba/nacos/releases 下载Nacos的安装包。

(2)解压Nacos安装包,并在命令行进入到Nacos的bin目录下执行如下命令以单机的方式启动Nacos。

startup.cmd -m standalone

(3)启动Nacos之后,在浏览器中输入链接http://localhost:8848/nacos 来访问Nacos的管理界面,默认的用户名和密码都是Nacos。

输入用户名和密码进入Nacos的管理界面,如下所示。
在这里插入图片描述
进入到Nacos的服务管理-服务列表菜单下,如下所示。
在这里插入图片描述
可以看到,在Nacos的服务管理-服务列表菜单下还没有任何服务,接下来,我们就对项目的代码进行改造。

1.4.2 集成Nacos注册中心

引入Nacos注册中心时,我们需要对项目的代码进行一定的改造,以便利用Nacos实现服务的注册与发现功能。

改造用户微服务

(1)在用户微服务的pom.xml文件中添加nacos的服务注册与发现依赖,如下所示。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

(2)在用户微服务的resources目录下的application.yml文件中添加Nacos注册中心的服务地址配置,如下所示。

spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848

(3)在用户微服务的启动类io.binghe.shop#UserStarter上标注@EnableDiscoveryClient注解,如下所示。

/**
 * @description 启动用户服的类
 */
@SpringBootApplication
@EnableTransactionManagement(proxyTargetClass = true)
@MapperScan(value = { "io.binghe.shop.user.mapper" })
@EnableDiscoveryClient
public class UserStarter {

    public static void main(String[] args){
        SpringApplication.run(UserStarter.class, args);
    }
}

此时,就完成了对用户微服务的代码改造。

(4)启动用户微服务,并刷新Nacos页面,如下所示。
在这里插入图片描述
可以看到,用户微服务已经成功注册到Nacos中。

改造其他微服务

我们可以用同样的方式来改造商品微服务和订单微服务的代码,改造好之后,分别启动商品微服务和订单微服务,并再次刷新Nacos的页面,如下所示。
在这里插入图片描述
可以看到,用户微服务、商品微服务和订单微服务都已成功注册到Nacos。

实现服务发现

按照整个项目的执行流程,用户执行下单操作时,订单微服务会调用用户微服务的接口获取用户的基本信息,会调用商品微服务的接口获取商品的基本信息。

在订单微服务中校验用户的合法性和校验商品库存是否充足,如果用户合法并且商品库存充足,就会向订单数据表中记录订单信息并调用商品微服务的接口来扣减商品的库存。

用户微服务和商品微服务作为服务的提供者,而订单微服务作为服务的消费者,如果要实现服务的发现功能,我们还需要对订单微服务的代码进行改造。将订单微服务中硬编码的用户微服务和商品微服务的IP地址和端口号修改成从Nacos中获取。

为了让小伙伴们能够更好的对比修改前和修改后的代码,这里,并没有在订单微服务的 io.binghe.shop.order.service.impl#OrderServiceImpl 类上直接修改,还是将其重命名为 io.binghe.shop.order.service.impl.OrderServiceV1Impl 类,同时,再次将其复制一份并命名为io.binghe.shop.order.service.impl.OrderServiceV2Impl类,在后续的开发过程中,如果涉及到大的代码变动,都会以这种方式进行更新。

注入服务发现类

(1)在io.binghe.shop.order.service.impl.OrderServiceV2Impl 类中首先注入DiscoveryClient类的对象,如下所示。

@Autowired
private DiscoveryClient discoveryClient;

创建动态服务地址方法

在io.binghe.shop.order.service.impl.OrderServiceV2Impl 类中创建一个从Nacos中通过服务名称获取IP和端口号的方法getServiceUrl(),并在getServiceUrl()方法中将IP和端口号拼接成IP:PORT的形式,如下所示。

private String getServiceUrl(String serviceName){
    ServiceInstance serviceInstance = discoveryClient.getInstances(serviceName).get(0);
    return serviceInstance.getHost() + ":" + serviceInstance.getPort();
}

具体的实现方式就是调用DiscoveryClient对象的getInstances()方法,并传入服务的名称,从Nacos注册中心中获取一个ServiceInstance类型的List集合,从List集合中获取第1个元素,也就是从List集合中获取到一个ServiceInstance对象,从ServiceInstance对象中获取到IP地址和端口号,并将其拼接成IP:PORT的形式。

定义服务提供者名称

在io.binghe.shop.order.service.impl.OrderServiceV2Impl 类中定义两个成员变量userServer和productServer,表示用户微服务和商品微服务的服务名称,并将其分别复制为server-user和server-product。

private String userServer = "server-user";
private String productServer = "server-product";

注意:userServer的值需要与用户微服务下的application.yml文件中的如下配置的值相同。

spring:
  application:
    name: server-user

productServer的值需要与商品微服务下的application.yml文件中的如下配置的值相同。

spring:
  application:
    name: server-product

修改提交订单逻辑

在io.binghe.shop.order.service.impl.OrderServiceV2Impl 类的saveOrder()方法中,将硬编码的用户微服务和商品微服务的IP和端口修改成从Nacos注册中心中获取,涉及改动的代码片段如下所示。

(1)添加获取用户微服务与商品微服务的IP和端口号的代码片段,如下所示。

//从Nacos服务中获取用户服务与商品服务的地址
String userUrl = this.getServiceUrl(userServer);
String productUrl = this.getServiceUrl(productServer);

(2)修改使用restTemplate获取用户信息的代码片段,修改前的代码片段如下所示。

User user = restTemplate.getForObject("http://localhost:8060/user/get/" + orderParams.getUserId(), User.class);
if (user == null){
    throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
}

修改后的代码片段如下所示。

User user = restTemplate.getForObject("http://" + userUrl + "/user/get/" + orderParams.getUserId(), User.class);
if (user == null){
    throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
}

可以看到,订单微服务获取用户微服务信息时,不再是硬编码用户微服务的IP地址和端口号了。

(3)修改使用restTemplate获取商品信息的代码片段,修改前的代码片段如下所示。

Product product = restTemplate.getForObject("http://localhost:8070/product/get/" + orderParams.getProductId(), Product.class);
if (product == null){
    throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
}

修改后的代码片段如下所示。

Product product = restTemplate.getForObject("http://" + productUrl + "/product/get/" + orderParams.getProductId(), Product.class);
if (product == null){
    throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
}

可以看到,订单微服务获取商品微服务信息时,不再是硬编码商品微服务的IP地址和端口号了。

(4)修改使用restTemplate扣减商品库存的代码片段,修改前的代码片段如下所示。

Result<Integer> result = restTemplate.getForObject("http://localhost:8070/product/update_count/" + orderParams.getProductId() + "/" + orderParams.getCount(), Result.class);
if (result.getCode() != HttpCode.SUCCESS){
    throw new RuntimeException("库存扣减失败");
}

修改后的代码片段如下所示。

Result<Integer> result = restTemplate.getForObject("http://" + productUrl + "/product/update_count/" + orderParams.getProductId() + "/" + orderParams.getCount(), Result.class);
if (result.getCode() != HttpCode.SUCCESS){
    throw new RuntimeException("库存扣减失败");
}

可以看到,订单微服务调用商品微服务的扣减商品库存接口时,不再是硬编码商品微服务的IP地址和端口号了。

注意:修改后的io.binghe.shop.order.service.impl.OrderServiceV2Impl 类的完整源码,小伙伴们可自行查看项目代码,冰河在这里不再赘述。

至此,整个项目就改造完成了。

2. Ribbon实现负载均衡

2.1 负载均衡

在正式优化程序代码之前,我们先来看看什么是负载均衡。说的直白点,负载均衡就是将原本由一台服务器处理的请求根据一定的规则分担到多台服务器上进行处理。目前,大部分系统都实现了负载均衡的功能。

负载均衡根据发生的位置,可以分为服务端负载均衡客户端负载均衡

2.1.1 服务端负载均衡

服务端负载均衡指的是在服务端处理负载均衡的逻辑,如下图所示。

在这里插入图片描述
图片来源:冰河技术,下同。

负载均衡在服务端进行处理,当客户端访问服务端的服务A时,首先访问到服务端的负载均衡器,由服务端的负载均衡器将客户端的请求均匀的分发到服务端部署的两个服务A上。

2.1.2 客户端负载均衡

客户端负载均衡指的是在客户端处理负载均衡的逻辑,如下图所示。
在这里插入图片描述
负载均衡逻辑在客户端一侧,客户端应用调用服务端的应用A时,在向服务端发送请求时,就已经经过负载均衡的逻辑处理,并直接向服务端的某个服务发送请求。

2.2 启动多服务

为了实现服务调用的负载均衡功能,我们在本地的IDEA环境中分别启动两个用户微服务进程和两个商品微服务进程。

2.2.1 启动多个用户微服务

这里,我们在IDEA开发环境中启动多个用户微服务,其实也就是在同一台机器(服务器)上启动多个用户微服务。启动用户微服务时,默认监听的端口为8060,主要由如下配置决定。

server:
  port: 8060

在同一台机器(服务器)上启动多个用户微服务时,只需要保证启动的多个用户微服务监听的端口号不同即可。

IDEA中可以通过配置不同的端口号来启动多个相同的服务,如下所示,再配置一个用户微服务,使其端口号为8061。

在这里插入图片描述
按照上图所示,在Name一栏输入UserStarter2,点击Main class一栏后面的弹出框按钮,选择用户微服务的启动类io.binghe.shop.UserStarter,最重要的就是在VM options一栏后面添加JVM启动参数-Dserver.port=8061,将新添加的用户微服务的监听端口设置为8061。

配置好之后,在IDEA中分别启动端口为8060和8061的两个用户微服务,启动后打开Nacos的服务列表,如下所示。

在这里插入图片描述
可以看到,在服务列表中出现了两个用户微服务的实例,说明两个用户微服务都启动成功了。

2.2.2 启动多个商品微服务

与用户微服务类似,在IDEA中再次配置一个端口号为8071的商品微服务,如下所示。

在这里插入图片描述接下来,分别启动端口为8070和8071的两个商品微服务,启动后查看Nacos的服务列表,如下所示。

在这里插入图片描述
可以看到,端口为8070和8071的两个商品微服务,也成功启动啦。

2.3 实现自定义负载均衡

过修改订单微服务的代码来实现自定义负载均衡,由于在整个项目中,订单微服务作为客户端存在,由订单微服务调用用户微服务和商品微服务,所以,这里采用的是客户端负载均衡的模式。

2.3.1 修改订单微服务代码

此处实现的逻辑在订单微服务的io.binghe.shop.order.service.impl.OrderServiceV3Impl类中,并且在OrderServiceV3Impl类上会标注@Service(“orderServiceV3”)注解。订单微服务的代码结构如下所示。

├─shop-order
│  │  pom.xml
│  │  shop-order.iml
│  │
│  └─src
│      └─main
│          ├─java
│          │  └─io
│          │      └─binghe
│          │          └─shop
│          │              │  OrderStarter.java
│          │              │
│          │              └─order
│          │                  ├─config
│          │                  │      LoadBalanceConfig.java
│          │                  │
│          │                  ├─controller
│          │                  │      OrderController.java
│          │                  │
│          │                  ├─mapper
│          │                  │      OrderItemMapper.java
│          │                  │      OrderMapper.java
│          │                  │
│          │                  └─service
│          │                      │  OrderService.java
│          │                      │
│          │                      └─impl
│          │                              OrderServiceV1Impl.java
│          │                              OrderServiceV2Impl.java
│          │                              OrderServiceV3Impl.java
│          │
│          └─resources
│              │  application.yml
│              │
│              └─mapper
│                      OrderItemMapper.xml
│                      OrderMapper.xml

首先,在OrderServiceV3Impl类中修改getServiceUrl()方法,使其能够在多个服务地址中随机获取一个服务地址,而不总是获取第一个服务地址。

修改前的代码如下所示。

private String getServiceUrl(String serviceName){
    ServiceInstance serviceInstance = discoveryClient.getInstances(serviceName).get(0);
    return serviceInstance.getHost() + ":" + serviceInstance.getPort();
}

修改后的代码如下所示。

private String getServiceUrl(String serviceName){
    List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
    int index = new Random().nextInt(instances.size());
    ServiceInstance serviceInstance = instances.get(index);
    String url = serviceInstance.getHost() + ":" + serviceInstance.getPort();
    log.info("负载均衡后的服务地址为:{}", url);
    return url;
}

在getServiceUrl()方法中,首先通过服务的名称在Nacos中获取到所有的服务实例列表,然后使用随机函数随机生成一个0到服务列表长度减1的整数,而这个随机生成的整数恰好在服务实例列表的下标范围内,然后以这个整数作为下标获取服务列表中的某个服务实例。从而以随机的方式实现了负载均衡,并从获取到的服务实例中获取到服务实例所在服务器的IP地址和端口号,拼接成IP:PORT的形式返回。

接下来,将io.binghe.shop.order.controller.OrderController类中的OrderService修改为注入beanName为orderServiceV3的OrderService对象,如下所示。

@Autowired
@Qualifier(value = "orderServiceV3")
private OrderService orderService;

至此,订单微服务的代码修改完成。

2.3.2 测试负载均衡效果

启动订单微服务,并在浏览器或其他测试工具中多次访问链接http://localhost:8080/order/submit_order?userId=1001&productId=1001&count=1,在订单微服务输出的日志信息中会存在如下所示的日志。

负载均衡后的服务地址为:192.168.0.27:8061
负载均衡后的服务地址为:192.168.0.27:8071
负载均衡后的服务地址为:192.168.0.27:8060
负载均衡后的服务地址为:192.168.0.27:8070
负载均衡后的服务地址为:192.168.0.27:8060
负载均衡后的服务地址为:192.168.0.27:8071
负载均衡后的服务地址为:192.168.0.27:8061
负载均衡后的服务地址为:192.168.0.27:8070

其中,端口为8060和8061的微服务为用户微服务,端口为8070和8071的微服务为商品微服务。初步实现了订单微服务调用用户微服务和商品微服务的负载均衡。

2.4 使用Ribbon实现负载均衡

Ribbon是SpringCloud提供的一个能够实现负载均衡的组件,使用Ribbon能够轻松实现微服务之间调用的负载均衡。

2.4.1 修改订单微服务代码

在订单微服务中的io.binghe.shop.order.config.LoadBalanceConfig 类的 restTemplate()方法上添加@LoadBalanced注解,如下所示。

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

接下来,实现的逻辑在订单微服务的io.binghe.shop.order.service.impl.OrderServiceV4Impl类中,并且在OrderServiceV4Impl类上会标注@Service(“orderServiceV4”)注解。订单微服务的代码结构如下所示。

├─shop-order
│  │  pom.xml
│  │  shop-order.iml
│  │
│  └─src
│      └─main
│          ├─java
│          │  └─io
│          │      └─binghe
│          │          └─shop
│          │              │  OrderStarter.java
│          │              │
│          │              └─order
│          │                  ├─config
│          │                  │      LoadBalanceConfig.java
│          │                  │
│          │                  ├─controller
│          │                  │      OrderController.java
│          │                  │
│          │                  ├─mapper
│          │                  │      OrderItemMapper.java
│          │                  │      OrderMapper.java
│          │                  │
│          │                  └─service
│          │                      │  OrderService.java
│          │                      │
│          │                      └─impl
│          │                              OrderServiceV1Impl.java
│          │                              OrderServiceV2Impl.java
│          │                              OrderServiceV3Impl.java
│          │                              OrderServiceV4Impl.java
│          │
│          └─resources
│              │  application.yml
│              │
│              └─mapper
│                      OrderItemMapper.xml
│                      OrderMapper.xml

在OrderServiceV4Impl类中删除如下代码。

@Autowired
private DiscoveryClient discoveryClient;

private String getServiceUrl(String serviceName){
    List<ServiceInstance> instances = discoveryClient.getInstances(serviceName);
    int index = new Random().nextInt(instances.size());
    ServiceInstance serviceInstance = instances.get(index);
    String url = serviceInstance.getHost() + ":" + serviceInstance.getPort();
    log.info("负载均衡后的服务地址为:{}", url);
    return url;
}

在saveOrder()方法中删除如下两行代码。

//从Nacos服务中获取用户服务与商品服务的地址
String userUrl = this.getServiceUrl(userServer);
String productUrl = this.getServiceUrl(productServer);

修改通过restTemplate调用用户微服务和商品微服务的方法。修改前的代码如下所示。

User user = restTemplate.getForObject("http://" + userUrl + "/user/get/" + orderParams.getUserId(), User.class);
if (user == null){
    throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
}
Product product = restTemplate.getForObject("http://" + productUrl + "/product/get/" + orderParams.getProductId(), Product.class);
if (product == null){
    throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
}

//#####################此处省略N行代码#########################

Result<Integer> result = restTemplate.getForObject("http://" + productUrl + "/product/update_count/" + orderParams.getProductId() + "/" + orderParams.getCount(), Result.class);
if (result.getCode() != HttpCode.SUCCESS){
    throw new RuntimeException("库存扣减失败");
}

修改后的代码如下所示。

User user = restTemplate.getForObject("http://" + userServer + "/user/get/" + orderParams.getUserId(), User.class);
if (user == null){
    throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
}
Product product = restTemplate.getForObject("http://" + productServer + "/product/get/" + orderParams.getProductId(), Product.class);
if (product == null){
    throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
}

//#####################此处省略N行代码#########################

Result<Integer> result = restTemplate.getForObject("http://" + productServer + "/product/update_count/" + orderParams.getProductId() + "/" + orderParams.getCount(), Result.class);
if (result.getCode() != HttpCode.SUCCESS){
    throw new RuntimeException("库存扣减失败");
}

接下来,将io.binghe.shop.order.controller.OrderController类中的OrderService修改为注入beanName为orderServiceV4的OrderService对象,如下所示。

@Autowired
@Qualifier(value = "orderServiceV4")
private OrderService orderService;

至此,订单微服务的代码修改完成。

2.4.2 测试负载均衡效果

启动订单微服务,并在浏览器或其他测试工具中多次访问链接http://localhost:8080/order/submit_order?userId=1001&productId=1001&count=1,启动的每个用户微服务和商品微服务都会打印相关的日志,说明使用Ribbon实现了负载均衡功能。

注意:这里就不粘贴测试时每个启动的微服务打印的日志了,小伙伴们可自行测试并演示效果。

2.5 使用Fegin实现负载均衡

Fegin是SpringCloud提供的一个HTTP客户端,但只是一个声明式的伪客户端,它能够使远程调用和本地调用一样简单。阿里巴巴开源的Nacos能够兼容Ribbon,而Fegin又集成了Ribbon,所以,使用Fegin也能够实现负载均衡

2.5.1 修改订单微服务代码

(1)在订单微服务的pom.xml文件中添加Fegin相关的依赖,如下所示。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

(2)在订单微服务的启动类io.binghe.shop.OrderStarter 上添加 @EnableFeignClients 注解,如下所示。

/**
 * @description 订单服务启动类
 */
@SpringBootApplication
@EnableTransactionManagement(proxyTargetClass = true)
@MapperScan(value = { "io.binghe.shop.order.mapper" })
@EnableDiscoveryClient
@EnableFeignClients
public class OrderStarter {
    public static void main(String[] args){
        SpringApplication.run(OrderStarter.class, args);
    }
}

(3)在订单微服务工程shop-order下新建io.binghe.shop.order.fegin包,并在io.binghe.shop.order.fegin下新建UserService接口,并在UserService接口上标注@FeignClient("server-user")注解,其中注解的value属性为用户微服务的服务名称。UserService接口用来远程调用用户微服务的接口,源码如下所示。

/**
 * @description 调用用户微服务的接口
 */
@FeignClient("server-user")
public interface UserService {
    @GetMapping(value = "/user/get/{uid}")
    User getUser(@PathVariable("uid") Long uid);
}

(4)在io.binghe.shop.order.fegin下新建ProductService接口,并在ProductService接口上标注@FeignClient("server-product")注解,其中注解的value属性为商品微服务的服务名称。ProductService接口用来远程调用商品微服务的接口,源码如下所示。

/**
 * @description 调用商品微服务的接口
 */
@FeignClient("server-product")
public interface ProductService {

    /**
     * 获取商品信息
     */
    @GetMapping(value = "/product/get/{pid}")
    Product getProduct(@PathVariable("pid") Long pid);

    /**
     * 更新库存数量
     */
    @GetMapping(value = "/product/update_count/{pid}/{count}")
    Result<Integer> updateCount(@PathVariable("pid") Long pid, @PathVariable("count") Integer count);
}

(5)接下来,实现的逻辑在订单微服务的io.binghe.shop.order.service.impl.OrderServiceV5Impl类中,并且在OrderServiceV5Impl类上会标注@Service("orderServiceV5")注解。订单微服务的代码结构如下所示。

├─shop-order
│  │  pom.xml
│  │  shop-order.iml
│  │
│  ├─src
│  │  └─main
│  │      ├─java
│  │      │  └─io
│  │      │      └─binghe
│  │      │          └─shop
│  │      │              │  OrderStarter.java
│  │      │              │
│  │      │              └─order
│  │      │                  ├─config
│  │      │                  │      LoadBalanceConfig.java
│  │      │                  │
│  │      │                  ├─controller
│  │      │                  │      OrderController.java
│  │      │                  │
│  │      │                  ├─fegin
│  │      │                  │      ProductService.java
│  │      │                  │      UserService.java
│  │      │                  │
│  │      │                  ├─mapper
│  │      │                  │      OrderItemMapper.java
│  │      │                  │      OrderMapper.java
│  │      │                  │
│  │      │                  └─service
│  │      │                      │  OrderService.java
│  │      │                      │
│  │      │                      └─impl
│  │      │                              OrderServiceV1Impl.java
│  │      │                              OrderServiceV2Impl.java
│  │      │                              OrderServiceV3Impl.java
│  │      │                              OrderServiceV4Impl.java
│  │      │                              OrderServiceV5Impl.java
│  │      │
│  │      └─resources
│  │          │  application.yml
│  │          │
│  │          └─mapper
│  │                  OrderItemMapper.xml
│  │                  OrderMapper.xml

修改OrderServiceV5Impl类的代码,修改前的代码如下所示。

@Autowired
private RestTemplate restTemplate;

private String userServer = "server-user";
private String productServer = "server-product";

修改后的代码如下所示。

@Autowired
private UserService userService;
@Autowired
private ProductService productService;

修改OrderServiceV5Impl类中saveOrder()的代码,修改前的代码如下所示。

User user = restTemplate.getForObject("http://" + userServer + "/user/get/" + orderParams.getUserId(), User.class);
if (user == null){
    throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
}
Product product = restTemplate.getForObject("http://" + productServer + "/product/get/" + orderParams.getProductId(), Product.class);
if (product == null){
    throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
}

//##################此处省略N行代码########################

Result<Integer> result = restTemplate.getForObject("http://" + productServer + "/product/update_count/" + orderParams.getProductId() + "/" + orderParams.getCount(), Result.class);
if (result.getCode() != HttpCode.SUCCESS){
    throw new RuntimeException("库存扣减失败");
}

修改后的代码如下所示。

User user = userService.getUser(orderParams.getUserId());
if (user == null){
    throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
}
Product product = productService.getProduct(orderParams.getProductId());
if (product == null){
    throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
}

//##################此处省略N行代码########################

Result<Integer> result = productService.updateCount(orderParams.getProductId(), orderParams.getCount());
if (result.getCode() != HttpCode.SUCCESS){
    throw new RuntimeException("库存扣减失败");
}

可以看到,修改后的代码使用Fegin调用远程的用户微服务和商品微服务,已经完全没有了拼接URL路径的痕迹

接下来,将io.binghe.shop.order.controller.OrderController类中的OrderService修改为注入beanName为orderServiceV5的OrderService对象,如下所示。

@Autowired
@Qualifier(value = "orderServiceV5")
private OrderService orderService;

至此,订单微服务的代码修改完成。

2.5.2 测试负载均衡效果

启动订单微服务,并在浏览器或其他测试工具中多次访问链接http://localhost:8080/order/submit_order?userId=1001&productId=1001&count=1,启动的每个用户微服务和商品微服务都会打印相关的日志,说明使用Fegin实现了负载均衡功能。

3. 服务容错:服务雪崩与容错方案

3.1 并发对系统的影响

当一个系统的架构设计采用微服务架构模式时,会将庞大而复杂的业务拆分成一个个小的微服务,各个微服务之间以接口或者RPC的形式进行互相调用。在调用的过程中,就会涉及到网路的问题,再加上微服务自身的原因,例如很难做到100%的高可用等。如果众多微服务当中的某个或某些微服务出现问题,不可用或者宕机了,那么其他微服务调用这些微服务的接口时就会出现延迟。如果此时有大量请求进入系统,就会造成请求任务的大量堆积,甚至会造成整体服务的瘫痪。

3.1.1 压测说明

为了更加直观的说明当系统没有容错能力时,高并发、大流量场景对于系统的影响,我们在这里模拟一个并发的场景。在订单微服务shop-order的io.binghe.shop.order.controller.OrderController类中新增一个concurrentRequest()方法,源码如下所示。

@GetMapping(value = "/concurrent_request")
public String concurrentRequest(){
    log.info("测试业务在高并发场景下是否存在问题");
    return "binghe";
}

接下来,为了更好的演示效果,我们限制下Tomcat处理请求的最大并发数,在订单微服务shop-order的resources目录下的application.yml文件中添加如下配置。

server:
  port: 8080
  tomcat:
    max-threads: 20

限制Tomcat一次最多只能处理20个请求。接下来,我们就使用JMeter对 http://localhost:8080/order/submit_order 接口进行压测,由于订单微服务中没有做任何的容错处理,当对 http://localhost:8080/order/submit_order 接口的请求压力过大时,我们再访问http://localhost:8080/order/concurrent_request 接口时,会发现http://localhost:8080/order/concurrent_request 接口会受到并发请求的影响,访问很慢甚至根本访问不到。

3.1.2 压测实战

使用JMeterhttp://localhost:8080/order/submit_order 接口进行压测,JMeter的配置过程如下所示。

(1)打开JMeter的主界面,如下所示。
在这里插入图片描述
(2)在JMeter中右键测试计划添加线程组,如下所示。
在这里插入图片描述(3)在JMeter中的线程组中配置并发线程数,如下所示。
在这里插入图片描述如上图所示,将线程数配置成50,Ramp-Up时间配置成0,循环次数为100。表示JMeter每次会在同一时刻向系统发送50个请求,发送100次为止。

(4)在JMeter中右键线程组添加HTTP请求,如下所示。
在这里插入图片描述
(5)在JMeter中配置HTTP请求,如下所示。
在这里插入图片描述具体配置如下所示。

  • 协议:http
  • 服务器名称或IP:localhost
  • 端口号:8080
  • 方法:GET
  • 路径:/order/submit_order?userId=1001&productId=1001&count=1
  • 内容编码:UTF-8

(6)配置好JMeter后,点击JMeter上的绿色小三角开始压测,如下所示。
在这里插入图片描述
点击后会弹出需要保存JMeter脚本的弹出框,根据实际需要点击保存即可。
在这里插入图片描述
点击保存后,开始对 http://localhost:8080/order/submit_order 接口进行压测,在压测的过程中会发现订单微服务打印日志时,会比较卡顿,同时在浏览器或其他工具中访问http://localhost:8080/order/concurrent_request 接口会卡顿,甚至根本访问不到。

说明订单微服务中的某个接口一旦访问的并发量过高,其他接口也会受到影响,进而导致订单微服务整体不可用。为了说明这个问题,我们再来看看服务雪崩是个什么鬼。

3.2 服务雪崩

系统采用分布式或微服务的架构模式后,由于网络或者服务自身的问题,一般服务是很难做到100%高可用的。如果一个服务出现问题,就可能会导致其他的服务级联出现问题,这种故障性问题会在整个系统中不断扩散,进而导致服务不可用,甚至宕机,最终会对整个系统造成灾难性后果。

为了最大程度的预防服务雪崩,组成整体系统的各个微服务需要支持服务容错的功能。

3.3 服务容错方案

服务容错在一定程度上就是尽最大努力来兼容错误情况的发生,因为在分布式和微服务环境中,不可避免的会出现一些异常情况,我们在设计分布式和微服务系统时,就要考虑到这些异常情况的发生,使得系统具备服务容错能力。

常见的服务错误方案包含:服务限流、服务隔离、服务超时、服务熔断和服务降级等。

3.3.1 服务限流

服务限流就是限制进入系统的流量,以防止进入系统的流量过大而压垮系统。其主要的作用就是保护服务节点或者集群后面的数据节点,防止瞬时流量过大使服务和数据崩溃(如前端缓存大量实效),造成不可用;还可用于平滑请求。

限流算法有两种:

  • 一种就是简单的请求总量计数,
  • 一种就是时间窗口限流(一般为1s),如令牌桶算法和漏牌桶算法就是时间窗口的限流算法。
3.3.2 服务隔离

服务隔离有点类似于系统的垂直拆分,就按照一定的规则将系统划分成多个服务模块,并且每个服务模块之间是互相独立的,不会存在强依赖的关系。如果某个拆分后的服务发生故障后,能够将故障产生的影响限制在某个具体的服务内,不会向其他服务扩散,自然也就不会对整体服务产生致命的影响。

互联网行业常用的服务隔离方式有:线程池隔离和信号量隔离

3.3.3 服务超时

整个系统采用分布式和微服务架构后,系统被拆分成一个个小服务,就会存在服务与服务之间互相调用的现象,从而形成一个个调用链。形成调用链关系的两个服务中,主动调用其他服务接口的服务处于调用链的上游,提供接口供其他服务调用的服务处于调用链的下游。

服务超时就是在上游服务调用下游服务时,设置一个最大响应时间,如果超过这个最大响应时间下游服务还未返回结果,则断开上游服务与下游服务之间的请求连接,释放资源。

3.3.4 服务熔断

在分布式与微服务系统中,如果下游服务因为访问压力过大导致响应很慢或者一直调用失败时,上游服务为了保证系统的整体可用性,会暂时断开与下游服务的调用连接。这种方式就是熔断。

服务熔断一般情况下会有三种状态:关闭、开启和半熔断

  • 关闭状态:服务一切正常,没有故障时,上游服务调用下游服务时,不会有任何限制。
  • 开启状态:上游服务不再调用下游服务的接口,会直接返回上游服务中预定的方法。
  • 半熔断状态:处于开启状态时,上游服务会根据一定的规则,尝试恢复对下游服务的调用。此时,上游服务会以有限的流量来调用下游服务,同时,会监控调用的成功率。如果成功率达到预期,则进入关闭状态。如果未达到预期,会重新进入开启状态。
3.3.5 服务降级

服务降级,说白了就是一种服务托底方案,如果服务无法完成正常的调用流程,就使用默认的托底方案来返回数据。例如,在商品详情页一般都会展示商品的介绍信息,一旦商品详情页系统出现故障无法调用时,会直接获取缓存中的商品介绍信息返回给前端页面。

4. Sentinel实现接口限流

下面将使用Sentinel实现接口的限流,并使用Feign整合Sentinel实现服务容错的功能。

当然,能够实现服务容错功能的组件不仅仅有Sentinel,比如:Hystrix和Resilience4J也能够实现服务容错的目的。

4.1 关于Sentinel

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

4.1.1 Sentinel的特征
  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
  • 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
4.1.2 Sentinel的主要特性

在这里插入图片描述

4.1.3 Sentinel的开源生态

在这里插入图片描述
Sentinel 分为两个部分:

  • 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
  • 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器

注意:上述内容来自Sentinel官方文档,链接地址为:

https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D

4.2 项目整合Sentinel

在微服务项目中整合Sentinel是非常简单的,只需要在项目的pom.xml文件中引入Sentinel的依赖即可,不过在使用Sentinel时,需要安装Sentinel的控制台。

4.2.1 安装Sentinel控制台

Sentinel 提供一个轻量级的控制台, 它提供机器发现、单机资源实时监控以及规则管理等功能。

(1)到链接

https://github.com/alibaba/Sentinel/releases 

下载Sentinel控制台,这里下载的Sentinel控制台是1.8.4版本。

(2)Sentinel控制台下载完成后,在本地启动Sentinel控制台,如下所示。

java -Dserver.port=8888 -Dcsp.sentinel.dashboard.server=localhost:8888 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.4.jar

小伙伴们如果想在CentOS服务器上以后台进程方式启动Sentinel控制台,可以使用如下命令

nohup java -Dserver.port=8888 -Dcsp.sentinel.dashboard.server=localhost:8888 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.4.jar >> /dev/null &

启动后在浏览器中输入 http://localhost:8888 访问Sentinel控制台,如下所示。
在这里插入图片描述
输入默认的用户名sentinel和密码sentinel,登录Sentinel控制台,如下所示。
在这里插入图片描述至此,Sentinel控制台下载并启动成功。

4.2.2 项目集成Sentinel

(1)在订单微服务的shop-order的pom.xml文件中添加Sentinel的相关依赖,如下所示。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

(2)在订单微服务的shop-order的application.yml文中加入Sentinel相关的配置,如下所示。

spring:
  cloud:
    sentinel:
      transport:
        port: 9999   #指定和Sentinel控制台交互的端口,任意指定一个未使用的端口即可
        dashboard: 127.0.0.1:8888  #Sentinel控制台服务地址

(3)为了让大家直观的感受到Sentinel的功能,这里我们先在订单微服务的io.binghe.shop.order.controller.OrderController类中新增一个测试接口,如下所示。

@GetMapping(value = "/test_sentinel")
public String testSentinel(){
    log.info("测试Sentinel");
    return "sentinel";
}

(4)启动订单微服务,在浏览器中输入http://localhost:8080/order/test_sentinel访问在订单微服务中新增的接口,如下所示。
在这里插入图片描述(5)刷新Sentinel页面,会发现已经显示了订单微服务的菜单,如下所示。
在这里插入图片描述
注意:直接启动订单微服务和Sentinel,会发现Sentinel中没有订单微服务的数据,因为Sentinel是懒加载机制,所以需要访问一下接口,再去访问Sentinel 就有数据了。

至此,订单微服务成功集成了Sentinel。

4.2.3 集成Sentinel限流功能

这里,我们使用Sentinel为http://localhost:8080/order/test_sentinel接口限流,步骤如下所示。

(1)在Sentinel控制台找到server-order下的簇点链路菜单,如下所示。

在这里插入图片描述
(2)在簇点链路列表中找到/test_sentinel,在右侧的操作中选择流控,如下所示。
在这里插入图片描述
点击流控按钮会显示 新增流控规则 的弹出框,如下所示。
在这里插入图片描述
这里,我们在单机阈值后直接填写1,如下所示。
在这里插入图片描述
配置好之后点击新增按钮。上述配置表示http://localhost:8080/order/test_sentinel接口的QPS为1,每秒访问1次。如果每秒访问的次数超过1次,则会被Sentinel限流。

(3)在浏览器上不断刷新http://localhost:8080/order/test_sentinel接口,当每秒中访问的次数超过1次时,会被Sentinel限流,如下所示。
在这里插入图片描述

4.2.4 对提交订单的接口限流

在提交订单的接口 http://localhost:8080/order/submit_order上实现限流,步骤如下。

(1)首先访问下提交订单的接口 http://localhost:8080/order/submit_order,使得Sentinel中能够捕获到提交订单的接口,并点击操作中的流控按钮,如下所示。
在这里插入图片描述这里的注意点还是:直接启动订单微服务和Sentinel,会发现Sentinel中没有订单微服务的数据,因为Sentinel是懒加载机制,所以需要访问一下接口,再去访问Sentinel 就有数据了。

(2)在新增流控规则显示框中的QPS单机阈值设置为1,点击新增按钮,如下所示。
在这里插入图片描述
(3)在浏览器中不断刷新 http://localhost:8080/order/submit_order?userId=1001&productId=1001&count=1 使得每秒访问的频率超过1次,会被Sentinel限流.

至此,项目中集成了Sentinel并使用Sentinel实现了接口的限流。

4.3 Feign整合Sentinel实现容错

之前在项目中集成了Sentinel,并使用Sentinel实现了限流,如果订单微服务的下游服务,比如用户微服务和商品微服务出现故障,无法访问时,那订单微服务该如何实现服务容错呢?使用Sentinel就可以轻松实现。

4.3.1 添加依赖并开启支持

(1)在订单微服务的shop-order的pom.xml文件中添加Sentinel的相关依赖,如下所示。

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

注意:这一步是为了整个案例的完整性加上的,如果小伙伴们按照文章实现了项目整合Sentinel,并在订单微服务的shop-order的pom.xml文件中添加了上述配置,则可忽略此步骤。

(2)在订单微服务的application.yml文件中添加如下配置开启Feign对Sentinel的支持。

feign:
  sentinel:
    enabled: true
4.3.2 为远程调用实现容错

(1)需要在订单微服务shop-order中,为远程调用接口实现容错方法。这里,先为用户微服务实现容错。在订单微服务中新建io.binghe.shop.order.Feign.fallback 包,并在 io.binghe.shop.order.Feign.fallback包下创建UserServiceFallBack类实现UserService接口,用于调用用户微服务的容错类,如下所示。

/**
 * @description 用户服务容错类
 */
@Component
public class UserServiceFallBack implements UserService {
    @Override
    public User getUser(Long uid) {
        User user = new User();
        user.setId(-1L);
        return user;
    }
}

注意:容错类需要实现一个被容错的接口,并实现这个接口的方法。

接下来,在订单微服务的io.binghe.shop.order.Feign.UserService接口上的@FeignClient注解上指定容错类,如下所示。

/**
 * @description 调用用户微服务的接口
 */
@FeignClient(value = "server-user", fallback = UserServiceFallBack.class)
public interface UserService {

    @GetMapping(value = "/user/get/{uid}")
    User getUser(@PathVariable("uid") Long uid);
}

(2)在订单微服务中的 io.binghe.shop.order.Feign.fallback包下创建ProductServiceFallBack类实现ProductService接口,用于调用商品微服务的容错类,如下所示。

/**
 * @description 商品微服务的容错类
 */
@Component
public class ProductServiceFallBack implements ProductService {
    @Override
    public Product getProduct(Long pid) {
        Product product = new Product();
        product.setId(-1L);
        return product;
    }

    @Override
    public Result<Integer> updateCount(Long pid, Integer count) {
        Result<Integer> result = new Result<>();
        result.setCode(1001);
        result.setCodeMsg("触发了容错逻辑");
        return result;
    }
}

接下来,在订单微服务的io.binghe.shop.order.fegin.ProductService接口的@FeignClient注解上指定容错类,如下所示。

/**
 * @description 调用商品微服务的接口
 */
@FeignClient(value = "server-product", fallback = ProductServiceFallBack.class)
public interface ProductService {

    /**
     * 获取商品信息
     */
    @GetMapping(value = "/product/get/{pid}")
    Product getProduct(@PathVariable("pid") Long pid);

    /**
     * 更新库存数量
     */
    @GetMapping(value = "/product/update_count/{pid}/{count}")
    Result<Integer> updateCount(@PathVariable("pid") Long pid, @PathVariable("count") Integer count);
}

(3)修改订单微服务的业务实现类中提交订单的业务方法,这里修改的方法位于io.binghe.shop.order.service.impl.OrderServiceV6Impl类中,同时需要将类上的@Service注解中指定bean的名称为orderServiceV6

@Slf4j
@Service("orderServiceV6")
public class OrderServiceV6Impl implements OrderService {
    //省略所有代码
}

在提交订单的业务方法中,修改前的代码片段如下所示。

User user = userService.getUser(orderParams.getUserId());
if (user == null){
    throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
}
Product product = productService.getProduct(orderParams.getProductId());
if (product == null){
    throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
}
//#####################省略N行代码##########################
Result<Integer> result = productService.updateCount(orderParams.getProductId(), orderParams.getCount());
if (result.getCode() != HttpCode.SUCCESS){
    throw new RuntimeException("库存扣减失败");
}

修改后的代码片段如下所示。

User user = userService.getUser(orderParams.getUserId());
if (user == null){
    throw new RuntimeException("未获取到用户信息: " + JSONObject.toJSONString(orderParams));
}
if (user.getId() == -1){
    throw new RuntimeException("触发了用户微服务的容错逻辑: " + JSONObject.toJSONString(orderParams));
}
Product product = productService.getProduct(orderParams.getProductId());
if (product == null){
    throw new RuntimeException("未获取到商品信息: " + JSONObject.toJSONString(orderParams));
}
if (product.getId() == -1){
    throw new RuntimeException("触发了商品微服务的容错逻辑: " + JSONObject.toJSONString(orderParams));
}
//#####################省略N行代码##########################
Result<Integer> result = productService.updateCount(orderParams.getProductId(), orderParams.getCount());
if (result.getCode() == 1001){
    throw new RuntimeException("触发了商品微服务的容错逻辑: " + JSONObject.toJSONString(orderParams));
}
if (result.getCode() != HttpCode.SUCCESS){
    throw new RuntimeException("库存扣减失败");
}

可以看到,修改后的提交订单的业务方法主要增加了服务容错的判断逻辑。

(4)在io.binghe.shop.order.controller.OrderController中注入bean名称为orderServiceV6OrderService对象,如下所示。

@Autowired
@Qualifier(value = "orderServiceV6")
private OrderService orderService;

至此,我们在项目中使用Sentinel实现了服务容错的功能。

4.3.3 测试服务容错

(1)停掉所有的商品微服务(也就是只启动用户微服务和订单微服务),在浏览器中访问http://localhost:8080/order/submit_order?userId=1001&productId=1001&count=1,结果如下所示。
在这里插入图片描述
返回的原始数据如下所示。

{"code":500,"codeMsg":"执行失败","data":"触发了商品微服务的容错逻辑: {\"count\":1,\"empty\":false,\"productId\":1001,\"userId\":1001}"}

说明停掉所有的商品微服务后,触发了商品微服务的容错逻辑。

(2)停掉所有的用户微服务(也就是只启动商品微服务和订单微服务)在浏览器中访问http://localhost:8080/order/submit_order?userId=1001&productId=1001&count=1,结果如下所示。
在这里插入图片描述
返回的原始数据如下所示。

{"code":500,"codeMsg":"执行失败","data":"触发了用户微服务的容错逻辑: {\"count\":1,\"empty\":false,\"productId\":1001,\"userId\":1001}"}

(3)停掉所有的用户微服务和商品微服务(也就是只启动订单微服务),在浏览器中访问http://localhost:8080/order/submit_order?userId=1001&productId=1001&count=1,结果如下所示。
在这里插入图片描述
返回的原始数据如下所示。

{"code":500,"codeMsg":"执行失败","data":"触发了用户微服务的容错逻辑: {\"count\":1,\"empty\":false,\"productId\":1001,\"userId\":1001}"}

说明项目集成Sentinel成功实现了服务的容错功能。

4.3.4 容错扩展

如果想要在订单微服务中获取到容错时的具体信息时,可以按照如下方式实现容错方案。
实现容错时获取异常

(1)在订单微服务shop-order中新建io.binghe.shop.order.fegin.fallback.factory包,在io.binghe.shop.order.fegin.fallback.factory包中新建UserServiceFallBackFactory类,并实现FallbackFactory接口,FallbackFactory接口的泛型指定为UserService,源码如下所示。

/**
 * @description 用户微服务容错Factory
 */
@Component
public class UserServiceFallBackFactory implements FallbackFactory<UserService> {
    @Override
    public UserService create(Throwable cause) {
        return new UserService() {
            @Override
            public User getUser(Long uid) {
                User user = new User();
                user.setId(-1L);
                return user;
            }
        };
    }
}

(2)在订单微服务的 io.binghe.shop.order.fegin.UserService 接口上的@FeignClient注解上指定fallbackFactory属性,如下所示。

/**
 * @description 调用用户微服务的接口
 */
//@FeignClient(value = "server-user", fallback = UserServiceFallBack.class)
@FeignClient(value = "server-user", fallbackFactory = UserServiceFallBackFactory.class)
public interface UserService {
    @GetMapping(value = "/user/get/{uid}")
    User getUser(@PathVariable("uid") Long uid);
}

(3)在io.binghe.shop.order.fegin.fallback.factory包中新建ProductServiceFallBackFactory类,并实现FallbackFactory接口,FallbackFactory接口的泛型指定为ProductService,源码如下所示。

/**
 * @description 商品微服务容错Factory
 */
@Component
public class ProductServiceFallBackFactory implements FallbackFactory<ProductService> {
    @Override
    public ProductService create(Throwable cause) {
        return new ProductService() {
            @Override
            public Product getProduct(Long pid) {
                Product product = new Product();
                product.setId(-1L);
                return product;
            }

            @Override
            public Result<Integer> updateCount(Long pid, Integer count) {
                Result<Integer> result = new Result<>();
                result.setCode(1001);
                result.setCodeMsg("触发了容错逻辑");
                return result;
            }
        };
    }
}

(4)在订单微服务的 io.binghe.shop.order.fegin.ProductService 接口上的@FeignClient注解上指定fallbackFactory属性,如下所示。

/**
 * @description 调用商品微服务的接口
 */
//@FeignClient(value = "server-product", fallback = ProductServiceFallBack.class)
@FeignClient(value = "server-product", fallbackFactory = ProductServiceFallBackFactory.class)
public interface ProductService {

    /**
     * 获取商品信息
     */
    @GetMapping(value = "/product/get/{pid}")
    Product getProduct(@PathVariable("pid") Long pid);

    /**
     * 更新库存数量
     */
    @GetMapping(value = "/product/update_count/{pid}/{count}")
    Result<Integer> updateCount(@PathVariable("pid") Long pid, @PathVariable("count") Integer count);
}
4.3.5 测试服务容错

与“Feign整合Sentinel实现容错-测试服务容错”中的测试方法相同,这里不再赘述。

至此,使用Sentinel实现限流和容错的功能就完成了。

最后,需要注意的是:使用Sentinel实现服务容错时,fallback和fallbackFactory只能使用其中一种方式实现服务容错,二者不能同时使用。

参考:

本文是参考冰河技术得SpringCloud Alibaba实战专栏内容整理的,只是方便学习使用(侵权删),具体想要了解或想要获取源码的,请参考下面冰河技术的链接,关注公众号获取源码。

SpringCloud Alibaba

冰河技术:《SpringCloud Alibaba实战》

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值