微服务的初步使用

11 篇文章 0 订阅
9 篇文章 0 订阅

目录

环境说明

微服务案例的搭建

新建父工程

微服务模块

product-service(商品服务)

创建子工程

添加依赖

商品模块业务开发

创建业务数据库

测试

order-service(订单服务)

创建子工程

添加依赖

订单模块业务开发

测试

注册中心的使用

搭建注册中心

创建子工程

添加依赖

注册中心代码开发

测试

把服务注册到注册中心

将商品服务注册到注册中心

添加依赖

服务注册

添加服务发现支持

测试

将订单服务注册到注册中心

用服务列表名称进行调用

原理

修改代码

测试

注册中心的高可用

原理

两台Eureka互相注册

把各个微服务注册到两台Eureka中

测试


环境说明

jdk1.8

maven3.6.3

mysql8

idea2022

微服务案例的搭建

新建父工程

打开IDEA,File->New ->Project,填写Name(工程名称)和选择Location(工程存储位置),选择Java语言和Maven,点击Create创建maven工程,该工程为所有工程的父工程

官方查看Spring Cloud与Spring Boot的版本匹配问题

Spring Cloud

Spring Boot2.7.x匹配的Spring Cloud的版本为2021.0.x

修改pom.xml

<?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>org.example</groupId>
    <artifactId>spring-cloud-bk-2023</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.12</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.4</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>


    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>2021.0.8</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <repositories>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>http://repo.spring.io/libs-snapshot-local</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>http://repo.spring.io/libs-milestone-local</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-releases</id>
            <name>Spring Releases</name>
            <url>http://repo.spring.io/libs-release-local</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>http://repo.spring.io/libs-snapshot-local</url>
            <snapshots>
                <enabled>true</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>http://repo.spring.io/libs-milestone-local</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
    </pluginRepositories>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • 父工程引入公共的依赖,例如:所有微服务模块均需要用到spring boot,spring boot依赖版本为2.7.12
  • 同时声明spring cloud的依赖。spring cloud依赖版本为2021.0.8,声明依赖后,以后子工程就不需要再指定spring cloud的相关版本了。
  • dependencies标签里配置远程仓库地址

注意:添加依赖后,需要刷新依赖。

微服务模块

父工程创建好之后,接下来就搭建各个微服务模块,这里以product-service(商品服务)和order-service(订单服务)为例。实现用户下订单的功能。

用户下订单业务流程如下:用户通过浏览器下订单,浏览器发起请求到订单服务,订单服务通过调用商品服务得到商品信息。

product-service(商品服务)
创建子工程

创建product-service子模块,右键父工程->New->Module

填写模块名称:product-service,选择Java,Maven,点击创建,如下图:

添加依赖

修改product-service的pom.xml,在</project>的上方添加如下依赖

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
    </dependencies>

刷新依赖

商品模块业务开发

代码结构如下

实体类

package org.example.product.entity;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.math.BigDecimal;

/**
 * 商品实体类
 */
@Data
@Entity
@Table(name="tb_product")
public class Product {
    @Id
    private Long id;
    private String productName;
    private Integer status;
    private BigDecimal price;
    private String productDesc;
    private String caption;
    private Integer inventory;
}

Dao接口

package org.example.product.dao;

import org.example.product.entity.Product;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;

public interface ProductDao extends JpaRepository<Product, Long>, JpaSpecificationExecutor<Product> {

}

Service接口

package org.example.product.service;

import org.example.product.entity.Product;

public interface ProductService {
    /**
     * 根据id查询
     */
    Product findById(Long id);

    /**
     * 保存
     */
    void save(Product product);
    /**
     * 更新
     */
    void update(Product product);
    /**
     * 删除
     */
    void delete(Long id);
}

Service接口实现类

package org.example.product.service.impl;

import org.example.product.dao.ProductDao;
import org.example.product.entity.Product;
import org.example.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductDao productDao;

    @Override
    public Product findById(Long id) {
        return productDao.findById(id).get();
    }

    @Override
    public void save(Product product) {
        productDao.save(product);
    }

    @Override
    public void update(Product product) {
        productDao.save(product);
    }

    @Override
    public void delete(Long id) {
        productDao.deleteById(id);
    }
}

Controller类

package org.example.product.controller;

import org.example.product.entity.Product;
import org.example.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private ProductService productService;

    @Value("${server.port}")
    private String port;

    @Value("${client.ip-address}")
    private String ip;

    @RequestMapping(value = "/{id}",method = RequestMethod.GET)
    public Product findById(@PathVariable Long id) {
        Product product = productService.findById(id);
        product.setProductName("访问的服务地址:"+ip + ":" + port);
        return product;
    }

    @RequestMapping(value = "",method = RequestMethod.POST)
    public String save(@RequestBody Product product) {
        productService.save(product);
        return "保存成功";
    }

}

启动类

package org.example.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;

@SpringBootApplication
@EntityScan("org.example.product.entity")
public class ProductApplication {
    public static void main(String[] args) {
        SpringApplication.run(ProductApplication.class, args);
    }
}

application.yml配置

server:
  port: 9001
spring:
  application:
    name: service-product
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shop1?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
    username: root
    password: 123
  jpa:
    database: MySQL
    show-sql: true
    open-in-view: true
    generate-ddl: true #自动创建表

client:
  ip-address: 10.111.50.229

注意修改数据库信息,例如url、username、password

创建业务数据库

使用mysql创建数据库:shop1

mysql> create database shop1;

测试

运行启动类:ProductApplication.java

因为application.yml的spring.jpa.generate-ddl 配置为true会自动创建表,启动成功后,刷新数据库能看到tb_product表,表还没有具体数据

手动为tb_product表添加两行测试数据,例如:

浏览器访问

http://localhost:9001/product/1

访问到了数据库的数据 

order-service(订单服务)
创建子工程

子模块:order-service

添加依赖

修改order-service的pom.xml,在</project>的上方添加如下依赖

	<dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.33</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
    </dependencies>

刷新依赖

订单模块业务开发

代码结构如下:

实体类

package org.example.order.entity;

import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import java.math.BigDecimal;

/**
 * 商品实体类
 */
@Data
@Entity
@Table(name="tb_product")
public class Product {
    @Id
    private Long id;
    private String productName;
    private Integer status;
    private BigDecimal price;
    private String productDesc;
    private String caption;
    private Integer inventory;
}

 控制类

package org.example.order.controller;

import org.example.order.entity.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping(value = "/buy/{id}", method = RequestMethod.GET)
    public Product findById(@PathVariable Long id){
        Product product = null;

        //调用其他微服务,get请求用getXxx post请求用postXxx
        product  = restTemplate.getForObject("http://localhost:9001/product/1", Product.class);


        return product;
    }

}

  启动类

package org.example.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EntityScan("org.example.order.entity")
public class OrderApplication {

    /**
     * 使用spring提供的RestTemplate发送http请求到商品服务
     *      1.创建RestTemplate对象交给容器管理
     *      2.在使用的时候,调用其方法完成操作 (getXX,postxxx)
     */
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }


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

  application.yml配置

server:
  port: 9002
spring:
  application:
    name: service-order
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/shop1?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
    username: root
    password: 123
  jpa:
    database: MySQL
    show-sql: true
    open-in-view: true
    generate-ddl: true #自动创建表

client:
  ip-address: 10.111.50.229

注意修改数据库信息。

测试

运行启动类:OrderApplication.java

浏览器访问

http://localhost:9002/order/buy/1

 效果如下

和之前直接访问product服务返回一致,说明order服务调用了product服务

http://localhost:9001/product/1

代码总结:

  1. 在order启动类,创建RestTemplate对象交给Spring容器管理
  2. 在order控制类,注入restTemplate对象,在具体方法里调用商品服务:restTemplate.getForObject("http://localhost:9001/product/1", Product.class);

注册中心的使用

这里使用Eureka作为注册中心。

搭建注册中心

创建子工程

在父工程下,创建子工程模块eureka_server

eureka_server代码结构如下

添加依赖

修改eureka_service的pom.xml,在</project>的上方添加如下依赖

	<dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>

刷新依赖

注册中心代码开发

启动类

package org.example.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
// 激活eurekaserver
@EnableEurekaServer
public class EurekaServerAppliation {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerAppliation.class, args);
    }
}

application.yml配置文件

spring:
  application:
    name: eureka-server
server:
  port: 9000
eureka:
  instance:
    hostname: localhost
  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
  server:
    enable-self-preservation: false
    eviction-interval-timer-in-ms: 4000

注意:eureka要顶格写,没有缩进。

Eureka配置含义:

register-with-eureka:是否将自己注册到注册中心

fetch-registry:是否从eureka中获取注册信息

service-url:配置暴露给Eureka Client的请求地址

enable-self-preservation:关闭自我保护

eviction-interval-timer-in-ms:剔除服务间隔的时间

测试

运行启动类

浏览器访问

http://localhost:9000/

能看到如下界面,说明eureka注册中心服务搭建成功 

把服务注册到注册中心

把各个微服务注册到注册中心步骤如下:

1.添加EurekaClient依赖

2.服务注册:修改application.yml添加EurekaServer的信息

3.修改启动类,添加服务发现的支持(可选)

将商品服务注册到注册中心

把product-serviceI商品服务注册到Eureka注册中心。

添加依赖

修改product-service的pom.xml,添加如下依赖

		<!-- 引入EurekaClient -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

刷新依赖

服务注册

修改product-service的application.yml添加EurekaServer的信息

eureka:
  client:
    service-url:
      defaultZone: http://localhost:9000/eureka/

添加服务发现支持

添加服务发现支持有3中方式,任意选3种方式其中之一进行操作。

方式1:

在启动类上方添加@EnableEurekaClient

// 激活EurekaClient
@EnableEurekaClient
public class ProductApplication {

方式2:在启动类上方添加@EnableDiscoveryClient

方式3:启动类不用加注解

测试

启动eureka服务和product服务

浏览器访问

http://localhost:9000/

Instances currently registered with Eureka看到了一行SERVICE-PRODUCT相关数据,说明商品服务成功注册到了Eureka注册中心

将订单服务注册到注册中心

与注册到商品服务同样的方式,把order-service(订单服务)注册到eureka中。

用服务列表名称进行调用

原理

之前的调用方式如下,直接把调用的服务地址写在代码(硬编码)里,如果调用的服务地址变化了,相应调用的地方都需要修改,代码耦合度高。

product  = restTemplate.getForObject("http://localhost:9001/product/1", Product.class);

解决代码耦合度高的方法是把所有服务都注册到注册中心,调用时使用的是服务名进行调用,服务名字到注册中心找到(发现)对应的服务地址,然后发起服务调用。

图中服务发现是通过服务名称从Eureka中拿到服务的元数据: 服务的主机名,ip等,只要服务名称不变,服务地址发生变化后只要把最新变化的信息注册到Eureka,就能从Eureka拿到最新的元数据,把元数据中的主机名和ip等信息进行拼接发起服务调用,从而避免服务调用的硬编码问题。

修改代码

修改OrderController.java

使用服务名称到Eureka发现服务实例

// 通过服务名称获取实例,同一个服务名称可能有多个实例
List<ServiceInstance> instances = discoveryClient.getInstances("SERVICE-PRODUCT");

 完整代码如下

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 注入DiscoveryClient
     *   springcloud提供的获取原数组的工具类
     *      调用方法获取服务的元数据信息
     */
    @Autowired
    private DiscoveryClient discoveryClient;

    @RequestMapping(value = "/buy/{id}", method = RequestMethod.GET)
    public Product findById(@PathVariable Long id){
        Product product = null;
        // 通过服务名称获取实例,同一个服务名称可能有多个实例
        List<ServiceInstance> instances = discoveryClient.getInstances("SERVICE-PRODUCT");
        for (ServiceInstance instance : instances) {
            System.out.println(instance);
        }

        //调用其他微服务,get请求用getXxx post请求用postXxx
        product  = restTemplate.getForObject("http://localhost:9001/product/1", Product.class);


        return product;
    }

}

在如下方是添加断点,进行调试。看到instance里面的信息有ipAddr主机信息和port端口信息。

拼接服务主机和端口,进行调用

        //获取对应的服务        
        ServiceInstance instance = instances.get(0);
        //解析得到主机和端口
        String host = instance.getHost();
        int port = instance.getPort();

        //调用其他微服务,拼接服务调用url
        product  = restTemplate.getForObject("http://"+host+":"+port+"/product/1", Product.class);

完整代码如下

package org.example.order.controller;

import org.example.order.entity.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;

import java.util.List;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private RestTemplate restTemplate;

    /**
     * 注入DiscoveryClient
     *   springcloud提供的获取原数组的工具类
     *      调用方法获取服务的元数据信息
     */
    @Autowired
    private DiscoveryClient discoveryClient;

    @RequestMapping(value = "/buy/{id}", method = RequestMethod.GET)
    public Product findById(@PathVariable Long id){
        Product product = null;
        // 通过服务名称(大小写不敏感)获取实例(元数据),同一个服务名称可能有多个实例
        List<ServiceInstance> instances = discoveryClient.getInstances("service-product");
//        for (ServiceInstance instance : instances) {
//            System.out.println(instance);
//        }
        ServiceInstance instance = instances.get(0);
        String host = instance.getHost();
        int port = instance.getPort();

        //调用其他微服务,get请求用getXxx post请求用postXxx
        product  = restTemplate.getForObject("http://"+host+":"+port+"/product/1", Product.class);
        
        return product;
    }

}
测试

启动eureka服务、product服务、order服务

浏览器访问

http://localhost:9002/order/buy/1

 能访问到数据,效果如下

解决了硬编码服务调用问题。

注册中心的高可用

原理

注册中心只有单节点Eureka服务,如果Eureka发生故障,这时候服务调用也被影响,存在单点故障问题。

注册中心的高可用方案是从1台Eureka变为2台Eureka(或更多),即使其中的一台Eureka出现故障,还有其他的Eureka提供服务,确保注册中心的高可用。

具体实现如下:

  1. 两个Eureka互相注册(通过启动两个Eureka实例得到两个Eureka服务)
  2. 各个微服务注册到两台Eureka中

两台Eureka互相注册

通过启动两个Eureka实例得到两个Eureka服务

  • Eureka1服务的端口为9000
  • Eureka2服务的端口为8000

修改eureka_server的application.yml,修改应用名称,把eureka-server1(9000)向eureka-server2(8000)注册

spring:
  application:
    name: eureka-server1
server:
  port: 9000
# Eureka配置
eureka:
  instance:
    hostname: localhost
  client:
    #配置暴露给Eureka Client的请求地址
    service-url:
      defaultZone: http://127.0.0.1:8000/eureka/
  server:
    #关闭自我保护
    enable-self-preservation: false
    #剔除服务间隔的时间
    eviction-interval-timer-in-ms: 4000

启动eureka-server1服务(9000端口)

这时候发现idea控制台输出如下异常,是正常情况,因为8000的实例还没有启动,等8000启动了就好了。

r-0] c.n.d.s.t.d.RedirectingEurekaHttpClient  : Request execution error. endpoint=DefaultEndpoint{ serviceUrl='http://127.0.0.1:8000/eureka/}, exception=java.net.ConnectException: Connection refused: connect stacktrace=com.sun.jersey.api.client.ClientHandlerException: java.net.ConnectException: Connection refused: connect

修改eureka_server的application.yml,修改端口号,修改应用名称,把eureka-server2(8000)向eureka-server1(9000)注册

spring:
  application:
    name: eureka-server2
server:
  port: 8000
# Eureka配置
eureka:
  instance:
    hostname: localhost
  client:
    #配置暴露给Eureka Client的请求地址
    service-url:
      defaultZone: http://127.0.0.1:9000/eureka/
  server:
    #关闭自我保护
    enable-self-preservation: false
    #剔除服务间隔的时间
    eviction-interval-timer-in-ms: 4000

再启动一个eureka实例(模拟启动,真实环境应该是在不同机器启动):右键EurakaServerApplication-->Copy Configuration

修改名字

点开Not Started,启动EurekaApplication2

浏览器访问

http://localhost:9000/

http://localhost:8000/

可以看到访问9000,能看到两个实例,访问8000也能看到两个实例。

我们只实例把9000注册到8000,把8000注册到9000,但不管我们访问到哪一个端口,都能看到两个实例,说明两个Eureka之间能进行信息同步。

可以进一步验证这个结论,查看product-service的application.yml,只向9000注册

我们启动product-service,再次查看9000端口和8000端口

只向9000注册SERVICE-PRODUCT服务,发现9000和8000都有SERVICE-PRODUCT

把各个微服务注册到两台Eureka中

既然存在两个Eureka,每个服务可以同时向这两个Eureka去获取,两个地址用逗号隔开

修改order和product的配置文件

eureka:
  client:
    service-url:
      defaultZone: http://localhost:9000/eureka/,http://localhost:8000/eureka/

测试

启动oder服务和product服务

分别查看9000和8000端口,能看到order服务和product服务都注册成功了

在其中一台Eureka模拟故障,例如停止EurekaServerApplication2,看注册中心是否依然正常可用

查看9000端口,order和product服务均正常看到,说明Eureka高可用实现了。

查看9000的日志报异常,因为9000向8000注册,8000端口服务停止了,所以属于正常情况。

Caused by: java.net.ConnectException: Connection refused: connect

 浏览器访问

http://localhost:9002/order/buy/1

 依然能访问到数据,效果如下

完成!enjoy it!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值