文章目錄
- 實現注冊中心-服務發現
- 模擬掉線
- 遠程調用
- 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宕機,調用失敗。