refactor: improve system stability, performance, and code structure
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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user