SpringCloud Alibaba学习笔记
阅读原文时间:2021年06月10日阅读:1

目录

为什么学

  • 组件性能更强
  • 良好的可视化界面
  • 搭建简单,学习曲线低
  • 文档丰富并且是中文

学习目标

  • Spring Cloud Alibaba核心组件的用法及实现原理
  • Spring Cloud Alibaba结合微信小程序从"0"学习真正开发中的使用
  • 实际工作中如何避免踩坑,正确的思考问题方式
  • Spring Cloud Alibaba的进阶:代码的优化和改善,微服务监控

进阶目标

  • 如何提升团队的代码质量

    • 编码技巧
    • 心得总结
  • 如何改善代码结构设计

    • 借助监控工具
    • 定位问题
    • 解决问题

思路

分析并拆解微服务->编写代码->分析现有架构问题->引入微服务组件->优化重构->总结完善

Spring Cloud Alibaba的重要组件

  • 服务发现Nacos

    • 服务发现原理剖析
    • Nacos Server/Client
    • 高可用Nacos搭建
  • 实现负载均衡Ribbon

    • 负载均衡的常见模式
    • RestTemplate整合Ribbon
    • Ribbon配置自定义
    • 如何扩展Ribbon
  • 声明式HTTP客户端-Feign

    • 如何使用Feign
    • Feign配置自定义
    • 如何扩展Feign
  • 服务容错Sentinel

    • 服务容错原理
    • Sentinel
    • Sentinel DashBoard
    • Sentinel核心原理分析
  • 消息驱动RocketMq

    • Spring Cloud Stream
    • 实现异步消息推送与消费
  • API网关Gateway

    • 整合Gateway
    • 三大核心
    • 聚合微服务请求
  • 用户认证与授权

    • 认证授权的常见方案
    • 改造Gateway
    • 扩展Feign
  • 配置管理Nacos

    • 配置如何管理
    • 配置动态刷新
    • 配置管理的最佳实践
  • 调用链监控Sleuth

    • 调用链监控原理剖析
    • Sleuth使用
    • Zipkin使用
  • JDK8

  • MySQL

  • Maven的安装与配置

  • IDEA

Spring Boot特性

  • 无需部署WAR文件
  • 提供stater简化配置
  • 尽可能自动配置Spring以及第三方库
  • 提供"生产就绪"功能,例如指标、健康检查、外部配置等
  • 无代码生成&无XML

编写第一个Spring Boot应用

Spring Boot应用组成分析

  • 依赖:pom.xml
  • 启动类:注解
  • 配置:application.properties
  • static目录:静态文件
  • templates目录:模板文件

Spring Boot开发三板斧

  • 加依赖
  • 写注解
  • 写配置

Spring Boot Actuator

监控工具

/actuator

入口

/health

健康检查

显示详情配置

management.endpoint.health.show-details=always
# 显示所有监控端点
management.endpoints.web.exposure.include=*

# 描述信息(自定义键值对)
info.app-name=spring-boot-demo
info.author=kim
info.email=xxx@163.com

Spring Boot配置管理

支持的配置格式

management:
  endpoint:
    health:
      show-details: always
  endpoints:
    web:
      exposure:
        include: '*'

  # 描述信息
info:
  app-name: spring-boot-demo
  author: kim
  email: xxx@163.com

注意:值是*,yml写法需要加引号

  • yml是使用趋势
  • yml在有的配置中可以表达顺序,properties不行

17种配置方式

实际项目种经常用到的配置管理方式:

  • 配置文件
  • 环境变量
  • 外部配置文件
  • 命令行参数

环境变量方式配置管理

application.yml

management:
  endpoint:
    health:
      show-details: ${SOME_ENV}
  endpoints:
    web:
      exposure:
        include: '*'

  # 描述信息
info:
  app-name: spring-boot-demo
  author: kim
  email: xxx@163.com

设置环境变量SOME_ENV

环境变量方式配置管理(java -jar方式)

mvn clean install -DskipTests

java -jar spring-boot-demo-0.0.1-SNAPSHOT.jar --SOME_ENV=always

外部配置文件方式配置管理

将打的jar包和配置文件放在同一目录,会优先读取该配置文件内配置

命令行参数方式配置管理

java -jar spring-boot-demo-0.0.1-SNAPSHOT.jar --server.port=8081

最佳实践

KISS,规避掉优先级,没人会记住17中配置姿势的优先级。

Profile

# 所有环境下公用的配置属性
management:
  endpoint:
    health:
      show-details: ${SOME_ENV}
  endpoints:
    web:
      exposure:
        include: '*'

  # 描述信息
info:
  app-name: spring-boot-demo
  author: kim
  email: xxx@163.com

# 连字符
---
# profile=x的专用属性,也就是说某个环境下的专用属性
# 开发环境
spring:
  profiles: dev

---
# profile=y的专用属性,也就是说某个环境下的专用属性
# 生产环境
spring:
  profiles: prod
server:
  tomcat:
    max-threads: 300
    max-connections: 1000

IDEA启动配置

访问http://localhost:8080/actuator/configprops通过actuator端口查看

默认使用default,可以通过添加配置设置默认profile

spring:
  profiles:
    active: dev

最佳实践

KISS,不要使用优先级,规划好公用和专用配置

  • 单体架构vs微服务架构

    • 单体架构是什么
    • 微服务是什么
    • 微服务特性
    • 微服务全景架构图
    • 微服务优缺点
    • 微服务适用场景
  • 业务分析与建模

    • 项目功能演示与分析
    • 微服务拆分
    • 项目架构图
    • 数据库设计
    • API文档
  • 编写微服务

    • 创建小程序
    • 创建项目
    • 编写用户微服务
    • 编写内容微服务

单体架构

优点:

  • 架构简单
  • 开发、测试、部署方便

缺点:

  • 复杂性高
  • 部署慢,频率低
  • 扩展能力受限(比如用户模块是CPU密集的,只能通过买更好的CPU的机器,比如内容模块是IO密集的,只能通过购买更多内存)
  • 阻碍技术创新(SpringMVC->Spring Web Flux,改动大)

不适合庞大复杂的系统

微服务

拆分后的小型服务

微服务的特性

  • 每个微服务可独立运行在自己的进程里;(每个服务一个Tomcat)
  • 一系列独立运行的微服务共同构建起整个系统
  • 每个服务为独立的业务开发,一个微服务只关注某个特定的功能,例如订单管理、用户管理
  • 可以使用不同的语言与数据存储技术(契合项目情况和团队实力)
  • 微服务之间通过轻量的通信机制进行通信,例如通过Rest API进行调用;(通信协议轻量、跨平台)
  • 全自动的部署机制

微服务全景架构图

优点

  • 单个服务更易于开发、维护
  • 单个微服务启动较快
  • 局部修改容易部署
  • 技术栈不受限

缺点

  • 运维要求高
  • 分布式固有的复杂性
  • 重复劳动(不同语言调用相同功能时)

适用场景

  • 大型、复杂的项目
  • 有快速迭代的需求
  • 访问压力大(微服务去中心化,把业务和数据都拆分了,可以应对访问压力)

不适用微服务的场景

  • 业务稳定
  • 迭代周期长

项目演示

微服务拆分

  • 业界流行的拆分方法论
  • 个人心得
  • 合理粒度
  • 小程序的拆分

方法论

  • 领域驱动设计(Domain Driven Design)(概念太多,学习曲线高)

  • 面向对象(by name./by verb)(通过名词(状态),动词(行为)拆分)

个人心得

职责划分

规划好微服务的边界。比如订单微服务只负责订单功能。

通用性划分

把一些通用功能做成微服务。比如消息中心和用户中心。

合理的粒度

  • 良好的满足业务(这是前提)
  • 幸福感(你的团队没有人认为微服务太大,难以维护,同时部署也非常高效,不会每次发布都发布N多微服务)
  • 增量迭代
  • 持续进化

小程序的拆分

以面向对象方式拆分

用户中心按照通用性划分,内容中心按照职责划分。

项目初期不建议拆分太细,后期如果发现某个微服务过分庞大再细分。

项目架构图

数据库设计

数据建模
建表

user-center-create-table.sql

USE `user_center`;

-- -----------------------------------------------------
-- Table `user`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `user` (
  `id` INT NOT NULL AUTO_INCREMENT COMMENT 'Id',
  `wx_id` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '微信id',
  `wx_nickname` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '微信昵称',
  `roles` VARCHAR(100) NOT NULL DEFAULT '' COMMENT '角色',
  `avatar_url` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '头像地址',
  `create_time` DATETIME NOT NULL COMMENT '创建时间',
  `update_time` DATETIME NOT NULL COMMENT '修改时间',
  `bonus` INT NOT NULL DEFAULT 300 COMMENT '积分',
  PRIMARY KEY (`id`))
COMMENT = '分享';

-- -----------------------------------------------------
-- Table `bonus_event_log`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `bonus_event_log` (
  `id` INT NOT NULL AUTO_INCREMENT COMMENT 'Id',
  `user_id` INT NULL COMMENT 'user.id',
  `value` INT NULL COMMENT '积分操作值',
  `event` VARCHAR(20) NULL COMMENT '发生的事件',
  `create_time` DATETIME NULL COMMENT '创建时间',
  `description` VARCHAR(100) NULL COMMENT '描述',
  PRIMARY KEY (`id`),
  INDEX `fk_bonus_event_log_user1_idx` (`user_id` ASC) )
ENGINE = InnoDB
COMMENT = '积分变更记录表';

content-center-create-table.sql

USE `content_center`;

-- -----------------------------------------------------
-- Table `share`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `share` (
  `id` INT NOT NULL AUTO_INCREMENT COMMENT 'id',
  `user_id` INT NOT NULL DEFAULT 0 COMMENT '发布人id',
  `title` VARCHAR(80) NOT NULL DEFAULT '' COMMENT '标题',
  `create_time` DATETIME NOT NULL COMMENT '创建时间',
  `update_time` DATETIME NOT NULL COMMENT '修改时间',
  `is_original` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否原创 0:否 1:是',
  `author` VARCHAR(45) NOT NULL DEFAULT '' COMMENT '作者',
  `cover` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '封面',
  `summary` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '概要信息',
  `price` INT NOT NULL DEFAULT 0 COMMENT '价格(需要的积分)',
  `download_url` VARCHAR(256) NOT NULL DEFAULT '' COMMENT '下载地址',
  `buy_count` INT NOT NULL DEFAULT 0 COMMENT '下载数 ',
  `show_flag` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否显示 0:否 1:是',
  `audit_status` VARCHAR(10) NOT NULL DEFAULT 0 COMMENT '审核状态 NOT_YET: 待审核 PASSED:审核通过 REJECTED:审核不通过',
  `reason` VARCHAR(200) NOT NULL DEFAULT '' COMMENT '审核不通过原因',
  PRIMARY KEY (`id`))
ENGINE = InnoDB
COMMENT = '分享表';

-- -----------------------------------------------------
-- Table `mid_user_share`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `mid_user_share` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `share_id` INT NOT NULL COMMENT 'share.id',
  `user_id` INT NOT NULL COMMENT 'user.id',
  PRIMARY KEY (`id`),
  INDEX `fk_mid_user_share_share1_idx` (`share_id` ASC) ,
  INDEX `fk_mid_user_share_user1_idx` (`user_id` ASC) )
ENGINE = InnoDB
COMMENT = '用户-分享中间表【描述用户购买的分享】';

-- -----------------------------------------------------
-- Table `notice`
-- -----------------------------------------------------
CREATE TABLE IF NOT EXISTS `notice` (
  `id` INT NOT NULL AUTO_INCREMENT COMMENT 'id',
  `content` VARCHAR(255) NOT NULL DEFAULT '' COMMENT '内容',
  `show_flag` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否显示 0:否 1:是',
  `create_time` DATETIME NOT NULL COMMENT '创建时间',
  PRIMARY KEY (`id`));

API 文档

课程文档主要分四类:

  1. API文档:https://t.itmuch.com/doc.html
  2. 课程配套代码:https://git.imooc.com/coding-358/
  3. 课程相关资源(例如检表语句、数据模型、课上用到的软件等):https://git.imooc.com/coding-358/resource
  4. 课上用到的一些课外读物(慕课网手记):http://www.imooc.com/t/1863086

如何创建小程序

注册账号:https://mp.weixin.qq.com

按照提示填写信息

前端代码如何使用

创建项目

技术选型
  • Spring Boot
  • Spring MVC
  • Mybatis+通用Mapper
  • Spring Cloud Alibaba(分布式)
工程结构规划

创建项目,整合框架

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>2.1.13.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.itmuch</groupId>
    <artifactId>user-center</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>user-center</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!--        引入通用mapper-->
        <dependency>
            <groupId>tk.mybatis</groupId>
            <artifactId>mapper-spring-boot-starter</artifactId>
            <version>2.1.5</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.mybatis.generator</groupId>
                <artifactId>mybatis-generator-maven-plugin</artifactId>
                <version>1.3.6</version>
                <configuration>
                    <configurationFile>
                        ${basedir}/src/main/resources/generator/generatorConfig.xml
                    </configurationFile>
                    <overwrite>true</overwrite>
                    <verbose>true</verbose>
                </configuration>
                <dependencies>
                    <dependency>
                        <groupId>mysql</groupId>
                        <artifactId>mysql-connector-java</artifactId>
                        <version>8.0.19</version>
                    </dependency>
                    <dependency>
                        <groupId>tk.mybatis</groupId>
                        <artifactId>mapper</artifactId>
                        <version>4.1.5</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>

</project>

通用Mapper包扫描配置

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;//注意是tk的MapperScan注解

@SpringBootApplication
@MapperScan("com.itmuch")
public class UserCenterApplication {

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

}

在resources目录下新建generator目录,添加mybatis.generator配置

generator/generatorConfig.xml

<!DOCTYPE generatorConfiguration
        PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
        "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
    <properties resource="generator/config.properties"/>

    <context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
        <property name="beginningDelimiter" value="`"/>
        <property name="endingDelimiter" value="`"/>

        <plugin type="tk.mybatis.mapper.generator.MapperPlugin">
            <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
            <property name="caseSensitive" value="true"/>
        </plugin>

        <jdbcConnection driverClass="${jdbc.driverClass}"
                        connectionURL="${jdbc.url}"
                        userId="${jdbc.user}"
                        password="${jdbc.password}">
        </jdbcConnection>

        <javaModelGenerator targetPackage="com.itmuch.usercenter.domain.entity.${moduleName}"
                            targetProject="src/main/java"/>

        <sqlMapGenerator targetPackage="com.itmuch.usercenter.dao.${moduleName}"
                         targetProject="src/main/resources"/>

        <javaClientGenerator targetPackage="com.itmuch.usercenter.dao.${moduleName}"
                             targetProject="src/main/java"
                             type="XMLMAPPER"/>

        <table tableName="${tableName}">
            <generatedKey column="id" sqlStatement="JDBC"/>
        </table>
    </context>
</generatorConfiguration>

generator/config.properties

jdbc.driverClass=com.mysql.cj.jdbc.Driver
# nullCatalogMeansCurrent=true 如果不加这个配置,出现表名user在其他库,比如系统库的,会生产系统库的user
jdbc.url=jdbc:mysql://localhost:3306/user_center?nullCatalogMeansCurrent=true
jdbc.user=root
jdbc.password=kim@2020

# 包名
moduleName=user
# 表名
tableName=user

application.yml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/user_center
    hikari:
      username: root
      password: kim@2020
      # >=6.x com.mysql.cj.jdbc.Driver
      # <=5.x com.mysql.jdbc.Driver
      driver-class-name: com.mysql.cj.jdbc.Driver

执行逆向生产代码

整合Lombok简化代码

<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.10</version>
            <scope>provided</scope>
        </dependency>

常用注解

@Data
@NoArgsConstructor//生成无参构造
@AllArgsConstructor//为所有参数生成构造
@RequiredArgsConstructor//为final属性生成构造方法
@Builder //建造者模式
@Slf4j

更多查询官网

通用mapper wikilombok,看有没有生成支持lombok的配置

mybatis.generator添加lombok支持
<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
            <property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
            <property name="caseSensitive" value="true"/>
            <property name="lombok" value="Getter,Setter,ToString"/><!-- 添加的行 -->
        </plugin>

文档也说了,目前只支持@Getter@Setter@ToString@Accessors(chain = true)4种注解,一般我们自己的domain上还是习惯加如下注解:

@Data
@NoArgsConstructor//生成无参构造
@AllArgsConstructor//为所有参数生成构造
@Builder //建造者模式

可以手动加,更简单。

解决IDEA的红色警告

出现警告的原因:

IDEA是非常智能的,它可以理解Spring的上下文。然而 UserMapper 这个接口是Mybatis的,IDEA理解不了。

@Autowired 注解,默认情况下要求依赖对象(也就是 userMapper )必须存在。而IDEA认为这个对象的实例/代理是个null,所以就友好地给个提示

解决方法:参见这篇手记

作业1: 课后研究一下@Resource和@Autowired注解
作业2: 研究@Repository、@Component、@Service、@Controller之间的区别和联系

编写用户微服务和内容微服务

注意:核心业务,一定要设计好业务流程,分析的过程中,使用业务流程图、活动图、用例图、序列图。重视业务和建模,没有建模的微服务是没有灵魂的。

实际开发流程

Schema First

1、分析业务(流程图、用例图…架构图等) 建模业务,确定架构

2、敲定业务流程(评审)

3、设计API/数据模型(表结构设计|类图|ER图)

4、编写API文档

5、编写代码

API First

1、分析业务(流程图、用例图…架构图等) 建模业务,确定架构

2、敲定业务流程(评审)

3、设计API/数据模型(表结构设计|类图|ER图)

4、编写代码

5、编写API文档

但是实际也不是完全按照这样等流程走。

编码。。。

RestTemplate的使用

现有架构存在的问题

  • 硬编码IP,IP变化怎么办
  • 如何实现负载均衡?
  • 用户中心挂了怎么办?

什么是Spring Cloud Alibaba

  • Spring Cloud的子项目
  • 致力于提供微服务开发的一站式解决方案
    • 包含微服务开发的必备组件
    • 基于Spring Cloud,符合Spring Cloud标准
    • 阿里的微服务解决方案

版本与兼容性

  • Spring Cloud 版本命名
  • Spring Cloud 生命周期
  • Spring Boot 、Spring Cloud、Spring Cloud Alibaba的兼容性关系
  • 生产环境怎么选择版本?

Spring Cloud 版本命名

语义化

2.1.13.RELEASE

2:主版本,第几代

1:次版本,一些功能的增加,但是架构没有太大变化,是兼容的

13:增量版本,bug修复

RELEASE:里程碑。SNAPSHOT:开发版 ,M:里程碑 ,RELEASE:正式版

Greenwich SR1 :Greenwich版本的第一个bug修复版

SR:Service Release bug修复

Release Train. 发布列车

伦敦地铁站站名。避免混淆,噱头。

Greenwich RELEASE: Greenwich版本的第一个正式版

Spring Cloud 生命周期

版本兼容性

https://spring.io/projects/spring-cloud-alibaba#overview

https://github.com/alibaba/spring-cloud-alibaba/blob/master/README-zh.md

生产环境怎么选择版本?

  • 坚决不用非稳定版本/end-of-life版本
  • 尽量用最新一代
    • xxx.RELEASE版本缓一缓
    • SR2之后一般可大规模使用

整合Spring Cloud Alibaba

整合好后,引入组件不需要指定版本

服务提供者与服务消费者

名次

定义

服务提供者

服务的被调用方(即:为其他微服务提供接口的微服务)

服务消费者

服务的调用方(即:调用其他微服务接口的微服务)

如何让服务消费者感知到服务提供者

服务消费者内部使用定时任务去服务发现组件获取提供者信息,并缓存到本地,服务消费者每次调用服务提供者从本地缓存那提供者信息。

添加心跳机制,通过心跳机制改变服务状态

什么是Nacos

官网什么是Nacos

搭建Nacos Server

选择Nacos Server版本

查看引入到spring-cloud-alibaba-dependencie依赖

启动服务器

startup.sh -m standalone

访问控制台

http://localhost:8848/nacos

默认用户名密码都是nacos

将应用注册到Nacos

  • 用户中心注册到Nacos
  • 内容中心注册到Nacos
  • 测试:内容中心总能找到用户中心

引入依赖

<dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

配置

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
  application:
    # 服务名称尽量用-,不要用_,不要用特殊字符
    name: content-center

引入服务发现

@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ShareService {
    private final ShareMapper shareMapper;

    private final RestTemplate restTemplate;

    private final DiscoveryClient discoveryClient;

    public ShareDto findById(Integer id){
        //获取分享详情
        Share share = this.shareMapper.selectByPrimaryKey(id);
        //发布人id
        Integer userId = share.getUserId();
        //用户中心所有实例的信息
        List<ServiceInstance> instances = discoveryClient.getInstances("user-center");
        String targetURL = instances.stream()
                .map(instance -> instance.getUri().toString() + "/users/{id}")
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("当前没有实例!"));

        log.info("请求的目标地址:{}", targetURL);
        UserDto userDto = restTemplate.getForObject(
                targetURL,
                UserDto.class,
                userId);
        ShareDto shareDto = new ShareDto();

        //消息的装配
        BeanUtils.copyProperties(share, shareDto);
        shareDto.setWxNickName(userDto.getWxNickname());
        return shareDto;
    }
}

Nacos服务发现的领域模型

Namespace:只要用来实现环境隔离,默认public

Group:默认DEFAULT_GROUP,管理服务分组

Service:微服务

Cluster:微服务集群,对指定微服务的虚拟划分

Instance:微服务实例

如何使用

Namespace,在控制台页面创建。配置的时候使用生成的uuid。

spring:
  cloud:
    nacos:
      discovery:
        # 指定nacos server的地址
        server-addr: localhost:8848
        cluster-name: BJ
        namespace: 56116141-d837-4d15-8842-94e153bb6cfb

Nacos元数据

元数据的作用:

  • 提供描述信息
  • 让微服务调用更灵活
    • 例如:微服务版本控制

如何为微服务设置元数据

  • 控制台界面

  • 配置文件指定

    spring:
    cloud:
    nacos:
    discovery:
    # 指定nacos server的地址
    server-addr: localhost:8848
    cluster-name: BJ
    namespace: 56116141-d837-4d15-8842-94e153bb6cfb
    metadata:
    instance: c
    haha: hehe
    version: 1

负载均衡的两种方式

  • 服务端负载均衡
  • 客户端负载均衡(客户端调用的时候使用选择负载均衡算法)

手写一个客户端负载均衡器

改写一下ShareServicefindById方法。从Nacos获取到URL列表,然后随机从列表中取一个作为本次请求的服务提供者实例。

List<String> targetURLs = instances.stream()
                .map(instance -> instance.getUri().toString() + "/users/{id}").collect(Collectors.toList());

        int i = ThreadLocalRandom.current().nextInt(targetURLs.size());
        String targetURL= targetURLs.get(i);

随后启动content-center

启动多个user-center

配置允许并行运行

修改端口,运行启动类

server:
  port: 8082

使用Ribbon实现负载均衡

  • Ribbon是什么
  • 引入Ribbon后到架构演进
  • 整合Ribbon实现负载均衡

Ribbon是什么

负载均衡器

架构演进

整合Ribbon实现负载均衡

引入Nacos

我们引入spring-cloud-starter-alibaba-nacos-discovery时,已经引入了Ribbon

直接使用就行了。

写注解

@Bean
@LoadBalanced
public RestTemplate restTemplate(){
  return new RestTemplate();
}

配置RestTemplate的地方添加@LoadBalanced注解即可。

使用

UserDto userDto = restTemplate.getForObject(
                "http://user-center/users/{userId}",
                UserDto.class,
                userId);

Ribbon组成

先有个印象。二次开发再回头看

Ribbon内置的负载均衡规则

默认是ZoneAvoidanceRule。

每一个负载均衡算法源码都值得看一下。

细粒度配置自定义

  • Java代码配置
  • 用配置属性配置
  • 最佳实践总结

场景:当内容中心调用用户中心微服务的时候使用随机负载,当内容中心调用其他微服务的时候使用默认负载均衡策略。

Java代码配置

新建配置类,注册一个RandomRule。

package ribbonconfiguration;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * 配置类所在的包必须是和启动类不一样的包
 */
@Configuration
public class RibbonConfiguration {

    @Bean
    public IRule ribbonRule(){
        return new RandomRule();
    }
}

新建一个user-centerribbon负载配置类,配置规则使用上面的随机规则。

package com.itmuch.contentcenter.configuration;

import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Configuration;
import ribbonconfiguration.RibbonConfiguration;

@Configuration
@RibbonClient(name = "user-center",configuration = RibbonConfiguration.class)
public class UserCenterRibbonConfiguration {
}

@RibbonClient注解配置Ribbon自定义配置

name="user-center"表示为user-center配置的。

configuration = RibbonConfiguration.class用来指定负载均衡算法,或者负载均衡规则

父子上下文

这里的上下文是指Spring Context

启动类拥有一个上下文,是父上下文,Ribbon会启动一个子上下文,父子上下文不能重叠

启动类的上下文,会扫描启动类所在包及子包下的Bean。

Ribbon的配置类不能被启动类的上下文扫描到。因为Spring context是一个树状上下文。父子上下文扫描到包如果重叠会有各种问题。比如,导致事务不生效

如果上面配置的RibbonConfiguration在启动类扫描范围内,会导致自定义配置失效,RibbonConfiguration配置的随机负载均衡全局生效。

配置属性方式

user-center:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

这种方式没有上下文重叠的坑

两种配置方式的对比

细粒度配置最佳实践

  • 尽量使用属性配置,属性方式实现不了的情况下再考虑用代码配置
  • 在同一个微服务内尽量保持单一性,比如统一使用属性配置,不要两种方式混用,增加定位代码的复杂性

全局配置

  • 方式一:让ComponentScan上下文重叠(强烈不建议使用)

  • 方式二:唯一正确的途径:@RibbonClients(defaultConfiguration = xxx.class)

    package com.itmuch.contentcenter.configuration;

    import org.springframework.cloud.netflix.ribbon.RibbonClients;
    import org.springframework.context.annotation.Configuration;
    import ribbonconfiguration.RibbonConfiguration;

    @Configuration
    @RibbonClients(defaultConfiguration = RibbonConfiguration.class)
    public class UserCenterRibbonConfiguration {
    }

支持的配置项

Java Config方式:见Ribbon组成一节的接口

配置文件方式:

饥饿加载

默认是懒加载,在调用restTemplate时才会创建一个叫user-centerRibbon Client

user-center是要调用的客户端名字

懒加载的问题:在第一次调用user-center的接口时,访问会慢。

可以使用饥饿加载避免这个问题。

ribbon:
  eager-load:
    enabled: true
    clients: user-center

扩展Ribbon

支持Nacos权重

首先了解一下,Nacos的权重在0-1之间,1最大

Ribbon内置的负载均衡规则都不支持Nacos的权重,需要自己定义一个负载均衡规则。

@Slf4j
public class NacosWeightedRule extends AbstractLoadBalancerRule {

    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
        //读取配置文件,并初始化当前配置NacosWeightedRule,一般不需要实现
    }

    @Override
    public Server choose(Object key) {
        BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
        log.info("loadBalancer = {}", loadBalancer);

        //想要请求的微服务的名称
        String name = loadBalancer.getName();

        //实现负载均衡算法
        //这里不自己实现,直接使用nacos提供的
        //拿到服务发现的相关API
        NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
        try {
            Instance instance = namingService.selectOneHealthyInstance(name);
            log.info("选择的实例是:port = {}, instance = {}", instance.getPort(), instance);
            return new NacosServer(instance);
        } catch (NacosException e) {
            return null;
        }
    }
}

配置为全局规则

/**
 * 配置类所在的包必须是和启动类不一样的包
 */
@Configuration
public class RibbonConfiguration {

    @Bean
    public IRule ribbonRule(){
        return new NacosWeightedRule();
    }
}

更多扩展方式可以扩展Ribbon支持Nacos权重的三种方式

同一集群优先调用

为了实现容灾,把内容中心和用户中心部署在北京机房和南京机房里,希望调用的时候同机房优先。

使用Nacos服务发现领域模型里的Cluster

编写同集群优先调用规则

@Slf4j
public class NacosSameClusterWeightedRule extends AbstractLoadBalancerRule {
    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;
    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {

    }

    @Override
    public Server choose(Object key) {
        //拿到配置文件中的集群名称
        String clusterName = nacosDiscoveryProperties.getClusterName();
        BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
        log.info("loadBalancer = {}", loadBalancer);

        //想要请求的微服务的名称
        String name = loadBalancer.getName();

        //拿到服务发现的相关API
        NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
        try {
            //1 找到指定服务的所有实例
            List<Instance> instances = namingService.selectInstances(name, true);
            //2 过滤出相同集群下的所有实例
            Stream<Instance> instanceStream = instances.stream()
                    .filter(instance -> Objects.equals(instance.getClusterName(), clusterName));
            List<Instance> sameClusterInstances = instanceStream.collect(Collectors.toList());

            List<Instance> instancesToBeChosen;
            if(CollectionUtils.isEmpty(sameClusterInstances)){
                instancesToBeChosen = instances;
                log.warn("发生跨集群调用,name = {}, clusterName = {}, instances = {}",
                        name,
                        clusterName,
                        instances);
            }else {
                instancesToBeChosen = sameClusterInstances;
            }
            //3 基于权重的负载均衡算法,返回1个实例
            Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToBeChosen);
            log.info("选择的实例是 port = {}, instances = {} ",instance.getPort(), instance);

            return new NacosServer(instance);
        } catch (NacosException e) {
            log.error("发生异常",e);
        }
        return null;
    }
}

class ExtendBalancer extends Balancer{
    //Nacos没有暴露从实例列表中选一个,只有selectOneHealthyInstance
    public static Instance getHostByRandomWeight2(List<Instance> hosts) {
        return getHostByRandomWeight(hosts);
    }
}

配置全局NacosSameClusterWeightedRule

@Configuration
public class RibbonConfiguration {

    @Bean
    public IRule ribbonRule(){
        return new NacosSameClusterWeightedRule();
    }
}

配置所在集群

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/content_center
    hikari:
      username: root
      password: kim@2020
      # >=6.x com.mysql.cj.jdbc.Driver
      # <=5.x com.mysql.jdbc.Driver
      driver-class-name: com.mysql.cj.jdbc.Driver
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        cluster-name: BJ
  application:
    # 服务名称尽量用-,不要用_,不要用特殊字符
    name: content-center

logging:
  level:
    com.itmuch.usercenter.dao.content: debug

server:
  servlet:
    context-path:
  port: 8010

#user-center:
#  ribbon:
#    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
ribbon:
  eager-load:
    enabled: true
    clients: user-center

启动内容中心服务

接下来,配置两个用户中心服务,分别配置不同的集群和端口

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/user_center
    hikari:
      username: root
      password: kim@2020
      # >=6.x com.mysql.cj.jdbc.Driver
      # <=5.x com.mysql.jdbc.Driver
      driver-class-name: com.mysql.cj.jdbc.Driver
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        # 多集群配置
        cluster-name: BJ
  application:
    # 服务名称尽量用-,不要用_,不要用特殊字符
    name: user-center

logging:
  level:
    com.itmuch.usercenter.dao.user: debug
server:
  # 本地启动多个实例,启动前记得改端口
  port: 8081

查看Nacos控制台

观察到user-center的集群数目是2。点击详情

页面访问请求http://localhost:8010/shares/1

可以看到总是请求到相同机房的实例(8081也属于BJ集群)。

模拟BJ集群下线。选择Nacos控制台里BJ集群的8081实例,将其下线。

再次浏览器访问http://localhost:8010/shares/1

可以观察到已经请求到了异地机房的NJ机房的实例8082

番外:为开源项目贡献代码

目前同集群优先调用规则已经在新版本中被采纳了,可以直接配置。我用的是2.1.0.RELEASE版本

同集群优先调用规则的类是com.alibaba.cloud.nacos.ribbon.NacosRule,直接配置这个类使用,不需要再扩展了。

基于元数据的版本控制

配置元数据,只要在spring.cloud.nacos.discovery.metadata下配置key-value对就可以

spring:
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        cluster-name: BJ
        metadata:
          version: v1.0

核心逻辑是,服务提供者和服务消费者配置相同的或不同的version元数据,在服务消费者请求服务提供者的时候,从待选实例中过滤一下,找到相同版本号的实例列表,再用一种负载算法从从版本号列表中选一个实例。

String version = nacosDiscoveryProperties.getMetadata().get("version");
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
        try {
            //1 找到指定服务的所有实例
            List<Instance> instances = namingService.selectInstances(name, true);
            //过滤出同集群的实例列表
            //过滤出版本号相同的实例列表
            List<Instance> sameVersionInstances = instancesToBeChosen.stream()
                    .filter(instance -> Objects.equals(instance.getMetadata().get("version"), version))
                    .collect(Collectors.toList());
            //从列表中选出一个实例
          Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToBeChosen);

具体实现参见手记

深入理解Namespace

配置namespace

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/content_center
    hikari:
      username: root
      password: kim@2020
      # >=6.x com.mysql.cj.jdbc.Driver
      # <=5.x com.mysql.jdbc.Driver
      driver-class-name: com.mysql.cj.jdbc.Driver
  cloud:
    nacos:
      discovery:
        server-addr: localhost:8848
        cluster-name: BJ
        metadata:
          version: v1.0
        # 指定namespace
        namespace: bc4f4e1a-bf4e-4bcc-86f1-7f6252f81e45
  application:
    # 服务名称尽量用-,不要用_,不要用特殊字符
    name: content-center

跨namespace不能调用

在用户中心和内容中心分别配上同样的命名空间ID。才可以正常访问。

现有架构存在的问题

  1. 代码不可读
  2. 复杂的url难以维护
  3. 难以响应需求变化,变化没有幸福感
  4. 编程体验不统一

使用Feign实现远程HTTP调用

引入依赖

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

写注解

启动类上加上@EnableFeignClients注解

写配置

暂时没有

实现一个Feign接口

@FeignClient(name = "user-center")
public interface UserCenterFeignClient {

    /**
     * http://user-center/users/{id}
     * @param id
     * @return
     */
    @GetMapping("/users/{id}")
    UserDto findById(@PathVariable Integer id);
}


@Slf4j
@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class ShareService {
    private final ShareMapper shareMapper;

    private final UserCenterFeignClient userCenterFeignClient;

    public ShareDto findById(Integer id){
        //获取分享详情
        Share share = this.shareMapper.selectByPrimaryKey(id);
        //发布人id
        Integer userId = share.getUserId();

        UserDto userDto = this.userCenterFeignClient.findById(userId);
        ShareDto shareDto = new ShareDto();

        //消息的装配
        BeanUtils.copyProperties(share, shareDto);
        shareDto.setWxNickName(userDto.getWxNickname());
        return shareDto;
    }
}

所谓的声明式HTTP客户端,就是只需要声明一个Feign Client接口,Feign就会根据声明的接口,自动帮我们构造请求的目标地址,并帮助你请求。

Feign的组成

细粒度配置自定义

默认Feign不打印任何日志,可以自定义Feign日志级别,让其打印日志

Feign日志级别

Java配置方式

UserCenterFeignConfiguration

/**
 * feign的配置类,最佳实践不要加@Configuration注解,否则必须挪到@ComponentScan能扫描的包以外。
 * 是因为重复扫描,父子上下文的问题
 */
public class UserCenterFeignConfiguration {

    @Bean
    public Logger.Level level(){
        //打印所有请求的细节
        return Logger.Level.FULL;
    }
}

UserCenterFeignClient

@FeignClient(name = "user-center", configuration = UserCenterFeignConfiguration.class)
public interface UserCenterFeignClient {

    /**
     * http://user-center/users/{id}
     * @param id
     * @return
     */
    @GetMapping("/users/{id}")
    UserDto findById(@PathVariable Integer id);
}


logging:
  level:
    com.itmuch.usercenter.dao.content: debug
    com.itmuch.contentcenter.feignclient.UserCenterFeignClient: debug

属性方式配置

feign:
  client:
    config:
      # 想要调用的微服务的名称
      user-center:
        loggerLevel: full

全局配置

代码方式

将细粒度的配置方式都注释掉

在启动类配置上全局配置

@EnableFeignClients(defaultConfiguration = UserCenterFeignConfiguration.class)

配置属性方式

feign:
  client:
    config:
      default:
        loggerLevel: full

支持的配置项

代码方式

属性配置方式

配置最佳实践

Ribbon配置 vs Feign配置

Ribbon是一个负载均衡器,帮我们选择一个实例

Feign是一个声明式HTTP客户端,帮助我们更优雅的请求

Feign代码方式vs属性方式

优先级:全局代码<全局属性<细粒度代码<细粒度属性

最佳实践

  • 尽量使用属性配置,属性方式实现不了的情况再考虑用代码配置
  • 在同一个微服务内尽量保持单一性,比如统一使用属性配置,不要两种方式混用,增加定位代码的复杂性

Feign的继承

这个特性带来了紧耦合,因为在微服务间共享接口,官方不建议使用。

现状:很多公司用,代码复用。

新项目如何选择:权衡利弊,会得到什么好处,失去什么,是不是划算,划算就上。

多参数请求构造

如何使用Feign构造多参数的请求

Get请求参数使用@SpringQueryMap注解

@FeignClient(name = "user-center")
public interface TestUserCenterFeignClient {

    @GetMapping("/q")
    UserDto query(@SpringQueryMap UserDto userDto);
}

Post请求多参数,也可以使用@RequestBody。

Feign脱离Ribbon使用

@FeignClient(name = "baidu", url="http://www.baidu.com")
public interface TestBaiduFeignClient {

    @GetMapping("")
    String index();
}


@GetMapping("baidu")
    public String baiduIndex(){
        return this.testBaiduFeignClient.index();
    }

RestTemplate vs Feign

如何选择?

  • 原则:尽量用Feign,杜绝使用RestTemplate

尽量减少开发人员的选择,共存会带来风格的不统一,额外的学习成本和额外的代码理解成本

  • 事无绝对,合理选择

Feign解决不了,才用RestTemplate

Feign的性能优化

  • 连接池【提升15%左右】,默认使用URLConnection,可以修改

可以选用httpclient或者okhttp

添加依赖

<dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-httpclient</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
        </dependency>


feign:
  client:
    config:
      default:
        loggerLevel: full
  httpclient:
    # 让feign使用apache httpclient做请求,而不是默认的urlclient
    enabled: true
    # feign的最大连接数
    max-connections: 200
    # feign单个路径的最大连接数
    max-connections-per-route: 50
  okhttp:
    enabled: true
    # feign的最大连接数
    max-connections: 200
    # feign单个路径的最大连接数
    max-connections-per-route: 50
  • 日志级别

生产环境建议设置为basic

Feign常见问题总结

常见问题总结

现有架构总结

雪崩效应:基础服务故障,导致导致上层服务故障,并且故障不断放大。又称为cascading failure,级联失效,级联故障。

雪崩效应是因为服务没有做好容错。

常见的容错方案(容错思想)

  • 超时
  • 限流
  • 仓壁模式(线程池隔离)
  • 断路器模式

5秒内错误率、错误次数达到就跳闸。

断路器三态:

使用Sentinel实现容错

是什么:轻量级的流量控制、熔断降级Java库。

整合Sentinel

<dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

可以使用/actuator/sentinel断点查看sentinel相关信息。

整合Actuator

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

需要加入配置才能暴露sentinel端点。

management:
  endpoints:
    web:
      exposure:
        include: '*'

Sentinel控制台

搭建Sentinel控制台

https://github.com/alibaba/Sentinel/releases

生产环境,控制台版本最好和整体版本一致。

启动sentinel控制台

java -jar /Users/kim/Downloads/sentinel-dashboard-1.7.2.jar

默认在localhost:8080端口,用户名密码都是sentinel。

为内容中心整合sentinel控制台

# 指定sentinel 控制台地址
spring.cloud.sentinel.transport.dashboard: localhost:8080

确保nacos、sentinel控制台、内容中心和用户中心都启动了。然后访问http://localhost:8010/shares/1多次,就可以在实时监控里看到效果。

流控规则

点击簇点链路,点击/shares/1的流控按钮,就可以为这个访问路径设置流控规则。

资源名

默认是请求路径。

针对来源

针对调用者限流。针对来源是调用者微服务名称。

阈值类型

QPS、线程数。比如选择QPS,表示:当调用当前资源的QPS达到阈值时,就去限流。

是否集群

流控模式

直接
关联

<1>当关联的资源达到阈值,就限流自己

比如我们设置关联资源为/actuator/sentinel,当关联资源的qps达到1时,就限流/shares/1

写一个测试类,调用/actuator/sentinel

public class SentinelTest {
    public static void main(String[] args) throws InterruptedException {
        RestTemplate restTemplate = new RestTemplate();
        for (int i = 0; i < 10000; i++) {
            String forObject = restTemplate.getForObject("http://localhost:8010/actuator/sentinel", String.class);
            Thread.sleep(500);
        }
    }
}

运行这个测试类,再去调用/shares/1,发信啊已经被限流了。

实际应用,如果希望修改优先,可以配置关联API为修改的API,资源名设置为查询的API。当修改的测试过多,就限流查询,保证性能。

链路

只记录指定链路上的流量

流控效果