目录
5.1:创建统一配置服务端springcloud_config
8:消息驱动微服务的框架 spring cloud stream RabbitMQ
1.新建一个eureka服务端
代码/配置如下
2.配置
两个application,让两个euraka相互注册
3.启动
eureka | 启动端口 | 注册地址 |
服务端application1 | 8761 | 8762 |
服务端application2 | 8762 | 8761 |
客户端application | 8080 | 8761 |
服务端application1,defaultZone配置成服务端application2的端口8762,启动application2,将defaultZone配置成application1的端口8761,客户端application我们就注册到一个服务端application1,也就是8761上面.
然后刷新8761和8762,得到结果,两个服务端都有客户端application,此时需要注意此时服务端相互注册的defaultZone需要配置成127.0.0.1,不要使用localhost,不然实现不了,搞半天.
此时完成了两个服务端的相互注册,然后客户端注册到一个服务端application1,此时两个服务端都能看到客户端注册上来的信息,但是此时如果断掉客户端application1,虽然此时还能再application2上面看到客户端,但是此时只是因为心跳原因,等下一个心跳检测后就会看不到了,因为我们的客户端application只注册到了服务端application1,application2只是从application1同步信息过来的.
方案就是将客户端application注册到服务端application1和服务端application2上,客户端配置:这样就算某个服务端挂了,另外的服务端还是能运行,客户端也能注册服务.
eureka | 启动端口 | 注册地址 |
服务端application1 | 8761 | 8762 |
服务端application2 | 8762 | 8761 |
客户端application | 8080 | 8761和8762 |
4.多个客户端之间相互通信
为了模拟,前面第一个客户端作为employee客户端,再新建一个department客户端.然后在department中调用employee客户端的接口
方式1:使用RestTemplate
@RequestMapping("testCloud")
public String testCloud(){
//springcloud之间通信
RestTemplate template = new RestTemplate();
String forObject = template.getForObject("http://127.0.0.1:8771/testCloud", String.class);
return forObject;
}
方式2:
@Autowired
private LoadBalancerClient loadBalancerClient;
//方式2:使用loadBalancerClient
RestTemplate template = new RestTemplate();
ServiceInstance serviceInstance = loadBalancerClient.choose("employee");
String format = String.format("http://%s:%s", serviceInstance.getHost(), serviceInstance.getPort()+"testCloud");
String forObject = template.getForObject(format, String.class);
return forObject;
方式3:
@Component
public class RestTemplateConfig {
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
//方式3,主要使用@LoadBalanced注解,EMPLOYEE是eureka中Application名称,实现负载均衡,通过服务名调用该服务名下面注册的多个客户端中的任意一个.
String forObject = restTemplate.getForObject("http://EMPLOYEE/testCloud", String.class);
return forObject;
多模块出现被调用项目中有参数的接口会出现参数传递不过去的问题,使用@PostMapping和@RequestBody能够解决这个问题.
5.使用统一配置文件
5.1:创建统一配置服务端springcloud_config
1.创建一个springboot项目,选择spring cloud config的config server和spring cloud discovery的eureka discovery client
2.加上@EnableDiscoveryClient标识是一个springcloud的client的项目
3.加上@EnableConfigServer标识是一个springcloud的同一配置服务项目
4.到github新建一个项目,然后创建一个文件,里面放我们需要统一配置的配置文件,命名employee.yml
5.2.config server项目加上配置
spring:
application:
name: config
cloud:
config:
server:
git:
uri: https://github.com/dearpeng/springcloud_config_repo// 新建的项目git地址
username: ****** //git登录账号
password: ****** //git的登录密码
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka/ //springcloud服务注册地址
server:
port: 9768
启动config server ,然后访问localhost:9768/employee-a.yml就能看到统一配置的配置文件
我们可以使用下面的方式来访问统一配置中心的配置
1: /文件名-环境.yml/properties/json,这个时候默认访问的是master分支的配置,例如:localhost:9768/employee-dev.yml
2: /分支/文件名-环境.yml/properties/json,这个时候访问的是某个分支下的配置文件,例如:
localhost:9768/feature_V_2020_09_17_September/employee-dev.yml
到这里,统一配置服务端已经完成了
5.3:统一配置客户端
其实就是我们需要引用配置中心的项目,比如我们需要在employee项目中引用配置中心的配置
5.3.1:引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
5.3.2:在employee项目中引用配置
spring:
application:
name: employeeApp //这个是employee的应用名,同时也要是统一配置中心配置文件的文件名
cloud:
config:
discovery:
enabled: true
service-id: CONFIG
profile: dev //配置中心配置文件的文件名,和application.name一起确定到使用哪个配置文件
注意事项:
1:配置中心有的配置文件
如果我们需要使用employeeApp-dev.yml这个配置文件,这个配置文件的 - 前面必须和spring.application.name相同,profile就是 - 的后一部分,总结就是通过 application.name-profile 来确定找到我们需要引用的配置文件
2:项目启动的时候回去找配置,但是现在我们应用的统一的配置,所以此时会报错,我们需要让配置文件先去统一配置中心拉取配置文件,所以要将application.yml文件设置陈bootstrap.yml.
3:项目启动的时候会将默认的也就是employee.yml和employee-dev.yml都加载下来,然后合并配置文件,所以在employee.yml中最好放所有环境通用的配置.
实验结果employee.yml配资eureka端口是8762,employee-dev.yml里面没有配置eureka,但是访问也没得到8762端口
结果启动完成
6.spring cloud bus更新统一配置
6.1:现在我们在通过config项目去统一配置中心git上面获取统一配置文件,但是每次修改git配置中心的配置都需要重启项目,我们可以使用spring cloud bus来灵活获取配置,现在我们用employee项目通过configserver项目去git拉取配置来举例
employee和configserver都需要加入springcloud 和rabbitmq的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>
1) employee的配置文件,从配置中心获取名字为employeeApp-dev.yml的配置文件
2) configserver项目的配置文件,主要是为了保留bus-refresh接口,management这个根据不同版本有区别
没有这个配置会出现404问题,找不到这个接口
3) 为了方便演示,我们在employee项目中获取一下此时employee项目使用的配置文件,类上要加上@RefreshScope注解,不然无效
此时env配置的是
访问接口得到我们现在需要的配置
4) 然后我们去修改env的配置为dev,此时如果直接访问,还是不行,依然是dev999121212,此时我们需要请求一下configserver的bus-refresh接口,让它更新一下配置,打开cmd,请求configserver的接口curl -v -X POST "http://localhost:9768/actuator/bus-refresh",请求的状态是204才是成功
6) 不需要重启employee项目,然后再次访问就是dev了,这个时候我们就已经拉取到了配置中心的配置了.
总结一下:
1: 配置configserver项目,暴露bus-refresh接口
2: employee项目类上加@RefreshScope注解刷新
3: 修改配置中心的配置
4: 调用一下configserver的 curl -v -X POST "http://localhost:9768/actuator/bus-refresh"接口,发送给rabbitmq,mq再通知employee项目.
5: 访问employee的test/print接口,看到我们修改的配置是dev
注意: 1: 需要发送接收mq消息的都需要加上bus-amqp依赖,这里是employee和configserver项目
2: 是要configserver,也就是连接employee和git配置中心的项目暴露bus-refresh接口,还有就是employee的RefreshScope注解别忘了.
思考:
我们这边每次修改springcloud_config_repo配置中心的文件时,我们都需要命令行窗口中调一下bus-refresh接口,我们可以使用git的webHooks功能,在payload url中写上我们http://localhost:9768/actuator/bus-refresh地址对应的外网地址.然后每次修改提交的时候都会发送一个post请求.内网转外网我们可以使用natapp,直接访问他的官网natapp.cn,弄一个免费的通道实验一下.
7.使用rabbitmq实现异步
7.1:导入rabbitMq依赖
<!--引入rabbitmq依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
7.2:编写消息接收代码
@Component
@Slf4j
public class MyRabbitMqReceive {
// @RabbitListener(queues = "myRabbitMq") //使用这个queues不会自己在rabbitmq创建一个名字为myRabbitMq的队列,需要手动创建
@RabbitListener(queuesToDeclare = @Queue("myRabbitMq"))
public void receive(String message) {
log.info("接收到rabbitMq消息:"+message);
}
}
7.3: 编写消息发送代码
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProductApplicationTests {
@Test
public void contextLoads() {
}
@Autowired
private AmqpTemplate amqpTemplate;
@Test
public void testSendMqMessage(){
amqpTemplate.convertAndSend("myRabbitMq","当前时间:"+ new Date());
}
}
7.4:启动项目,运行单元测试结果
疑问:假设订单系统下单后,根据不同的订单类型发送到不同的队列中去,当然我们可以定义不同的队列,不使用exchange的Routing key去路由到对应的队列,但是最优雅的方式还是使用exchange来完成这个功能,消息发送先通过exchange,然后通过Routing key路由到对应的队列中去,代码修改如下:
接收端:
@Component
@Slf4j
public class MyRabbitMqReceive {
// @RabbitListener(queues = "myRabbitMq") //使用这个queues不会自己在rabbitmq创建一个名字为myRabbitMq的队列,需要手动创建
// @RabbitListener(queuesToDeclare = @Queue("myRabbitMq")) //会自动在rabbitmq创建队列
@RabbitListener(bindings = @QueueBinding(
key = "computer", //消息发送出去,通过key来指定到那个queue中去,发送给队列的唯一标识,
exchange = @Exchange("myExchange"), //消息先通过exchange,然后通过key分发到queue中
value = @Queue("computerQueue")
)) //有的时候我们需要将exchange和队列进行绑定,订单系统下了水果订单和电脑订单分别发送给水果和电脑项目中去
public void computeReceive(String message) {
log.info("电脑项目到rabbitMq消息:" + message);
}
@RabbitListener(
bindings = @QueueBinding(
key = "fruit", //消息发送出去,通过key来指定到那个queue中去,发送给队列的唯一标识,
exchange = @Exchange("myExchange"), //消息先通过exchange,然后通过key分发到queue中
value = @Queue("fruitQueue")
)
)
public void fruitReceive(String message) {
log.info("水果项目接收到rabbitmq消息:" + message);
}
}
发送端:发送到myExchange然后再通过computer这个Routing key路由到computerQueue队列中
@Test
public void testExchangeSendMqMessage(){
amqpTemplate.convertAndSend("myExchange","computer","当前时间:"+ new Date());
}
然后得到的结果是
顺便看看rabbitmq上面是什么样子
8:消息驱动微服务的框架 spring cloud stream RabbitMQ
其实这个功能还是消息,只是在spring cloud中的消息,具体有哪些好处等我深入学习后再来总结
学习前,先查了下这个stream的基本只是,copy来的,刚开始学,基本的还是需要了解一下.
8.1:spring cloud stream相关概念
从上图我们知道spring cloud stream 通过input channels和output channels跟Binder交互,特定Binder在与对应的外部broker节点交互。
Binder 是 Spring Cloud Stream 的一个抽象概念,是应用与消息中间件之间的粘合剂。目前 Spring Cloud Stream 实现了 Kafka 和 Rabbit MQ 的binder。
通过 binder ,可以很方便的连接中间件,可以动态的改变消息的
destinations(对应于 Kafka 的topic,Rabbit MQ 的 exchanges),这些都可以通过外部配置项来做到。
甚至可以任意的改变中间件的类型而不需要修改一行代码。
消息的发布(Publish)和订阅(Subscribe)是事件驱动的经典模式。Spring Cloud Stream 的数据交互也是基于这个思想。生产者把消息通过某个 topic 广播出去(Spring Cloud Stream 中的 destinations)。其他的微服务,通过订阅特定 topic 来获取广播出来的消息来触发业务的进行。
这种模式,极大的降低了生产者与消费者之间的耦合。即使有新的应用的引入,也不需要破坏当前系统的整体结构。
Spring Cloud Stream 的这个分组概念的意思基本和 Kafka 一致。
微服务中通过对同一个应用创建多个实例来达到更高的处理能力是非常必须的。对于这种情况,同一个事件防止被重复消费,只要把这些应用放置于同一个 “group” 中,就能够保证消息只会被其中一个应用消费一次。
通过spring.cloud.stream.bindings.<channelName>.group=demo这种方式在配置文件中配置。
消息事件的持久化是必不可少的。Spring Cloud Stream 可以动态的选择一个消息队列是持久化,还是非持久化。
bindings 是我们通过配置把应用和spring cloud stream 的 binder 绑定在一起,之后我们只需要修改 binding 的配置来达到动态修改topic、exchange、type等一系列信息而不需要修改一行代码。
8.2:spring cloud stream的简单应用
8.2.1: 导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
8.2.2: 消息发送通道接口
public interface MyStreamMqClient {
public final static String channelName = "myStreamRabbitMq";
/**
* stream 输入渠道
* @return
*/
//@Input(channelName)
//SubscribableChannel input();
/**
* stream输出渠道
* @return
*/
@Output(channelName)
MessageChannel output();
}
这里有一个问题存在,就是input和output写在一个类里面启动的时候会报错,我看网上也没有相关的答案,按照有些博主的使用,然后也没怎么注意他们是两个工程这个问题,因为在rabbitmq直接使用的时候在一个工程是可以的.报的问题如下
解决这个问题的灵感来之org.springframework.cloud.stream.messaging.Processor这个类,看到有博主使用这个类的通道名,然后这个类继承了Source, Sink,分别有output和input方法,看到别人视频这两个写在一个类里面没报错
8.2.3: 消息发送
使用单元测试重要启动,所以使用controller直接调用
@RequestMapping("stream/mq")
@RestController
public class MyStreamMqSenderController {
@Autowired
MyStreamMqOutputClient myStreamMqOutputClient;
/* @Autowired
private Processor processor;*/
@GetMapping("sendMsg")
public String sendMsg() {
String msg = "当前时间:" + new Date();
// processor.output().send(MessageBuilder.withPayload(msg).build());
System.out.println("发送的消息:"+msg);
myStreamMqOutputClient.output().send(MessageBuilder.withPayload(msg).build());
return msg;
}
}
8.2.4: 消息接收
消息接收要绑定通道@EnableBinding,使用@StreamListener注解
@EnableBinding(MyStreamMqOutputClient.class) //绑定通道,接收该通道的消息
@Slf4j
@Component
public class MyStreamMqReceiver {
@StreamListener(MyStreamMqOutputClient.channelName)
public void print(Message message) {
log.info("获取到的消息:{}", message.getPayload().toString());
}
}
8.2.5: 消息分组,就算多个消费者,只会一个收到消息
spring:
stream:
bindings:
myStreamRabbitMq:
group: employee # 多实例出现重复消费消息,将消息分组
content-type: application/json #这是设置rabbitmq客户端上查看消息里面看到的消息格式为json格式
8.2.6:收到消息后回推一个通知
按照开始的思路,肯定也得在某个接口中声明一个输出渠道,然后让原来的发送端收到我们回推的通知,然后使用@sendTo注解,消费端使用@StreamListerner监听新建渠道的消息
8.3:将department改造,使用rabbitmq
8.3.1 使用rabbitmq发送一个消息到队列
@Override
public Integer update(Department department) {
int count = departmentMapper.updateByPrimaryKey(department);
//更新完了,发送消息通知
amqpTemplate.convertAndSend("deptRoutingKey", JSON.toJSONString(department));
return count;
}
8.3.2:employee项目接收rabbitmq消息
@RabbitListener(queuesToDeclare = @Queue("deptRoutingKey"))
public void receiveDeptMqMessage(String message){
DepartmentInfoVO departmentInfoVO = JSON.parseObject(message, DepartmentInfoVO.class);
log.info("employee接收方接到消息:{}",JSON.toJSONString(departmentInfoVO));
}
使用的是controller暴露出来方法,然后使用postman调用方法,在employee中得到消息
接到消息,我们将消息放到redis里面
1)导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2),直接使用redis的方法
@Autowired
private RedisTemplate redisTemplate;
@RabbitListener(queuesToDeclare = @Queue("deptRoutingKey"))
public void receiveDeptMqMessage(String message){
DepartmentInfoVO departmentInfoVO = JSON.parseObject(message, DepartmentInfoVO.class);
log.info("employee接收方接到消息:{}",JSON.toJSONString(departmentInfoVO));
redisTemplate.opsForValue().set(message,message);
}
3)配置redis
spring:
redis:
host: localhost
port: 6379
password: '123456'
启动后像上面那样postman请求一下,看到缓存数据