目录

springboot与分布式

分布式应用

在分布式系统中,国内常用zookeeper+dubbo组合,而Spring Boot推荐使用 全栈的Spring,Spring Boot+Spring Cloud。

分布式系统:

http://img.cana.space/picStore/20201027142402.png

单一应用架构

当网站流量很小时,只需一个应用,将所有功能都部署在一起,以减少部署节点和成 本。此时,用于简化增删改查工作量的数据访问框架(ORM)是关键。

垂直应用架构

当访问量逐渐增大,单一应用增加机器带来的加速度越来越小,将应用拆成互不相干 的几个应用,以提升效率。此时,用于加速前端页面开发的Web框架(MVC)是关键。

分布式服务架构

当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服 务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时, 用于提高业务复用及整合的分布式服务框架(RPC)是关键。

流动计算架构

当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个 调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。

Zookeeper和Dubbo

Zookeeper

ZooKeeper 是一个分布式的,开放源码的分布式应用程序协调服务。它是 一个为分布式应用提供一致性服务的软件,提供的功能包括:配置维护、 域名服务、分布式同步、组服务等。

Dubbo

dubbo github地址

Dubbo是Alibaba开源的分布式服务框架,它最大的特点是按照分层的方 式来架构,使用这种方式可以使各个层之间解耦合(或者最大限度地松耦 合)。从服务模型的角度来看,Dubbo采用的是一种非常简单的模型,要 么是提供方提供服务,要么是消费方消费服务,所以基于这一点可以抽象 出服务提供方(Provider)和服务消费方(Consumer)两个角色。

dubbo架构图

http://img.cana.space/picStore/20201027142801.png

dubbo-admin

下载地址

为了监控方便我们引入dubbo-admin,它是一个管理后台,可以查看注册了哪些服务,哪些服务被消费了

使用步骤:

Production Setup

  1. Clone source code on develop branch git clone https://github.com/apache/dubbo-admin.git

    这里不要使用完整clone,会很慢,使用下面的命令:

    1
    
    git clone --depth=1 -b dev https://github.com/apache/dubbo-admin.git
    
  2. Specify registry address in dubbo-admin-server/src/main/resources/application.properties

    dubbo-admin是一个spring-boot项目,修改配置文件即可,我的项目名就是dubbo-admin,默认配置

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    server.port=7001
    spring.velocity.cache=false
    spring.velocity.charset=UTF-8
    spring.velocity.layout-url=/templates/default.vm
    spring.messages.fallback-to-system-locale=false
    spring.messages.basename=i18n/message
    spring.root.password=root
    spring.guest.password=guest
       
    dubbo.registry.address=zookeeper://127.0.0.1:2181
    

    这里使用默认值,直接下一步

  3. Build

    • mvn clean package
  4. Start

    进入到dubbo-admin/dubbo-admin目录下

    • mvn --projects dubbo-admin-server spring-boot:run OR
    • cd dubbo-admin/target; java -jar dubbo-admin-0.0.1-SNAPSHOT.jar

    这里我们采用第二种方法运行

  5. 服务启动成功后,访问http://localhost:7001/,输入在配置文件中的用户名和密码进入管理界面,如下

    http://img.cana.space/picStore/20201027225821.png

springboot整合dubbo

dubbo-spring-boot-starter

完整示例地址

为了上传方便,创建一个空工程 springboot-dubbo-zk-demo

安装zookeeper作为注册中心

docker 安装参考

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
$ docker pull zookeeper:3.4.9
# Start a Zookeeper server instance
$ docker run --name zookeeper --restart always -d zookeeper
# This image includes EXPOSE 2181 2888 3888 8080 (the zookeeper client port, follower port, election port, AdminServer port respectively), so standard container linking will make it automatically available to the linked containers. Since the Zookeeper "fails fast" it's better to always restart it.
# 上面会自动做端口映射,我们不做集群,所以只暴露2181端口即可,记得一定要填端口映射否则localhost:2181访问不到
$ docker run --name zookeeper -p 2181:2181 --restart always -d zookeeper
# david @ Davids-Macbook-Pro in /tmp/docker [0:57:48] C:1
$ de fd bash
root@fde8bab60b90:/apache-zookeeper-3.6.2-bin# ./bin/zkCli.sh
Connecting to localhost:2181
......省略
2020-10-27 17:01:56,759 [myid:localhost:2181] - INFO  [main-SendThread(localhost:2181):ClientCnxn$SendThread@1433] - Session establishment complete on server localhost/127.0.0.1:2181, session id = 0x100044d7a990006, negotiated timeout = 30000

WATCHER::

WatchedEvent state:SyncConnected type:None path:null
[zk: localhost:2181(CONNECTED) 0]
# Ok, zk启动成功
# 监控服务端日志
$ docker logs -f fd

编写服务提供者

引入dubbo和zk依赖

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!--整合dubbo-->
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-spring-boot-starter</artifactId>
    <version>2.7.0</version>
</dependency>
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo</artifactId>
    <version>2.7.0</version>
</dependency>
<!-- Zookeeper -->
<dependency>
    <groupId>org.apache.zookeeper</groupId>
    <artifactId>zookeeper</artifactId>
    <version>3.4.9</version>
    <!--解决日志冲突,排除self4j-log4j12-->
    <exclusions>
        <exclusion>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-log4j12</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>4.2.0</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.2.0</version>
</dependency>

编写服务提供者接口和实现

1
2
3
4
5
package com.eh.ticket.service;

public interface TicketService {
    String getTicket();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
package com.eh.ticket.service;

import org.apache.dubbo.config.annotation.Service;
import org.springframework.stereotype.Component;


@Component
@Service(version = "1.0.0") //可以被扫描到,在项目启动时自动注册到注册中心
public class TicketServiceImpl implements TicketService {
    @Override
    public String getTicket() {
        return "《三国演义》";
    }
}

配置

1
2
3
4
5
6
7
8
server.port=8081
server.context-path=/
# dubbo应用名称
dubbo.application.name=provider-ticket
# 注册中心地址
dubbo.registry.address=zookeeper://localhost:2181
# 哪些服务需要被注册
dubbo.scan.base-packages=com.eh.ticket.service

启动应用,启动服务后会报在zk日志里会看到如下错误:

1
EndOfStreamException: Unable to read additional data from client, it probably closed the socket: address = /172.17.0.1:48996, session = 0x0

这个报错其实不算是一个严重报错,不影响生产者消费者功能的使用。它是由zookeeper管理的其它组件报错引起的,只要把对应报错组件的报错解决,此报错就会消失。(以上内容全为个人推测)

此时在管理后台应该能看到被暴露出来的服务,如下

http://img.cana.space/picStore/20201028013526.png

编写服务消费者

引入依赖,同provider,另外还要引入provider提供的接口所在jar依赖,为了方便直接在consumer工程里建立相同路径的同名接口

使用提供的服务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
package com.eh.user.controller;

import com.eh.ticket.service.TicketService;
import org.apache.dubbo.config.annotation.Reference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    //timeout 可以不指定,但是version一定要指定 不然会找不到服务 直连需要加url="dubbo://localhost:20880"
    @Reference(version = "1.0.0")
    TicketService ticketService;

    @GetMapping("/hello")
    public void hello() {
        String ticket = ticketService.getTicket();
        System.out.println("买到票了:" + ticket);
    }

}

启动消费者应用,可以在dubbo管理后台看到实例被识别了

http://img.cana.space/picStore/20201028014017.png

访问 http://localhost:8082/hello,可以看到在控制台打印

1
买到票了:《三国演义》

至此,一个简单的dubbo+zk的springboot应用整合完毕

Spring Boot和Spring Cloud

Spring Cloud介绍

Spring Cloud是一个分布式的整体解决方案。Spring Cloud 为开发者提供了在分布式系统(配 置管理,服务发现,熔断,路由,微代理,控制总线,一次性token,全局琐,leader选举,分 布式session,集群状态)中快速构建的工具,使用Spring Cloud的开发者可以快速的启动服务 或构建应用、同时能够快速和云平台资源进行对接。

SpringCloud分布式开发五大常用组件

  • 服务发现——Netflix Eureka

  • 客服端负载均衡——Netflix Ribbon

  • 断路器——Netflix Hystrix

  • 服务网关——Netflix Zuul

  • 分布式配置——Spring Cloud Config

spring boot整合Spring Cloud

创建空工程,依次建立eureka-server、provider、consumer

eureka-server

引入依赖:

1
2
3
4
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

配置

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
server:
  port: 8761

eureka:
  instance:
    hostname: eureka-server # eureka实例的主机名
  client:
    register-with-eureka: false # 不做高可用的情况下,不需要把自己注册到注册中心
    fetch-registry: false # 不从eureka上获取服务的注册信息(本身作为注册中心,不是消费者client)
    service-url: # 指定注册中心的地址(provider和client会使用)
      defaultZone: http://localhost:8761/eureka # service-url是一个map,defaultZone默认也是这个url

在主程序上添加注解@EnableEurekaServer,启动后访问http://localhost:8761/ ,可以看到一个类似dubbo-admin的页面

http://img.cana.space/picStore/20201028110647.png

provider

引入eureka发现依赖

1
2
3
4
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

简单编写一个ticket的rest服务,配置provider参数将自己暴露出去

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
server:
  port: 8081

spring:
  application:
    name: provider-ticket

eureka:
  instance:
    prefer-ip-address: true # 注册服务的时候使用服务的ip地址
  client:
    register-with-eureka: true # 把自己注册到注册中心
    fetch-registry: true # 从eureka上获取服务的注册信息
    service-url: # 指定注册中心的地址(provider和client会使用)
      defaultZone: http://localhost:8761/eureka # service-url是一个map,defaultZone默认也是这个url

启动后可以在eureka管理后台看到已经有服务注册进来了

为了演示多个提供者实例,我们使用不同的端口号package多份并分别使用jar命令启动,比如我这里增加一个8082的实例

打包后移动到临时目录

1
2
$ mv provider-ticket-0.0.1-SNAPSHOT.jar /tmp/com.eh/provider-ticket-0.0.1-SNAPSHOT-8081.jar
$ mv provider-ticket-0.0.1-SNAPSHOT.jar /tmp/com.eh/provider-ticket-0.0.1-SNAPSHOT-8081.jar
1
2
3
4
5
6
7
$ pwd
/tmp/com.eh

# david @ Davids-Macbook-Pro in /tmp/com.eh [11:15:33]
$ ll
-rw-r--r--  1 david  staff    44M 10 28 11:11 provider-ticket-0.0.1-SNAPSHOT-8081.jar
-rw-r--r--  1 david  staff    44M 10 28 11:14 provider-ticket-0.0.1-SNAPSHOT-8082.jar

可以看到已经有两个端口对应的提供者应用了,分别启动

1
$ java -jar provider-ticket-0.0.1-SNAPSHOT-8082.jar

查看eureka后台管理页面,可以看到已经有两个提供者实例被注册进来了。

http://img.cana.space/picStore/20201028111750.png

consumer

同样引入eureka发现依赖

1
2
3
4
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

编写配置文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
server:
  port: 8090

spring:
  application:
    name: consumer-user

eureka:
  instance:
    prefer-ip-address: true # 注册服务的时候使用服务的ip地址
  client:
    register-with-eureka: true # 把自己注册到注册中心
    fetch-registry: true # 从eureka上获取服务的注册信息
    service-url: # 指定注册中心的地址(provider和client会使用)
      defaultZone: http://localhost:8761/eureka # service-url是一个map,defaultZone默认也是这个url

添加@EnableDiscoveryClient注解并且注册RestTemplate Bean,使用负载均衡

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.eh.user;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerUserApplication {

    public static void main(String[] args) {
        SpringApplication.run(ConsumerUserApplication.class, args);
    }

    @LoadBalanced // 使用负载均衡机制
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

调用出票服务

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
package com.eh.user.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
public class UserController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/buy")
    public String buyTicket(String username) {
      // PROVIDER-TICKET是提供者应用的名称,可以在管理后台看到
        String ticketName = restTemplate.getForObject("http://PROVIDER-TICKET/ticket", String.class);
        return username + " 购买了" + ticketName;
    }
}

启动消费者服务,查看管理后台

http://img.cana.space/picStore/20201028114818.png

可以看到消费者也被注册进来了,我们访问:http://localhost:8090/buy?username=张三

http://img.cana.space/picStore/20201028114900.png

至此,整合完毕

附:完整示例地址