refactor: improve system stability, performance, and code structure
All checks were successful
Build Backend / build (push) Successful in 3m2s
Build Backend / build-docker (push) Successful in 2m44s

This commit introduces several architectural improvements and optimizations across the codebase:

- **Performance & Reliability**:
  - Implemented Redis pipelining in `ConversationCache.CacheMessage` to reduce network round-trips.
  - Added a circuit breaker to the JPush client to prevent cascading failures.
  - Introduced batch deletion and batch member addition capabilities in repositories.
  - Added message idempotency support using `client_msg_id` and a Redis-based cache.
  - Optimized WebSocket handling with connection limits (total and per-user) and improved error logging.

- **Code Refactoring**:
  - Refactored `Router` to use a `RouterDeps` struct, simplifying the constructor and improving maintainability.
  - Unified model ID generation logic using new `id_helper.go` (supporting UUID and Snowflake).
  - Standardized JSON serialization/deserialization in models using `json_helper.go`.
  - Refactored DTO conversion logic, specifically for `UserResponse` (using functional options) and `Report` responses.
  - Removed redundant/deprecated DTOs like `PostDetailResponse` and `TradeItemDetailResponse`.

- **Cache Improvements**:
  - Enhanced `LayeredCache` with `SetRaw` to avoid double-encoding when promoting values from Redis to local cache.
  - Added `DeleteBatch` support to the cache interface.

- **Other Changes**:
  - Cleaned up `config.go` by removing redundant default values and explicit environment variable overrides.
  - Improved WebSocket registration flow to handle connection limits gracefully.
This commit is contained in:
2026-05-04 13:07:03 +08:00
parent b2b55ea52d
commit ee78071d4d
65 changed files with 1293 additions and 975 deletions

View File

@@ -2,6 +2,7 @@ package ws
import (
"encoding/json"
"errors"
"sync"
"sync/atomic"
"time"
@@ -10,8 +11,12 @@ import (
)
const (
defaultUserBufferSize = 128
maxReplayEvents = 200
defaultUserBufferSize = 128
maxReplayEvents = 200
groupPushConcurrency = 64
groupPushQueueSize = 512
maxTotalConnections = 100000
maxConnectionsPerUser = 10
)
// Event 表示一个WebSocket事件
@@ -31,6 +36,9 @@ type Client struct {
Quit chan struct{}
}
// ErrConnectionLimit 连接数限制错误
var ErrConnectionLimit = errors.New("connection limit reached")
// Message 表示WebSocket消息
type Message struct {
Type string `json:"type"`
@@ -73,14 +81,15 @@ type ConnectHandler func(userID string, isFirstConnection bool)
// Hub WebSocket连接管理器
type Hub struct {
seq uint64
seq uint64
connCount atomic.Int64
mu sync.RWMutex
clients map[string]map[uint64]*Client // userID -> clientID -> Client
history map[string][]Event // userID -> []Event (用于断线重连历史回放)
subscribers map[string]map[uint64]*subscriber // userID -> subscriberID -> subscriber
disconnectHandlers []DisconnectHandler // 断开连接事件处理器列表
connectHandlers []ConnectHandler // 连接事件处理器列表
clients map[string]map[uint64]*Client
history map[string][]Event
subscribers map[string]map[uint64]*subscriber
disconnectHandlers []DisconnectHandler
connectHandlers []ConnectHandler
}
// NewHub 创建WebSocket Hub
@@ -99,11 +108,19 @@ func (h *Hub) NextID() uint64 {
return atomic.AddUint64(&h.seq, 1)
}
// Register 注册客户端连接
func (h *Hub) Register(client *Client) []Event {
// Register 注册客户端连接,返回回放事件和可能的错误(连接数超限)
func (h *Hub) Register(client *Client) ([]Event, error) {
if h.connCount.Load() >= int64(maxTotalConnections) {
return nil, ErrConnectionLimit
}
h.mu.Lock()
// 检查是否是该用户的第一个连接(之前完全离线)
if len(h.clients[client.UserID]) >= maxConnectionsPerUser {
h.mu.Unlock()
return nil, ErrConnectionLimit
}
_, existed := h.clients[client.UserID]
isFirstConnection := !existed
@@ -111,8 +128,8 @@ func (h *Hub) Register(client *Client) []Event {
h.clients[client.UserID] = make(map[uint64]*Client)
}
h.clients[client.UserID][client.ID] = client
h.connCount.Add(1)
// 获取历史事件用于回放
replay := make([]Event, 0)
for _, e := range h.history[client.UserID] {
replay = append(replay, e)
@@ -122,15 +139,14 @@ func (h *Hub) Register(client *Client) []Event {
zap.String("user_id", client.UserID),
zap.Uint64("client_id", client.ID),
zap.Int("total_clients", len(h.clients[client.UserID])),
zap.Int64("total_connections", h.connCount.Load()),
zap.Bool("is_first_connection", isFirstConnection),
)
// 复制处理器列表避免锁竞争
handlers := make([]ConnectHandler, len(h.connectHandlers))
copy(handlers, h.connectHandlers)
h.mu.Unlock()
// 异步调用连接处理器,避免阻塞 Register
if len(handlers) > 0 {
go func() {
for _, handler := range handlers {
@@ -139,7 +155,7 @@ func (h *Hub) Register(client *Client) []Event {
}()
}
return replay
return replay, nil
}
// Unregister 注销客户端连接
@@ -151,6 +167,7 @@ func (h *Hub) Unregister(client *Client) {
if _, exists := userClients[client.ID]; exists {
delete(userClients, client.ID)
close(client.Quit)
h.connCount.Add(-1)
remainingCount = len(userClients)
if remainingCount == 0 {
@@ -187,6 +204,20 @@ func (h *Hub) HasClients(userID string) bool {
return len(h.clients[userID]) > 0
}
// FilterOnline 批量检查用户在线状态,一次加锁返回在线/离线列表
func (h *Hub) FilterOnline(userIDs []string) (online []string, offline []string) {
h.mu.RLock()
defer h.mu.RUnlock()
for _, uid := range userIDs {
if len(h.clients[uid]) > 0 {
online = append(online, uid)
} else {
offline = append(offline, uid)
}
}
return
}
// OnDisconnect 注册断开连接事件处理器
// 当用户断开 WebSocket 连接时,会调用所有注册的处理器
// 处理器会收到用户ID和该用户剩余的连接数
@@ -224,10 +255,65 @@ func (h *Hub) PublishToUser(userID string, eventType string, payload any) Event
return ev
}
// PublishToUsers 向多个用户发送事件
// PublishToUsers 向多个用户发送事件(共享一次序列化)
func (h *Hub) PublishToUsers(userIDs []string, eventType string, payload any) {
if len(userIDs) == 0 {
return
}
ev := Event{
ID: h.NextID(),
Type: eventType,
TS: time.Now().UnixMilli(),
Payload: payload,
}
msg := ResponseMessage{
EventID: ev.ID,
Type: eventType,
TS: ev.TS,
Payload: payload,
}
data, err := json.Marshal(msg)
if err != nil {
zap.L().Error("Failed to marshal WebSocket message",
zap.String("event_type", eventType),
zap.Error(err),
)
return
}
for _, uid := range userIDs {
h.PublishToUser(uid, eventType, payload)
h.publishPreMarshaled(uid, ev, data)
}
}
// publishPreMarshaled 使用预序列化的数据发送事件
func (h *Hub) publishPreMarshaled(userID string, ev Event, data []byte) {
h.mu.Lock()
history := append(h.history[userID], ev)
if len(history) > maxReplayEvents {
history = history[len(history)-maxReplayEvents:]
}
h.history[userID] = history
targets := make([]*Client, 0, len(h.clients[userID]))
for _, c := range h.clients[userID] {
targets = append(targets, c)
}
h.mu.Unlock()
for _, c := range targets {
select {
case <-c.Quit:
case c.Send <- data:
default:
zap.L().Warn("WebSocket client buffer full, closing slow client",
zap.String("user_id", userID),
zap.Uint64("client_id", c.ID),
)
close(c.Quit)
}
}
}
@@ -338,19 +424,16 @@ func (h *Hub) publish(userID string, ev Event) {
return
}
// 非阻塞发送,慢消费者丢弃消息
for _, c := range targets {
select {
case <-c.Quit:
// 客户端已断开
case c.Send <- data:
// 消息已发送
default:
// 发送缓冲区满,丢弃消息
zap.L().Warn("WebSocket client buffer full, dropping message",
zap.L().Warn("WebSocket client buffer full, closing slow client",
zap.String("user_id", userID),
zap.Uint64("client_id", c.ID),
)
close(c.Quit)
}
}
}
@@ -387,6 +470,81 @@ func (h *Hub) Broadcast(eventType string, payload any) {
h.PublishToUsers(userIDs, eventType, payload)
}
// PublishToUsersConcurrent 并发推送消息给多个用户(适用于群聊等大列表场景)
// 使用有界并发控制,避免 goroutine 爆炸
func (h *Hub) PublishToUsersConcurrent(userIDs []string, eventType string, payload any) {
if len(userIDs) == 0 {
return
}
ev := Event{
ID: h.NextID(),
Type: eventType,
TS: time.Now().UnixMilli(),
Payload: payload,
}
msg := ResponseMessage{
EventID: ev.ID,
Type: eventType,
TS: ev.TS,
Payload: payload,
}
data, err := json.Marshal(msg)
if err != nil {
zap.L().Error("Failed to marshal WebSocket message for batch push",
zap.String("event_type", eventType),
zap.Error(err),
)
return
}
sem := make(chan struct{}, groupPushConcurrency)
var wg sync.WaitGroup
wg.Add(len(userIDs))
for _, uid := range userIDs {
sem <- struct{}{}
go func(userID string) {
defer wg.Done()
defer func() { <-sem }()
h.mu.RLock()
targets := make([]*Client, 0, len(h.clients[userID]))
for _, c := range h.clients[userID] {
targets = append(targets, c)
}
h.mu.RUnlock()
for _, c := range targets {
select {
case <-c.Quit:
case c.Send <- data:
default:
zap.L().Warn("WebSocket client buffer full, closing slow client",
zap.String("user_id", userID),
zap.Uint64("client_id", c.ID),
)
close(c.Quit)
}
}
}(uid)
}
wg.Wait()
// Store history for each user for reconnection replay
h.mu.Lock()
for _, uid := range userIDs {
history := append(h.history[uid], ev)
if len(history) > maxReplayEvents {
history = history[len(history)-maxReplayEvents:]
}
h.history[uid] = history
}
h.mu.Unlock()
}
// GetOnlineUsers 获取在线用户列表
func (h *Hub) GetOnlineUsers() []string {
h.mu.RLock()
@@ -406,6 +564,29 @@ func (h *Hub) GetOnlineCount() int {
return len(h.clients)
}
// GetConnectionCount 获取总连接数
func (h *Hub) GetConnectionCount() int64 {
return h.connCount.Load()
}
// CloseAllConnections 关闭所有WebSocket连接用于优雅关闭
func (h *Hub) CloseAllConnections() {
h.mu.Lock()
defer h.mu.Unlock()
for _, userClients := range h.clients {
for _, c := range userClients {
select {
case <-c.Quit:
default:
close(c.Quit)
}
}
}
h.clients = make(map[string]map[uint64]*Client)
h.connCount.Store(0)
}
// Subscribe 订阅事件
func (h *Hub) Subscribe(userID string, afterID uint64) (chan Event, func(), []Event) {
subID := h.NextID()