news的day13

1

第十三章 kafkaStream新热文章计算

今日目标

  • 能够理解什么是实时流式计算

  • 能够理解kafkaStream处理实时流式计算的流程

  • 能够完成kafkaStream实时流式计算的入门案例

  • 能够完成app端热点文章计算的功能

  • 能够完成app端文章列表接口的优化改造

1 实时流式计算

1.1 概念

任何类型的数据都是作为事件流产生的。信用卡交易,传感器测量,机器日志或网站或移动应用程序上的用户交互,所有这些数据都作为流生成。 数据可以作为无界或有界流处理。

一般流式计算会与批量计算相比较。在流式计算模型中,输入是持续的,可以认为在时间上是无界的,也就意味着,永远拿不到全量数据去做计算。同时,计算结果是持续输出的,也即计算结果在时间上也是无界的。流式计算一般对实时性要求较高,同时一般是先定义目标计算,然后数据到来之后将计算逻辑应用于数据。同时为了提高计算效率,往往尽可能采用增量计算代替全量计算。

image-20210125144237354

有界流

  • 有定义流的开始,也有定义流的结束。

  • 有界流可以在摄取所有数据后再进行计算。

  • 有界流所有数据可以被排序,所以并不需要有序摄取。

  • 有界流处理通常被称为批处理。

无界流

  • 有定义流的开始,但没有定义流的结束。

  • 它们会无休止地产生数据。

  • 无界流的数据必须持续处理,即数据被摄取后需要立刻处理。我们不能等到所有数据都到达再处理,因为输入是无限的,在任何时候输入都不会完成。

  • 处理无界数据通常要求以特定顺序摄取事件,例如事件发生的顺序,以便能够推断结果的完整性。

1.2 应用场景

  • 日志分析

    网站的用户访问日志进行实时的分析,计算访问量,用户画像,留存率等等,实时的进行数据分析,帮助企业进行决策

  • 大屏看板统计

    可以实时的查看网站注册数量,订单数量,购买数量,金额等。

  • 公交实时数据

    可以随时更新公交车方位,计算多久到达站牌等

  • 实时文章分值计算

    头条类文章的分值计算,通过用户的行为实时文章的分值,分值越高就越被推荐。

1.3 技术方案选型

  • Hadoop

    1588518932145

  • 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主题

1588520036121

2.3 KStream&KTable

(1)数据结构类似于map,如下图,key-value键值对

1588521104765

(2)KStream

KStream数据流(data stream),即是一段顺序的,可以无限长,不断更新的数据集。 数据流中比较常记录的是事件,这些事件可以是一次鼠标点击(click),一次交易,或是传感器记录的位置数据。

特点:每一条消息代表一条不可变的新记录。

(3)KTable

KTable传统数据库,包含了各种存储了大量状态(state)的表格。KTable负责抽象的,就是表状数据。每一次操作,都是更新插入(update)

特点:每条消息代表一个更新,几条key相同的消息会将该key的值更新为最后一条消息的值

对于KStream 和 KTable 区别:

同时给KStreamKTable 发送两条消息: {"key":1}{"key":2}

  • KStream 做 sum 计算: 结果为 {"key":3}

  • KTable 做 sum 计算: 结果为 {"key":2}

2.4 Kafka Stream入门案例编写

需求: 接收kafka消息内容并计算输入消息内单词的个数

image-20210125144853202

如:接收消息—— 数据源

  • 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>

结果:

通过流式计算,会把生产者的多条消息汇总成一条发送到消费者中输出(需要稍等一会)

image-20201213190200856

时间窗口:

Stream流中的数据 是无界的,要想做聚合运算 必须要有时间窗口的概念:

根据时间窗口做聚合,是在实时计算中非常重要的功能。比如我们经常需要统计最近一段时间内的count、sum、avg等统计数据。

Kafka中有这样四种时间窗口

Window nameBehaviorShort description
Tumbling time windowTime-basedFixed-size, non-overlapping, gap-less windows 翻滚时间窗口
Hopping time windowTime-basedFixed-size, overlapping windows 跳跃时间窗口
Sliding time windowTime-basedFixed-size, overlapping windows that work on differences between record timestamps 滑动时间窗口
Session windowSession-basedDynamically-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)

image-20201213195547518

整体实现思路共分为3步

  • 定时计算热点文章

    • 定时任务每天凌晨1点,查询前5天的文章

    • 计算每个文章的分值,其中不同的行为设置不同的权重

      (阅读:1,点赞:3,评论:5,收藏:8)

    • 按照分值排序,给每个频道找出分值较高的30条数据,存入缓存中

      为什么要按照频道缓存?

      在前端工程中的如下代码:

      1602523130349

      这些就是首页的频道信息,其中的id就是与ad_channel表中的id要对应上。

      1602566762256

  • 实时计算热点文章

    • 行为微服务,用户阅读或点赞了某一篇文章(目前实现这两个功能),发送消息给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 单元测试
  1. 需要首先启动admin微服务

  2. 启动Redis服务

  3. 在数据库中准备点数据:把数据库的时间修改为前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>
  1. 可以先使用单元测试调试代码

<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连接工具

1602582360990

新建连接

1602582439544

查看数据

1602582470671

4)定时任务创建

访问:http://192.168.200.129:8888/xxl-job-admin/

① 在xxl-job-admin中新建执行器和任务

新建执行器: leadnews-article-executor

1588663861049

新建任务

1588663967517

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完成测试

image-20201213214636987

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新增发送消息

如图:

1602524058339

完整代码如下:

<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发送消息

如图:

1602524153350

完整代码:

<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端文章行为收发消息聚合计算测试

启动前端和后端服务测试是否可以接收到消息,并且是否可以完成聚合运算

行为微服务:

image-20201214002322168

文章微服务:

image-20201214002425072

3.3.2.4 文章微服务-更新文章分值

文章微服务接收到kafka stream 实时流式处理后的结果消息后,需要做两件事情:

  1. 更新MySQL数据库文章表的相关行为的分值

  2. 更新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 部署流程

传统方案

1613360499457

基于docker

1613360577411

2 持续集成&持续部署方案

随着软件开发复杂度的不断提高,团队开发成员间如何更好地协同工作以确保软件 开发的质量已经慢慢成为开发过程中不可回避的问题。互联网软件的开发和发布,已经形成了一套标准流程。

如: 在互联网企业中,每时每刻都有需求的变更,bug的修复, 为了将改动及时更新到生产服务器上,下面的图片我们需要每天执行N多次,开发人员完整代码自测后提交到git,然后需要将git中最新的代码生成镜像并部署到测试服务器,如果测试通过了还需要将最新代码部署到生产服务器。如果采用手动方式操作,那将会浪费大量的时间浪费在运维部署方面。

1605489947242

现在的互联网企业,基本都会采用以下方案解决:

持续集成(Continuous integration,简称 CI)。

持续部署(continuous deployment, 简称 CD)

2.1 持续集成

持续集成 (Continuous integration,简称 CI) 指的是,频繁地(一天多次)将代码集成到主干。

它的好处主要有两个。

1、快速发现错误。每完成一点更新,就集成到主干,可以快速发现错误,定位错误也比较容易。

2、防止分支大幅偏离主干。如果不是经常集成,主干又在不断更新,会导致以后集成的难度变大,甚至难以集成。

持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量。它的核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。

Martin Fowler 说过,”持续集成并不能消除 Bug,而是让它们非常容易发现和改正。”

与持续集成相关的,还有两个概念,分别是持续交付和持续部署。

2.2 持续部署

持续部署(continuous deployment)是持续交付的下一步,指的是代码通过评审以后,自动部署到生产环境。

持续部署的目标是,代码在任何时刻都是可部署的,可以进入生产阶段。

持续部署的前提是能自动化完成测试、构建、部署等步骤。

2.3 流程说明

为了保证团队开发人员提交代码的质量,减轻了软件发布时的压力; 持续集成中的任何一个环节都是自动完成的,无需太多的人工干预,有利于减少重复 过程以节省时间、费用和工作量;接下来我们会演示一套基本的自动化持续集成和持续部署方案,来帮助大家理解互联网企业的软件部署方案。

计划如下:

1605837774557

1. 开发人员将代码提交到 git 指定分支   如: dev
​
2. git仓库触发push事件,发送webhooks通知到持续集成软件
​
3. 持续集成软件触发构建任务,对dev分支的代码进行构建、编译、单元测试
​
4. 如果构建失败,发送邮件提醒代码提交人员或管理员
​
5. 如果构建成功,最新代码将会被构建Docker镜像并上传到注册中心
​
6. 构建成功触发webhooks通知容器编排软件,进行服务升级
​
7. 容器编排软件,触发对应的服务升级任务, 将创建对应服务的新容器替换之前的容器
​
8. 完成最新代码的自动构建与自动部署,全程无工作人员干预

2.4 jenkins安装部署

2.4.1 Jenkins介绍

1603175880959

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安装和持续集成环境配置

1603176000242

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

  1. 采用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
  2. 采用RPM安装包方式

    Jenkins安装包下载地址

    wget https://pkg.jenkins.io/redhat-stable/jenkins-2.222.3-1.1.noarch.rpm

    执行安装:

    rpm -ivh jenkins-2.249-1.1.noarch.rpm
  3. 配置:

    修改配置文件:

    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

    管理后台初始化设置

    http://192.168.200.100:16060/

    需要输入管理密码, 在以下位置查看:

    cat /var/lib/jenkins/secrets/initialAdminPassword

    1569564399216

    按默认设置,把建议的插件都安装上

    1569564606846

    这一步等待时间较长, 安装完成之后, 创建管理员用户:

    1569564966999

配置访问地址:

1569564989527

配置完成之后, 会进行重启, 之后可以看到管理后台:

1569565238541

2.4.3 Jenkins插件安装

在实现持续集成之前, 需要确保以下插件安装成功。

  • Maven Integration plugin: Maven 集成管理插件。

  • Docker plugin: Docker集成插件。

  • GitLab Plugin: GitLab集成插件。

  • git Plugin: git集成插件

  • Publish Over SSH:远程文件发布插件。

  • SSH: 远程脚本执行插件。

安装方法:

  1. 进入【系统管理】-【插件管理】

  2. 点击标签页的【可选插件】

    在过滤框中搜索插件名称

    1569742624798

  3. 勾选插件, 点击直接安装即可。

注意,如果没有安装按钮,需要更改配置

在安装插件的高级配置中,修改升级站点的连接为:http://updates.jenkins.io/update-center.json 保存

1603521604783

1603521622211

2.4.3 Maven安装配置
  1. 下载安装包

    下载地址: Download Apache Maven – Maven

  2. 解压安装包

    cd /usr/local
    unzip -o apache-maven-3.3.9.zip
    
    # 不支持unzip
    yum install -y unzip
  3. 配置

    环境变量配置

    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
    

    需要把本机的仓库打包上传到服务器上(不上传会自动下载)

    然后指定上传后的仓库配置

    1603521851632

2.4.4 Docker安装配置(已安装)
  1. 更新软件包版本

    yum -y update
  2. 卸载旧版本

    yum -y remove docker  docker-common docker-selinux docker-engine
  3. 安装软件依赖包

    yum install -y yum-utils device-mapper-persistent-data lvm2
  4. 设置yum源为阿里云

    sudo yum-config-manager --add-repo http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
  5. 安装后查看docker版本

    docker -v
  6. 启动

    设置开机启动:

    systemctl enable docker

    启动docker

    systemctl start docker
2.4.5 Docker Registry私有仓库安装配置

对于持续集成环境的配置,Jenkins会发布大量的微服务, 要与多台机器进行交互, 可以采用docker镜像的保存与导出功能结合SSH实现, 但这样交互繁琐,稳定性差, 而且不便管理, 这里我们通过搭建Docker的私有仓库来实现, 这个有点类似GIT仓库, 集中统一管理资源, 由客户端拉取或更新。

  1. 下载最新Registry镜像

    docker pull registry:latest
  2. 启动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 下的数据

  3. 查看仓库资源

    访问地址:http://192.168.200.100:5000/v2/_catalog

    启动正常, 可以看到返回:

    {"repositories":[]}

    目前并没有上传镜像, 显示空数据。

    如果上传成功, 可以看到数据:

    1603522230134

  4. 配置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 持续集成生产实践配置

  1. 进入【系统管理】--> 【全局工具配置】

    1603200359461

  2. MAVEN配置全局设置

    1603200435502

  3. 指定JDK配置

    1603200401796

  1. 指定MAVEN 目录

    1603200472287

  2. 指定DOCKER目录

    1603200520246

    如果不清楚docker的安装的目录,可以使用whereis docker 命令查看docker的安装的目录

    如果 阿里云服务器中的git配置报红: yum -y install git

  3. 设置远程应用服务主机

    添加凭证:

    1603735746602

    新增凭证,输入用户名和密码保存即可

    1603735794051

    进入【系统管理】-【系统设置】

    1603200611899

    输入主机名称和登陆信息, 点击【check connections】验证, 如果成功, 会显示“Successfull connection”。

    1603735668415

3 黑马头条项目部署演示

黑马头条涉及前端后端服务众多,设备性能有限 所以我们简化下部署步骤, 基于docker + docker-compose方式快速部署,先来了解下黑马头条的部署架构

1613375860330

  1. nginx作为接入层 所有请求全部通过nginx进入 (部署在100服务 端口80)

  2. 前端工程app、admin、wemedia全部部署在nginx中

  3. nginx通过反向代理将对微服务的请求,代理到网关

  4. 网关根据路由规则,将请求转发到下面的微服务

  5. 所有微服务都会注册到nacos注册中心

  6. 所有微服务都会将配置存储到nacos中进行统一配置

  7. 网关服务及所有微服务部署在 100 服务器 jenkins 微服务

  8. 所有依赖的软件部署在 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

使用步骤

  1. 运行所有容器:

# 运行
docker-compose up -d

# 停止
docker-compose stop

# 停止并删除容器
docker-compose down

# 查看日志
docker-compose logs -f [service...]

# 查看命令
docker-compose --help
  1. 导入sql语句

导入微服务相关数据库

导入xxljob使用数据库

导入nacos配置中心数据库

导入seata分布式事务数据库
  1. 安装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
  1. 创建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 # 配置中心

1613379539546

将application.yml中的配置文件内容复制到nacos中

1613379700439

配置文件是可导入导出的,导入资料中的nacos_config配置压缩包

然后根据自己的配置情况进行修改

1613379876496

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

1603556179776

3.3.1 基础依赖打包配置

在微服务运行之前需要在本地仓库中先去install所依赖的jar包,所以第一步应该是从git中拉取代码,并且把基础的依赖部分安装到仓库中

(1)新创建一个item,起名为heima-leadnews

1603202842506

1603556912178

(2)配置当前heima-leadnews

  • 描述项目

1603556984260

  • 源码管理:

    选中git,输入git的仓库地址(前提条件,需要把代码上传到gitee仓库中),最后输入gitee的用户名和密码

    如果没有配置Credentials,可以选择添加,然后输入用户名密码即可 (公开仓库无需密码)

1603557100765

  • 其中构建触发器构建环境暂不设置

  • 设置构建配置

    选择Invoke top-level Maven targets

1603557353285

maven版本:就是之前在jenkins中配置的maven

目标:输入maven的命令 clean install -Dmaven.test.skip=true 跳过测试安装

1603557446935

(3)启动项目

创建完成以后可以在主页上看到这个item

1603557661626

启动项目:点击刚才创建的项目,然后Build Now

1603557703328

在左侧可以查看构建的进度:

1603557774990

点进去以后,可以查看构建的日志信息

构建的过程中,会不断的输入日志信息,如果报错也会提示错误信息

1603557824124

jenkins会先从git仓库中拉取代码,然后执行maven的install命令,把代码安装到本地仓库中

最终如果是success则为构建成功

1603557903228

3.3.2 微服务打包配置

(1)新建item,以heima-leadnews-admin微服务为例

1603558664549

(2)配置

  • 概述

1603558714167

  • 源码管理

1603558748960

  • 构建

配置maven

1603558861469

执行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)

image-20210427115048199

1603558973180

执行shell命令

1603558910209

1603559164996

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中拉取代码

  • 编译打包项目

  • 构建镜像

  • 创建容器

  • 删除多余的镜像

可以从服务器中查看镜像

1603559728626

容器也已创建完毕

1603559790306

可以使用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

1603561212541

heima-leadnews-user微服务的配置:

maven命令:

clean install -Dmaven.test.skip=true dockerfile:build -f heima-leadnews-services/user-service/pom.xml

1603561293105

所有项目构建完成以后,在本地启动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

1613376602041

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文件夹中

1613377149886

1613377225038

把dist文件夹上传到服务器上,拷贝到150虚拟机的/root/workspace/admin目录中

wemedia前端工程发布

在wemedia工程下,打开cmd 输入: npm run build 进行发布

发布后的静态文件,会存放到dist文件夹中

1613377649182

把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

1613377967699

把releases文件夹下的web文件夹上传到服务器上,拷贝到150虚拟机的/root/workspace/目录中

3.4.3 nginx配置前端工程访问

对于不同的前端工程 , 我们会通过不同的域名来访问, 先给三个前端工程准备3个访问域名

  1. 使用type下载hosts插件

  2. 配置3个域名:

  3. 47.100.130.118 admin.leadnews.com 运营端

  4. 47.100.130.118 wemedia.leadnews.com 媒体端

  5. 47.100.130.118 web.leadnews.com app端

1613378464091

1613378379712

小贴士: 
如果想部署外网访问的项目,可以使用内网穿透 准备三个外网地址
全部映射到
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

输入网址访问前端工程:

1613378750447

1613378843939

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

1606929689394

(2) gitee生成访问令牌

首先,去下面网址生成gitee访问令牌

https://gitee.com/profile/personal_access_tokens

1606929726997

添加令牌描述,提交,弹出框输入密码

1606929755320

复制令牌

1606929779247

(3) jenkins中配置Gitee

系统管理 --> 系统配置 --> Gitee配置

  1. 链接名: gitee

  2. 域名: Gitee - 基于 Git 的代码托管和研发协作平台

  3. 令牌: Gitee Api 令牌 (需要点击添加按下图配置)

  4. 配置好后测试连接

  5. 测试成功后保存配置

1606929817791

令牌配置:

  1. 类型选择Gitee API令牌

  2. 私人令牌: 将码云中生成的令牌复制过来

  3. 点击添加

1606929845138

3.6.2 修改jenkins构建任务

修改配置接收webhooks通知

任务详情中点击配置来修改任务

1606929890564

点击构建触发器页签,勾选Gitee webhook

1606929947046

生成Gitee Webhook密码

1606929980749

保存好触发路径和webhook密码,到gitee中配置webhook通知

如:

触发路径: http://192.168.200.151:8888/gitee-project/dockerDemo

触发密码: a591baa17f90e094500e0a11b831af9c

3.6.3 Gitee添加webhooks通知

gitee仓库配置webhooks通知

点击仓库页面的管理

1606930088046

添加webhook

  1. 点击webhooks菜单,然后点击添加

  2. 配置jenkins通知地址

  3. 填写密码

  4. 点击添加

1606930153212

但在点击添加时,提示失败 gitee中需要配置一个公有IP或域名,这里我们可以通过内网穿透来解决

1606930181199

这个时候需要使用内网穿透来映射本地的ip和端口号

1606930276099

在gitee中将上面的外网地址替换之前的ip和端口部分,再次添加

1606930326225

3.6.4 测试自动构建

添加完毕后测试一下:

提交leadnews-admin的代码测试是否自动触发了jenkins中的构建任务

1606930472587

基于阿里云ECS服务器实战部署

1 ECS服务器准备

1.1 ECS服务器购买

购买地址: 阿里云ECS

1 选择配置

image-20210403160340872

2 选择服务和对应的操作系统

image-20210502162922255

3 选择网络带宽 5M

默认5m即可,当然想更快可以设置大一些,但流量是单独收费的

image-20210502162940451

4 直接点击确认订单

image-20210403160806200

5 点击创建实例

image-20210403161009743

image-20210403161041703

点击管理控制台进入服务器管理界面如下:

image-20210403161137366

其中:

公网IP: 47.103.2.34.130

私网IP: 172.20.170.76

收到短信: 发送服务器的 密码

重置密码:

image-20210403161550152

image-20210403161713527

接收短信后,立即重启生效.

1.2 客户端工具连接

推荐使用 finalshell 连接.

1 打开 finalshell 创建 ssh 连接

image-20210403161945737

2 输入 阿里云的 用户名(root) 和 设置的新密码

image-20210403162116660

image-20210403162150013

image-20210403162412058

1.3 安全组设置

在云服务器中,只有配置了安全规则的端口才允许被外界访问

一般默认 开启: 80 (http) 443 (https) 22 (ssh远程连接) 3389 (windows远程连接)

image-20210502163125902

image-20210502163310344

那如果你安装了mysql 端口是3306 ,那么外界是无法直接访问到的,需要配置一下规则

在入方向中配置安全规则: ( 用户 访问 --> 阿里云)

image-20210502163458317

如果配置端口范围: 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)阿里云镜像加速

阿里云开设了一个容器开发平台

需要注册一个属于自己的阿里云账户,登录后进入管理中心

image-20210403231903208

针对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环境

  1. 下载安装包

    下载地址: Download Apache Maven – Maven

  2. 解压安装包

    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
    
    
  3. 配置

    环境变量配置

    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环境搭建

  1. 采用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
  2. 采用RPM安装包方式(采用)

    Jenkins安装包下载地址

    可以导入资料中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
  3. 配置:

    修改配置文件:

    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

    1569564399216

    按默认设置,把建议的插件都安装上

    1569564606846

    这一步等待时间较长, 安装完成之后, 创建管理员用户:

    1569564966999

配置访问地址:

1569564989527

配置完成之后, 会进行重启, 之后可以看到管理后台:

1569565238541

如果下载缓慢,或下载失败:

删除掉 /var/lib/jenkins/updates 下的default.json

将资料中的default.json拷贝进入

image-20210721154336532

3.2 Jenkins插件安装

在实现持续集成之前, 需要确保以下插件安装成功。

  • Maven Integration plugin: Maven 集成管理插件。(必装)

  • Docker plugin: Docker集成插件。

  • GitLab Plugin: GitLab集成插件。

  • Git Plugin: Git 集成插件。(必装)

  • Publish Over SSH:远程文件发布插件。( 可选 )

  • SSH: 远程脚本执行插件。( 可选 )

安装方法:

  1. 进入【系统管理】-【插件管理】

  2. 点击标签页的【可选插件】

    在过滤框中搜索插件名称

    1569742624798

  3. 勾选插件, 点击直接安装即可。

注意,如果没有安装按钮,需要更改配置

在安装插件的高级配置中,修改升级站点的连接为:http://updates.jenkins.io/update-center.json 保存

1603521604783

1603521622211

  1. 安装完毕后重启jenkins

systemctl restart jenkins

3.3 jenkins全局配置

  1. 进入【系统管理】--> 【全局工具配置】

    1603200359461

  2. MAVEN配置全局设置

    1603200435502

  3. 指定JDK配置

    不用指定, 前面已安装

  1. 指定MAVEN 目录

    点击新增maven 配置name: maven 配置maven地址: /usr/local/maven/apache-maven-3.3.9

    1603200472287

  2. 设置远程应用服务主机(不涉及远程服务器构建,可不做)

    添加凭证:

    1603735746602

    新增凭证,输入用户名和密码保存即可

    1603735794051

    进入【系统管理】-【系统设置】

    1603200611899

    输入主机名称和登陆信息, 点击【check connections】验证, 如果成功, 会显示“Successfull connection”。

    1603735668415

4 docker-compose安装依赖软件

黑马头条涉及前端后端服务众多,设备性能有限 所以我们简化下部署步骤, 基于docker + docker-compose方式快速部署,先来了解下黑马头条的部署架构

1613375860330

  1. nginx作为接入层 所有请求全部通过nginx进入 (部署在100服务 端口80)

  2. 前端工程app、admin、wemedia全部部署在nginx中

  3. nginx通过反向代理将对微服务的请求,代理到网关

  4. 网关根据路由规则,将请求转发到下面的微服务

  5. 所有微服务都会注册到nacos注册中心

  6. 所有微服务都会将配置存储到nacos中进行统一配置

  7. 网关服务及所有微服务部署在 100 服务器 jenkins 微服务

  8. 所有依赖的软件部署在 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

使用步骤

  1. 运行所有容器:

# 运行
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统一配置

image-20210821115115028

4.3 实现nacos统一配置

将application.yml中的配置文件内容复制到nacos中

1613379700439

配置文件是可导入导出的,导入资料中的nacos_config配置压缩包

然后根据自己的配置情况进行修改

1613379876496

需要在 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

1603556179776

5.1 基础依赖打包配置

在微服务运行之前需要在本地仓库中先去install所依赖的jar包,所以第一步应该是从git中拉取代码,并且把基础的依赖部分安装到仓库中

(1)新创建一个item,起名为heima-leadnews

1603202842506

1603556912178

(2)配置当前heima-leadnews

  • 描述项目

1603556984260

  • 源码管理:

    选中git,输入git的仓库地址(前提条件,需要把代码上传到gitee仓库中),最后输入gitee的用户名和密码

    如果没有配置Credentials,可以选择添加,然后输入用户名密码即可 (公开仓库无需密码)

    jenkins拉取gitlab代码 ssh配置 Jenkins使用SSH的方式从GitLab拉取代码-优快云博客

1603557100765

  • 其中构建触发器构建环境暂不设置

  • 设置构建配置

    选择Invoke top-level Maven targets

1603557353285

maven版本:就是之前在jenkins中配置的maven

目标:输入maven的命令 clean install -Dmaven.test.skip=true 跳过测试安装

1603557446935

(3)启动项目

创建完成以后可以在主页上看到这个item

1603557661626

启动项目:点击刚才创建的项目,然后Build Now

1603557703328

在左侧可以查看构建的进度:

1603557774990

点进去以后,可以查看构建的日志信息

构建的过程中,会不断的输入日志信息,如果报错也会提示错误信息

1603557824124

jenkins会先从git仓库中拉取代码,然后执行maven的install命令,把代码安装到本地仓库中

最终如果是success则为构建成功

1603557903228

5.2 微服务打包配置

(1)新建item,以heima-leadnews-admin微服务为例

1603558664549

(2)配置

  • 概述

1603558714167

  • 源码管理

1603558748960

  • 构建

配置maven

1603558861469

执行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)

image-20210427115048199

1603558973180

执行shell命令

1603558910209

1603559164996

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中拉取代码

  • 编译打包项目

  • 构建镜像

  • 创建容器

  • 删除多余的镜像

可以从服务器中查看镜像

1603559728626

容器也已创建完毕

1603559790306

可以使用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

1603561212541

heima-leadnews-user微服务的配置:

maven命令:

clean install -Dmaven.test.skip=true dockerfile:build -f heima-leadnews-services/user-service/pom.xml

1603561293105

所有项目构建完成以后,在本地启动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

1613376602041

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文件夹中

1613377149886

1613377225038

把dist文件夹上传到服务器上,拷贝到150虚拟机的/root/workspace/admin目录中

wemedia前端工程发布

在wemedia工程下,打开cmd 输入: npm run build 进行发布

发布后的静态文件,会存放到dist文件夹中

1613377649182

把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

1613377967699

把releases文件夹下的web文件夹上传到服务器上,拷贝到150虚拟机的/root/workspace/目录中

6.3 nginx配置前端工程访问

对于不同的前端工程 , 我们会通过不同的域名来访问, 先给三个前端工程准备3个访问域名

  1. 使用type下载hosts插件

  2. 配置3个域名:

  3. 47.100.216.65 admin.leadnews.com 运营端

  4. 47.100.216.65 wemedia.leadnews.com 媒体端

  5. 47.100.216.65 web.leadnews.com app端

1613378464091

1613378379712

小贴士: 
如果想部署外网访问的项目,可以使用内网穿透 准备三个外网地址
全部映射到
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

输入网址访问前端工程:

1613378750447

1613378843939

7 域名设置与绑定(了解)

7.1 域名购买

在阿里云服务上部署完项目,你的项目就已经正式在互联网上线了 不过目前还是只能通过 外网的IP地址访问

如果要买域名 ==> 阿里云域名交易首页

7.2 域名备案

域名购买完毕后是需要备案的

如果您的网站托管在阿里云中国内地(大陆)节点服务器上,且网站的主办人和域名从未办理过备案,在网站开通服务前,您需通过阿里云ICP代备案系统完成ICP备案。

备案前您需准备备案所需的相关资料,通过PC端或App端进行备案信息填写、资料上传、真实性核验等,备案信息提交后需通过阿里云初审、短信核验和管局审核,整个备案流程预计所需时长约1~22个工作日左右,具体时长以实际操作时间为准。

阿里云ICP备案流程概述_备案(ICP Filing)-阿里云帮助中心

7.3 域名绑定

域名需要和你的外网阿里云IP地址 绑定方可使用

配置域名解析地址==> 阿里云登录 - 欢迎登录阿里云,安全稳定的云计算服务平台

image-20210428172804434

我们使用内网穿透 将地址 映射的 阿里云外网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

1606929689394

(2) gitee生成访问令牌

首先,去下面网址生成gitee访问令牌

https://gitee.com/profile/personal_access_tokens

1606929726997

添加令牌描述,提交,弹出框输入密码

1606929755320

复制令牌

1606929779247

(3) jenkins中配置Gitee

系统管理 --> 系统配置 --> Gitee配置

  1. 链接名: gitee

  2. 域名: Gitee - 基于 Git 的代码托管和研发协作平台

  3. 令牌: Gitee Api 令牌 (需要点击添加按下图配置)

  4. 配置好后测试连接

  5. 测试成功后保存配置

1606929817791

令牌配置:

  1. 类型选择Gitee API令牌

  2. 私人令牌: 将码云中生成的令牌复制过来

  3. 点击添加

1606929845138

8.2 修改jenkins构建任务

修改配置接收webhooks通知

任务详情中点击配置来修改任务

1606929890564

点击构建触发器页签,勾选Gitee webhook

1606929947046

生成Gitee Webhook密码

1606929980749

保存好触发路径和webhook密码,到gitee中配置webhook通知

如:

触发路径: http://192.168.200.151:8888/gitee-project/dockerDemo

触发密码: a591baa17f90e094500e0a11b831af9c

8.3 Gitee添加webhooks通知

gitee仓库配置webhooks通知

点击仓库页面的管理

1606930088046

添加webhook

  1. 点击webhooks菜单,然后点击添加

  2. 配置jenkins通知地址

  3. 填写密码

  4. 点击添加

1606930153212

但在点击添加时,提示失败 gitee中需要配置一个公有IP或域名,这里我们可以通过内网穿透来解决

1606930181199

这个时候需要使用内网穿透来映射本地的ip和端口号

1606930276099

在gitee中将上面的外网地址替换之前的ip和端口部分,再次添加

1606930326225

8.4 测试自动构建

添加完毕后测试一下:

提交leadnews-admin的代码测试是否自动触发了jenkins中的构建任务

1606930472587

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值