# 共享订阅

EMQX 实现了 MQTT 的共享订阅功能。共享订阅是一种订阅模式，用于在多个订阅者之间实现负载均衡。客户端可以分为多个订阅组，消息仍然会被转发到所有订阅组，但每个订阅组内只有一个客户端接收消息。您可以为一组订阅者的原始主题添加前缀以启用共享订阅。EMQX 支持两种格式的共享订阅前缀，分别为带群组的共享订阅（前缀为 `$share/<group-name>/`）和不带群组的共享订阅（前缀为 `$queue/`）。两种共享订阅格式示例如下：

| 前缀格式     | 示例           | 前缀        | 真实主题名 |
| ------------ | -------------- | ----------- | ---------- |
| 带群组格式   | $share/abc/t/1 | $share/abc/ |t/1|
| 不带群组格式 | $queue/t/1     | $queue/ |t/1|

您可以使用客户端工具连接 EMQX 并尝试这个消息服务。 本节介绍了共享订阅的机制并演示了如何使用 [MQTTX Desktop](https://mqttx.app/zh) 和 [MQTTX CLI](https://mqttx.app/zh/cli) 来模拟客户端尝试通过共享订阅来接收消息。

## 带群组的共享订阅

您可以通过在原始主题前添加 `$share/<group-name>` 前缀为分组的订阅者启用共享订阅。组名可以是任意字符串。EMQX 同时将消息转发给不同的组，属于同一组的订阅者可以使用负载均衡接收消息。

例如，如果订阅者 `s1`、`s2` 和 `s3` 是组 `g1` 的成员，订阅者 `s4` 和 `s5` 是组 `g2` 的成员，而所有订阅者都订阅了原始主题 `t1`。共享订阅的主题必须是 `$share/g1/t1` 和 `$share/g2/t1`。当 EMQX 发布消息 `msg1` 到原始主题 `t1` 时：

- EMQX 将 `msg1` 发送给 `g1` 和 `g2` 两个组。
- `s1`、`s2`、`s3` 中的一个订阅者将接收 `msg1`。
- `s4` 和 `s5` 中的一个订阅者将接收 `msg1`。

<img src="./assets/shared_subscription_group.png" alt="shared_subscription_group" style="zoom:50%;" />

## 不带群组的共享订阅

以 `$queue/` 为前缀的共享订阅是不带群组的共享订阅。它是 `$share` 订阅的一种特例。您可以将其理解为所有订阅者都在一个订阅组中，如 `$share/$queue`。

<img src="./assets/shared_subscription_queue.jpg" alt="shared_subscription_queue" style="zoom:50%;" />

## 共享订阅与会话

当客户端具有持久会话并订阅了共享订阅时，会话将在客户端断开连接时继续接收发布到共享订阅主题的消息。如果客户端长时间断开连接且消息发布速率很高，会话状态中的内部消息队列可能会溢出。为了避免这个问题，建议为共享订阅使用 clean_session=true 的会话。即：会话在客户端断开连接后立即过期。

当客户端使用 MQTT v5 时，建议设置短会话过期时间（如果不是 0）。这样客户端可以暂时断开连接并重新连接以接收在断开连接期间发布的消息。当会话过期时，发送队列中的 QoS1 和 QoS2 消息，或者飞行窗口中的 QoS1 消息将被重新分发到同一组中的其他会话。当最后一个会话过期时，所有待处理的消息将被丢弃。

更多关于持久会话功能的信息，参阅 [MQTT 持久会话与 Clean Session 详解](https://www.emqx.com/zh/blog/mqtt-session)。

## 配置共享订阅策略

EMQX 允许对共享订阅组内消息的分发方式进行精细控制。共享订阅策略用于定义 EMQX 在多个订阅者之间选择消息接收者所采用的算法。通过调整该策略，您可以根据不同的工作负载、客户端部署模式以及集群拓扑来优化消息流转效果。

### 共享订阅调度策略

共享订阅调度策略定义了 EMQX 在共享订阅组中如何选择具体的订阅者来接收消息。

您可以通过配置项 `mqtt.shared_subscription_strategy` 来设置该策略。

| 策略                        | 说明                                                         |
| --------------------------- | ------------------------------------------------------------ |
| **`random`**                | 在每次消息分发时，从共享订阅组内随机选择一个订阅者，实现整体上较为均匀但不可预测的消息分发。 |
| **`round_robin`**（默认）   | 在共享订阅组内按顺序轮询将消息分发给订阅者。EMQX 为每个发布客户端独立维护轮询进度，因此来自不同发布者的消息可能连续被投递到同一个订阅者。 |
| **`round_robin_per_group`** | 与 `round_robin` 类似，但轮询进度在每个节点上独立维护。这意味着来自集群中不同节点的消息可能仍会被投递到同一个订阅者。该策略适用于多节点集群场景，每个节点可独立处理部分发布者负载。 |
| **`sticky`**                | 在订阅者保持连接的情况下，消息会持续分发到同一个订阅者，直到该订阅者断开连接或会话结束。此策略通过将消息流保持在单一订阅者上，避免重复处理或跨订阅者共享状态。<br />首次选择的订阅者由配置项 `mqtt.shared_subscription_initial_sticky_pick` 决定。 |
| **`local`**                 | 优先将消息分发给当前节点上的本地订阅者。如果没有本地订阅者，EMQX 将随机选择集群中其他节点的订阅者，从而降低跨节点流量与延迟。 |
| **`hash_clientid`**         | 基于发布客户端的 Client ID 计算哈希值，将同一发布者的所有消息始终路由到同一个订阅者，实现确定性、稳定的消息路由。该策略适用于按客户端维度进行有状态处理的场景。 |
| **`hash_topic`**            | 基于发布主题名称计算哈希值，将相同主题的所有消息路由到同一个订阅者。该策略适用于基于主题分片（sharding）或需要稳定消息处理的场景。 |

> EMQX 会为每个发布客户端独立维护分发状态（例如轮询进度或 Sticky 订阅者分配情况）。当发布客户端断开并重新连接后，该状态会被重置并重新初始化，消息分发将重新开始。

### 初始 Sticky 选择策略

当共享订阅策略设置为 `sticky` 时，EMQX 需要确定应将首次消息流关联到哪个订阅者。EMQX 会根据配置项 `mqtt.shared_subscription_initial_sticky_pick` 来确定首次接收消息的订阅者。

该设置可控制 EMQX 是优先选择本地订阅者、基于哈希算法进行确定性选择，还是随机选择订阅者。

### 通过 Dashboard 配置策略

您可以通过 EMQX Dashboard 配置这些策略：

1. 导航至 **管理** -> **MQTT 配置** -> **通用**。
2. 确保**允许共享订阅**已启用。
3. 从下拉列表中选择所需的**共享订阅策略**。默认为 `round_robin`。
4. 如果选择了 `sticky`，需配置合适的**共享订阅初始 Sticky 选择策略**。默认为 `random`。
5. 点击**保存修改**。

这些设置会立即生效，使 EMQX 能够调整消息调度逻辑，而无需客户端进行任何额外配置。

## 使用 MQTTX Desktop 尝试共享订阅

:::tip 前置准备

- 了解 MQTT 的[共享订阅](./mqtt-concepts.md#共享订阅)。
- 能使用 [MQTTX](./publish-and-subscribe.md) 进行基本的发布和订阅操作。

:::

以下步骤演示了如何为原始主题加上 `share` 前缀让不同组的订阅者共享相同主题的订阅，以及这些订阅者将如何接收来自共享订阅的消息。

在本演示中，您可以创建一个名为 `Demo` 的客户端连接作为发布者，向主题 `t/1` 发布消息。然后，您可以创建 4 个客户端连接作为订阅者，例如 `Subscriber1`、`Subscriber2 `、 `Subscriber3` 和 `Subscriber4`。订阅者可以分为 `a` 和 `b` 两个组，并且两个组都订阅主题 `t/1`。

1. 启动 EMQX 和 MQTTX Desktop。点击**新建连接**创建一个名为 `Demo` 的客户端连接作为发布者。

   - 在**名称**栏中输入`Demo`。
   - 在本演示中，**服务器地址**使用本地主机 `127.0.0.1` 作为示例。
   - 其它设置保持默认，点击**连接**。

   ::: tip

   [MQTTX Desktop](./publish-and-subscribe.md#mqttx-desktop) 中介绍了更多详细的连接创建信息。

   :::

   <img src="./assets/retain-message-new-connection.png" alt="retain-message-new-connection-general" style="zoom:35%;" />

2. 点击**连接**窗格中的 **+** -> **新建连接**创建 4 个新连接作为订阅者。将名称分别设置为 `Subscriber1`，`Subscriber2`，`Subscriber3` 和 `Subscriber4`。

3. 在**连接**窗格中依次选择订阅者客户端，点击**添加订阅**为各个订阅者创建共享订阅。根据下面的规则在主题栏中输入正确的主题。

   为了给多个订阅者分组，您需要在订阅的主题`t/1`前加上组名 `{group}` 。为了使他们同时订阅同一个主题，您还需要在组名前加上前缀 `$share`。

   在**添加订阅**弹出窗口中：

   - 将 `Subscribe1` 和 `Subscriber2` 订阅的**主题**设为 `$share/a/t/1`。
   - 将 `Subscriber3` 和 `Subscriber4`的**主题**设为 `$share/b/t/1`。

   在以上主题示例中，

   - 前缀 `$share` 表明这是一个共享订阅。
   - `{group}` 为 `a` 和 `b`，也可以是其他自定义的名称。
   - `t/1` 是原始主题。

   其他选项保留为默认设置。点击**确定**。

   <img src="./assets/shared-subscription.png" alt="shared-subscription" style="zoom:35%;" />

4. 在**连接**窗格中选择客户端 `Demo`发布消息。

   - 发布一条主题为 `t/1` 的消息。`a` 组的客户端 `Subscriber1` 和 `b` 组的 `Subscriber4` 都会收到消息。

     <img src="./assets/shared-subscription-1.png" alt="shared-subscription-1" style="zoom:35%;" />

   - 再次发送一条相同的消息。`a` 组的客户端 `Subscriber2` 和 `b` 组的客户端 `Subscriber3` 都会收到消息。

     <img src="./assets/shared-subscription-2.png" alt="shared-subscription-2" style="zoom:35%;" />

::: tip

当共享订阅的消息被发布， EMQX 会同时将消息转发到不同的组，但是同一个组内一次只有一个订阅者会收到消息。

:::

## 使用 MQTTX CLI 尝试共享订阅

1. 将四个订阅者分为两个组，并订阅主题 `t/1`：

   ```bash
   # 客户端 A 和 B 订阅主题 `$share/my_group1/t/1`
   mqttx sub -t '$share/my_group1/t/1' -h 'localhost' -p 1883
   
   ## 客户端 C 和 D 订阅主题 `$share/my_group2/t/1`
   mqttx sub -t '$share/my_group2/t/1' -h 'localhost' -p 1883
   ```

2. 使用一个新的客户端，向原始主题 `t/1` 发布 4 条 payload 为 `1`、`2`、`3` 和 `4` 的消息：

   ```bash
   mqttx pub -t 't/1' -m '1' -h 'localhost' -p 1883
   mqttx pub -t 't/1' -m '2' -h 'localhost' -p 1883
   mqttx pub -t 't/1' -m '3' -h 'localhost' -p 1883
   mqttx pub -t 't/1' -m '4' -h 'localhost' -p 1883
   ```

3. 检查每个订阅组中的客户端接收到的消息：

   - 订阅组 1(A 和 B) 和 订阅组 2 (C 和 D) 同时接收到消息。
   - 同一组中的订阅者每次只有一个接收到消息。
