本系列笔记涉及到的代码在GitHub上,地址:https://github.com/zsllsz/cloud
本文涉及知识点:
1、consul是什么? consul可以做服务的注册与发现,和eureka、zookeeper类似,也能做配置管理,zookeeper也能做配置管理。由go语言开发,和eureka一样有web可视化界面。
2、安装和运行consul:
consul --version
,如果显示版本信息,则安装成功。consul agent -dev -client 0.0.0.0 -ui
,表示使用开发者模式启动;执行consul agent -server -data-dir /opt/consul -client 0.0.0.0 -ui
表示以服务模式启动。consul启动成功
3、springcloud整合consul:
<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.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>
</dependency>
<!-- consul -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
</dependencies>
server:
port: 8005
spring:
application:
name: consul-provider-payment
#consul相关配置
cloud:
consul:
host: 192.168.0.104
port: 8500
discovery:
service-name: ${spring.application.name}
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain80065{
public static void main(String[] args) throws Exception {
SpringApplication.run(PaymentMain8006.class, args);
}
}
@RestController
@RequestMapping("/payment")
public class PaymentController {
@Value("${server.port}")
private String port;
@GetMapping("/consul")
public String paymentzk() {
return "该服务端口号为:" + port;
}
}
服务成功注册到consul
接下来就消费端:
最后启动payment8005和consumerconsul-order80,可以看到consul的web页面有两个我们自己的这两个服务,并且访问order80可以成功调用到payment8005。
成功调用
4、整合可能遇到的问题:
1、CAP理论:
综上,CAP三者最多只能满足两个,且P是必须有的,因此要么是AP,要么是CP。
2、eureka、zookeeper、consul在CAP理论中的区别:
1、是什么? 是一套实现远程调用和负载均衡的客户端工具。之前我们在客户端order80调用服务端payment8001和payment8002是直接通过RestTemplate来实现的,而Ribbon呢,其实就是负载均衡 + RestTemplate。目前Ribbon也进入了维护模式,不晓得后面后续还会不会更新。
2、能干嘛? 上面说了主要是做服务调用和负载均衡(Load balance,简称LB)。关于负载均衡可能大家会想到nginx的负载均衡,有什么区别呢?
也就是说,我有3个order80,部署在三台服务器,现有15个用户访问order80,nginx挡在最前面,通过负载均衡算法,让这15个请求分摊到这3台服务器上,这就是nginx干的事。然后order80需要调用payment8001,payment8001也在3台服务器上部署着,order80具体调用哪台服务器上的payment8001,这就通过Ribbon来搞定。
3、怎么用? (1):引入依赖:本案例是集合eureka的,我们用的eureka-client的依赖就已经包含Ribbon了,所以不需要额外的依赖。
(2):RestTemplate的xxxForObject方法和xxxForEntity方法的区别:
之前用的是xxxForObject方法,xxxForEntity用法如下:
@GetMapping("/payment/get/info/{id}")
public JsonResult<Payment> getPaymentInfo(@PathVariable("id") Long id){
return restTemplate.getForEntity(PAYMENT_URL + "/payment/" + id, JsonResult.class).getBody();
}
这个方法和getForObject将会是一样的效果,因为我最后只是get了body。
4、Ribbon负载均衡:
5、如何选择负载均衡算法? 下面就修改和eureka集成的那个order80,来选择Ribbon的LB算法。 注意:Ribbon的配置类不能放在与springboot启动类同级的包或者子包下,必须要让@SpringBootApplication注解扫描不到。
@Configuration
public class MyRibbonRule {
// 随机
@Bean
public IRule randRule() {
return new RandomRule();
}
}
@RibbonClient(name = "CLOUD-PAYMENT-SERVICE",configuration=MyRibbonRule.class)
这样就ok了,负载均衡算法就变成了随机。在上面讲了Ribbon自带有7中算法,在配置类中new对应的类即可。
6、手写Ribbon负载均衡算法:
实际调用的服务器位置下标 = 接口第几次请求数 % 服务集群数
比如我们现在有payment8001和payment8002,那么服务集群数就是2,第1次请求时,1 % 2 = 1,实际调用的就是1号服务器;第2次请求时,2 % 2 = 0,实际调用的就是0号服务器。如果服务器重启了,那么就会开始新的一轮轮询。
@Component
public class MyLb implements LoadBalancer{
private AtomicInteger ainteger = new AtomicInteger(0);
private final int getAndIncrement() {
int current;
int next;
// 这里用到的是自旋锁,CAS
do {
current = this.ainteger.get();
next = current >= Integer.MAX_VALUE ? 0 : (current + 1);
} while(!this.ainteger.compareAndSet(current, next));
System.out.println("next的值===========" + next);
return next;
}
@Override
public ServiceInstance instance(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
// ==================== 第一轮开始 ===================
ainteger = 0,
current = aintegere = 0,
next = current + 1 = 1
ainteger调用compareAndSet方法,期望值是0,更新值是1,
此时ainteger内存值是0等于期望值,所以成功将ainteger改为1
该方法返回true,取反就是false,因此跳出循环,将next = 1 返回,第一轮结束。
// ==================== 第二轮开始 ===================
ainteger = 1,
current = ainteger = 1,
next = current + 1 = 2,
ainteger调用compareAndSet方法,期望值是1,更新值是2,
此时ainteger内存值是1等于期望值,所以成功将ainteger改为2
该方法返回true,取反就是false,因此跳出循环,将next = 2 返回,第二轮结束。
所以该方法就是第一次调用就返回1,第二次调用就返回2……
// =================== 第一轮开始 ====================
index = 1 % 2 = 1,
返回1号服务实例
// =================== 第二轮开始 ====================
index = 2 % 2 = 0,
返回0号服务实例
// =================== 第三轮开始 ====================
index = 3 % 2 = 1,
返回1号服务实例
……
所以这两个方法就实现了轮询,接下来看在controller中如何用:
@Autowired
private LoadBalancer loadBalancer;
@Autowired
private DiscoveryClient discoveryClient;
@Autowired
private RestTemplate restTemplate;
@GetMapping("/payment/get/lb/{id}")
public JsonResult<Payment> getPaymentlb(@PathVariable("id") Long id){
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PAYMENT-SERVICE");
if (instances.isEmpty()) {
return null;
}
ServiceInstance service = loadBalancer.instance(instances);
return restTemplate.getForEntity(service.getUri() + "/payment/" + id, JsonResult.class).getBody();
}
这样就自己实现了轮询算法。
1、是什么? 可以理解为就是基于Ribbon又做了一层封装。以前是Ribbon + RestTemplate,OpenFeign相当于封装了它们俩,我们使用时就直接用OpenFeign的注解即可。
2、怎么用? 新建一个consumer-feign-order80的module,用openfeigh做服务的负载与调用,用eureka做服务的注册与发现。
<!-- eureka client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
引入openfeign后,发现它的包下有ribbon,进一步说明了openfeign就是基于ribbon再封装的。
server:
port: 80
eureka:
client:
register-with-eureka: false #不把该项目注册进eureka了
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
@SpringBootApplication
@EnableFeignClients // 激活openfeign
public class OrderFeignMain80 {
public static void main(String[] args) throws Exception {
SpringApplication.run(OrderFeignMain80.class, args);
}
}
@FeignClient(value = "CLOUD-PAYMENT-SERVICE") // 8001的微服务名称
public interface PaymentFeignService {
@GetMapping(value = "/payment/{id}")
JsonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}
加上@FeignClient,value就是8001的微服务名称,然后这个方法就是8001controller中对应的方法。注意路径要写对,不然就会404了。
@RestController
@RequestMapping("/order")
public class OrderFeignController {
@Autowired
private PaymentFeignService pfService;
@GetMapping("/{id}")
public JsonResult<Payment> getPaymentById(@PathVariable("id") Long id){
return pfService.getPaymentById(id);
}
}
最后访问localhost/order/1,也可以成功返回数据。
调用成功
3、openfeign超时控制: order80中通过openfeign去调用payment8001,可能8001处理这个业务需要3秒钟,但是order80等待了1秒钟(openfeign默认就是等待1秒钟)发现还没返回,它就认为失败了,然后就报超时的错误。
try {TimeUnit.SECONDS.sleep(3);} catch (Exception e) {e.printStackTrace();}
然后再通过localhost/order/1去访问,就会发现返回超时错误:
调用超时
默认等1秒钟,但是如果服务端处理确实1秒钟不能完成,那么我们就需要在order80的配置文件钟配置这个超时时间。所以order80的yml就变成了下面这个亚子:
server:
port: 80
eureka:
client:
register-with-eureka: false #不把该项目注册进eureka了
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
ribbon:
# 建立连接后从服务器获取可用资源的时间
ReadTimeout: 5000
# 建立连接所有的时间
ConnectTimeout: 5000
再次访问localhost/order/1,就会发现可以访问了。注意,在yml中加ribbon的超时控制配置时,ide没有提示是正常的,不要以为没有提示就是没有该属性。
4、openfeign的日志打印: openfeign提供了日志打印功能,从而让我们了解到调用细节。我们可以对日志级别进行配置,它提供了以下日志级别:
配置方法:
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
logging:
level:
# openfeign日志以什么级别监控哪个接口
com.zhusl.springcloud.service.PaymentFeignService: debug
现在这样配置就表示以debug级别监控PaymentFeignService的FULL日志。
1、服务的注册与发现:
2、服务负载与调用: