springcloud:全家桶+嵌入第三方组件(Netflix)
consumer:调用方
provider:被调用方
一个接口一般会充当两个角色(不是同时充当)
简介:微服务下电商项目基础模块设计,分离几个模块,课程围绕这个基础项目进行学习
微服务注册中心:主要用于服务管理,以及动态维护服务注册表。
服务提供者(provider):启动的时候向注册中心上报自己的网络信息
服务消费者(consumer):启动的时候向注册中心上报自己的网络信息,拉取provider的相关网络信息
下面通过一张图来理解微服务注册中心的运行流程:
假定项目有两个微服务,分别为商品服务和订单服务,其中商品服务包含两个接口(商品列表、商品详情),订单服务包含两个接口(下单接口、我的订单)。
以上两个服务启动后会将其中的接口信息发送到注册中心,注册中心中有一张维护服务列表,该列表记录了所有服务接口的信息。
同时服务列表中有一种动态维护机制(心跳机制)保证每个服务接口都是可用的。例如商品服务,作为一个电商项目,会利用集群部署,即很多台服务器上都在运行同一个服务,这时候就需要保证注册列表中的接口都是可用的(对应的服务器正常运行),心跳机制就是每台服务器定时向注册中心发送心跳包,告诉注册中心,我还活着,让我保留在服务列表中。
通过注册中心,对各个服务之间进行了解耦。例如但订单服务需要访问商品服务时,它不直接访问配置文件,而是从注册中心拉取对应的信息,再进行访问。
为什么要用注册中心:
因为服务应用和机器越来越多,调用方需要知道接口的网络地址,如果靠配置文件的方式去控制网络地址,对于动态新增机器、维护带来很大的问题。
主流的注册中心:
zookeeper
、Eureka
、consul
、etcd
等
CAP定理:
指的是在一个分布式系统中,Consistency
(一致性)、Availability
(可用性)、Partition tolerance
(分区容错性),三者不可同时获得
CAP理论就是说在分布式存储系统中,最多只能实现上面的两点。而由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容错性是我们必须需要实现的。所以我们只能在一致性和可用性之间进行权衡。
按照以上步骤点击Finish后,喝杯咖啡,等待maven配置环境…
初始化工程目录:
初始化启动文件:
需要添加依赖(按照以上步骤创建,IDEA自动配置好):
<dependency> <groupId>org.springframework.cloudgroupId> <artifactId>spring-cloud-starter-netflix-eureka-serverartifactId> dependency>
关于配置文件application.yml和application.properties都是可以的
application.properties:
server.port=8761
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
eureka.client.service-url.http://${eureka.instance.hostname}:${server.port}/eureka/
application.yml:
server:
port: 8761
eureka:
instance:
hostname: localhost
client:
register-with-eureka: false
fetch-registry: false
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
yml中注意空格:yml文件中,每个字段的冒号(:)后面都需要加空格,否则无法提取字段
需要根据Maven配置大量环境,建议Maven设置为国内阿里云镜像
Spring官方文档:https://cloud.spring.io/spring-cloud-netflix/reference/html/#netflix-eureka-client-starter
创建工程目录,结构为下图所示的三层结构
/*ProductController.java*/
package net.xdclass.product_service.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import net.xdclass.product_service.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@Api(tags = "搭建商品服务实战")
@RestController
@RequestMapping("/api/v1/product")
public class ProductController {
@Autowired
private ProductService productService;
/**
* 获取所有商品列表
*/
@ApiOperation(value = "获取商品清单")
@RequestMapping(value = "/list", method = RequestMethod.GET)
public Object list() {
return productService.listProduct();
}
/**
* 根据id查找商品详情
*/
@ApiOperation(value = "根据编码获取商品信息")
@RequestMapping(value = "/find", method = RequestMethod.GET)
public Object findById(@RequestParam("id") Integer id) {
return productService.findById(id);
}
}
/*Product.java*/
package net.xdclass.product_service.domain;
import java.io.Serializable;
public class Product implements Serializable {
private static final long serialVersionUID = -2646223751221766417L;
/*编号*/
private Integer id;
/*名称*/
private String name;
/*价格*/
private Integer price;
/*库存*/
private Integer store;
public Product() {
}
public Product(Integer id, String name, Integer price, Integer store) {
this.id = id;
this.name = name;
this.price = price;
this.store = store;
}
/*省略get和set方法*/
}
/*ProductService.java*/
package net.xdclass.product_service.service;
import net.xdclass.product_service.domain.Product;
import java.util.List;
public interface ProductService {
List<Product> listProduct();
Product findById(Integer id);
}
/*ProductServiceImpl.java*/
package net.xdclass.product_service.service.impl;
import net.xdclass.product_service.domain.Product;
import net.xdclass.product_service.service.ProductService;
import org.omg.PortableInterceptor.AdapterManagerIdHelper;
import org.springframework.stereotype.Service;
import java.io.Serializable;
import java.util.*;
@Service
public class ProductServiceImpl implements ProductService, Serializable {
private static final long serialVersionUID = 2432700951541406918L;
private static Map<Integer, Product> daoMap = new HashMap<>();
/*使用静态加载模拟内存中的数据*/
static {
Product p1 = new Product(1, "iphonex", 9999, 10);
Product p2 = new Product(2, "冰箱", 999, 100);
Product p3 = new Product(3, "洗衣机", 99, 101);
Product p4 = new Product(4, "电话", 9, 110);
Product p5 = new Product(5, "汽车", 11, 130);
Product p6 = new Product(6, "椅子", 22, 140);
Product p7 = new Product(7, "书", 33, 160);
daoMap.put(p1.getId(), p1);
daoMap.put(p2.getId(), p2);
daoMap.put(p3.getId(), p3);
daoMap.put(p4.getId(), p4);
daoMap.put(p5.getId(), p5);
daoMap.put(p6.getId(), p6);
daoMap.put(p7.getId(), p7);
}
@Override
public List<Product> listProduct() {
return new ArrayList<>(daoMap.values());
}
@Override
public Product findById(Integer id) {
return daoMap.get(id);
}
}
.properties文件
#配置端口
server.port=8771
#配置当前微服务名称
eureka.instance.appname=product-service
#eureka.instance.app-group-name=product
#配置到注册中心
eureka.client.register-with-eureka=true
eureka.client.service-url.http://localhost:8761/eureka/
.yml文件
server:
prot: 8771
#指定注册中心
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
#服务器名称
spring:
appliaction:
name: product-service
下图所示,即注册成功
下面进行同个服务多端口注册:
第一步:打开配置中心
第二步:端口设置
如下图步骤三,输入
-Dserver.port=端口号
点击ok
第三步:重新启动微服务,注册新端口到eurake
如图所示,表示两个端口成功注册到eurake
当然你可以再次注册多个不同的端口,只需要重复上述步骤即可。
第四步:测试不同端口的服务功能
利用postman分别对两个端口的服务进行测试:
问题一:
去除图中警告只需要在注册中心配置文件中进行配置即可(将自我保护模式关闭)
.properties文件
eureka.server.enable-self-preservation=false
.yml文件
eureka:
server:
enable-self-preservation: false
问题二:
只需要添加一个注册地址就可以进行注册,而不需要在启动类上添加注解@EnableEurakeClient
官方文档:
Note that the preceding example shows a normal Spring Boot application. By having spring-cloud-starter-netflix-eureka-client on the classpath, your application automatically registers with the Eureka Server. Configuration is required to locate the Eureka server, as shown in the following example:
翻译:
注意,前面的示例显示了一个普通的Spring启动应用程序。通过在类路径上拥有spring-cloud-starter-netflix-eureka-client,您的应用程序将自动向Eureka服务器注册。配置需要找到Eureka服务器,如下例所示:
即只需要在配置文件中配置注册路径即可
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
RPC:
远程过程调用,像本地服务(方法)一样调用服务器的服务
支持同步、异步调用
客户端和服务器之间建立TCP连接,可以一次建立一个,也可以多个调用复用一次连接
通讯层协议为protobuf、thrift、avro,直接采用二进制的形式。
Rest(Http):
http请求,支持多种协议和功能
开发方便成本低
简洁:实战电商项目 订单服务 调用商品服务获取商品信息
/*OrderController.java*/
package net.xdclass.order_service.controller;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import net.xdclass.order_service.service.ProductOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "商品订单")
@RestController
@RequestMapping("/api/v1/order")
public class OrderController {
@Autowired
private ProductOrderService productOrderService;
@ApiOperation(value = "保存订单信息")
@RequestMapping(value = "/save", method = RequestMethod.GET)
public Object save(@RequestParam("userId") Integer userId, @RequestParam("productId") Integer productId) {
return productOrderService.save(userId, productId);
}
}
/*ProductOrder.java*/
package net.xdclass.order_service.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import java.io.Serializable;
import java.util.Date;
@Api("商品订单实体类")
public class ProductOrder implements Serializable {
private static final long serialVersionUID = -6799431680127887373L;
@ApiModelProperty(value = "订单ID", name = "id")
private Integer id;
@ApiModelProperty(value = "商品名称", name = "productName")
private String productName;
@ApiModelProperty(value = "订单流水号", name = "tredeNo")
private String tredeNo;
@ApiModelProperty(value = "订单价格", name = "price")
private Integer price;
@JsonFormat(pattern = "YYYY-MM-dd HH:mm:ss", timezone = "GMT+8")
@ApiModelProperty(value = "订单创建时间", name = "createTime")
private Date createTime;
@ApiModelProperty(value = "用户ID", name = "userId")
private Integer userId;
@ApiModelProperty(value = "用户名称", name = "userName")
private String userName;
/*省略set和get方法*/
}
/*ProductOrderImpl.java*/
package net.xdclass.order_service.service.impl;
import net.xdclass.order_service.domain.ProductOrder;
import net.xdclass.order_service.service.ProductOrderService;
import net.xdclass.product_service.domain.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.util.Date;
import java.util.UUID;
@Service
public class ProductOrderImpl implements ProductOrderService {
@Value("${server.port}")
private String port;
@Autowired
private RestTemplate restTemplate;
@Override
public ProductOrder save(Integer userId, Integer productId) {
//获取商品详情
Product object = restTemplate.getForObject(
"http://product-service/api/v1/product/find?id=" + productId, Product.class);
ProductOrder productOrder = new ProductOrder();
if (null != object) {
System.out.println(object);
productOrder.setId(object.getId());
productOrder.setProductName(object.getName());
productOrder.setPrice(object.getPrice());
productOrder.setUserName(port);
productOrder.setCreateTime(new Date());
productOrder.setUserId(userId);
productOrder.setTredeNo(UUID.randomUUID().toString().replaceAll("-", ""));
}
return productOrder;
}
}
/*ProductOrderService.java*/
package net.xdclass.order_service.service;
import net.xdclass.order_service.domain.ProductOrder;
public interface ProductOrderService {
public ProductOrder save(Integer userId, Integer productId);
}
启动类中添加一下代码,生成RestTemplate对象用于自动注入
@Bean
@LoadBalanced//使用负载均衡器Ribbon
public RestTemplate restTemplate(){
return new RestTemplate();
}
启动多个product-service服务端口
如下图可以看出,ribbon自动将访问请求分配到了不同端口的服务上,实现负载均衡
另一种ribbon的实现
官方文档:
You can also use the LoadBalancerClient
directly, as shown in the following example:
public class MyClass {
@Autowired
private LoadBalancerClient loadBalancer;
public void doStuff() {
ServiceInstance instance = loadBalancer.choose("stores");
URI storesUri = URI.create(String.format("http://%s:%s", instance.getHost(), instance.getPort()));
// ... do something with the URI
}
}
public class MyClass {
@Autowired
private LoadBalancerClient loadBalancer;
public void doStuff() {
ServiceInstance instance = loadBalancer.choose("stores");
String url = String.format("http://%s:%s", instance.getHost(), instance.getPort());
RestTemplate restTemplate = new RestTemplate();
// ... do something with the restTemplate
//Object object = restTemplate.getForObject(url, Object.class);
}
}
该种方式下不再需要在启动类中添加
@Bean
@LoadBalanced//使用负载均衡器Ribbon
public RestTemplate restTemplate(){
return new RestTemplate();
}
而是哪里需要配哪里即可
@Service
public class ProductOrderImpl implements ProductOrderService {
@Value("${server.port}")
private String port;
@Autowired
private LoadBalancerClient loadBalancer;
@Override
public ProductOrder save(Integer userId, Integer productId) {
/*关键代码*/
ServiceInstance instance = loadBalancer.choose("product-service");
String url = String.format("http://%s:%s/api/v1/product/find?id=" + productId, instance.getHost(), instance.getPort());
RestTemplate restTemplate = new RestTemplate();
//获取商品详情
Product object = restTemplate.getForObject(url, Product.class);
ProductOrder productOrder = new ProductOrder();
if (null != object) {
System.out.println(object);
productOrder.setId(object.getId());
productOrder.setProductName(object.getName());
productOrder.setPrice(object.getPrice());
productOrder.setUserName(port);
productOrder.setCreateTime(new Date());
productOrder.setUserId(userId);
productOrder.setTredeNo(UUID.randomUUID().toString().replaceAll("-", ""));
}
return productOrder;
}
}
@LoadBalanced:
- 首先从注册中心获取provider的列表
- 通过一定的策略选择其中一个节点
- 再返回给restTemplate调用
简洁:改造电商项目 订单服务 调用商品服务获取商品信息
Fegin:伪RPC客户端(本质还是用http)
使用feign步骤讲解
加入依赖(新旧版本依赖名称不一样)
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
代码实战
这里的代码在order-service实现ribbon负载均衡上修改而得
/*OrderServiceApplication.java*/
@SpringBootApplication
@EnableSwagger2
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
创建Feign接口
/*Feign.java*/
package net.xdclass.order_service.service;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(name = "product-service")
public interface ProductClient {
@GetMapping("/api/v1/product/find")
String findById(@RequestParam(value = "id") Integer id);
}
注意:
@FeignClient(name = “”)中name指定的服务名称必须同生产者的服务名称一致
@GetMapping("")中指定的路径必须与调用生产者服务的访问请求路径保持一致
消费者业务逻辑实现类
/*ProductOrderImpl.java*/
package net.xdclass.order_service.service.impl;
@Service
public class ProductOrderImpl implements ProductOrderService {
@Value("${server.port}")
private String port;
@Autowired
private ProductClient productClient;
@Override
public ProductOrder save(Integer userId, Integer productId) {
/*关键代码*/
String response = productClient.findById(productId);
JsonNode jsonNode = JsonUtils.strTOJsonNode(response);
ProductOrder productOrder = new ProductOrder();
if (null != jsonNode) {
System.out.println(jsonNode);
productOrder.setId(Integer.parseInt(jsonNode.get("id").toString()));
productOrder.setProductName(jsonNode.get("name").toString());
productOrder.setPrice(Integer.parseInt(jsonNode.get("price").toString()));
productOrder.setUserName(port);
productOrder.setCreateTime(new Date());
productOrder.setUserId(userId);
productOrder.setTredeNo(UUID.randomUUID().toString().replaceAll("-", ""));
}
return productOrder;
}
}
Json工具类
/*JsonUtils.java*/
package net.xdclass.order_service.utils;
public class JsonUtils {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static JsonNode strTOJsonNode(String str) {
try {
return OBJECT_MAPPER.readTree(str);
} catch (IOException e) {
return null;
}
}
}
注意点:
- 路径
- Http方法必须对应
- 使用@requestBady,应该使用POSTMAPPING
- 多个参数的时候,通过添加@RequestParam
官方文档:
How to Include Feign
To include Feign in your project use the starter with group
org.springframework.cloud
and artifact idspring-cloud-starter-openfeign
. See the Spring Cloud Project page for details on setting up your build system with the current Spring Cloud Release Train.Example spring boot app
@SpringBootApplication @EnableFeignClients public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
StoreClient.java.
@FeignClient("stores") public interface StoreClient { @RequestMapping(method = RequestMethod.GET, value = "/stores") List
getStores(); @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json") Store update(@PathVariable("storeId") Long storeId, Store store); } In the
@FeignClient
annotation the String value (“stores” above) is an arbitrary client name, which is used to create a Ribbon load balancer (see below for details of Ribbon support). You can also specify a URL using theurl
attribute (absolute value or just a hostname). The name of the bean in the application context is the fully qualified name of the interface. To specify your own alias value you can use thequalifier
value of the@FeignClient
annotation.The Ribbon client above will want to discover the physical addresses for the “stores” service. If your application is a Eureka client then it will resolve the service in the Eureka service registry. If you don’t want to use Eureka, you can simply configure a list of servers in your external configuration (see above for example).
Ribbon和Feign的选择使用
一般选择Feign,因为Feign默认集成了Ribbon,代码编写方式更加清晰和方便采用注解方式进行配置,配置熔断等方式方便。
负载均衡策略设定
Feign中包含了Ribbon和Hystrix,因此可以直接设置Ribbon改变均衡策略。
以下配置代码表示:将product-service应用的均衡策略配置为随机分配,默认情况下是以轮询的方式进行分配(即每个人一次,依次循环分配)
product-service.ribbon.NFLoadBalancerRuleClassName=com.netflix.loadbalancer.RandomRule
Feign超时配置
在开发过程中每个微服务的响应时间都是不一样的,可以配置超时时间:
配置语法:
#properties
feign.client.config.default.connect-timeout=2000
feign.client.config.default.read-timeout=2000
#yml
feign:
client:
config:
default:
connectTimeout: 2000
readTimeout: 2000
注意:配置语句中的
default
为假名,表示配置默认的超时设置,即@FeignClient下的所有接口都将设置为该超时设置
默认设置readtimeout为60s,但是由于Hystrix默认是1s超时时间,spring优先以1s超时时间为准