SpringCloud Alibaba学习笔记
目录#
导学#
为什么学#
- 组件性能更强
- 良好的可视化界面
- 搭建简单,学习曲线低
- 文档丰富并且是中文
学习目标#
- 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必知必会#
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 文档#
课程文档主要分四类:
- API文档:https://t.itmuch.com/doc.html
- 课程配套代码:https://git.imooc.com/coding-358/
- 课程相关资源(例如检表语句、数据模型、课上用到的软件等):https://git.imooc.com/coding-358/resource
- 课上用到的一些课外读物(慕课网手记):http://www.imooc.com/t/1863086
如何创建小程序#
按照提示填写信息
前端代码如何使用#
创建项目#
技术选型
- 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>
<span class="hljs-tag"><<span class="hljs-name">properties>
<span class="hljs-tag"><<span class="hljs-name">java.version>1.8<span class="hljs-tag"></<span class="hljs-name">java.version>
<span class="hljs-tag"></<span class="hljs-name">properties>
<span class="hljs-tag"><<span class="hljs-name">dependencies>
<span class="hljs-tag"><<span class="hljs-name">dependency>
<span class="hljs-tag"><<span class="hljs-name">groupId>org.springframework.boot<span class="hljs-tag"></<span class="hljs-name">groupId>
<span class="hljs-tag"><<span class="hljs-name">artifactId>spring-boot-starter-web<span class="hljs-tag"></<span class="hljs-name">artifactId>
<span class="hljs-tag"></<span class="hljs-name">dependency>
<!-- 引入通用mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
<span class="hljs-tag"><<span class="hljs-name">dependency>
<span class="hljs-tag"><<span class="hljs-name">groupId>mysql<span class="hljs-tag"></<span class="hljs-name">groupId>
<span class="hljs-tag"><<span class="hljs-name">artifactId>mysql-connector-java<span class="hljs-tag"></<span class="hljs-name">artifactId>
<span class="hljs-tag"><<span class="hljs-name">scope>runtime<span class="hljs-tag"></<span class="hljs-name">scope>
<span class="hljs-tag"></<span class="hljs-name">dependency>
<span class="hljs-tag"><<span class="hljs-name">dependency>
<span class="hljs-tag"><<span class="hljs-name">groupId>org.springframework.boot<span class="hljs-tag"></<span class="hljs-name">groupId>
<span class="hljs-tag"><<span class="hljs-name">artifactId>spring-boot-starter-test<span class="hljs-tag"></<span class="hljs-name">artifactId>
<span class="hljs-tag"><<span class="hljs-name">scope>test<span class="hljs-tag"></<span class="hljs-name">scope>
<span class="hljs-tag"><<span class="hljs-name">exclusions>
<span class="hljs-tag"><<span class="hljs-name">exclusion>
<span class="hljs-tag"><<span class="hljs-name">groupId>org.junit.vintage<span class="hljs-tag"></<span class="hljs-name">groupId>
<span class="hljs-tag"><<span class="hljs-name">artifactId>junit-vintage-engine<span class="hljs-tag"></<span class="hljs-name">artifactId>
<span class="hljs-tag"></<span class="hljs-name">exclusion>
<span class="hljs-tag"></<span class="hljs-name">exclusions>
<span class="hljs-tag"></<span class="hljs-name">dependency>
<span class="hljs-tag"></<span class="hljs-name">dependencies>
<span class="hljs-tag"><<span class="hljs-name">build>
<span class="hljs-tag"><<span class="hljs-name">plugins>
<span class="hljs-tag"><<span class="hljs-name">plugin>
<span class="hljs-tag"><<span class="hljs-name">groupId>org.springframework.boot<span class="hljs-tag"></<span class="hljs-name">groupId>
<span class="hljs-tag"><<span class="hljs-name">artifactId>spring-boot-maven-plugin<span class="hljs-tag"></<span class="hljs-name">artifactId>
<span class="hljs-tag"></<span class="hljs-name">plugin>
<span class="hljs-tag"><<span class="hljs-name">plugin>
<span class="hljs-tag"><<span class="hljs-name">groupId>org.mybatis.generator<span class="hljs-tag"></<span class="hljs-name">groupId>
<span class="hljs-tag"><<span class="hljs-name">artifactId>mybatis-generator-maven-plugin<span class="hljs-tag"></<span class="hljs-name">artifactId>
<span class="hljs-tag"><<span class="hljs-name">version>1.3.6<span class="hljs-tag"></<span class="hljs-name">version>
<span class="hljs-tag"><<span class="hljs-name">configuration>
<span class="hljs-tag"><<span class="hljs-name">configurationFile>
${basedir}/src/main/resources/generator/generatorConfig.xml
<span class="hljs-tag"></<span class="hljs-name">configurationFile>
<span class="hljs-tag"><<span class="hljs-name">overwrite>true<span class="hljs-tag"></<span class="hljs-name">overwrite>
<span class="hljs-tag"><<span class="hljs-name">verbose>true<span class="hljs-tag"></<span class="hljs-name">verbose>
<span class="hljs-tag"></<span class="hljs-name">configuration>
<span class="hljs-tag"><<span class="hljs-name">dependencies>
<span class="hljs-tag"><<span class="hljs-name">dependency>
<span class="hljs-tag"><<span class="hljs-name">groupId>mysql<span class="hljs-tag"></<span class="hljs-name">groupId>
<span class="hljs-tag"><<span class="hljs-name">artifactId>mysql-connector-java<span class="hljs-tag"></<span class="hljs-name">artifactId>
<span class="hljs-tag"><<span class="hljs-name">version>8.0.19<span class="hljs-tag"></<span class="hljs-name">version>
<span class="hljs-tag"></<span class="hljs-name">dependency>
<span class="hljs-tag"><<span class="hljs-name">dependency>
<span class="hljs-tag"><<span class="hljs-name">groupId>tk.mybatis<span class="hljs-tag"></<span class="hljs-name">groupId>
<span class="hljs-tag"><<span class="hljs-name">artifactId>mapper<span class="hljs-tag"></<span class="hljs-name">artifactId>
<span class="hljs-tag"><<span class="hljs-name">version>4.1.5<span class="hljs-tag"></<span class="hljs-name">version>
<span class="hljs-tag"></<span class="hljs-name">dependency>
<span class="hljs-tag"></<span class="hljs-name">dependencies>
<span class="hljs-tag"></<span class="hljs-name">plugin>
<span class="hljs-tag"></<span class="hljs-name">plugins>
<span class="hljs-tag"></<span class="hljs-name">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 {
<span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-keyword">static <span class="hljs-keyword">void <span class="hljs-title">main<span class="hljs-params">(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"/>
<span class="hljs-tag"><<span class="hljs-name">context <span class="hljs-attr">id=<span class="hljs-string">"Mysql" <span class="hljs-attr">targetRuntime=<span class="hljs-string">"MyBatis3Simple" <span class="hljs-attr">defaultModelType=<span class="hljs-string">"flat">
<span class="hljs-tag"><<span class="hljs-name">property <span class="hljs-attr">name=<span class="hljs-string">"beginningDelimiter" <span class="hljs-attr">value=<span class="hljs-string">"`"/>
<span class="hljs-tag"><<span class="hljs-name">property <span class="hljs-attr">name=<span class="hljs-string">"endingDelimiter" <span class="hljs-attr">value=<span class="hljs-string">"`"/>
<span class="hljs-tag"><<span class="hljs-name">plugin <span class="hljs-attr">type=<span class="hljs-string">"tk.mybatis.mapper.generator.MapperPlugin">
<span class="hljs-tag"><<span class="hljs-name">property <span class="hljs-attr">name=<span class="hljs-string">"mappers" <span class="hljs-attr">value=<span class="hljs-string">"tk.mybatis.mapper.common.Mapper"/>
<span class="hljs-tag"><<span class="hljs-name">property <span class="hljs-attr">name=<span class="hljs-string">"caseSensitive" <span class="hljs-attr">value=<span class="hljs-string">"true"/>
<span class="hljs-tag"></<span class="hljs-name">plugin>
<span class="hljs-tag"><<span class="hljs-name">jdbcConnection <span class="hljs-attr">driverClass=<span class="hljs-string">"${jdbc.driverClass}"
<span class="hljs-attr">connectionURL=<span class="hljs-string">"${jdbc.url}"
<span class="hljs-attr">userId=<span class="hljs-string">"${jdbc.user}"
<span class="hljs-attr">password=<span class="hljs-string">"${jdbc.password}">
<span class="hljs-tag"></<span class="hljs-name">jdbcConnection>
<span class="hljs-tag"><<span class="hljs-name">javaModelGenerator <span class="hljs-attr">targetPackage=<span class="hljs-string">"com.itmuch.usercenter.domain.entity.${moduleName}"
<span class="hljs-attr">targetProject=<span class="hljs-string">"src/main/java"/>
<span class="hljs-tag"><<span class="hljs-name">sqlMapGenerator <span class="hljs-attr">targetPackage=<span class="hljs-string">"com.itmuch.usercenter.dao.${moduleName}"
<span class="hljs-attr">targetProject=<span class="hljs-string">"src/main/resources"/>
<span class="hljs-tag"><<span class="hljs-name">javaClientGenerator <span class="hljs-attr">targetPackage=<span class="hljs-string">"com.itmuch.usercenter.dao.${moduleName}"
<span class="hljs-attr">targetProject=<span class="hljs-string">"src/main/java"
<span class="hljs-attr">type=<span class="hljs-string">"XMLMAPPER"/>
<span class="hljs-tag"><<span class="hljs-name">table <span class="hljs-attr">tableName=<span class="hljs-string">"${tableName}">
<span class="hljs-tag"><<span class="hljs-name">generatedKey <span class="hljs-attr">column=<span class="hljs-string">"id" <span class="hljs-attr">sqlStatement=<span class="hljs-string">"JDBC"/>
<span class="hljs-tag"></<span class="hljs-name">table>
<span class="hljs-tag"></<span class="hljs-name">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
wiki
搜lombok
,看有没有生成支持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介绍#
什么是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 Server#
选择Nacos Server
版本
查看引入到spring-cloud-alibaba-dependencie
依赖
- 下载地址
- 搭建Nacos Server
启动服务器
startup.sh -m standalone
访问控制台
默认用户名密码都是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;
<span class="hljs-keyword">private <span class="hljs-keyword">final RestTemplate restTemplate;
<span class="hljs-keyword">private <span class="hljs-keyword">final DiscoveryClient discoveryClient;
<span class="hljs-function"><span class="hljs-keyword">public ShareDto <span class="hljs-title">findById<span class="hljs-params">(Integer id){
<span class="hljs-comment">//获取分享详情
Share share = <span class="hljs-keyword">this.shareMapper.selectByPrimaryKey(id);
<span class="hljs-comment">//发布人id
Integer userId = share.getUserId();
<span class="hljs-comment">//用户中心所有实例的信息
List<ServiceInstance> instances = discoveryClient.getInstances(<span class="hljs-string">"user-center");
String targetURL = instances.stream()
.map(instance -> instance.getUri().toString() + <span class="hljs-string">"/users/{id}")
.findFirst()
.orElseThrow(() -> <span class="hljs-keyword">new IllegalArgumentException(<span class="hljs-string">"当前没有实例!"));
log.info(<span class="hljs-string">"请求的目标地址:{}", targetURL);
UserDto userDto = restTemplate.getForObject(
targetURL,
UserDto.class,
userId);
ShareDto shareDto = <span class="hljs-keyword">new ShareDto();
<span class="hljs-comment">//消息的装配
BeanUtils.copyProperties(share, shareDto);
shareDto.setWxNickName(userDto.getWxNickname());
<span class="hljs-keyword">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元数据#
- 官方描述:https://nacos.io/zh-ch/docs/concepts.html
- 级别:【服务级别、集群级别、实例级别】
元数据的作用:
- 提供描述信息
- 让微服务调用更灵活
- 例如:微服务版本控制
如何为微服务设置元数据
- 控制台界面
- 配置文件指定
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
实现负载均衡-Ribbon#
负载均衡的两种方式#
- 服务端负载均衡
- 客户端负载均衡(客户端调用的时候使用选择负载均衡算法)
手写一个客户端负载均衡器#
改写一下ShareService
的findById
方法。从Nacos
获取到URL
列表,然后随机从列表中取一个作为本次请求的服务提供者实例。
List<String> targetURLs = instances.stream()
.map(instance -> instance.getUri().toString() + "/users/{id}").collect(Collectors.toList());
<span class="hljs-keyword">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;
/**
配置类所在的包必须是和启动类不一样的包
*/
新建一个user-center
的ribbon
负载配置类,配置规则使用上面的随机规则。
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-center
的Ribbon 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 {
<span class="hljs-meta">@Autowired
<span class="hljs-keyword">private NacosDiscoveryProperties nacosDiscoveryProperties;
<span class="hljs-meta">@Override
<span class="hljs-function"><span class="hljs-keyword">public <span class="hljs-keyword">void <span class="hljs-title">initWithNiwsConfig<span class="hljs-params">(IClientConfig iClientConfig) {
<span class="hljs-comment">//读取配置文件,并初始化当前配置NacosWeightedRule,一般不需要实现
}
<span class="hljs-meta">@Override
<span class="hljs-function"><span class="hljs-keyword">public Server <span class="hljs-title">choose<span class="hljs-params">(Object key) {
BaseLoadBalancer loadBalancer = (BaseLoadBalancer) <span class="hljs-keyword">this.getLoadBalancer();
log.info(<span class="hljs-string">"loadBalancer = {}", loadBalancer);
<span class="hljs-comment">//想要请求的微服务的名称
String name = loadBalancer.getName();
<span class="hljs-comment">//实现负载均衡算法
<span class="hljs-comment">//这里不自己实现,直接使用nacos提供的
<span class="hljs-comment">//拿到服务发现的相关API
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
<span class="hljs-keyword">try {
Instance instance = namingService.selectOneHealthyInstance(name);
log.info(<span class="hljs-string">"选择的实例是:port = {}, instance = {}", instance.getPort(), instance);
<span class="hljs-keyword">return <span class="hljs-keyword">new NacosServer(instance);
} <span class="hljs-keyword">catch (NacosException e) {
<span class="hljs-keyword">return <span class="hljs-keyword">null;
}
}
}
配置为全局规则
/**
* 配置类所在的包必须是和启动类不一样的包
*/
@Configuration
public class RibbonConfiguration {
<span class="hljs-meta">@Bean
<span class="hljs-function"><span class="hljs-keyword">public IRule <span class="hljs-title">ribbonRule<span class="hljs-params">(){
<span class="hljs-keyword">return <span class="hljs-keyword">new NacosWeightedRule();
}
}
更多扩展方式可以扩展Ribbon支持Nacos权重的三种方式
同一集群优先调用#
为了实现容灾,把内容中心和用户中心部署在北京机房和南京机房里,希望调用的时候同机房优先。
使用Nacos服务发现领域模型里的Cluster
编写同集群优先调用规则
@Slf4j
public class NacosSameClusterWeightedRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
<span class="hljs-meta">@Override
<span class="hljs-function"><span class="hljs-keyword">public Server <span class="hljs-title">choose<span class="hljs-params">(Object key) {
<span class="hljs-comment">//拿到配置文件中的集群名称
String clusterName = nacosDiscoveryProperties.getClusterName();
BaseLoadBalancer loadBalancer = (BaseLoadBalancer) <span class="hljs-keyword">this.getLoadBalancer();
log.info(<span class="hljs-string">"loadBalancer = {}", loadBalancer);
<span class="hljs-comment">//想要请求的微服务的名称
String name = loadBalancer.getName();
<span class="hljs-comment">//拿到服务发现的相关API
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
<span class="hljs-keyword">try {
<span class="hljs-comment">//1 找到指定服务的所有实例
List<Instance> instances = namingService.selectInstances(name, <span class="hljs-keyword">true);
<span class="hljs-comment">//2 过滤出相同集群下的所有实例
Stream<Instance> instanceStream = instances.stream()
.filter(instance -> Objects.equals(instance.getClusterName(), clusterName));
List<Instance> sameClusterInstances = instanceStream.collect(Collectors.toList());
List<Instance> instancesToBeChosen;
<span class="hljs-keyword">if(CollectionUtils.isEmpty(sameClusterInstances)){
instancesToBeChosen = instances;
log.warn(<span class="hljs-string">"发生跨集群调用,name = {}, clusterName = {}, instances = {}",
name,
clusterName,
instances);
}<span class="hljs-keyword">else {
instancesToBeChosen = sameClusterInstances;
}
<span class="hljs-comment">//3 基于权重的负载均衡算法,返回1个实例
Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToBeChosen);
log.info(<span class="hljs-string">"选择的实例是 port = {}, instances = {} ",instance.getPort(), instance);
<span class="hljs-keyword">return <span class="hljs-keyword">new NacosServer(instance);
} <span class="hljs-keyword">catch (NacosException e) {
log.error(<span class="hljs-string">"发生异常",e);
}
<span class="hljs-keyword">return <span class="hljs-keyword">null;
}
}
class ExtendBalancer extends Balancer{
//Nacos没有暴露从实例列表中选一个,只有selectOneHealthyInstance
public static Instance getHostByRandomWeight2(List<Instance> hosts) {
return getHostByRandomWeight(hosts);
}
}
配置全局NacosSameClusterWeightedRule
@Configuration
public class RibbonConfiguration {
<span class="hljs-meta">@Bean
<span class="hljs-function"><span class="hljs-keyword">public IRule <span class="hljs-title">ribbonRule<span class="hljs-params">(){
<span class="hljs-keyword">return <span class="hljs-keyword">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。才可以正常访问。
现有架构存在的问题#
- 代码不可读
- 复杂的url难以维护
- 难以响应需求变化,变化没有幸福感
- 编程体验不统一
声明式HTTP客户端Feign#
- Feign是Netflix开源的声明式HTTP客户端
- Github地址
使用Feign实现远程HTTP调用#
引入依赖#
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
写注解#
启动类上加上@EnableFeignClients
注解
写配置#
暂时没有
实现一个Feign接口
@FeignClient(name = "user-center")
public interface UserCenterFeignClient {
<span class="hljs-comment">/**
* http://user-center/users/{id}
* <span class="hljs-doctag">@param id
* <span class="hljs-doctag">@return
*/
<span class="hljs-meta">@GetMapping("/users/{id}")
<span class="hljs-function">UserDto <span class="hljs-title">findById<span class="hljs-params">(<span class="hljs-meta">@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 {
<span class="hljs-meta">@Bean
<span class="hljs-keyword">public Logger.<span class="hljs-function">Level <span class="hljs-title">level<span class="hljs-params">(){
<span class="hljs-comment">//打印所有请求的细节
<span class="hljs-keyword">return Logger.Level.FULL;
}
}
UserCenterFeignClient
@FeignClient(name = "user-center", configuration = UserCenterFeignConfiguration.class)
public interface UserCenterFeignClient {
<span class="hljs-comment">/**
* http://user-center/users/{id}
* <span class="hljs-doctag">@param id
* <span class="hljs-doctag">@return
*/
<span class="hljs-meta">@GetMapping("/users/{id}")
<span class="hljs-function">UserDto <span class="hljs-title">findById<span class="hljs-params">(<span class="hljs-meta">@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的继承#
这个特性带来了紧耦合,因为在微服务间共享接口,官方不建议使用。
现状:很多公司用,代码复用。
新项目如何选择:权衡利弊,会得到什么好处,失去什么,是不是划算,划算就上。
多参数请求构造#
Get请求参数使用@SpringQueryMap注解
@FeignClient(name = "user-center")
public interface TestUserCenterFeignClient {
<span class="hljs-meta">@GetMapping("/q")
<span class="hljs-function">UserDto <span class="hljs-title">query<span class="hljs-params">(<span class="hljs-meta">@SpringQueryMap UserDto userDto);
}
Post请求多参数,也可以使用@RequestBody。
Feign脱离Ribbon使用#
@FeignClient(name = "baidu", url="http://www.baidu.com")
public interface TestBaiduFeignClient {
<span class="hljs-meta">@GetMapping("")
<span class="hljs-function">String <span class="hljs-title">index<span class="hljs-params">();
}
@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常见问题总结#
现有架构总结#
服务容错-Sentinel#
雪崩效应:基础服务故障,导致导致上层服务故障,并且故障不断放大。又称为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。当修改的测试过多,就限流查询,保证性能。
链路
只记录指定链路上的流量