feat(chat): implement sequence pre-allocation, versioned sync, and push worker
All checks were successful
Build Backend / build (push) Successful in 4m20s
Build Backend / build-docker (push) Successful in 1m7s

Introduce several performance and synchronization enhancements:
- Implement `SeqBufferManager` to allow sequence number pre-allocation via Redis Lua scripts, reducing atomic increment overhead.
- Add `PushWorker` to handle asynchronous message pushing using Redis Streams.
- Implement incremental conversation synchronization via `ConversationVersionLog` to allow clients to fetch only recent changes.
- Add support for Gzip compression in WebSocket communications to reduce bandwidth usage.
- Update dependency injection and configuration to support these new components.
This commit is contained in:
2026-05-17 23:38:04 +08:00
parent f63c795dcb
commit 6bf87fec46
26 changed files with 1450 additions and 82 deletions

View File

@@ -585,6 +585,37 @@ func (h *MessageHandler) HandleGetSyncData(c *gin.Context) {
})
}
// HandleGetSyncByVersion 增量同步:按版本号获取会话变更
// GET /api/v1/conversations/sync?version=0&limit=100
func (h *MessageHandler) HandleGetSyncByVersion(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
versionStr := c.DefaultQuery("version", "0")
sinceVersion, err := strconv.ParseInt(versionStr, 10, 64)
if err != nil || sinceVersion < 0 {
response.BadRequest(c, "invalid version parameter")
return
}
limitStr := c.DefaultQuery("limit", "100")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit < 1 {
limit = 100
}
result, err := h.chatService.GetSyncByVersion(c.Request.Context(), userID, sinceVersion, limit)
if err != nil {
response.InternalServerError(c, err.Error())
return
}
response.Success(c, result)
}
// GetConversationUnreadCount 获取单个会话的未读数
// GET /api/conversations/:id/unread/count
func (h *MessageHandler) GetConversationUnreadCount(c *gin.Context) {

View File

@@ -151,12 +151,14 @@ func (h *WSHandler) HandleWebSocket(c *gin.Context) {
// 3. 创建客户端
clientID := atomic.AddUint64(&h.clientSeq, 1)
compressEnabled := c.Query("compress") == "1"
client := &ws.Client{
ID: clientID,
UserID: userID,
Send: make(chan []byte, defaultUserBufferSize),
Quit: make(chan struct{}),
PendingAcks: make(map[string]time.Time),
ID: clientID,
UserID: userID,
Send: make(chan []byte, defaultUserBufferSize),
Quit: make(chan struct{}),
PendingAcks: make(map[string]time.Time),
CompressEnabled: compressEnabled,
}
// 4. 注册客户端
@@ -243,9 +245,21 @@ func (h *WSHandler) readPump(conn *websocket.Conn, client *ws.Client) {
break
}
// 解析消息
// 解析消息(支持 gzip 解压)
var rawData []byte
if client.CompressEnabled && ws.IsCompressed(message) {
decompressed, dErr := ws.DefaultCompressor.Decompress(message)
if dErr != nil {
h.publisher.SendError(client, "decompress_error", "failed to decompress gzip message")
continue
}
rawData = decompressed
} else {
rawData = message
}
var msg ws.Message
if err := json.Unmarshal(message, &msg); err != nil {
if err := json.Unmarshal(rawData, &msg); err != nil {
h.publisher.SendError(client, "parse_error", "invalid message format")
continue
}
@@ -280,31 +294,60 @@ func (h *WSHandler) writePump(conn *websocket.Conn, client *ws.Client) {
return
}
w, err := conn.NextWriter(websocket.TextMessage)
if err != nil {
zap.L().Warn("WebSocket write error (NextWriter)",
zap.String("user_id", client.UserID),
zap.Uint64("client_id", client.ID),
zap.Error(err),
)
return
}
w.Write(message)
if client.CompressEnabled {
buf := make([]byte, 0, len(message)+64)
buf = append(buf, message...)
n := len(client.Send)
for i := 0; i < n; i++ {
buf = append(buf, '\n')
buf = append(buf, <-client.Send...)
}
compressed, cErr := ws.DefaultCompressor.Compress(buf)
if cErr != nil {
zap.L().Warn("WebSocket compress error, sending raw",
zap.String("user_id", client.UserID),
zap.Error(cErr),
)
w, err := conn.NextWriter(websocket.TextMessage)
if err != nil {
return
}
w.Write(buf)
w.Close()
} else {
w, err := conn.NextWriter(websocket.BinaryMessage)
if err != nil {
return
}
w.Write(compressed)
w.Close()
}
} else {
w, err := conn.NextWriter(websocket.TextMessage)
if err != nil {
zap.L().Warn("WebSocket write error (NextWriter)",
zap.String("user_id", client.UserID),
zap.Uint64("client_id", client.ID),
zap.Error(err),
)
return
}
w.Write(message)
// 批量发送缓冲区中的消息
n := len(client.Send)
for i := 0; i < n; i++ {
w.Write([]byte{'\n'})
w.Write(<-client.Send)
}
n := len(client.Send)
for i := 0; i < n; i++ {
w.Write([]byte{'\n'})
w.Write(<-client.Send)
}
if err := w.Close(); err != nil {
zap.L().Warn("WebSocket write error (Close)",
zap.String("user_id", client.UserID),
zap.Uint64("client_id", client.ID),
zap.Error(err),
)
return
if err := w.Close(); err != nil {
zap.L().Warn("WebSocket write error (Close)",
zap.String("user_id", client.UserID),
zap.Uint64("client_id", client.ID),
zap.Error(err),
)
return
}
}
case <-ticker.C:
conn.SetWriteDeadline(time.Now().Add(10 * time.Second))