Spring Cloud Alibaba快速入门02-Nacos(中)
文章目录
- 实现注册中心-服务发现
- 模拟掉线
- 远程调用
- 1.订单和商品模块的接口
- 商品服务
- 订单服务
- 2.抽取实体类
- 3.订单服务拿到需要调用服务的ip和端口
- 负载均衡
- 步骤1
- 步骤2
- 步骤3
- 步骤4
- 面试题:注册中心宕机,远程调用还能成功吗?
- 1、调用过;远程调用不在依赖注册中心,可以通过
- 2、没调用过:(第一次发起远程调用);不能通过
实现注册中心-服务发现
使用@EnableDiscoveryClient开启服务发现功能
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@EnableDiscoveryClient//开启服务发现功能
@SpringBootApplication
public class ProductApplication {public static void main(String[] args) {SpringApplication.run(ProductApplication.class, args);}
}
导入单元测试依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><!--只在test目录下生效-->
</dependency>
执行
import com.alibaba.cloud.nacos.discovery.NacosServiceDiscovery;
import com.alibaba.nacos.api.exception.NacosException;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;import java.util.List;@SpringBootTest
public class DiscoveryTest {@AutowiredDiscoveryClient discoveryClient;@Testvoid discoveryClientTest(){for (String service : discoveryClient.getServices()) {System.out.println("service = " + service);//获取ip+portList<ServiceInstance> instances = discoveryClient.getInstances(service);for (ServiceInstance instance : instances) {System.out.println("ip:"+instance.getHost()+";"+"port = " + instance.getPort());}}/*service = qf-service-orderip:192.168.109.1;port = 8080ip:192.168.109.1;port = 8081service = qf-service-productip:192.168.109.1;port = 8181ip:192.168.109.1;port = 8180ip:192.168.109.1;port = 8182*/}@AutowiredNacosServiceDiscovery nacosServiceDiscovery;@Testvoid nacosServiceDiscoveryTest() throws NacosException {for (String service : nacosServiceDiscovery.getServices()) {System.out.println("service = " + service);List<ServiceInstance> instances = nacosServiceDiscovery.getInstances(service);for (ServiceInstance instance : instances) {System.out.println("ip:"+instance.getHost()+";"+"port = " + instance.getPort());}}/*service = qf-service-orderip:192.168.109.1;port = 8080ip:192.168.109.1;port = 8081service = qf-service-productip:192.168.109.1;port = 8181ip:192.168.109.1;port = 8180ip:192.168.109.1;port = 8182*/}
}
DiscoveryClient是spring的规范,NacosServiceDiscovery是nacos的
模拟掉线
运行nacosServiceDiscoveryTest()
service = qf-service-order
ip:192.168.109.1;port = 8080
service = qf-service-product
ip:192.168.109.1;port = 8181
ip:192.168.109.1;port = 8180
ip:192.168.109.1;port = 8182
远程调用
这里暂时使用RestTemplate实现远程调用
1.订单和商品模块的接口
先完善订单和商品模块的接口,完成启动各自项目后调用各自的接口即可
商品服务
import lombok.Data;
import java.math.BigDecimal;
@Data
public class Product {private Long id;private BigDecimal price;private String productName;private int num;
}
import com.qf.entity.Product;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;import java.math.BigDecimal;@RestController
public class ProductController {@GetMapping("/product/{id}")public Product getProduct(@PathVariable("id") Long id){Product product = new Product();product.setId(id);product.setProductName("华为手机");product.setPrice(new BigDecimal(5000));product.setNum(100);return product;}
}
订单服务
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;@Data
public class Order {private Long id;private Long userId;private BigDecimal totalAmount;private String address;private List<Product> productList;
}
Controller
import com.qf.entity.Order;
import com.qf.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
public class OrderController {@Autowiredprivate OrderService orderService;@GetMapping("/create")public Order createOrder(@RequestParam("userId") Long userId,@RequestParam("productId") Long productId) {return orderService.createOrder(userId, productId);}
}
服务类
import com.qf.entity.Order;
import org.springframework.web.bind.annotation.RequestParam;public interface OrderService {Order createOrder(@RequestParam("userId") Long userId, @RequestParam("productId") Long productId);
}
import com.qf.entity.Order;
import com.qf.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;import java.math.BigDecimal;
import java.util.List;@Slf4j
@Component
public class OrderServiceImpl implements OrderService {@Overridepublic Order createOrder(Long userId, Long productId) {Order order = new Order();order.setId(1L);order.setUserId(userId);order.setTotalAmount(new BigDecimal(100));order.setAddress("北京");order.setProductList(null);return order;}
}
2.抽取实体类
因为在远程调用时要使用对方的实体类,所以直接将实体类单独放在一个项目(取名model)中
在services父项目中依赖model
<dependency><groupId>com.qf</groupId><artifactId>model</artifactId><version>0.0.1-SNAPSHOT</version>
</dependency>
项目结构
3.订单服务拿到需要调用服务的ip和端口
配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;@Configuration
public class BeanConfig {@BeanRestTemplate restTemplate(){return new RestTemplate();}
}
订单服务类
import com.qf.entity.Order;
import com.qf.entity.Product;
import com.qf.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;import java.math.BigDecimal;
import java.util.Arrays;
import java.util.List;@Slf4j
@Component
public class OrderServiceImpl implements OrderService {@Overridepublic Order createOrder(Long userId, Long productId) {Product product = getProductFromRemote(productId);Order order = new Order();order.setId(1L);order.setUserId(userId);order.setTotalAmount(new BigDecimal(100));order.setAddress("北京");order.setProductList(Arrays.asList(product));return order;}//远程调用@AutowiredDiscoveryClient discoveryClient;@AutowiredRestTemplate restTemplate;private Product getProductFromRemote(Long productId) {//1、获取到商品服务所在的所有机器IP+portList<ServiceInstance> instances = discoveryClient.getInstances("qf-service-product");//拿取第一个地址ServiceInstance instance = instances.get(0);//拼接远程URLString url = "http://" + instance.getHost() + ":" + instance.getPort() + "/product/" + productId;log.info("远程请求:{}", url);//2、给远程发送请求,自动将json格式转为对象Product product = restTemplate.getForObject(url, Product.class);return product;}
}
订单调用接口时,通过RestTemplate实现远程调用商品服务,在nacos中下线商品服务可以看到日志调用其他端口。
现在只是从nacos拿到商品服务第一个ip和端口,接下来通过负载均衡依次调用多个商品服务的ip和端口
负载均衡
步骤1
order添加依赖
<!-- 单元测试--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope><!--只在test目录下生效--></dependency>
<!-- nacos负载均衡--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency>
步骤2
测试类
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;import java.util.List;@SpringBootTest
public class LoadBalancerTest {@AutowiredDiscoveryClient discoveryClient;@AutowiredLoadBalancerClient loadBalancerClient;@Testvoid Test(){List<ServiceInstance> instances = discoveryClient.getInstances("qf-service-product");ServiceInstance choose = loadBalancerClient.choose("qf-service-product");System.out.println(choose.getHost()+"-"+choose.getPort());choose = loadBalancerClient.choose("qf-service-product");System.out.println(choose.getHost()+"-"+choose.getPort());choose = loadBalancerClient.choose("qf-service-product");System.out.println(choose.getHost()+"-"+choose.getPort());choose = loadBalancerClient.choose("qf-service-product");System.out.println(choose.getHost()+"-"+choose.getPort());choose = loadBalancerClient.choose("qf-service-product");System.out.println(choose.getHost()+"-"+choose.getPort());choose = loadBalancerClient.choose("qf-service-product");System.out.println(choose.getHost()+"-"+choose.getPort());System.out.println();}
}
此时商品服务在线情况
输出
192.168.109.1-8181
192.168.109.1-8180
192.168.109.1-8082
192.168.109.1-8181
192.168.109.1-8180
192.168.109.1-8082
步骤3
在订单服务实现类中添加负载均衡代码
@Override
public Order createOrder(Long userId, Long productId) {Product product = getProductFromRemoteWithLoadBalance(productId);Order order = new Order();order.setId(1L);order.setUserId(userId);order.setTotalAmount(new BigDecimal(100));order.setAddress("北京");order.setProductList(Arrays.asList(product));return order;
}//负载均衡
@Autowired
private LoadBalancerClient loadBalancerClient;
// 完成负载均衡发送请求
private Product getProductFromRemoteWithLoadBalance(Long productId){//1、获取到商品服务所在的所有机器IP+portServiceInstance choose = loadBalancerClient.choose("qf-service-product");//远程URLString url = "http://"+choose.getHost() +":" +choose.getPort() +"/product/"+productId;log.info("远程请求:{}",url);//2、给远程发送请求Product product = restTemplate.getForObject(url, Product.class);return product;
}
打印
2025-02-20T07:24:15.842+08:00 INFO 40732 --- [qf-service-order] [nio-8080-exec-2] com.qf.service.Impl.OrderServiceImpl : 远程请求:http://192.168.109.1:8181/product/22
2025-02-20T07:24:16.641+08:00 INFO 40732 --- [qf-service-order] [nio-8080-exec-1] com.qf.service.Impl.OrderServiceImpl : 远程请求:http://192.168.109.1:8180/product/22
2025-02-20T07:24:17.349+08:00 INFO 40732 --- [qf-service-order] [nio-8080-exec-3] com.qf.service.Impl.OrderServiceImpl : 远程请求:http://192.168.109.1:8082/product/22
2025-02-20T07:24:17.823+08:00 INFO 40732 --- [qf-service-order] [nio-8080-exec-4] com.qf.service.Impl.OrderServiceImpl : 远程请求:http://192.168.109.1:8181/product/22
2025-02-20T07:24:18.311+08:00 INFO 40732 --- [qf-service-order] [nio-8080-exec-5] com.qf.service.Impl.OrderServiceImpl : 远程请求:http://192.168.109.1:8180/product/22
2025-02-20T07:24:18.836+08:00 INFO 40732 --- [qf-service-order] [nio-8080-exec-6] com.qf.service.Impl.OrderServiceImpl : 远程请求:http://192.168.109.1:8082/product/22
2025-02-20T07:24:19.697+08:00 INFO 40732 --- [qf-service-order] [nio-8080-exec-7] com.qf.service.Impl.OrderServiceImpl : 远程请求:http://192.168.109.1:8181/product/22
2025-02-20T07:24:20.328+08:00 INFO 40732 --- [qf-service-order] [nio-8080-exec-8] com.qf.service.Impl.OrderServiceImpl : 远程请求:http://192.168.109.1:8180/product/22
可以看到在轮询访问商品服务
步骤4
基于注解的负载均衡
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;@Configuration
public class BeanConfig {@BeanRestTemplate restTemplate(){return new RestTemplate();}@Bean@LoadBalancedRestTemplate restTemplateAndLoadBalanced(){return new RestTemplate();}
}
服务类
@Slf4j
@Component
public class OrderServiceImpl implements OrderService {@Overridepublic Order createOrder(Long userId, Long productId) {Product product = getProductFromRemoteWithLoadBalanceAnnotation(productId);Order order = new Order();order.setId(1L);order.setUserId(userId);order.setTotalAmount(new BigDecimal(100));order.setAddress("北京");order.setProductList(Arrays.asList(product));return order;}//负载均衡@AutowiredRestTemplate restTemplateAndLoadBalanced;//注解方式// 基于注解的负载均衡private Product getProductFromRemoteWithLoadBalanceAnnotation(Long productId){String url = "http://qf-service-product/product/"+productId;//2、给远程发送请求; service-product 会被动态替换Product product = restTemplateAndLoadBalanced.getForObject(url, Product.class);return product;}
}
在商品服务中打断点或打印,可以发现轮询的访问每个商品服务
面试题:注册中心宕机,远程调用还能成功吗?
一般情况下订单服务会先从注册中心拿到地址列表,在访问商品服务。为了不每次远程调用都访问注册中心,增加了实例缓存,实例缓存实时更新在注册中心中的地址列表。
1、调用过;远程调用不在依赖注册中心,可以通过
当订单服务从注册中心中拿过商品服务的列表后,因为放在了实例缓存中,所以当注册中心(nacos)关闭时,只要商品服务未关闭,仍然是可以继续访问的。
2、没调用过:(第一次发起远程调用);不能通过
重新开启nacos,重启所有的服务,但订单服务先不访问商品服务。
关闭nacos,订单服务访问商品服务,此时发生报错
2025-02-20T07:53:38.094+08:00 ERROR 30652 --- [qf-service-order] [nio-8080-exec-1] scoveryClientServiceInstanceListSupplier : Exception occurred while retrieving instances for service qf-service-productjava.lang.RuntimeException: Can not get hosts from nacos server. serviceId: qf-service-productat com.alibaba.cloud.nacos.discovery.NacosDiscoveryClient.getInstances(NacosDiscoveryClient.java:72) ~[spring-cloud-starter-alibaba-nacos-discovery-2023.0.3.2.jar:2023.0.3.2]at org.springframework.cloud.client.discovery.composite.CompositeDiscoveryClient.getInstances(CompositeDiscoveryClient.java:54) ~[spring-cloud-commons-4.1.4.jar:4.1.4]at org.springframework.cloud.loadbalancer.core.DiscoveryClientServiceInstanceListSupplier.lambda$new$0(DiscoveryClientServiceInstanceListSupplier.java:64) ~[spring-cloud-loadbalancer-4.1.4.jar:4.1.4]at reactor.core.publisher.MonoCallable$MonoCallableSubscription.request(MonoCallable.java:137) ~[reactor-core-3.6.10.jar:3.6.10]...
所以当订单服务没调用过商品服务时(第一次发起远程调用),,此时nacos宕机,调用失败。