[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-40":3},{"id":4,"title":5,"title_en":6,"abstract":7,"abstract_en":8,"content":9,"content_en":10,"category":11,"banner_id":12,"banner_path":13,"tags":14,"is_recommend":15,"prev_article":16,"next_article":20,"created_at":24},40,"避坑指南：IM 系统中如何优雅处理“多端登录”与连接竞争？","Guide to pitch-avoidance: How to gracefully handle \"multi-terminal login\" and connection competition in the IM system?","在构建即时通讯（IM）系统时，最让开发者头疼的往往不是消息怎么发，而是连接怎么管。\n\n想象一个场景：","When building instant messaging (IM) systems, the biggest headache for developers is often not how to send messages, but how to manage connections.\n\nImagine a scene:","在构建即时通讯（IM）系统时，最让开发者头疼的往往不是消息怎么发，而是连接怎么管。\n\n想象一个场景：用户小明在手机端聊着天，突然打开电脑端登录。此时，系统是该把手机端“踢下线”？还是允许两端同时在线？如果用户网络波动，短时间内产生两次重连请求，服务器该如何确保不会出现“旧连接没断开，新连接连不上”的尴尬？\n\n今天我们通过 Axiom 的工程实践，聊聊如何用 Go 优雅地解决多端连接竞争。\n\n---\n\n## 1. 建模：从“一对一”到“一对多”\n\n大多数初学者会将连接管理简单地设计为 `map[UserID]Connection`。但这在现代互联网场景下是行不通的。\n\n> [!IMPORTANT]\n> **深度点**：真正的多端系统需要支持“UserID + DeviceID”的双重索引。\n\n在我们的项目中，连接管家（Manager）采用了嵌套映射结构：\n\n```go\ntype Manager struct {\n    \u002F\u002F UserID -> DeviceID -> Client\n    Clients map[uint]map[string]*Client\n    Lock    sync.RWMutex\n}\n```\n\n这种结构允许同一用户在 Web、iOS、Android 等多个设备同时在线，为消息多端同步打下了物理基础。\n\n---\n\n## 2. 核心战术：先“抢占”，后“清理”\n\n当一个新的 WebSocket 连接请求进来时，最优雅的处理方式不是加个大锁死等，而是**乐观抢占**。\n\n### 2.1 抢占式注册\n在 Manager 的主循环中，我们处理新连接（Register）的逻辑如下：\n1. **保存新连接**：直接将新的 `Client` 实例存入 Map，覆盖旧的同设备记录。\n2. **异步关闭旧连接**：如果发现该设备之前已经有一个连接，不立即阻塞等待，而是拿到旧连接的引用。\n3. **触发自清理**：调用旧连接的 `Close()`。\n\n### 2.2 为什么这样更优雅？\n在 Go 中，关闭一个网络连接会触发其对应的 Read 协程（Goroutine）退出，从而自动触发注销（Unregister）流程。这种“借力打力”的设计，避免了在注册逻辑中编写复杂的清理代码，保证了主循环的轻量化。\n\n---\n\n## 3. 细节魔鬼：防止“误伤”\n\n在高并发场景下，可能会出现一种诡异情况：**连接 A 正在断开，连接 B 几乎同时连上。** 如果逻辑处理不当，连接 A 的下线逻辑可能会把刚连上的连接 B 给删掉。\n\n> [!WARNING]\n> **深度避坑**：在 Unregister（注销）逻辑中，必须进行对象指针的比对。\n\n```go\ncase client := \u003C-m.Unregister:\n    m.Lock.Lock()\n    if devices, ok := m.Clients[client.UserID]; ok {\n        \u002F\u002F 关键：只有当前内存里的连接对象和要注销的对象是同一个，才执行删除\n        if current, exists := devices[client.DeviceID]; exists && current == client {\n            delete(devices, client.DeviceID)\n        }\n    }\n    m.Lock.Unlock()\n```\n\n这种“对象级”的判断，确保了即使发生毫秒级的竞争，系统也能准确识别谁该走、谁该留，不会发生误杀。\n\n---\n\n## 4. 状态延伸：从内存到 Redis\n\n单机内存管理连接在单机环境下是完美的，但当你的 IM 系统需要水平扩展（分布式部署）时，连接状态就变成了“孤岛”。\n\n> [TIP]\n> **深度方案**：我们在处理连接竞争的同时，同步维护一份 Redis 中的在线状态。\n\n* **上线时**：`setOnlineStatus(uid, device, true)`\n* **下线时**：`setOnlineStatus(uid, device, false)`\n\n这样，当 A 服务器上的用户给 B 服务器上的用户发消息时，可以通过 Redis 快速定位目标连接在哪台机器上，从而实现跨节点的精准投递。\n\n---\n\n## 5. 总结\n\n优雅地处理连接竞争，核心不在于高深算法，而在于对并发模型的敬畏：\n* **数据结构**要支撑多维度业务（如用户+设备双重索引）。\n* **资源清理**要遵循 Go 的协程哲学，利用 Close 信号自动触发。\n* **逻辑判断**要精确到对象级别（指针比对），而非仅依赖 ID。\n\n> [NOTE]\n> **作者后记**：在 IM 系统中，连接是廉价的，但状态的一致性是昂贵的。希望这篇文章能给你在处理长连接业务时提供一些参考。","When building Instant Messaging (IM) systems, what causes developers the most headache is often not how to send messages, but how to manage connections.\n\nImagine a scenario: user Xiao Ming is chatting on their phone, and suddenly opens their computer to log in. At this moment, should the system \"kick\" the mobile client offline? Or should it allow both clients to be online simultaneously? If the user's network fluctuates, generating two reconnection requests in a short period of time, how does the server ensure that it won't face the awkward situation where \"the old connection is not disconnected, and the new connection cannot be established\"?\n\nToday, we will talk about how to elegantly resolve multi-device connection competition using Go, through Axiom's engineering practices.\n\n---\n\n## 1. Modeling: From \"One-to-One\" to \"One-to-Many\"\n\nMost beginners simply design connection management as a `map[UserID]Connection`. However, this is not viable in modern internet scenarios.\n\n> [!IMPORTANT]\n> **Deep Dive**: A real multi-device system needs to support a dual index of \"UserID + DeviceID\".\n\nIn our project, the connection manager (Manager) adopts a nested map structure:\n\n```go\ntype Manager struct {\n    \u002F\u002F UserID -> DeviceID -> Client\n    Clients map[uint]map[string]*Client\n    Lock    sync.RWMutex\n}\n```\n\nThis structure allows the same user to be online on multiple devices such as Web, iOS, and Android at the same time, laying the physical foundation for multi-device message synchronization.\n\n---\n\n## 2. Core Tactics: \"Preempt\" First, \"Clean Up\" Later\n\nWhen a new WebSocket connection request comes in, the most elegant way to handle it is not to wait with a giant lock, but to use **optimistic preemption**.\n\n### 2.1 Preemptive Registration\nIn the main loop of the Manager, our logic for handling new connections (Register) is as follows:\n1. **Save new connection**: Directly store the new `Client` instance in the map, overwriting the old record for the same device.\n2. **Asynchronously close old connection**: If it is found that the device already had an active connection previously, instead of immediately blocking to wait, we get a reference to the old connection.\n3. **Trigger self-cleanup**: Call `Close()` on the old connection.\n\n### 2.2 Why is this more elegant?\nIn Go, closing a network connection triggers its corresponding read Goroutine to exit, thereby automatically triggering the unregistration (Unregister) process. This \"leveraging external forces\" design avoids writing complex cleanup code inside the registration logic, keeping the main loop lightweight.\n\n---\n\n## 3. Devil in the Details: Preventing \"Collateral Damage\"\n\nIn high-concurrency scenarios, a bizarre situation may arise: **connection A is disconnecting, while connection B is connecting at almost the same instant.** If the logic is handled improperly, the offline logic of connection A might delete the newly connected connection B.\n\n> [!WARNING]\n> **Deep Trap Warning**: In the Unregister logic, an object pointer comparison must be performed.\n\n```go\ncase client := \u003C-m.Unregister:\n    m.Lock.Lock()\n    if devices, ok := m.Clients[client.UserID]; ok {\n        \u002F\u002F Key: only perform deletion if the connection object in current memory is the exact same one to be unregistered\n        if current, exists := devices[client.DeviceID]; exists && current == client {\n            delete(devices, client.DeviceID)\n        }\n    }\n    m.Lock.Unlock()\n```\n\nThis \"object-level\" check ensures that even if millisecond-level race conditions occur, the system can accurately identify who should leave and who should stay, avoiding accidental deletions.\n\n---\n\n## 4. State Extension: From Memory to Redis\n\nManaging connections in single-machine memory is perfect in a single-instance environment. However, when your IM system needs to scale horizontally (distributed deployment), connection states become \"isolated islands\".\n\n> [!TIP]\n> **Deep Solution**: While handling connection competition, we synchronously maintain online states in Redis.\n\n* **When going online**: `setOnlineStatus(uid, device, true)`\n* **When going offline**: `setOnlineStatus(uid, device, false)`\n\nThis way, when a user on Server A sends a message to a user on Server B, the target connection can be quickly located on which machine it resides via Redis, achieving precise cross-node delivery.\n\n---\n\n## 5. Summary\n\nElegantly handling connection competition does not lie in advanced algorithms, but in respect for concurrency models:\n* The **data structure** must support multi-dimensional business logic (such as dual user + device indexing).\n* **Resource cleanup** must follow Go's concurrency philosophy, triggered automatically by Close signals.\n* **Logical checks** must be precise to the object level (pointer comparison), rather than relying solely on IDs.\n\n> [!NOTE]\n> **Author's Postscript**: In IM systems, connections are cheap, but state consistency is expensive. I hope this article provides you with some reference when dealing with persistent connection operations.\n","GO",29,"https:\u002F\u002Fblog4-1316398321.cos.ap-nanjing.myqcloud.com\u002Fblog5\u002F20260621004623__东京-动漫大屏-城市.png",[11],false,{"id":17,"title":18,"title_en":19},39,"当 Go 遇见 Gemini：构建高可靠 AI 向量化流水线的实战","When Go Meets Gemini: Building a High-Reliability AI Vectorization Pipeline in Practice",{"id":21,"title":22,"title_en":23},41,"《10万行“屎山”与一人公司的赛博传销：AI成了外行收割外行的智商税？》","\"100,000 lines of\" shit mountains \"and the cyberpyramid scheme of one-person companies: AI has become a layman's IQ tax?\"","2026-06-20T23:29:49.590443+08:00"]