基于springcloud的微服务设计

280 阅读7分钟

前言:随着互联网的快速发展和用户规模的急剧增长,传统的单体应用架构已经远远超出了单一电脑能处理的极限。在这种背景下,微服务架构应运而生。从原理上说,微服务架构通过将单一应用拆分成多个小型、独立的服务,这些服务可以分布在多个计算机上共同工作,体现的是分而治之的思想。除此任意微服务可扩容。对某热点服务单独扩容,提高经济价值和资源利用率。本篇就介绍如何搭建微服务,实现服务的注册,发现,调用。

服务注册和发现

微服务项目本身其实就是一个个的springboot项目的有机组合。springcloud也要是在springboot基础上搭建的,就像springboot是基于spring拓展出的。但单独的微服务,并没有关联。需要使用一个注册中心进行管理不同服务的信息。可以使用nacos进行服务注册和发现。nacos是比较主流的注册中心,相对于zookeeper在配置管理上更加方便。

nacos安装

nacos官网安装方式

部署手册概览 | Nacos 官网

考虑到部署的流畅性,这边则采用docker进行部署。

docker run --restart=always --name nacos -e MODE=standalone -p 8848:8848  -p 9848:9848 -d nacos/nacos-server
:v2.1.1

验证可以登录到nacos的后台

默认密码使用 nacos/nacos

nacos配置

创建一个book的命名空间

项目搭建

服务设计遵守一定的基本思路,需要定义服务边界,确保每个服务都有明确的责任

通常可以根据业务功能进行划分服务。

  • 用户服务:负责用户账户管理和认证。
  • 订单服务:处理订单创建、查询和取消等功能。
  • 商品服务:管理商品信息,如价格、库存等。
  • 支付服务:处理支付流程,包括支付网关对接等。
  • 评论服务:管理用户对商品的评价和反馈。

这样不同的服务专注自己的领域内容。团队协作更加高效。

设计说明

本次设计一个简单的图书管理系统说明

服务包含:

用户服务模块

图书借阅服务模块

基础结构

先创建一个普通的java项目,删除src文件夹,和改造父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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.lar</groupId>
    <artifactId>book</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>pom</packaging>
    <name>book</name>
    <description>book</description>

    <modules>
        <module>user-server</module>
        <module>borrow-server</module>
    </modules>

    <properties>
        <java.version>17</java.version>
        <latest.version>2.3</latest.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
                <version>3.3.5</version>
            </dependency>

            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
                <version>3.3.5</version>
            </dependency>

            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.31</version>
            </dependency>

            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
                <version>2023.0.1.2</version>
            </dependency>

            <!--读取bootstrap文件-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-bootstrap</artifactId>
                <version>4.1.3</version>
            </dependency>

            <!--nacos discovery-->
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
                <version>2023.0.1.2</version>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.34</version>
        </dependency>
        <!--        json包-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.83</version>
        </dependency>

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

这个步骤主要是添加了nacos配置和注册发现的相关依赖。

用户服务模块

对根目录新建一个springboot模块,然后进行创建。下一步骤依赖先什么都不选

修改子模块的pom文件的parent

<parent>
      <groupId>com.lar</groupId>
      <artifactId>book</artifactId>
      <version>0.0.1-SNAPSHOT</version>
  </parent>

然后修改父pom,添加子module管理

子模块添加如下依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--读取bootstrap文件-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bootstrap</artifactId>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

启动类增加@EnableDiscoveryClient注解

在resources下面分别添加application.yml 和bootstrap.yml 文件,增加上配置

application.yml

server:
  port: 8081

boostrap.yml

spring:
  application:
    name: user-server # 服务名称
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848 # nacos地址
      config:
        file-extension: yaml # 文件后缀名
      discovery:
        namespace: 407243bd-1987-4e5e-bdef-5eaacf06bd16
#        shared-configs: # 共享配置
#          - dataId: system-config.yaml 
#          - dataId: datasource.yaml

此处使用第一步骤新建的命名空间隔离开,这在实际开发中调试很有帮助。

对服务添加一个boot服务配置

然后需要点击编译一下

运行启动服务

可以在nacos看见我们的user服务出现在服务列表

图书服务模块

新建模块borrow-server

和上面的类似步骤,唯一不同的是配置上新的端口和服务名要改一下

最终启动后,可以看见新的服务上去了

编写测试代码

本来想用mysql演示下,但是由于篇幅,为了方便说明微服务自身,而不脱离核心,改用模拟数据演示

usermodle的内容

package com.lar.user.model;

import lombok.Data;

@Data
public class User {
    private String username;
    private String age;
    // 是主键也是学号
    private String id;
}

usercontroller内容

package com.lar.user.controller;

import com.lar.user.model.User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import  java.util.Map;

@RestController
@RequestMapping("/api/user/")
public class UserController {
    private static Map<String,User> userMap = new HashMap();
    static {
        User user = new User();
        user.setUsername("张三");
        user.setAge("18");
        user.setId("1");

        User user2 = new User();
        user2.setUsername("李四");
        user2.setAge("18");
        user2.setId("2");

        User user3 = new User();
        user3.setUsername("王五");
        user3.setAge("18");
        user3.setId("3");
        userMap.put("1",user);
        userMap.put("2",user2);
        userMap.put("3",user3);
    }


    @GetMapping("/info/{uid}")
    public User findUserById(@PathVariable("uid") String uid){
       return userMap.get(uid);
    }
}

bookmodel内容

package com.lar.borrow.model;

import lombok.Data;

@Data
public class BookInfo {
    private String id;
    private String bookName;
}

bookcontroller内容

package com.lar.borrow.controller;

import com.lar.borrow.model.BookInfo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.Mapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/api/book/")
public class BookController {
    private static Map<String, BookInfo> bookMap = new HashMap();

    static {
        BookInfo book = new BookInfo();
        book.setBookName("Java并发编程实战");
        book.setId("1");

        BookInfo book2 = new BookInfo();
        book2.setBookName("Effective Java 中文版(第3版)");
        book2.setId("2");

        BookInfo book3 = new BookInfo();
        book3.setBookName("微服务设计与实践");
        book3.setId("3");
        bookMap.put("1", book);
        bookMap.put("2", book2);
        bookMap.put("3", book3);
    }


    @GetMapping("/info/{uid}")
    public BookInfo findBookInfoById(@PathVariable("uid") String uid) {
        return bookMap.get(uid);
    }

    @PostMapping("/list")
    public List<BookInfo> list() {
        return new ArrayList<BookInfo>(bookMap.values());
    }
}

服务调用

直连端口

这边使用apifox客户端请求工具测试

通过服务的具体端口请求服务,非常直接,也是传统单体项目的使用方式

统一网关

上面的方式虽然能使用,但是是有缺陷的。因为试想一下如果使用直连服务,势必会对前端开发人员造成困扰,不同的服务可能不同域名,不同的端口。而后期如果添加新的服务,需要加新的域名端口管理。而且端口暴露过多,也是不安全的做法。容易被针对攻击。所以微服务架构,微服务网关必不可少。必须要实现统一网关,让不同的服务使用统一的端口

新创建一个gateway模块,和上面创建的步骤类似

在bootstrap.yml添加如下内容

spring:
  application:
    name: gateway-server # 服务名称
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848 # nacos地址
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: # 共享配置
          - dataId: router.yaml
      discovery:
        namespace: 407243bd-1987-4e5e-bdef-5eaacf06bd16

yml增加8080端口

增加依赖

<!--网关-->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-gateway</artifactId>
			<version>4.1.5</version>
		</dependency>
		<!--负载均衡-->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-loadbalancer</artifactId>
			<version>4.1.4</version>
		</dependency>

特别注意新的gateway模块要去掉starter-web依赖(否则启动会冲突导致无法启动)

在nacos新增一个router.yml的配置文件,内容如下

spring:
  cloud:
    gateway:
      routes:
        # 用户服务
        - id: user-server
          uri: lb://user-server
          predicates:
            - Path=/api/user/**
        # 书籍服务
        - id: book-server
          uri: lb://borrow-server
          predicates:
            - Path=/api/book/**

修改书籍和用户服务的bootstrap,引入配置

上面就是配置好了对应的路由规则。如/api/book的前缀就会跑到book-server的微服务上去。具体匹配的就是在各自服务上面的name来识别的

同时额外一个好处是随着以后服务的拓展。springcloud gataway能自动的进行负载均衡。

可以看到8080可以直接打通,http://localhost:8080/api/book/list 会自动转发到 http://localhost:8082/api/book/list.

feign调用

很多时候服务的调用不是从客户端发起,即服务和服务内部如何调用的问题。而答案就是用spring cloud中的feign客户端。

在需要的服务增加依赖

<!--openfeign -->
		<dependency>
			<groupId>org.springframework.cloud</groupId>
			<artifactId>spring-cloud-starter-openfeign</artifactId>
		</dependency>

启动类添加注解@EnableFeignClients

新建一个api

@FeignClient(name = "borrow-server")
@Component
public interface BookApi {
    @GetMapping("/api/book/info/{uid}")
    public BookInfo findBookInfoById(@PathVariable("uid") String uid);

}

在使用的地方使用导入即可,实际场景需要把不同微服务的api模块单独封装出去

这边可以进一步增加熔断和降级策略。添加一个fallback类,然后标注

@FeignClient(name = "borrow-server",fallback = BookApiFallBack.class)

public class BookApiFallBack implements BookApi{
    @Override
    public BookInfo findBookInfoById(String uid) {
        BookInfo bookInfo = new BookInfo();
        bookInfo.setBookName("暂无数据");
        return bookInfo;
    }
}

总结

本篇大致从0开始搭建一个微服务创建的过程。包括一些常用的组件的使用。

其实还有很多细节可能是需要自己多多动手尝试才能体会。微服务本身的设计考量很多。需要多看看其他人的设计思路,博采众长,每个人其实都应该有自己的思考,本篇是一个简单引导。希望和大家共勉,共同进步。