开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

物联网系列 - MQTT协议原理与数据包结构

发表于 2021-11-02

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」。

  1. MQTT协议原理

1. MQTT协议实现方式

实现MQTT协议需要客户端和服务器端通讯完成,在通讯过程中,MQTT协议中有三种身份:发布者
(Publish)、代理(Broker)(服务器)、订阅者(Subscribe)。其中,消息的发布者和订阅者都是客户端,消
息代理是服务器,消息发布者可以同时是订阅者。

MQTT传输的消息分为:主题(Topic)和负载(payload)两部分:

  • (1)Topic,可以理解为消息的类型,订阅者订阅(Subscribe)后,就会收到该主题的消息内容(payload);
  • (2)payload,可以理解为消息的内容,是指订阅者具体要使用的内容。

2. 网络传输与应用消息

MQTT会构建底层网络传输:它将建立客户端到服务器的连接,提供两者之间的一个有序的、无损的、基于字
节流的双向传输。

当应用数据通过MQTT网络发送时,MQTT会把与之相关的服务质量(QoS)和主题名(Topic)相关连。

3. MQTT客户端

一个使用MQTT协议的应用程序或者设备,它总是建立到服务器的网络连接。客户端可以:

  • (1)发布其他客户端可能会订阅的信息;
  • (2)订阅其它客户端发布的消息;
  • (3)退订或删除应用程序的消息;
  • (4)断开与服务器连接。

4. MQTT服务器端

MQTT服务器以称为”消息代理”(Broker),可以是一个应用程序或一台设备。它是位于消息发布者和订阅者
之间,它可以:

  • (1)接受来自客户的网络连接;
  • (2)接受客户发布的应用信息;
  • (3)处理来自客户端的订阅和退订请求;
  • (4)向订阅的客户转发应用程序消息。

5. 发布/订阅、主题、会话

MQTT 是基于 发布(Publish)/订阅(Subscribe) 模式来进行通信及数据交换的,与 HTTP 的 请求(Request)/应
答(Response) 的模式有本质的不同。

订阅者(Subscriber) 会向 消息服务器(Broker) 订阅一个 主题(Topic) 。成功订阅后,消息服务器会将该主题
下的消息转发给所有的订阅者。

主题(Topic)以 ‘/’ 为分隔符区分不同的层级。包含通配符 ‘+’ 或 ‘#’ 的主题又称为 主题过滤器(Topic Filters); 不
含通配符的称为 主题名(Topic Names) 例如:

1
2
3
4
5
6
7
8
9
bash复制代码1. chat/room/1
2. sensor/10/temperature
3. sensor/+/temperature
4. $SYS/broker/metrics/packets/received
5. $SYS/broker/metrics/#

1. '+': 表示通配一个层级,例如a/+,匹配a/x, a/y
2. '#': 表示通配多个层级,例如a/#,匹配a/x, a/b/c/d
3. 注: ‘+’ 通配一个层级,’#’ 通配多个层级(必须在末尾)。

发布者(Publisher) 只能向 ‘主题名’ 发布消息,订阅者(Subscriber) 则可以通过订阅 ‘主题过滤器’ 来通配多个主题名称。

会话(Session)每个客户端与服务器建立连接后就是一个会话,客户端和服务器之间有状态交互。会话存在于一个网络之间,也可能在客户端和服务器之间跨越多个连续的网络连接。

6. MQTT协议中的方法

MQTT协议中定义了一些方法(也被称为动作),来于表示对确定资源所进行操作。这个资源可以代表预先存
在的数据或动态生成数据,这取决于服务器的实现。通常来说,资源指服务器上的文件或输出。主要方法有:

  • (1)CONNECT:客户端连接到服务器
  • (2)CONNACK:连接确认
  • (3)PUBLISH:发布消息
  • (4)PUBACK:发布确认
  • (5)PUBREC:发布的消息已接收
  • (6)PUBREL:发布的消息已释放
  • (7)PUBCOMP:发布完成
  • (8)SUBSCRIBE:订阅请求
  • (9)SUBACK:订阅确认
  • (10)UNSUBSCRIBE:取消订阅
  • (11)UNSUBACK:取消订阅确认
  • (12)PINGREQ:客户端发送心跳
  • (13)PINGRESP:服务端心跳响应
  • (14)DISCONNECT:断开连接
  • (15)AUTH:认证
  1. MQTT协议数据包结构

1. 结构组成

官方文档中对于MQTT协议包的结构有着具体的说明:mqtt.org/documentati…

在MQTT协议中,一个MQTT数据包由:固定头(Fixed header)、可变头(Variable header)、消息体
(payload)三部分构成。MQTT数据包结构如下:

mqtt2.jpg

  • (1)固定头(Fixed header)。存在于所有MQTT数据包中,表示数据包类型及数据包的分组类标识,如连接,发布,订阅,心跳等。其中固定头是必须的,所有类型的MQTT协议中,都必须包含固定头。
  • (2)可变头(Variable header)。存在于部分MQTT数据包中,数据包类型决定了可变头是否存在及其具体内容。可变头部不是可选的意思,而是指这部分在有些协议类型中存在,在有些协议中不存在。
  • (3)消息体(Payload)。存在于部分MQTT数据包中,表示客户端收到的具体内容。 与可变头一样,在有些协议类型中有消息内容,有些协议类型中没有消息内容。

2. 固定头(Fixed header)

mqtt3.jpg

固定头存在于所有MQTT数据包中, 固定头包含两部分内容,首字节(字节1)和剩余消息报文长度(从第二个字
节开始,长度为1-4字节),剩余长度是当前包中剩余内容长度的字节数,包括变量头和有效负载中的数据)。剩余
长度不包含用来编码剩余长度的字节。

剩余长度使用了一种可变长度的结构来编码,这种结构使用单一字节表示0-127的值。大于127的值如下处
理。每个字节的低7位用来编码数据,最高位用来表示是否还有后续字节。因此每个字节可以编码128个值,再加
上一个标识位。剩余长度最多可以用四个字节来表示。

1. 数据包类型

位置:第一个字节(Byte 1) 中的7-4个bit位(Bit[7-4]),表示4位无符号值

通过第一个字节的高4位确定消息报文的类型,4个bit位能确定16种类型,其中0000和1111是保留字段。
MQTT消息报文类型如下:

mqtt4.jpg

2.标志位

位置:第一个字节中的0-3个bit位(Bit[3-0])。意思是字节位Bit[3-0]用作报文的标识。

首字节的低4位(bit3~bit0)用来表示某些报文类型的控制字段,实际上只有少数报文类型有控制位,如下图:

mqtt5.jpg

  • (1):其中Bit[3]为DUP字段,如果该值为1,表明这个数据包是一条重复的消息;否则该数据包就是第一次发布的消息。
  • (2):Bit[2-1]为Qos字段:
    • 如果Bit 1和Bit 2都为0,表示QoS 0:至多一次;
    • 如果Bit 1为1,表示QoS 1:至少一次;
    • 如果Bit 2为1,表示QoS 2:只有一次;
    • 如果同时将Bit 1和Bit 2都设置成1,那么客户端或服务器认为这是一条非法的消息,会关闭当前连接。

目前Bit[3-0]只在PUBLISH协议中使用有效,并且表中指明了是MQTT 3.1.1版本。对于其它MQTT协议版
本,内容可能不同。所有固定头标记为”保留”的协议类型,Bit[3-0]必须保持与表中保持一致,如SUBSCRIBE
协议,其Bit 1必须为1。如果接收方接收到非法的消息,会强行关闭当前连接。

3. MQTT消息QoS

MQTT发布消息服务质量保证(QoS)不是端到端的,是客户端与服务器之间的。订阅者收到MQTT消息的
QoS级别,最终取决于发布消息的QoS和主题订阅的QoS。

mqtt6.jpg

1. Qos0消息发布订阅

mqtt7.jpg

2. Qos1消息发布订阅

mqtt8.jpg

3. Qos2消息发布订阅

mqtt9.jpg

Bit[0]为RETAIN字段,发布保留标识,表示服务器要保留这次推送的信息,如果有新的订阅者出现,
就把这消息推送给它,如果设有那么推送至当前订阅者后释放。

3. 可变头(Variable Header)

可变头的意思是可变化的消息头部。有些报文类型包含可变头部有些报文则不包含。可变头部在固定头部和消
息内容之间,其内容根据报文类型不同而不同。

mqtt10.jpg

1. 协议名

协议名是表示协议名MQTT的UTF-8编码的字符串。MQTT规范的后续版本不会改变这个字符串的偏移和
长度。

image.png

支持多种协议的服务端使用协议名字段判断数据是否为MQTT报文。协议名必须是UTF-8字符串“MQTT”。如果服务端不愿意接受CONNECT但希望表明其MQTT服务端身份,可以发送包含原因码为0x84(不支持的协议版本)的CONNACK报文,然后必须关闭网络连接

2. 协议版本

位无符号值表示客户端的版本等级。3.1.1版本的协议等级是4,MQTT v5.0的协议版本字段为5(0x05)

3. MQTT会话(Clean Session)

MQTT客户端向服务器发起CONNECT请求时,可以通过’Clean Session’标志设置会话。

  • ‘Clean Session’设置为0,表示创建一个持久会话,在客户端断开连接时,会话仍然保持并保存离线消息,直到会话超时注销。
  • ‘Clean Session’设置为1,表示创建一个新的临时会话,在客户端断开时,会话自动销毁。
1.Will Flag/Will Qos/Will Retain

如果Will Flag被设置为1,这意味着,如果连接请求被接受,服务端必须存储一个Will Message,并和网
络连接关联起来。之后在网络连接断开的时候必须发布Will Message,除非服务端收到DISCONNECT包删掉
了Will Message

Will Message会在某些情况下发布,包括但不限于:

  • 服务端发现I/O错误或网络失败。
  • 客户端在Keep Alive时间内通信失败。
  • 客户端没有发送DISCONNECT包就关闭了网络连接。
  • 服务端因协议错误关闭了网络连接。
    如果Will Flag被设置为1,连接标识中的Will QoS和Will Retain字段将会被服务端用到

Will QoS这两个bit表示发布Will Message时使用QoS的等级

Will Retain这个bit表示Will Message在发布之后是否需要保留。

如果Will Flag设置为0,那么Will Retain必须是0

如果Will Flag设置为1:

  • 如果Will Retain设置为0,那么服务端必须发布Will Message,不必保存
  • 如果Will Retain设置为1,那么服务端必须发布Will Message,并保存
2. User Name Flag
  • 如果User Name Flag设置为0,那么用户名不必出现在载荷中
  • 如果User Name Flag设置为1,那么用户名必须出现在载荷中
3. Password Flag
  • 如果Password Flag设置为0,那么密码不必出现在载荷中
  • 如果Password Flag设置为1,那么密码必须出现在载荷中
  • 如果User Name Flag设置为0,那么Password Flag必须设置为0
4. MQTT连接保活心跳

心跳的作用:

1
2
3
4
5
diff复制代码PINGREQ包从客户端发往服务端,可以用来:
- 1:在没有其他控制包从客户端发送给服务端的时候,告知服务端客户端的存活状态。
- 2:请求服务端响应,来确认服务端是否存活。
- 3:确认网络连接的有效性。
PINGRESP包从服务端发送给客户端来响应PINGREQ包。它代表服务端是存活的。

MQTT客户端向服务器发起CONNECT请求时,通过KeepAlive参数设置保活周期。

Keep Alive是以秒为单位的时间间隔。用2字节表示,它指的是客户端从发送完成一个控制包到开始发送
下一个的最大时间间隔。客户端有责任确保两个控制包发送的间隔不能超过Keep Alive的值。如果没有其他
控制包可发,客户端必须发送PINGREQ包

客户端可以在任何时间发送PINGREQ包,不用关心Keep Alive的值,用PINGRESP来判断与服务端的网络连接是否正常。

如果Keep Alive的值非0,而且服务端在一个半Keep Alive的周期内没有收到客户端的控制包,服务端必须作为网络故障断开网络连接

如果客户端在发送了PINGREQ后,在一个合理的时间都没有收到PINGRESP包,客户端应该关闭和服务端的网络连接。

Keep Alive的值为0,就关闭了维持的机制。这意味着,在这种情况下,服务端不会断开静默的客户端。

5. MQTT遗愿消息(Last Will)

MQTT客户端向服务器端CONNECT请求时,可以设置是否发送遗愿消息(Will Message)标志,和遗愿消息主题(Topic)与内容(Payload)。

MQTT客户端异常下线时(客户端断开前未向服务器发送DISCONNECT消息),MQTT消息服务器会发布遗愿消息。

常见的一种可变头比如:Packet Identifier(消息ID)

一个消息ID包含2字节,高字节在前,低字节在后。包含Packet Identifier的协议类型包括:

PUBLISH( QoS > 0 )、 PUBACK 、 PUBREC 、 PUBREL 、 PUBCOMP 、 SUBSCRIBE 、 SUBACK 、
UNSUBSCRIBE 、 UNSUBACK

消息ID默认是从1开始并自增,如果一个消息ID被用完后,这个消息ID可以被重用。对于PUBLISH (QoS 1)来
说,如果发送端接收到PUBACK,那么这个消息ID就用完了。对于PUBLISH(QoS 2),如果接收方收到PUBCOMP,那么这个消息ID就用完了。对于SUBSCRIBE和UNSUBSCRIBE,消息ID使用完成的标记是发送方收到了对应的SUBACK和UNSUBACK。

另外客户端和服务端的消息ID是独立分配的,客户端和服务端可以同时使用同一个消息ID。

4. 消息体(Payload)

有些报文类型是包含Payload的,Payload意思是消息载体的意思

如PUBLISH的Payload就是指消息内容(应用程序发布的消息内容)。而CONNECT的Payload则包含Client
Identifier,Will Topic,Will Message,Username,Password等信息。

包含payload的报文类型如下:

mqtt11.jpg

5. 总结

我们介绍了MQTT协议的消息格式,MQTT消息格式包含Fixed Header, Variable Header和Payload。
因为MQTT消息格式非常精简,所以可以高效的传输数据。

Fixed Header中包含首字节,高4位用来表示报文类型,低4位用于类型控制。目前只有PUBLISH使用了类型
控制字段。其它控制字段被保留并且必须与协议定义保持一致。

Fixed Header同时包含Remaining Length,这是剩余消息长度,最大长度为4字节,理论上一条MQTT最大可
以传输256MB数据。Remaining Length=Variable Header+Payload长度。

Variable Header是可变头部,有些报文类型中需要包含可变头部,可变头部根据报文类型不同而不同。比如
Packet Identifier在发布,订阅/取消订阅等报文中都使用到。

Payload是消息内容,也只在某些报文类型中出现,其内容和格式也根据报文类型不同而不同。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

近期业务大量突增微服务性能优化总结-2开发日志输出异常堆栈

发表于 2021-11-02

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

最近,业务增长的很迅猛,对于我们后台这块也是一个不小的挑战,这次遇到的核心业务接口的性能瓶颈,并不是单独的一个问题导致的,而是几个问题揉在一起:我们解决一个之后,发上线,之后发现还有另一个的性能瓶颈问题。这也是我经验不足,导致没能一下子定位解决;而我又对我们后台整个团队有着固执的自尊,不想通过大量水平扩容这种方式挺过压力高峰,导致线上连续几晚都出现了不同程度的问题,肯定对于我们的业务增长是有影响的。这也是我不成熟和要反思的地方。这系列文章主要记录下我们针对这次业务增长,对于我们后台微服务系统做的通用技术优化,针对业务流程和缓存的优化由于只适用于我们的业务,这里就不再赘述了。本系列会分为如下几篇:

  1. 改进客户端负载均衡算法
  2. 开发日志输出异常堆栈的过滤插件
  3. 针对 x86 云环境改进异步日志等待策略
  4. 增加对于同步微服务的 HTTP 请求等待队列的监控以及云上部署,需要小心达到实例网络流量上限导致的请求响应缓慢
  5. 针对系统关键业务增加必要的侵入式监控

开发日志输出异常堆栈的过滤插件

我们一般会在异常发生时,打印日志,同时日志中带有异常堆栈。

在线上因为某个基础组件或者某个存储慢导致大量超时异常发生时,如果都打印完整的异常栈,则一下子会输出大量的日志,导致写入日志也会成为瓶颈(虽然我们使用了 Log4j2 的异步日志 ,但是如果 RingBuffer 满了输出日志还是会导致业务线程阻塞)。从而导致同一微服务中其他本来正常的业务,也变得不正常了。

并且,我们发现

为何 JVM 参数中加入 -XX:-OmitStackTraceInFastThrow

为了避免这个问题可能首先想到的是,JVM 参数中去掉 -XX:-OmitStackTraceInFastThrow。这个 OmitStackTraceInFastThrow 默认是启用的,其作用是,当某个 JDK 内置异常通过某一个方法抛出过多次数时(常见是 NullPointerException),自动省略异常堆栈,其实就是 Throwable.getOurStacktrace() 返回空数组。底层实现的方式是当这些异常被抛出时,会记录在方法的 method_data 中。当这些异常被抛出时,检查对应方法的 method_data 是否有过多次数的这些异常被抛出,如果有,则使用不含堆栈的异常对象替换原有异常对象从而实现异常堆栈被忽略。对应源码:

graphKit.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
ini复制代码  bool treat_throw_as_hot = false;
ciMethodData* md = method()->method_data();

if (ProfileTraps) {
//如果有太多次,则 treat_throw_as_hot 为 true
if (too_many_traps(reason)) {
treat_throw_as_hot = true;
}
if (C->trap_count(reason) != 0
&& method()->method_data()->trap_count(reason) != 0
&& has_ex_handler()) {
treat_throw_as_hot = true;
}
}

if (treat_throw_as_hot
&& (!StackTraceInThrowable || OmitStackTraceInFastThrow)) {
ciInstance* ex_obj = NULL;
switch (reason) {
case Deoptimization::Reason_null_check:
//对于 NullPointerException 返回对应的空堆栈的内置 NullPointerException 对象
ex_obj = env()->NullPointerException_instance();
break;
case Deoptimization::Reason_div0_check:
//对于 ArithmeticException 返回对应的空堆栈的内置 ArithmeticException 对象
ex_obj = env()->ArithmeticException_instance();
break;
case Deoptimization::Reason_range_check:
//对于 ArrayIndexOutOfBounds 返回对应的空堆栈的内置 NullPoArrayIndexOutOfBoundsinterException 对象
ex_obj = env()->ArrayIndexOutOfBoundsException_instance();
break;
case Deoptimization::Reason_class_check:
if (java_bc() == Bytecodes::_aastore) {
//对于 ArrayStoreException 返回对应的空堆栈的内置 ArrayStoreException 对象
ex_obj = env()->ArrayStoreException_instance();
} else {
//对于 ClassCastException 返回对应的空堆栈的内置 ClassCastException 对象
ex_obj = env()->ClassCastException_instance();
}
break;
default:
break;
}

但是,我们一般会在 JVM 启动参数中加入 -XX:-OmitStackTraceInFastThrow 将其关闭,主要原因是:

  1. OmitStackTraceInFastThrow 这个参数仅针对某些 Java 内置异常(上面源码已经列出),对于我们自定义或者框架自定义的异常没用。
  2. 分析是否过多,仅对于抛出异常的方法,但是是否是同一调用路径,并没有考虑。
  3. 微服务线程可能会运行很长时间,我们业务日志量非常大,每一个小时产生一个新文件。假设某个方法每天抛出一定量的 NullPointerException 但是不多,并没有及时发现。日积月累,某一天突然就没有堆栈了。之后如果这个方法大量抛出 NullPointerException,我们却看不到堆栈,还得去挨个翻之前的日志,这样太低效率了。

我们对于异常日志的需求

由于我们项目中使用了各种框架,有的使用了异步框架,导致异常栈会非常非常长(有的甚至有 1000 行),所以其实最符合我们需求的是:

  1. 每次异常都输出异常栈
  2. 但是异常栈只包括我们关心的包,其他的包都被省略,防止异常栈过长导致输出大量日志。

Log4j2 官方关于异常的配置

Log4j2 官方只是提供了黑名单包的配置,也就是哪些包的异常栈被省略掉;还有关于精简异常栈的就是输出日志的前几行,但是我们无法保证我们关心的日志一定位于日志的前几行,尤其是针对异步响应式代码调用的异常栈的时候。

image

不过 Log4j2 的这些配置,是通过 Pattern 插件的方式实现的,我们也可以通过开发自定义的 Pattern 插件实现。

开发自定义 Pattern 插件

首先,Log4j2 官方只是提供了黑名单包的配置,我们可以将黑名单包的配置的判断逻辑取反,其实就变成了白名单,其他的配置格式解析照旧。于是我们将 Log4j2 的代码 ThrowableProxyRenderer 复制出来,命名为我们自定义的 CustomizedThrowableProxyRenderer,同时将其中 package-private 的方法改成 public 的,这样我们可以在任何地方调用。并且将其中黑名单判断取反,改成白名单:

完整源码参考:github.com/JoJoTec/spr…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
arduino复制代码public class CustomizedThrowableProxyRenderer {
//省略没有修改的方法

//该方法改为 public
public static void formatExtendedStackTraceTo(final ThrowableProxy src, final StringBuilder sb, final List<String> whiteListedPackages, final TextRenderer textRenderer, final String suffix, final String lineSeparator) {
textRenderer.render(src.getName(), sb, "Name");
textRenderer.render(": ", sb, "NameMessageSeparator");
textRenderer.render(src.getMessage(), sb, "Message");
renderSuffix(suffix, sb, textRenderer);
textRenderer.render(lineSeparator, sb, "Text");
final StackTraceElement[] causedTrace = src.getThrowable() != null ? src.getThrowable().getStackTrace() : null;
formatElements(sb, Strings.EMPTY, 0, causedTrace, src.getExtendedStackTrace(), whiteListedPackages, textRenderer, suffix, lineSeparator);
formatSuppressed(sb, TAB, src.getSuppressedProxies(), whiteListedPackages, textRenderer, suffix, lineSeparator);
formatCause(sb, Strings.EMPTY, src.getCauseProxy(), whiteListedPackages, textRenderer, suffix, lineSeparator);
}


//原来的 blackListElement 方法改成 whiteListedElement
//结果取反,改成白名单
private static boolean whiteListedElement(final StackTraceElement element, final List<String> whiteListedPackages) {
if (whiteListedPackages != null) {
final String className = element.getClassName();
for (final String pkg : whiteListedPackages) {
if (className.startsWith(pkg)) {
return true;
}
}
}
return false;
}
}

然后,开发我们自定义的 Log4j2 Pattern 插件:

CustomizedThrowablePatternConverter.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
java复制代码@Plugin(name = "CustomizedThrowablePatternConverter", category = PatternConverter.CATEGORY)
@ConverterKeys({ "cusEx", "cusThrowable", "cusException" })
public class CustomizedThrowablePatternConverter extends ThrowablePatternConverter {
public static CustomizedThrowablePatternConverter newInstance(final Configuration config, final String[] options) {
return new CustomizedThrowablePatternConverter(config, options);
}
private CustomizedThrowablePatternConverter(final Configuration config, final String[] options) {
super("CustomizedThrowable", "throwable", options, config);
}

@Override
public void format(final LogEvent event, final StringBuilder toAppendTo) {
final ThrowableProxy proxy = event.getThrownProxy();
final Throwable throwable = event.getThrown();
if ((throwable != null || proxy != null) && options.anyLines()) {
if (proxy == null) {
super.format(event, toAppendTo);
return;
}
final int len = toAppendTo.length();
if (len > 0 && !Character.isWhitespace(toAppendTo.charAt(len - 1))) {
toAppendTo.append(' ');
}
//调用上面的工具类,格式化异常
CustomizedThrowableProxyRenderer.formatExtendedStackTraceTo(proxy, toAppendTo, options.getIgnorePackages(),
options.getTextRenderer(), getSuffix(event), options.getSeparator());
}
}
}

这样,我们可以在我们的 Log4j2 配置中,加入这个异常定义 PatternLayout,例如:

1
2
3
4
5
6
7
8
ini复制代码<RollingFile name="file" append="true"
filePattern="${LOG_ROOT}/app.log-%d{yyyy.MM.dd.HH}-${sys:LOCAL_IP}">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [%X{traceId},%X{spanId}] [${sys:PID}] [%t]: %m%n%cusEx{filters(java, com.mycompany)}"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
</Policies>
<DirectWriteRolloverStrategy maxFiles="72"/>
</RollingFile>

其中的 %cusEx{filters(java, com.mycompany)},代表异常栈就只会输出这些包开头的异常堆栈,这里是 java 和 com.mycompany 开头的。如此一来,log.error("something error!", e);,输出的日志就会类似于:

1
2
3
4
5
6
7
8
9
10
11
12
less复制代码2021-10-30 16:00:19.254  ERROR [65f6eda3bf6a48ee,d25cc4c9eb1deed6] [30] [subscribe-pool-9]: something error!
... suppressed 27 lines
at com.mycompany.spring.cloud.parent.web.common.undertow.jfr.JFRTracingFilter.doFilter(JFRTracingFilter.java:40) ~[spring-cloud-parent-web-common-2020.0.3-SNAPSHOT.jar!/:2020.0.3-SNAPSHOT]
at com.mycompany.spring.cloud.parent.web.common.undertow.jfr.LazyJFRTracingFilter.doFilter(LazyJFRTracingFilter.java:23) ~[spring-cloud-parent-web-common-2020.0.3-SNAPSHOT.jar!/:2020.0.3-SNAPSHOT]
... suppressed 46 lines
Caused by: com.alibaba.fastjson.JSONException: write javaBean error, fastjson version 1.2.75, class com.hopegaming.factsCenter.query.revo.controller.frontend.share.matches.MatchController$EventVO, fieldName : data
... suppressed 8 lines
... 74 more
Caused by: java.lang.NullPointerException
at com.mycompany.OrderController.postOrder(OrderController.java:103) ~[classes!/:2020.0.3-SNAPSHOT]
... suppressed 13 lines
... 74 more

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【摸鱼高手】搭个自己的FTP服务器玩玩 🏘️ 一、前言 🏚️

发表于 2021-11-02

「这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战」。


  • 💬 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦。

🏘️ 一、前言

  • 大家好,我是小诚,在开始学习之前,先给大家推荐下最近筹划的【技术圈子】,在没有更文的20多天里,一直在梳理之前的文章知识和学习资源,现在已经初步梳理完成,圈子中所有资源都免费分享,资源包括但不限于面试资源、简历模板、年终汇报PPT、CSDN VIP下载资源等等,需要得请点击主页查看,期待您的加入。
  • 如果文章对你有帮助,可以帮忙一键三连和专栏订阅哦!
  • 本篇文章重点介绍:Window环境搭建FTP服务器,JAVA程序实现FTP服务器文件上传、下载功能。

🏚️ 二、完成效果

  1、上传文件到FTP服务器:

上传文件到FTP服务器

  2、从FTP服务器下载文件到本地:

从FTP服务器下载文件到本地

🏠 三、Window系统FTP服务器搭建

  在搭建FTP服务器之前,先跟大家介绍下什么是FTP服务器,不然怕有些小伙伴可能只是有个模糊的概念,学习要尽量知其然,知其所以然,不要一知半解,否则自己用着也不踏实。

♈ 3.1、FTP服务器怎么玩

  在介绍前,我们先来看看搭建完FTP服务器后能怎么玩,不然总是有些小伙伴觉的文章太长看到一半就跑路了,错过就没有了!

  1、上传、下载小视频(共享文件),自定义权限控制,控制使用用户。

  2、实现某些业务场景下文件存储和文件下载(即文件服务器操作)。

  3、更多玩法等你开发,尽情发挥你的脑洞…

♉ 3.2、FTP(File Transfer Protocol,文件传输协议)

  先简单认识下FTP协议,FTP即文件传输协议的简称,它是TCP/IP协议簇中的一员,也是Internet上最早使用的协议之一,通过它可以实现电脑与电脑间对文件的各种操作(如文件的增、删、改、查、传送等),FTP的目标是提高文件的共享性,提供非直接使用远程计算机,实现计算机文件的相互操作,使存储介质对用户透明和可靠高效地传送数据。

  它是基于C/S(客户端/服务端)模型设计,工作在网络体系结构中的应用层,使用TCP进行传输,保证客户与服务器之间的连接是可靠的。

  支持的连接方式:

  FTP支持Standard (PORT方式,主动方式),Passive (PASV,被动方式)两种连接模式,连接的流程大致如下:

  1、FTP客户端发起FTP会话,与FTP服务器建立相应的连接,在会话期间,FTP会建立控制信息进程与数据进程两个连接。

  2、控制进程连接的用途: 用于传输FTP内部命令以及命令的响应等控制信息,无法进行数据传输。

  3、数据进程连接的用途: 用于客户端与服务端之间数据的传输,它是全双工的,可以支持双向数据传输,当数据传输完成后,它就会撤销然后回到FTP会话状态,直到控制连接进程也取消,退出整个FTP会话。

  PORT模式:

  FTP客户端会与服务端的TCP 21端口创建连接(控制连接),用于发送命令,当客户端需要接收数据时,会通过这个连接向服务端发送PORT命名,PORT命令中包含了会使用什么端口来接收服务端传输的数据,此时,服务端会通过TCP 20端口跟FTP客户端创建连接(数据连接)完成数据传输。

  Passive模式:

  FTP客户端会与服务端的TCP 21端口创建连接(控制连接),用于发送命令,当客户端需要接收数据时,会通过这个连接向服务端发送Pasv命名,服务器收到Pasv命令后,打开一个临时端口(端口号大于1023小于65535)并且通知客户端在这个端口上传送数据的请求,客户端连接FTP服务器此端口,然后FTP服务器将通过这个端口传送数据。

  说明:上面FTP协议知识介绍参考百度百科:FTP协议

♊ 3.3、FTP服务器

  了解了FTP协议,那FTP服务器就很容易理解了。FTP服务器就是支持FTP协议的服务器,我们平常可以在电脑上安装一个FTP工具就可实现与FTP服务器进行文件传输,FTP服务器常见分为:Windows FTP服务器和Linux FTP服务器。

  我们自己的电脑也可以当做一个FTP服务器,如Windows系统就可以通过自带的ISS管理器来搭建一个FTP服务器(本文案例就是使用这个),Linux系统最常用的借助vsftp软件做FTP服务器搭建。

  常见的例子: 在学校里上电脑课或者电脑考试时,老师会将上课题目或者考试题目放在某个文件夹中,让学生访问某个地址如:ftp://ip地址,通过这个地址每位同学看到老师共享的文件,下载的对应的试题完成考试。

  上面例子上过电脑课的同学应该都经历过(多么美好的学生时代),学生们访问到的其实就是老师搭建好的FTP服务器,老师提前将共享的文件上传到FTP服务器,学生们可以进行下载等操作。

  啰啰嗦嗦了一大堆,下面开始进行FTP服务器搭建和上传下载功能开发吧!

♋ 3.4、FTP服务器搭建

  安装环境: Win10

  步骤一: 安装FTP服务器支持和IIS管理平台。

  操作步骤: 电脑 => 控制面板 => 程序和功能 => 启用和关闭Windows功能 => Internet Infomation Services => 勾选【FTP服务器】和Web管理工具的【IIS管理控制台】=> 点击确定等待安装完成

  步骤二: 打开IIS管理器

  操作步骤: 电脑 => 控制面板 => 管理工具 => Internet Infomation Services(IIS)管理器

  步骤三: 创建FTP服务器

  操作1: 在某个盘符如D盘,创建一个FTP共享文件夹,用于FTP共享文件存放地址

  步骤2: 右键IIS管理器左边导航栏 => 添加FTP站点

  步骤3: 指定【站点名称】和【FTP共享的文件夹路径】

  步骤4: 配置FTP服务器相关信息

  步骤5: 配置FTP服务器验证和权限信息【注意:如果想通过程序实现上传、下载功能,身份验证中的基本选项需要勾选上,后面程序需要通过这个方式使用账号和密码登录到FTP服务器】

  步骤5: 到这一步,一个FTP服务器就已经搭建完成了,在IIS管理器还可以对搭建好的FTP服务器进行配置管理。

  步骤6: 在同一网段的小伙伴可以通过:ftp://ftp配置的ip地址 格式访问到FTP服务器。

♌ 3.5、FTP服务器搭建出现的问题

  问题一: FTP按照流程搭建完成后,在同一网段的小伙伴却无法访问!

  原因: 可能是开启了防火墙拦截,需要在防火墙放行FTP服务器。

  解决: 电脑 => 控制面板 => Windows Defender 防火墙 => 允许应用通过Windows Defender 防火墙进行通信 => 勾选【FTP服务器】

🏡 四、通过程序实现FTP文件的上传和下载

  通过上面的步骤,我们完成了FTP服务器的搭建,可以手动将文件上传到服务器,让在同一网段的小伙伴自由从上面下载,但是,在实际业务开发中,我们需要的是通过程序实现上传和下载,而不是通过人为手动的方式,下面,就来看看如何实现吧。

  说明: 文章只贴出部分代码,全部案例代码已经上传到Gitee,需要者可直接访问下载(有帮助记得给个star呀):【实战-FTP服务器搭建,实现上传、下载】

💗 4.1、项目结构

💙 4.2、实现技术

  1、Apache下的commons-net依赖包: 它包含了一组网络实用工具和协议实现,支持的协议包括:FTP、NNTP、NTP、POP3(S)、SMTP(S)、Telnet、Whois等等,可以用它来实现文件的上传和下载功能。

  2、spring-boot-starter-web: web相关支持

  3、SpringBoot依赖: 快速构建JAVA项目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码		<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
<version>3.6</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

💚 4.2、相关配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码ftp:
client:
# ftp客户端文件使用的字符集
charset: GBK
server:
# ftp服务器绑定ip或者域名
hostname: 127.0.0.1
# 端口
port: 21
# 连接ftp服务器的用户名
username: user
# 密码
password: 123456
# ftp的共享文件路径
workingPath: D:/share/FTPServer
# ftp服务器文件使用的字符集(用于上传包含中文名的文件和下载包含中文名的文件 - 很重要)
charset: ISO-8859-1

💛 4.3、核心代码

  因为FTP服务器的上传、下载都是很通用的功能,所以博主封装成了一个工具类,有需要的小伙伴可以引入依赖和相关配置后,直接就可以使用该工具类。

  1、上传核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码/**
* 上传
*
* @return
*/
public boolean upload(FtpUploadParam param) {
boolean flag = false;
FTPClient ftpClient = new FTPClient();
//1 测试连接
if (connect(ftpClient, param.getHostname(), param.getPort(), param.getUsername(), param.getPassword())) {
try {
//2 检查工作目录是否存在,不存在则创建
if (!ftpClient.changeWorkingDirectory(param.getWorkingPath())) {
ftpClient.makeDirectory(param.getWorkingPath());
}
// 将文件编码成Ftp服务器支持的编码类型(FTP协议里面,规定文件名编码为iso-8859-1,所以目录名或文件名需要转码。)
String fileName = new String(param.getSaveName().getBytes(ftpClientCharset), ftpServerCharset);
// 3 上传文件
if (ftpClient.storeFile(fileName, param.getInputStream())) {
flag = true;
} else {
log.warn("FtpUtils uploadFile unsuccessfully!!");
}
} catch (IOException e) {
log.error("FtpUtils upload in error:{}", e);
} finally {
disconnect(ftpClient);
}
}
return flag;
}

  2、下载核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
java复制代码public boolean download(FtpDownloadParam param, String downloadFileName) {
FTPClient ftpClient = new FTPClient();
FileOutputStream out = null;
//1 测试连接
if (connect(ftpClient, param.getHostname(), param.getPort(), param.getUsername(), param.getPassword())) {
try {
File file;
String localPath = param.getDownloadPath() + param.getFileName();
out = new FileOutputStream(new File(localPath));
//2 检查工作目录是否存在,不存在返回
// if (!ftpClient.changeWorkingDirectory(param.getWorkingPath())) {
// return false;
// }
/*
* 打开FTP服务器的PASS模式(不记得FTP协议支持的模式请翻到文章第一阶段)
* 这个方法的意思就是每次数据连接之前,ftp client告诉ftp server开通一个端口来传输数据. 因为ftp
* server可能每次开启不同的端口来传输数据,但是在linux上,由于安全限制,可能某些端口没有开启,可能出现出现阻塞
*/
ftpClient.enterLocalPassiveMode();
// 设置文件的传输方式
ftpClient.setFileType(FTP.BINARY_FILE_TYPE);
// 将文件编码成Ftp服务器支持的编码类型(FTP协议里面,规定文件名编码为iso-8859-1,所以目录名或文件名需要转码。)
// 缺少编码转换会导致:从FTP服务器下载下来的文件是破损的,无法被打开
boolean b = ftpClient.retrieveFile(new String(downloadFileName
.getBytes(clientCharset), serverCharset), out);
out.flush();
} catch (IOException e) {
log.error("FtpUtils upload in error:{}", e);
return false;
} finally {
try{
if(Objects.nonNull(out)){
out.close();
}
}catch (Exception e){
log.error("FtpUtils upload in error:{}", e);
}
disconnect(ftpClient);
}
}
return true;
}

💜 4.4、执行结果

  1、演示代码:

  2、上传文件到FTP服务器:

上传文件到FTP服务器

  3、从FTP服务器下载文件到本地:

从FTP服务器下载文件到本地

💝 4.5、开发过程中遇到的坑

  问题1、连接FTP服务器失败:

  问题描述: 在配置文件中指定了账号和密码,但是却连接失败。

  解决方案: 经过排查,发现是在搭建FTP服务器的时候只开启了匿名验证,没有开启基本验证(账号和密码登录的方式),只需要到IIS管理器中开启【基本验证】即可。

  解决步骤: 电脑 => 控制面板 => 管理工具 => IIS管理器 => 搭建好的FTP服务器 => FTP身份验证 => 开启基本身份验证模式。

  问题2、FTP上传中文文件失败:

  问题描述: 选择文件名为英文的文件上传正常,但是选择中文的文件名上传却失败,错误信息:550-The filename, directory name, or volume label syntax is incorrect. 。

  解决方案: 经过排查,发现 FTP协议里面,规定文件名编码为iso-8859-1(注意:这个现在是在Windows搭建的FTP服务器出现的情况,如果是Linux环境的话,还需要查看linux默认的支持编码而定,但是需要将上传的文件名编码这个步骤是确定的),所以目录名或文件名需要转码。 所以在上传文件代码处你会看到下面的对文件解码再编码的代码:

  问题3、调用FTPClient的切换目录方法changeWorkingDirectory总是失败

  原因和解决: FTP服务器搭建的时候需要我们制定共享的一个文件路径,当我们和FTP服务器建立连接后,默认就在这个目录下了,如果想切换到该目录下的子目录,不需要写全路径。

  示例: 如果FTP服务器共享的文件夹路径为:D:/ftpserver,此时我们需要切换到ftpserver文件夹下的子文件demo中,正确的写法:fTPClient.changeWorkingDirectory("demo") 而不是 fTPClient.changeWorkingDirectory(“D:/ftpserver/demo”)

  问题4、从FTP服务器下载的文件破损,无法打开

  问题描述: 尝试从FTP服务器下载有中文字符文件名的文件,成功下载到本地后却无法正常打开,提示已经破损。

  问题原因: 原因其实和第二个问题一样,是因为包含中文字符的文件名下载时需要进行编码转换,否则下载后无法被打开。

🚀 六、写在最后

  FTP服务器实战项目所有代码都已上传到Gitee,有需要可以自取(后面会传到CSDN免费下载),如果有帮助不要忘了star哦,Gitee项目直通车如下:【实战-FTP服务器搭建,实现上传、下载】

  推荐一下: 【技术圈子】中有免费面试资源、简历模板、年终汇报PPT、CSDN VIP下载资源等等,期待您的加入!

  感兴趣者可查看主页进入。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

RabbitMQ(3) 集成 Springboot 项目

发表于 2021-11-02

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

前言

前两篇文章分别介绍了 RabbitMQ 的搭建与基础使用,哪些基础都是学习 MQ 的必要知识,而想要在项目中用到,则需要集成到我们的 Spring 项目中,本文只介绍 Springboot 如何集成 RabbitMQ,通过这个中间件给其他的微服务发送消息。

一、生产者服务搭建

1.1、建立springboot父子工程。

其中的xiaolei-server 是rabbitmq-producer 的父项目,在其下添加一个 rabbitmq的生产者服务。这样代码比较有结构。而middleware-rabbitmq 是后期对rabbitmq 封装的 SDK,里面封装一些方法和配置,后期需要,这里可以不用管他。

image-20211029104829081

1.2、配置 application.yml 文件

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码# 服务端口
server:
port: 8200
# 配置rabbitmq服务
spring:
rabbitmq:
username: test
password: test
virtual-host: test
host: 192.168.81.102
port: 5672

1.3、在项目中导入依赖

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

1.4、定义生产者,发送消息

生产者,我们继续发送上一文中讲的影片的案例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码@RestController
@RequestMapping("/firm")
public class SendFirmMsgController {

@Autowired
private RabbitTemplate rabbitTemplate;
// 1、定义交换机
private String exchangeName = "exchange_firm";
// 2、定义路由key
private String routeKey1 = "爱国.吴京";
private String routeKey2 = "爱国.沈腾";
private String routeKey3 = "动作.吴京";
private String routeKey4 = "喜剧.沈腾";

@PostMapping("/send")
public void sendMsg(){

for (int i = 1; i <=40; i++) {
// @params1: 交换机exchange
// @params2: 队列名称/routing
// @params3: 属性配置
// @params4: 发送消息的内容
if(i%4==0){
rabbitTemplate.convertAndSend(exchangeName,routeKey1,("爱国.吴京,说第"+i+"遍。").getBytes());
}else if(i%4 ==1){
rabbitTemplate.convertAndSend(exchangeName,routeKey2,("爱国.沈腾,说第"+i+"遍。").getBytes());
}else if(i%4 ==2){
rabbitTemplate.convertAndSend(exchangeName,routeKey3,("动作.吴京,说第"+i+"遍。").getBytes());
}else if(i%4 ==3){
rabbitTemplate.convertAndSend(exchangeName,routeKey4,("喜剧.沈腾,说第"+i+"遍。").getBytes());
}
System.out.println("发送第"+i);
}
}
}

1.5 初始化队列和交换机关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
typescript复制代码package com.xiaolei.rabbitmq.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* 主题消费类型配置
* @Author xiaolei
* @Date 2021/10/29 11:03
**/
@Configuration
public class TopicRabbitConfig {

/**
* 给队列取名字
* @return
*/
@Bean
public Queue firstQueue() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
// return new Queue("TestDirectQueue",true,true,false);

//一般设置一下队列的持久化就好,其余两个就是默认false
return new Queue ("queue1",true);
}
@Bean
public Queue SecondQueue() {
return new Queue ("queue2",true);
}

@Bean
public Queue ThreeQueue() {
return new Queue ("queue3",true);
}

@Bean
public Queue FourQueue() {
return new Queue ("queue4",true);
}

/**
* 给交换机取名
* @return
*/
@Bean
public TopicExchange topicExchange(){
return new TopicExchange("exchange_firm",true,false);
}

@Bean
public Binding bindingTopic1(){
return BindingBuilder.bind(firstQueue()).to(topicExchange()).with("*.吴京");
}

@Bean
public Binding bindingTopic2(){
return BindingBuilder.bind(SecondQueue()).to(topicExchange()).with("爱国.吴京");
}

@Bean
public Binding bindingTopic3(){
return BindingBuilder.bind(ThreeQueue()).to(topicExchange()).with("爱国.*");
}
@Bean
public Binding bindingTopic4(){
return BindingBuilder.bind(FourQueue()).to(topicExchange()).with("#.沈腾");
}
}

1.6 调用 postman 接口测试

发现此时在rabbitmq 上存在了新的 交换机和topci信息

image-20211029120043364

二、消费者服务搭建

image-20211029121221570

2.1 导入依赖

这过程我们在父项目中已经导入了,所以这里可以省略。

1
2
3
4
xml复制代码<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

2.2 application.yml 配置

1
2
3
4
5
6
7
8
9
10
11
yaml复制代码# 服务端口
server:
port: 8201
# 配置rabbitmq服务
spring:
rabbitmq:
username: test
password: test
virtual-host: test
host: 192.168.81.102
port: 5672

2.3 定义四个消费者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
less复制代码@RestController
public class MsgController {

@RabbitListener(bindings = @QueueBinding(
// 指定队列名字
value = @Queue(value = "queue1",autoDelete = "false"),
// 指定交换机的名字
exchange = @Exchange(value = "exchange_firm",type = ExchangeTypes.TOPIC)
))
@RabbitHandler
public void consumrmsg1(String msg){
System.out.println("吴京粉丝-------------->" + msg);
}

@RabbitListener(bindings = @QueueBinding(value = @Queue(value = "queue2",autoDelete = "false"),exchange = @Exchange(value = "exchange_firm",type = ExchangeTypes.TOPIC)))
@RabbitHandler
public void consumrmsg2(String msg){
System.out.println("爱国吴京粉丝-------------->" + msg);
}

@RabbitListener(bindings = @QueueBinding(value = @Queue(value = "queue3",autoDelete = "false"),exchange = @Exchange(value = "exchange_firm",type = ExchangeTypes.TOPIC)))
@RabbitHandler
public void consumrmsg3(String msg){
System.out.println("爱国粉丝-------------->" + msg);
}

@RabbitListener(bindings = @QueueBinding(value = @Queue(value = "queue4",autoDelete = "false"),exchange = @Exchange(value = "exchange_firm",type = ExchangeTypes.TOPIC)))
@RabbitHandler
public void consumrmsg4(String msg){
System.out.println("沈腾粉丝-------------->" + msg);
}
}

打印效果如下:

image-20211029120459138

2.4 细节讲解

消费者类中通过 @RabbitListener 和 @RabbitHandler 注解将一个方法定义为消息监听的方法。

image-20211029120838050

其他几种的类型都差不多,我们只要自己来配置对应的类就好了。目前基础配置已经完成。

三、小结

本文介绍了 RabbitMq 中与 Springboot 的集成,这是属于初级应用,后面的文章中,我们将再考虑其他的问题,研究 RabbitMq的相关特性。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文搞懂用户登录验证流程(附图) 前言 业务图解 总结 其它

发表于 2021-11-02

前言

本文通过图示及代码的方式介绍用户登录流程及技术实现,内容包括用户登录,用户验证,如何获取操作用户的信息以及一些黑名单及匿名接口如何免验证相关的实现。

结合网关相关知识食用更佳

业务图解

对于用户登录来说、涉及到了用户注册、登录验证几个方面,通过流程图演示如何处理(新用户/老用户)登录

image.png

流程解读

  • 客户端-登录界面(通常手机验证码登录)

1.填写手机号 2.发送验证码 3.填写验证码 4.勾选新用户自动注册

  • 服务端-用户验证

1.验证账号验证码是否正确 2.验证用户是否存在(不存在出初始化用户信息) 3.完成验证生成token 4.将token返回给客户端

用户信息设计

字段 描述 类型 是否唯一
telephone 手机号 varchar 是
nickname 昵称 varchar 根据业务决定是否可重复
account 账号 varchar 是
password 密码 varchar 否
create_time 创建时间 datetime 否
modify_time 修改时间 datetime 否
…省略小程序授权码等等、根据自身业务进行增加

验证流程图解

image.png

登录验证流程涉及到了两个接口,两个缓存。1.获取验证码接口,给手机号发送验证码并设置验证码缓存,设置过期时间;2.登录接口,提交手机号及验证码,读取缓存进行匹配验证,成功则生成token返回给客户端,客户端登录成功,登录后请求头携带token进行业务请求即可。

关于token过期时间

通常我们token的过期时间是根据客户端的类型来定义的,app的过期时间会更长一些(通常一个星期),web端过期时间以小时为单位,如果控制过期时间可以将web登录和app登录拆分为两个接口(能够分流,接口压力更小),或者是根据请求头信息进行判断即可,是移动端就设置7天,是web端就设置两小时。

关于业务请求token验证

登录成功后,客户端每次请求都会携带token,通常我们会有一个网关来进行token验证,网关用于登录验证的核心就是登录成功后写入的token作为key,值为用户基础信息的缓存,图解如下:

image.png

验证成功后,重写内部请求头,将用户的的id,账号,昵称信息放入请求头中,这样可以方便业务系统获取当前操作用户信息以及权限控制等等

关于登出操作

用户携带token请求登出接口,登出接口对token对应的缓存进行删除操作,返回401即可,客户端获取到401就会跳转到登录页面

关于匿名请求(免登录)

通常匿名请求放行有两种方案,1.授权token,为token设置单位时间内请求次数;2.配置路径放行规则,对请求接口路径进行正则匹配,符合正则规则的进行放行

方案1:授权token,限制单位时间请求次数

优点就是虽然是免登录接口,但是接口的操作对象可以追溯,请求次数可控,避免被非法利用;缺点就是需要更多的编码及配置工作

技术实现

  • 1.提供一个授权token管理页面,主要管理token使用者,token的值,单位时间访问次数(如每分钟60次)
  • 2.增删改查,将授权token存放到缓存中,使用map进行存储,key为token,值为每分钟访问次数
  • 3.单位时间计数缓存,过期时间为1分钟

这时候我们需要在上面的验证流程图基础上进行升级

image.png

请求次数检查代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
java复制代码
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

/**
* 授权token请求限制缓存
*
* @author 热黄油啤酒
* @since 2021-11-01
*/
@Component
public class AuthTokenRequestLimitCache {

@Autowired
private RedisTemplate<String, Integer> redisTemplate;

private static final String AUTH_TOKEN_LIMIT_KEY_PREFIX = "auth_token_limit";

/**
* 请求次数+1并检查是否超限
*
* @param token
* @return 是否放行
*/
public boolean incrementWithCheck(String token) {
// 1.获取token请求次数限制,获取为null代表授权配置已被修改,此token已经不具备权限
Integer limit = getLimit(token);
if (limit == null) {
return false;
}
// 2.组装缓存key,读取缓存
String key = String.join(":", AUTH_TOKEN_LIMIT_KEY_PREFIX, token);
Integer count = redisTemplate.opsForValue().get(key);
// 3.没有值代表一分钟内没有请求产生了
if (count == null) {
// 初始化值
redisTemplate.opsForValue().increment(key);
// 设置过期时间
redisTemplate.expire(key, 1L, TimeUnit.MINUTES);
return true;
}
// 自增并获取当前值 大于限制的话 返回false 网关过滤器返回提示信息(如请求过于频繁)
Long inc = redisTemplate.opsForValue().increment(key);
return inc <= limit;
}

/**
* 获取限值
*
* @param token
* @return
*/
public Integer getLimit(String token) {
Object limit = redisTemplate.opsForHash().get("auth_token_limit", token);
return limit == null ? null : (Integer) limit;
}
}

对于授权接口,通常是只允许get操作,对数据进行提交或者更新是不被允许的,当然这个是业务层面的,最终取决于系统设计

方案2:请求路径正则校验

我们在网关的配置文件中增加匿名接口规则,请求到网关时,检查请求的路径是否符合匿名接口规则,是则放行,不是则进行token校验,方案比较简单,只需要对网关进行处理即可。

关于黑名单

对于一个系统来说,黑名单是最后一道关卡,所以为了安全我们需要对问题用户进行黑名单操作,具体实现也比较简单

  • 1.用户管理页面提供一个拉黑的按钮,拉黑后,这些用户的id会存储到一个set集合中去
  • 2.登录时候检查用户是否在黑名单中,是则拒绝登录并提示
  • 3.如果用户已经登录后进行拉黑操作,网关会在鉴权通过后检查用户是否在黑名单中,是则删除token对应缓存,返回401,401就会跳到登录页,步骤2就会进行拦截。

总结

用户系统是非常基础的系统,但是很多程序员工作中可能并没有真正的参与到用户系统的开发,通过此文可以对用户登录流程及配套功能有一个全面的了解。

其它登录鉴权相关文章

阿里需求挑战-十分钟内连续登录5次失败,需要等待30分钟才能登录【附图】 - 掘金 (juejin.cn)

快速搭建一个网关服务,动态路由、鉴权看完就会(含流程图) - 掘金 (juejin.cn)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用jenkins持续化集成springboot

发表于 2021-11-02

这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战

在开发环境中,打版部署是一件非常麻烦的事情,所以持续化集成能够大大减少运维的工作量,此篇文章将介绍如何持续化部署springboot项目。

1.jenkens持续化部署的原理

持续化部署的原理很简单,就是首先到你提供的svn/git上下载代码,然后获取配置的打包语句,打成jar包。然后使用ssh连接上需要发布的服务器,并执行你提供的启动语法。

2.安装

2.安装jdk

3.安装maven

4.配置jdk与maven

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

*_HOME与环境变量一致

5.其他配置

修改ssh配置。
在这里插入图片描述

配置远程地址,也就是运行项目服务器。
在这里插入图片描述

修改端口号。
在这里插入图片描述

6.安装插件

安装Maven Integration,Publish Over SSH,使用ssh工具连接运行jar包的服务器。

在这里插入图片描述

在这里插入图片描述

7.新建运行空间

在这里插入图片描述

在这里插入图片描述

  1. 1处:为新建空间名称
  2. 2处:需要下载前文的maven插件

8.配置

1.配置保留的jar包数

在这里插入图片描述

这里红圈的意思表示,只保留最新打版的3个jar包。否则jenkens服务器会因为内存不足而崩溃。

2.配置svn地址

在这里插入图片描述

1处填写svn的地址,点击3处会弹出以下页面。需要填写以上svn的账号密码。然后在2处选择3处填写好的信息。

在这里插入图片描述

3.设置打版频率

以下设置为每一次提交都打版。
在这里插入图片描述

此处也可以定时构建。配置如下。
在这里插入图片描述

4.此处添加日志输出

在这里插入图片描述

5.打包命令

在这里插入图片描述

6.配置ssh命令

在这里插入图片描述

以上表示只有在打包成功情况下执行下文流程。

在这里插入图片描述

  • 1处:前面配置运行文件服务器的配置 。
  • 2处:jenkins服务器本地发布jar包的路径 + jar包名称 可以填写 target/*.jar
  • 3处:打包路径 可以填写 target
  • 4处:ssh 发送jar包的路径
  • 5处:ssh的命令

部署原理是,jenkin把代码拉到本地,然后打成jar包到2位置处 ,然后在jar包发到服务器4的路径下,然后使用SSH执行5处脚本。

5处的命令。

1
2
3
js复制代码cd /usr/local/*
./*.sh stop
./*.sh start

另提供服务器上启动脚本 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
bash复制代码#替换这里jar包的路径,其它代码无需更改
APP_NAME=/usr/local/jar/*.jar
#使用说明,用来提示输入参数
usage() {
echo "Usage: sh item.sh [start|stop|restart|status]"
exit 1
}

#检查程序是否在运行
is_exist(){
pid=`ps -ef|grep $APP_NAME|grep -v grep|awk '{print $2}'`
#如果不存在返回1,存在返回0
if [ -z "${pid}" ]; then
return 1
else
return 0
fi
}

#启动方法
start(){
is_exist
if [ $? -eq 0 ]; then
echo "${APP_NAME} is already running. pid=${pid}"
else
nohup /usr/local/jdk/jdk1.8.0_231/bin/java -jar ${APP_NAME} > earlywarning.out 2>&1 &
echo "
===============
程序成功启动!
==============="
fi
}

#停止方法
stop(){
is_exist
if [ $? -eq "0" ]; then
kill -9 $pid
echo "
==============
程序成功关闭!
=============="
else
echo "${APP_NAME} is not running"
fi
}

#输出运行状态
status(){
is_exist
if [ $? -eq "0" ]; then
echo "${APP_NAME} is running. Pid is ${pid}"
else
echo "${APP_NAME} is NOT running."
fi
}

#重启
restart(){
stop
sleep 5
start
}

#根据输入参数,选择执行对应方法,不输入则执行使用说明
case "$1" in
"start")
start
;;
"stop")
stop
;;
"status")
status
;;
"restart")
restart
;;
*)
usage
;;
esac

以上脚本有个坑,启动时需要填写jdk绝对路径,网上说的什么杀掉进程都不对。

1
js复制代码nohup  /usr/local/jdk/jdk1.8.0_231/bin/java -jar ${APP_NAME} > earlywarning.out 2>&1 &

3.启动与关闭命令

1
2
3
4
5
6
7
8
js复制代码后台启动
nohup java -jar jenkins.war
关闭Jenkins
http://localhost:8080/jenkins/exit
重启Jenkies
http://localhost:8080/jenkins/restart
重新加载配置信息
http://localhost:8080/jenkins/reload

4.问题

1.问题一

Jenkins 启动一直显示 Jenkins正在启动,请稍后…

见:blog.csdn.net/heatdeath/a…

1
2
3
4
5
6
js复制代码需要你进入jenkins的工作目录,打开
hudson.model.UpdateCenter.xml
把
http://updates.jenkins-ci.org/update-center.json
改成
http://mirror.xmission.com/jenkins/updates/update-center.json

2.问题二

解决nohup: 忽略输入并把输出追加到”nohup.out”或者nohup: 忽略输入重定向错误到标准输出端。

见:www.yayihouse.com/yayishuwu/c…

1
2
3
4
5
js复制代码执行nohup java -jar do_iptable.jar & 运行jar会提示:nohup: 忽略输入并把输出追加到"nohup.out"

执行nohup java -jar do_iptable.jar >/dev/null & 运行jar会提示:nohup: 忽略输入重定向错误到标准输出端

修改运行方式为nohup java -jar do_iptable.jar >/dev/null 2>&1 &即可。

这里有个坑:当自定义start版本号不改变但是内容改变,jenkin不会从新拉取jar包。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试官问我JVM内存结构,我真的是

发表于 2021-11-02

面试官:今天来聊聊JVM的内存结构吧?

候选者:嗯,好的

候选者:前几次面试的时候也提到了:class文件会被类加载器装载至JVM中,并且JVM会负责程序「运行时」的「内存管理」

候选者:而JVM的内存结构,往往指的就是JVM定义的「运行时数据区域」

候选者:简单来说就分为了5大块:方法区、堆、程序计数器、虚拟机栈、本地方法栈

候选者:要值得注意的是:这是JVM「规范」的分区概念,到具体的实现落地,不同的厂商实现可能是有所区别的。

面试官:嗯,顺便讲下你这图上每个区域的内容吧。

候选者:好的,那我就先从「程序计数器」开始讲起吧。

候选者:Java是多线程的语言,我们知道假设线程数大于CPU数,就很有可能有「线程切换」现象,切换意味着「中断」和「恢复」,那自然就需要有一块区域来保存「当前线程的执行信息」

候选者:所以,程序计数器就是用于记录各个线程执行的字节码的地址(分支、循环、跳转、异常、线程恢复等都依赖于计数器)

面试官:好的,理解了。

候选者:那接下来我就说下「虚拟机栈」吧

候选者:每个线程在创建的时候都会创建一个「虚拟机栈」,每次方法调用都会创建一个「栈帧」。每个「栈帧」会包含几块内容:局部变量表、操作数栈、动态连接和返回地址

候选者:了解了「虚拟机栈」的组成后,也不难猜出它的作用了:它保存方法了局部变量、部分变量的计算并参与了方法的调用和返回。

面试官:ok,了解了

候选者:下面就说下「本地方法栈」吧

候选者:本地方法栈跟虚拟机栈的功能类似,虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。这里的「本地方法」指的是「非Java方法」,一般本地方法是使用C语言实现的。

面试官:嗯…

候选者:嗯,说完了「本地方法栈」、「虚拟机栈」和「程序计数器」,哦,下面还有「方法区」和「堆」

候选者:那我先说「方法区」吧

候选者:前面提到了运行时数据区这个「分区」是JVM的「规范」,具体的落地实现,不同的虚拟机厂商可能是不一样的

候选者:所以「方法区」也只是 JVM 中规范的一部分而已。

候选者:在HotSpot虚拟机,就会常常提到「永久代」这个词。HotSpot虚拟机在「JDK8前」用「永久代」实现了「方法区」,而很多其他厂商的虚拟机其实是没有「永久代」的概念的。

候选者:我们下面的内容就都用HotSpot虚拟机来说明好了。

候选者:在JDK8中,已经用「元空间」来替代了「永久代」作为「方法区」的实现了

面试官:嗯…

候选者:方法区主要是用来存放已被虚拟机加载的「类相关信息」:包括类信息、常量池

候选者:类信息又包括了类的版本、字段、方法、接口和父类等信息。

候选者:常量池又可以分「静态常量池」和「运行时常量池」

候选者:静态常量池主要存储的是「字面量」以及「符号引用」等信息,静态常量池也包括了我们说的「字符串常量池」。

候选者:「运行时常量池」存储的是「类加载」时生成的「直接引用」等信息。

面试官:嗯…

候选者:又值得注意的是:从「逻辑分区」的角度而言「常量池」是属于「方法区」的

候选者:但自从在「JDK7」以后,就已经把「运行时常量池」和「静态常量池」转移到了「堆」内存中进行存储(对于「物理分区」来说「运行时常量池」和「静态常量池』就属于堆)

面试官:嗯,这信息量有点多

面试官:我想问下,你说从「JDK8」已经把「方法区」的实现从「永久代」变成「元空间」,有什么区别?

候选者:最主要的区别就是:「元空间」存储不在虚拟机中,而是使用本地内存,JVM 不会再出现方法区的内存溢出,以往「永久代」经常因为内存不够用导致跑出OOM异常。

候选者:按JDK8版本,总结起来其实就相当于:「类信息」是存储在「元空间」的(也有人把「类信息」这块叫做「类信息常量池」,主要是叫法不同,意思到位就好)

候选者:而「常量池」用JDK7开始,从「物理存储」角度上就在「堆中」,这是没有变化的。

面试官:嗯,我听懂了

面试官:最后来讲讲「堆」这块区域吧

候选者:嗯,「堆」是线程共享的区域,几乎类的实例和数组分配的内存都来自于它

候选者:「堆」被划分为「新生代」和「老年代」,「新生代」又被进一步划分为 Eden 和 Survivor 区,最后 Survivor 由 From Survivor 和 To Survivor 组成

候选者:不多BB,我也画图吧

候选者:将「堆内存」分开了几块区域,主要跟「内存回收」有关(垃圾回收机制)

面试官:那垃圾回收这块等下次吧,这个延伸下去又很多东西了

面试官:你要不先讲讲JVM内存结构和Java内存模型有啥区别吧?

候选者:他们俩没有啥直接关联,其实两次面试过后,应该你就有感觉了

候选者:Java内存模型是跟「并发」相关的,它是为了屏蔽底层细节而提出的规范,希望在上层(Java层面上)在操作内存时在不同的平台上也有相同的效果

候选者:Java内存结构(又称为运行时数据区域),它描述着当我们的class文件加载至虚拟机后,各个分区的「逻辑结构」是如何的,每个分区承担着什么作用。

面试官:了解了

今日总结:JVM内存结构组成(JVM内存结构又称为「运行时数据区域」。主要有五部分组成:虚拟机栈、本地方法栈、程序计数器、方法区和堆。其中方法区和堆是线程共享的。虚拟机栈、本地方法栈以及程序计数器是线程隔离的)

欢迎关注我的微信公众号【Java3y】来聊聊Java面试,对线面试官系列持续更新中!

【对线面试官-移动端】系列 一周两篇持续更新中!

【对线面试官-电脑端】系列 一周两篇持续更新中!

原创不易!!求三连!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

CDP客户数据管理平台体系化搭建 一、Cdp系统简介 二、业

发表于 2021-11-02

一、Cdp系统简介

1、基本概念

客户数据平台(Customer-Data-Platform),简称CDP;通过采集多方客户数据(主体与线索)等,从而进行精准的客户分析和人群细分,进而实现高效的客户维系和发掘以及日常营销运营。

业务面上看Cdp是客户管理流程上的一个节点,技术面上看是重度偏向数据分析的一个平台。

数据构成

  • 主体资料:多方客户(一方、三方、线下)数据汇集,基于唯一ID标识进行客户主体构建与行为数据映射,实现结构化的模型数据管理;
  • 行为线索:通过SDK埋点的方式,采集客户多种事件类型的行为数据,例如注册、登录、点击、浏览、购买等,作为客户跟进的核心线索;

不断的完善客户主体的数据,完善相关画像分析,然后通过相关行为采集,进行精准实时的跟进,例如新客的浏览行为,老客户的点击等,都有潜在需求的可能,在Cdp系统采集到这类线索之后,迅速对客户进行沟通跟进,进行精准高效的服务。

基础流程

  • 数据采集:客户主体即多端(Web、APP、小程序等)注册用户的汇集或者渠道拓展的录入,线索多来自埋点手段的采集;
  • 客户模型:基于唯一客户ID标识,构建客户的主体结构,业务模型等,收集与整合多个业务场景下的需求数据;
  • 数据分析:对于客户数据的基本分析能力,常见的分层细化,标签化管理,画像与报表分析等,以此精准的识别客户;
  • 营销运营:上述的一系列操作,皆是为了能够对客户进行精细化的运营,以此提高客户价值降低维护的时间和营销成本;

核心价值

流量背景下获取客户的成本是非常高的,所以获客之后的精细运营,避免大量流失就尤为重要,建立一批忠诚度高的客户是成本最低但价值最高的运营手段,而CDP系统就是为了支撑该策略的实现。

2、对比Crm系统

与客户管理概念相关联的系统有不少,例如常说的CRM、CDP、DMP等等,可以不过度纠结这些系统的概念,只需要整体上有认识即可,在大多数场景中可能都是高度聚合在一个系统中,只是通过权限进行划分控制。

  • CDP:核心围绕客户数据的获取、管理、精细运营、营销等,促进客户产生交易行为;
  • CRM:核心围绕客户交易环节,数据层面相对静态,主要在于交易流程的管理、记录、服务等;
  • DMP:核心围绕标签化的数据管理平台,与CDP有部分牵扯和联系,基于标签透视客户群体;

系统平台的划分其本质是对业务流程节点的拆解,当业务复杂度较高时,这样有利于单个业务系统的快速迭代与扩展;在初期可能就一个管理系统,划分很多模块,以此降低开发和运维的成本;不同时期有不同的处理策略,对整个流程环节有清晰的认知才是应对业务多变的核心能力。

二、业务周期

1、核心模块

客户主体

客户的基本档案信息,这类数据的最大特点就是变化的频率相对低,不易获取但是容易维护,除此之外相对完善的客户主体还包括:客户联系人、系统跟进人等模块;这样构成一个完整的客户主体档案。

线索事件

通过多个产品端和业务线,进行埋点采集数据,作为跟进客户的核心线索,可以精准触达用户的需求,例如新客注册、浏览点击、其背后都是需求的驱动,通过线索事件捕捉用户需求,进而进行跟进销售推广。

客户跟进

通过线索获取客户的潜在需求,进而进行快速跟进,明确客户的需求,不断维护客户的跟进记录,持续提供精准服务的能力,这里的跟进方式可以是多样的,例如电话、拜访、短信等。

维度分析

对于客户的分析是多个方面的,常用的手段中,人群细分、标签化管理、业务报表、综合维度评分、流失预警、周期模型等,细致化的客群分析是数据识别的核心手段,这样从技术层面对客户有一次价值评估,在不同业务场景下跟进相应的重点客群。

营销推广

通过对客群数据的分析,以及标签化体系的建立,这样就可以对客群进行精准式的推广和营销,例如:基于标签的智能营销,基于种子人群的客户获取,数据越精准,营销的成本就越低,回馈的价值就越高。

2、客户周期

Cdp平台背后的业务本质,即对客户生命周期的识别和管理,不同阶段下有相应策略与手段,例如常见的客户周期划分:

  • 新客:新注册的用户,需求不明朗;
  • 普通客户:有特定的需求,但是具备一些不确定性;
  • 会员:需求明确,同时具备确定性;
  • 超级客户:提供专人跟进,差异化服务;
  • 流失客户:持续跟进没有效果,多次唤醒失败;

不管是什么类型的客户,都存在一定流失的风险,当客户流失情况出现时,从业务侧提供流失原因分析,也要从技术侧反思,是不是流程周期上不够细致,流失风险识别不及时等。

围绕客户数据采集和业务模型的搭建,从而明确客户的周期,建设已有客群的精细化运营能力。

三、架构设计

对于任何业务平台的建设,首先都是明确其背后需要解决的业务逻辑,然后对业务流程进行拆解,模块化管理和落地实现,当基本结构完善后,就是不断的迭代和优化:

客户增长

  • 数据采集:主要针对两个方面数据,主体资料持续完善,新数据与线索不断积累;
  • 渠道管理:数据采集来源的管理,不单是线上,还有线下,商务自拓等多个渠道;
  • 场景分析:不同场景下数据特点分析,识别高质量的采集环境,数据优先处理;
  • 质量监测:通过数据采集的维度,或者信息质量的识别,进行初始化过滤或者清洗;

客户档案

  • 主体数据:客户主体信息的完善,进而在各个业务环节使用,在结构上需要相对独立;
  • 线索事件:在不同业务节点采集到的线索数据,不同线索事件的背后是需求的挖掘;
  • 跟进管理:跟进人员分配,跟进结果反馈,基于结果分析客户的需求是否明确,价值高低;

客户分析

  • 细化分层:数据分层是基础能力,例如基于:客单价、交易次数、区域划分、业务价值等;
  • 标签识别:基于标签体系的客群管理,画像透视,在客户精准搜索和营销中十分关键;
  • 维度评分:综合评估客户的价值,例如常说的:活跃度、需求、购买力、数据完善度;
  • 周期模型:基于历史数据分析,阶段性评估客户所在的周期节点,进行策略化管理;
  • 流失预警:不同周期或者级别下的客户提供静默度分析,并提供预警信息,避免流失;

客户营销

  • 运营活动:在运营体系中,针对客群的特征,提供不同特点的活动,进行差异化的产品推广;
  • 营销策略:不同标签类别下的客户,进行差异化营销,或基于优质客群的共同特征营销;
  • 结果反馈:不管采用何种营销手段,对结果的反馈是至关重要的,以此验证优化营销策略;

基础能力

  • 数据存储:不同特点的数据采用相应的存储组件,在数据库选型上视野要开阔;
  • 搜索引擎:高度依赖数据的平台,对于搜索引擎建设极其重要,支撑多维度的数据查询;
  • 业务对接:例如Crm、DMP等系统对接,通常核心在数据层面,以及应用中的交互;

上述是针对Cdp平台业务流转去分析的,像一些系统基础功能,例如:权限控制、操作日志等没有多余的描述,实际上当数据体量不断膨胀时,会逐步引入大数据相关组件、规则引擎等技术来处理。

很多能力都是在遇到问题情况下,找方案、学习、试错、处理、反思总结,然后就这样积累下来了。

四、源代码地址

1
2
3
4
ruby复制代码GitEE·地址
https://gitee.com/cicadasmile
Wiki·地址
https://gitee.com/cicadasmile/butte-java-note/wikis

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

人人都会设计模式:单例模式(8种常见写法)

发表于 2021-11-02

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」

本文被《从小工到专家的 Java 进阶之旅》收录。

你好,我是看山。

个人认为单例模式是设计模式中最简单也是最常用的一种,是对有限资源合理利用的一种方式。这个模式看似简单,但是其中蕴含了关于并发、类加载、序列化等一系列深层次的知识,如果理解不够深,就有可能在高并发时遇到难以预期的异常,或者会造成资源浪费。

所以本文会从将目前Java领域最常用的几种单例模式列出来,供大家参考。

WHAT

维基百科给出了解释、实现的思路以及应该注意的地方:

单例模式,也叫单子模式,是一种常用的软件设计模式,属于创建型模式的一种。在应用这个模式时,单例对象的类必须保证只有一个实例存在。

实现单例模式的思路是:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。

单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。

类图是:

图片

WHY

正如定义所说,单例模式就是整个内存模型中,只有一个实例。实例少了,内存占用就少。同时,只有一个实例,也就只需要构建一个对象,计算就少。对于构造过程中需要大量计算或者占用大量资源的对象,只创建一次,就减少了资源占用和内存占用。

HOW

饿汉式

饿汉式是最简单的一种实现,在类装载过程中,完成实例化,避免多线程问题。

实现一:静态实例参数与静态代码块

1
2
3
4
5
6
7
8
9
10
csharp复制代码public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();

private EagerSingleton() {
}

public static EagerSingleton getInstance() {
return INSTANCE;
}
}

根据java的特性,饿汉式还可以变种写法,有的地方称为静态代码块方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码public class EagerSingleton {
private static EagerSingleton INSTANCE = null;

static {
INSTANCE = new EagerSingleton();
}

private EagerSingleton() {
}

public static EagerSingleton getInstance() {
return INSTANCE;
}
}

这两种方式只是在写法上的区别,优缺点没有区别,只是借助Java语言特性的不同写法,所以归为一类。

饿汉式有两个明显的缺点:

  1. 类装载过程即完成实例化,如果整个应用生命周期内,实例没有使用,也就是浪费资源了。
  2. 因为没有办法向构造函数传递不同的参数,如果需要通过个性化参数定制实例时,这种方式就不支持了。

实现二:静态内部类

针对饿汉式第一个缺点,我们可以借助静态内部类的方式,将对象实例化的时间延后。

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码public class EagerSingleton {
private EagerSingleton() {
}

private static class EagerSingletonInstance {
private static final EagerSingleton INSTANCE = new EagerSingleton();
}

public static EagerSingleton getInstance() {
return EagerSingletonInstance.INSTANCE;
}
}

但是,依然不能很好的解决第二个缺点,如果需要根据不同的参数实现不同的实例,可以采用下面说的懒汉式实现。

懒汉式

懒汉式比饿汉式的一个优点,就是能够在使用的时候再进行实例化。但是,馅饼总是要伴随着陷阱,懒汉式写法有更多的坑,一不小心就摔着了。

错误一:单线程实现

1
2
3
4
5
6
7
8
9
10
11
12
13
csharp复制代码public class LazySingleton {
private static LazySingleton INSTANCE = null;

private LazySingleton() {
}

public static LazySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}

之所以定义为单线程实现,是因为 INSTANCE==null这个判断,一个线程通过这个判断,开始进行对象实例化,但是还没有实例化完成,另一个线程又来了,这个时候,对象还没有实例化,就也会开始进行实例化,造成不必要的浪费。

错误二:同步方法

1
2
3
4
5
6
7
8
9
10
11
12
13
csharp复制代码public class LazySingleton {
private static LazySingleton INSTANCE = null;

private LazySingleton() {
}

public static synchronized LazySingleton getInstance() {
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
return INSTANCE;
}
}

这种方式解决了多线程的问题,但是也引入了新的性能问题:太慢。synchronized把整个方法包起来,也就是每个线程进入的时候,都需要等待其他线程结束调用,才能拿到实例,在性能敏感的场景,是比较致命的。

错误三:同步代码块之单次检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
csharp复制代码public class LazySingleton {
private static LazySingleton INSTANCE = null;

private LazySingleton() {
}

public static LazySingleton getInstance() {
if (INSTANCE == null) {
synchronized (LazySingleton.class) {
INSTANCE = new LazySingleton();
}
}
return INSTANCE;
}
}

这种写法看似将同步代码缩小,但也缩小了多线程保障,也犯了第一种写法的错误,属于没有对多线程有基本了解写出的低级错误代码。

错误四:同步代码块之双重检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
csharp复制代码public class LazySingleton {
private static LazySingleton INSTANCE = null;

private LazySingleton() {
}

public static LazySingleton getInstance() {
if (INSTANCE == null) {
synchronized (LazySingleton.class) {
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
}
}
return INSTANCE;
}
}

这种写法在一定程度上属于正确的写法,双重判断可以很好的实现线程安全和延迟加载。如果到这里就结束,那就是谬以千里的毫厘之差。

双重检查和同步代码块都没有问题,问题出在 INSTANCE=newLazySingleton()这句话。在JVM中,为了充分利用CPU计算能力,会进行重排序优化, INSTANCE=newLazySingleton()做了三件事:

  1. 为 INSTANCE 初始化栈空间
  2. 为 LazySingleton 分配内存空间,实例化对象
  3. INSTANCE 指向 LazySingleton 实例分配的内存空间

因为重排序优化的存在,真正执行的过程中,可能会出现1-2-3的顺序,也可能出现1-3-2的顺序。如果是1-3-2,INSTANCE 指向了 LazySingleton 实例分配的内存空间后,就不是null,另外一个线程进入判断null时,就会直接返回 INSTANCE,但是这个时候 LazySingleton 实例化还没有完成,就可能出现意想不到的异常。

正确:双重检查+阻止重排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
csharp复制代码public class LazySingleton {
private static volatile LazySingleton INSTANCE = null;

private LazySingleton() {
}

public static LazySingleton getInstance() {
if (INSTANCE == null) {
synchronized (LazySingleton.class) {
if (INSTANCE == null) {
INSTANCE = new LazySingleton();
}
}
}
return INSTANCE;
}
}

这种写法比上面那种,就差在 volatile这个关键字。

枚举

懒汉式和饿汉式都能够适用于多线程并发场景,但是通过反序列化或反射可以实例化对象,这样依然不能满足单例模式的要求,所以可以借助枚举实现,枚举可以完美避免多线程并发问题,而且可以防止反序列化和反射创建新对象。第一次看到这样定义单例模式,是在《Effective Java》中,多读经典书还是挺好的。

1
2
3
4
5
6
7
8
9
10
11
12
typescript复制代码public enum EnumSingleton {
INSTANCE;

public void method1() {
// do something
}

public Object method2() {
// do something and return something else
return new Object();
}
}

在开发实践中,枚举可以满足绝大部分场景,而且写法简单,定义单例的逻辑只需要三行代码,简洁而不简单,三行代码可以保证线程安全。同时枚举的反序列化只是通过name查找对象,不会产生新的对象;根据JVM规范,通过反射创建枚举对象时,会抛出 IllegalArgumentException异常。这样,相当于通过语法糖防止反序列化和反射破坏单例。

场景

  1. 无状态工具类:这种工具类不需要记录状态,只保证正确的应用就行,可以通过单例模式来定义。
  2. 数据共享:即多个不相关的两个线程或者进程之间实现通信。因为是一个实例,如果它的属性或者变量值被修改,所有引用都是同时修改的,当然需要 volatile 来定义变量。比如网站的计数器。
  3. 日志应用:通常应用会向日志文件写日志信息,为了实时向文件写,通常会使用单例模式,保证有一个实例持有文件,然后进行操作。
  4. 数据库连接池:数据库连接是一种数据库资源,使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,通过单例模式来维护,就可以大大降低这种损耗。
  5. Web应用的配置对象:读取文件需要消耗时间,如果读取大文件,消耗的时间和资源更久,所以通过单例模式可以大大降低消耗。
  6. 。。。

单例模式的场景还是比较多的,这里只是列出里几个简单的应用场景,算是抛砖引玉,如果看官们有什么其他应用场景,可以在留言区回复。


你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

什么是微服务? 微服务的挑战 微服务是什么 推荐阅读

发表于 2021-11-02

「这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战」

你好,我是看山。

我所理解的微服务,就六个字:“高内聚,低耦合”。

没错,就是这个在软件开发过程中被反复提到的六个字,各类设计模式、架构设计、从入门到放弃等各种书中总会提到,从初级到高级到骨灰级程序员、架构师挂在嘴边的也是这六个字。只不过,在微服务概念之前,这六个字被用在类、模块、组件上,微服务则是将它放在服务上。

注:上面是精简版,下面是完整版,看官自便。

微服务,2014年被大神马丁·福勒提出(所以2014年被称为微服务元年),在他的博客中提出他对微服务的理解。这个时候,微服务还高高在上,大家不知道如何落地实现。一直到2016年,随着各种技术和工具的落地,很多互联网公司才初步实现微服务,至此,微服务才真正造福(还是祸乱?)于开发界。

那到底什么是微服务?这里引用马丁·福勒原文:

The microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.

概括大意就是:

微服务是一种架构风格:

  1. 一组小服务
  2. 每个服务运行在独立进程中
  3. 服务之间使用轻量级通信
  4. 服务可独立部署
  5. 是基于业务能力实现
  6. 无集中式管理

基本上,符合上面这几条建议的架构风格,就是微服务。不过业界现在也没有统一定义,所以,如果发现以后微服务定义和上面的有偏差,一定是微服务的兼容性、包容性使然。

业界对于微服务还有一句更加剪短的描述:微服务是一组小而自治的服务。

这两个概念互相补充,微服务就是一组小、自治、基于业务的服务,以松散的服务方式,构建可独立部署的模块化应用。

小

说到“小”,有很多种观点,有的以代码行数界定,有的以重构时间界定,我个人认为这两种方式都不好。

首先,来说说以代码行数界定的方式。大家都是业内人士,都清楚行数有时候和编程习惯、开发能力有直接关系。比如,IP地址校验,正则表达式的写法和自己判断编写规则判断,代码行数就完全不一样。再者,代码行数是定2万行还是10万行,还是多少?假设定2万行,那20001行是不是就不符合微服务定义。再者,对于一个项目所依赖的基础类库、应用框架、编码规范,这些定义不同,代码行数也会有偏差。最后,对于动态语言和静态语言,其编译器、运行环境、支撑类库不同,代码行数也不尽相同。所以,用代码行数评判,是完全不可行的。

第二,用重构时间来界定。重构是一个考验对业务和架构理解的行为,功夫深浅不同,重构时间不同,功夫不到,还可能进入重构地狱,重构最后还会流产;功夫到了,可能几天时间就能重构一个异常复杂、异常庞大的系统。所以,用这种看经验、看水平的方式区分服务的微还是不微,也不可行。

我认为,这里说的小,可以类比“单一职责原则”。服务小而专,服务之间耦合低,可以实现业务的高度自治。简单说,就是能够让普通开发人员理解,业务上不掺杂不相干的业务,保持业务原子性。能够做到这样,就是小。

拿电商系统中的商品服务举例,商品包括商品基本信息、价格、库存等一系列功能。这些都属于商品,但如果把这些业务都放在一个服务里,拿这个服务就违反了小这个概念。商品主要是电商中的信息展示、下单、发货场景,展示基本信息、价格、库存,下单校验价格、库存,发货需要商品库存和仓储信息。看似几个功能中商品数据都有重叠,但是又不是完全重叠。比如商品信息展示,基本信息是最重要的,价格第二等重要,库存重要性最低。如果遇到高峰或者压力过大,库存可以降级,在下单时再做校验;如果压力再升,价格也可以不用实时更新变价。所以一个商品就可以拆分为三个服务:基本信息服务、商品价格服务、商品库存服务。这样一拆,职责单一,功能内聚。

可能有人认为把商品放在一个服务多好,接口区分就好了,实现简单。其实就“小”这个概念来说,也不是不可以,但是这会和下面的几条建议有冲突。

自治

每个微服务都是独立的个体,部署在独立进程中,每个服务一个进程。进程彼此独立,也就互相之间没有制约,无论是升级、修改、发布、回滚等一系列操作,都可以在一定情况下忽略其他服务。而且,服务在独立进程中,就可以独立监控,可以比较清晰的知道服务的运行情况,监控服务占用物理资源、运行Metric等就更加简单。这也算是服务独立进程部署的一个附加价值。

服务部署在独立进程中,彼此之间耦合低,通过网络通信时,还需要承诺使用技术无关的轻量级通信协议,最常见的是 HTTP/REST 或者 RPC 通信。很多关于微服务通信的建议中不推荐 RPC,是因为这个很可能会引入技术限制,但是目前的很多 RPC 组件,都提供了多语言支持,而且 RPC 的效率明显高于 HTTP/REST。但是不推荐使用强语言依赖的通信协议,比如Java RMI。同时也不推荐使用使用比较重的协议,比如 WebService。这样做的好处就是服务之间不耦合,可以选择更加适合的语言、工具或平台。有一条比较好的建议是,微服务的服务通信接口不应该随意变动,如果需要变动,需要提供更加兼容的方式,这样能够在一定程度上减少服务修改造成的影响。

每个微服务部署在独立进程中,从另一个维度解释就是,每个服务可以独立部署,在持续集成方面就有个更灵活的时间。部署过程中的提交、静态代码扫描、单元测试等流水线,可以独立或极少的依赖其他服务进行写作测试,也能够应用蓝绿部署、金丝雀部署等各种优秀的部署实践。退一步讲,可以独立的部署,也就可以独立的回滚。

还有一点比较重要,自治能够帮助每个微服务个体可以根据业务需要由选择技术栈,需要关注的是业务本身适合的技术,而不是其他依赖或不兼容的情况。(当然,技术栈不是越多越好,技术栈过多,可能在人员协调和招聘方面会有一定的影响。)

基于业务

微服务团队是围绕业务组织的跨职能团队,产品、设计、研发、测试、架构、运维等各种角色齐全,按照业务能力组织团队,在不同业务深耕,不断优化迭代。

这里不得不提到康威定律:设计系统的组织,其产生的设计等同于组织之内、组织之间的沟通结构。

第一定律:Communication dictates design(组织沟通方式会通过系统设计表达出来)

第二定律:There is never enough time to do something right, but there is always enough time to do it over(时间再多一件事情也不可能做的完美,但总有时间做完一件事情)

第三定律:There is a homomorphism from the linear graph of a system to the linear graph of its design organization(线型系统和线型组织架构间有潜在的异质同态特性)

第四定律:The structures of large systems tend to disintegrate during development, qualitatively more so than with small systems(大的系统组织总是比小系统更倾向于分解)

组件基于业务的团队,还可以是团队成员关注产品本身,而不是只关注与项目。团队成员就是产品的主人,有责任和义务保证产品的快速发展和演进迭代。同时可以制定更加行之有效的奖惩机制,成员有更强的产品成就感、荣誉感。亚马逊有一条建议”you build, you run it”,业务团队对生产中产品付全部责任,

微服务自治的特性中提过,基于业务能力,可以更加业务特性选择不同的技术栈,这一点非常重要。

微服务的挑战

IT界有句名言:没有银弹。也就是说,没有完美的解决方案,微服务也是。

微服务是一个重型杀伤性武器,是叶轻眉留给范闲的巴雷特,但是前提是,范闲有子弹,而且会用。微服务有千百种好处,但是这些好处都是有代价的。

系统复杂性

由单体服务拆解为一系列的微服务,也就形成了分布式系统,系统复杂性不可同日而语。

首先是服务性能。相较于进程内的方法调用,微服务之间只能通过进程间通信协议进行沟通,其通信效率依赖于网络和带宽等物理因素。同时,从原来的进程内部方法调用,变成服务之间网络通信,也会对网络和带宽带来压力。两者之间,互为因果,彼此影响。

然后是服务的可靠性,服务增多,服务发生故障的概率不变的情况下,发生故障的次数就会增多,可靠性随之降低。同时,由于第一中情况引入的网络延迟等,服务通信失败的情况更加复杂。比如,网络超时这种异常情况:被调用方已经开始处理请求,而且最终会成功,但是调用方出现网络超时这种错误,如果单纯的认为服务调用异常,本地事务回滚,就可能造成数据的不一致性(也是第二个复杂性)。

最后就是开发、测试、问题定位也变得不可控:

  • 对于开发,在单体架构中,业务之间都是方法调用,这个再简单不过,但是在微服务中,就得改为接口调用,而且还要根据通信接口的特性捕捉异常,根据捕捉异常的类型进行处理。

而且,在需要事务的场景中,单体架构本地事务就可以解决绝大部分问题,但是在微服务场景中,就需要引入分布式事务,不同的分布式事务实践又需要不同的回滚实现。

  • 对于测试,单体架构的测试可以使用简单的测试用例,但是在微服务中,服务之间彼此依赖,一个业务可能涉及多个服务之间的数据,启动多个服务联合测试,会造成人力成本提升;

使用Mock接口,就增加了开发工作量,也对测试人员有了更高的要求。

  • 对于问题定位,一旦测试过程中或线上出现问题,需要定位问题,找到问题出现的原因,进而解决问题。

但是在微服务架构中,因为服务众多,一个错误产生的原因,可能是调用方数据不准确,也可能是被调用方逻辑有问题,甚至是被调用方的下游被调用方出现的异常。

数据一致性

如上面提到的,因为微服务之间调用的方式由方法调用变为服务通信调用,而且数据分而治之,所以没有办法依赖于简单的数据库事务解决,所以数据一致性就是问题。所以会出现两种方式,一种是分布式事务保证数据同步一致性,一种的基于异步消息保证数据最终一致性。无论哪种方式,又会增强第一种挑战:系统复杂性。

运维复杂性

运维的复杂性主要体现在配置、部署、监控等方面。

随着服务的增多,服务参数、数据库、缓存等一些列启动运行和依赖配置随之增多,而且变得复杂,而且不同的环境,还需要不同的配置。

服务增多,部署成本随之增高。在单体架构时代,只有一个应用包。但是在微服务时代,可能需要上百个服务。如果没有流水线这样的持续发布工具,单靠人力,部署将会是运维的噩梦。

在微服务时代,监控是必不可少的。监控包括服务监控、系统监控、指标告警、日志采集等,不单单是需要监控的终端变多,监控的数据量、异常分析都会增多,这也都提高了运维复杂性。

测试复杂性

在系统复杂性中提到过测试变得更加复杂。这个复杂不单单是测试的方式,还有测试的范围。在微服务中,为了覆盖所有的场景,测试可以分单元测试、服务测试、端到端测试。为了简便,服务测试又分了Mock和Stub等不同的流派。

微服务是什么

微服务就是将“高内聚、低耦合”应用到服务中的一种软件研发建议,是一种更适合当下业务快速多变的架构风格,是一组小、自治、基于业务的模块化松散服务。

推荐阅读

  • 什么是微服务?
  • 微服务编程范式
  • 微服务的基建工作
  • 微服务中服务注册和发现的可行性方案
  • 从单体架构到微服务架构
  • 如何在微服务团队中高效使用 Git 管理代码?
  • 关于微服务系统中数据一致性的总结
  • 实现DevOps的三步工作法
  • 系统设计系列之如何设计一个短链服务
  • 系统设计系列之任务队列
  • 软件架构-缓存技术
  • 软件架构-事件驱动架构

你好,我是看山。游于码界,戏享人生。如果文章对您有帮助,请点赞、收藏、关注。欢迎关注公众号「看山的小屋」,发现不一样的世界。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…438439440…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%