1. 为什么要在Spring Boot里接入MQTT
我最初接触MQTT,是在做智能硬件数据采集的时候。设备端上报数据、服务端下发指令,听起来就是一个TCP长连接的事,但真正自己写协议、管会话、做心跳,你会发现那些看似简单的长连接在弱网环境下崩溃得毫无脾气。后来换成了MQTT,整个通信链路一下子清爽了:设备只管连Broker、订阅主题、收发消息,服务端也只需要关心业务逻辑,不用维护成千上万个连接状态。
MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)是一种基于发布/订阅模型的轻量级消息协议,设计目标就是低带宽、高延迟、不可靠网络下的机器间通信。它和HTTP最大的区别在于:HTTP是请求-响应模型,客户端主动请求,服务端被动应答;而MQTT是事件驱动模型,任何一端都可以随时发布消息,对端通过订阅关系主动接收。这个特性决定了它非常适合物联网设备上报、服务端推送、消息广播这类场景。
这篇东西主要面向两类人:一类是刚接手Spring Boot项目,需要在里面集成MQTT通信的开发者,另一类是已经能跑通基础流程,但在可靠性、动态订阅、消息不丢失这些实际问题上还在踩坑的人。我会把从Broker搭建到Spring Boot整合、从消息可靠投递到动态订阅的实现细节全部拆开讲一遍,再加上我在生产环境里实测过的问题排查记录,尽量做到你可以照着写、抄着用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先搞懂MQTT的几个核心概念,再看代码
2.1 主题(Topic)与通配符,消息路由的核心机制
MQTT里没有队列的概念,消息路由全靠主题。主题是一个UTF-8字符串,用斜杠分层,比如device/001/temperature、device/002/status。发布者往主题上发消息,订阅者订阅主题,Broker负责把消息从发布者转发给所有匹配的订阅者。
主题设计直接影响业务代码的复杂度。我的建议是:第一层放业务域,第二层放设备类型,第三层放设备ID,之后可以继续细化。比如factory/device/temperature/001,这样既方便按类型订阅,也方便按设备精确订阅。
通配符是订阅时用的,分两种:+匹配一层,#匹配多层。订阅device/+/temperature能收到所有设备的温度数据,订阅factory/#能收到factory域下所有消息。需要注意,通配符只能用在订阅端,发布端不允许使用通配符。这点很多新手会搞混,发了半天消息发现Broker报错,回头一看是在publish的主题里写了+。
2.2 QoS等级,决定消息能不能丢
QoS是MQTT最核心、也最容易让开发者困惑的机制,分0、1、2三级:
- QoS 0:最多一次。发出去了就不管了,网络断了消息就丢。适合遥测数据、日志这类丢了也无所谓的场景。
- QoS 1:至少一次。Broker收到消息后会回一个PUBACK,没收到就重发。能保证不丢,但可能重复。
- QoS 2:恰好一次。通过四步握手保证消息既不丢也不重复,代价是性能最差、开销最大。
这里有个关键点:QoS是发布消息时的发送质量,也是订阅时的接收质量。发布端设QoS 1、订阅端设QoS 0,最终实际生效的是两者中较低的等级——QoS 0。所以如果业务要求消息不能丢,发布和订阅两端都得设成QoS 1或更高,否则你一厢情愿的“至少一次”根本不会生效。
2.3 Clean Session与会话机制,离线消息怎么收
连接Broker时有个cleanSession参数,这个在Spring Boot的MQTT客户端配置里经常被忽略,但它决定了离线消息的行为。如果cleanSession设为true,Broker不保存会话状态,设备离线期间的消息直接丢掉;设为false时,Broker会保存会话和离线消息,等设备重新连上来再补发。
对服务端集成的场景,我的建议是cleanSession设为false,同时把QoS设为1,这样设备短暂断线期间的消息不会被丢。但要注意,这会增加Broker的存储开销。高并发场景下大量客户端都开着持久会话,Broker的内存和磁盘压力会很明显,需要配合消息过期时间(message expiry interval)来控制积压。
2.4 心跳、遗嘱和保留消息
这三个机制在开发中各有用途。心跳(Keep Alive)是客户端在空闲时发送PINGREQ维持连接,Broker超过1.5倍心跳周期没收到消息就判定掉线。遗嘱(Will)是客户端在连接时预置一条”遗言”,当客户端异常掉线时Broker帮它发出去,可以用来做设备上下线通知。保留消息(Retained Message)是Broker为某个主题保留的最后一条消息,新订阅者上线时立刻收到,适合下发设备初始配置。
3. 环境准备:MQTT Broker的搭建与选型
3.1 Broker选型,从Mosquitto到EMQX
做本地开发和测试,Mosquitto是最轻量的选择,Windows上解压就能跑,配置文件也简单。生产环境我推荐EMQX,它基于Erlang/OTP开发,千万级连接是它的设计目标,支持集群、规则引擎、数据桥接,而且有可视化的Dashboard,排查问题方便。规则引擎允许你做消息转发、格式转换,比如设备上报的原始数据想在MQTT层直接转成HTTP回调,就不需要业务代码介入,在EMQX里配一条规则即可。
选型逻辑很简单:开发阶段Mosquitto够了,因为你要验证的是代码逻辑,不是Broker性能;生产环境直接用EMQX,能省掉很多运维层面的麻烦,比如连接数监控、消息轨迹追踪、慢订阅检测这些功能,都是自带能力。
3.2 Windows下把Mosquitto装成本地服务
Mosquitto的Windows安装包内置了服务安装功能。下载zip包解压后,找一个管理员权限的终端,先注册服务:
bash复制mosquitto install
装好之后可以通过Windows服务管理器启动,或者命令行net start mosquitto。我用这种方式装了好几次,有个细节需要注意:配置文件路径必须是绝对路径,相对路径会导致服务启动失败,而且Windows服务管理器里只显示“启动失败”,不显示具体原因,排查起来很头大。建议在安装前先把mosquitto.conf里需要改的配置(监听端口、日志路径)写好,默认的127.0.0.1默认监听端口是1883,如果要开启匿名访问还是设置密码,都在这个文件里控制。
3.3 Docker方式快速起一个EMQX
Docker部署是最省心的方式,EMQX官方镜像直接就能用:
bash复制docker run -d --name emqx -p 1883:1883 -p 8083:8083 -p 8084:8084 \
-p 18083:18083 emqx/emqx:5.0.0
端口说明:1883是MQTT普通端口,8883是SSL端口,8083是WebSocket端口,18083是Dashboard管理界面。起来之后浏览器访问http://localhost:18083,默认账号admin/public,就能看到设备连接状态、消息收发情况、主题订阅列表,排查问题比看日志直观得多。
3.4 网络连接测试工具
Broker起来之后,我习惯先用MQTTX(一个跨平台客户端工具)手动连一下,发条消息验证Broker本身没问题再开始写代码。这个步骤能帮你把问题隔离在Broker环境还是代码逻辑上,别等到代码写完了才连不上,结果发现是Broker配置的问题,白折腾半天。
4. Spring Boot整合MQTT:从配置到代码实现
4.1 项目依赖与基础配置
Spring Boot没有官方原生的MQTT starter,但Spring Integration提供了一个spring-integration-mqtt模块,它和Spring Boot的整合性很好,可以基于spring-integration的框架做消息通道处理,也可以直接用Eclipse Paho (org.eclipse.paho.client.mqttv3) 这个最常用的Java MQTT客户端库。我选的是Paho,因为它足够简洁,API不绕,出了问题也好排查。
xml复制<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.5</version>
</dependency>
配置文件application.yml:
yaml复制mqtt:
broker:
url: tcp://localhost:1883
username: admin
password: public
client:
id: spring-boot-server-001
# 连接超时时间(秒)
connection-timeout: 10
# 心跳间隔(秒)
keep-alive: 20
# 清理会话
clean-session: false
# 自动重连
automatic-reconnect: true
这里有个关键点:automatic-reconnect虽然是Paho自带的重连机制,但实测下来它只负责断线的自动重连,如果断开期间业务需要恢复订阅,就得自己处理。因为cleanSession为false时,Broker会记录订阅关系,重连后不需要重新订阅;但如果设成true或者Broker重启导致会话丢失,就需要在连接恢复后重新走一遍订阅逻辑。
4.2 配置类与连接管理
java复制@Configuration
@ConfigurationProperties(prefix = "mqtt")
public class MqttConfig {
private String broker;
private String username;
private String password;
private String clientId;
@Bean
public MqttClient mqttClient() throws MqttException {
MqttClient client = new MqttClient(broker, clientId, new MemoryPersistence());
MqttConnectOptions options = new MqttConnectOptions();
options.setUserName(username);
options.setPassword(password.toCharArray());
options.setCleanSession(false);
options.setConnectionTimeout(10);
options.setKeepAliveInterval(20);
options.setAutomaticReconnect(true);
client.connect(options);
return client;
}
}
这里的MemoryPersistence是Paho的默认持久化方式,把消息存在内存里。如果应用重启,未完成的消息就丢了。业务层面如果要求不丢消息,可以考虑用MqttDefaultFilePersistence把消息落盘,Broker宕机、客户端重启后都能恢复。不过在生产环境,我更推荐消息进业务系统后自己做持久化,MQTT层只负责传输,不承担存储职责。
4.3 订阅与消息回调的处理方式
Paho的MqttCallback接口里有三个方法:connectionLost(连接丢失)、messageArrived(消息到达)、deliveryComplete(消息送达)。实际开发中消息处理应该起独立线程,不要在回调里直接阻塞——回调线程被占住,后续的消息全部排队,积压多了就超时断开。
java复制@Component
public class MqttMessageHandler implements MqttCallback {
@Autowired
private MqttClient mqttClient;
@PostConstruct
public void init() throws MqttException {
mqttClient.setCallback(this);
mqttClient.subscribe("factory/#", 1);
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
// 这里用线程池处理消息,避免阻塞回调线程
executorService.submit(() -> {
try {
// 业务处理:解析JSON、落库、调用其他服务
} catch (Exception e) {
// 记录失败消息到本地表,等补偿
}
});
}
@Override
public void connectionLost(Throwable cause) {
// 记录日志,触发告警,必要时手动重连
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
// 消息确认完成
}
}
subscribe的第二个参数同样控制订阅这端的QoS,这里设为1,配合发布端QoS 1,才能保证消息至少一次投递。注意factory/#订阅了整个域,如果这个服务只处理特定类型的消息,订阅越精确越好,可以减轻无谓的消息流量。
4.4 消息发布工具封装
java复制@Component
public class MqttPublisher {
@Autowired
private MqttClient mqttClient;
public void publish(String topic, String payload, int qos) {
MqttMessage message = new MqttMessage(payload.getBytes(StandardCharsets.UTF_8));
message.setQos(qos);
message.setRetained(false);
try {
mqttClient.publish(topic, message);
} catch (MqttException e) {
// 发布失败处理
}
}
}
这里有个容易踩的坑:publish方法是同步的,如果Broker响应慢或者网络有问题,会阻塞调用线程。并发量大的时候,可以用MqttAsyncClient做异步发送,或者用一个专门的发送线程池。另外,发布端的ClientID要全局唯一,如果两个客户端用相同的ClientID连接同一个Broker,后面的会把前面的踢掉。常见问题就是多个服务实例共用同一个ClientID,导致连接互相抢占,消息收到一半就掉线。
5. 动态订阅与多主题管理,项目改造的关键
5.1 为什么要动态订阅
订单里的subscribe("factory/#", 1)是写死在初始化阶段的,但项目做到后面就会发现这远远不够。典型的场景是:设备接入平台后才分配Topic,或者业务上按设备类型订阅不同主题。比如水表采集器、电表采集器、网关设备各有自己的主题结构,加上设备数量上千,不可能把每个主题都写死在配置里。而且设备可能随时上下线、修改归属,订阅关系也要动态变化。
5.2 从写死到动态:实现思路
动态订阅的实现思路分两步:第一步维护一个订阅关系表,持久化在数据库或Redis里;第二步在设备上线、业务变更时调用mqttClient.subscribe增加订阅,设备下线时调用mqttClient.unsubscribe取消订阅。
java复制@Service
public class SubscribeManager {
@Autowired
private MqttClient mqttClient;
private Map<String, Integer> subscribeTopics = new ConcurrentHashMap<>();
public void addTopic(String topic, int qos) {
try {
mqttClient.subscribe(topic, qos);
subscribeTopics.put(topic, qos);
} catch (MqttException e) {
// 记录日志,订阅失败不影响主流程
}
}
public void removeTopic(String topic) {
try {
mqttClient.unsubscribe(topic);
subscribeTopics.remove(topic);
} catch (MqttException e) {
// 处理异常
}
}
public void restoreSubscriptions() {
subscribeTopics.forEach((topic, qos) -> {
try {
mqttClient.subscribe(topic, qos);
} catch (MqttException e) {
// 恢复失败记录日志
}
});
}
}
restoreSubscriptions这个方法是关键:服务重启后,内存里的订阅关系全没了,而业务上的订阅关系存在库里,所以需要在应用启动时扫描数据库,把该订阅的主题全部重新订阅一遍。如果不做这一步,服务重启了,设备上报的消息全收不到,业务侧没感知,只能等告警电话打过来才知道出了问题。
5.3 动态订阅的使用场景:设备接入与配置下发
典型的设备接入流程是:设备上线,服务端收到一个系统事件(比如EMQX的$SYS/brokers/+/clients/+/connected主题事件),然后动态订阅这台设备的数据主题。比如水表采集器接入平台,它的数据主题是water/meter/{deviceId}/data,控制主题是water/meter/{deviceId}/cmd,接入成功后订阅这两个主题。
设备配置下发也是同样的逻辑:新设备注册时先订阅它的控制主题,设备上线后立刻通过这个主题下发初始化配置;如果设备还没上线,就发一条保留消息,等设备上线时立刻收到。这套机制比我见过很多团队自己维护在线状态表、定时轮询下发配置的方式高效得多,也符合MQTT事件驱动的设计意图。
5.4 主题映射策略:用Map还是用通配符
在实际项目里,我见过两种维护订阅关系的做法:一种是把所有具体设备的主题都存起来,逐个订阅;另一种是用通配符覆盖一类设备。做法一的问题是主题数量多时订阅管理复杂,而且每加一台设备都要发一次subscribe请求;做法二的问题是通配符订阅会把不相关的消息也收进来,带来额外流量。
权衡下来,按业务域做通配符订阅是最划算的方案。比如水表域订阅water/#,电表域订阅electric/#,网关域订阅gateway/#。新的水表设备接入不需要改任何订阅代码,它本来就属于water/#范围。只有出现全新的业务域时,才需要增加新的订阅。这样既控制了订阅数量,又不会漏收。
生产环境的做法是:在EMQX上把消息按业务域路由到不同的MQTT主题前缀,Spring Boot这边按前缀订阅,从源头就把消息路由简化了。这其实是个架构层面的思考,不只是代码层面的订阅逻辑。
6. 可靠性设计:消息不丢、不重、不乱
6.1 QoS 1加手动ACK,把“至少一次”做实
用到MQTT这个阶段,你会面临一个核心问题:怎么保证消息不丢。我之前拆过很多次"至少一次"的实现链路,结论是:光靠QoS 1还不够,因为QoS 1本身只是“不丢”,它同时意味着可能重复。要真正做到业务不丢也不乱,必须把消费端的手动ACK和业务幂等等机制配合起来用。
Paho客户端默认是自动ACK,消息到达messageArrived回调返回后就自动回PUBACK给Broker。问题在于:如果你的业务处理发生在另一条线程里,回调方法返回时业务可能还没处理完,此时ACK已经发回去,Broker就把这条消息清掉了。如果业务线程处理失败抛异常,这条消息就永远丢了。解决方法是回调里手动ACK:
java复制@Override
public void messageArrived(String topic, MqttMessage message) {
// 业务处理
boolean success = processMessage(topic, message);
if (success) {
// 业务处理成功,才发送ACK
mqttClient.messageArrivedComplete(message.getId());
} else {
// 处理失败,不发送ACK,Broker会重发
// 但要控制重发次数和频率,做好限流
}
}
这里有几个坑要注意:手动ACK只在MqttConnectOptions里关闭自动ACK的情况下才有意义,Paho里没有直接的开关,实际上只要你在回调里不调用messageArrivedComplete,默认走自动ACK;如果想完全手动,需要用MqttCallbackExtended和一些额外处理。实际项目中我一般不开手动ACK,而是用“业务表记录+定时扫描补偿”的方式,把失败的消息记录到数据库,定时任务重试,效果更可靠,也更容易排查。
6.2 消费幂等性:重复消息怎么扛住
QoS 1允许重复消息,尤其是在Broker重启、客户端重连之后,重发是常态。如果消费端不做幂等处理,重复下发指令可能导致设备重复执行动作,上报数据重复入库存量翻倍。
幂等处理的通用方案是唯一键去重。每条消息在业务侧生成一个messageId,消费端处理前先查一下这个messageId有没有处理过,处理过就跳过。最常用的实现是把消息ID存Redis:
java复制public boolean isDuplicate(String messageId) {
Boolean success = redisTemplate.opsForValue()
.setIfAbsent("mqtt:msg:" + messageId, "1", Duration.ofMinutes(10));
return !success;
}
这个方案的粒度是10分钟内去重,如果你有更长的去重需求,可以按消息的有效期设定。如果是数据上报入数据库,则可以直接在数据库表里给业务主键加唯一约束,让数据库帮你去重,代码更简单,也省了Redis的开销。
6.3 重连恢复与订阅重建,防掉线风暴
生产环境最怕的是网络抖动导致大批客户端同时掉线重连,这会对Broker造成连接风暴。Spring Boot服务在Paho的automatic-reconnect开启后,断线会快速重连,但需要注意两点:
第一,重连后的订阅恢复。前面讲到过,如果cleanSession为false,Broker会记住订阅关系,重连后不需要重新订阅。但如果Broker这边也重启了(比如EMQX升级重启),会话信息也会丢,客户端这边重新连接成功后,之前的所有订阅全部失效,必须在连接成功回调里主动恢复订阅。
第二,重连要有退避策略。Paho默认的重连间隔很短,快速重试模式下Broker压力很大。生产环境建议关闭automatic-reconnect,自己实现重连逻辑:第一次间隔1秒,第二次2秒,指数退避到最大30秒封顶。
我在一个项目里就遇到过掉线风暴:电表集中器固件升级,几千台设备同时掉线重连,Broker CPU直接飙到100%,最后EMQX都拒绝新连接了。当时以为代码逻辑没问题(确实没问题),但完全没有考虑到“瞬时并发重连”这种场景。从那以后我的服务端连接逻辑都加了自己写的退避重连,问题再没出现过。
6.4 消息顺序性保持与补偿机制
MQTT协议本身不保证消息的全局顺序,尤其在不同客户端、不同主题之间。同一个客户端往同一个主题发消息,QoS 1场景下基本能保持顺序,但Broker重启或网络异常时重发机制可能导致乱序。
业务上如果对顺序有要求,比如设备控制指令必须先“开启”再“调整”,就不能依赖MQTT的天然顺序。我的做法是消息体内带上业务序号(sequence number),消费端按设备维度缓存最近处理的消息序号,序号小于等于当前处理进度的直接丢弃,保证顺序且天然幂等。
补偿机制方面,我习惯在应用里加一张mqtt_message_record表,记录每一条接收到的消息和处理状态(处理中/成功/失败)。定时任务每隔一分钟扫描失败记录,重新发送到业务处理链路。这张表本身也就成了消息审计的依据——出问题的时候能精确看到某条消息什么时候收到、什么时候处理完、失败原因是什么。
7. 常见问题与排查技巧实录
7.1 连接被断开或反复重连
反复断开重连,优先看心跳周期和Broker配置是否匹配。keepAlive设置太短会导致频繁发送PINGREQ,增加网络开销;设置太长,Broker判定掉线的时间就长,客户端死锁了还要等半天才能发现。常规建议是10~30秒。
另一个常见原因是用户名密码错误、ClientID冲突。用户名密码错误时Broker会直接断开,日志会显示连接不成功;ClientID冲突则是两个客户端互踢,一连接就被挤掉,日志里看到“another client connected with same clientID”就要去查自己的部署有没有多个实例共用了同一个ID。
7.2 消息堆积与消费线程耗尽
消息量大时,回调线程满了、线程池拒绝新任务,消息就开始在Broker端积压。此时先别急着加大线程池,优先做两件事:分析消息类型,确认有没有应该被过滤掉的无效消息;检查消费速率和业务处理耗时,如果单条消息处理超过100毫秒,500条消息/秒的流量就需要50个线程,这个账很好算。
我常用的排查命令是EMQX Dashboard里看积压消息数和消费速率,如果积压持续上涨,基本可以断定消费端处理能力不足或者出现了阻塞点(比如数据库慢查询、外部接口超时)。在线程池里给每条任务加上超时控制,是防止线程被拖死的有效手段。
7.3 消息丢失的几个隐藏原因
消息丢失不一定是QoS配置问题,有几个隐藏原因很容易被忽略:
- 订阅端QoS设为0,即使发布端QoS是1,实际生效也是0,消息在网络断开时会丢。
cleanSession设为true + 客户端离线,离线期间的消息会被Broker丢弃。- 订阅关系没有恢复,服务重启后没有重新订阅之前的主题,消息到了Broker但没有订阅者,直接被丢弃。
- Retained消息只保留最后一条,如果业务依赖每条上报数据都入库,就不该用retained来传输业务数据。
排查消息丢失,最直接的手段是EMQX Dashboard的“消息追踪”功能,开启追踪后可以看到某条消息从发布、Broker接收到转发订阅者的完整链路,精确判断丢弃发生在哪一环。
7.4 报文过大导致消息发送失败
MQTT协议默认最大报文是256MB(MQTT 3.1.1规范),但Broker端通常有自己的限制。EMQX默认允许的最大Packet Size是1MB。设备端如果上报大的图片、视频类的数据就不该走MQTT,应该走HTTP或对象存储。如果确实需要用MQTT传大报文,要确认Broker的max_packet_size配置,并且客户端这边把消息分片传输,接收端按序号组装。
7.5 数据序列化格式的选择
MQTT消息体本身是二进制,用什么格式全靠业务约定。我实测过的几种格式对比:
| 格式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| JSON | 可读性好,调试方便 | 体积大,解析性能一般 | 常规业务数据、调试期 |
| Protobuf | 体积小,解析快 | 二进制不可读,需要生成代码 | 高吞吐、低带宽场景 |
| MessagePack | 体积介于JSON和Protobuf之间 | 需要引入序列化库 | 对体积有要求但不想维护Proto文件 |
物联网设备上报场景,我的默认选择是JSON,因为设备端用C语言或单片机的话,拼JSON字符串比编ProtoBuf容易得多。如果服务端是Java,接收JSON后直接用Jackson解析,开发效率高。只有在数据量极大(比如每秒上千条)时,才考虑切Protobuf优化带宽和解析性能。
7.6 测试环境与生产环境的差异处理
本地测试用的MqttClient、生产环境用的连接池和Broker集群,要注意几个配置差异:测试环境网络稳定,automatic-reconnect开着也无所谓;生产环境强烈建议自己控制重连。测试环境消息量小,线程池给个5个线程就够;生产环境要根据QPS估算线程数,并且预留缓冲。最关键的是配置要分离,把application.yml按环境拆开,连接参数、Topic前缀、ClientID生成规则都做成环境变量,避免测试环境连接串了生产环境Broker的尴尬情况。
8. 最后,几个提醒
整个Spring Boot整合MQTT的过程中,我最深的体会是:MQTT容易上手,但用好很难。上手的难在于它的概念少、示例代码多,照着写一个能收发消息的demo只要十分钟;真正的难点在于生产环境的可靠性、动态订阅、重连部署这些“非功能性”需求,而这些往往决定了系统能不能稳定跑下去。
如果你刚开始做这块,我建议按这个顺序推进:先用MQTTX手动收发消息验证Broker,再写Spring Boot的配置类和回调,跑通基础收发;接着做动态订阅和订阅恢复;最后补上幂等和补偿机制。不要一上来就追求各种高端特性,先让主链路稳定,再逐步加防护体系。
还有一点要强调:不要把MQTT当成万能的。大文件传输、实时交互这类场景,MQTT并不是最佳选择。认清它擅长的事,然后在它的能力边界内把可靠性做到极致,这比盲目追求协议特性更实际。希望这篇内容能帮你少踩几个坑,也欢迎在实践中遇到具体问题的时候交流讨论。
