微服务入门学习
部分资料来自于黑马程序员
1. 热门微服务技术对比

2. 简单实现微服务的代码

此处分别为用户服务和订单服务

订单服务的端口为8080,且调用cloud_order数据库

用户服务在8081,且数据库为cloud_user
加上简单的业务代码,这样就能实现简单的微服务。
访问user_service

访问order_service

3. 微服务远程调用
问题描述:
由于微服务架构中,一个完整的项目被分成了很多可以独立工作的部分,但我们在实际调用这些模块的时候,可能并不只是调用其中一个,我们可能需要多个模块协同工作后的结果,比如上面举例的用户的订单模块,如果我们想要在查询订单的时候一块查询到用户信息,而订单模块又无法直接操作用户数据库,应该怎么办?
这就需要微服务的远程调用,即用模块调用模块,实现方法类似于用java代码发送http请求给目标模块
spring提供了可以实现这一功能的工具:RestTemplate
首先先在启动类里面注册RestTemplate
1 2 3 4 5 6
| @Bean public RestTemplate restTemplate(){ return new RestTemplate(); } }
|
然后在需要的地方注入并调用RestTemplate
注入
1 2
| @Autowired private RestTemplate restTemplate;
|
调用
1 2 3
| String url = "http://localhost:8081/user/" + order.getUserId(); User user = restTemplate.getForObject(url, User.class);
|
restTemplate.getForObject 发送get请求
restTemplate.postForObject 发送post请求
默认返回json格式字符串,可以自己定义返回类型

返回的json同时有user模块的内容
需要注意的是:url一定记得完整,加http://,然后不要忘记参数前加/
4. Eureka注册中心
上述远程调用看似很成功,实则存在很多问题:
- 服务消费者如何获知提供者的地址信息(肯定不是写localhost)
- 如果存在多个提供者,消费者如何从中选择
- 消费者如何知道提供者的健康状态(服务器是否挂了)
这就需要Eureka,它作为一个中间媒介,在每一个服务启动时,都会向eureka注册服务信息(名字,地址端口),然后如果有服务有远程调用需要,它会联系Eureka,然后Eureka提供信息,如果存在多个提供者,则会利用负载均衡算法。至于健康状态,Eureka每30s会从每个服务接受一次信号(心跳续约),如果收不到,该服务的信息就会被剔除。
5. Eureka实践
导入依赖包
1 2 3 4 5
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency>
|
创建启动类,并加入@EnableEurekaServer注解
1 2 3 4 5 6 7 8 9 10
| @SpringBootApplication @EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) { SpringApplication.run(EurekaApplication.class,args); }
}
|
创建配置文件,配置eureka
1 2 3 4 5 6 7 8 9 10 11
| server: port: 8082 spring: application: name: eurekaserver
eureka: client: service-url: defaultZone: http://localhost:8082/eureka
|
启动eureka
访问defaultZone: http://localhost:8082/eureka,看到下面的控制台代表成功

6. Eureka服务注册
之前提到过,Eureka是一个中间媒介,所有的服务都会在它上面进行一个注册,同时所有的远程调用都会经过它得到服务提供者,所以接下来要做的是将每个服务都注册到Eureka上
先引入依赖
1 2 3 4 5
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
|
配置注册信息(本服务的名字,以及Eureka服务的地址),此处以user-service为例,order-service一致
1 2 3 4 5 6 7
| spring: application: name: userserver eureka: client: service-url: defaultZone: http://localhost:8082/eureka
|
然后就可以启动看一下是否已经在Eureka注册表里了

7. 怎么使用Eureka
我们之前利用template远程调用时,在url中写的是死代码
1
| String url = "http://localhost:8081/user/" + order.getUserId();
|
有了Eureka后,我们就可以利用服务名称来替代ip地址和端口号部分
即
1
| String url = "http://userserver/user/" + order.getUserId();
|
还要在启动类的注册resttemplate函数上加上@LoadBalanced注解用来启动负载均衡
1 2 3 4 5 6
| @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); }
|
注意,这是必须要加的。
8. Ribbon负载均衡
首先这个地址http://userserver/user/...,是一个无效地址,即我们无法通过浏览器直接访问该地址下的内容,而我们最终却可以访问到我们要的数据,这是因为中间有负载均衡拦截器拦截了这个请求,然后交给Ribbon并实现了解析,然后交给IRule执行负载均衡算法选择了合适的服务提供者
9. 负载均衡策略
内置负载均衡规则类 |
规则描述 |
RoundRobinRule |
简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 |
AvailabilityFilteringRule |
对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的..ActiveConnectionsLimit属性进行配置。 |
WeightedResponseTimeRule |
为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 |
ZoneAvoidanceRule |
以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 |
BestAvailableRule |
忽略那些短路的服务器,并选择并发数较低的服务器。 |
RandomRule |
随机选择一个可用的服务器。 |
RetryRule |
重试机制的选择逻辑 |
默认负载均衡策略为轮训ZoneAvoidanceRule,怎么改变这个默认策略:
方法1:
自己注入一个(配置类或者启动类中)
1 2 3 4
| @Bean public IRule randomRule(){ return new RandomRule(); }
|
方法二:
在配置文件中针对某项微服务修改(此处针对userserver)
1 2 3
| userserver: ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
|
10. Ribbon饥饿加载
Ribbon默认是懒加载,即第一次访问才创建LoadBalanceClient,请求时间长。饥饿加载会在项目启动时加载,降低第一次访问时的时间。
在需要饥饿加载的服务配置中加入以下内容
1 2 3 4 5 6
| ribbon: eager-load: enabled: true clients: -userserver -xxxserver
|
11. Nacos
Nacos是阿里提供的注册中心,功能类似于Eureka且要更强大,两种注册中心都要学一下
11.1 安装
GitHub主页:https://github.com/alibaba/nacos
GitHub的Release下载页:https://github.com/alibaba/nacos/releases
安装完成后进行解压

11.2 配置
conf/application.properties这是nacos的配置文件,可以在里面进行一些常规的端口配置
11.3 启动
在bin目录下,你可以直接双击startup.cmd

也可以在命令行模式下运行指令
startup.cmd -m standalone
此处为单机启动
启动界面:

然后我们可以访问上面给出的网页,可以看到nacos的界面

默认登录名和密码都是nacos

这就是nacos的界面,之后我们会在这里进行操作
关闭的话就是运行shutdown.cmd
12. Nacos快速入门
首先在项目的父工程中添加spring-cloud-alilbaba管理依赖,进行包管理
1 2 3 4 5 6 7
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.2.5.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency>
|
然后在服务中添加nacos客户端依赖(注释掉之前的eureka依赖)
1 2 3 4 5
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
|
之后修改配置文件(注释掉eureka)
配置结束后,正常启动服务,然后启动nacos进入控制台,就会看到两个服务被注册进来了

同样实现了注册中心的功能。
13. Nacos服务分级储存模型
实现服务的集群化
一个服务可以有多个实例,例如我们的user-service,可以有:
- 127.0.0.1:8081
- 127.0.0.1:8082
- 127.0.0.1:8083
假如这些实例分布于全国各地的不同机房,例如:
- 127.0.0.1:8081,在上海机房
- 127.0.0.1:8082,在上海机房
- 127.0.0.1:8083,在杭州机房
Nacos就将同一机房内的实例 划分为一个集群。
也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:

微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。例如:

杭州机房内的order-service应该优先访问同机房的user-service。
操作:
1 2 3 4
| nacos: server-addr: localhost:8848 discovery: cluster-name: AH
|
14. Nacos负载均衡
1 2 3 4
| userserver: ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
|
NacosRule优先同集群内随机
15. 配置权重
权重可以影响被分配的几率,越大越容易被分配到,这个在nacos中就可以设置

16. 环境隔离
Nacos提供了namespace来实现环境隔离功能。
- nacos中可以有多个namespace
- namespace下可以有group、service等
- 不同namespace之间相互隔离,例如不同namespace的服务互相不可见
方法
首先创建命名空间:

然后进入到服务的配置文件进行配置
1 2 3 4 5
| nacos: server-addr: localhost:8848 discovery: cluster-name: AH namespace: 25cd75bc-82c3-4396-ba42-a42a5de77b92
|
这样就实现了隔离
17. Nacos和Eureka相同和不同
- Nacos与eureka的共同点
- 都支持服务注册和服务拉取
- 都支持服务提供者心跳方式做健康检测
- Nacos与Eureka的区别
- Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
- 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
18. 配置管理
在实际生产过程中,可能会出现频繁修改配置的情况,但是如果修改配置过程中涉及的微服务数量多了,会很麻烦且修改配置的微服务需要重启以生效新的配置,但重启可能会带来很大的损失,所以我们需要Nacos的配置管理,他可以保存服务中一些比较重要的核心配置,服务在启动时只需读取上面的配置,然后结合自身配置组成一套完整的配置文件。通过配置热更新,如果要修改只需要修改Nacos的配置管理,其余微服务就会读取到变化后的配置,然后加以应用,实现热更新。
19. Nacos实现配置管理
直接在Nacos控制台里操作


20. 微服务获取Nacos中的配置

由上面的流程图我们可以看到,bootstrap这一配置文件的优先级是要高于application的,因此我们需要把连接Nacos服务的配置配置到该文件里面,这样Spring会先根据这个配置文件访问到Nacos,并提取到其中的服务,然后在把其中的配置和本地application中的配置合并成完整的配置,这就是整个配置管理的全过程。
首先导入Nacos配置管理依赖
1 2 3 4 5
| <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
|
然后在resource目录下添加bootstrap.yml文件

然后配置bootstrap,将Nacos服务连接配置,还有想要获取的被Nacos管理的配置信息写进去,注意一定要和Nacos中的名字相对应
1 2 3 4 5 6 7 8 9 10
| spring: application: name: userserver profiles: active: dev cloud: nacos: server-addr: localhost:8848 config: file-extension: yaml
|
由于application中已经存在了Nacos服务连接配置,所以需要删除,还有原先在application中配置的dateformat

然后验证一下是否已经读到配置了,我们可以利用@Value注解拿到配置文件中的配置

看结果,成功读到:

21. 配置热更新
热更新即让我们的微服务能实时获取Nacos中变化的配置,而无需重启微服务
有两种方法
法一:
在@Value注入的类上添加注解@RefreshScope

法二(推荐):
利用@ConfigurationProperties注解自动注入代替@Value注解。
为此我们可以单独建一个类,来获取配置文件的属性值
1 2 3 4 5 6
| @Component @Data @ConfigurationProperties(prefix = "pattern") public class PatternProperties { private String dateformat; }
|
@ConfigurationProperties会自动将符合前缀名并且属性名和该类中的属性一致的配置的值,自动注入到属性中。
获取的话,首先利用@Autowired注入该类,因为已经添加过@Data注解,直接调用get方法即可
1
| System.out.println(patternProperties.getDateformat());
|
然后让我们试验一下
重启微服务
先修改Nacos配置管理中的配置

然后再次访问我们写了输出语句的方法,热更新成功

再改一次

再试试,依然成功

22. 多环境配置共享
微服务启动时会从Nacos中读取多个配置
我们可以在Nacos中再加一个配置,格式是name + .yaml

这样这个配置所有环境下的userserver都可以读到
1 2 3 4 5
| @ConfigurationProperties(prefix = "pattern") public class PatternProperties { private String dateformat; private String sharedValue; }
|
然后我们再访问

访问到了两个配置文件中的内容,但实际上当前userserver是dev环境,这说明没有标明环境的配置文件可被多个微服务共享
优先级的话:
环境配置 > 多环境配置 > 本地
23. Nacos集群

举例:
其中包含3个nacos节点,然后一个负载均衡器代理3个Nacos。这里负载均衡器可以使用nginx。
23.1 搭建集群
23.1.1 初始化数据库
首先新建一个数据库,命名为nacos,而后导入下面的SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
| CREATE TABLE `config_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(255) DEFAULT NULL, `content` longtext NOT NULL COMMENT 'content', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', `c_desc` varchar(256) DEFAULT NULL, `c_use` varchar(64) DEFAULT NULL, `effect` varchar(64) DEFAULT NULL, `type` varchar(64) DEFAULT NULL, `c_schema` text, PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfo_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info';
CREATE TABLE `config_info_aggr` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(255) NOT NULL COMMENT 'group_id', `datum_id` varchar(255) NOT NULL COMMENT 'datum_id', `content` longtext NOT NULL COMMENT '内容', `gmt_modified` datetime NOT NULL COMMENT '修改时间', `app_name` varchar(128) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfoaggr_datagrouptenantdatum` (`data_id`,`group_id`,`tenant_id`,`datum_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='增加租户字段';
CREATE TABLE `config_info_beta` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL COMMENT 'content', `beta_ips` varchar(1024) DEFAULT NULL COMMENT 'betaIps', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfobeta_datagrouptenant` (`data_id`,`group_id`,`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_beta';
CREATE TABLE `config_info_tag` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', `tag_id` varchar(128) NOT NULL COMMENT 'tag_id', `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL COMMENT 'content', `md5` varchar(32) DEFAULT NULL COMMENT 'md5', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', `src_user` text COMMENT 'source user', `src_ip` varchar(50) DEFAULT NULL COMMENT 'source ip', PRIMARY KEY (`id`), UNIQUE KEY `uk_configinfotag_datagrouptenanttag` (`data_id`,`group_id`,`tenant_id`,`tag_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_info_tag';
CREATE TABLE `config_tags_relation` ( `id` bigint(20) NOT NULL COMMENT 'id', `tag_name` varchar(128) NOT NULL COMMENT 'tag_name', `tag_type` varchar(64) DEFAULT NULL COMMENT 'tag_type', `data_id` varchar(255) NOT NULL COMMENT 'data_id', `group_id` varchar(128) NOT NULL COMMENT 'group_id', `tenant_id` varchar(128) DEFAULT '' COMMENT 'tenant_id', `nid` bigint(20) NOT NULL AUTO_INCREMENT, PRIMARY KEY (`nid`), UNIQUE KEY `uk_configtagrelation_configidtag` (`id`,`tag_name`,`tag_type`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='config_tag_relation';
CREATE TABLE `group_capacity` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `group_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Group ID,空字符表示整个集群', `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数,,0表示使用默认值', `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_group_id` (`group_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='集群、各Group容量信息表';
CREATE TABLE `his_config_info` ( `id` bigint(64) unsigned NOT NULL, `nid` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `data_id` varchar(255) NOT NULL, `group_id` varchar(128) NOT NULL, `app_name` varchar(128) DEFAULT NULL COMMENT 'app_name', `content` longtext NOT NULL, `md5` varchar(32) DEFAULT NULL, `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `src_user` text, `src_ip` varchar(50) DEFAULT NULL, `op_type` char(10) DEFAULT NULL, `tenant_id` varchar(128) DEFAULT '' COMMENT '租户字段', PRIMARY KEY (`nid`), KEY `idx_gmt_create` (`gmt_create`), KEY `idx_gmt_modified` (`gmt_modified`), KEY `idx_did` (`data_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='多租户改造';
CREATE TABLE `tenant_capacity` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID', `tenant_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Tenant ID', `quota` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '配额,0表示使用默认值', `usage` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '使用量', `max_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个配置大小上限,单位为字节,0表示使用默认值', `max_aggr_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '聚合子配置最大个数', `max_aggr_size` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '单个聚合数据的子配置大小上限,单位为字节,0表示使用默认值', `max_history_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '最大变更历史数量', `gmt_create` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `gmt_modified` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='租户容量信息表';
CREATE TABLE `tenant_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `kp` varchar(128) NOT NULL COMMENT 'kp', `tenant_id` varchar(128) default '' COMMENT 'tenant_id', `tenant_name` varchar(128) default '' COMMENT 'tenant_name', `tenant_desc` varchar(256) DEFAULT NULL COMMENT 'tenant_desc', `create_source` varchar(32) DEFAULT NULL COMMENT 'create_source', `gmt_create` bigint(20) NOT NULL COMMENT '创建时间', `gmt_modified` bigint(20) NOT NULL COMMENT '修改时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_tenant_info_kptenantid` (`kp`,`tenant_id`), KEY `idx_tenant_id` (`tenant_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='tenant_info';
CREATE TABLE `users` ( `username` varchar(50) NOT NULL PRIMARY KEY, `password` varchar(500) NOT NULL, `enabled` boolean NOT NULL );
CREATE TABLE `roles` ( `username` varchar(50) NOT NULL, `role` varchar(50) NOT NULL, UNIQUE INDEX `idx_user_role` (`username` ASC, `role` ASC) USING BTREE );
CREATE TABLE `permissions` ( `role` varchar(50) NOT NULL, `resource` varchar(255) NOT NULL, `action` varchar(8) NOT NULL, UNIQUE INDEX `uk_role_permission` (`role`,`resource`,`action`) USING BTREE );
INSERT INTO users (username, password, enabled) VALUES ('nacos', '$2a$10$EuWPZHzz32dJN7jexM34MOeYirDdFAZm2kuWj7VEOJhhZkDrxfvUu', TRUE);
INSERT INTO roles (username, role) VALUES ('nacos', 'ROLE_ADMIN');
|
23.1.2 配置Nacos
进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf:

然后添加内容(分别对应你三个nacos节点的地址和端口):
1 2 3
| 127.0.0.1:8845 127.0.0.1.8846 127.0.0.1.8847
|
然后修改application.properties文件,添加数据库配置,对应自己电脑的数据库地址还有账号密码
1 2 3 4 5 6 7
| spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC db.user.0=root db.password.0=123
|
还有每个nacos的端口号要对应cluster.conf
(每个nacos都要进行配置)
23.1.3 启动nacos
这里直接运行startup.cmd,默认集群启动
23.1.4 配置nginx
修改conf/nginx.conf文件,配置如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| upstream nacos-cluster { server 127.0.0.1:8845; server 127.0.0.1:8846; server 127.0.0.1:8847; }
server { listen 80; server_name localhost;
location /nacos { proxy_pass http://nacos-cluster; } }
|
这就使用了nginx反向代理了Nacos
23.1.5 启动Nginx
而后在浏览器访问:http://localhost/nacos即可。
代码中application.yml文件配置如下:
1 2 3 4
| spring: cloud: nacos: server-addr: localhost:80
|
因为nginx监听80端口
24.Feign客户端
Feign是用来替代RestTemplate为服务与服务之间请求搭建桥梁
我们之前使用RestTemplate,其用法是
1 2 3
| String url = "http://localhost:8081/user/" + order.getUserId(); User user = restTemplate.getForObject(url, User.class);
|
25.使用Feign
引入依赖
1 2 3 4
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
|
在启动类上加注解,启动feign功能
1 2 3
| @EnableFeignClients
public class OrderApplication {
|
编写Feign的服务端接口
1 2 3 4 5
| @FeignClient("userserver") public interface UserClient { @GetMapping("/user/{id}") User findById(@PathVariable("id") Long id); }
|
@FeignClient(“userserver”):里面写访问服务名
@GetMapping(“/user/{id}”):写访问路径,有参数写参数(restful风格)
调用一下试试

成功查询出带用户信息的订单
26.Feign优化
Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:
•URLConnection:默认实现,不支持连接池
•Apache HttpClient :支持连接池
•OKHttp:支持连接池
因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。
首先引入依赖
1 2 3 4 5
| <dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
|
配置连接池
1 2 3 4 5
| feign: httpclient: enabled: true max-connections: 200 max-connections-per-route: 50
|
27. Feign最佳实践
27.1 继承方式
一样的代码可以通过继承来共享:
1)定义一个API接口,利用定义方法,并基于SpringMVC注解做声明。
2)Feign客户端和Controller都继承该接口

优点:
缺点:
27.2 抽取方式
将Feign的Client抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用。
例如,将UserClient、User、Feign的默认配置都抽取到一个feign-api包中,所有微服务引用该依赖包,即可直接使用。

28. 抽取方式实践
创建新的模块feign-api,用来放置feign服务

将原本order-service中的client还有它需要的user实体类提取出来,放到feign-api中,原项目里的就可以删除了

然后将feign-api以依赖的形式导入到order-service中
1 2 3 4 5
| <dependency> <groupId>cn.itcast.demo</groupId> <artifactId>feign-api</artifactId> <version>1.0</version> </dependency>
|
最后修改启动类的@EnableFeignClients注解,它默认是扫描当前模块的包,由于我们当前项目的client已经提取出去了,所以此处是扫描不到的,因此我们需要修改包扫描位置,改为导入的feign依赖的位置
1
| @EnableFeignClients(clients = UserClient.class)
|
29. 统一网关
网关功能
- 身份认证权限认证
- 服务路由,负载均衡(判断是访问哪个功能)
- 请求限流
实现
30. 网关搭建
创建网关模块,并引入依赖
1 2 3 4 5 6 7 8 9 10 11
| <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
|
配置路由文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| server: port: 10010 spring: application: name: gateway cloud: nacos: server-addr: localhost:8848 gateway: routes: - id: user-service uri: lb://userserver predicates: - Path=/user/** - id: order-service uri: lb://orderserver predicates: - Path=/order/**
|
这样我们就能从网关访问到其他服务

31. 路由断言predicates
我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件
用到哪个去官网查哪个怎么写
名称 |
说明 |
示例 |
After |
是某个时间点后的请求 |
- After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before |
是某个时间点之前的请求 |
- Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between |
是某两个时间点之前的请求 |
- Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie |
请求必须包含某些cookie |
- Cookie=chocolate, ch.p |
Header |
请求必须包含某些header |
- Header=X-Request-Id, \d+ |
Host |
请求必须是访问某个host(域名) |
- Host=.somehost.org,.anotherhost.org |
Method |
请求方式必须是指定方式 |
- Method=GET,POST |
Path |
请求路径必须符合指定规则 |
- Path=/red/{segment},/blue/** |
Query |
请求参数必须包含指定参数 |
- Query=name, Jack或者- Query=name |
RemoteAddr |
请求者的ip必须是指定范围 |
- RemoteAddr=192.168.1.1/24 |
Weight |
权重处理 |
|
32. 网关过滤器
过滤器的作用是什么?
① 对路由的请求或响应做加工处理,比如添加请求头
② 配置在路由下的过滤器只对当前路由的请求生效
defaultFilters的作用是什么?
① 对所有路由都生效的过滤器
1 2 3 4 5 6 7 8 9
| routes: - id: user-service uri: lb://userservice predicates: - Path=/user/** filters: - AddRequestHeader= Hello world! default-filters: - AddRequestHeader=Truth, Itcast is freaking awesome!
|
33. 全局过滤器
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。(之前说的那个是官方固定的几个写法)
首先创建过滤器接口,继承GlobalFilter,并实现filter方法
1 2 3 4 5 6
| public class AuthorizeFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { return null; } }
|
自己写拦截器接口中的拦截逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public class AuthorizeFilter implements GlobalFilter { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest(); MultiValueMap<String, String> queryParams = request.getQueryParams();
String authorization = queryParams.getFirst("authorization");
if("admin".equals(authorization)){
return chain.filter(exchange); }
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } }
|
在类上加注解
1 2 3
| @Order(-1) @Component public class AuthorizeFilter implements GlobalFilter {
|
Order是拦截器的访问顺序
然后重启网关,再次访问
发现无法访问了,再加上我们刚刚设置的请求参数,成功放行

(犯了一个错,创建gateway启动类的时候忘了,启动类应该在最外层,因为包扫描默认扫描的是启动类的同一目录下的内容,导致我配置的拦截器失效,把启动类放到最外层就可以了)
34. 过滤器排序规则
排序的规则是什么呢?
- 每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。
- GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
- 路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
- 当过滤器的order值一样时,会按照 defaultFilter > 路由过滤器 > GlobalFilter的顺序执行。
35. 解决跨域问题
在gateway服务的application.yml文件中,添加下面的配置:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| spring: cloud: gateway: globalcors: add-to-simple-url-handler-mapping: true corsConfigurations: '[/**]': allowedOrigins: - "http://localhost:8090" allowedMethods: - "GET" - "POST" - "DELETE" - "PUT" - "OPTIONS" allowedHeaders: "*" allowCredentials: true maxAge: 360000
|
36. RabbitMQ
微服务间通讯有同步和异步两种方式:
同步通讯:就像打电话,需要实时响应。
异步通讯:就像发邮件,不需要马上回复。
36.1 同步调用
同步调用的优点:
同步调用的问题:
- 耦合度高
- 性能和吞吐能力下降
- 有额外的资源消耗
- 有级联失败问题
36.2 异步调用
好处:
缺点:
- 架构复杂了,业务没有明显的流程线,不好管理
- 需要依赖于Broker的可靠、安全、性能
36.3 RabbitMQ安装
推荐使用docker镜像
安装完镜像后运行:
1 2 3 4 5 6 7 8 9
| docker run \ -e RABBITMQ_DEFAULT_USER=linfeng \ -e RABBITMQ_DEFAULT_PASS=123456 \ --name mq \ --hostname mq1 \ -p 15672:15672 \ -p 5672:5672 \ -d \ rabbitmq:3-management
|
用户名:RABBITMQ_DEFAULT_USER=linfeng
密码:RABBITMQ_DEFAULT_PASS=123456
15672是可视化平台的访问端口


36.4 消息模型
RabbitMQ官方提供了5个不同的Demo示例,对应了不同的消息模型:

36.5 RMQ的简单实现
依赖
1 2 3 4 5
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
|
发送消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public void testSendMessage() throws IOException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.10.128"); factory.setPort(5672); factory.setVirtualHost("/"); factory.setUsername("linfeng"); factory.setPassword("123456"); Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
String queueName = "simple.queue"; channel.queueDeclare(queueName, false, false, false, null);
String message = "hello, rabbitmq!"; channel.basicPublish("", queueName, null, message.getBytes()); System.out.println("发送消息成功:【" + message + "】");
channel.close(); connection.close();
}
|
接收消息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public static void main(String[] args) throws IOException, TimeoutException { ConnectionFactory factory = new ConnectionFactory(); factory.setHost("192.168.10.128"); factory.setPort(5672); factory.setVirtualHost("/"); factory.setUsername("linfeng"); factory.setPassword("123456"); Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
String queueName = "simple.queue"; channel.queueDeclare(queueName, false, false, false, null);
channel.basicConsume(queueName, true, new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { String message = new String(body); System.out.println("接收到消息:【" + message + "】"); } }); System.out.println("等待接收消息。。。。"); }
|
36.6 测试结果
运行发送端:

同时在RabbitMQ控制台中就能看到队列中有内容

运行接受端

37. SpringAMQP(hello world队列)
之前的实现方法是RabbitMQ官方的实现方法,可以看到代码非常多,很复杂。
SpringAMQP是基于RabbitMQ封装的一套模板,并且还利用SpringBoot对其实现了自动装配,使用起来非常方便。
SpringAMQP提供了三个功能:
- 自动声明队列、交换机及其绑定关系
- 基于注解的监听器模式,异步接收消息
- 封装了RabbitTemplate工具,用于发送消息
37.1 使用SpringAMQP
引入依赖
1 2 3 4 5
| <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
|
添加mq连接配置
1 2 3 4 5 6 7
| spring: rabbitmq: host: 192.168.10.128 port: 5672 username: linfeng password: 123456 virtual-host: /
|
编写发送代码
1 2 3 4 5 6 7 8 9 10 11 12
| public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate;
@Test public void testMessage(){ String queueName = "simple.queue"; String message = "hello,linfeng!hello world!";
rabbitTemplate.convertAndSend(queueName,message); } }
|
接收端也要配置
1 2 3 4 5 6 7
| spring: rabbitmq: host: 192.168.10.128 port: 5672 username: linfeng password: 123456 virtual-host: /
|
接受消息逻辑,编写监听类
1 2 3 4 5 6 7 8
| @Component public class SpringRabblitListener {
@RabbitListener(queues = "simple.queue") public void listenerQ(String msg){ System.out.println("接收到消息:" + msg); } }
|
运行springboot项目,便会自动监听
37.2 使用SpringAMQP(工作队列)
Work queues,也被称为(Task queues),任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息。

当消息处理比较耗时的时候,可能生产消息的速度会远远大于消息的消费速度。长此以往,消息就会堆积越来越多,无法及时处理。
此时就可以使用work 模型,多个消费者共同处理消息处理,速度就能大大提高了。
在多消息时,两个接收端会一起接受,差不多就是下图这种情况

37.3 消息预取限制
因为队列中的消息会提前分配到消费者中,分配的量是由消息预取值决定的,默认是无限,但这没有考虑根据消费者的能力进行考虑。
可以在配置文件中调整
1 2 3 4 5 6 7 8 9 10
| spring: rabbitmq: host: 192.168.10.128 port: 5672 username: linfeng password: 123456 virtual-host: / listener: simple: prefetch: 1
|
38. 发布订阅模式
发布订阅的模型如图:

可以看到,在订阅模型中,多了一个exchange角色,而且过程略有变化:
- Publisher:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
- Exchange:交换机,图中的X。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有以下3种类型:
- Fanout:广播,将消息交给所有绑定到交换机的队列
- Direct:定向,把消息交给符合指定routing key 的队列
- Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
- Consumer:消费者,与以前一样,订阅队列,没有变化
- Queue:消息队列也与以前一样,接收消息、缓存消息。
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
38.1 Fanout Exchange 广发
该种交换价接收到的消息会转发给所有于其绑定的队列上
绑定过程如下,新建一个交换机配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Configuration public class FanoutConfig {
@Bean public FanoutExchange fanoutExchange(){ return new FanoutExchange("linfeng.fanout"); }
@Bean public Queue fanoutQ1(){ return new Queue("fanout.q1"); }
@Bean public Queue fanoutQ2(){ return new Queue("fanout.q2"); }
@Bean public Binding fanoutBinding(Queue fanoutQ1,FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQ1).to(fanoutExchange); }
@Bean public Binding fanoutBinding2(Queue fanoutQ2,FanoutExchange fanoutExchange){ return BindingBuilder.bind(fanoutQ2).to(fanoutExchange); } }
|
接收消息端代码没什么变化,listener队列名即可
发送端将原先的队列名位置改为交换机名,发送方法需要routingKey(当前设置为空)
1 2 3 4 5 6 7 8
| public void testSendFanoutExchange(){
String exchangename = "linfeng.fanout";
String msg = "hello fanout exchange";
rabbitTemplate.convertAndSend(exchangename,"",msg); }
|
结果就是两个消费者哦度收到了同样的消息

38.2 DirectExchange 指定路由
每个Queue都会与Exchange设置一个BindingKey,发布者发送消息时也会指定一个RoutingKey,然后exchange将消息转发到和RoutingKey一致的BindingKey所指的消息队列上,另外不同Queue可以指定相同key

首先是创建交换机,然后将它和消息队列进行绑定,之前我们是用@Bean来实现的,太麻烦,这里提出改进方法,用注解的形式直接完成创建,到绑定,到指定key
1 2 3 4 5 6 7 8 9
| @RabbitListener( bindings = @QueueBinding( value = @Queue(name="direct.q1"), exchange = @Exchange(name="linfeng.direct",type= ExchangeTypes.DIRECT), key={"red","yellow"} )) public void listenDirectQ1(String msg){ System.out.println("接收到了:" + msg); }
|
这样就会自动创建交换机和绑定关系
下面写发送代码
1 2 3 4 5 6 7 8
| public void testSendDirectExchange(){
String exchangename = "linfeng.direct";
String msg = "hello Direct Exchange";
rabbitTemplate.convertAndSend(exchangename,"red",msg); }
|
rabbitTemplate.convertAndSend(exchangename,”red”,msg);
此处的red就是之前定义的key,因为red是两个队列都绑定的key,所以本此消息类似于群发

如果将key改为blue

只有2收到了
38.3 发布订阅TopicExchange
Topic
类型的Exchange
与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型Exchange
可以让队列在绑定Routing key
的时候使用通配符!
Routingkey
一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#
:匹配一个或多个词
*
:匹配不多不少恰好1个词
举例:
item.#
:能够匹配item.spu.insert
或者 item.spu
item.*
:只能匹配item.spu
图示:

解释:
- Queue1:绑定的是
china.#
,因此凡是以 china.
开头的routing key
都会被匹配到。包括china.news和china.weather
- Queue2:绑定的是
#.news
,因此凡是以 .news
结尾的 routing key
都会被匹配。包括china.news和japan.news
举例实践一把:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @RabbitListener( bindings = @QueueBinding( value = @Queue(name = "topic.q1"), exchange = @Exchange(name = "linfeng.topic",type = ExchangeTypes.TOPIC), key = "china.#" ) ) public void listenTopicQ1(String msg){ System.out.println("1接收到的消息:" + msg); }
@RabbitListener( bindings = @QueueBinding( value = @Queue(name = "topic.q2"), exchange = @Exchange(name = "linfeng.topic",type = ExchangeTypes.TOPIC), key = "#.news" ) ) public void listenTopicQ2(String msg){ System.out.println("2接收到的消息:" + msg); }
|
简单绑定两个,和之前的设置差不多,下面是发送端
1 2 3 4 5 6
| public void testSendTopicExchange(){ String exchangename = "linfeng.topic";
String msg = "hello world!"; rabbitTemplate.convertAndSend(exchangename,"china.huainan",msg + "china.huainan"); }
|
指定的是china.huainan,很显然符合key:china.#

如果改为huainan.news,很显然会匹配到Q2

39. 消息转换器
Spring会把你发送的消息序列化为字节发送给MQ,接收消息的时候,还会把字节反序列化为Java对象。

只不过,默认情况下Spring采用的序列化方式是JDK序列化。众所周知,JDK序列化存在下列问题:
推荐使用json转换器
39.1 json转换器配置
在publisher和consumer两个服务中都引入依赖:
1 2 3 4 5
| <dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> <version>2.9.10</version> </dependency>
|
配置消息转换器。
在启动类中添加一个Bean即可:
1 2 3 4
| @Bean public MessageConverter jsonMessageConverter(){ return new Jackson2JsonMessageConverter(); }
|