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

@@ -56,6 +56,10 @@ type ChatService interface {
// 仅保存消息到数据库,不发送实时推送(供群聊等自行推送的场景使用)
SaveMessage(ctx context.Context, senderID string, conversationID string, segments model.MessageSegments, replyToID *string) (*model.Message, error)
// 批量查询(解决 N+1 问题)
GetUnreadCountBatch(ctx context.Context, userID string, convIDs []string) (map[string]int64, error)
GetLastMessagesBatch(ctx context.Context, convIDs []string) (map[string]*model.Message, error)
}
// chatServiceImpl 聊天服务实现
@@ -333,6 +337,19 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
Status: model.MessageStatusNormal,
}
// 从 Redis 获取下一个 seq
if s.conversationCache != nil {
seq, err := s.conversationCache.GetNextSeq(ctx, conversationID)
if err != nil {
zap.L().Warn("redis get next seq failed, falling back to DB",
zap.String("convID", conversationID),
zap.Error(err),
)
} else {
message.Seq = seq
}
}
// 使用事务创建消息并更新seq
if err := s.repo.CreateMessageWithSeq(message); err != nil {
return nil, fmt.Errorf("failed to save message: %w", err)
@@ -397,6 +414,20 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
}
if s.conversationCache != nil {
// 使用 Redis Hash 预计算未读数
for _, p := range participants {
if p.UserID == senderID {
continue
}
if incrErr := s.conversationCache.IncrementUnread(context.Background(), p.UserID, conversationID); incrErr != nil {
zap.L().Warn("increment unread from redis hash failed",
zap.String("userID", p.UserID),
zap.String("convID", conversationID),
zap.Error(incrErr),
)
}
}
keys := make([]string, 0, len(participants)*2)
for _, p := range participants {
keys = append(keys,
@@ -559,11 +590,18 @@ func (s *chatServiceImpl) MarkAsRead(ctx context.Context, conversationID string,
return fmt.Errorf("failed to update last read seq: %w", err)
}
// 2. DB 写入成功后,失效缓存Cache-Aside 模式)
// 2. DB 写入成功后,清除 Redis 未读数
if s.conversationCache != nil {
if clearErr := s.conversationCache.ClearUnread(ctx, userID, conversationID); clearErr != nil {
zap.L().Warn("clear unread from redis hash failed",
zap.String("userID", userID),
zap.String("convID", conversationID),
zap.Error(clearErr),
)
}
// 失效参与者缓存,下次读取时会从 DB 加载最新数据
s.conversationCache.InvalidateParticipant(conversationID, userID)
// 失效未读数缓存
// 失效未读数缓存(旧 cache-aside 键)
s.conversationCache.InvalidateUnreadCount(userID, conversationID)
// 失效会话列表缓存
s.conversationCache.InvalidateConversationList(userID)
@@ -612,8 +650,13 @@ func (s *chatServiceImpl) GetUnreadCount(ctx context.Context, conversationID str
return 0, fmt.Errorf("failed to get participant: %w", err)
}
// 优先使用缓存
// 优先从 Redis Hash 读取
if s.conversationCache != nil {
count, err := s.conversationCache.GetUnreadCountFromHash(ctx, userID, conversationID)
if err == nil {
return count, nil
}
// 降级到旧缓存路径
return s.conversationCache.GetUnreadCount(ctx, userID, conversationID)
}
@@ -622,6 +665,12 @@ func (s *chatServiceImpl) GetUnreadCount(ctx context.Context, conversationID str
// GetAllUnreadCount 获取所有会话的未读消息总数
func (s *chatServiceImpl) GetAllUnreadCount(ctx context.Context, userID string) (int64, error) {
// 优先从 Redis Hash 读取
if s.conversationCache != nil {
if total, err := s.conversationCache.GetTotalUnread(ctx, userID); err == nil {
return total, nil
}
}
return s.repo.GetAllUnreadCount(userID)
}
@@ -798,6 +847,19 @@ func (s *chatServiceImpl) SaveMessage(ctx context.Context, senderID string, conv
Status: model.MessageStatusNormal,
}
// 从 Redis 获取下一个 seqSaveMessage 方法)
if s.conversationCache != nil {
seq, err := s.conversationCache.GetNextSeq(ctx, conversationID)
if err != nil {
zap.L().Warn("redis get next seq failed, falling back to DB",
zap.String("convID", conversationID),
zap.Error(err),
)
} else {
message.Seq = seq
}
}
if err := s.repo.CreateMessageWithSeq(message); err != nil {
return nil, fmt.Errorf("failed to save message: %w", err)
}
@@ -821,3 +883,13 @@ func (s *chatServiceImpl) SaveMessage(ctx context.Context, senderID string, conv
return message, nil
}
// GetUnreadCountBatch 批量获取用户在多个会话中的未读数
func (s *chatServiceImpl) GetUnreadCountBatch(ctx context.Context, userID string, convIDs []string) (map[string]int64, error) {
return s.repo.GetUnreadCountBatch(ctx, userID, convIDs)
}
// GetLastMessagesBatch 批量获取每个会话的最后一条消息
func (s *chatServiceImpl) GetLastMessagesBatch(ctx context.Context, convIDs []string) (map[string]*model.Message, error) {
return s.repo.GetLastMessagesBatch(ctx, convIDs)
}