本项目主要针对两个端口,80(即默认端口,现在访问网站都是自动默认80,可省略),8001。
端口号 | 作用 |
---|---|
8001 | 微服务的提供者,支付module |
80 | 微服务的消费者,下订单module |
其中将两个微服务都会用到的entities实体类放在maven工程“cloud-api-common”下,clean,install,再在另两个微服务的pom中引入依赖即可使用。
父pom:采用dependencyManagement这样能够在子pom直接继承这里的版本号,原本要写gav,到子pom中可以不写v了,并可以统一进行
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud2020</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>cloud-provider-payment8001</module>
<module>cloud-consumer-order80</module>
<module>cloud-api-common</module>
</modules>
<packaging>pom</packaging>
<!--统一管理jar包和版本-->
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<junit.version>4.12</junit.version>
<log4j.version>1.2.17</log4j.version>
<lombok.version>1.16.18</lombok.version>
<mysql.version>8.0.18</mysql.version>
<druid.verison>1.1.16</druid.verison>
<mybatis.spring.boot.verison>1.3.0</mybatis.spring.boot.verison>
</properties>
<!--子模块继承之后,提供作用:锁定版本+子module不用谢groupId和version-->
<dependencyManagement>
<dependencies>
<!--spring boot 2.2.2-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.2.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud Hoxton.SR1-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba 2.1.0.RELEASE-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- MySql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!-- Druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.verison}</version>
</dependency>
<!-- mybatis-springboot整合 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.spring.boot.verison}</version>
</dependency>
<!--lombok-->
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
<scope>provided</scope>
</dependency>
<!--junit-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
<!-- log4j -->
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core -->
<!-- log4j日志系统 -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.12</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<addResources>true</addResources>
</configuration>
</plugin>
</plugins>
</build>
</project>
entities包中
CommonResult类
package com.atguigu.springcloud.entities;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
private Integer code;
private String message;
private T data;
public CommonResult(Integer code,String message){
this(code,message, null);
}
}
Payment类
package com.atguigu.springcloud.entities;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Payment implements Serializable {
private Long id;
private String serial;
}
pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-api-common</artifactId>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
</dependencies>
</project>
再clean,install,其他微服务只需引入依赖即可
server:
port: 8001
spring:
application:
name: cloud-payment-service
datasource:
# 当前数据源操作类型
type: com.alibaba.druid.pool.DruidDataSource
# mysql驱动类
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/cloud?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=GMT%2B8
username: root
password: root
mybatis:
mapper-locations: classpath*:mapper/*.xml
type-aliases-package: com.atguigu.springcloud.entities
子pom
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020</artifactId>
<groupId>com.atguigu.springcloud</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>cloud-provider-payment8001</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--监控-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<!--如果没写版本,从父层面找,找到了就直接用,全局统一-->
</dependency>
<!--mysql-connector-java-->
<dependency>
<groupId>com.atguigu.springcloud</groupId>
<artifactId>cloud-api-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--jdbc-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<!--热部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
sqlyog建表:
CREATE TABLE `payment`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`serial` VARCHAR(200) DEFAULT '' ,
PRIMARY KEY (`id`)
)ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
dao接口
package com.atguigu.springcloud.dao;
import com.atguigu.springcloud.entities.Payment;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface PaymentDao {
public int create(Payment payment);
public Payment getPaymentById(@Param("id") Long id);
}
由于采用了mybatis,写成mapper
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.springcloud.dao.PaymentDao">
<insert id="create" parameterType="Payment" useGeneratedKeys="true" keyProperty="id">
insert into payment(serial) values(#{serial});
</insert>
<resultMap id="BaseResultMap" type="com.atguigu.springcloud.entities.Payment">
<id column="id" property="id" jdbcType="BIGINT"/>
<id column="serial" property="serial" jdbcType="VARCHAR"/>
</resultMap>
<select id="getPaymentById" parameterType="long" resultMap="BaseResultMap">
select * from payment where id =#{id};
</select>
</mapper>
service层PaymentService接口
package com.atguigu.springcloud.service;
import com.atguigu.springcloud.entities.Payment;
import org.apache.ibatis.annotations.Param;
public interface PaymentSerice {
public int create(Payment payment); //写
public Payment getPaymentById(@Param("id") Long id); //读取
}
对应的实现类
package com.atguigu.springcloud.service.impl;
import com.atguigu.springcloud.dao.PaymentDao;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentSerice;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class PaymentServiceImpl implements PaymentSerice {
@Autowired
private PaymentDao paymentDao;
@Override
public int create(Payment payment) {
return paymentDao.create(payment);
}
@Override
public Payment getPaymentById(Long id) {
return paymentDao.getPaymentById(id);
}
}
controller:
package com.atguigu.springcloud.controller;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import com.atguigu.springcloud.service.PaymentSerice;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@Slf4j
public class PaymentController {
@Autowired
private PaymentSerice paymentSerice;
@PostMapping(value = "/payment/create")
public CommonResult create(@RequestBody Payment payment){
int result = paymentSerice.create(payment);
log.info("插入结果:"+result);
if(result>0){
return new CommonResult(200,"插入数据库成功",result);
}else{
return new CommonResult(444,"插入数据库失败",null);
}
}
@GetMapping(value = "/payment/get/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id) {
Payment payment = paymentSerice.getPaymentById(id);
log.info("查询结果:"+payment);
if(payment!=null){
return new CommonResult(1200,"查询成功",payment);
}else{
return new CommonResult(444,"没有对应记录",null);
}
}
}
最重要的是记住:controller调用了service,service调用dao层。
由于前后端交互采用json串,所以在controller层返回值均为CommonResult类
server:
port: 80
作为消费者类没有service和dao,而是需要调用pay模块的方法,并且这里还没有微服务的远程调用,那么如果要调用另外一个模块,则需要使用基本的api调用,可以使用RestTemplate调用pay模块。
RestTemplate介绍:
原本发送Http请求我们会使用封装好的HttpClient工具类,而Spring为我们提供了RestTemplate工具集。它提供了多种便携访问远程Http服务的方法,是一种简单便捷的访问restful服务模板类。
config配置类
package com.atguigu.springcloud.congif;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class ApplicationContextConfig {
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
controller
package com.atguigu.springcloud.controller;
import com.atguigu.springcloud.entities.CommonResult;
import com.atguigu.springcloud.entities.Payment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
@RestController
@Slf4j
public class OrderController {
public static final String PAYMENT_URL = "http://localhost:8001";
@Autowired
private RestTemplate restTemplate;
@GetMapping("/consumer/payment/create")
public CommonResult create(Payment payment){
return restTemplate.postForObject(PAYMENT_URL+"/payment/create",payment, CommonResult.class);
}
@GetMapping("/consumer/payment/get/{id}")
public CommonResult getPayment(@PathVariable("id") Long id){
return restTemplate.getForObject(PAYMENT_URL+"/payment/get/"+id,CommonResult.class);
}
}
代码结束
目前搭建了简单的“消费者80端口调用提供者端口的8001项目”,消费者下订单,跳转提供者支付功能,注意到现在是两个端口,但是实际上可能有消费者集群和提供者集群,这样就必须需要一些技术来提供服务注册。
已经实现了两个服务模块一个端口通过http调用另一个端口,当模块多时就不好监控了,引入注册中心,避免单点故障,采取集群
Eureka包含了两个组件:
上面的图,consumer是80,provider是8001,都有了,缺少eureka server,创建他
pom文件:注意第一个spring-cloud-starter-netflix-eureka-server代表这是服务器
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>cloud2020artifactId>
<groupId>com.atguigu.springcloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloud-eruka-server7001artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
<dependency>
<groupId>com.atguigu.springcloudgroupId>
<artifactId>cloud-api-commonartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
dependency>
dependencies>
project>
yml文件:
server:
port: 7001
eureka:
instance:
hostname: localhost #eureka服务端的实例名字
client:
register-with-eureka: false
#表识不向注册中心注册自己
fetch-registry: false
#表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
#设置与eureka server交互的地址查询服务和注册服务都需要依赖这个地址
主启动类:注意注解@EnableEurekaSever代表这个是服务器端
启动访问localhost:7001,得到:
更改之前的8001端口服务即可,cloud-provider-payment8001
添加pom:
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
添加yml:
eureka:
client:
#注册到eureka
register-with-eureka: true
#需要检索服务
fetchRegistry: true
service-url:
defaultZone: http://localhost:7001/eureka
主启动加上注解
同样是client端,pom与上面相同,yml也更改相同。
为了实现高可用(不能总搬家)
RPC:Remote Procedure Call 远程过程调用
互相注册,相互守望,对外暴露一个整体。
如图,两个server,互相注册,7001注册进7002, 7002注册进7001.
由于是单机实现两种,具体步骤看https://blog.csdn.net/MOKEXFDGH/article/details/107258228#comments_16609600
将支付模块、订单模块都这样改即可
上图中有两个服务提供者,负载均衡步骤:
在8001的PaymentController中加入如下代码:
@Resource
private DiscoveryClient discoveryClient;
@GetMapping(value = "/payment/discovery")
public Object discovery(){
List<String> services = discoveryClient.getServices();
for (String element : services) {
log.info("***** element:"+element);
}
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
log.info(instance.getServiceId()+"\t"+instance.getHost()+"\t"+instance.getPort()+"\t"+instance.getUri());
}
return this.discoveryClient;
}
在8001主启动类上面写注解@EnableDiscoveryClient
得到“关于我们”的说明!
某时刻可能某个微服务不可用了,但是Eureka不会立即清理,而是会对其的信息进行保存。
属于CAP的AP分支。
正常情况下,如果EurekaServer在一定时间内没有接收到某个微服务实例的心跳,就会注销该实例。
在linux上安装好zookeeper,再关闭linux的防火墙。
<!--SpringBoot整合Zookeeper客户端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zookeeper-discovery</artifactId>
<exclusions>
<!--先排除自带的zookeeper3.5.3-->
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--添加zookeeper3.6.1版本-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.6.1</version>
</dependency>
server:
port: 8004
spring:
application:
name: cloud-provider-payment
cloud:
zookeeper:
connect-string: hw.mokespace.cn:2181
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain8004 {
public static void main(String[] args){
SpringApplication.run(PaymentMain8004.class,args);
}
}
@RestController
@Slf4j
@RequestMapping("/payment")
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/zk")
public String paymentzk(){
return "springcloud with zookeeper"+serverPort+"\t"+ UUID.randomUUID().toString();
}
}
@RestController
@Slf4j
@RequestMapping("/consumer")
public class OrderController {
private static final String INVOKE_URL = "http://cloud-provider-payment";
@Resource
private RestTemplate restTemplate;
@GetMapping("/payment/zk")
public String paymentInfo(){
return restTemplate.getForObject(INVOKE_URL+"/payment/zk",String.class);
}
}
一段时间内有就还有。没了就没了。Zookeeper比Eureka更加干脆
https://www.consul.io/downloads
中文教程 springcloud.cc/spring-cloud-consul.html
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
server:
port: 8004
spring:
application:
name: cloud-provider-payment
cloud:
consul:
host: localhost
port: 8500
discovery:
service-name: ${spring.application.name}
主启动类、controller 和 8004 差不多,编写好后启动
Nginx当成是个大门,全进去再分
Ribbon就是在家就分好了
pom:
<!--eureka client-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
这里就已经自带了ribbon
RestTemplate类:
xxxForObject()方法:返回的是响应体中的数据
xxxForEntity()方法:返回的是entity对象,包含响应体数据、响应体信息(状态码等)
Ribbon默认自带的负载均衡的算法:
com.netflix.loadbalancer.RoundRobinRule:轮询
com.netflix.loadbalancer.RandomRule:随机
com.netflix.loadbalancer.RetryRule:先轮询,失败则在指定时间内进行重试
WeightedResponseTimeRule:对轮询的扩展,响应速度越快权重越大
BestAvailableRule:先过滤处于断路状态的服务,选择一个并发量最小的服务
AvailabilityFilteringRule:先过滤故障实例,再选择并发较小的实例
ZoneAvoidanceRule:默认规则,根据Server所在区域的性能和可用性选择服务器。
改成随机
修改Order模块:
这里使用 Eureka 的相关服务
IRule 自定义配置类不能放在@ComponentScan所扫描的当前包以及子包下(有@SpringBootApplication注解的Main所在的包)源码中有@CompontScan
在这个包中创建配置类
@Configuration
public class MySelfRule {
@Bean
public IRule myRule(){
return new RandomRule();//随机负载均衡算法
}
}
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration = MySelfRule.class)
public class OrderMain80 {
public static void main(String[] args){
SpringApplication.run(OrderMain80.class,args);
}
}
public Server choose(ILoadBalancer lb, Object key) {
if (lb == null) {
log.warn("no load balancer");
return null;
} else {
Server server = null;
int count = 0;
while(true) {
if (server == null && count++ < 10) {
List<Server> reachableServers = lb.getReachableServers();
List<Server> allServers = lb.getAllServers();
int upCount = reachableServers.size();
int serverCount = allServers.size();有效集群的总数量
if (upCount != 0 && serverCount != 0) {
int nextServerIndex = this.incrementAndGetModulo(serverCount);
server = (Server)allServers.get(nextServerIndex);
if (server == null) {
Thread.yield();
} else {
if (server.isAlive() && server.isReadyToServe()) {
return server;
}
server = null;
}
continue;
}
log.warn("No up servers available from load balancer: " + lb);
return null;
}
if (count >= 10) {
log.warn("No available alive servers after 10 tries from load balancer: " + lb);
}
return server;
}
}
}
@GetMapping("/lb")
public String getPaymentLB(){
return serverPort;
}
public interface LoadBalancer {
//从服务列表中选取一个服务
ServiceInstance instances(List<ServiceInstance> serviceInstances);
}
@Component
public class MyLB implements LoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);
private final int getAndIncrement(){
int current;
int next;
do{
current = this.atomicInteger.get();
next = current >= Integer.MAX_VALUE ? 0 : current+1;
}while(!this.atomicInteger.compareAndSet(current,next));
System.out.println("****next: "+next);
return next;
}
@Override
public ServiceInstance instances(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
@Resource
private LoadBalancer loadBalancer;//自定义的
@Resource
private DiscoveryClient discoveryClient;
@GetMapping("/payment/lb")
public String getPaymentLB(){
List<ServiceInstance> serviceInstances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if(serviceInstances == null || serviceInstances.size() <= 0){
return null;
}
ServiceInstance instance = loadBalancer.instances(serviceInstances);
URI url = instance.getUri();
return restTemplate.getForObject(url+"/payment/lb",String.class);
}
已经有Ribbon了,OpenFeign是干嘛的?
是Ribbon的进一步封装,仅需写接口以后加注解。
<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
@SpringBootApplication
@EnableFeignClients //开启
public class OrderFeginMain80 {
public static void main(String[] args){
SpringApplication.run(OrderFeginMain80.class,args);
}
}
@Component
@FeignClient(value = "CLOUD-PAYMENT-SERVICE")
public interface PaymentService {
@GetMapping("/payment/get/{id}")
public CommonResult getPaymentById(@PathVariable("id") Long id);
}
@RestController
@Slf4j
@RequestMapping("/consumer")
public class OrderFeginController {
@Resource
private PaymentService paymentService;
@GetMapping("/payment/get/{id}")
public CommonResult<Payment> getPyamentById(@PathVariable("id")Long id){
return paymentService.getPaymentById(id);
}
}
接口加注解的形式(客户端):
服务提供者:提供一个耗时的服务。
默认只等1秒,所以超时以后就不行了,具体的超时时间设置在yaml中设置即可:
ribbon:
# 指的是建立连接后从服务器读取到可用资源所用的时间, 默认1000,即1s
ReadTimeout: 5000
# 指的是建立连接所用的时间,适用于网络状态正常的情况下,两端连接所用的时间
ConnectTimeout: 5000
日志分类:
@Configuration
public class FeignConfig {
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;//日志级别
}
}
logging:
level:
# feign 日志以什么级别监控哪个接口
com.moke.springcloud.service.PaymentService: debug
背景:服务雪崩
多个微服务之间调用时,假设微服务 A 调用微服务 B 和微服务 C,微服务 B 和微服务 C 又调用其它的微服务,这就是所谓的”扇出“。如果扇出的链路上某个微服务的调用响应时间过长或者不可用,对微服务 A 的调用就会占用越来越多的系统资源,进而导致系统”雪崩效应“。
服务降级:比如当某个服务繁忙,不能让客户端的请求一直等待,应该立刻返回给客户端一个备选方案。(兜底的解决方法)
服务熔断:当某个服务出现问题,卡死了,不能让用户一直等待,需要关闭所有对此服务的访问然后调用服务降级。(保险丝,类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示)
服务限流:比如秒杀场景,不能访问用户瞬间都访问服务器,限制一次只可以有多少请求。
接近实时的监控
@Service
public class PaymentService {
//成功
public String paymentInfo_OK(Integer id){
return "线程池:"+Thread.currentThread().getName()+" paymentInfo_OK,id: "+id+"\t"+"哈哈哈" ;
}
//失败
public String paymentInfo_TimeOut(Integer id){
int timeNumber = 3;
try { TimeUnit.SECONDS.sleep(timeNumber); }catch (Exception e) {e.printStackTrace();}
return "线程池:"+Thread.currentThread().getName()+" paymentInfo_TimeOut,id: "+id+"\t"+"呜呜呜"+" 耗时(秒)"+timeNumber;
}
}
controller:
@RestController
@Slf4j
public class PaymentController {
@Resource
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id){
String result = paymentService.paymentInfo_OK(id);
log.info("*******result:"+result);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentService.paymentInfo_TimeOut(id);
log.info("*******result:"+result);
return result;
}
}
主启动类
yml文件:
server:
port: 8001
eureka:
client:
register-with-eureka: true #表识不向注册中心注册自己
fetch-registry: true #表示自己就是注册中心,职责是维护服务实例,并不需要去检索服务
service-url:
defaultZone: http://localhost:7001/eureka/
spring:
application:
name: cloud-provider-hystrix-payment
启动以后:
完全没问题,但是经过Jmeter的压测以后,发现这个变得很慢了。(原因:用尽了tomcat线程数)
正因为有上述故障或不佳表现,才有我们的降级/容错/限流等技术诞生。
Hystrix在消费侧和提供侧都能实现,一般用于消费侧。
cloud-consumer-feign-hystrix-order80:
pom:
<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- hystrix-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
yml:
server:
port: 80
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka7001.com:7001/eureka
主启动类:
@SpringBootApplication
@EnableFeignClients
public class OrderHystrixMain80 {
public static void main(String[] args){
SpringApplication.run(OrderHystrixMain80.class,args);
}
}
Service接口:
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT")
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id);
}
Controller:
@RestController
@Slf4j
@RequestMapping("/consumer")
public class OrderHystrixController {
@Resource
private PaymentHystrixService paymentHystrixService;
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id){
String result = paymentHystrixService.paymentInfo_OK(id);
return result;
}
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
}
PaymentService.java中添加如下:注意这里的fallback方法的名字指定了兜底方法(无论是系统繁忙还是报错了都走这个方法)
/*
会超时报错的方法
*/
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "3000")})//设置调用超时时间的峰值,超时则会调用设置的paymentInfo_TimeOutHandler方法
public String paymentInfo_TimeOut(Integer id){
int timeNumber = 5;
//int age = 10/0;
//其他异常也会触发服务降级
try{
TimeUnit.SECONDS.sleep(timeNumber);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池:"+Thread.currentThread().getName()+" paymentInfo_TimeOut,id:"+id+"\t"+"!!!";
}
public String paymentInfo_TimeOutHandler(Integer id){
return "线程池:"+Thread.currentThread().getName()+" 系统繁忙,请稍后再试,id:"+id+"\t"+"。。。";
}
主启动类加注解
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
public class PaymentHystrixMain8001 {
public static void main(String[] args){
SpringApplication.run(PaymentHystrixMain8001.class,args);
}
}
一般的服务降级都放在客户端即消费者!
feign:
hystrix:
enabled: true
@GetMapping("/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentInfo_TimeOutHandler",commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1500")
})
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
public String paymentInfo_TimeOutHandler(Integer id){
return "本80系统繁忙,请稍后再试,id:"+id+"\t"+"。。。";
}
客户端在1.5s内都OK,超过不玩了!
每个业务逻辑都有一个兜底的,代码膨胀了!
全局的fallback:
在Controller中写:
//全局降级方法
public String paymentInfo_Global_FallbackMethod(){
return "Global异常处理信息,请稍后再试,。。。";
}
再在这个Controller头上加注解:
@RestController
@Slf4j
@RequestMapping("/consumer")
@DefaultProperties(defaultFallback = "paymentInfo_Global_FallbackMethod")
public class OrderHystrixController {
...
}
如果这个方法头上没规定fallback就找全局的,自己规定好了就走自己的:
@HystrixCommand
public String paymentInfo_TimeOut(@PathVariable("id") Integer id){
String result = paymentHystrixService.paymentInfo_TimeOut(id);
return result;
}
全局的fallback代码和业务逻辑混合在一起了,尽量分开
根据cloud-consumer-feign-hystrix-order80已经有的PaymentHystrixService接口,重新新建一个类(PaymentFallbackService)实现该接口,统一为接口里面的方法进行异常处理
创建他的实现类
确保配置文件中开启了feign的hystrix支持
保险丝,比降级更厉害。
熔断机制:
应对雪崩效应的微服务链路保护机制,当扇出链路其中的某个微服务出错不可用了或者响应时间太长了就会进行服务的降级,进而熔断,返回错误的响应信息,当检测到该节点微服务正常以后,恢复链路!
在“时间窗口期”内失败率大于“失败率”就会跳闸
//服务熔断
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback",commandProperties = {
@HystrixProperty(name = "circuitBreaker.enable",value = "true"),//是否启用断路器
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold",value = "10"),//请求次数
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds",value = "10000"),//时间窗口期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage",value = "60")//失败率达到多少后跳闸
})
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
if(id<0)
throw new RuntimeException("****id 不能为负数");
String serialNumber = IdUtil.simpleUUID();//hutool工具
return Thread.currentThread().getName()+"\t"+"调用成功,流水号:"+serialNumber;
}
public String paymentCircuitBreaker_fallback(Integer id){
return "id:"+id+",不能为负数,请稍后再试,。。。";
}
如果并发超过10个且10个并发中失败了6个,就会开启断路器!
在时间窗口期10秒之后会尝试请求(半开),如果请求成功就会关闭断路器!
在Controller中调用service即可:
//服务熔断
@GetMapping("/circuit/{id}")
public String paymentCircuitBreaker(@PathVariable("id") Integer id){
String result = paymentService.paymentCircuitBreaker(id);
log.info("****result:"+result);
return result;
}
断路器打开以后: 自动调用fallback!
主逻辑如何恢复: Hystrix提供自动恢复功能,对主逻辑进行熔断以后,Hystrix会启动一个休眠时间窗,在这个时间内,降级逻辑fallback,过了时间窗口期,进入半开状态,释放一次请求到原来的主逻辑上,如果此次成功了,断路器失效,服务正常,如果此次没成功,继续断路,窗口期 重新计时!
pom
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>
yml:
server:
port: 9001
主启动:
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboarMain9001 {
public static void main(String[] args){
SpringApplication.run(HystrixDashboarMain9001.class,args);
}
}
其他想看的pay模块(8001,8002,8003…)添加一个pom依赖,我们之前都配过了
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
启动9001即可,访问: http://localhost:9001/hystrix
此时仅仅是可以访问HystrixDashboard,并不代表已经监控了8001,8002。如果要监控,还需要配置(8001为例):
注意此处必须mysql8.0!其余都不好使
打开网站:http://localhost:8848/nacos
@RestController
public class PaymentController
{
@Value("${server.port}")
private String serverPort;
@GetMapping(value = "/payment/nacos/{id}")
public String getPayment(@PathVariable("id") Integer id)
{
return "nacos registry, serverPort: "+ serverPort+"\t id"+id;
}
}
主函数:
@EnableDiscoveryClient
@SpringBootApplication
public class PaymentMain9001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9001.class,args);
}
}
yml:
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址
management:
endpoints:
web:
exposure:
include: '*'
pom:
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency><dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
</dependencies>
nacos:AP或CP
A:可用性:所有的请求都会受到响应
C:强一致性:所有节点在同一时间看到的数据是一致的
P:分布容错性
AP:为了服务的可能性减弱了一致性,因此AP模式仅支持注册临时实例!
CP:保证一致性,支持注册持久化实例!
springboot中配置中心的加载优先级,bootstrap>application。先从配置中心拉取再读取本地的。
Controller:
两个yml:bootstrap.yml和application.yml
一图总结:
之后:
注意这里会报错,因为nacos网站上后缀名仅支持yaml,不可写成yml。
注意Controller上的注解**@RefreshScope**,加上这个才支持动态刷新!
Nacos自带嵌入式数据库derby,但是集群中每个Nacos都自带一个,集群环境就必须将其化为一个mysql.
通过以上的步骤就放弃了derby,进入了Mysql。
中文文档 :https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
下载地址 :https://github.com/alibaba/Sentinel/releases
确保有 Java 8 的环境,且 8080 端口没有被占用
进入存放 Sentinel 的目录,并运行命令
java -jar sentinel-dashboard-1.8.0.jar
访问 :http://localhost:8080
登录账号密码均为 :sentinel
启动8848Nacos和8080Sentinel,引入这三个包
sentinel懒加载,必须执行一次访问才能出现。
结论:8080正在监控8848
当关联的资源达到阈值时,就限流自己
实际场景:对于同级别的服务,比如 下单、支付 两个服务,当 支付 流量过高,撑不住了,就限制一下 下单 服务,把资源留给 支付 服务,先撑过去。
在/testB非常忙的时候,无法
只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就进行限流)。
只要从这个入口资源进来的流量超过了阈值就对A限流。
默认 coldFactor 为 3,即请求 QPS 从 threshold/3 开始,经预热时长逐渐升至设定的QPS阈值。
一直发出 /testA 请求
发现刚开始提示 Blocked by Sentinel(flow limiting) 的频率比较高
越往后频率越低
最后几乎不会出现
这就是阈值再慢慢预热的过程
让请求以匀速的速度通过,阈值类型必须设置为QPS,否则无效
每个请求,愿意等就等,不愿意等就超时重试
@GetMapping("/testHotKey")
@SentinelResource(value = "testHotKey", blockHandler = "deal_testHotKey")
public String hotKeyTest(@RequestParam(value = "p1", required = false)String p1,
@RequestParam(value = "p2", required = false)String p2){
return "===== (*^_^*) 成功了 (*^_^*) =====";
}
public String deal_testHotKey(String p1, String p2, BlockException exception){
return "===== (ㄒoㄒ) 失败了 (ㄒoㄒ) =====";
}
Sentinel的断路器是没有半开状态的
pom
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>
即@GlobalTransactional注解,放在业务方法上
TC:全局管理者——seata服务器
TM:事物的发起方——头上戴了这个注解的业务类
RM:事务的参与方——数据库
执行流程:
因为“业务SQL”在一阶段已经提交至数据库,所以Seata框架只需将一阶段保存的快照数据和行锁删掉,完成数据清理即可.