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

@@ -94,6 +94,7 @@ type pushTask struct {
userID string
message *model.Message
priority int
recordID int64 // DB record ID for crash recovery
}
// NewPushService 创建推送服务
@@ -152,21 +153,31 @@ func (s *pushServiceImpl) PushToUser(ctx context.Context, userID string, message
}
}
// 极光推送也失败,加入推送队列等待重试
// 极光推送也失败,先持久化推送记录再入内存队列
expiredAt := time.Now().Add(DefaultExpiredTime)
record := &model.PushRecord{
UserID: userID,
MessageID: message.ID,
PushChannel: model.PushChannelJPush,
PushStatus: model.PushStatusPending,
MaxRetry: MaxRetryCount,
ExpiredAt: &expiredAt,
}
if err := s.pushRepo.Create(record); err != nil {
return fmt.Errorf("failed to persist push record: %w", err)
}
select {
case s.pushQueue <- &pushTask{
userID: userID,
message: message,
priority: priority,
recordID: record.ID,
}:
return nil
default:
// 队列已满,直接创建待推送记录
_, err := s.CreatePushRecord(ctx, userID, message.ID, model.PushChannelJPush)
if err != nil {
return fmt.Errorf("failed to create pending push record: %w", err)
}
return errors.New("push queue is full, message queued for later delivery")
// 队列已满,记录已持久化,等待重试协程处理
return errors.New("push queue is full, message persisted for later delivery")
}
}
@@ -435,10 +446,50 @@ func (s *pushServiceImpl) GetUserDevices(ctx context.Context, userID string) ([]
// StartPushWorker 启动推送工作协程
func (s *pushServiceImpl) StartPushWorker(ctx context.Context) {
s.recoverPendingPushes()
go s.processPushQueue()
go s.retryFailedPushes()
}
// recoverPendingPushes 从数据库恢复未处理的推送记录
func (s *pushServiceImpl) recoverPendingPushes() {
records, err := s.pushRepo.GetPendingPushes(100)
if err != nil {
zap.L().Error("failed to recover pending push records", zap.Error(err))
return
}
recovered := 0
for _, record := range records {
if record.IsExpired() {
s.pushRepo.UpdateStatus(record.ID, model.PushStatusExpired)
continue
}
message, err := s.messageRepo.GetMessageByID(record.MessageID)
if err != nil {
s.pushRepo.MarkAsFailed(record.ID, "message not found during recovery")
continue
}
select {
case s.pushQueue <- &pushTask{
userID: record.UserID,
message: message,
priority: int(PriorityNormal),
recordID: record.ID,
}:
recovered++
default:
// 队列已满,保留 pending 状态等待下次恢复
}
}
if recovered > 0 {
zap.L().Info("recovered pending push records", zap.Int("count", recovered))
}
}
// StopPushWorker 停止推送工作协程
func (s *pushServiceImpl) StopPushWorker() {
close(s.stopChan)
@@ -464,8 +515,8 @@ func (s *pushServiceImpl) processPushTask(task *pushTask) {
// 获取用户活跃设备
devices, err := s.deviceRepo.GetActiveByUserID(task.userID)
if err != nil || len(devices) == 0 {
// 没有可用设备,创建待推送记录
s.CreatePushRecord(ctx, task.userID, task.message.ID, model.PushChannelJPush)
// 没有可用设备,保留记录为 pending 等待重试
s.pushRepo.UpdateStatus(task.recordID, model.PushStatusFailed)
return
}
@@ -480,6 +531,8 @@ func (s *pushServiceImpl) processPushTask(task *pushTask) {
err := s.pushViaJPushBatch(mobileDevices, payload.Title, payload.Content, payload.Extras)
if err == nil {
s.batchCreatePushedRecords(ctx, task.userID, task.message.ID, mobileDevices)
// 原始 pending 记录标记为已推送(被批量记录替代)
s.pushRepo.UpdateStatus(task.recordID, model.PushStatusPushed)
return
}
}
@@ -508,6 +561,8 @@ func (s *pushServiceImpl) processPushTask(task *pushTask) {
}
s.pushRepo.Update(record)
}
// 原始 pending 记录标记为已处理
s.pushRepo.UpdateStatus(task.recordID, model.PushStatusPushed)
}
}