前言:本課程是在慕課網上學習Spring Cloud微服務實戰 第五章 應用通信 時所做的筆記,供本人複習之用.
目錄
第一章 HTTP與PRC
應用間的通信方式主要有兩種HTTP與RPC,SpringCloud與Dubbo正好是這兩種方式的代表.
Dubbo本身的定位就是一個RPC框架,基於Dubbo開發的應用還是要依賴周邊的平臺存在,相比其它RPC框架,Dubbo在服務治理功能上非常完善,不僅提供了服務註冊發現,負載均衡,路由以及面向分佈式集羣的技術能力,還涉及了面向開發測試階段的mocker泛化調用等機制,同時也提供了服務治理和監控的可視化平臺,SpringCloud在沒有出來之前Dubbo在國內應用的相當廣泛,Dubbo的定位始終是一款RPC框架,而SpringCloud的目標是微服務架構下的一站式解決方案,在重啓維護後,Dubbo官方表示,要積極尋求適配到SpringCloud的方式,比如作爲SpringCloud的二進制通信方案來發揮Dubbo的性能優勢,或者Dubbo通過模塊化或者對http的支持適配到SpringCloud,不過當前兩者還是不兼容.
SpringCloud微服務架構下,微服務之間使用的是http restful通信方式,http restful本身輕量易用,適用性強可以很容易的跨語言,跨平臺,或者與已有的系統交互,我們前端舉的例子node.js的Eureka Client也印證了這一點.
SpringCloud通過以下兩種restful調用方式.
RestTemplate與Feign.
第二章 RestTemplate的三種使用方法
RestTemplate是一個http客戶端,功能與java中的httpClient類似,用法上更加簡單,下面我們將用訂單服務調用商品服務的接口.我們可以將訂單服務理解成客戶端,商品服務理解成服務端,
服務端配置application.properties,引入的依賴在上一篇中:
spring.application.name=product
#datasource
spring.datasource.url=jdbc:mysql://localhost:3306/SpringCloud_Sell?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#Jpa
spring.jpa.open-in-view=true
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
spring.jpa.show-sql = true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
eureka.client.service-url.defaultZone = http://localhost:8761/eureka
server.port=8083
客戶端配置:
spring.application.name=order
#datasource
spring.datasource.url=jdbc:mysql://localhost:3306/SpringCloud_Sell?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#Jpa
spring.jpa.open-in-view=true
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
spring.jpa.show-sql = true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
eureka.client.service-url.defaultZone = http://localhost:8761/eureka
server.port=8081
服務中心配置:
eureka.client.service-url.defaultZone = http://localhost:8761/eureka/
eureka.client.register-with-eureka=false
eureka.server.enable-self-preservation=false
spring.application.name=eureka
server.port=8761
我們先調用幾個簡單的方法.
2.1 第一種使用方式
product服務端的controller(我們要在客戶端中進行調用的)
@RestController
public class ServerController {
@GetMapping("/msg")
public String msg(){
return "this is product' msg2";
}
}
order客戶端中的controller:
直接指定地址和返回值.
@RequestMapping("/getProductMsg")
public String getProductMsg(){
//第一種方式,到那時當有多個地址的時候就有問題了
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject("http://localhost:8083/msg",String.class);
log.info("response={}",response);
return response;
}
啓動應用,訪問地址
訪問成功
這樣的弊端是地址寫死,如果不知道對方的地址,或者對方有兩個地址就會無從下手.
2.2 第二種使用方式
現在我們有兩個服務器
product服務端配置不變.
order客戶端的服務方式變爲:
@RestController
@Slf4j
public class ClientController {
@Autowired
private LoadBalancerClient loadBalancerClient;
@RequestMapping("/getProductMsg")
public String getProductMsg(){
ServiceInstance serviceInstance = loadBalancerClient.choose("Product");
String url = String.format("http://%s:%s",serviceInstance.getHost(),serviceInstance.getPort())+"/msg";
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(url,String.class);
log.info("response={}",response);
return response;
}
}
也訪問到了數據
我們這樣通過應用名就訪問到了要調用的服務,但是這種方式還有一些繁瑣.
2.3 第三種使用方式
第三種方式主要是使用了@LoadBalanced註解
新建Config類.
@Component
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
}
product服務端配置不變.
order客戶端的服務方式變爲:
@RestController
@Slf4j
public class ClientController {
@Autowired
private RestTemplate restTemplate;
@RequestMapping("/getProductMsg")
public String getProductMsg(){
String response = restTemplate.getForObject("http://PRODUCT/msg",String.class);
log.info("response={}",response);
return response;
}
}
繼續訪問可以獲得信息.
這三種方式都可以歸結爲使用restTemplate的方式調用應用的接口,值得注意的是這裏有一個負載均衡的問題,它會幫我們去選擇ip地址,至於負載均衡的策略,可以隨機,輪詢等等.
第三章 負載均衡器Ribbon
3.1 Ribbon概要介紹
在講解Eureka的時候,我們也說過它是客戶端發現,Eureka屬於客戶端發現的方式,它的負載均衡是軟負載,也就是客戶端會向服務器拉取已註冊的可用服務信息,然後根據負載均衡策略,直接命中每臺服務器發送請求,整個過程都是在客戶端完成的,並不需要服務器的參與,SpringCloud客戶端的負載均衡就是Ribbon組件,它是基於netflix Ribbon實現的,通過SpringCloud的封裝,可以輕鬆的面向服務的Rest模板請求,自動轉化成客戶端負載均衡服務調用.
RestTemplate Feign Zuul都使用到了Ribbon,SpirngCloud在結合了Ribbon的負載均衡實現中,封裝增加了HttpClient和OkHttp兩種請求端實現,默認使用ribbon對eureka服務發現的負載均衡Client,第二章中介紹了RestTemplate三種實現方式,其中通過添加@LoadBalanced註解或者直接寫代碼的時候使用loadBalanceClient,其實用到的就是Ribbon的組件,添加@LoadBalanced註解後,Ribbon會通過loadBalanceClient自動幫助你基於某種規則比如簡單的輪詢,隨機連接等去連接目標服務,從而很容易使用Ribbnon實現自定義負載均衡算法.
Ribbon實現軟負載均衡核心有三點
1.服務發現,也就是發現依賴服務的列表.也就是依據服務的名字,找出服務中的所有實例.
2.服務選擇規則,依據規則策略,如何從多個服務中選擇一個有效的服務.
3.服務監聽,也就是檢測失效的服務,做到高效剔除.
Ribbon的主要組件
ServerList,IRule,ServerListFilter等
主要流程是這樣的,通過ServerList獲得所有的可用服務列表,然後通過ServerListFilter過濾掉一部分地址,最後剩下的地址中通過IRule選擇一個實例作爲最終目標結果.
3.2 Ribbon源碼追蹤
我們用2.2的方式來查看源碼.
ServiceInstance serviceInstance = loadBalancerClient.choose("Product");
idea:鼠標放在choose上,在按alt+ctrl+b找到其實現.
找到choose源碼,getServer就是要將服務列表找出來
public class RibbonLoadBalancerClient implements LoadBalancerClient {
//...省略
@Override
public ServiceInstance choose(String serviceId) {
return choose(serviceId, null);
}
public ServiceInstance choose(String serviceId, Object hint) {
Server server = getServer(getLoadBalancer(serviceId), hint);
if (server == null) {
return null;
}
return new RibbonServer(serviceId, server, isSecure(server, serviceId),
serverIntrospector(serviceId).getMetadata(server));
}
//...省略
}
我們點進getServer中,看到其用ILoadBalancer在找.
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {
if (loadBalancer == null) {
return null;
}
// Use 'default' on a null hint, or just pass it on?
return loadBalancer.chooseServer(hint != null ? hint : "default");
}
我們點進chooseServer中,選擇BaseLoadBalancer類,如圖1所示,其中的getAllServers就是獲取所有的服務列表.
@Override
public List<Server> getAllServers() {
return Collections.unmodifiableList(allServerList);
}
再在BaseLoadBalancer類中找到chooseServer方法,rule.choose表示用規則選擇服務,我們點進rule
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
我們發現rule使用的是默認的規則RoundRobinRule,這是一個輪詢的方式.
private final static IRule DEFAULT_RULE = new RoundRobinRule();
private final static SerialPingStrategy DEFAULT_PING_STRATEGY = new SerialPingStrategy();
private static final String DEFAULT_NAME = "default";
private static final String PREFIX = "LoadBalancer_";
protected IRule rule = DEFAULT_RULE;
我們在DynamicServerListLoadBalancer打上斷點也能發現其用的是輪詢的規則.
通過如下可以切換規,比如圖2就表示換成隨機規則.在springcloud的官方文檔中搜索Ribbon關鍵字找到Customizing default for all Ribbon Clients可以查看更多相關信息.
第四章 Feign
Feign是一個聲明式REST客戶端,採用了接口加註解的方式,Feign內部也使用了Ribbon.
前面使用了RestTemplate,這章使用Feign進行應用間的通信.訂單服務調用商品服務.
在訂單服務中增加Feign的依賴.
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
在啓動主類上加註解@EnableFeignClients
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
在訂單服務中新建接口,name說明的是要應用哪個應用,getmapping指明要用應用中的哪個controller.方法名只是一個標識.
@FeignClient(name="product")
public interface ProductClient {
@GetMapping("msg")
String productMsg();
}
在訂單中直接注入,然後當作方法調用即可.
@RestController
@Slf4j
public class ClientController {
@Autowired
private ProductClient productClient;
@GetMapping("/getProductMsg")
public String getProductMsg(){
String response = productClient.productMsg();
log.info("response={}",response);
return response;
}
}
第五章 使用Feign做服務間的通信
5.1 查詢商品詳情
我們要用訂單服務去調用商品服務,從訂單服務中給出商品的id,然後商品服務根據id查詢商品信息,並將查詢到的商品信息返回給訂單服務.
寫之前有兩點需要注意:@ResquestBody註解必須要用PostMapping的形式來映射,無參,單個參數@RequestParam,或者@PathVariable註解都可以用get.
5.1.1 商品服務
主要是傳入productId,返回product詳細信息集合.
controller:
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
/*獲取商品列表(給訂單服務用的)*/
@PostMapping("/listForOrder")
public List<ProductInfo> listForOrder(@RequestBody List<String> productList){
return productService.findByProductIdIn(productList);
}
}
service:
@Override
public List<ProductInfo> findByProductIdIn(List<String> productIdList) {
return productInfoRepository.findByProductIdIn(productIdList);
}
dao:
public interface ProductInfoRepository extends JpaRepository<ProductInfo,String> {
List<ProductInfo> findByProductIdIn(List<String> productId);
}
5.1.2 訂單服務
指明要使用的服務
@FeignClient(name="product")
public interface ProductClient {
@PostMapping("/product/listForOrder")
List<ProductInfo> listForOrder(@RequestBody List<String> productList);
}
在controller中進行調用
@GetMapping("/getProductList")
public String getProductList(){
List<ProductInfo> productInfoList = productClient.listForOrder(Arrays.asList("164103465734242707"));
log.info("response:{}",productInfoList);
return "ok";
}
訪問地址http://localhost:8081/getProductList,獲得信息成功
5.2 扣庫存
訂單服務將要扣庫存的商品id與數量傳入商品服務,由商品服務進行商品數量的更改.
5.2.1 商品服務
數據對象:
@Data
public class CartDTO {
private String productId;
private Integer productQuantity;
public CartDTO(String productId, Integer productQuantity) {
this.productId = productId;
this.productQuantity = productQuantity;
}
}
service層:
@Override
@Transactional
public void decreaseStock(List<CartDTO> cartDTOList) {
for(CartDTO cartDTO:cartDTOList){
//從數據庫中查詢處商品信息
Optional<ProductInfo> productInfoOptional = productInfoRepository.findById(cartDTO.getProductId());
//商品不存在拋出異常
if(!productInfoOptional.isPresent()){
throw new ProductException(ResultEnum.PRODUCT_NOT_EXIST);
}
ProductInfo productInfo = productInfoOptional.get();
//商品數量不足拋出異常
Integer result = productInfo.getProductStock() - cartDTO.getProductQuantity();
if(result<0){
throw new ProductException(ResultEnum.PRODUCT_STOCK_ERROR);
}
//將更改數量的商品存入數據庫
productInfo.setProductStock(result);
productInfoRepository.save(productInfo);
}
}
controller層:
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping("/decreaseStock")
public void decreaseStock(@RequestBody List<CartDTO> cartDTOList){
productService.decreaseStock(cartDTOList);
}
}
5.2.2 訂單服務
指明商品服務配置:
@FeignClient(name="product")
public interface ProductClient {
@PostMapping("/product/decreaseStock")
void decreaseStock(@RequestBody List<CartDTO> cartDTOList);
}
controller進行調用:
@GetMapping("productDecreaseStock")
public String productDecreaseStock(){
productClient.decreaseStock(Arrays.asList(new CartDTO("164103465734242707",3)));
return "ok";
}
運行,商品庫存減少.
5.3 完善整個下單流程
前臺的請求參數就是用戶信息以及購買的各種商品數量,類似下圖所示
下單流程: 查詢商品信息->計算總價->扣庫存->訂單入庫
5.3.1 訂單服務
controller層:
@Autowired
private OrderService orderService;
@PostMapping("/create")
public ResultVO<Map<String,String>> create(@Valid OrderForm orderForm, BindingResult bindingResult){
if(bindingResult.hasErrors()){
log.error("創建訂單參數不正確 ={}",orderForm);
throw new OrderException(1,bindingResult.getFieldError().getDefaultMessage());
}
OrderDTO orderDTO = OrderForm2OrderDTO.convert(orderForm);
//判斷購物車是否爲空
if(CollectionUtils.isEmpty(orderDTO.getOrderDetailList())){
log.error("創建訂單信息失敗,購物車爲空");
throw new OrderException(-1,"購物車爲空");
}
OrderDTO result = orderService.create(orderDTO);
Map<String,String> map = new HashMap<>();
map.put("orderId",result.getOrderId());
return ResultVOUtil.success(map);
}
service層:
@Override
public OrderDTO create(OrderDTO orderDTO) {
String orderId = KeyUtil.getUniqueKey();
//查詢商品信息(調用商品服務)
List<String> productIdList = orderDTO.getOrderDetailList().stream()
.map(OrderDetail::getProductId)
.collect(Collectors.toList());
log.info("查詢商品信息(調用商品服務)");
List<ProductInfo> productInfoList = productClient.listForOrder(productIdList);
//計算總價
BigDecimal orderAmout = new BigDecimal(BigInteger.ZERO);
for (OrderDetail orderDetail: orderDTO.getOrderDetailList()) {
for (ProductInfo productInfo: productInfoList) {
if (productInfo.getProductId().equals(orderDetail.getProductId())) {
//單價*數量
orderAmout = productInfo.getProductPrice()
.multiply(new BigDecimal(orderDetail.getProductQuantity()))
.add(orderAmout);
BeanUtils.copyProperties(productInfo, orderDetail);
orderDetail.setOrderId(orderId);
orderDetail.setDetailId(KeyUtil.getUniqueKey());
//訂單詳情入庫
orderDetailRepository.save(orderDetail);
}
}
}
//扣庫存(調用商品服務)
List<CartDTO> decreaseStockInputList = orderDTO.getOrderDetailList().stream()
.map(e -> new CartDTO(e.getProductId(), e.getProductQuantity()))
.collect(Collectors.toList());
productClient.decreaseStock(decreaseStockInputList);
//訂單入庫
OrderMaster orderMaster = new 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;
}
到這裏基本業務流程基本走通了,下一篇要進行多模塊的構建.