项目地址:源代码
仅作为学习用例使用,是我开发过程中的总结、实际的一部分使用方式
开发环境:
jdk11
springboot2.7.6
springcloud2021.0.5
alibabacloud 2021.0.4.0
redis6.0
mysql8.0
一、项目搭建
wdz-api:存放远程服务调用相关接口
wdz-auth:认证业务
wdz-gateway:网关
wdz-modules:业务模块微服务
wdz-common:存放公用中间件、数据库等服务
pom
<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><packaging>pom</packaging><modules><module>wdz-common</module><module>wdz-gateway</module><module>wdz-api</module><module>wdz-auth</module><module>wdz-modules</module></modules><groupId>com.wdz</groupId><artifactId>wdz-ruzhou</artifactId><version>1.0.0</version><name>wdz-ruzhou</name><description>微服务架构</description><properties><java.version>11</java.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding><spring-boot.version>2.7.6</spring-boot.version><spring-cloud.version>2021.0.5</spring-cloud.version><spring-cloud.alibaba>2021.0.4.0</spring-cloud.alibaba><fastjson2.version>2.0.14</fastjson2.version><lombok.version>1.18.24</lombok.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${spring-cloud.alibaba}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>${lombok.version}</version></dependency><dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>${fastjson2.version}</version></dependency></dependencies></dependencyManagement><profiles><profile><id>dev</id><properties><profiles.active>dev</profiles.active><nacos.username>nacos</nacos.username><nacos.password>nacos</nacos.password><nacos.server>127.0.0.1:8848</nacos.server><nacos.discovery.namespace>5efed786-91f6-44c4-8e14-049df72b2a48</nacos.discovery.namespace><nacos.config.namespace>5efed786-91f6-44c4-8e14-049df72b2a48</nacos.config.namespace></properties><activation><activeByDefault>true</activeByDefault></activation></profile><profile><id>test</id><properties><profiles.active>test</profiles.active><nacos.username>nacos</nacos.username><nacos.password>nacos</nacos.password><nacos.server>127.0.0.1:8848</nacos.server><nacos.discovery.namespace>f02ccd30-e3d5-4235-9282-d0a613bd322b</nacos.discovery.namespace><nacos.config.namespace>f02ccd30-e3d5-4235-9282-d0a613bd322b</nacos.config.namespace></properties></profile><profile><id>prod</id><properties><profiles.active>prod</profiles.active><nacos.username>nacos</nacos.username><nacos.password>nacos</nacos.password><nacos.server>127.0.0.1:8848</nacos.server><nacos.discovery.namespace>747829ca-6d8a-4417-a46a-bd31a6f5e442</nacos.discovery.namespace><nacos.config.namespace>747829ca-6d8a-4417-a46a-bd31a6f5e442</nacos.config.namespace></properties></profile></profiles><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>3.0.0</version></plugin><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><configuration><source>${java.version}</source><target>${java.version}</target><encoding>${project.build.sourceEncoding}</encoding></configuration></plugin></plugins><resources><resource><directory>src/main/resources</directory><includes><include>application*</include><include>bootstrap*</include><include>logback*</include></includes><filtering>true</filtering></resource></resources></build></project>
nacos(2.2.0.1)
我为什么选择nacos作为注册中心?
1、独立的可视化管理后台
2、持久化,配置数据直接存储在数据库中
3、动态配置,修改配置可实时生效,不用重启
4、活跃度高,资料文件丰富
5、功能全面
6、是经过阿里巴巴考验的
戳==>官网
安装nacos
点击选择版本下载
windows建议下载zip
linux 建议下载tar.gz
下载完成解压即可
配置数据库
打开conf下的application.properties文件:
修改数据库配置,将注释打开,并配置数据源
创建数据库目前只支持mysql,版本要求:5.6.5+
需要IPV6支持使用:derby-schema.sql 否则使用:mysql-schema.sql 配置完成后
单机启动nacos:
Linux/Unix/Mac使用命令:sh startup.sh -m standalone
windows使用命令:./startup.cmd -m standalone
standalone:表示单机模式运行,非集群模式
官方文档
启动过程中如果出现jwt认证问题启动失败,请参考:戳详情
错误示例
修改文件:
nacos.core.auth.plugin.nacos.token.secret.key
使用base64 处理数据长度大于等于32即可
访问:http://localhost:8848/nacos,默认用户名密码:nacos
集群启动 官方文档
如果是在一台电脑上/服务器上模拟集群:
将nacos解压之后的文件nacos复制多份,
并修改对应conf文件夹下的application.properties文件端口配置
再将conf下的cluster.conf.example 复制一份并修改后缀为conf
将其他nacos地址配置在cluster.conf中
依次启动服务:
windows命令:./startup.cmd
linux命令: sh ./startup.sh
访问对应nacos
nacos 服务配置config与发现 discovery
创建测试注册服务网关服务,nacos作为注册中心有多种方式,本文用javaSDK的方式
pom文件
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>wdz-ruzhou</artifactId><groupId>com.wdz</groupId><version>1.0.0</version></parent><modelVersion>4.0.0</modelVersion><groupId>com.wdz</groupId><artifactId>wdz-gateway</artifactId><packaging>jar</packaging><dependencies><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><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>
yml配置:
server:port: 8080servlet:context-path: /spring:application:name: wdz-gatewayprofiles:active: @profiles.active@---# 使用导入的方式读取所有需要的nacos中的配置文件spring:config:import:- optional:nacos:application-common.yml- optional:nacos:${spring.application.name}.ymlcloud:nacos:username: @nacos.username@password: @nacos.password@server-addr: @nacos.server@# 服务发现discovery:namespace: @nacos.discovery.namespace@# 配置config:namespace: @nacos.config.namespace@
集群nacos负载配置:
通过nginx配置服务访问的nacos
upstream nacos-cluster{server 127.0.0.1:8247;server 127.0.0.1:8549;server 127.0.0.1:8848;}server{listen 801;server_name localhost;location /nacos{proxy_pass http://nacos-cluster;}}
结果
负载均衡Ribbon
内置负载均衡规则类 | 规则描述 |
---|---|
RoundRobinRule | 简单轮询服务列表选择服务器,它是Ribbon默认的负载均衡规则 |
AvailabilityFilteringRule | 忽略规则:1、链接3次失败,会标记为短路状态,将持续30秒,如果还是失败则持续短路,时间增长,2、并发数过高的服务链接,并发数达到上限则会被忽略 |
WeightedResponseTimeRule | 权重策略,每个服务器配置一个权重值,权重值越小,选择这个服务器的比重就会越小 |
ZoneAvooidanceRule | 以区域可用的服务器为基础进行服务器的选择,使用Zone对服务器进行分类,Zone相当于一个机房,一个区域,然后再对Zone内的多个服务做轮询 |
BestAvailableRule | 忽略哪些短路服务器,并选择并发数较低的服务器 |
RandomRule | 随机选择一个可用服务器 |
RetryRule | 重试机制的选择逻辑 |
自定义策略方式:
代码方式:作用的是所有服务@BeanpublicIRule diyRule(){return new RandomRule();}
yml方式:作用的是所配置的服务servicename: # 服务名称如:userserviceribbon:NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule
ribbon默认是懒加载,第一次访问的时候响应时间会比较长,初次加载之后响应就会变短。
开启ribbon饥饿加载有效提高第一次访问响应时间
ribbon:eager-load:enabled: true# 开启饥饿加载clients: # 指定加载的服务名称- userservice- orderservice- systemservice- XXXservice
nacos服务分级存储模型
一级是服务:如:userservice、orderservice
二级是集群:配置了同一个discovery.cluster-name:
三级是实例:如杭州机房的某台部署了要访问的服务器
设置实例的集群属性:名称一样的在同一个集群内
spring.cloud.nacos.discovery.cluster-name:NacosRule负载均衡策略:
1、优先选择同集群的服务
2、本地集群找不到提供者,才去其他集群找,并且会报警告
3、确定了可用实例列表后,再采用随机负载均衡挑选实例
NacosRule配置:
servicename: # 服务名称如:userserviceribbon:NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
环境隔离:namespace
spring:cloud:nacos:discovery:namespace: @nacos.discovery.namespace@ # 不同名称空间之间的服务不能互通,如user使用的prod order使用的dev,这两个服务不能互通
nacos特点
1、支持服务端主动监测提供者状态,临时实例采用心跳模式,非临时实例采用主动监测模式
2、林实施例心跳不正常会被剔除nacos的服务列表,非临时实例不会被剔除
3、服务列表变更消息推送模式,服务列表更新及时
4、nacos集群默认采用AP方式,集群中存在非临时实例时,采用CP模式
CAP:C(一致性),A(可用性),P(分区容错)原文
Feign 源码
feign是声明式的http客户端
使用方式
pom引入依赖<dependency> <groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>java代码@FeignClient("applicationName")public interface ApplicationName{@GetMapping("/get")Object queryById(Long id);}
自定义feign配置
类型 作用 说明 feign.Logger.Level 修改日志级别 包含四种不同的级别:none、basic、headers、full feign.codec.Decoder 响应结果解析器 http远程调用的结果做解析,例如解析json字符串为java对象 feign.codec.Encoder 请求参数编码 将请求参数编码,便于通过http请求发送 feign.Contract 支持注解格式 默认是SpringMvc的注解 feign.Retryer 失败重试机制 请求失败的重试机制,默认没有,会使用Ribbon的重试 none: 不记录日志
basic:只记录请求方法和URL以及响应状态代码和执行时间
headers:记录基本信息以及请求和响应标头
full:记录请求和响应的头部、主体和元数据
yml日志配置
feign:client:config:default: # DETAULT 是全局配置,如果换成application.name 则是指定服务有效loggerLevel: FULLfeign:client:config:userservice: # 针对userservice有效loggerLevel: FULL
代码方式:
public class FeignClientConfig{@Beanpublic Logger.Level feignLogLevel(){return Logger.Level.BASIC;}}然后将该配置添加到:全局配置方式启动类上EnableFeignClients(defaultConfiguration=FeignClentConfig.class)局部配置方式@FeignClient(value="applicationname",configuration=FeignClientConfig)
feign性能优化
URLConnection:默认实现,不支持连接池
Apache HttpClient:支持链接池
OKHttp:支持连接池
因为http链接时要三次握手,断开时要4次挥手,导致的性能浪费
优化的性能主要包括:
1、使用连接池代替默认的URLConnection
2、日志级别,最好用basic或none,日志也会浪费性能
连接池配置:
引入依赖
<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-httpclient</artifactId></dependency>
配置yml
feign:httpclient:enabled: true # 开启feign对httpClient的支持max-connections: 200 # 最大链接数可根据测试结果最优配置max-connections-per-route: 50 # 每个路径最大链接数 可根据测试结果最优配置
实践:
system项目引用
EnableFeignClients.clients 指定feignClient接口,及默认配置
如果发现RemoteUserService 加载不到提示,spring-cloud-starter-loadbalancer依赖缺失
在api中添加依赖:负载均衡依赖
<dependency> <groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency>
测试接口
目标服务接口
测试结果:
SpringAMQP
springboot封装的消息队列,使用的是rabbitmq
生产端
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
配置
spring:rabbitmq:host: 127.0.0.1 # 主机port: 5672 # 端口virtual-host: /# 虚拟主机username: rabbitmq # 用户名password: 123456# 密码
消费端
引入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
配置
spring:rabbitmq:host: 127.0.0.1 # 主机port: 5672 # 端口virtual-host: /# 虚拟主机username: rabbitmq # 用户名password: 123456# 密码
监听java代码:
@Componentpublic class RabbitMqListener {@RabbitListener(queues = "mq-user")public void listenerQueueMsg(String msg){System.out.println(msg);}}
如果提示异常:队列不存在,且启动失败
添加配置:
@Configurationpublic class RabbitMqConfig {@Beanpublic Queue initQueue(){return new Queue("mq-user");}}
由于rabbitmq的消费预取机制,会导致处理能力弱的消费端处理速度变慢,所以限制预取信息条数
spring:rabbitmq:listener:simple:prefetch: 1 # 每次只能获取一条消息,处理完成之后才能获取下一个
监听端信息:
@Slf4j@Componentpublic class RabbitMqListener {@RabbitListener(queues = "mq-user")public void listenerQueueMsg(String msg) {log.info("[listenerQueueMsg]=========:{}",msg);}@RabbitListener(queues = "mq-user")public void listenerQueueMsg2(String msg) {log.info("[listenerQueueMsg2]=========:{}",msg);}}
消息发布/订阅
发布订阅模式允许将同一消息发送给多个消费者,实现方式是加入exchange(交换机):
fanout:广播
direct:路由
topic:话题
交换机只处理转发,会造成消息丢失
FanoutExchange 方式
发送消息用户服务:
@GetMapping("/send") public LoginUser send(String username) { String queueName = "demo.fanout"; String message = username; rabbitTemplate.convertAndSend(queueName,"", message); return new LoginUser(1L, username, "123456"); } @GetMapping("/to") public LoginUser to(String username) { String queueName = "demo.fanout"; String message = username; rabbitTemplate.convertAndSend(queueName,"", message); return new LoginUser(1L, username, "123456"); }
接收消息 system服务:
监听代码 @RabbitListener(queues = "demo.fanout2") public void listenerFanoutMsg2(String msg) { log.info("[demo.fanout2]=========:{}", msg); }
@Configurationpublic class FanoutExchangeConfig {/** * 一个消息多个消费者监 * fanout 广播模式 * 定义交换机 */@Beanpublic FanoutExchange fanoutExchange() {return new FanoutExchange("demo.fanout");}@Beanpublic Queue fanoutQueue() {return new Queue("demo.fanout");}/** * 绑定交换机与队列关系 * * @param fanoutExchange * @param fanoutQueue * @return */@Beanpublic Binding fanoutBinding(FanoutExchange fanoutExchange,Queue fanoutQueue) {return BindingBuilder.bind(fanoutQueue).to(fanoutExchange);}}
接收消息 order服务:
监听@Slf4j@Componentpublic class RabbitMqListener {@RabbitListener(queues = "demo.fanout")public void listenerFanoutMsg(String msg) {log.info("[demo.fanout]=========:{}", msg);}}
fanout 配置
@Configurationpublic class FanoutExchangeConfig {/** * 一个消息多个消费者监 * fanout 广播模式 * 定义交换机 */@Beanpublic FanoutExchange fanoutExchange() {return new FanoutExchange("demo.fanout");}@Beanpublic Queue fanoutQueue() {return new Queue("demo.fanout2");}/** * 绑定交换机与队列关系 * * @param fanoutExchange * @param fanoutQueue * @return */@Beanpublic Binding fanoutBinding(FanoutExchange fanoutExchange,Queue fanoutQueue) {return BindingBuilder.bind(fanoutQueue).to(fanoutExchange);}}
均可接收到消息
DirectExchange 方式
direct exchange 会将接收到的消息根据规则路由到指定的queue
每个queue都与exchange设置一个bindingkey
发布者发送消息时指定消息的RoutingKey
exchange将消息路由到BingingKey与消息RoutingKey一致的队列
order/system中的监听
// 注解方式处理监听信息// bindings 代码实现的Binding// @QueueBinding 绑定信息// value = @Queue(name = "direct.queue")绑定消息队列//exchange = @Exchange(name = "demo.direct",type = ExchangeTypes.DIRECT)// 绑定交换机,名称是demo.direct 类型是direct// key 是direct模式的主要配置,可同时监听多个key@RabbitListener(bindings = @QueueBinding(value = @Queue(name = "direct.queue"),exchange = @Exchange(name = "demo.direct",type = ExchangeTypes.DIRECT),key = {"system","direct"}))public void listenerMessage(String msg) {log.info("[system.direct]=========:{}", msg);}
消息发送者:
/***@param exchange交换机名称*@param message 消息内容*@param routingKeydirect 路由key*@return*/ @GetMapping("/direct") public void direct(String exchange,String message,String routingKey) { rabbitTemplate.convertAndSend(exchange,routingKey, message); }
发送信息:
http://localhost/user/direct” />Topic 方式
topic方式与direct方式类似,只是key由精准匹配转换为模糊匹配
// 监听 @RabbitListener(bindings = @QueueBinding(value = @Queue(name = "topic.queue"),exchange = @Exchange(name = "topic-wdz",type = ExchangeTypes.TOPIC),key = "#.order"))public void listenerTopicMessage(String msg) {log.info("[listenerTopicMessage]=========:{}", msg);}
发送
@GetMapping("/direct")public void direct(String exchange,String message,String routingKey) {rabbitTemplate.convertAndSend(exchange,routingKey, message);}
优化
由于springamqp默认使用的是jdk默认的序列化(ObjectOutputStream)方式,性能问题有待提升。
更换为JSON方式序列化。
生产者/消费者引入依赖
<dependency><groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformats-binary</artifactId><version>2.13.4</version></dependency>
初始化bean即可
@BeanpublicMessageConverter messageConverter(){return new Jackson2JsonMessageConverter();}
Sentinel
分布式事务seata
事务(transaction):遵守ACID原则
原子性(Atomicity):事务中的所有操作,要么全部成功,要么全部失败
一致性(consistency):要保证数据库内部完整性的约束、声明性约束
隔离性(isolation):对同一自愿操作的事务不能同时发生
持久性(durability):对数据库的一切修改将永久保存,不管是否出现故障
演示案例:
下单流程:
订单服务创建订单–>账户服务扣减余额–>商品服务扣减库存
1、访问订单服务创建订单
2、通过feign调用user扣减余额
3、通过feign调用goods扣减库存
feign接口
@FeignClient(value = "wdz-goods")public interface RemoteGoodsService {@GetMapping("/goods/subtractStock")void subtractStock(@PathVariable("username") String username);}
@FeignClient(value = "wdz-user")public interface RemoteUserService {@GetMapping("/user/subtractBalance")void subtractBalance();}
控制器代码
订单服务控制器
@RestController@RequestMapping("order")public class OrderController {@Autowiredprivate RemoteGoodsService remoteGoodsService;@Autowiredprivate RemoteUserService remoteUserService;@GetMapping("create")public void create(){System.out.println("订单服务:创建了订单");remoteGoodsService.subtractStock();remoteUserService.subtractBalance();}}
用户服务控制器代码
@RestController@RequestMapping("user")public class UserController {@GetMapping("subtractBalance")public void subtractBalance(){System.out.println("用戶服务:减余额");}}
商品控制器代码
@RestController@RequestMapping("goods")public class GoodsController {@GetMapping("subtractStock")public void subtractStock(){System.out.println("商品服务:减少了库存");}}
依次启动nacos-->gateway-->user-->goods-->order访问:http://localhost:8080/order/order/create订单服务:创建了订单用戶服务:减余额商品服务:减少了库存
待续。。。。。