feat(core): optimize performance and reliability through batching and redis-backed unread counts
All checks were successful
Build Backend / build (push) Successful in 2m7s
Build Backend / build-docker (push) Successful in 1m51s

This commit introduces several significant architectural improvements to enhance system performance, scalability, and reliability:

- **Performance Optimization (N+1 Problem Resolution)**: Implemented batching mechanisms across `MessageHandler`, `ChatService`, `GroupService`, `MessageService`, and `UserService`. This replaces multiple individual database/cache queries with single batch operations for fetching unread counts, last messages, participants, and user information.
- **Enhanced Unread Count Management**: Migrated unread count tracking to a Redis Hash-based approach (`unread#️⃣{userID}`). This allows for atomic increments/decrements and efficient retrieval of both individual conversation unread counts and total unread counts for a user.
- **Improved Message Sequencing**: Integrated Redis-based sequence (`seq`) pre-allocation for messages to ensure strict ordering and reduce database contention during high-concurrency message creation.
- **Push Service Reliability**: Refactored the push notification system to include persistent `PushRecord` tracking. Added a recovery mechanism (`recoverPendingPushes`) to reload pending notifications from the database upon service startup, ensuring better delivery guarantees.
- **WebSocket Reliability**: Updated the WebSocket hub to move away from in-memory history replay in favor of a client-driven synchronization model (`sync_required` event and `ack` handling), reducing memory overhead and improving connection stability.
- **Cache Layer Enhancements**: Added `HIncrBy` and `IncrBySeq` to the `Cache` interface and its implementations (`RedisCache`, `LayeredCache`) to support the new unread and sequence management logic.
This commit is contained in:
2026-05-04 18:31:03 +08:00
parent ee78071d4d
commit 90c57f1a1c
18 changed files with 752 additions and 218 deletions

View File

@@ -140,14 +140,15 @@ func (h *WSHandler) HandleWebSocket(c *gin.Context) {
// 3. 创建客户端
clientID := atomic.AddUint64(&h.clientSeq, 1)
client := &ws.Client{
ID: clientID,
UserID: userID,
Send: make(chan []byte, defaultUserBufferSize),
Quit: make(chan struct{}),
ID: clientID,
UserID: userID,
Send: make(chan []byte, defaultUserBufferSize),
Quit: make(chan struct{}),
PendingAcks: make(map[string]time.Time),
}
// 4. 注册客户端
replayEvents, regErr := h.wsHub.Register(client)
regErr := h.wsHub.Register(client)
if regErr != nil {
zap.L().Warn("WebSocket registration rejected",
zap.String("user_id", userID),
@@ -165,23 +166,18 @@ func (h *WSHandler) HandleWebSocket(c *gin.Context) {
zap.Uint64("client_id", clientID),
)
// 5. 发送历史回放消息
go func() {
for _, ev := range replayEvents {
msg := ws.ResponseMessage{
EventID: ev.ID,
Type: ev.Type,
TS: ev.TS,
Payload: ev.Payload,
}
data, _ := json.Marshal(msg)
select {
case <-client.Quit:
return
case client.Send <- data:
}
// 5. 提示客户端进行 seq 同步
syncMsg := ws.ResponseMessage{
EventID: h.wsHub.NextID(),
Type: "sync_required",
TS: time.Now().UnixMilli(),
}
if syncData, err := json.Marshal(syncMsg); err == nil {
select {
case <-client.Quit:
case client.Send <- syncData:
}
}()
}
// 6. 启动读写goroutine
go h.writePump(conn, client)
@@ -322,6 +318,8 @@ func (h *WSHandler) handleMessage(client *ws.Client, msg *ws.Message) {
h.handleRead(ctx, client, msg.Payload)
case "recall":
h.handleRecall(ctx, client, msg.Payload)
case "ack":
h.handleAck(client, msg.Payload)
// 通话信令
case "call_invite":
h.handleCallInvite(ctx, client, msg.Payload)
@@ -511,6 +509,17 @@ func (h *WSHandler) handleRecall(ctx context.Context, client *ws.Client, payload
const defaultUserBufferSize = 128
// handleAck 处理客户端ACK确认
func (h *WSHandler) handleAck(client *ws.Client, payload json.RawMessage) {
var req struct {
MessageID string `json:"message_id"`
}
if err := json.Unmarshal(payload, &req); err != nil || req.MessageID == "" {
return
}
h.wsHub.AckMessage(client.UserID, req.MessageID)
}
// isVerified 检查用户是否已通过身份认证
func (h *WSHandler) isVerified(ctx context.Context, client *ws.Client) bool {
user, err := h.userRepo.GetByID(client.UserID)