在大型系统的微服务化构建中,一个系统被拆分成了许多模块。这些模块负责不同的功能,组合成系统,最终可以提供丰富的功能。在这种架构中,一次请求往往需要涉及到多个服务。互联网应用构建在不同的软件模块集上,这些软件模块,有可能是由不同的团队开发、可能使用不同的编程语言来实现、有可能布在了几千台服务器,横跨多个不同的数据中心,也就意味着这种架构形式也会存在一些问题:
分布式链路追踪(Distributed Tracing),就是将一次分布式请求还原成调用链路,进行日志记录,性能监控并将一次分布式请求的调用情况集中展示。比如各个服务节点上的耗时、请求具体到达哪台机器上、每个服务节点的请求状态等等。
常见的链路追踪技术有下面这些:
由大众点评开源,基于Java开发的实时应用监控平台,包括实时应用监控,业务监控 。 集成方案是通过代码埋点的方式来实现监控,比如: 拦截器,过滤器等。 对代码的侵入性很大,集成成本较高。风险较大。
由Twitter公司开源,开放源代码分布式的跟踪系统,用于收集服务的定时数据,以解决微服务架构中的延迟问题,包括:数据的收集、存储、查找和展现。该产品结合spring-cloud-sleuth使用较为简单, 集成很方便, 但是功能较简单。
Pinpoint是韩国人开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多种插件, UI功能强大,接入端无代码侵入。
SkyWalking是本土开源的基于字节码注入的调用链分析,以及应用监控分析工具。特点是支持多种插件, UI功能较强,接入端无代码侵入。目前已加入Apache孵化器。
SpringCloud 提供的分布式系统中链路追踪解决方案。
注意: SpringCloud alibaba技术栈中并没有提供自己的链路追踪技术的,我们可以采用Sleuth +Zinkin来做链路追踪解决方案
SpringCloud Sleuth主要功能就是在分布式系统中提供追踪解决方案。它大量借用了Google Dapper的设计, 先来了解一下Sleuth中的术语和相关概念。
由一组Trace Id相同的Span串联形成一个树状结构。为了实现请求跟踪,当请求到达分布式系统的入口端点时,只需要服务跟踪框架为该请求创建一个唯一的标识(即TraceId),同时在分布式系统内部流转的时候,框架始终保持传递该唯一值,直到整个请求的返回。那么我们就可以使用该唯一标识将所有的请求串联起来,形成一条完整的请求链路。
代表了一组基本的工作单元。为了统计各处理单元的延迟,当请求到达各个服务组件的时候,也通过一个唯一标识(SpanId)来标记它的开始、具体过程和结束。通过SpanId的开始和结束时间戳,就能统计该span的调用时间,除此之外,我们还可以获取如事件的名称。请求信息等元数据
用它记录一段时间内的事件,内部使用的重要注释:
微服务名称, traceId, spanid,是否将链路的追踪结果输出到第三方平台
[api-gateway,3977125f73391553,3977125f73391553,false]
[service-order,3977125f73391553,57547b5bf71f8242,false]
[service-product,3977125f73391553,449f5b3f3ef8d5c5,false]
接下来通过之前的项目案例整合Sleuth,完成入门案例的编写
修改父工程引入Sleuth依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-sleuthartifactId>
dependency>
启动微服务,调用之后,我们可以在控制台观察到sleuth的日志输出
其中 5399d5cb061971bd 是TraceId, 5399d5cb061971bd 是SpanId,依次调用有一个全局的 TraceId,将调用链路串起来。仔细分析每个微服务的日志,不难看出请求的具体过程。
查看日志文件并不是一个很好的方法,当微服务越来越多日志文件也会越来越多,通过Zipkin可以将日志聚合,并进行可视化展示和全文检索。
Zipkin 是 Twitter 的一个开源项目,它基于Google Dapper实现,它致力于收集服务的定时数据,以解决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。
我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的REST API接口来辅助我们查询跟踪数据以实现对分布式系统的监控程序,从而及时地发现系统中出现的延迟升高问题并找出系统性能瓶颈的根源。
除了面向开发的 API 接口之外,它也提供了方便的UI组件来帮助我们直观的搜索跟踪信息和分析请求链路明细,比如:可以查询某段时间内各用户请求的处理时间等。
Zipkin 提供了可插拔数据存储方式: In-Memory、 MySql、 Cassandra 以及 Elasticsearch。
上图展示了 Zipkin 的基础架构,它主要由 4 个核心组件构成:
Zipkin分为两端,一个是 Zipkin服务端,一个是 Zipkin客户端,客户端也就是微服务的应用。 客户端会配置服务端的 URL 地址,一旦发生服务间的调用的时候,会被配置在微服务里面的 Sleuth 的监听器监听,并生成相应的 Trace 和 Span 信息发送给服务端。
第1步: 下载ZipKin的jar包
https://search.maven.org/remote_content?g=io.zipkin.java&a=zipkinserver&v=LATEST&c=exec
访问上面的网址,即可得到一个jar包,这就是ZipKin服务端的jar包
第2步: 通过命令行,输入下面的命令启动ZipKin Server
java -jar zipkin-server-2.12.9-exec.jar
第3步:通过浏览器访问 http://localhost:9411访问
ZipKin客户端和Sleuth的集成非常简单,只需要在微服务中添加其依赖和配置即可。
第1步:在每个微服务上添加依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
第2步:添加配置
spring:
zipkin:
base-url: http://127.0.0.1:9411/ #zipkin server的请求地址
discoveryClientEnabled: false #让nacos把它当成一个URL, 而不要当做服务名
sleuth:
sampler:
probability: 1.0 #采样的百分比
第3步: 访问微服务,地址:http://localhost:7000/order-serv/order/prod/1
第4步: 访问zipkin的UI界面,观察效果
第5步:点击其中一条记录,可观察一次访问的详细线路。
Zipkin Server默认会将追踪数据信息保存到内存,但这种方式不适合生产环境。 Zipkin支持将追踪数据持久化到mysql数据库或elasticsearch中。
第1步: 创建mysql数据环境
CREATE TABLE IF NOT EXISTS zipkin_spans (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL,
`id` BIGINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`parent_id` BIGINT,
`debug` BIT(1),
`start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',
`duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_spans ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `id`)
COMMENT 'ignore insert on duplicate';
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`, `id`)
COMMENT 'for joining with zipkin_annotations';
ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';
ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';
CREATE TABLE IF NOT EXISTS zipkin_annotations (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',
`span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',
`a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',
`a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',
`a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',
`a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',
`endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',
`endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';
ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job';
CREATE TABLE IF NOT EXISTS zipkin_dependencies (
`day` DATE NOT NULL,
`parent` VARCHAR(255) NOT NULL,
`child` VARCHAR(255) NOT NULL,
`call_count` BIGINT
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;
ALTER TABLE zipkin_dependencies ADD UNIQUE KEY(`day`, `parent`, `child`);
第2步: 在启动ZipKin Server的时候,指定数据保存的mysql的信息
java -jar zipkin-server-2.12.9-exec.jar --STORAGE_TYPE=mysql -- MYSQL_HOST=127.0.0.1 --MYSQL_TCP_PORT=3306 --MYSQL_DB=zipkin --MYSQL_USER=root --MYSQL_PASS=root
第1步: 下载elasticsearch
下载地址: https://www.elastic.co/cn/downloads/past-releases/elasticsearch-6-8-4
第2步: 启动elasticsearch
MQ(Message Queue)是一种跨进程的通信机制,用于传递消息。通俗点说,就是一个先进先出的数据结构。
最常见的一个场景是用户注册后,需要发送注册邮件和短信通知,以告知用户注册成功。传统的做法如下:
此架构下注册、邮件、短信三个任务全部完成后,才返回注册结果到客户端,用户才能使用账号登录。但是对于用户来说,注册功能实际只需要注册系统存储用户的账户信息后,该用户便可以登录,而后续的注册短信和邮件不是即时需要关注的步骤。
所以实际当数据写入注册系统后,注册系统就可以把其他的操作放入对应的消息队列 MQ 中然后马上返回用户结果,由消息队列 MQ 异步地进行这些操作。架构图如下:
异步解耦是消息队列 MQ 的主要特点,主要目的是减少请求响应时间和解耦。主要的使用场景就是将比较耗时而且不需要即时(同步)返回结果的操作作为消息放入消息队列。同时,由于使用了消息队列MQ,只要保证消息格式不变,消息的发送方和接收方并不需要彼此联系,也不需要受对方的影响,即解耦合。
流量削峰也是消息队列 MQ 的常用场景,一般在秒杀或团队抢购(高并发)活动中使用广泛。
在秒杀或团队抢购活动中,由于用户请求量较大,导致流量暴增,秒杀的应用在处理如此大量的访问流量后,下游的通知系统无法承载海量的调用量,甚至会导致系统崩溃等问题而发生漏通知的情况。为解决这些问题,可在应用和下游通知系统之间加入消息队列 MQ。
秒杀处理流程如下所述:
目前业界有很多MQ产品,比较出名的有下面这些:
号称最快的消息队列系统,尤其针对大吞吐量的需求场景。扩展性好,开发比较灵活,采用C语言实现,实际上只是一个socket库的重新封装,如果做为消息队列使用,需要开发大量的代码。ZeroMQ仅提供非持久性的队列,也就是说如果down机,数据将会丢失。
使用erlang语言开发,性能较好,适合于企业级的开发。但是不利于做二次开发和维护。
历史悠久的Apache开源项目。已经在很多产品中得到应用,实现了JMS1.1规范,可以和spring-jms轻松融合,实现了多种协议,支持持久化到数据库,对队列数较多的情况支持不好。
阿里巴巴的MQ中间件,由java语言开发,性能非常好,能够撑住双十一的大流量,而且使用起来很简单。
Kafka是Apache下的一个子项目,是一个高性能跨语言分布式Publish/Subscribe消息队列系统,相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。
RocketMQ是阿里巴巴开源的分布式消息中间件,现在是Apache的一个顶级项目。在阿里内部使用非常广泛,已经经过了"双11"这种万亿级的消息流转。
接下来我们先在linux平台下安装一个RocketMQ的服务
http://rocketmq.apache.org/release_notes/release-notes-4.4.0/
1、上传文件到Linux系统
[root@heima rocketmq]# ls /usr/local/src/
rocketmq-all-4.4.0-bin-release.zip
2、解压到安装目录
[root@heima src]# unzip rocketmq-all-4.4.0-bin-release.zip
[root@heima src]# mv rocketmq-all-4.4.0-bin-release ../rocketmq
1、切换到安装目录
[root@heima rocketmq]# ls
benchmark bin conf lib LICENSE NOTICE README.md
2、启动NameServer
[root@heima rocketmq]# nohup ./bin/mqnamesrv &
[1] 1467
# 只要进程不报错,就应该是启动成功了,可以查看一下日志
[root@heima rocketmq]# tail -f /root/logs/rocketmqlogs/namesrv.log
3、启动Broker
# 编辑bin/runbroker.sh 和 bin/runserver.sh文件,修改里面的
# JAVA_OPT="${JAVA_OPT} -server -Xms8g -Xmx8g -Xmn4g"
# 为JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn128m"
[root@heima rocketmq]# nohup bin/mqbroker -n localhost:9876 &
[root@heima rocketmq]# tail -f /root/logs/rocketmqlogs/broker.log
1、测试消息发送
[root@heima rocketmq]# export NAMESRV_ADDR=localhost:9876
[root@heima rocketmq]# bin/tools.sh
org.apache.rocketmq.example.quickstart.Producer
2、测试消息接收
[root@heima rocketmq]# export NAMESRV_ADDR=localhost:9876
[root@heima rocketmq]# bin/tools.sh
org.apache.rocketmq.example.quickstart.Consumer
[root@heima rocketmq]# bin/mqshutdown broker
[root@heima rocketmq]# bin/mqshutdown namesrv
如上图所示,整体可以分成4个角色,分别是: NameServer, Broker, Producer, Consumer。
1、下载
# 在git上下载下面的工程 rocketmq-console-1.0.0
https://github.com/apache/rocketmq-externals/releases
2、修改配置文件
# 修改配置文件 rocketmq-console\src\main\resources\application.properties
server.port=7777 #项目启动后的端口号
rocketmq.config.namesrvAddr=192.168.109.131:9876 #nameserv的地址, 注意防火墙要开启9876端口
3、打成jar包,并启动
# 进入控制台项目, 将工程打成jar包
mvn clean package -Dmaven.test.skip=true
# 启动控制台
java -jar target/rocketmq-console-ng-1.0.0.jar
接下来我们使用Java代码来演示消息的发送和接收
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-spring-boot-starterartifactId>
<version>2.0.2version>
dependency>
消息发送步骤:
package com.itheima.test;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
//发送消息
public class RocketMQSendTest {
public static void main(String[] args) throws Exception {
//1. 创建消息生产者, 指定生产者所属的组名
DefaultMQProducer producer = new DefaultMQProducer("myproducer-group");
//2. 指定Nameserver地址
producer.setNamesrvAddr("192.168.109.131:9876");
//3. 启动生产者
producer.start();
//4. 创建消息对象, 指定主题、 标签和消息体
Message msg = new Message("myTopic", "myTag",
("RocketMQ Message").getBytes());
//5. 发送消息
SendResult sendResult = producer.send(msg, 10000);
System.out.println(sendResult);
//6. 关闭生产者
producer.shutdown();
}
}
消息接收步骤:
package com.itheima.test;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
//接收消息
public class RocketMQReceiveTest {
public static void main(String[] args) throws Exception {
//1. 创建消息消费者, 指定消费者所属的组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("myconsumergroup");
//2. 指定Nameserver地址
consumer.setNamesrvAddr("192.168.109.131:9876");
//3. 指定消费者订阅的主题和标签
consumer.subscribe("myTopic", "*");
//4. 设置回调函数, 编写处理消息的方法
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
System.out.println("Receive New Messages: " + msgs);
//返回消费状态
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5. 启动消息消费者
consumer.start();
System.out.println("Consumer Started.");
}
}
接下来我们模拟一种场景: 下单成功之后,向下单用户发送短信。设计图如下:
1、在 shop-order 中添加rocketmq的依赖
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-spring-boot-starterartifactId>
<version>2.0.2version>
dependency>
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-clientartifactId>
<version>4.4.0version>
dependency>
2、添加配置
rocketmq:
name-server: 192.168.109.131:9876 #rocketMQ服务的地址
producer:
group: shop-order # 生产者组
3、编写测试代码
package com.itheima.controller;
import com.alibaba.fastjson.JSON;
import com.itheima.pojo.Order;
import com.itheima.pojo.Product;
import com.itheima.service.OrderService;
import com.itheima.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class OrderController2 {
@Autowired
private OrderService orderService;
@Autowired
private ProductService productService;
@Autowired
private RocketMQTemplate rocketMQTemplate;
//准备买1件商品
@GetMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info(">>客户下单,这时候要调用商品微服务查询商品信息");
//通过fegin调用商品微服务
Product product = productService.findByPid(pid);
if (product == null) {
Order order = new Order();
order.setPname("下单失败");
return order;
}
log.info(">>商品信息,查询结果:" + JSON.toJSONString(product));
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(product.getPid());
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.save(order);
//下单成功之后,将消息放到mq中
rocketMQTemplate.convertAndSend("order-topic", order);
return order;
}
}
1、修改 shop-user
模块配置
<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>springcloud-alibabaartifactId>
<groupId>com.itheimagroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>shop-userartifactId>
<dependencies>
<dependency>
<groupId>com.itheimagroupId>
<artifactId>shop-commonartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-spring-boot-starterartifactId>
<version>2.0.2version>
dependency>
<dependency>
<groupId>org.apache.rocketmqgroupId>
<artifactId>rocketmq-clientartifactId>
<version>4.4.0version>
dependency>
dependencies>
project>
2、修改主类
package com.itheima;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
3、修改配置文件
server:
port: 8071
spring:
application:
name: service-user
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: root
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
rocketmq:
name-server: 192.168.109.131:9876
4、编写消息接收服务
package com.itheima.service;
import com.alibaba.fastjson.JSON;
import com.itheima.pojo.Order;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.springframework.stereotype.Service;
//发送短信的服务
@Slf4j
@Service
@RocketMQMessageListener(consumerGroup = "shop-user", topic = "order-topic")
public class SmsService {
public void onMessage(Order order) {
log.info("收到一个订单信息{},接下来发送短信", JSON.toJSONString(order));
}
}
5 、启动服务,执行下单操作,观看后台输出
RocketMQ提供三种方式来发送普通消息:可靠同步发送、可靠异步发送和单向发送。
同步发送是指消息发送方发出数据后,会在收到接收方发回响应之后才发下一个数据包的通讯方式。
此种方式应用场景非常广泛,例如重要通知邮件、报名短信通知、营销短信系统等
异步发送是指发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。发送方通过回调接口接收服务器响应,并对响应结果进行处理。
异步发送一般用于链路耗时较长,对 RT 响应时间较为敏感的业务场景,例如用户视频上传后通知启动转码服务,转码完成后通知推送转码结果等。
单向发送是指发送方只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答。
适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
dependency>
package com.itheima.test;
import com.itheima.OrderApplication;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
//测试
@RunWith(SpringRunner.class)
@SpringBootTest(classes = OrderApplication.class)
public class MessageTypeTest {
@Autowired
private RocketMQTemplate rocketMQTemplate;
//同步消息
@Test
public void testSyncSend() {
//参数一: topic, 如果想添加tag 可以使用"topic:tag"的写法
//参数二: 消息内容
SendResult sendResult = rocketMQTemplate.syncSend("test-topic-1", "这是一条同步消息");
System.out.println(sendResult);
}
public void testSyncSendMsg() throws Exception {
//参数一: topic, 如果想添加tag 可以使用"topic:tag"的写法
//参数二: 消息内容
//参数三: 回调函数, 处理返回结果
rocketMQTemplate.asyncSend("test-topic-1", "这是一条异步消息", new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
@Override
public void onException(Throwable throwable) {
System.out.println(throwable);
}
});
//让线程不要终止
Thread.sleep(30000000);
}
//单向消息
@Test
public void testOneWay() {
rocketMQTemplate.sendOneWay("test-topic-1", "这是一条单向消息");
}
}
发送方式 | 发送 TPS | 发送结果反馈 | 可靠性 |
---|---|---|---|
同步发送 | 快 | 有 | 不丢失 |
异步发送 | 快 | 有 | 不丢失 |
单向发送 | 最快 | 无 | 可能丢失 |
顺序消息是消息队列提供的一种严格按照顺序来发布和消费的消息类型。
//同步顺序消息[异步顺序 单向顺序写法类似]
public void testSyncSendOrderly() {
//第三个参数用于队列的选择
rocketMQTemplate.syncSendOrderly("test-topic-1", "这是一条异步顺序消息", "xxxx");
}
RocketMQ提供了事务消息,通过事务消息就能达到分布式事务的最终一致。
暂不能投递的消息,发送方已经成功地将消息发送到了RocketMQ服务端,但是服务端未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半事务消息。
由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,RocketMQ服务端通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit 或是 Rollback),该询问过程即消息回查。
1、发送方将半事务消息发送至RocketMQ服务端
2、 RocketMQ服务端将消息持久化之后,向发送方返回Ack确认消息已经发送成功,此时消息为半事务消息。
3、发送方开始执行本地事务逻辑。
4、发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback),服务端收到Commit 状态则将半事务消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback 状态则删除半事务消息,订阅方将不会接受该消息。
1、在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查。
2、发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。
3、发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半事务消息进行操作。
package com.itheima.config;
import lombok.Data;
import javax.persistence.Entity;
import javax.persistence.Id;
import java.util.Date;
//事物日志
@Entity(name = "shop_txlog")
@Data
public class TxLog {
@Id
private String txLogId;
private String content;
private Date date;
}
package com.itheima.service.impl;
import com.itheima.config.TxLog;
import com.itheima.dao.OrderDao;
import com.itheima.dao.TxLogDao;
import com.itheima.pojo.Order;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
import java.util.UUID;
@Service
public class OrderServiceImpl4 {
private OrderDao orderDao;
@Autowired
private TxLogDao txLogDao;
@Autowired
private RocketMQTemplate rocketMQTemplate;
public void createOrderBefore(Order order) {
String txId = UUID.randomUUID().toString();
//发送半事务消息
rocketMQTemplate.sendMessageInTransaction(
"tx_producer_group",
"tx_topic",
MessageBuilder.withPayload(order).setHeader("txId", txId).build(), order);
}
//本地事物
@Transactional
public void createOrder(String txId, Order order) {
//本地事物代码
orderDao.save(order);
//记录日志到数据库,回查使用
TxLog txLog = new TxLog();
txLog.setTxLogId(txId);
txLog.setContent("事物测试");
txLog.setDate(new Date());
txLogDao.save(txLog);
}
}
package com.itheima.service.impl;
import com.itheima.config.TxLog;
import com.itheima.dao.TxLogDao;
import com.itheima.pojo.Order;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
@RocketMQTransactionListener(txProducerGroup = "tx_producer_group")
public class OrderServiceImpl4Listener implements RocketMQLocalTransactionListener {
@Autowired
private TxLogDao txLogDao;
@Autowired
private OrderServiceImpl4 orderServiceImpl4;
//执行本地事物
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
//本地事物
orderServiceImpl4.createOrder((String) msg.getHeaders().get("txId"),
(Order) arg);
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
//消息回查
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
//查询日志记录
TxLog txLog = txLogDao.findById((String) msg.getHeaders().get("txId")).get();
if (txLog == null) {
return RocketMQLocalTransactionState.COMMIT;
} else {
return RocketMQLocalTransactionState.ROLLBACK;
}
}
}
package com.itheima.service;
import com.alibaba.fastjson.JSON;
import com.itheima.pojo.Order;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
@Slf4j
@RocketMQMessageListener(
consumerGroup = "shop",//消费者分组
topic = "order-topic",//要消费的主题
consumeMode = ConsumeMode.CONCURRENTLY, //消费模式:无序和有序
messageModel = MessageModel.CLUSTERING //消息模式:广播和集群,默认是集群
)
public class SmsService1 implements RocketMQListener<Order> {
public void onMessage(Order order) {
log.info("收到一个订单信息{},接下来发送短信", JSON.toJSONString(order));
}
}
RocketMQ支持两种消息模式:
短信服务(Short Message Service)是阿里云为用户提供的一种通信服务的能力。
接下来,我们使用短信验证码功能来演示短信服务的使用。流程如下:
https://help.aliyun.com/document_detail/48263.html?spm=a2c4g.11186623.2.25.1f9415ec9MLqKD
调用SendSms发送短信。
名称 | 类型 | 是否必选 | 示例值 | 描述 |
---|---|---|---|---|
PhoneNumbers | String | 是 | 15900000000 | 接收短信的手机号码。 |
SignName | String | 是 | 阿里云 | 短信签名名称。 |
TemplateCode | String | 是 | SMS_153055065 | 短信模板ID。 |
TemplateParam | String | 否 | {“code”:“1111”} | 短信模板变量的值, JSON格式 |
名称 | 类型 | 示例值 | 描述 |
---|---|---|---|
BizId | String | 900619746936498440^0 | 发送回执ID,可根据它查询具体的发送状态 |
Code | String | OK | 请求状态码。返回OK代表请求成功。 |
Message | String | OK | 状态码的描述。 |
RequestId | String | F655A8D5-B967-440B-8683 | 请求ID。 |
调用QuerySendDetails接口查看短信发送记录和发送状态。
名称 | 类型 | 是否必选 | 示例值 | 描述 |
---|---|---|---|---|
CurrentPage | Long | 是 | 1 | 分页查看,指定发送记录的的当前页码 |
PageSize | Long | 是 | 10 | 分页查看,指定每页显示的短信记录数量 |
PhoneNumber | String | 是 | 15900000000 | 接收短信的手机号码。 |
SendDate | String | 是 | 20181228 | 短信发送日期,支持查询最近30天的记录 |
BizId | String | 否 | 134523^4351232 | 发送回执ID,即发送流水号。 |
名称 | 类型 | 示例值 | 描述 |
---|---|---|---|
Code | String | OK | 求状态码。返回OK代表请求成功。 |
Message | String | OK | 状态码的描述。 |
RequestId | String | 819BE656-D2E0 | 请求ID。 |
SmsSendDetailDTOs | Array | 短信发送明细 | |
TotalCount | String | 1 | 短信发送总条数。 |
第1步: 引入阿里云服务依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alicloud-smsartifactId>
dependency>
第2步: 使用阿里云提供的Demo测试短信发送
package com.itheima.test;
import com.aliyun.mns.common.ClientException;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.QuerySendDetailsResponse;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import java.text.SimpleDateFormat;
import java.util.Date;
public class SmsDemo {
//产品名称:云通信短信API产品,开发者无需替换
static final String product = "Dysmsapi";
//产品域名,开发者无需替换
static final String domain = "dysmsapi.aliyuncs.com";
// TODO 此处需要替换成开发者自己的AK(在阿里云访问控制台寻找)
static final String accessKeyId = "yourAccessKeyId";
static final String accessKeySecret = "yourAccessKeySecret";
//短信发送
public static SendSmsResponse sendSms() throws Exception {
//可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
//组装请求对象-具体描述见控制台-文档部分内容
SendSmsRequest request = new SendSmsRequest();
//必填:待发送手机号
request.setPhoneNumbers("15000000000");
//必填:短信签名-可在短信控制台中找到
request.setSignName("云通信");
//必填:短信模板-可在短信控制台中找到
request.setTemplateCode("SMS_1000000");
//可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}"时,此处的值为
request.setTemplateParam("{\"name\":\"Tom\", \"code\":\"123\"}");
//选填-上行短信扩展码(无特殊需求用户请忽略此字段)
//request.setSmsUpExtendCode("90997");
//可选:outId为提供给业务方扩展字段,最终在短信回执消息中将此值带回给调用者
request.setOutId("yourOutId");
//hint 此处可能会抛出异常,注意catch
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
return sendSmsResponse;
}
//短信查询
public static QuerySendDetailsResponse querySendDetails(String bizId) throws Exception {
//可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
//组装请求对象
QuerySendDetailsRequest request = new QuerySendDetailsRequest();
//必填-号码
request.setPhoneNumber("15000000000");
//可选-流水号
request.setBizId(bizId);
//必填-发送日期 支持30天内记录查询,格式yyyyMMdd
SimpleDateFormat ft = new SimpleDateFormat("yyyyMMdd");
request.setSendDate(ft.format(new Date()));
//必填-页大小
request.setPageSize(10L);
//必填-当前页码从1开始计数
request.setCurrentPage(1L);
//hint 此处可能会抛出异常,注意catch
QuerySendDetailsResponse querySendDetailsResponse = acsClient.getAcsResponse(request);
return querySendDetailsResponse;
}
public static void main(String[] args) throws Exception {
//发短信
SendSmsResponse response = sendSms();
System.out.println("短信接口返回的数据----------------");
System.out.println("Code=" + response.getCode());
System.out.println("Message=" + response.getMessage());
System.out.println("RequestId=" + response.getRequestId());
System.out.println("BizId=" + response.getBizId());
Thread.sleep(3000L);//查明细
if (response.getCode() != null && response.getCode().equals("OK")) {
QuerySendDetailsResponse querySendDetailsResponse = querySendDetails(response.getBizId());
System.out.println("短信明细查询接口返回数据----------------");
System.out.println("Code=" + querySendDetailsResponse.getCode());
System.out.println("Message=" + querySendDetailsResponse.getMessage());
int i = 0;
for (QuerySendDetailsResponse.SmsSendDetailDTO smsSendDetailDTO : querySendDetailsResponse.getSmsSendDetailDTOs()) {
System.out.println("SmsSendDetailDTO[" + i + "]:");
System.out.println("Content=" + smsSendDetailDTO.getContent());
System.out.println("ErrCode=" + smsSendDetailDTO.getErrCode());
System.out.println("OutId=" + smsSendDetailDTO.getOutId());
System.out.println("PhoneNum=" + smsSendDetailDTO.getPhoneNum());
System.out.println("ReceiveDate=" + smsSendDetailDTO.getReceiveDate());
System.out.println("SendDate=" + smsSendDetailDTO.getSendDate());
System.out.println("SendStatus=" + smsSendDetailDTO.getSendStatus());
System.out.println("Template=" + smsSendDetailDTO.getTemplateCode());
}
System.out.println("TotalCount=" + querySendDetailsResponse.getTotalCount());
System.out.println("RequestId=" + querySendDetailsResponse.getRequestId());
}
}
}
1、 在 shop-user 模块中加入sms依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alicloud-smsartifactId>
dependency>
2 、将阿里短信给出的demo封装成工具类
package com.itheima.util;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
public class SmsUtil {
//替换成自己申请的accessKeyId
private static String accessKeyId = "LTAIMLlf8NKYXn1M";
//替换成自己申请的accessKeySecret
private static String accessKeySecret = "hqyW0zTNzeSIFnZhMEkOaZXVVcr3Gj";
static final String product = "Dysmsapi";
static final String domain = "dysmsapi.aliyuncs.com";
/**
* 发送短信
* @param phoneNumbers 要发送短信到哪个手机号
* @param signName 短信签名[必须使用前面申请的]
* @param templateCode 短信短信模板ID[必须使用前面申请的]
* @param param 模板中${code}位置传递的内容
*/
public static void sendSms(String phoneNumbers, String signName, String templateCode, String param) {
try {
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
//初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setPhoneNumbers(phoneNumbers);
request.setSignName(signName);
request.setTemplateCode(templateCode);
request.setTemplateParam(param);
request.setOutId("yourOutId");
SendSmsResponse sendSmsResponse = acsClient.getAcsResponse(request);
if (!"OK".equals(sendSmsResponse.getCode())) {
throw new RuntimeException(sendSmsResponse.getMessage());
}
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("发送短信失败");
}
}
}
3、修改短信发送的服务
package com.itheima.service;
import com.alibaba.fastjson.JSON;
import com.itheima.dao.UserDao;
import com.itheima.pojo.Order;
import com.itheima.pojo.User;
import com.itheima.util.SmsUtil;
import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.MessageModel;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Random;
//发送短信的服务
@Slf4j
@Service("shopSmsService")
@RocketMQMessageListener(
consumerGroup = "shop-user", //消费者组名
topic = "order-topic",//消费主题
consumeMode = ConsumeMode.CONCURRENTLY,//消费模式
messageModel = MessageModel.CLUSTERING//消息模式
)
public class SmsService2 implements RocketMQListener<Order> {
@Autowired
private UserDao userDao;
//消费逻辑
@Override
public void onMessage(Order message) {
log.info("接收到了一个订单信息{},接下来就可以发送短信通知了", message);
//根据uid 获取手机号
User user = userDao.findById(message.getUid()).get();
//生成验证码
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 6; i++) {
builder.append(new Random().nextInt(9) + 1);
}
String smsCode = builder.toString();
Param param = new Param(smsCode);
try {
//发送短信 {"code":"123456"}
SmsUtil.sendSms(user.getTelephone(), "黑马旅游网", "SMS_170836451", JSON.toJSONString(param));
log.info("短信发送成功");
} catch (Exception e) {
e.printStackTrace();
}
}
@AllArgsConstructor
@NoArgsConstructor
class Param {
private String code;
}
}
首先我们来看一下,微服务架构下关于配置文件的一些问题:
1、 配置文件相对分散。在一个微服务架构下,配置文件会随着微服务的增多变的越来越多,而且分散在各个微服务中,不好统一配置和管理。
2、配置文件无法区分环境。微服务项目可能会有多个环境,例如:测试环境、预发布环境、生产环境。每一个环境所使用的配置理论上都是不同的,一旦需要修改,就需要我们去各个微服务下手动维护,这比较困难。
3、配置文件无法实时更新。我们修改了配置文件之后,必须重新启动微服务才能使配置生效,这对一个正在运行的项目来说是非常不友好的。
基于上面这些问题,我们就需要配置中心的加入来解决这些问题。
配置中心的思路是:
当加入了服务配置中心之后,我们的系统架构图会变成下面这样:
在业界常见的服务配置中心,有下面这些:
Apollo是由携程开源的分布式配置中心。特点有很多,比如:配置更新之后可以实时生效,支持灰度发布功能,并且能对所有的配置进行版本管理、操作审计等功能,提供开放平台API。并且资料也写的很详细。
Disconf是由百度开源的分布式配置中心。它是基于Zookeeper来实现配置变更后实时通知和生效的。
这是Spring Cloud中带的配置中心组件。它和Spring是无缝集成,使用起来非常方便,并且它的配置存储支持Git。不过它没有可视化的操作界面,配置的生效也不是实时的,需要重启或去刷新。
这是SpingCloud alibaba技术栈中的一个组件,前面我们已经使用它做过服务注册中心。其实它也集成了服务配置的功能,我们可以直接使用它作为服务配置中心。
使用nacos作为配置中心,其实就是将nacos当做一个服务端,将各个微服务看成是客户端,我们将各个微服务的配置文件统一存放在nacos上,然后各个微服务从nacos上拉取配置即可。
接下来我们以商品微服务为例,学习nacos config的使用。
1、搭建nacos环境【使用现有的nacos环境即可】
2、在微服务中引入nacos的依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
3、在微服务中添加nacos config的配置
注意:不能使用原来的application.yml作为配置文件,而是新建一个bootstrap.yml作为配置文件
配置文件优先级(由高到低):
bootstrap.properties -> bootstrap.yml -> application.properties -> application.yml
spring:
application:
name: service-product
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 #nacos中心地址
file-extension: yaml # 配置文件格式
profiles:
active: dev # 环境标识
4、在nacos中添加配置
点击配置列表,点击右边+号,新建配置。在新建配置过程中,要注意下面的细节:
1) Data ID不能随便写,要跟配置文件中的对应,对应关系如图所示
2)配置文件格式要跟配置文件的格式对应,且目前仅仅支持YAML和Properties
3)配置内容按照上面选定的格式书写
5、注释本地的application.yam中的内容, 启动程序进行测试
如果依旧可以成功访问程序,说明我们nacos的配置中心功能已经实现
在入门案例中,我们实现了配置的远程存放,但是此时如果修改了配置,我们的程序是无法读取到的,因此,我们需要开启配置的动态刷新功能。
在nacos中的service-product-dev.yaml配置项中添加下面配置:
config:
appName: product
package com.itheima.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class NacosConfigController {
@Autowired
private ConfigurableApplicationContext applicationContext;
@GetMapping("/nacos-config-test1")
public String nacosConfingTest1() {
return applicationContext.getEnvironment().getProperty("config.appName");
}
}
package com.itheima.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RefreshScope//只需要在需要动态读取配置的类上添加此注解就可以
public class NacosConfigController2 {
@Value("${config.appName}")
private String appName;
//2 注解方式
@GetMapping("/nacos-config-test2")
public String nacosConfingTest2() {
return appName;
}
}
当配置越来越多的时候,我们就发现有很多配置是重复的,这时候就考虑可不可以将公共配置文件提取出来,然后实现共享呢?当然是可以的。接下来我们就来探讨如何实现这一功能。
如果想在同一个微服务的不同环境之间实现配置共享,其实很简单。
只需要提取一个以 spring.application.name
命名的配置文件,然后将其所有环境的公共配置放在里面即可。
1、新建一个名为service-product.yaml配置存放商品微服务的公共配置
2、新建一个名为service-product-test.yaml配置存放测试环境的配置
3、新建一个名为consumer-dev.yaml配置存放开发环境的配置
4、添加测试方法
package com.itheima.controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RefreshScope
public class NacosConfigController3 {
@Value("${config.env}")
private String env;
//3 同一微服务的不同环境下共享配置
@GetMapping("/nacos-config-test3")
public String nacosConfingTest3() {
return env;
}
}
5、访问测试
6、 接下来,修改bootstrap.yml中的配置,将active设置成test,再次访问,观察结果
spring:
profiles:
active: test # 环境标识
不同为服务之间实现配置共享的原理类似于文件引入,就是定义一个公共配置,然后在当前配置中引入。
1、 在nacos中定义一个DataID为all-service.yaml的配置,用于所有微服务共享
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql:///shop?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true
username: root
password: root
jpa:
properties:
hibernate:
hbm2ddl:
auto: update
dialect: org.hibernate.dialect.MySQL5InnoDBDialect
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
2、在nacos的中修改service-product.yaml中为下面内容
server:
port: 8081
config:
appName: product
3、修改bootstrap.yaml
spring:
application:
name: service-product
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 #nacos中心地址
file-extension: yaml # 配置文件格式
shared-dataids: all-service.yaml # 配置要引入的配置
refreshable-dataids: all-service.yaml # 配置要实现动态配置刷新的配置
profiles:
active: dev # 环境标识
4、启动商品微服务进行测试
命名空间可用于进行不同环境的配置隔离。一般一个环境划分到一个命名空间
配置分组用于将不同的服务可以归类到同一分组。一般将一个项目的配置分到一组
在系统中,一个配置文件通常就是一个配置集。一般微服务的配置就是一个配置集
事务指的就是一个操作单元,在这个操作单元中的所有操作最终要保持一致的行为,要么所有操作都成功,要么所有的操作都被撤销。简单地说,事务提供一种“要么什么都不做,要么做全套”机制。
本地事务其实可以认为是数据库提供的事务机制。说到数据库事务就不得不说,数据库事务中的四大特性:
数据库事务在实现时会将一次事务涉及的所有操作全部纳入到一个不可分割的执行单元,该执行单元中的所有操作要么都成功,要么都失败,只要其中任一操作执行失败,都将导致整个事务的回滚
分布式事务指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。
本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
全局事务基于DTP模型实现。 DTP是由X/Open组织提出的一种分布式事务模型——X/Open Distributed Transaction Processing Reference Model。它规定了要实现分布式事务,需要三种角色:
整个事务分成两个阶段:
提高了数据一致性的概率,实现成本较低
基于可靠消息服务的方案是通过消息中间件保证上、下游应用数据操作的一致性。假设有A和B两个系统,分别可以处理任务A和任务B。此时存在一个业务流程,需要将任务A和任务B在同一个事务中处理。就可以使用消息中间件来实现这种分布式事务。
系统A除了实现正常的业务流程外,还需提供一个事务询问的接口,供消息中间件调用。当消息中间件收到发布消息便开始计时,如果到了超时没收到确认指令,就会主动调用系统A提供的事务询问接口询问该系统目前的状态。该接口会返回三种结果,中间件根据三种结果做出不同反应:
消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统便立即进行任务的处理,任务处理完成后便向消息中间件返回应答。
一般消息中间件可以设置消息重试的次数和时间间隔,如果最终还是不能成功投递,则需要手工干预。这里之所以使用人工干预,而不是使用让A系统回滚,主要是考虑到整个系统设计的复杂度问题。
基于可靠消息服务的分布式事务,前半部分使用异步,注重性能;后半部分使用同步,注重开发成本。
最大努力通知也被称为定期校对,其实是对第二种解决方案的进一步优化。它引入了本地消息表来记录错误消息,然后加入失败消息的定期校对功能,来进一步保证消息会被下游系统消费。
这种方式的优缺点:
TCC即为Try Confirm Cancel,它属于补偿型分布式事务。 TCC实现分布式事务一共有三个步骤:
这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的全部资源
确认执行业务操作,不做任何业务检查, 只使用Try阶段预留的业务资源。通常情况下,采用TCC则认为 Confirm阶段是不会出错的。即:只要Try成功, Confirm一定成功。若Confirm阶段真的出错了,需引入重试机制或人工处理。
取消Try阶段预留的业务资源。通常情况下,采用TCC则认为Cancel阶段也是一定成功的。若Cancel阶段真的出错了,需引入重试机制或人工处理。
XA是资源层面的分布式事务,强一致性,在两阶段提交的整个过程中,一直会持有资源的锁。
TCC是业务层面的分布式事务,最终一致性,不会一直持有资源的锁。
TCC事务的优缺点:
2019 年 1 月,阿里巴巴中间件团队发起了开源项目 Fescar(Fast & EaSy Commit And Rollback),其愿景是让分布式事务的使用像本地事务的使用一样,简单和高效,并逐步解决开发者们遇到的分布式事务方面的所有难题。后来更名为 Seata,意为: Simple Extensible Autonomous Transaction Architecture,是一套分布式事务解决方案。
Seata的设计目标是对业务无侵入,因此从业务无侵入的2PC方案着手,在传统2PC的基础上演进。它把一个分布式事务理解成一个包含了若干分支事务的全局事务。全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。此外,通常分支事务本身就是一个关系数据库的本地事务。
1、架构层次方面,传统2PC方案的 RM 实际上是在数据库层, RM本质上就是数据库自身,通过XA协议实现,而 Seata的RM是以jar包的形式作为中间件层部署在应用程序这一侧的。
2、 两阶段提交方面,传统2PC无论第二阶段的决议是commit还是rollback,事务性资源的锁都要保持到Phase2完成才释放。而Seata的做法是在Phase1 就将本地事务提交,这样就可以省去Phase2持锁的时间,整体提高效率。
本示例通过Seata中间件实现分布式事务,模拟电商中的下单和扣库存的过程
我们通过订单微服务执行下单操作,然后由订单微服务调用商品微服务扣除库存
package com.itheima.controller;
import com.itheima.pojo.Order;
import com.itheima.service.impl.OrderServiceImpl5;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class OrderController5 {
@Autowired
private OrderServiceImpl5 orderService;
//下单
@RequestMapping("/order/prod/{pid}")
public Order order(@PathVariable("pid") Integer pid) {
log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", pid);
return orderService.createOrder(pid);
}
}
package com.itheima.service.impl;
import com.alibaba.fastjson.JSON;
import com.itheima.dao.OrderDao;
import com.itheima.pojo.Order;
import com.itheima.pojo.Product;
import com.itheima.service.OrderService5;
import com.itheima.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class OrderServiceImpl5 implements OrderService5 {
@Autowired
private OrderDao orderDao;
@Autowired
private ProductService productService;
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Transactional
@Override
public Order createOrder(Integer pid) {
//1 调用商品微服务,查询商品信息
Product product = productService.findByPid(pid);
log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product));
//2 下单(创建订单)
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(pid);
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderDao.save(order);
log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
//3 扣库存
productService.reduceInventory(pid, order.getNumber());
//4 向mq中投递一个下单成功的消息
rocketMQTemplate.convertAndSend("order-topic", order);
return order;
}
}
//value用于指定调用nacos下哪个微服务
@FeignClient(value = "service-product")//声明调用的提供者的name
public interface ProductService {
//减库存
@RequestMapping("/product/reduceInventory")
void reduceInventory(@RequestParam("pid") Integer pid, @RequestParam("num") int num);
}
//减少库存
@RequestMapping("/product/reduceInventory")
public void reduceInventory(Integer pid, int num) {
productService.reduceInventory(pid, num);
}
@Override
public void reduceInventory(Integer pid, int num) {
Product product = productDao.findById(pid).get();
product.setStock(product.getStock() - num);//减库存
productDao.save(product);
}
在ProductServiceImpl的代码中模拟一个异常, 然后调用下单接口
@Override
public void reduceInventory(Integer pid, int num) {
Product product = productDao.findById(pid).get();
if (product.getStock() < num) {
throw new RuntimeException("库存不足");
}
int i = 1 / 0;
product.setStock(product.getStock() - num);
productDao.save(product);
}
下载地址: https://github.com/seata/seata/releases/v0.9.0/
将下载得到的压缩包进行解压,进入conf目录,调整下面的配置文件:
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = "public"
cluster = "default"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = "public"
cluster = "default"
}
}
service.vgroup_mapping.service-product=default
service.vgroup_mapping.service-order=default
这里的语法为: service.vgroup_mapping.${your-service-gruop}=default
,中间的${your-service-gruop}
为自己定义的服务组名称, 这里需要我们在程序的配置文件中配置。
# 初始化seata 的nacos配置
# 注意: 这里要保证nacos是已经正常运行的
cd conf
nacos-config.sh 127.0.0.1
执行成功后可以打开Nacos的控制台,在配置列表中,可以看到初始化了很多Group为SEATA_GROUP的配置。
cd bin
seata-server.bat -p 9000 -m file
启动后在 Nacos 的服务列表下面可以看到一个名为 serverAddr 的服务。
在我们的数据库中加入一张undo_log表,这是Seata记录事务日志要用到的表
CREATE TABLE `undo_log`(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) NOT NULL,
`context` VARCHAR(128) NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT(11) NOT NULL,
`log_created` DATETIME NOT NULL,
`log_modified` DATETIME NOT NULL,
`ext` VARCHAR(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = INNODB AUTO_INCREMENT = 1 DEFAULT CHARSET = utf8;
在需要进行分布式控制的微服务中进行下面几项配置:
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-seataartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
Seata 是通过代理数据源实现事务分支的,所以需要配置 io.seata.rm.datasource.DataSourceProxy
的Bean,且是 @Primary默认的数据源,否则事务不会回滚,无法实现分布式事务
package com.itheima.config;
import com.alibaba.druid.pool.DruidDataSource;
import io.seata.rm.datasource.DataSourceProxy;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class DataSourceProxyConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DruidDataSource druidDataSource() {
return new DruidDataSource();
}
@Primary
@Bean
public DataSourceProxy dataSource(DruidDataSource druidDataSource) {
return new DataSourceProxy(druidDataSource);
}
}
在resources下添加Seata的配置文件 registry.conf
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = "public"
cluster = "default"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"
nacos {
serverAddr = "localhost"
namespace = "public"
cluster = "default"
}
}
spring:
application:
name: service-product
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848 #nacos中心地址
namespace: public
group: SEATA_GROUP
alibaba:
seata:
tx-service-group: ${
spring.application.name}
@GlobalTransactional//全局事务控制
public Order createOrder(Integer pid) {
}
再次下单测试
1、每个RM使用DataSourceProxy连接数据库,其目的是使用ConnectionProxy,使用数据源和数据连接代理的目的就是在第一阶段将undo_log和业务数据放在一个本地事务提交,这样就保存了只要有业务操作就一定有undo_log。
2、在第一阶段undo_log中存放了数据修改前和修改后的值,为事务回滚作好准备,所以第一阶段完成就已经将分支事务提交,也就释放了锁资源。
3、 TM开启全局事务开始,将XID全局事务id放在事务上下文中,通过feign调用也将XID传入下游分支事务,每个分支事务将自己的Branch ID分支事务ID与XID关联。
4、第二阶段全局事务提交, TC会通知各各分支参与者提交分支事务,在第一阶段就已经提交了分支事务,这里各各参与者只需要删除undo_log即可,并且可以异步执行,第二阶段很快可以完成。
5、第二阶段全局事务回滚, TC会通知各各分支参与者回滚分支事务,通过 XID 和 Branch ID 找到相应的回滚日志,通过回滚日志生成反向的 SQL 并执行,以完成分支事务回滚到之前的状态,如果回滚失败则会重试回滚操作。
Dubbo是阿里巴巴开源的基于 Java 的高性能 RPC分布式服务框架,致力于提供高性能和透明化的RPC远程服务调用方案,以及SOA服务治理方案。
Spring-cloud-alibaba-dubbo 是基于SpringCloudAlibaba技术栈对dubbo技术的一种封装,目的在于实现基于RPC的服务调用。
public interface ProductService {
Product findByPid(Integer pid);
}
1、 添加依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-dubboartifactId>
dependency>
2、 添加dubbo配置
dubbo:
scan:
base-packages: com.itheima.service.impl # 开启包扫描
protocol:
dubbo:
name: dubbo # 服务协议
port: -1 # 服务端口
registry:
address: spring-cloud://localhost # 注册中心
3、写并暴露服务
//暴露服务:注意这里使用的是dubbo提供的注解@Service,而不是Spring的
@Service
public class ProductServiceImpl implements ProductService {
@Autowired
private ProductDao productDao;
@Override
public Product findByPid(Integer pid) {
return productDao.findById(pid).get();
}
}
1 、添加依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-dubboartifactId>
dependency>
2、 添加dubbo配置
dubbo:
registry:
address: spring-cloud://localhost # 注册中心
cloud:
subscribed-services: service-product # 订阅的提供者名称
3 、引用服务
@RestController
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
//引用服务
@Reference
private ProductService productService;
@RequestMapping("/order/prod/{pid}")
public Order order(@PathVariable Integer pid) {
log.info("接收到{}号商品的下单请求,接下来调用商品微服务查询此商品信息", pid);
//调用商品微服务,查询商品信息
Product product = productService.findByPid(pid);
log.info("查询到{}号商品的信息,内容是:{}", pid, JSON.toJSONString(product));
//下单(创建订单)
Order order = new Order();
order.setUid(1);
order.setUsername("测试用户");
order.setPid(pid);
order.setPname(product.getPname());
order.setPprice(product.getPprice());
order.setNumber(1);
orderService.createOrder(order);
log.info("创建订单成功,订单信息为{}", JSON.toJSONString(order));
return order;
}
}