1
第十三章 kafkaStream新热文章计算
今日目标
-
能够理解什么是实时流式计算
-
能够理解kafkaStream处理实时流式计算的流程
-
能够完成kafkaStream实时流式计算的入门案例
-
能够完成app端热点文章计算的功能
-
能够完成app端文章列表接口的优化改造
1 实时流式计算
1.1 概念
任何类型的数据都是作为事件流产生的。信用卡交易,传感器测量,机器日志或网站或移动应用程序上的用户交互,所有这些数据都作为流生成。 数据可以作为无界或有界流处理。
一般流式计算会与批量计算相比较。在流式计算模型中,输入是持续的,可以认为在时间上是无界的,也就意味着,永远拿不到全量数据去做计算。同时,计算结果是持续输出的,也即计算结果在时间上也是无界的。流式计算一般对实时性要求较高,同时一般是先定义目标计算,然后数据到来之后将计算逻辑应用于数据。同时为了提高计算效率,往往尽可能采用增量计算代替全量计算。
有界流
-
有定义流的开始,也有定义流的结束。
-
有界流可以在摄取所有数据后再进行计算。
-
有界流所有数据可以被排序,所以并不需要有序摄取。
-
有界流处理通常被称为批处理。
无界流
-
有定义流的开始,但没有定义流的结束。
-
它们会无休止地产生数据。
-
无界流的数据必须持续处理,即数据被摄取后需要立刻处理。我们不能等到所有数据都到达再处理,因为输入是无限的,在任何时候输入都不会完成。
-
处理无界数据通常要求以特定顺序摄取事件,例如事件发生的顺序,以便能够推断结果的完整性。
1.2 应用场景
-
日志分析
网站的用户访问日志进行实时的分析,计算访问量,用户画像,留存率等等,实时的进行数据分析,帮助企业进行决策
-
大屏看板统计
可以实时的查看网站注册数量,订单数量,购买数量,金额等。
-
公交实时数据
可以随时更新公交车方位,计算多久到达站牌等
-
实时文章分值计算
头条类文章的分值计算,通过用户的行为实时文章的分值,分值越高就越被推荐。
1.3 技术方案选型
-
Hadoop
-
Apche Storm/Flink
Storm 是一个分布式实时大数据处理系统,可以帮助我们方便地处理海量数据,具有高可靠、高容错、高扩展的特点。是流式框架,有很高的数据吞吐能力。
-
Kafka Stream
可以轻松地将其嵌入任何Java应用程序中,并与用户为其流应用程序所拥有的任何现有打包,部署和操作工具集成。
2 Kafka Stream
2.1 概述
Kafka Stream是Apache Kafka从0.10版本引入的一个新Feature。它是提供了对存储于Kafka内的数据进行流式处理和分析的功能。
Kafka Stream的特点如下:
-
Kafka Stream提供了一个非常简单而轻量的Library,它可以非常方便地嵌入任意Java应用中,也可以任意方式打包和部署
-
除了Kafka外,无任何外部依赖
-
充分利用Kafka分区机制实现水平扩展和顺序性保证
-
通过可容错的state store实现高效的状态操作(如windowed join和aggregation)
-
提供记录级的处理能力,从而实现毫秒级的低延迟
-
支持基于事件时间的窗口操作,并且可处理晚到的数据(late arrival of records)
-
同时提供底层的处理原语Processor(类似于Storm的spout和bolt),以及高层抽象的DSL(类似于Spark的map/group/reduce)
2.2 Kafka Streams的关键概念
(1)Stream处理拓扑
-
流是Kafka Stream提出的最重要的抽象概念:它表示一个无限的,不断更新的数据集。流是一个有序的,可重放(反复的使用),不可变的容错序列,数据记录的格式是键值对(key-value)。
-
通过Kafka Streams编写一个或多个的计算逻辑的处理器拓扑。其中处理器拓扑是一个由流(边缘)连接的流处理(节点)的图。
-
流处理器是
处理器拓扑
中的一个节点;它表示一个处理的步骤,用来转换流中的数据(从拓扑中的上游处理器一次接受一个输入消息,并且随后产生一个或多个输出消息到其下游处理器中)。
(2)在拓扑中有两个特别的处理器:
-
源处理器(Source Processor):源处理器是一个没有任何上游处理器的特殊类型的流处理器。它从一个或多个kafka主题生成输入流。通过消费这些主题的消息并将它们转发到下游处理器。
-
Sink处理器:sink处理器是一个没有下游流处理器的特殊类型的流处理器。它接收上游流处理器的消息发送到一个指定的Kafka主题。
2.3 KStream&KTable
(1)数据结构类似于map,如下图,key-value键值对
(2)KStream
KStream数据流(data stream),即是一段顺序的,可以无限长,不断更新的数据集。 数据流中比较常记录的是事件,这些事件可以是一次鼠标点击(click),一次交易,或是传感器记录的位置数据。
特点:每一条消息代表一条不可变的新记录。
(3)KTable
KTable传统数据库,包含了各种存储了大量状态(state)的表格。KTable负责抽象的,就是表状数据。每一次操作,都是更新插入(update)
特点:每条消息代表一个更新,几条key相同的消息会将该key的值更新为最后一条消息的值
对于KStream 和 KTable 区别:
同时给KStream 和 KTable 发送两条消息: {"key":1}
和 {"key":2}
-
KStream 做 sum 计算: 结果为
{"key":3}
-
KTable 做 sum 计算: 结果为
{"key":2}
2.4 Kafka Stream入门案例编写
需求: 接收kafka消息内容并计算输入消息内单词的个数
如:接收消息—— 数据源
hello kafka streams
hello heima kafka
hello shanghai heima kafka
将每个value消息 按照 空格拆分 ,对单词的个数进行统计
结果输出:
hello: 3
kafka: 3
streams:1
heima: 2
shanghai: 1
(1)引入依赖
在之前的kafka-demo工程的pom文件中引入
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#999977"><</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>org.apache.kafka<span style="color:#999977"></</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>kafka-streams<span style="color:#999977"></</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>
<span style="color:#999977"></</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span></span></span>
(2)创建类
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#c88fd0">package</span> <span style="color:#8d8df0">com</span>.<span style="color:#b8bfc6">itheima</span>.<span style="color:#b8bfc6">stream</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">ch</span>.<span style="color:#b8bfc6">qos</span>.<span style="color:#b8bfc6">logback</span>.<span style="color:#b8bfc6">classic</span>.<span style="color:#b8bfc6">Level</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">ch</span>.<span style="color:#b8bfc6">qos</span>.<span style="color:#b8bfc6">logback</span>.<span style="color:#b8bfc6">classic</span>.<span style="color:#b8bfc6">Logger</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">ch</span>.<span style="color:#b8bfc6">qos</span>.<span style="color:#b8bfc6">logback</span>.<span style="color:#b8bfc6">classic</span>.<span style="color:#b8bfc6">LoggerContext</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">common</span>.<span style="color:#b8bfc6">serialization</span>.<span style="color:#b8bfc6">Serdes</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">streams</span>.<span style="color:#b8bfc6">KafkaStreams</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">streams</span>.<span style="color:#b8bfc6">KeyValue</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">streams</span>.<span style="color:#b8bfc6">StreamsBuilder</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">streams</span>.<span style="color:#b8bfc6">StreamsConfig</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">streams</span>.<span style="color:#b8bfc6">kstream</span>.<span style="color:#b8bfc6">KStream</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">streams</span>.<span style="color:#b8bfc6">kstream</span>.<span style="color:#b8bfc6">TimeWindows</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">streams</span>.<span style="color:#b8bfc6">kstream</span>.<span style="color:#b8bfc6">ValueMapper</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">slf4j</span>.<span style="color:#b8bfc6">LoggerFactory</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">java</span>.<span style="color:#b8bfc6">time</span>.<span style="color:#b8bfc6">Duration</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">java</span>.<span style="color:#b8bfc6">util</span>.<span style="color:#b8bfc6">Arrays</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">java</span>.<span style="color:#b8bfc6">util</span>.<span style="color:#b8bfc6">Properties</span>;
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">class</span> <span style="color:#8d8df0">KafkaStreamFastStart</span> {
<span style="color:#c88fd0">static</span> {
<span style="color:#b8bfc6">LoggerContext</span> <span style="color:#b8bfc6">loggerContext</span> <span style="color:#b8bfc6">=</span> (<span style="color:#b8bfc6">LoggerContext</span>) <span style="color:#b8bfc6">LoggerFactory</span>.<span style="color:#b8bfc6">getILoggerFactory</span>();
<span style="color:#b8bfc6">Logger</span> <span style="color:#b8bfc6">root</span> <span style="color:#b8bfc6">=</span> <span style="color:#b8bfc6">loggerContext</span>.<span style="color:#b8bfc6">getLogger</span>(<span style="color:#d26b6b">"root"</span>);
<span style="color:#b8bfc6">root</span>.<span style="color:#b8bfc6">setLevel</span>(<span style="color:#b8bfc6">Level</span>.<span style="color:#b8bfc6">INFO</span>);
}
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">static</span> <span style="color:#1cc685">void</span> <span style="color:#b8bfc6">main</span>(<span style="color:#1cc685">String</span>[] <span style="color:#b8bfc6">args</span>) {
<span style="color:#da924a">//1 kafka配置信息</span>
<span style="color:#b8bfc6">Properties</span> <span style="color:#b8bfc6">prop</span> <span style="color:#b8bfc6">=</span> <span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">Properties</span>();
<span style="color:#b8bfc6">prop</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">StreamsConfig</span>.<span style="color:#b8bfc6">BOOTSTRAP_SERVERS_CONFIG</span>, <span style="color:#d26b6b">"192.168.200.131:9092"</span>);
<span style="color:#b8bfc6">prop</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">StreamsConfig</span>.<span style="color:#b8bfc6">DEFAULT_KEY_SERDE_CLASS_CONFIG</span>, <span style="color:#b8bfc6">Serdes</span>.<span style="color:#1cc685">String</span>().<span style="color:#b8bfc6">getClass</span>());
<span style="color:#b8bfc6">prop</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">StreamsConfig</span>.<span style="color:#b8bfc6">DEFAULT_VALUE_SERDE_CLASS_CONFIG</span>, <span style="color:#b8bfc6">Serdes</span>.<span style="color:#1cc685">String</span>().<span style="color:#b8bfc6">getClass</span>());
<span style="color:#b8bfc6">prop</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">StreamsConfig</span>.<span style="color:#b8bfc6">APPLICATION_ID_CONFIG</span>, <span style="color:#d26b6b">"streams-sample"</span>);
<span style="color:#da924a">//2 stream构建器</span>
<span style="color:#b8bfc6">StreamsBuilder</span> <span style="color:#b8bfc6">builder</span> <span style="color:#b8bfc6">=</span> <span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">StreamsBuilder</span>();
<span style="color:#da924a">// 流式计算</span>
<span style="color:#b8bfc6">streamProcessor</span>(<span style="color:#b8bfc6">builder</span>);
<span style="color:#da924a">//3 创建 kafkaStreams</span>
<span style="color:#b8bfc6">KafkaStreams</span> <span style="color:#b8bfc6">kafkaStreams</span> <span style="color:#b8bfc6">=</span> <span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">KafkaStreams</span>(<span style="color:#b8bfc6">builder</span>.<span style="color:#b8bfc6">build</span>(), <span style="color:#b8bfc6">prop</span>);
<span style="color:#da924a">//4 开启kafka流计算</span>
<span style="color:#b8bfc6">System</span>.<span style="color:#b8bfc6">out</span>.<span style="color:#b8bfc6">println</span>(<span style="color:#d26b6b">"streamProcessor start: "</span>);
<span style="color:#b8bfc6">kafkaStreams</span>.<span style="color:#b8bfc6">start</span>();
}
<span style="color:#c88fd0">private</span> <span style="color:#c88fd0">static</span> <span style="color:#1cc685">void</span> <span style="color:#b8bfc6">streamProcessor</span>(<span style="color:#b8bfc6">StreamsBuilder</span> <span style="color:#b8bfc6">builder</span>) {
<span style="color:#da924a">// 接收生产者发送消息</span>
<span style="color:#b8bfc6">KStream</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span>, <span style="color:#1cc685">String</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">stream</span> <span style="color:#b8bfc6">=</span> <span style="color:#b8bfc6">builder</span>.<span style="color:#b8bfc6">stream</span>(<span style="color:#d26b6b">"itcast-topic-input"</span>);
<span style="color:#b8bfc6">stream</span>.<span style="color:#b8bfc6">flatMapValues</span>(<span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">ValueMapper</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span>, <span style="color:#b8bfc6">Iterable</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span><span style="color:#b8bfc6">>></span>() {
<span style="color:#b7b3b3">@Override</span>
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">Iterable</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">apply</span>(<span style="color:#1cc685">String</span> <span style="color:#b8bfc6">value</span>) {
<span style="color:#da924a">// value 接收消息的具体内容</span>
<span style="color:#b8bfc6">System</span>.<span style="color:#b8bfc6">out</span>.<span style="color:#b8bfc6">println</span>(<span style="color:#d26b6b">"消息内容:"</span><span style="color:#b8bfc6">+</span><span style="color:#b8bfc6">value</span>);
<span style="color:#c88fd0">return</span> <span style="color:#b8bfc6">Arrays</span>.<span style="color:#b8bfc6">asList</span>(<span style="color:#b8bfc6">value</span>.<span style="color:#b8bfc6">split</span>(<span style="color:#d26b6b">" "</span>));
}
})
<span style="color:#da924a">// 根据value进行分组</span>
.<span style="color:#b8bfc6">groupBy</span>((<span style="color:#b8bfc6">key</span>,<span style="color:#b8bfc6">value</span>)<span style="color:#b8bfc6">-></span><span style="color:#b8bfc6">value</span>)
<span style="color:#da924a">// 时间窗口 滚动窗口 窗口间隔时间5S</span>
.<span style="color:#b8bfc6">windowedBy</span>(<span style="color:#b8bfc6">TimeWindows</span>.<span style="color:#b8bfc6">of</span>(<span style="color:#b8bfc6">Duration</span>.<span style="color:#b8bfc6">ofSeconds</span>(<span style="color:#64ab8f">5</span>)))
<span style="color:#da924a">// 聚合查询:求单词总个数</span>
.<span style="color:#b8bfc6">count</span>()
<span style="color:#da924a">// 将持续计算的聚合结果 在转成 KStream</span>
.<span style="color:#b8bfc6">toStream</span>()
<span style="color:#da924a">// 处理后结果key和value转成string</span>
.<span style="color:#b8bfc6">map</span>((<span style="color:#b8bfc6">key</span>, <span style="color:#b8bfc6">value</span>) <span style="color:#b8bfc6">-></span> {
<span style="color:#c88fd0">return</span> <span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">KeyValue</span><span style="color:#b8bfc6"><></span>(<span style="color:#b8bfc6">key</span>.<span style="color:#b8bfc6">key</span>().<span style="color:#b8bfc6">toString</span>(), <span style="color:#b8bfc6">value</span>.<span style="color:#b8bfc6">toString</span>());
})
<span style="color:#da924a">// 处理后的结果转发给消费方</span>
.<span style="color:#b8bfc6">to</span>(<span style="color:#d26b6b">"itcast-topic-output"</span>);
}
}</span></span>
(3)测试
准备
使用生产者ProducerkfkFastStart
在topic为:itcast-topic-input
中发送多条消息
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#c88fd0">package</span> <span style="color:#8d8df0">com</span>.<span style="color:#b8bfc6">itheima</span>.<span style="color:#b8bfc6">stream</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">ch</span>.<span style="color:#b8bfc6">qos</span>.<span style="color:#b8bfc6">logback</span>.<span style="color:#b8bfc6">classic</span>.<span style="color:#b8bfc6">Level</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">ch</span>.<span style="color:#b8bfc6">qos</span>.<span style="color:#b8bfc6">logback</span>.<span style="color:#b8bfc6">classic</span>.<span style="color:#b8bfc6">Logger</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">ch</span>.<span style="color:#b8bfc6">qos</span>.<span style="color:#b8bfc6">logback</span>.<span style="color:#b8bfc6">classic</span>.<span style="color:#b8bfc6">LoggerContext</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">clients</span>.<span style="color:#b8bfc6">producer</span>.<span style="color:#b8bfc6">KafkaProducer</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">clients</span>.<span style="color:#b8bfc6">producer</span>.<span style="color:#b8bfc6">ProducerConfig</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">clients</span>.<span style="color:#b8bfc6">producer</span>.<span style="color:#b8bfc6">ProducerRecord</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">slf4j</span>.<span style="color:#b8bfc6">LoggerFactory</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">java</span>.<span style="color:#b8bfc6">util</span>.<span style="color:#b8bfc6">Properties</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 消息生产者</span>
<span style="color:#da924a">*/</span>
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">class</span> <span style="color:#8d8df0">ProducerkfkFastStart</span> {
<span style="color:#c88fd0">static</span> {
<span style="color:#b8bfc6">LoggerContext</span> <span style="color:#b8bfc6">loggerContext</span> <span style="color:#b8bfc6">=</span> (<span style="color:#b8bfc6">LoggerContext</span>) <span style="color:#b8bfc6">LoggerFactory</span>.<span style="color:#b8bfc6">getILoggerFactory</span>();
<span style="color:#b8bfc6">Logger</span> <span style="color:#b8bfc6">root</span> <span style="color:#b8bfc6">=</span> <span style="color:#b8bfc6">loggerContext</span>.<span style="color:#b8bfc6">getLogger</span>(<span style="color:#d26b6b">"root"</span>);
<span style="color:#b8bfc6">root</span>.<span style="color:#b8bfc6">setLevel</span>(<span style="color:#b8bfc6">Level</span>.<span style="color:#b8bfc6">INFO</span>);
}
<span style="color:#c88fd0">private</span> <span style="color:#c88fd0">static</span> <span style="color:#c88fd0">final</span> <span style="color:#1cc685">String</span> <span style="color:#b8bfc6">TOPIC</span> <span style="color:#b8bfc6">=</span> <span style="color:#d26b6b">"itcast-topic-input"</span>;
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">static</span> <span style="color:#1cc685">void</span> <span style="color:#b8bfc6">main</span>(<span style="color:#1cc685">String</span>[] <span style="color:#b8bfc6">args</span>) {
<span style="color:#da924a">//添加kafka的配置信息</span>
<span style="color:#b8bfc6">Properties</span> <span style="color:#b8bfc6">properties</span> <span style="color:#b8bfc6">=</span> <span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">Properties</span>();
<span style="color:#da924a">//配置broker信息</span>
<span style="color:#b8bfc6">properties</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#d26b6b">"bootstrap.servers"</span>,<span style="color:#d26b6b">"192.168.200.131:9092"</span>);
<span style="color:#b8bfc6">properties</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">ProducerConfig</span>.<span style="color:#b8bfc6">KEY_SERIALIZER_CLASS_CONFIG</span>,<span style="color:#d26b6b">"org.apache.kafka.common.serialization.StringSerializer"</span>);
<span style="color:#b8bfc6">properties</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">ProducerConfig</span>.<span style="color:#b8bfc6">VALUE_SERIALIZER_CLASS_CONFIG</span>,<span style="color:#d26b6b">"org.apache.kafka.common.serialization.StringSerializer"</span>);
<span style="color:#b8bfc6">properties</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">ProducerConfig</span>.<span style="color:#b8bfc6">RETRIES_CONFIG</span>,<span style="color:#64ab8f">10</span>);
<span style="color:#da924a">//生产者对象</span>
<span style="color:#b8bfc6">KafkaProducer</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span>,<span style="color:#1cc685">String</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">producer</span> <span style="color:#b8bfc6">=</span> <span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">KafkaProducer</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span>, <span style="color:#1cc685">String</span><span style="color:#b8bfc6">></span>(<span style="color:#b8bfc6">properties</span>);
<span style="color:#c88fd0">try</span> {
<span style="color:#da924a">//封装消息</span>
<span style="color:#b8bfc6">ProducerRecord</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span>,<span style="color:#1cc685">String</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">record</span> <span style="color:#b8bfc6">=</span>
<span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">ProducerRecord</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span>, <span style="color:#1cc685">String</span><span style="color:#b8bfc6">></span>(<span style="color:#b8bfc6">TOPIC</span>,<span style="color:#d26b6b">"k001"</span>,<span style="color:#d26b6b">"hello shanghai kafka stream hello"</span>);
<span style="color:#da924a">//发送消息</span>
<span style="color:#b8bfc6">producer</span>.<span style="color:#b8bfc6">send</span>(<span style="color:#b8bfc6">record</span>);
<span style="color:#b8bfc6">System</span>.<span style="color:#b8bfc6">out</span>.<span style="color:#b8bfc6">println</span>(<span style="color:#d26b6b">"发送消息:"</span><span style="color:#b8bfc6">+</span><span style="color:#b8bfc6">record</span>.<span style="color:#b8bfc6">value</span>());
}<span style="color:#c88fd0">catch</span> (<span style="color:#b8bfc6">Exception</span> <span style="color:#b8bfc6">e</span>){
<span style="color:#b8bfc6">e</span>.<span style="color:#b8bfc6">printStackTrace</span>();
}
<span style="color:#da924a">//关系消息通道</span>
<span style="color:#b8bfc6">producer</span>.<span style="color:#b8bfc6">close</span>();
}
}</span></span>
使用消费者ConsumerkfkFastStart
接收topic为:itcast-topic-output
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#c88fd0">package</span> <span style="color:#8d8df0">com</span>.<span style="color:#b8bfc6">itheima</span>.<span style="color:#b8bfc6">stream</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">ch</span>.<span style="color:#b8bfc6">qos</span>.<span style="color:#b8bfc6">logback</span>.<span style="color:#b8bfc6">classic</span>.<span style="color:#b8bfc6">Level</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">ch</span>.<span style="color:#b8bfc6">qos</span>.<span style="color:#b8bfc6">logback</span>.<span style="color:#b8bfc6">classic</span>.<span style="color:#b8bfc6">Logger</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">ch</span>.<span style="color:#b8bfc6">qos</span>.<span style="color:#b8bfc6">logback</span>.<span style="color:#b8bfc6">classic</span>.<span style="color:#b8bfc6">LoggerContext</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">clients</span>.<span style="color:#b8bfc6">consumer</span>.<span style="color:#b8bfc6">ConsumerConfig</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">clients</span>.<span style="color:#b8bfc6">consumer</span>.<span style="color:#b8bfc6">ConsumerRecord</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">clients</span>.<span style="color:#b8bfc6">consumer</span>.<span style="color:#b8bfc6">ConsumerRecords</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">clients</span>.<span style="color:#b8bfc6">consumer</span>.<span style="color:#b8bfc6">KafkaConsumer</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">slf4j</span>.<span style="color:#b8bfc6">LoggerFactory</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">java</span>.<span style="color:#b8bfc6">time</span>.<span style="color:#b8bfc6">Duration</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">java</span>.<span style="color:#b8bfc6">util</span>.<span style="color:#b8bfc6">Collections</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">java</span>.<span style="color:#b8bfc6">util</span>.<span style="color:#b8bfc6">Properties</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 消息消费者</span>
<span style="color:#da924a">*/</span>
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">class</span> <span style="color:#8d8df0">ConsumerkfkFastStart</span> {
<span style="color:#c88fd0">static</span> {
<span style="color:#b8bfc6">LoggerContext</span> <span style="color:#b8bfc6">loggerContext</span> <span style="color:#b8bfc6">=</span> (<span style="color:#b8bfc6">LoggerContext</span>) <span style="color:#b8bfc6">LoggerFactory</span>.<span style="color:#b8bfc6">getILoggerFactory</span>();
<span style="color:#b8bfc6">Logger</span> <span style="color:#b8bfc6">root</span> <span style="color:#b8bfc6">=</span> <span style="color:#b8bfc6">loggerContext</span>.<span style="color:#b8bfc6">getLogger</span>(<span style="color:#d26b6b">"root"</span>);
<span style="color:#b8bfc6">root</span>.<span style="color:#b8bfc6">setLevel</span>(<span style="color:#b8bfc6">Level</span>.<span style="color:#b8bfc6">INFO</span>);
}
<span style="color:#c88fd0">private</span> <span style="color:#c88fd0">static</span> <span style="color:#c88fd0">final</span> <span style="color:#1cc685">String</span> <span style="color:#b8bfc6">TOPIC</span> <span style="color:#b8bfc6">=</span> <span style="color:#d26b6b">"itcast-topic-output"</span>;
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">static</span> <span style="color:#1cc685">void</span> <span style="color:#b8bfc6">main</span>(<span style="color:#1cc685">String</span>[] <span style="color:#b8bfc6">args</span>) {
<span style="color:#da924a">//添加配置信息</span>
<span style="color:#b8bfc6">Properties</span> <span style="color:#b8bfc6">properties</span> <span style="color:#b8bfc6">=</span> <span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">Properties</span>();
<span style="color:#b8bfc6">properties</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">ConsumerConfig</span>.<span style="color:#b8bfc6">BOOTSTRAP_SERVERS_CONFIG</span>,<span style="color:#d26b6b">"192.168.200.131:9092"</span>);
<span style="color:#b8bfc6">properties</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">ConsumerConfig</span>.<span style="color:#b8bfc6">KEY_DESERIALIZER_CLASS_CONFIG</span>,<span style="color:#d26b6b">"org.apache.kafka.common.serialization.StringDeserializer"</span>);
<span style="color:#b8bfc6">properties</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">ConsumerConfig</span>.<span style="color:#b8bfc6">VALUE_DESERIALIZER_CLASS_CONFIG</span>,<span style="color:#d26b6b">"org.apache.kafka.common.serialization.StringDeserializer"</span>);
<span style="color:#da924a">//设置分组</span>
<span style="color:#b8bfc6">properties</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">ConsumerConfig</span>.<span style="color:#b8bfc6">GROUP_ID_CONFIG</span>,<span style="color:#d26b6b">"group1"</span>);
<span style="color:#da924a">//创建消费者</span>
<span style="color:#b8bfc6">KafkaConsumer</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span>, <span style="color:#1cc685">String</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">consumer</span> <span style="color:#b8bfc6">=</span> <span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">KafkaConsumer</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span>, <span style="color:#1cc685">String</span><span style="color:#b8bfc6">></span>(<span style="color:#b8bfc6">properties</span>);
<span style="color:#da924a">//订阅主题</span>
<span style="color:#b8bfc6">consumer</span>.<span style="color:#b8bfc6">subscribe</span>(<span style="color:#b8bfc6">Collections</span>.<span style="color:#b8bfc6">singletonList</span>(<span style="color:#b8bfc6">TOPIC</span>));
<span style="color:#b8bfc6">System</span>.<span style="color:#b8bfc6">out</span>.<span style="color:#b8bfc6">println</span>(<span style="color:#d26b6b">"消费方获取处理后结果:"</span>);
<span style="color:#c88fd0">while</span> (<span style="color:#84b6cb">true</span>){
<span style="color:#b8bfc6">ConsumerRecords</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span>, <span style="color:#1cc685">String</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">records</span> <span style="color:#b8bfc6">=</span> <span style="color:#b8bfc6">consumer</span>.<span style="color:#b8bfc6">poll</span>(<span style="color:#b8bfc6">Duration</span>.<span style="color:#b8bfc6">ofMillis</span>(<span style="color:#64ab8f">1000</span>));
<span style="color:#c88fd0">for</span> (<span style="color:#b8bfc6">ConsumerRecord</span> <span style="color:#b8bfc6">record</span> : <span style="color:#b8bfc6">records</span>) {
<span style="color:#b8bfc6">System</span>.<span style="color:#b8bfc6">out</span>.<span style="color:#b8bfc6">println</span>(<span style="color:#b8bfc6">record</span>.<span style="color:#b8bfc6">key</span>()<span style="color:#b8bfc6">+</span><span style="color:#d26b6b">": "</span><span style="color:#b8bfc6">+</span><span style="color:#b8bfc6">record</span>.<span style="color:#b8bfc6">value</span>());
}
}
}
}</span></span>
结果:
通过流式计算,会把生产者的多条消息汇总成一条发送到消费者中输出(需要稍等一会)
时间窗口:
Stream流中的数据 是无界的,要想做聚合运算 必须要有时间窗口的概念:
根据时间窗口做聚合,是在实时计算中非常重要的功能。比如我们经常需要统计最近一段时间内的count、sum、avg等统计数据。
Window name | Behavior | Short description |
---|---|---|
Tumbling time window | Time-based | Fixed-size, non-overlapping, gap-less windows 翻滚时间窗口 |
Hopping time window | Time-based | Fixed-size, overlapping windows 跳跃时间窗口 |
Sliding time window | Time-based | Fixed-size, overlapping windows that work on differences between record timestamps 滑动时间窗口 |
Session window | Session-based | Dynamically-sized, non-overlapping, data-driven windows |
2.5 SpringBoot集成Kafka Stream
当前kafka-demo项目需要添加lombok的依赖包
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#999977"><</span><span style="color:#7df46a">properties</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">lombok.version</span><span style="color:#999977">></span>1.18.8<span style="color:#999977"></</span><span style="color:#7df46a">lombok.version</span><span style="color:#999977">></span>
<span style="color:#999977"></</span><span style="color:#7df46a">properties</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>org.projectlombok<span style="color:#999977"></</span><span style="color:#7df46a">groupId</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>lombok<span style="color:#999977"></</span><span style="color:#7df46a">artifactId</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">version</span><span style="color:#999977">></span>${lombok.version}<span style="color:#999977"></</span><span style="color:#7df46a">version</span><span style="color:#999977">></span>
<span style="color:#999977"><</span><span style="color:#7df46a">scope</span><span style="color:#999977">></span>provided<span style="color:#999977"></</span><span style="color:#7df46a">scope</span><span style="color:#999977">></span>
<span style="color:#999977"></</span><span style="color:#7df46a">dependency</span><span style="color:#999977">></span></span></span>
(1)自定配置参数
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#c88fd0">package</span> <span style="color:#8d8df0">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">test</span>.<span style="color:#b8bfc6">stream</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">lombok</span>.<span style="color:#b8bfc6">Getter</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">lombok</span>.<span style="color:#b8bfc6">Setter</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">common</span>.<span style="color:#b8bfc6">serialization</span>.<span style="color:#b8bfc6">Serdes</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">streams</span>.<span style="color:#b8bfc6">StreamsConfig</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">boot</span>.<span style="color:#b8bfc6">context</span>.<span style="color:#b8bfc6">properties</span>.<span style="color:#b8bfc6">ConfigurationProperties</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">context</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">Bean</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">context</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">Configuration</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">EnableKafkaStreams</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">KafkaStreamsDefaultConfiguration</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">config</span>.<span style="color:#b8bfc6">KafkaStreamsConfiguration</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">java</span>.<span style="color:#b8bfc6">util</span>.<span style="color:#b8bfc6">HashMap</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">java</span>.<span style="color:#b8bfc6">util</span>.<span style="color:#b8bfc6">Map</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 通过重新注册KafkaStreamsConfiguration对象,设置自定配置参数</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@Setter</span>
<span style="color:#b7b3b3">@Getter</span>
<span style="color:#b7b3b3">@Configuration</span>
<span style="color:#b7b3b3">@EnableKafkaStreams</span>
<span style="color:#b7b3b3">@ConfigurationProperties</span>(<span style="color:#b8bfc6">prefix</span><span style="color:#b8bfc6">=</span><span style="color:#d26b6b">"kafka"</span>)
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">class</span> <span style="color:#8d8df0">KafkaStreamConfig</span> {
<span style="color:#c88fd0">private</span> <span style="color:#c88fd0">static</span> <span style="color:#c88fd0">final</span> <span style="color:#1cc685">int</span> <span style="color:#b8bfc6">MAX_MESSAGE_SIZE</span> <span style="color:#b8bfc6">=</span> <span style="color:#64ab8f">16</span><span style="color:#b8bfc6">*</span> <span style="color:#64ab8f">1024</span> <span style="color:#b8bfc6">*</span> <span style="color:#64ab8f">1024</span>;
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">String</span> <span style="color:#b8bfc6">hosts</span>;
<span style="color:#c88fd0">private</span> <span style="color:#1cc685">String</span> <span style="color:#b8bfc6">group</span>;
<span style="color:#da924a">/**</span>
<span style="color:#da924a">* 重新定义默认的KafkaStreams配置属性,包括:</span>
<span style="color:#da924a">* 1、服务器地址</span>
<span style="color:#da924a">* 2、应用ID</span>
<span style="color:#da924a">* 3、流消息的副本数等配置</span>
<span style="color:#da924a">* @return</span>
<span style="color:#da924a">*/</span>
<span style="color:#b7b3b3">@Bean</span>(<span style="color:#b8bfc6">name</span> <span style="color:#b8bfc6">=</span> <span style="color:#b8bfc6">KafkaStreamsDefaultConfiguration</span>.<span style="color:#b8bfc6">DEFAULT_STREAMS_CONFIG_BEAN_NAME</span>)
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">KafkaStreamsConfiguration</span> <span style="color:#b8bfc6">defaultKafkaStreamsConfig</span>() {
<span style="color:#b8bfc6">Map</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span>, <span style="color:#1cc685">Object</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">props</span> <span style="color:#b8bfc6">=</span> <span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">HashMap</span><span style="color:#b8bfc6"><></span>();
<span style="color:#b8bfc6">props</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">StreamsConfig</span>.<span style="color:#b8bfc6">BOOTSTRAP_SERVERS_CONFIG</span>, <span style="color:#b8bfc6">hosts</span>);
<span style="color:#b8bfc6">props</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">StreamsConfig</span>.<span style="color:#b8bfc6">APPLICATION_ID_CONFIG</span>, <span style="color:#c88fd0">this</span>.<span style="color:#b8bfc6">getGroup</span>()<span style="color:#b8bfc6">+</span><span style="color:#d26b6b">"_stream_aid"</span>);
<span style="color:#b8bfc6">props</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">StreamsConfig</span>.<span style="color:#b8bfc6">CLIENT_ID_CONFIG</span>, <span style="color:#c88fd0">this</span>.<span style="color:#b8bfc6">getGroup</span>()<span style="color:#b8bfc6">+</span><span style="color:#d26b6b">"_stream_cid"</span>);
<span style="color:#b8bfc6">props</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">StreamsConfig</span>.<span style="color:#b8bfc6">RETRIES_CONFIG</span>, <span style="color:#64ab8f">10</span>);
<span style="color:#b8bfc6">props</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">StreamsConfig</span>.<span style="color:#b8bfc6">DEFAULT_KEY_SERDE_CLASS_CONFIG</span>, <span style="color:#b8bfc6">Serdes</span>.<span style="color:#1cc685">String</span>().<span style="color:#b8bfc6">getClass</span>());
<span style="color:#b8bfc6">props</span>.<span style="color:#b8bfc6">put</span>(<span style="color:#b8bfc6">StreamsConfig</span>.<span style="color:#b8bfc6">DEFAULT_VALUE_SERDE_CLASS_CONFIG</span>, <span style="color:#b8bfc6">Serdes</span>.<span style="color:#1cc685">String</span>().<span style="color:#b8bfc6">getClass</span>());
<span style="color:#c88fd0">return</span> <span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">KafkaStreamsConfiguration</span>(<span style="color:#b8bfc6">props</span>);
}
}</span></span>
修改application.yml文件,在最下方添加自定义配置
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#84b6cb">kafka</span><span style="color:#b7b3b3">:</span>
<span style="color:#84b6cb"> hosts</span><span style="color:#b7b3b3">: </span>192.168.200.130<span style="color:#b7b3b3">:</span><span style="color:#64ab8f">9092</span>
<span style="color:#84b6cb"> group</span><span style="color:#b7b3b3">: </span>$<span style="color:#b7b3b3">{</span>spring.application.name<span style="color:#b7b3b3">}</span></span></span>
(2)定义监听实现
<span style="background-color:#333333"><span style="color:#b8bfc6"><span style="color:#c88fd0">package</span> <span style="color:#8d8df0">com</span>.<span style="color:#b8bfc6">heima</span>.<span style="color:#b8bfc6">test</span>.<span style="color:#b8bfc6">stream</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">lombok</span>.<span style="color:#b8bfc6">extern</span>.<span style="color:#b8bfc6">slf4j</span>.<span style="color:#b8bfc6">Slf4j</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">streams</span>.<span style="color:#b8bfc6">KeyValue</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">streams</span>.<span style="color:#b8bfc6">StreamsBuilder</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">streams</span>.<span style="color:#b8bfc6">kstream</span>.<span style="color:#b8bfc6">KStream</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">streams</span>.<span style="color:#b8bfc6">kstream</span>.<span style="color:#b8bfc6">TimeWindows</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">apache</span>.<span style="color:#b8bfc6">kafka</span>.<span style="color:#b8bfc6">streams</span>.<span style="color:#b8bfc6">kstream</span>.<span style="color:#b8bfc6">ValueMapper</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">context</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">Bean</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">org</span>.<span style="color:#b8bfc6">springframework</span>.<span style="color:#b8bfc6">context</span>.<span style="color:#b8bfc6">annotation</span>.<span style="color:#b8bfc6">Configuration</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">java</span>.<span style="color:#b8bfc6">time</span>.<span style="color:#b8bfc6">Duration</span>;
<span style="color:#c88fd0">import</span> <span style="color:#b8bfc6">java</span>.<span style="color:#b8bfc6">util</span>.<span style="color:#b8bfc6">Arrays</span>;
<span style="color:#b7b3b3">@Configuration</span>
<span style="color:#b7b3b3">@Slf4j</span>
<span style="color:#c88fd0">public</span> <span style="color:#c88fd0">class</span> <span style="color:#8d8df0">KafkaStreamHelloListener</span> {
<span style="color:#b7b3b3">@Bean</span>
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">KStream</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span>,<span style="color:#1cc685">String</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">kStream</span>(<span style="color:#b8bfc6">StreamsBuilder</span> <span style="color:#b8bfc6">streamsBuilder</span>){
<span style="color:#b8bfc6">KStream</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span>, <span style="color:#1cc685">String</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">stream</span> <span style="color:#b8bfc6">=</span> <span style="color:#b8bfc6">streamsBuilder</span>.<span style="color:#b8bfc6">stream</span>(<span style="color:#d26b6b">"itcast-topic-input"</span>);
<span style="color:#da924a">//处理消息</span>
<span style="color:#b8bfc6">stream</span>.<span style="color:#b8bfc6">flatMapValues</span>(<span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">ValueMapper</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span>, <span style="color:#b8bfc6">Iterable</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span><span style="color:#b8bfc6">>></span>() {
<span style="color:#b7b3b3">@Override</span>
<span style="color:#c88fd0">public</span> <span style="color:#b8bfc6">Iterable</span><span style="color:#b8bfc6"><</span><span style="color:#1cc685">String</span><span style="color:#b8bfc6">></span> <span style="color:#b8bfc6">apply</span>(<span style="color:#1cc685">String</span> <span style="color:#b8bfc6">value</span>) {
<span style="color:#b8bfc6">System</span>.<span style="color:#b8bfc6">out</span>.<span style="color:#b8bfc6">println</span>(<span style="color:#d26b6b">"消息内容为:"</span><span style="color:#b8bfc6">+</span><span style="color:#b8bfc6">value</span>);
<span style="color:#c88fd0">return</span> <span style="color:#b8bfc6">Arrays</span>.<span style="color:#b8bfc6">asList</span>(<span style="color:#b8bfc6">value</span>.<span style="color:#b8bfc6">split</span>(<span style="color:#d26b6b">" "</span>));
}
})
<span style="color:#da924a">//根据value进行分组</span>
.<span style="color:#b8bfc6">groupBy</span>((<span style="color:#b8bfc6">key</span>,<span style="color:#b8bfc6">value</span>)<span style="color:#b8bfc6">-></span><span style="color:#b8bfc6">value</span>)
<span style="color:#da924a">//聚合计算时间间隔</span>
.<span style="color:#b8bfc6">windowedBy</span>(<span style="color:#b8bfc6">TimeWindows</span>.<span style="color:#b8bfc6">of</span>(<span style="color:#b8bfc6">Duration</span>.<span style="color:#b8bfc6">ofSeconds</span>(<span style="color:#64ab8f">10</span>)))
<span style="color:#da924a">//聚合查询:求单词总个数</span>
.<span style="color:#b8bfc6">count</span>()
<span style="color:#da924a">// 转成 KStream</span>
.<span style="color:#b8bfc6">toStream</span>()
<span style="color:#da924a">// 处理后结果key和value转成string</span>
.<span style="color:#b8bfc6">map</span>((<span style="color:#b8bfc6">key</span>,<span style="color:#b8bfc6">value</span>)<span style="color:#b8bfc6">-></span>{
<span style="color:#b8bfc6">System</span>.<span style="color:#b8bfc6">out</span>.<span style="color:#b8bfc6">println</span>(<span style="color:#d26b6b">"key:"</span><span style="color:#b8bfc6">+</span><span style="color:#b8bfc6">key</span><span style="color:#b8bfc6">+</span><span style="color:#d26b6b">",value:"</span><span style="color:#b8bfc6">+</span><span style="color:#b8bfc6">value</span>);
<span style="color:#c88fd0">return</span> <span style="color:#c88fd0">new</span> <span style="color:#b8bfc6">KeyValue</span><span style="color:#b8bfc6"><></span>(<span style="color:#b8bfc6">key</span>.<span style="color:#b8bfc6">key</span>().<span style="color:#b8bfc6">toString</span>(),<span style="color:#b8bfc6">value</span>.<span style="color:#b8bfc6">toString</span>());
})
<span style="color:#da924a">//发送消息</span>
.<span style="color:#b8bfc6">to</span>(<span style="color:#d26b6b">"itcast-topic-out"</span>);
<span style="color:#c88fd0">return</span> <span style="color:#b8bfc6">stream</span>;
}
}</span></span>
测试:
启动微服务,正常发送消息,可以正常接收到消息
3 app端热点文章计算
3.1 需求分析
-
筛选出文章列表中最近5天热度较高的文章在每个频道的首页展示
-
根据用户的行为(阅读、点赞、评论、收藏)实时计算热点文章
3.2 思路分析
如下图:(如果看不清楚则可以开发资料中的pdf)
整体实现思路共分为3步
-
定时计算热点文章
-
定时任务每天凌晨1点,查询前5天的文章
-
计算每个文章的分值,其中不同的行为设置不同的权重
(阅读:1,点赞:3,评论:5,收藏:8)
-
按照分值排序,给每个频道找出分值较高的30条数据,存入缓存中
为什么要按照频道缓存?
在前端工程中的如下代码:
这些就是首页的频道信息,其中的
id
就是与ad_channel表中的id要对应上。
-
-
实时计算热点文章
-
行为微服务,用户阅读或点赞了某一篇文章(目前实现这两个功能),发送消息给kafka
-
文章微服务,接收行为消息,使用kafkastream流式处理进行聚合,发消息给kafka
-
文章微服务,接收聚合之后的消息,计算文章分值(当日分值计算方式,在原有权重的基础上再*3)
-
根据当前文章的频道id查询缓存中的数据
-
当前文章分值与缓存中的数据比较,如果当前分值大于某一条缓存中的数据,则直接替换
-
新数据重新设置到缓存中
-
更新数据库文章的行为数量
-
-
查询热点数据
-
判断是否是首页
-
是首页,选择是推荐,tag值为
__all__
,从所有缓存中筛选出分值最高的30条数据返回 -
是首页,选择是具体的频道,tag是具体的数字,从缓存中获取对应的频道中的数据返回
-
不是,则查询数据库中的数据
-
3.3 功能实现
3.3.1 文章分值定时计算
3.3.1.1 集成Redis和远程接口准备
分值计算不涉及到前端工程,也无需提供api接口,是一个纯后台的功能的开发。
1)redis集成
在article-service
模块中 pom.xml 添加redis依赖,并引入redis 配置
<span style="background-color:#333333"><span style="color:#b8bfc6"> <dependency>
<groupId>com.heima</groupId>
<artifactId>heima-cache-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency></span></span>
<span style="background-color:#333333"><span style="color:#b8bfc6">spring:
application:
name: leadnews-article
redis:
host: 192.168.200.130
port: 6379</span></span>
2)频道列表远程接口准备
计算完成新热数据后,需要给每个频道缓存一份数据,所以需要查询所有频道信息
① 定义admin的远程接口
<span style="background-color:#333333"><span style="color:#b8bfc6">@FeignClient(
value = "leadnews-admin",
fallbackFactory = AdminFeignFallback.class,
configuration = HeimaFeignAutoConfiguration.class
)
public interface AdminFeign {
@GetMapping("/api/v1/channel/channels")
ResponseResult<List<AdChannel>> selectAllChannel();
}</span></span>
服务降级
<span style="background-color:#333333"><span style="color:#b8bfc6">@Component
@Slf4j
public class AdminFeignFallback implements FallbackFactory<AdminFeign> {
@Override
public AdminFeign create(Throwable throwable) {
throwable.printStackTrace();
return new AdminFeign() {
@Override
public ResponseResult<List<AdChannel>> selectAllChannel() {
log.error("AdminFeign selectAllChannel 远程调用出错啦 ~~~ !!!! {} ",throwable.getMessage());
return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR);
}
};
}
}</span></span>
② admin端提供接口
该功能之前已实现
3.3.1.2 定时计算业务实现
定义业务层接口
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.article.service;
/**
* <p>
* 热文章表 服务类
* </p>
*
* @author itheima
*/
public interface HotArticleService{
/**
* 计算热文章
*/
public void computeHotArticle();
}</span></span>
修改ArticleConstans,添加一个属性
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.common.constants.article;
public class ArticleConstants{
public static final Short LOADTYPE_LOAD_MORE = 1;
public static final Short LOADTYPE_LOAD_NEW = 2;
public static final String DEFAULT_TAG = "__all__";
// 文章行为分值
public static final Integer HOT_ARTICLE_VIEW_WEIGHT = 1;
public static final Integer HOT_ARTICLE_LIKE_WEIGHT = 3;
public static final Integer HOT_ARTICLE_COMMENT_WEIGHT = 5;
public static final Integer HOT_ARTICLE_COLLECTION_WEIGHT = 8;
// 存到redis热文章前缀
public static final String HOT_ARTICLE_FIRST_PAGE = "hot_article_first_page_";
}</span></span>
创建一个vo接收计算分值后的对象
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.model.article.vos;
import com.heima.model.article.pojos.ApArticle;
import lombok.Data;
@Data
public class HotArticleVo extends ApArticle {
/**
* 分值
*/
private Integer score;
}</span></span>
ApArticleMapper提供按照时间查询方法
<span style="background-color:#333333"><span style="color:#b8bfc6"> @Select("select aa.* from ap_article aa left join ap_article_config aac on aa.id=aac.article_id " +
"where aac.is_delete!=1 and aac.is_down != 1 " +
"and aa.publish_time > #{beginDate}")
public List<ApArticle> selectArticleByDate(@Param("beginDate") String beginDate);</span></span>
业务层实现类
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.article.service.impl;
import com.alibaba.fastjson.JSON;
import com.heima.article.mapper.ApArticleMapper;
import com.heima.article.service.HotArticleService;
import com.heima.common.constants.article.ArticleConstants;
import com.heima.feigns.AdminFeign;
import com.heima.model.admin.pojos.AdChannel;
import com.heima.model.article.pojos.ApArticle;
import com.heima.model.article.vos.HotArticleVo;
import com.heima.model.common.dtos.ResponseResult;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Description:
* @Version: V1.0
*/
@Service
@Transactional
public class HotArticleServiceImpl implements HotArticleService {
@Autowired
private ApArticleMapper apArticleMapper;
/**
* 计算热文章
*/
@Override
public void computeHotArticle() {
//1 查询前5天的 (已上架、未删除) 文章数据
String date = LocalDateTime.now().minusDays(5)
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd 00:00:00"));
List<ApArticle> articleList = apArticleMapper.selectArticleByDate(date);
//2 计算热点文章分值
List<HotArticleVo> hotArticleVoList = computeArticleScore(articleList);
//3 为每一个频道缓存热点较高的30条文章
cacheTagToRedis(hotArticleVoList);
}
@Autowired
AdminFeign adminFeign;
@Autowired
private RedisTemplate<String,String> redisTemplate;
/**
* 3 频道缓存热点较高的30条文章
* @param hotArticleVoList
*/
private void cacheTagToRedis(List<HotArticleVo> hotArticleVoList) {
//1 查询所有的频道列表
ResponseResult responseResult = adminFeign.selectAllChannel();
if (responseResult.getCode() == 0) {
List<AdChannel> list = JSON.parseArray(JSON.toJSONString(responseResult.getData()), AdChannel.class);
//2 遍历频道列表,筛选当前频道下的文章
for (AdChannel adChannel : list) {
//3 给每个频道下的文章进行缓存(已排序)
List<HotArticleVo> hotArticleVos = hotArticleVoList.stream()
// 当前频道下的文章列表
.filter(hotArticle -> hotArticle.getChannelId().equals(adChannel.getId()))
.collect(Collectors.toList());
sortAndCache(hotArticleVos, ArticleConstants.HOT_ARTICLE_FIRST_PAGE + adChannel.getId());
}
}
//4 给推荐频道缓存30条数据 所有文章排序之后的前30条
sortAndCache(hotArticleVoList, ArticleConstants.HOT_ARTICLE_FIRST_PAGE + ArticleConstants.DEFAULT_TAG);
}
/**
* 缓存热点文章
* @param hotArticleVos
*/
private void sortAndCache(List<HotArticleVo> hotArticleVos, String cacheKey) {
// 对文章进行排序
hotArticleVos = hotArticleVos.stream()
.sorted(Comparator.comparing(HotArticleVo::getScore).reversed())
.limit(30)
.collect(Collectors.toList());
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(hotArticleVos));
}
/**
* 2 计算热点文章的分值
* @param articleList
* @return
*/
private List<HotArticleVo> computeArticleScore(List<ApArticle> articleList) {
// 定义返回集合
return articleList.stream().map(apArticle -> {
HotArticleVo hotArticleVo = new HotArticleVo();
BeanUtils.copyProperties(apArticle,hotArticleVo);
// 2.1计算文章分值算法
Integer score = computeScore(apArticle);
hotArticleVo.setScore(score);
return hotArticleVo;
}).collect(Collectors.toList());
}
/**
* 2.1计算文章分值算法
* @param apArticle
* @return
*/
private Integer computeScore(ApArticle apArticle) {
int score = 0;
// 阅读 1
if (apArticle.getViews() != null) {
score += apArticle.getViews() * ArticleConstants.HOT_ARTICLE_VIEW_WEIGHT;
}
// 点赞 3
if (apArticle.getLikes() != null) {
score += apArticle.getLikes() * ArticleConstants.HOT_ARTICLE_LIKE_WEIGHT;
}
// 评论 5
if (apArticle.getComment() != null) {
score += apArticle.getComment() * ArticleConstants.HOT_ARTICLE_COMMENT_WEIGHT;
}
// 收藏 8
if (apArticle.getCollection() != null) {
score += apArticle.getCollection() * ArticleConstants.HOT_ARTICLE_COLLECTION_WEIGHT;
}
return score;
}
}</span></span>
3.3.1.3 单元测试
-
需要首先启动admin微服务
-
启动Redis服务
-
在数据库中准备点数据:把数据库的时间修改为前5天的
<span style="background-color:#333333"><span style="color:#b8bfc6"># 在原有时间基础上 + 指定天数
UPDATE ap_article SET publish_time = DATE_ADD(publish_time,INTERVAL 42 day)
# 改为指定时间
UPDATE ap_article SET publish_time = '2021-08-17 00:00:00'</span></span>
-
可以先使用单元测试调试代码
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.article.test;
import com.heima.article.ArticleApplication;
import com.heima.article.service.HotArticleService;
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;
@SpringBootTest(classes = ArticleApplication.class)
@RunWith(SpringRunner.class)
public class HotArticleServiceTest {
@Autowired
private HotArticleService hotArticleService;
@Test
public void testComputeHotArticle(){
hotArticleService.computeHotArticle();
}
}</span></span>
测试完成以后,可以安装资料文件夹下的redis连接工具
新建连接
查看数据
4)定时任务创建
访问:http://192.168.200.129:8888/xxl-job-admin/
① 在xxl-job-admin中新建执行器和任务
新建执行器: leadnews-article-executor
新建任务
② article-service
中集成xxl-job
引入xxljob通用配置依赖
<span style="background-color:#333333"><span style="color:#b8bfc6"> <dependency>
<groupId>com.heima</groupId>
<artifactId>heima-schedule-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency></span></span>
修改application.yml,新增以下内容,其中appname和port要与其他任务区分
<span style="background-color:#333333"><span style="color:#b8bfc6">xxljob:
admin:
addresses: http://192.168.200.130:8888/xxl-job-admin
executor:
appname: leadnews-article-executor
port: 9991
logPath: D:/xxljob/logs</span></span>
③ java程序新建任务
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.article.job;
import com.heima.article.service.HotArticleService;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.annotation.XxlJob;
import lombok.extern.log4j.Log4j2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
@Log4j2
public class ComputeHotArticleJob {
@Autowired
private HotArticleService hotArticleService;
@XxlJob("computeHotArticleJob")
public ReturnT<String> handle(String param) throws Exception {
log.info("热文章分值计算调度任务开始执行....");
hotArticleService.computeHotArticle();
log.info("热文章分值计算调度任务完成....");
return ReturnT.SUCCESS;
}
}</span></span>
5)测试
启动 leadnews-admin,leadnews-behavior,leadnews-article完成测试
3.3.2 文章分值实时计算
3.3.2.1 行为微服务-消息生产方
1)用户行为(阅读量,评论,点赞,收藏)发送消息,目前课程中完成的有阅读和点赞
① 在leadnews-behavior微服务中集成kafka生产者配置
修改application.yml,新增内容
<span style="background-color:#333333"><span style="color:#b8bfc6">spring:
application:
name: leadnews-behavior
kafka:
bootstrap-servers: 192.168.200.129:9092
producer:
retries: 1
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.apache.kafka.common.serialization.StringSerializer</span></span>
定义消息发送封装类:UpdateArticleMess
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.model.mess.app;
import lombok.Data;
@Data
public class UpdateArticleMess {
/**
* 修改文章的字段类型
*/
private UpdateArticleType type;
/**
* 文章ID
*/
private Long articleId;
/**
* 次数 +1 -1
*/
private Integer add;
public enum UpdateArticleType{ // 行为类型
COLLECTION,COMMENT,LIKES,VIEWS;
}
}</span></span>
topic常量类:
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.common.constants.message;
public class HotArticleConstants {
public static final String HOTARTICLE_SCORE_INPUT_TOPIC="hot.article.score.topic";
}</span></span>
② 修改ApLikesBehaviorServiceImpl新增发送消息
如图:
完整代码如下:
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.behavior.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.behavior.mapper.ApLikesBehaviorMapper;
import com.heima.behavior.service.ApBehaviorEntryService;
import com.heima.behavior.service.ApLikesBehaviorService;
import com.heima.common.constans.message.HotArticleConstants;
import com.heima.model.article.mess.UpdateArticleMess;
import com.heima.model.behavior.dtos.LikesBehaviorDto;
import com.heima.model.behavior.pojos.ApBehaviorEntry;
import com.heima.model.behavior.pojos.ApLikesBehavior;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.user.pojos.ApUser;
import com.heima.utils.threadlocal.AppThreadLocalUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.UUID;
@Service
public class ApLikesBehaviorServiceImpl extends ServiceImpl<ApLikesBehaviorMapper, ApLikesBehavior> implements ApLikesBehaviorService {
@Autowired
private ApBehaviorEntryService apBehaviorEntryService;
@Autowired
KafkaTemplate kafkaTemplate;
@Override
public ResponseResult like(LikesBehaviorDto dto) {
//1.检查参数
//2.查询行为实体
//3.点赞或取消点赞
//*****添加文章点赞-发送消息******
// =======================新加代码==========================
UpdateArticleMess mess = new UpdateArticleMess();
mess.setType(UpdateArticleMess.UpdateArticleType.LIKES);
mess.setArticleId(dto.getArticleId());
mess.setAdd(operation==0?1:-1);
kafkaTemplate.send(HotArticleConstants.HOTARTICLE_SCORE_INPUT_TOPIC, JSON.toJSONString(mess));
log.info("点赞行为 发送kafka消息 ==>{}",JSON.toJSONString(mess));
// =======================新加代码==========================
return ResponseResult.okResult();
}
}</span></span>
③ 修改阅读行为的类ApReadBehaviorServiceImpl发送消息
如图:
完整代码:
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.behavior.service.impl;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.behavior.mapper.ApReadBehaviorMapper;
import com.heima.behavior.service.ApBehaviorEntryService;
import com.heima.behavior.service.ApReadBehaviorService;
import com.heima.common.constants.message.HotArticleConstants;
import com.heima.exception.CustException;
import com.heima.model.behavior.dtos.ReadBehaviorDto;
import com.heima.model.behavior.pojos.ApBehaviorEntry;
import com.heima.model.behavior.pojos.ApReadBehavior;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.mess.app.UpdateArticleMess;
import com.heima.model.user.pojos.ApUser;
import com.heima.utils.threadlocal.AppThreadLocalUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.UUID;
/**
* @Description:
* @Version: V1.0
*/
@Service
public class ApReadBehaviorServiceImpl extends ServiceImpl<ApReadBehaviorMapper, ApReadBehavior> implements ApReadBehaviorService {
@Autowired
ApBehaviorEntryService apBehaviorEntryService;
@Autowired
KafkaTemplate kafkaTemplate;
/**
* 保存阅读行为
* @param dto
* @return
*/
@Override
public ResponseResult readBehavior(ReadBehaviorDto dto) {
// 1 参数检查
// 2 查询行为实体
//3.保存或更新阅读的行为
// =======================新加代码==========================
UpdateArticleMess mess = new UpdateArticleMess();
mess.setType(UpdateArticleMess.UpdateArticleType.VIEWS);
mess.setArticleId(dto.getArticleId());
mess.setAdd(1);
kafkaTemplate.send(HotArticleConstants.HOTARTICLE_SCORE_INPUT_TOPIC,JSON.toJSONString(mess));
log.info("阅读行为 发送kafka消息 ==>{}",JSON.toJSONString(mess));
// =======================新加代码==========================
return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
}
}
</span></span>
3.3.2.2 文章微服务-消息消费方实时计算实现
使用kafkaStream实时接收消息,根据文章的ID和文章的行为(阅读、点赞、评论)聚合内容,计算分值。
① 在article-service微服务中集成 kafkaStream
article-service
引入pom.xml依赖
<span style="background-color:#333333"><span style="color:#b8bfc6"> <dependency>
<groupId>org.apache.kafka</groupId>
<artifactId>kafka-streams</artifactId>
</dependency></span></span>
application.yml中新增自定义配置
<span style="background-color:#333333"><span style="color:#b8bfc6">kafka:
hosts: 192.168.200.130:9092
group: ${spring.application.name}</span></span>
② 定义实体类,用于聚合之后的分值封装
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.model.mess.app;
import lombok.Data;
@Data
public class ArticleVisitStreamMess {
/**
* 文章id
*/
private Long articleId;
/**
* 阅读
*/
private long view;
/**
* 收藏
*/
private long collect;
/**
* 评论
*/
private long comment;
/**
* 点赞
*/
private long like;
}</span></span>
修改常量类:增加常量
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.common.constans.message;
public class HotArticleConstants {
// 接收用户文章行为后发送消息topic
public static final String HOTARTICLE_SCORE_INPUT_TOPIC="hot.article.score.topic";
// 计算文章分值成功后发送消息topic
public static final String HOTARTICLE_INCR_HANDLE_OUPUT_TOPIC="hot.article.incr.handle.topic";
}</span></span>
③ 定义stream,接收消息并聚合
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.article.stream;
import com.alibaba.fastjson.JSON;
import com.heima.common.constants.message.HotArticleConstants;
import com.heima.model.message.ArticleVisitStreamMess;
import com.heima.model.message.UpdateArticleMess;
import lombok.extern.slf4j.Slf4j;
import org.apache.kafka.streams.KeyValue;
import org.apache.kafka.streams.StreamsBuilder;
import org.apache.kafka.streams.kstream.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
@Slf4j
public class HotArticleStreamHandler {
@Bean
public KStream<String,String> kStream(StreamsBuilder streamsBuilder){
//接收消息
KStream<String, String> stream = streamsBuilder.stream(HotArticleConstants.HOTARTICLE_SCORE_INPUT_TOPIC);
//流式处理
stream.map((key, value) -> {
UpdateArticleMess updateArticleMess = JSON.parseObject(value, UpdateArticleMess.class);
return new KeyValue<>(updateArticleMess.getArticleId().toString(), updateArticleMess.getType().name() + ":" + updateArticleMess.getAdd());
})
//按照key进行聚合
.groupBy((key, value) -> key)
//聚合计算时间间隔
.windowedBy(TimeWindows.of(Duration.ofSeconds(10)))
//聚合操作
.aggregate(new Initializer<String>() {
@Override
public String apply() {
return JSON.toJSONString(new ArticleVisitStreamMess());
}
}, new Aggregator<String, String, String>() {
@Override
public String apply(String aggKey, String value, String aggValue) {
String[] valAry = value.split(":");
ArticleVisitStreamMess streamMess = JSON.parseObject(value, ArticleVisitStreamMess.class);
/**
* 累加计算,操作类型有可能是正数,也有可能是负数
*/
switch (UpdateArticleMess.UpdateArticleType.valueOf(valAry[0])) {
case COLLECTION: // 设置收藏
streamMess.setCollect(streamMess.getCollect()+Integer.parseInt(valAry[1]));
break;
case COMMENT: // 设置评论
streamMess.setComment(streamMess.getComment()+Integer.parseInt(valAry[1]));
break;
case LIKES: // 设置点赞
streamMess.setLike(streamMess.getLike()+Integer.parseInt(valAry[1]));
break;
case VIEWS: // 设置阅读
streamMess.setView(streamMess.getView()+Integer.parseInt(valAry[1]));
break;
}
log.info("当前时间窗口内的消息处理结果:{}", streamMess);
return JSON.toJSONString(streamMess);
}
})
//转换为Kstream
.toStream()
//聚合之后的数据
.map((key, value) -> {
ArticleVisitStreamMess streamMess = JSON.parseObject(value, ArticleVisitStreamMess.class);
streamMess.setArticleId(Long.valueOf(key.key()));
log.info("聚合之后的数据为:{}",streamMess);
return new KeyValue<>(key.key().toString(), JSON.toJSONString(streamMess));
})
//发送消息
.to(HotArticleConstants.HOTARTICLE_INCR_HANDLE_OUPUT_TOPIC);
log.info("kStream流式消息处理完毕");
return stream;
}
}</span></span>
3.3.2.3 APP端文章行为收发消息聚合计算测试
启动前端和后端服务测试是否可以接收到消息,并且是否可以完成聚合运算
行为微服务:
文章微服务:
3.3.2.4 文章微服务-更新文章分值
文章微服务接收到kafka stream 实时流式处理后的结果消息后,需要做两件事情:
-
更新MySQL数据库文章表的相关行为的分值
-
更新Redis缓存中热点文章
1)重新计算文章的分值,更新到数据库和缓存中
① 定义监听,接收聚合之后的数据,文章的分值重新进行计算
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.article.listener;
import com.alibaba.fastjson.JSON;
import com.heima.article.service.ApArticleService;
import com.heima.common.constants.message.HotArticleConstants;
import com.heima.model.article.mess.ArticleVisitStreamMess;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.kafka.annotation.KafkaListener;
import org.springframework.stereotype.Component;
/**
* @Description:
* @Version: V1.0
*/
@Component
@Slf4j
public class ArticleIncrHandleListener {
@Autowired
ApArticleService apArticleService;
@KafkaListener(topics = HotArticleConstants.HOTARTICLE_INCR_HANDLE_OUPUT_TOPIC)
public void receiveMessage(String message){
log.info("kafka监听触发 更新文章热度值 :{}", message);
apArticleService.updateApArticle(JSON.parseObject(message, ArticleVisitStreamMess.class));
}
}</span></span>
② 在ApArticleService添加方法,用于更新数据库中的文章分值
<span style="background-color:#333333"><span style="color:#b8bfc6"> /**
* 重新计算文章分值
* @param mess
*/
public void updateApArticle(ArticleVisitStreamMess mess);</span></span>
实现类方法
<span style="background-color:#333333"><span style="color:#b8bfc6"> @Autowired
RedisTemplate<String,String> redisTemplate;
/**
* 重新计算文章分值
* @param mess
*/
@Override
public void updateApArticle(ArticleVisitStreamMess mess) {
log.info("updateApArticle is begin: {}",mess);
//1 查询文档
ApArticle apArticle = getById(mess.getArticleId());
if (apArticle == null) {
log.error("apArticle is null id:{}", mess.getArticleId());
CustException.cust(AppHttpCodeEnum.DATA_NOT_EXIST);
}
//2 修改文章的行为数据(阅读1、点赞3、评论5、收藏8)
if (mess.getView() != 0) {
int view = (int) (apArticle.getViews() == null ? mess.getView() : mess.getView() + apArticle.getViews());
apArticle.setViews(view);
}
if (mess.getLike() != 0) {
int like = (int) (apArticle.getLikes() == null ? mess.getLike() : mess.getLike() + apArticle.getLikes());
apArticle.setLikes(like);
}
if (mess.getComment() != 0) {
int comment = (int) (apArticle.getComment() == null ? mess.getComment() : mess.getComment() + apArticle.getComment());
apArticle.setComment(comment);
}
if (mess.getCollect() != 0) {
int collection = (int) (apArticle.getCollection() == null ? mess.getCollect() : mess.getCollect() + apArticle.getCollection());
apArticle.setCollection(collection);
}
updateById(apArticle);
//3 计算文章分值
Integer score = computeScore(apArticle);
// 如果是今天发布的文章,热度*3
String publishStr = DateUtils.dateToString(apArticle.getPublishTime());
String nowStr = DateUtils.dateToString(new Date());
if (publishStr.equals(nowStr)){
score = score*3; //当天热点数据 *3
}
//4 更新缓存(频道)
updateArticleCache(apArticle, score, ArticleConstants.HOT_ARTICLE_FIRST_PAGE + apArticle.getChannelId());
//5 更新推荐列表的缓存
updateArticleCache(apArticle, score, ArticleConstants.HOT_ARTICLE_FIRST_PAGE+ ArticleConstants.DEFAULT_TAG);
log.info("updateApArticle is success");
}
/**
* 更新文章缓存
* @param apArticle 当前文章
* @param score 分数
* @param cacheKey
*/
private void updateArticleCache(ApArticle apArticle, Integer score, String cacheKey) {
log.info("updateApArticle updateArticleCache apArticle:{},score:{}",apArticle,score);
boolean flag = false;
String hotArticleListJson = redisTemplate.opsForValue().get(cacheKey);
if (StringUtils.isNotBlank(hotArticleListJson)) {
List<HotArticleVo> hotArticleList = JSONArray.parseArray(hotArticleListJson,HotArticleVo.class);
//1 如果当前缓存中有当前文章,更新分值
for (HotArticleVo hotArticleVo : hotArticleList) {
if (hotArticleVo.getId().equals(apArticle.getId())) {
hotArticleVo.setScore(score);
flag = true;
break;
}
}
//2 缓存中没有当前文章
if (!flag) {
HotArticleVo hotArticle = new HotArticleVo();
BeanUtils.copyProperties(apArticle, hotArticle);
hotArticle.setScore(score);
hotArticleList.add(hotArticle);
}
//3. 将热点文章集合 按得分降序排序 取前30条缓存至redis中
hotArticleList = hotArticleList.stream()
.sorted(Comparator.comparing(HotArticleVo::getScore).reversed())
.limit(30)
.collect(Collectors.toList());
log.info("updateApArticle updateArticleCache success");
redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(hotArticleList));
}
}
/**
* 2.1计算文章分值算法
* @param apArticle
* @return
*/
private Integer computeScore(ApArticle apArticle) {
int score = 0;
// 阅读 1
if (apArticle.getViews() != null) {
score += apArticle.getViews() * ArticleConstants.HOT_ARTICLE_VIEW_WEIGHT;
}
// 点赞 3
if (apArticle.getLikes() != null) {
score += apArticle.getLikes() * ArticleConstants.HOT_ARTICLE_LIKE_WEIGHT;
}
// 评论 5
if (apArticle.getComment() != null) {
score += apArticle.getComment() * ArticleConstants.HOT_ARTICLE_COMMENT_WEIGHT;
}
// 收藏 8
if (apArticle.getCollection() != null) {
score += apArticle.getCollection() * ArticleConstants.HOT_ARTICLE_COLLECTION_WEIGHT;
}
return score;
}</span></span>
③ 测试,启动服务
3.3.3 用户查询热文章接口改造
1)在ApArticleService中新增方法
<span style="background-color:#333333"><span style="color:#b8bfc6">/**
* 根据参数加载文章列表 v2
* @param loadtypeLoadMore
* @param dto
* @param firstPage
* @return
*/
public ResponseResult load2(Short loadtypeLoadMore, ArticleHomeDto dto,boolean firstPage);</span></span>
实现类:
<span style="background-color:#333333"><span style="color:#b8bfc6"> /**
* 根据参数加载文章列表 v2
*
* @param loadtypeLoadMore
* @param dto
* @param firstPage
* @return
*/
@Override
public ResponseResult load2(Short loadtypeLoadMore, ArticleHomeDto dto, boolean firstPage) {
if(firstPage){
String jsonStr = (String) redisTemplate.opsForValue().get(ArticleConstants.HOT_ARTICLE_FIRST_PAGE + dto.getTag());
if(StringUtils.isNotBlank(jsonStr)){
List<HotArticleVo> hotArticleVoList = JSON.parseArray(jsonStr, HotArticleVo.class);
if(!hotArticleVoList.isEmpty()&& hotArticleVoList.size() > 0){
ResponseResult responseResult = ResponseResult.okResult(hotArticleVoList);
responseResult.setHost(webSite);
return responseResult;
}
}
}
return load(loadtypeLoadMore,dto);
}</span></span>
2)定义v2控制器
<span style="background-color:#333333"><span style="color:#b8bfc6">package com.heima.article.controller.v1;
import com.heima.api.article.ArticleHomeControllerApi;
import com.heima.article.service.ApArticleService;
import com.heima.common.constants.article.ArticleConstans;
import com.heima.model.article.dtos.ArticleHomeDto;
import com.heima.model.common.dtos.ResponseResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/v1/article")
public class ArticleHomeController implements ArticleHomeControllerApi {
@Autowired
private ApArticleService articleService;
@PostMapping("/load")
@Override
public ResponseResult load(@RequestBody ArticleHomeDto dto) {
// return articleService.load(dto, ArticleConstans.LOADTYPE_LOAD_MORE);
return articleService.load2(dto,ArticleConstans.LOADTYPE_LOAD_MORE,true);
}
=====================略============================================
}</span></span>
第十四章 项目部署_持续集成
学习目标
-
能够理解什么是持续集成
-
能够完成jenkins环境的搭建
-
能够完成jenkins插件的安装及配置
-
能够理解黑马头条的部署架构
-
能够完成黑马头条的项目部署
1 单架构部署方案
1.1 部署流程
传统方案
基于docker
2 持续集成&持续部署方案
随着软件开发复杂度的不断提高,团队开发成员间如何更好地协同工作以确保软件 开发的质量已经慢慢成为开发过程中不可回避的问题。互联网软件的开发和发布,已经形成了一套标准流程。
如: 在互联网企业中,每时每刻都有需求的变更,bug的修复, 为了将改动及时更新到生产服务器上,下面的图片我们需要每天执行N多次,开发人员完整代码自测后提交到git,然后需要将git中最新的代码生成镜像并部署到测试服务器,如果测试通过了还需要将最新代码部署到生产服务器。如果采用手动方式操作,那将会浪费大量的时间浪费在运维部署方面。
现在的互联网企业,基本都会采用以下方案解决:
持续集成(Continuous integration,简称 CI)。
持续部署(continuous deployment, 简称 CD)
2.1 持续集成
持续集成 (Continuous integration,简称 CI) 指的是,频繁地(一天多次)将代码集成到主干。
它的好处主要有两个。
1、快速发现错误。每完成一点更新,就集成到主干,可以快速发现错误,定位错误也比较容易。
2、防止分支大幅偏离主干。如果不是经常集成,主干又在不断更新,会导致以后集成的难度变大,甚至难以集成。
持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量。它的核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。
Martin Fowler 说过,”持续集成并不能消除 Bug,而是让它们非常容易发现和改正。”
与持续集成相关的,还有两个概念,分别是持续交付和持续部署。
2.2 持续部署
持续部署(continuous deployment)是持续交付的下一步,指的是代码通过评审以后,自动部署到生产环境。
持续部署的目标是,代码在任何时刻都是可部署的,可以进入生产阶段。
持续部署的前提是能自动化完成测试、构建、部署等步骤。
2.3 流程说明
为了保证团队开发人员提交代码的质量,减轻了软件发布时的压力; 持续集成中的任何一个环节都是自动完成的,无需太多的人工干预,有利于减少重复 过程以节省时间、费用和工作量;接下来我们会演示一套基本的自动化持续集成和持续部署方案,来帮助大家理解互联网企业的软件部署方案。
计划如下:
1. 开发人员将代码提交到 git 指定分支 如: dev 2. git仓库触发push事件,发送webhooks通知到持续集成软件 3. 持续集成软件触发构建任务,对dev分支的代码进行构建、编译、单元测试 4. 如果构建失败,发送邮件提醒代码提交人员或管理员 5. 如果构建成功,最新代码将会被构建Docker镜像并上传到注册中心 6. 构建成功触发webhooks通知容器编排软件,进行服务升级 7. 容器编排软件,触发对应的服务升级任务, 将创建对应服务的新容器替换之前的容器 8. 完成最新代码的自动构建与自动部署,全程无工作人员干预
2.4 jenkins安装部署
2.4.1 Jenkins介绍
Jenkins 是一款流行的开源持续集成(Continuous Integration)工具,广泛用于项目开发,具有自动化构建、测试和部署等功能。官网: Jenkins。
Jenkins的特征:
-
开源的 Java语言开发持续集成工具,支持持续集成,持续部署。
-
易于安装部署配置:可通过 yum安装,或下载war包以及通过docker容器等快速实现安装部署,可方便web界面配置管理。
-
消息通知及测试报告:集成 RSS/E-mail通过RSS发布构建结果或当构建完成时通过e-mail通知,生成JUnit/TestNG测试报告。
-
分布式构建:支持 Jenkins能够让多台计算机一起构建/测试。
-
文件识别: Jenkins能够跟踪哪次构建生成哪些jar,哪次构建使用哪个版本的jar等。
-
丰富的插件支持:支持扩展插件,你可以开发适合自己团队使用的工具,如 git,svn,maven,docker等。
Jenkins安装和持续集成环境配置
1 )首先,开发人员每天进行代码提交,提交到Git仓库
2)然后,Jenkins作为持续集成工具,使用Git工具到Git仓库拉取代码到集成服务器,再配合JDK,Maven等软件完成代码编译,代码测试与审查,测试,打包等工作,在这个过程中每一步出错,都重新再执行一次整个流程。
3)最后,Jenkins把生成的jar或war包分发到测试服务器或者生产服务器,测试人员或用户就可以访问应用。
2.4.2 Jenkins环境搭建
可以导入资料中的镜像:
服务器 IP:192.168.200.100 用户名:root 密码:itcast
jenkins 用户名:itcast 密码:itcast
-
采用YUM方式安装
加入jenkins安装源:
sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo --no-check-certificate sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
执行yum命令安装:
yum -y install jenkins
-
采用RPM安装包方式
wget https://pkg.jenkins.io/redhat-stable/jenkins-2.222.3-1.1.noarch.rpm
执行安装:
rpm -ivh jenkins-2.249-1.1.noarch.rpm
-
配置:
修改配置文件:
vi /etc/sysconfig/jenkins
修改内容:
# 修改为对应的目标用户, 这里使用的是root $JENKINS_USER="root" # 服务监听端口 JENKINS_PORT="16060"
目录权限:
chown -R root:root /var/lib/jenkins chown -R root:root /var/cache/jenkins chown -R root:root /var/log/jenkins
重启:
systemctl restart jenkins
如果启动失败, 出现错误信息:
Starting Jenkins bash: /usr/bin/java: No such file or directory
创建JAVA环境的软链接:
ln -s /usr/local/jdk/bin/java /usr/bin/java
注意: 如果阿里云服务器中未安装JDK 可以直接按下面方式安装
阿里云ECS--CentOS8安装jdk1.8 - 小赵不吃溜溜梅 - 博客园
yum install java-1.8.0-openjdk* -y
管理后台初始化设置
需要输入管理密码, 在以下位置查看:
cat /var/lib/jenkins/secrets/initialAdminPassword
按默认设置,把建议的插件都安装上
这一步等待时间较长, 安装完成之后, 创建管理员用户:
配置访问地址:
配置完成之后, 会进行重启, 之后可以看到管理后台:
2.4.3 Jenkins插件安装
在实现持续集成之前, 需要确保以下插件安装成功。
-
Maven Integration plugin: Maven 集成管理插件。
-
Docker plugin: Docker集成插件。
-
GitLab Plugin: GitLab集成插件。
-
git Plugin: git集成插件
-
Publish Over SSH:远程文件发布插件。
-
SSH: 远程脚本执行插件。
安装方法:
-
进入【系统管理】-【插件管理】
-
点击标签页的【可选插件】
在过滤框中搜索插件名称
-
勾选插件, 点击直接安装即可。
注意,如果没有安装按钮,需要更改配置
在安装插件的高级配置中,修改升级站点的连接为:http://updates.jenkins.io/update-center.json 保存
2.4.3 Maven安装配置
-
下载安装包
-
解压安装包
cd /usr/local unzip -o apache-maven-3.3.9.zip # 不支持unzip yum install -y unzip
-
配置
环境变量配置
vi /etc/profile
增加:
export MAVEN_HOME=/usr/local/apache-maven-3.3.9 export PATH=$PATH:$MAVEN_HOME/bin
如果权限不够,则需要增加当前目录的权限
chmod 777 /usr/local/apache-maven-3.3.9/bin/mvn
修改镜像仓库配置:
vi /usr/local/apache-maven-3.3.9/conf/settings.xml # 让配置生效 source /etc/profile
需要把本机的仓库打包上传到服务器上(不上传会自动下载)
然后指定上传后的仓库配置
2.4.4 Docker安装配置(已安装)
-
更新软件包版本
yum -y update
-
卸载旧版本
yum -y remove docker docker-common docker-selinux docker-engine
-
安装软件依赖包
yum install -y yum-utils device-mapper-persistent-data lvm2
-
设置yum源为阿里云
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
-
安装后查看docker版本
docker -v
-
启动
设置开机启动:
systemctl enable docker
启动docker
systemctl start docker
2.4.5 Docker Registry私有仓库安装配置
对于持续集成环境的配置,Jenkins会发布大量的微服务, 要与多台机器进行交互, 可以采用docker镜像的保存与导出功能结合SSH实现, 但这样交互繁琐,稳定性差, 而且不便管理, 这里我们通过搭建Docker的私有仓库来实现, 这个有点类似GIT仓库, 集中统一管理资源, 由客户端拉取或更新。
-
下载最新Registry镜像
docker pull registry:latest
-
启动Registry镜像服务
docker run -d -p 5000:5000 --name registry -v /usr/local/docker/registry:/var/lib/registry registry:latest
映射5000端口; -v是将Registry内的镜像数据卷与本地文件关联, 便于管理和维护Registry内的数据。
删除/usr/local/docker/registry/docker/registry/v2/repositories 下的数据
-
查看仓库资源
访问地址:http://192.168.200.100:5000/v2/_catalog
启动正常, 可以看到返回:
{"repositories":[]}
目前并没有上传镜像, 显示空数据。
如果上传成功, 可以看到数据:
-
配置Docker客户端
正常生产环境中使用, 要配置HTTPS服务, 确保安全,内部开发或测试集成的局域网环境,可以采用简便的方式, 不做安全控制。
先确保持续集成环境的机器已安装好Docker客户端, 然后做以下修改:
vi /lib/systemd/system/docker.service
修改内容:
ExecStart=/usr/bin/dockerd --insecure-registry 192.168.200.100:5000
指向安装Registry的服务IP与端口。
重启生效:
systemctl daemon-reolad systemctl restart docker.service
2.5 持续集成生产实践配置
-
进入【系统管理】--> 【全局工具配置】
-
MAVEN配置全局设置
-
指定JDK配置
-
指定MAVEN 目录
-
指定DOCKER目录
如果不清楚docker的安装的目录,可以使用
whereis docker
命令查看docker的安装的目录如果 阿里云服务器中的git配置报红:
yum -y install git
-
设置远程应用服务主机
添加凭证:
新增凭证,输入用户名和密码保存即可
进入【系统管理】-【系统设置】
输入主机名称和登陆信息, 点击【check connections】验证, 如果成功, 会显示“Successfull connection”。
3 黑马头条项目部署演示
黑马头条涉及前端后端服务众多,设备性能有限 所以我们简化下部署步骤, 基于docker + docker-compose方式快速部署,先来了解下黑马头条的部署架构
-
nginx作为接入层 所有请求全部通过nginx进入 (部署在100服务 端口80)
-
前端工程app、admin、wemedia全部部署在nginx中
-
nginx通过反向代理将对微服务的请求,代理到网关
-
网关根据路由规则,将请求转发到下面的微服务
-
所有微服务都会注册到nacos注册中心
-
所有微服务都会将配置存储到nacos中进行统一配置
-
网关服务及所有微服务部署在 100 服务器 jenkins 微服务
-
所有依赖的软件部署在 131 服务器
3.1 相关软件部署
MAC下或者Windows下的Docker自带Compose功能,无需安装。
Linux下需要通过命令安装:
# 安装 curl -L https://github.com/docker/compose/releases/download/1.24.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose # 上传资料中的 docker-compose 文件到 /usr/local/bin/ # 修改权限 chmod +x /usr/local/bin/docker-compose ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
黑马头条相关软件 一键脚本
# 涉及软件 容器启动脚本 # 将 当前文件夹内容拷贝到有docker的虚拟机 # 通过docker命令即可启动所有软件 version: '3' services: mysql: image: mysql:5.7 ports: - "3306:3306" volumes: - "/root/mysql/conf:/etc/mysql/conf.d" - "/root/mysql/logs:/logs" - "/root/mysql/data:/var/lib/mysql" environment: - MYSQL_ROOT_PASSWORD=root restart: always nacos: image: nacos/nacos-server:1.3.2 ports: - "8848:8848" restart: always environment: - MODE=standalone - JVM_XMS=256m - JVM_XMX=256m - JVM_XMN=128m - SPRING_DATASOURCE_PLATFORM=mysql - MYSQL_SERVICE_HOST=mysql - MYSQL_SERVICE_PORT=3306 - MYSQL_SERVICE_USER=root - MYSQL_SERVICE_PASSWORD=root - MYSQL_SERVICE_DB_NAME=nacos_config - NACOS_SERVER_IP=47.100.130.118 depends_on: - mysql seata: image: seataio/seata-server:1.3.0 ports: - "8091:8091" environment: - "SEATA_IP=47.100.130.118" restart: always zookeeper: image: zookeeper:3.4.14 restart: always expose: - 2181 kafka: image: wurstmeister/kafka:2.12-2.3.1 environment: KAFKA_ADVERTISED_HOST_NAME: 47.100.130.118 KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://47.100.130.118:9092 KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092 KAFKA_HEAP_OPTS: "-Xmx256M -Xms256M" ports: - "9092:9092" depends_on: - zookeeper xxljob: image: xuxueli/xxl-job-admin:2.2.0 volumes: - "/tmp:/data/applogs" environment: PARAMS: "--spring.datasource.url=jdbc:mysql://47.100.130.118:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.username=root --spring.datasource.password=root" ports: - "8888:8080" depends_on: - mysql reids: image: redis ports: - "6379:6379" mongo: image: mongo:4.2.5 ports: - "27017:27017" elasticsearch: image: elasticsearch:7.4.2 ports: - "9200:9200" - "9300:9300" environment: - "discovery.type=single-node" - "ES_JAVA_OPTS=-Xms256m -Xmx256m" volumes: - "/usr/share/elasticsearch/plugins:/usr/share/elasticsearch/plugins" kibana: image: kibana:7.4.2 links: - elasticsearch environment: - "ELASTICSEARCH_URL=http://elasticsearch:9200" ports: - "5601:5601" depends_on: - elasticsearch
使用步骤
-
运行所有容器:
# 运行 docker-compose up -d # 停止 docker-compose stop # 停止并删除容器 docker-compose down # 查看日志 docker-compose logs -f [service...] # 查看命令 docker-compose --help
-
导入sql语句
导入微服务相关数据库 导入xxljob使用数据库 导入nacos配置中心数据库 导入seata分布式事务数据库
-
安装es 中文ik分词器
把资料中的 elasticsearch-analysis-ik-7.4.2.zip 上传到服务器上,放到对应目录(plugins)解压 #切换目录 cd /usr/share/elasticsearch/plugins #新建目录 mkdir analysis-ik cd analysis-ik #root根目录中拷贝文件 mv elasticsearch-analysis-ik-7.4.2.zip /usr/share/elasticsearch/plugins/analysis-ik #解压文件 cd /usr/share/elasticsearch/plugins/analysis-ik unzip elasticsearch-analysis-ik-7.4.2.zip
-
创建es索引库
PUT app_info_article { "mappings":{ "properties":{ "id":{ "type":"long" }, "publishTime":{ "type":"date" }, "layout":{ "type":"integer" }, "images":{ "type":"keyword" }, "authorId": { "type": "long" }, "title":{ "type":"text", "analyzer":"ik_smart" } } } }
3.2 nacos统一配置管理
在需要运行的微服务中
注意: 下面两种配置没有整合 需要单独修改
seata 起步依赖中 file.conf 中的seata服务端连接地址需要修改
comment微服务中 redission的配置需要修改
加入nacos配置依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
注释掉application.yml配置文件,创建bootstrap.yml配置文件
spring: application: name: leadnews-admin # 微服务名称 profiles: active: dev # 配置环境 cloud: nacos: discovery: server-addr: 192.168.200.129:8848 # 注册中心 config: file-extension: yml # 配置文件后缀 server-addr: 192.168.200.129:8848 # 配置中心
将application.yml中的配置文件内容复制到nacos中
配置文件是可导入导出的,导入资料中的nacos_config配置压缩包
然后根据自己的配置情况进行修改
3.3 微服务持续部署
每个微服务使用的dockerfile的方式进行构建镜像后创建容器,需要在每个微服务中添加docker相关的配置
(1)修改每个微服务的pom文件,添加dockerfile的插件
<properties> <docker.image>docker_storage</docker.image> </properties> <build> <finalName>heima-leadnews-wemedia</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.7.0</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> </configuration> </plugin> <plugin> <groupId>com.spotify</groupId> <artifactId>dockerfile-maven-plugin</artifactId> <version>1.3.6</version> <configuration> <repository>${docker.image}/${project.build.finalName}</repository> <buildArgs> <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE> </buildArgs> </configuration> </plugin> </plugins> </build>
(2)在每个微服务的根目录下创建Dockerfile文件,如下:
# 设置JAVA版本 FROM java:8 # 指定存储卷, 任何向/tmp写入的信息都不会记录到容器存储层 VOLUME /tmp # 拷贝运行JAR包 ARG JAR_FILE COPY ${JAR_FILE} app.jar # 设置JVM运行参数, 这里限定下内存大小,减少开销 ENV JAVA_OPTS="\ -server \ -Xms256m \ -Xmx512m \ -XX:MetaspaceSize=256m \ -XX:MaxMetaspaceSize=512m" # 入口点, 执行JAVA运行命令 ENTRYPOINT java ${JAVA_OPTS} -jar /app.jar
3.3.1 基础依赖打包配置
在微服务运行之前需要在本地仓库中先去install所依赖的jar包,所以第一步应该是从git中拉取代码,并且把基础的依赖部分安装到仓库中
(1)新创建一个item,起名为heima-leadnews
(2)配置当前heima-leadnews
-
描述项目
-
源码管理:
选中git,输入git的仓库地址(前提条件,需要把代码上传到gitee仓库中),最后输入gitee的用户名和密码
如果没有配置Credentials,可以选择添加,然后输入用户名密码即可 (公开仓库无需密码)
-
其中构建触发器与构建环境暂不设置
-
设置构建配置
选择
Invoke top-level Maven targets
maven版本:就是之前在jenkins中配置的maven
目标:输入maven的命令 clean install -Dmaven.test.skip=true
跳过测试安装
(3)启动项目
创建完成以后可以在主页上看到这个item
启动项目:点击刚才创建的项目,然后Build Now
在左侧可以查看构建的进度:
点进去以后,可以查看构建的日志信息
构建的过程中,会不断的输入日志信息,如果报错也会提示错误信息
jenkins会先从git仓库中拉取代码,然后执行maven的install命令,把代码安装到本地仓库中
最终如果是success则为构建成功
3.3.2 微服务打包配置
(1)新建item,以heima-leadnews-admin微服务为例
(2)配置
-
概述
-
源码管理
-
构建
配置maven
执行maven命令:
clean install -Dmaven.test.skip=true -P dev dockerfile:build -f heima-leadnews-services/admin-service/pom.xml
注意目录接口, maven命令要找到pom.xml的位置
-Dmaven.test.skip=true 跳过测试
-P prod 指定环境为生成环境
dockerfile:build 启动dockerfile插件构建容器
-f heima-leadnews-admin/pom.xml 指定需要构建的文件(必须是pom)
执行shell命令
if [ -n "$(docker ps -a -f name=heima-$JOB_NAME --format '{{.ID}}' )" ] then #删除之前的容器 docker rm -f $(docker ps -a -f name=heima-$JOB_NAME --format '{{.ID}}' ) fi # 清理镜像 docker image prune -f # 启动docker服务 docker run -d --net=host --name heima-$JOB_NAME docker_storage/heima-$JOB_NAME
这里不是只单纯的启动服务, 我们要考虑每次构建, 都会产生镜像, 所以要先做检查清理, 然后再启动服务。
Docker有五种网络连接模式, 因为我们不是所有服务都采用docker构建, 中间件服务部署在宿主机上面, 这里我们采用host模式, 这样docker容器和主机服务之间就是互通的。
-
bridge模式
使用命令: --net=bridge, 这是dokcer网络的默认设置,为容器创建独立的网络命名空间,容器具有独立的网卡等所有单独的网络栈,这是默认模式。
-
host模式
使用命令: --net=host,直接使用容器宿主机的网络命名空间, 即没有独立的网络环境。它使用宿主机的ip和端口。
-
none模式
命令: --net=none, 为容器创建独立网络命名空间, 这个模式下,dokcer不为容器进行任何网络配置。需要我们自己为容器添加网卡,配置IP。
-
container模式
命令: --net=container:NAME_or_ID, 与host模式类似, 这个模式就是指定一个已有的容器,共享该容器的IP和端口。
-
自定义模式
docker 1.9版本以后新增的特性,允许容器使用第三方的网络实现或者创建单独的bridge网络,提供网络隔离能力。
到此就配置完毕了,保存即可
(3)启动该项目 Build Now
-
首先从git中拉取代码
-
编译打包项目
-
构建镜像
-
创建容器
-
删除多余的镜像
可以从服务器中查看镜像
容器也已创建完毕
可以使用postman测试测试该服务接口
3.3.2 构建其他微服务
可以参考admin微服务创建其他微服务,每个项目可能会有不同的maven构建命令,请按照实际需求配置
-
heima-leadnews-admin-gateway微服务的配置:
maven命令: clean install -Dmaven.test.skip=true dockerfile:build -f heima-leadnews-gateways/admin-gateway/pom.xml
heima-leadnews-user微服务的配置:
maven命令: clean install -Dmaven.test.skip=true dockerfile:build -f heima-leadnews-services/user-service/pom.xml
所有项目构建完成以后,在本地启动admin前端工程,修改configs中的网关地址为:192.168.200.100
,进行效果测试
同样方式配置其它微服务
3.4 接入层及前端部署
3.4.1 接入层nginx搭建
官方网站下载 nginx:nginx,也可以使用资料中的安装包,版本为:nginx-1.18.0
安装依赖
-
需要安装 gcc 的环境
yum install gcc-c++
-
第三方的开发包。
-
PCRE(Perl Compatible Regular Expressions)是一个 Perl 库,包括 perl 兼容的正则表达式库。nginx 的 http 模块使用 pcre 来解析正则表达式,所以需要在 linux 上安装 pcre 库。
yum install -y pcre pcre-devel
注:pcre-devel 是使用 pcre 开发的一个二次开发库。nginx 也需要此库。
-
zlib 库提供了很多种压缩和解压缩的方式,nginx 使用 zlib 对 http 包的内容进行 gzip,所以需要在 linux 上安装 zlib 库。
yum install -y zlib zlib-devel
-
OpenSSL 是一个强大的安全套接字层密码库,囊括主要的密码算法、常用的密钥和证书封装管理功能及 SSL 协议,并提供丰富的应用程序供测试或其它目的使用。nginx 不仅支持 http 协议,还支持 https(即在 ssl 协议上传输 http),所以需要在 linux安装 openssl 库。
yum install -y openssl openssl-devel
-
Nginx安装
第一步:把 nginx 的源码包nginx-1.18.0.tar.gz上传到 linux 系统
第二步:解压缩
tar -zxvf nginx-1.18.0.tar.gz
第三步:进入nginx-1.18.0目录 使用 configure 命令创建一 makeFile 文件。
./configure \ --prefix=/usr/local/nginx \ --pid-path=/var/run/nginx/nginx.pid \ --lock-path=/var/lock/nginx.lock \ --error-log-path=/var/log/nginx/error.log \ --http-log-path=/var/log/nginx/access.log \ --with-http_gzip_static_module \ --http-client-body-temp-path=/var/temp/nginx/client \ --http-proxy-temp-path=/var/temp/nginx/proxy \ --http-fastcgi-temp-path=/var/temp/nginx/fastcgi \ --http-uwsgi-temp-path=/var/temp/nginx/uwsgi \ --http-scgi-temp-path=/var/temp/nginx/scgi
执行后可以看到Makefile文件
第四步:编译
make
第五步:安装
make install
第六步:启动
注意:启动nginx 之前,上边将临时文件目录指定为/var/temp/nginx/client, 需要在/var 下创建此 目录
mkdir /var/temp/nginx/client -p
进入到Nginx目录下的sbin目录
cd /usr/local/nginx/sbin
输入命令启动Nginx
./nginx
启动后查看进程
ps aux|grep nginx
3.4.2 发布前端工程
前端在开发时,是基于node环境在本地开发,引用了非常多的基于node的js 在开发完毕后也许要发布,webpack依赖就是用于发布打包的,它会将很多依赖的js进行整合,最终打包成 html css js 这三种格式的文件,我们把发布后的静态文件拷贝到nginx管理的文件夹中,即可完成部署
# 创建目录 用于存放对应的前端静态资源 mkdir -p /root/workspace/admin mkdir -p /root/workspace/web mkdir -p /root/workspace/wemedia
admin前端工程发布
在admin工程下,打开cmd 输入: npm run build
进行发布
发布后的静态文件,会存放到dist文件夹中
把dist文件夹上传到服务器上,拷贝到150虚拟机的/root/workspace/admin目录中
wemedia前端工程发布
在wemedia工程下,打开cmd 输入: npm run build
进行发布
发布后的静态文件,会存放到dist文件夹中
把dist文件夹上传到服务器上,拷贝到150虚拟机的/root/workspace/wemedia目录中
app前端工程发布
前端工程比较特殊,因为使用了被称为三端合一的weex框架,也就是说它即可以发布android端,也可以发布ios端,也可以发布web端。命令会有区别
在app工程下,打开cmd 输入: npm run clean:web && npm run build:prod:web
进行发布web端
小贴士: 其它端需要安装对应软件才能发布,比如android需要有android studio npm run pack:android 发布安卓 npm run pack:ios 发布ios
把releases文件夹下的web文件夹上传到服务器上,拷贝到150虚拟机的/root/workspace/目录中
3.4.3 nginx配置前端工程访问
对于不同的前端工程 , 我们会通过不同的域名来访问, 先给三个前端工程准备3个访问域名
-
使用type下载hosts插件
-
配置3个域名:
-
47.100.130.118 admin.leadnews.com 运营端
-
47.100.130.118 wemedia.leadnews.com 媒体端
-
47.100.130.118 web.leadnews.com app端
小贴士: 如果想部署外网访问的项目,可以使用内网穿透 准备三个外网地址 全部映射到 47.100.130.118 的 80 端口 下面的nginx也使用对应的外网地址
打开linux的目录:/usr/local/nginx/conf
编辑nginx.conf文件,替换如下:
user root; worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; # 反向代理配置 代理admin gateway upstream heima-admin-gateway{ server 192.168.200.100:6001; } # 反向代理配置 代理wemedia gateway upstream heima-wemedia-gateway{ server 192.168.200.100:6002; } # 反向代理配置 代理app gateway upstream heima-app-gateway{ server 192.168.200.100:5001; } server { listen 80; server_name localhost; location / { root /usr/local/nginx/html; index index.html ; } } server { listen 80; server_name hmttapp.cn.utools.club; location / { root /root/workspace/web; index index.html ; } location ~/app/(.*) { proxy_pass http://heima-app-gateway/$1; proxy_set_header HOST $host; proxy_pass_request_body on; proxy_pass_request_headers on; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } server { listen 80; server_name hmttadmin.cn.utools.club; location / { root /root/workspace/admin/dist; index index.html ; } location ~/service_6001/(.*) { proxy_pass http://heima-admin-gateway/$1; proxy_set_header HOST $host; proxy_pass_request_body on; proxy_pass_request_headers on; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } server { listen 80; server_name hmttwemedia.cn.utools.club; location / { root /root/workspace/wemedia/dist; index index.html ; } location ~/wemedia/MEDIA/(.*) { proxy_pass http://heima-wemedia-gateway/$1; proxy_set_header HOST $host; proxy_pass_request_body on; proxy_pass_request_headers on; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } }
配置完毕后,重启nginx
命令: /usr/local/nginx/sbin/nginx -s reload
输入网址访问前端工程:
3.5 前后端联调测试
访问前端工程,测试各类功能,完成项目部署
3.6 自动通知jenkins触发任务
主流的git软件都提供了webhooks功能(web钩子), 通俗点说就是git在发生某些事件的时候可以通过POST请求调用我们指定的URL路径,那在这个案例中,我们可以在push事件上指定jenkins的任务通知路径。
3.6.1 jenkins配置Gitee插件
jenkins下载webhooks插件
gitee插件介绍: Jenkins 插件 - Gitee.com
jenkins也支持通过url路径来启动任务,具体设置方法:
jenkins的默认下载中仅下载了github的通知触发,我们需要先下载一个插件
(1) 下载gitee插件
系统管理-->插件管理-->可选插件-->搜索 Gitee
下载-->重启jenkins
(2) gitee生成访问令牌
首先,去下面网址生成gitee访问令牌
https://gitee.com/profile/personal_access_tokens
添加令牌描述,提交,弹出框输入密码
复制令牌
(3) jenkins中配置Gitee
系统管理 --> 系统配置 --> Gitee配置
-
链接名: gitee
-
令牌: Gitee Api 令牌 (需要点击添加按下图配置)
-
配置好后测试连接
-
测试成功后保存配置
令牌配置:
-
类型选择Gitee API令牌
-
私人令牌: 将码云中生成的令牌复制过来
-
点击添加
3.6.2 修改jenkins构建任务
修改配置接收webhooks通知
任务详情中点击配置来修改任务
点击构建触发器页签,勾选Gitee webhook
生成Gitee Webhook密码
保存好触发路径和webhook密码,到gitee中配置webhook通知
如:
触发路径: http://192.168.200.151:8888/gitee-project/dockerDemo
触发密码: a591baa17f90e094500e0a11b831af9c
3.6.3 Gitee添加webhooks通知
gitee仓库配置webhooks通知
点击仓库页面的管理
添加webhook
-
点击webhooks菜单,然后点击添加
-
配置jenkins通知地址
-
填写密码
-
点击添加
但在点击添加时,提示失败 gitee中需要配置一个公有IP或域名,这里我们可以通过内网穿透来解决
这个时候需要使用内网穿透来映射本地的ip和端口号
在gitee中将上面的外网地址替换之前的ip和端口部分,再次添加
3.6.4 测试自动构建
添加完毕后测试一下:
提交leadnews-admin的代码测试是否自动触发了jenkins中的构建任务
基于阿里云ECS服务器实战部署
1 ECS服务器准备
1.1 ECS服务器购买
购买地址: 阿里云ECS
1 选择配置
2 选择服务和对应的操作系统
3 选择网络带宽 5M
默认5m即可,当然想更快可以设置大一些,但流量是单独收费的
4 直接点击确认订单
5 点击创建实例
点击管理控制台进入服务器管理界面如下:
其中:
公网IP: 47.103.2.34.130
私网IP: 172.20.170.76
收到短信: 发送服务器的 密码
重置密码:
接收短信后,立即重启生效.
1.2 客户端工具连接
推荐使用 finalshell 连接.
1 打开 finalshell 创建 ssh 连接
2 输入 阿里云的 用户名(root) 和 设置的新密码
1.3 安全组设置
在云服务器中,只有配置了安全规则的端口才允许被外界访问
一般默认 开启: 80 (http) 443 (https) 22 (ssh远程连接) 3389 (windows远程连接)
那如果你安装了mysql 端口是3306 ,那么外界是无法直接访问到的,需要配置一下规则
在入方向中配置安全规则: ( 用户 访问 --> 阿里云)
如果配置端口范围: 3306/3306 那就是允许3306端口访问
但是我们的软件很多, 可以通过 1/65535
范围 来开放所有的端口访问(不安全,学习阶段这么搞)
2 基础环境配置
2.1 配置docker环境
(1)yum 包更新到最新
sudo yum update -y
(2)安装需要的软件包, yum-util 提供yum-config-manager功能,另外两个是devicemapper驱动依赖的
sudo yum install -y yum-utils device-mapper-persistent-data lvm2
(3)设置yum源为阿里云
sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
(4)安装docker
sudo yum -y install docker-ce
(5)安装后查看docker版本
docker -v
(6)启动docker
systemctl start docker #设置开机自启 systemctl enable docker
(7)阿里云镜像加速
阿里云开设了一个容器开发平台
需要注册一个属于自己的阿里云账户,登录后进入管理中心
针对Docker客户端版本大于 3.2.10.0 的用户
您可以通过修改daemon配置文件/etc/docker/daemon.json来使用加速器
sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": ["https://hf23ud62.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-reload sudo systemctl restart docker
观察镜像是否生效:
docker info
2.2 配置jdk环境
云服务可以直接使用下面命令 一键安装jdk1.8
yum install java-1.8.0-openjdk* -y
2.3 配置maven环境
-
下载安装包
-
解压安装包
mkdir -p /usr/local/maven cd /usr/local/maven # 下载unzip命令 yum install -y unzip # 将压缩包上传至 /usr/local/maven下 解压 unzip -o apache-maven-3.3.9.zip
-
配置
环境变量配置
vi /etc/profile
增加:
export MAVEN_HOME=/usr/local/maven/apache-maven-3.3.9 export PATH=$PATH:$MAVEN_HOME/bin
如果权限不够,则需要增加当前目录的权限
chmod 777 /usr/local/maven/apache-maven-3.3.9/bin/mvn
修改镜像仓库配置:
# 修改maven镜像中心为 阿里云镜像中心 (不用配置以改好) vi /usr/local/maven/apache-maven-3.3.9/conf/settings.xml # 让配置生效 source /etc/profile # 测试是否安装成功 mvn -v
2.4 配置git环境
# 安装git客户端 yum -y install git
3. 准备持续集成软件jenkins
3.1 Jenkins环境搭建
-
采用YUM方式安装
加入jenkins安装源:
sudo wget -O /etc/yum.repos.d/jenkins.repo https://pkg.jenkins.io/redhat-stable/jenkins.repo --no-check-certificate sudo rpm --import https://pkg.jenkins.io/redhat-stable/jenkins.io.key
执行yum命令安装:
yum -y install jenkins
-
采用RPM安装包方式(采用)
可以导入资料中jenkins RPM安装包:
wget https://pkg.jenkins.io/redhat-stable/jenkins-2.249-1.1.noarch.rpm
执行安装:
rpm -ivh jenkins-2.249-1.1.noarch.rpm
-
配置:
修改配置文件:
vi /etc/sysconfig/jenkins
修改内容:
# 修改为对应的目标用户, 这里使用的是root $JENKINS_USER="root" # 服务监听端口 JENKINS_PORT="16060"
目录权限:
chown -R root:root /var/lib/jenkins chown -R root:root /var/cache/jenkins chown -R root:root /var/log/jenkins
重启:
systemctl restart jenkins
管理后台初始化设置
http://阿里云外网IP地址:16060/
需要输入管理密码, 在以下位置查看:
cat /var/lib/jenkins/secrets/initialAdminPassword
按默认设置,把建议的插件都安装上
这一步等待时间较长, 安装完成之后, 创建管理员用户:
配置访问地址:
配置完成之后, 会进行重启, 之后可以看到管理后台:
如果下载缓慢,或下载失败:
删除掉 /var/lib/jenkins/updates 下的default.json
将资料中的default.json拷贝进入
3.2 Jenkins插件安装
在实现持续集成之前, 需要确保以下插件安装成功。
-
Maven Integration plugin
: Maven 集成管理插件。(必装) -
Docker plugin
: Docker集成插件。 -
GitLab Plugin
: GitLab集成插件。 -
Git Plugin
: Git 集成插件。(必装) -
Publish Over SSH
:远程文件发布插件。( 可选 ) -
SSH
: 远程脚本执行插件。( 可选 )
安装方法:
-
进入【系统管理】-【插件管理】
-
点击标签页的【可选插件】
在过滤框中搜索插件名称
-
勾选插件, 点击直接安装即可。
注意,如果没有安装按钮,需要更改配置
在安装插件的高级配置中,修改升级站点的连接为:http://updates.jenkins.io/update-center.json 保存
-
安装完毕后重启jenkins
systemctl restart jenkins
3.3 jenkins全局配置
-
进入【系统管理】--> 【全局工具配置】
-
MAVEN配置全局设置
-
指定JDK配置
不用指定, 前面已安装
-
指定MAVEN 目录
点击新增maven 配置name: maven 配置maven地址: /usr/local/maven/apache-maven-3.3.9
-
设置远程应用服务主机(不涉及远程服务器构建,可不做)
添加凭证:
新增凭证,输入用户名和密码保存即可
进入【系统管理】-【系统设置】
输入主机名称和登陆信息, 点击【check connections】验证, 如果成功, 会显示“Successfull connection”。
4 docker-compose安装依赖软件
黑马头条涉及前端后端服务众多,设备性能有限 所以我们简化下部署步骤, 基于docker + docker-compose方式快速部署,先来了解下黑马头条的部署架构
-
nginx作为接入层 所有请求全部通过nginx进入 (部署在100服务 端口80)
-
前端工程app、admin、wemedia全部部署在nginx中
-
nginx通过反向代理将对微服务的请求,代理到网关
-
网关根据路由规则,将请求转发到下面的微服务
-
所有微服务都会注册到nacos注册中心
-
所有微服务都会将配置存储到nacos中进行统一配置
-
网关服务及所有微服务部署在 100 服务器 jenkins 微服务
-
所有依赖的软件部署在 131 服务器
4.1 相关软件部署
MAC下或者Windows下的Docker自带Compose功能,无需安装。
Linux下需要通过命令安装:
# 安装 curl -L https://github.com/docker/compose/releases/download/1.24.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose # * 如果下载慢,可以上传资料中的 docker-compose 文件到 /usr/local/bin/ # 修改权限 chmod +x /usr/local/bin/docker-compose ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
黑马头条相关软件 一键脚本
# 通过docker命令即可启动所有软件 version: '3' services: mysql: image: mysql:5.7 ports: - "3306:3306" volumes: - "/root/mysql/conf:/etc/mysql/conf.d" - "/root/mysql/logs:/logs" - "/root/mysql/data:/var/lib/mysql" environment: - MYSQL_ROOT_PASSWORD=root restart: always nacos: image: nacos/nacos-server:1.3.2 ports: - "8848:8848" restart: always environment: - MODE=standalone - JVM_XMS=256m - JVM_XMX=256m - JVM_XMN=128m - SPRING_DATASOURCE_PLATFORM=mysql - MYSQL_SERVICE_HOST=mysql - MYSQL_SERVICE_PORT=3306 - MYSQL_SERVICE_USER=root - MYSQL_SERVICE_PASSWORD=root - MYSQL_SERVICE_DB_NAME=nacos_config - NACOS_SERVER_IP=47.100.216.65 depends_on: - mysql seata: image: seataio/seata-server:1.3.0 ports: - "8091:8091" environment: - "SEATA_IP=47.100.216.65" restart: always zookeeper: image: zookeeper:3.4.14 restart: always expose: - 2181 kafka: image: wurstmeister/kafka:2.12-2.3.1 environment: KAFKA_ADVERTISED_HOST_NAME: 47.100.216.65 KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181 KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://47.100.216.65:9092 KAFKA_LISTENERS: PLAINTEXT://0.0.0.0:9092 KAFKA_HEAP_OPTS: "-Xmx256M -Xms256M" ports: - "9092:9092" depends_on: - zookeeper restart: always xxljob: image: xuxueli/xxl-job-admin:2.2.0 volumes: - "/tmp:/data/applogs" environment: PARAMS: "--spring.datasource.url=jdbc:mysql://47.100.216.65:3306/xxl_job?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai --spring.datasource.username=root --spring.datasource.password=root" ports: - "8888:8080" depends_on: - mysql restart: always reids: image: redis ports: - "6379:6379" restart: always mongo: image: mongo:4.2.5 ports: - "27017:27017" restart: always elasticsearch: image: elasticsearch:7.4.2 ports: - "9200:9200" - "9300:9300" environment: - "discovery.type=single-node" - "ES_JAVA_OPTS=-Xms256m -Xmx256m" volumes: - "/usr/share/elasticsearch/plugins:/usr/share/elasticsearch/plugins" restart: always kibana: image: kibana:7.4.2 links: - elasticsearch environment: - "ELASTICSEARCH_URL=http://elasticsearch:9200" ports: - "5601:5601" depends_on: - elasticsearch restart: always minio: image: minio/minio:RELEASE.2021-06-14T01-29-23Z ports: - 9090:9000 environment: - "MINIO_ACCESS_KEY=minio" - "MINIO_SECRET_KEY=minio123" volumes: - "/home/data:/data" - "/home/config:/root/.minio" command: server /data restart: always logstash: image: logstash:7.4.2 ports: - 5044:5044 restart: always
使用步骤
-
运行所有容器:
# 运行 docker-compose up -d # 停止 docker-compose stop # 停止并删除容器 docker-compose down # 查看日志 docker-compose logs -f [service...] # 查看命令 docker-compose --help
4.2 相关软件配置
导入sql语句
导入微服务相关数据库 导入xxljob使用数据库 导入nacos配置中心数据库 导入seata分布式事务数据库
安装es 中文ik分词器
把资料中的 elasticsearch-analysis-ik-7.4.2.zip 上传到服务器上,放到对应目录(plugins)解压 #切换目录 cd /usr/share/elasticsearch/plugins #新建目录 mkdir analysis-ik cd analysis-ik #root根目录中拷贝文件 mv elasticsearch-analysis-ik-7.4.2.zip /usr/share/elasticsearch/plugins/analysis-ik #解压文件 cd /usr/share/elasticsearch/plugins/analysis-ik unzip elasticsearch-analysis-ik-7.4.2.zip
创建es索引库
PUT app_info_article { "mappings":{ "properties":{ "id":{ "type":"long" }, "publishTime":{ "type":"date" }, "layout":{ "type":"integer" }, "images":{ "type":"keyword", "index": false }, "staticUrl":{ "type":"keyword", "index": false }, "authorId": { "type": "long" }, "title":{ "type":"text", "analyzer":"ik_smart" } } } }
minio中创建bucket 设置读写权限
将项目minio中的article文件夹下载 并上传到外网服务器的minio中 注意: minIO ==> bucket ==> article ==> plugins ==> js ==> index.js 中对于后台的请求路径需要改为 外网服务器路径
修改seata统一配置
4.3 实现nacos统一配置
将application.yml中的配置文件内容复制到nacos中
配置文件是可导入导出的,导入资料中的nacos_config配置压缩包
然后根据自己的配置情况进行修改
需要在 gateways 和 services 两个父工程中 引入nacos配置中心依赖
<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
修改每个微服务 注释掉原来的 application.yml配置 ,创建 bootstrap.yml 启动配置
spring: cloud: nacos: config: # 配置中心 name - 环境 .yml server-addr: 47.100.216.65:8848 file-extension: yml # 配置文件的后缀 namespace: b6bd13e2-1323-428a-b43e-bb9d5d84a4f2 # 连接指定环境的配置 discovery: # 注册中心 server-addr: 47.100.216.65:8848 application: name: leadnews-admin-gateway profiles: active: dev
5 微服务持续部署
每个微服务使用的dockerfile的方式进行构建镜像后创建容器,需要在每个微服务中添加docker相关的配置
(1)修改每个微服务的pom文件,添加dockerfile的插件
<properties> <docker.image>docker_storage</docker.image> </properties> <build> <finalName>heima-leadnews-wemedia</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <executions> <execution> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.7.0</version> <configuration> <source>${java.version}</source> <target>${java.version}</target> </configuration> </plugin> <plugin> <groupId>com.spotify</groupId> <artifactId>dockerfile-maven-plugin</artifactId> <version>1.3.6</version> <configuration> <repository>${docker.image}/${project.build.finalName}</repository> <buildArgs> <JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE> </buildArgs> </configuration> </plugin> </plugins> </build>
(2)在每个微服务的根目录下创建Dockerfile文件,如下:
# 设置JAVA版本 FROM java:8 # 指定存储卷, 任何向/tmp写入的信息都不会记录到容器存储层 VOLUME /tmp # 拷贝运行JAR包 ARG JAR_FILE COPY ${JAR_FILE} app.jar # 设置JVM运行参数, 这里限定下内存大小,减少开销 ENV JAVA_OPTS="\ -server \ -Xms256m \ -Xmx512m \ -XX:MetaspaceSize=256m \ -XX:MaxMetaspaceSize=512m" # 入口点, 执行JAVA运行命令 ENTRYPOINT java ${JAVA_OPTS} -jar /app.jar
5.1 基础依赖打包配置
在微服务运行之前需要在本地仓库中先去install所依赖的jar包,所以第一步应该是从git中拉取代码,并且把基础的依赖部分安装到仓库中
(1)新创建一个item,起名为heima-leadnews
(2)配置当前heima-leadnews
-
描述项目
-
源码管理:
选中git,输入git的仓库地址(前提条件,需要把代码上传到gitee仓库中),最后输入gitee的用户名和密码
如果没有配置Credentials,可以选择添加,然后输入用户名密码即可 (公开仓库无需密码)
jenkins拉取gitlab代码 ssh配置 Jenkins使用SSH的方式从GitLab拉取代码-优快云博客
-
其中构建触发器与构建环境暂不设置
-
设置构建配置
选择
Invoke top-level Maven targets
maven版本:就是之前在jenkins中配置的maven
目标:输入maven的命令 clean install -Dmaven.test.skip=true
跳过测试安装
(3)启动项目
创建完成以后可以在主页上看到这个item
启动项目:点击刚才创建的项目,然后Build Now
在左侧可以查看构建的进度:
点进去以后,可以查看构建的日志信息
构建的过程中,会不断的输入日志信息,如果报错也会提示错误信息
jenkins会先从git仓库中拉取代码,然后执行maven的install命令,把代码安装到本地仓库中
最终如果是success则为构建成功
5.2 微服务打包配置
(1)新建item,以heima-leadnews-admin微服务为例
(2)配置
-
概述
-
源码管理
-
构建
配置maven
执行maven命令:
clean install -Dmaven.test.skip=true -P dev dockerfile:build -f heima-leadnews-services/admin-service/pom.xml
注意目录接口, maven命令要找到pom.xml的位置
-Dmaven.test.skip=true 跳过测试
-P prod 指定环境为生成环境
dockerfile:build 启动dockerfile插件构建容器
-f heima-leadnews-admin/pom.xml 指定需要构建的文件(必须是pom)
执行shell命令
if [ -n "$(docker ps -a -f name=heima-$JOB_NAME --format '{{.ID}}' )" ] then #删除之前的容器 docker rm -f $(docker ps -a -f name=heima-$JOB_NAME --format '{{.ID}}' ) fi # 清理镜像 docker image prune -f # 启动docker服务 docker run -d --net=host --name heima-$JOB_NAME docker_storage/heima-$JOB_NAME
这里不是只单纯的启动服务, 我们要考虑每次构建, 都会产生镜像, 所以要先做检查清理, 然后再启动服务。
Docker有五种网络连接模式, 因为我们不是所有服务都采用docker构建, 中间件服务部署在宿主机上面, 这里我们采用host模式, 这样docker容器和主机服务之间就是互通的。
-
bridge模式
使用命令: --net=bridge, 这是dokcer网络的默认设置,为容器创建独立的网络命名空间,容器具有独立的网卡等所有单独的网络栈,这是默认模式。
-
host模式
使用命令: --net=host,直接使用容器宿主机的网络命名空间, 即没有独立的网络环境。它使用宿主机的ip和端口。
-
none模式
命令: --net=none, 为容器创建独立网络命名空间, 这个模式下,dokcer不为容器进行任何网络配置。需要我们自己为容器添加网卡,配置IP。
-
container模式
命令: --net=container:NAME_or_ID, 与host模式类似, 这个模式就是指定一个已有的容器,共享该容器的IP和端口。
-
自定义模式
docker 1.9版本以后新增的特性,允许容器使用第三方的网络实现或者创建单独的bridge网络,提供网络隔离能力。
到此就配置完毕了,保存即可
(3)启动该项目 Build Now
-
首先从git中拉取代码
-
编译打包项目
-
构建镜像
-
创建容器
-
删除多余的镜像
可以从服务器中查看镜像
容器也已创建完毕
可以使用postman测试测试该服务接口
5.3 构建其他微服务
可以参考admin微服务创建其他微服务,每个项目可能会有不同的maven构建命令,请按照实际需求配置
-
heima-leadnews-admin-gateway微服务的配置:
maven命令: clean install -Dmaven.test.skip=true dockerfile:build -f heima-leadnews-gateways/admin-gateway/pom.xml
heima-leadnews-user微服务的配置:
maven命令: clean install -Dmaven.test.skip=true dockerfile:build -f heima-leadnews-services/user-service/pom.xml
所有项目构建完成以后,在本地启动admin前端工程,修改configs中的网关地址为:192.168.200.100
,进行效果测试
同样方式配置其它微服务
6 接入层及前端部署
6.1 接入层nginx搭建
官方网站下载 nginx:nginx,也可以使用资料中的安装包,版本为:nginx-1.18.0
安装依赖
-
需要安装 gcc 的环境
yum install -y gcc-c++
-
第三方的开发包。
-
PCRE(Perl Compatible Regular Expressions)是一个 Perl 库,包括 perl 兼容的正则表达式库。nginx 的 http 模块使用 pcre 来解析正则表达式,所以需要在 linux 上安装 pcre 库。
yum install -y pcre pcre-devel
注:pcre-devel 是使用 pcre 开发的一个二次开发库。nginx 也需要此库。
-
zlib 库提供了很多种压缩和解压缩的方式,nginx 使用 zlib 对 http 包的内容进行 gzip,所以需要在 linux 上安装 zlib 库。
yum install -y zlib zlib-devel
-
OpenSSL 是一个强大的安全套接字层密码库,囊括主要的密码算法、常用的密钥和证书封装管理功能及 SSL 协议,并提供丰富的应用程序供测试或其它目的使用。nginx 不仅支持 http 协议,还支持 https(即在 ssl 协议上传输 http),所以需要在 linux安装 openssl 库。
yum install -y openssl openssl-devel
-
Nginx安装
第一步:把 nginx 的源码包nginx-1.18.0.tar.gz上传到 linux 系统
第二步:解压缩
tar -zxvf nginx-1.18.0.tar.gz
第三步:进入nginx-1.18.0目录 使用 configure 命令创建一 makeFile 文件。
./configure \ --prefix=/usr/local/nginx \ --pid-path=/var/run/nginx/nginx.pid \ --lock-path=/var/lock/nginx.lock \ --error-log-path=/var/log/nginx/error.log \ --http-log-path=/var/log/nginx/access.log \ --with-http_gzip_static_module \ --http-client-body-temp-path=/var/temp/nginx/client \ --http-proxy-temp-path=/var/temp/nginx/proxy \ --http-fastcgi-temp-path=/var/temp/nginx/fastcgi \ --http-uwsgi-temp-path=/var/temp/nginx/uwsgi \ --http-scgi-temp-path=/var/temp/nginx/scgi
执行后可以看到Makefile文件
第四步:编译
make
第五步:安装
make install
第六步:启动
注意:启动nginx 之前,上边将临时文件目录指定为/var/temp/nginx/client, 需要在/var 下创建此 目录
mkdir /var/temp/nginx/client -p
进入到Nginx目录下的sbin目录
cd /usr/local/nginx/sbin
输入命令启动Nginx
./nginx
启动后查看进程
ps aux|grep nginx
6.2 发布前端工程
前端在开发时,是基于node环境在本地开发,引用了非常多的基于node的js 在开发完毕后也许要发布,webpack依赖就是用于发布打包的,它会将很多依赖的js进行整合,最终打包成 html css js 这三种格式的文件,我们把发布后的静态文件拷贝到nginx管理的文件夹中,即可完成部署
# 创建目录 用于存放对应的前端静态资源 mkdir -p /root/workspace/admin mkdir -p /root/workspace/web mkdir -p /root/workspace/wemedia
admin前端工程发布
在admin工程下,打开cmd 输入: npm run build
进行发布
发布后的静态文件,会存放到dist文件夹中
把dist文件夹上传到服务器上,拷贝到150虚拟机的/root/workspace/admin目录中
wemedia前端工程发布
在wemedia工程下,打开cmd 输入: npm run build
进行发布
发布后的静态文件,会存放到dist文件夹中
把dist文件夹上传到服务器上,拷贝到150虚拟机的/root/workspace/wemedia目录中
app前端工程发布
前端工程比较特殊,因为使用了被称为三端合一的weex框架,也就是说它即可以发布android端,也可以发布ios端,也可以发布web端。命令会有区别
在app工程下,打开cmd 输入: npm run clean:web && npm run build:prod:web
进行发布web端
小贴士: 其它端需要安装对应软件才能发布,比如android需要有android studio npm run pack:android 发布安卓 npm run pack:ios 发布ios
把releases文件夹下的web文件夹上传到服务器上,拷贝到150虚拟机的/root/workspace/目录中
6.3 nginx配置前端工程访问
对于不同的前端工程 , 我们会通过不同的域名来访问, 先给三个前端工程准备3个访问域名
-
使用type下载hosts插件
-
配置3个域名:
-
47.100.216.65 admin.leadnews.com 运营端
-
47.100.216.65 wemedia.leadnews.com 媒体端
-
47.100.216.65 web.leadnews.com app端
小贴士: 如果想部署外网访问的项目,可以使用内网穿透 准备三个外网地址 全部映射到 47.100.216.65 的 80 端口 下面的nginx也使用对应的外网地址
打开linux的目录:/usr/local/nginx/conf
编辑nginx.conf文件,替换如下:
网关地址请按自己实际地址配置
访问三个端的域名,请按自己实际地址配置
user root; worker_processes 1; events { worker_connections 1024; } http { include mime.types; default_type application/octet-stream; # 反向代理配置 代理admin gateway upstream heima-admin-gateway{ server 47.100.216.65:6001; } # 反向代理配置 代理wemedia gateway upstream heima-wemedia-gateway{ server 47.100.216.65:6002; } # 反向代理配置 代理app gateway upstream heima-app-gateway{ server 47.100.216.65:5001; } server { listen 80; server_name localhost; location / { root /usr/local/nginx/html; index index.html ; } } server { listen 80; server_name web1.chenjin.net.cn; location / { root /root/workspace/web; index index.html ; } location ~/app/(.*) { proxy_pass http://heima-app-gateway/$1; proxy_set_header HOST $host; proxy_pass_request_body on; proxy_pass_request_headers on; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } server { listen 80; server_name admin1.chenjin.net.cn; location / { root /root/workspace/admin/dist; index index.html ; } location ~/service_6001/(.*) { proxy_pass http://heima-admin-gateway/$1; proxy_set_header HOST $host; proxy_pass_request_body on; proxy_pass_request_headers on; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } server { listen 80; server_name wemedia1.chenjin.net.cn; location / { root /root/workspace/wemedia/dist; index index.html ; } location ~/wemedia/MEDIA/(.*) { proxy_pass http://heima-wemedia-gateway/$1; proxy_set_header HOST $host; proxy_pass_request_body on; proxy_pass_request_headers on; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } } }
配置完毕后,重启nginx
命令: /usr/local/nginx/sbin/nginx -s reload
输入网址访问前端工程:
7 域名设置与绑定(了解)
7.1 域名购买
在阿里云服务上部署完项目,你的项目就已经正式在互联网上线了 不过目前还是只能通过 外网的IP地址访问
如果要买域名 ==> 阿里云域名交易首页
7.2 域名备案
域名购买完毕后是需要备案的
如果您的网站托管在阿里云中国内地(大陆)节点服务器上,且网站的主办人和域名从未办理过备案,在网站开通服务前,您需通过阿里云ICP代备案系统完成ICP备案。
备案前您需准备备案所需的相关资料,通过PC端或App端进行备案信息填写、资料上传、真实性核验等,备案信息提交后需通过阿里云初审、短信核验和管局审核,整个备案流程预计所需时长约1~22个工作日左右,具体时长以实际操作时间为准。
阿里云ICP备案流程概述_备案(ICP Filing)-阿里云帮助中心
7.3 域名绑定
域名需要和你的外网阿里云IP地址 绑定方可使用
配置域名解析地址==> 阿里云登录 - 欢迎登录阿里云,安全稳定的云计算服务平台
我们使用内网穿透 将地址 映射的 阿里云外网IP即可
8.0 自动通知jenkins触发任务(了解)
主流的git软件都提供了webhooks功能(web钩子), 通俗点说就是git在发生某些事件的时候可以通过POST请求调用我们指定的URL路径,那在这个案例中,我们可以在push事件上指定jenkins的任务通知路径。
8.1 jenkins配置Gitee插件
jenkins下载webhooks插件
gitee插件介绍: Jenkins 插件 - Gitee.com
jenkins也支持通过url路径来启动任务,具体设置方法:
jenkins的默认下载中仅下载了github的通知触发,我们需要先下载一个插件
(1) 下载gitee插件
系统管理-->插件管理-->可选插件-->搜索 Gitee
下载-->重启jenkins
(2) gitee生成访问令牌
首先,去下面网址生成gitee访问令牌
https://gitee.com/profile/personal_access_tokens
添加令牌描述,提交,弹出框输入密码
复制令牌
(3) jenkins中配置Gitee
系统管理 --> 系统配置 --> Gitee配置
-
链接名: gitee
-
令牌: Gitee Api 令牌 (需要点击添加按下图配置)
-
配置好后测试连接
-
测试成功后保存配置
令牌配置:
-
类型选择Gitee API令牌
-
私人令牌: 将码云中生成的令牌复制过来
-
点击添加
8.2 修改jenkins构建任务
修改配置接收webhooks通知
任务详情中点击配置来修改任务
点击构建触发器页签,勾选Gitee webhook
生成Gitee Webhook密码
保存好触发路径和webhook密码,到gitee中配置webhook通知
如:
触发路径: http://192.168.200.151:8888/gitee-project/dockerDemo
触发密码: a591baa17f90e094500e0a11b831af9c
8.3 Gitee添加webhooks通知
gitee仓库配置webhooks通知
点击仓库页面的管理
添加webhook
-
点击webhooks菜单,然后点击添加
-
配置jenkins通知地址
-
填写密码
-
点击添加
但在点击添加时,提示失败 gitee中需要配置一个公有IP或域名,这里我们可以通过内网穿透来解决
这个时候需要使用内网穿透来映射本地的ip和端口号
在gitee中将上面的外网地址替换之前的ip和端口部分,再次添加
8.4 测试自动构建
添加完毕后测试一下:
提交leadnews-admin的代码测试是否自动触发了jenkins中的构建任务