从0开始搭建一个微服务后端系统-基础入门篇

前言

今天开始,我和大家一起,从0开始,基于Spring Cloud Alibaba,搭建一套基本的微服务架构的项目。

主要用到下面的知识内容

  • JDK8/IDEA/Maven
  • Spring/SpringBoot/Spring Cloud Alibaba
  • Dubbo/openfeign:服务间调用
    • 这里之所以引入了两种调用方式,是为了方便根据接口的实际情况,选择合适的调用方式
  • Seata:分布式事务
  • MySQL
  • Redis
  • RabbitMQ/RocketMQ
  • RBAC:这里,我不打算使用常见的 Spring Security/Spring Security OAuth2.0/Shiro等常见的认证授权技术,我打算自己手动撸一套简易的认证授权体系出来

之所以这么干,我还是考虑到一个定制化和灵活性的问题,在真正的大企业中,其认证授权体系是非常个性化的,如果用了某项技术,往往会制约这种个性化的实现。好吧,我这里主要想吐槽的就是 Spring Security太复杂了,做一个简单的功能,要理解的概念太多了。既然RBAC理论和OAuth2.0协议本身已经非常成熟了,我们根据自己的需要,手撸一个也不是什么大问题

理论:微服务与Spring Cloud

什么是微服务

首先,微服务本身,是一种软件设计,软件架构的思想,并不是具体的某一种技术。可千万别把微服务和Spring Cloud给划上等号。

  • 单体应用

我们先来说说传统的单体应用,也就是在一个项目中,把所有的代码都写在一个应用中。

这么做其实在后续功能不断迭代之后,在软件管理和设计上,是会带来种种问题的。

1、应用整体挂机的概率增强

我们写代码的时候要有一个底线思维,就是要把情况往糟糕的情况下去考虑,然后给出解决方案。

只有这样严格要求自己,我们的代码才会更加健壮。

但即使是这样,只要是人,就会犯错,并且,一个项目中的代码,迭代几轮之后,不知道经了多少人的手。

保不齐就有个小伙伴埋了个雷,一旦这个雷爆发,那么我们整个系统也就挂了。

2、技术受限制

当我们在已有的应用中添加功能代码的时候,是需要受当前应用的限制的。

比如说,我举个极端的例子,这个项目比较老了,使用的是Spring2.5,而现在我们做开发一般至少都用JDK8.

那么很不幸,Spring2.5和JDK8是不兼容的。于是我们就没法使用JDK1.8提供的各种实用的特性。

3、部署复杂

这个部署复杂,是针对代码量急剧增加之后来说的。

随着代码量越来越庞大,不管是启动还是测试,上线部署,都将变得越来越慢。

有时候明明改的是一个很小的功能,但是没办法,我们就是需要整个项目都部署一遍

  • 分布式

与单体应用相对的,就是分布式系统。

所谓分布式,就是说我们把一个项目,按照一定的划分原则,将各个功能的实现拆开来,部署的时候分开部署。

应用之间通过约定的方式调用。一般是http协议或者rpc协议(如 gRPC、thrift、Dubbo等)。

当我们按照功能拆开后进行部署和开发,带来的好处是很明显的,至少把上面讲的,单体应用的几个缺点,是都给解决掉了。

但是同时,也引入了新的问题

1、事务怎么办,在单体应用中,一个进程中操作一个数据库,回滚是非常方便的,直接使用数据库的特性就可以实现回滚

2、基础功能重复开发怎么办?即如何复用?各个模块中有些功能可能是公用的,一旦拆开后,怎么复用这些代码?

3、测试困难,原先只要启动一个应用就够了,现在如果一个功能的测试,依赖于多个模块,那么相关模块就都要运行起来

但是,后端开发进入现在这个阶段,我们对大数据量高并发的要求,相对于分布式系统的一点点缺点,其带来的好处是不言而喻的。

所以,现在基本上一家企业如果已经过了市场验证阶段,没理由继续使用单体应用,肯定都是要改造成分布式系统的。

  • 微服务

那么什么是微服务呢?

微服务肯定是分布式的。那么微服务和分布式系统又有什么区别呢?

以我自己不成熟的看法来说的话,所谓微服务,除了解决分布式系统领域的问题之外,其特点,重在一个微。

怎么样的一个服务,可以称之为微服务呢?这个才是难点。一个系统中,我们怎么拆分,不管是从业务的角度还是技术的角度,各个应用单元之间如何协作,这个才是微服务设计中最最复杂的部分,技术上的难点,其实互联网上的大厂,基本上都已经有解决方案了。

从0开始搭建一个微服务后端系统-基础入门篇_第1张图片

微服务设计6大原则

  • 微服务和单体应用的选择

既然微服务有这么多优点,单体应用有那么多缺点,我们在实际项目中到底选择使用哪种开发模式呢?

这个其实和公司当前的实际情况是挂钩的,我们做开发的,一定要理清楚一点,技术,是为业务服务的,或者说是为了需求服务的。

千万不要为了技术而技术。如果一家公司,刚起步,人手不足、业务量也不大、技术实力也不够雄厚,这个时候贸贸然跟风上微服务,这不就是自己给自己找事儿做吗?

当公司业务上来了,团队规模也起来了,人员配备足够覆盖微服务本身的规模,能够驾驭了,这个时候,才能够切实感受到微服务对整个系统带来的好处。

Spring Cloud和微服务是什么关系

Spring Cloud,就是针对微服务的一套实现。

比较著名的所有 Spring Cloud Netflix和Spring Cloud Alibaba

小结

以上讲的都是一些概念性质的东西,具体怎么开发已经详细的理论,我会在后续实战部分,逐个进行讲解。

理论:微服务软件架构设计

我们先来对这一整个微服务体系,进行整体设计。

从0开始搭建一个微服务后端系统-基础入门篇_第2张图片

从0开始搭建一个微服务后端系统-基础入门篇_第3张图片

第一张图,是我在网上找的一个关于微服务架构的社交

第二张图,我重新精简了一下,基本上就划分为网关、认证中心、其他业务模块这几种类型。

实战:搭建项目骨架

  • chan
    • chan-dep:全局版本依赖管理
    • chan-common:通用工具类
    • chan-auth:认证授权模块
    • chan-user:用户管理模块
    • chan-log:日志模块
    • chan-gateway:网关
    • …:其他业务模块

我们创建了一个项目来进行依赖管理,其他大多数pom关系不大,

不过,对于Spring Cloud、Spring Cloud Alibaba、SpringBoot,这三者之间的版本,是有关联的。

Spring Cloud Alibaba的版本与Spring Boot一致

参见:https://start.spring.io/actuator/info

https://gitee.com/test-qqqq/spring-cloud-alibaba

Spring Cloud的版本与Spring Boot 一致

参考:https://spring.io/projects/spring-cloud/#overview

Spring Cloud Alibaba及其组件(Dubbo、Seata、Sentinel、Nacos),他们之间的版本也是有关联的。

我们在配置这几个组件的版本的时候,最好先去网上查一下,他们是怎么搭配的。

一般这种配置,除非是大版本的更新,否则一旦配置好后,就不要去修改了,避免引起不必要麻烦

理论:注册中心之Nacos

多个服务之间在相互调用的时候,是需要建立连接的。

那么他们怎么知道对方部署时的ip和port是多少呢?

  • 代码中写死?
    • 重启怎么办?
    • 扩容怎么办?

这个时候,就要用到注册中心。

  • 在微服务中,注册中心是个什么概念

注册中心主要是用来服务注册和发现的

实战:服务间简单调用

Nacos环境搭建

参考

git clone https://gitee.com/test-qqqq/nacos-docker.git

我这里就直接把nacos-docker clone到chan项目了,但是,我把nacos-docker加入了git忽略列表,不提交。

之所以将nacos-docker放到chan中,只是为了将所有与项目有关的物料,都放在此。

cd nacos-docker
# 启动单机版,用于开发测试足矣,生产上如果是部署在阿里云环境,可以购买服务,也不需要自己构建
docker-compose -f example/standalone-mysql-5.7.yaml up -d
# 停止
docker-compose -f example/standalone-mysql-5.7.yaml down

不过,这个standalone-mysql-5.7.yaml,里面的镜像速度超有点慢,我把他们都上传到自己的镜像仓库了,然后修改yaml文件如下

version: "2"
services:
  nacos:
    image: registry.cn-hangzhou.aliyuncs.com/sherry/nacos-server:latest
    container_name: nacos-standalone-mysql
    env_file:
      - ../env/nacos-standlone-mysql.env
    volumes:
      - ./standalone-logs/:/home/nacos/logs
      - ./init.d/custom.properties:/home/nacos/init.d/custom.properties
    ports:
      - "8848:8848"
      - "9555:9555"
    depends_on:
      - mysql
    restart: on-failure
  mysql:
    container_name: mysql
    image: registry.cn-hangzhou.aliyuncs.com/sherry/nacos-mysql:5.7
    env_file:
      - ../env/mysql.env
    volumes:
      - ./mysql:/var/lib/mysql
    ports:
      - "3306:3306"
  prometheus:
    container_name: prometheus
    image: registry.cn-hangzhou.aliyuncs.com/sherry/prometheus:latest
    volumes:
      - ./prometheus/prometheus-standalone.yaml:/etc/prometheus/prometheus.yml
    ports:
      - "9090:9090"
    depends_on:
      - nacos
    restart: on-failure
  grafana:
    container_name: grafana
    image: registry.cn-hangzhou.aliyuncs.com/sherry/grafana:latest
    ports:
      - 3000:3000
    restart: on-failure

1598028159897

  • 验证

http://localhost:8848/nacos

默认账号密码均为nacos

从0开始搭建一个微服务后端系统-基础入门篇_第4张图片

OpenFeign

  • 创建chan-auth和chan-user,通过OpenFeign,chan-auth调用chan-user

从0开始搭建一个微服务后端系统-基础入门篇_第5张图片

  • chan-user模块

1、编写一个正常的接口

@RequestMapping("/user")
@Slf4j
@RestController
public class UserController {
   

    @GetMapping(params = "userId")
    public R userInfo(@RequestParam String userId) {
   
        return R.ok("返回id为" + userId + "的用户信息");
    }

}

2、在api模块中暴露此接口

@FeignClient(name = "chan-user-svc")
public interface UserServiceApi {
   

    /**
     * 查询
     *
     * @param userId
     * @return
     */
    @GetMapping("/user")
    R user(@RequestParam(name = "userId") String userId);
}
  • chan-auth

1、引入user-api模块

        <dependency>
            <groupId>com.zhanglngroupId>
            <artifactId>chan-user-apiartifactId>
            <version>${project.parent.version}version>
        dependency>

2、auth模块调用user

从0开始搭建一个微服务后端系统-基础入门篇_第6张图片

3、auth模块中打开Feign支持

从0开始搭建一个微服务后端系统-基础入门篇_第7张图片

测试

  • 依次启动ChanAuthApplication和ChanUserApplication

从0开始搭建一个微服务后端系统-基础入门篇_第8张图片

  • 调用接口

从0开始搭建一个微服务后端系统-基础入门篇_第9张图片

实战:配置中心之Nacos

实战:网关

跨域问题

package com.zhangln.chan.gateway.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

@Configuration
public class CorsConfig {
   
    @Bean
    public CorsWebFilter corsFilter() {
   
        CorsConfiguration config = new CorsConfiguration();
        config.addAllowedMethod("*");
        config.addAllowedOrigin("*");
        config.addAllowedHeader("*");

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);

        return new CorsWebFilter(source);
    }

路由规则配置

spring:
  application:
    name: chan-gateway-svc
  main:
    allow-bean-definition-overriding: true
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
    gateway:
      discovery:
        locator:
          enabled: true
      routes:
        - id: auth_route
          uri: lb://chan-auth-svc
          predicates:
            - Path=/auth/**
        - id: user_route
          uri: lb://chan-user-svc
          predicates:
            - Path=/user/**

全局日志

/**
 * 日志记录
 *
 * @author sherry
 * @description
 * @date Create in 2020/8/22
 * @modified By:
 */
@Component
@Slf4j
public class LogFilter implements GlobalFilter, Ordered {
   
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
   

        GatewayLogDto gatewayLogDto = new GatewayLogDto();
        gatewayLogDto.setId(UUID.randomUUID().toString().replace("-", ""));
        gatewayLogDto.setReqTime(LocalDateTime.now());

        log.info("{},日志过滤器--start", gatewayLogDto.getId());


        ServerHttpRequest serverHttpRequest = exchange.getRequest();
        ServerHttpResponse serverHttpResponse = exchange.getResponse();

        gatewayLogDto.setUri(serverHttpRequest.getURI().getRawPath());
        HttpHeaders headers = serverHttpRequest.getHeaders();
        gatewayLogDto.setHeaderMap(headers);

        gatewayLogDto.setHost(headers.getHost().getHostName());


        MultiValueMap<String, String> queryParams = serverHttpRequest.getQueryParams();
        gatewayLogDto.setParamMap(queryParams);

        String method = serverHttpRequest.getMethodValue();
        gatewayLogDto.setMethod(method);
        String contentType = serverHttpRequest.getHeaders().getFirst("Content-Type");

        //向headers中放文件,记得build
        ServerHttpRequest host = null;
        try {
   

            URI newUri = null;
//            对URL参数进行URL编码
            if (!Objects.isNull(queryParams)) {
   
                StringBuilder query = new StringBuilder();
                Set<String> keySet = queryParams.keySet();
                for (Iterator<String> iterator = keySet.iterator(); iterator.hasNext(); ) {
   
                    String k = iterator.

你可能感兴趣的:(SpringCloud与微服务)