refactor(messaging): shift sequence generation responsibility to database transactions
Move the responsibility of sequence (`seq`) allocation from the application/cache layer to the database layer to ensure absolute consistency. Previously, sequences were pre-allocated via Redis/Lua, which introduced complexity in managing synchronization between the cache and the database. Key changes: - Implement `CreateMessageWithSeq` in `message_repo.go` using `SELECT FOR UPDATE` to lock the conversation row and atomically increment the `last_seq` within a single transaction. - Simplify `SeqBufferManager` by removing the complex local buffering and Lua-based pre-allocation logic, reverting to a simpler model. - Update `chat_service.go`, `group_service.go`, and `message_service.go` to remove manual sequence retrieval, relying instead on the repository's transactional allocation. - Introduce `SyncConvSeq` in `conversation_cache.go` to perform "write-through" updates to Redis, ensuring the cache remains synchronized with the database's source of truth. - Improve cold-start handling in `conversation_cache.go` with a new `syncSeqLua` script to prevent stale sequence reads.
This commit is contained in:
@@ -479,20 +479,18 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
|
||||
Status: model.MessageStatusNormal,
|
||||
}
|
||||
|
||||
// 从 Redis 获取下一个 seq
|
||||
if s.conversationCache != nil {
|
||||
seq, err := s.conversationCache.GetNextSeqWithGroup(ctx, conversationID, conv.Type == model.ConversationTypeGroup)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get next seq for conversation %s: %w", conversationID, err)
|
||||
}
|
||||
message.Seq = seq
|
||||
}
|
||||
// seq 由 CreateMessageWithSeq 在事务内原子分配
|
||||
|
||||
// 使用事务创建消息并更新seq
|
||||
if err := s.repo.CreateMessageWithSeq(message); err != nil {
|
||||
return nil, fmt.Errorf("failed to save message: %w", err)
|
||||
}
|
||||
|
||||
// 写透 seq 到 Redis,保持缓存与 DB 一致
|
||||
if s.conversationCache != nil {
|
||||
s.conversationCache.SyncConvSeq(context.Background(), conversationID, message.Seq)
|
||||
}
|
||||
|
||||
// 发送者自动已读:将发送者的 lastReadSeq 更新到消息 seq
|
||||
_ = s.repo.UpdateLastReadSeq(conversationID, senderID, message.Seq)
|
||||
if s.conversationCache != nil {
|
||||
@@ -1042,7 +1040,7 @@ func (s *chatServiceImpl) IsUserOnline(userID string) bool {
|
||||
// 适用于群聊等由调用方自行负责推送的场景
|
||||
func (s *chatServiceImpl) SaveMessage(ctx context.Context, senderID string, conversationID string, segments model.MessageSegments, replyToID *string) (*model.Message, error) {
|
||||
// 验证会话是否存在
|
||||
conv, err := s.getConversation(ctx, conversationID)
|
||||
_, err := s.getConversation(ctx, conversationID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("会话不存在,请重新创建会话")
|
||||
@@ -1073,19 +1071,17 @@ func (s *chatServiceImpl) SaveMessage(ctx context.Context, senderID string, conv
|
||||
Status: model.MessageStatusNormal,
|
||||
}
|
||||
|
||||
// 从 Redis 获取下一个 seq(SaveMessage 方法)
|
||||
if s.conversationCache != nil {
|
||||
seq, err := s.conversationCache.GetNextSeqWithGroup(ctx, conversationID, conv.Type == model.ConversationTypeGroup)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get next seq for conversation %s: %w", conversationID, err)
|
||||
}
|
||||
message.Seq = seq
|
||||
}
|
||||
// seq 由 CreateMessageWithSeq 在事务内原子分配
|
||||
|
||||
if err := s.repo.CreateMessageWithSeq(message); err != nil {
|
||||
return nil, fmt.Errorf("failed to save message: %w", err)
|
||||
}
|
||||
|
||||
// 写透 seq 到 Redis,保持缓存与 DB 一致
|
||||
if s.conversationCache != nil {
|
||||
s.conversationCache.SyncConvSeq(context.Background(), conversationID, message.Seq)
|
||||
}
|
||||
|
||||
// 新消息会改变分页结果,先失效分页缓存,避免读到旧列表
|
||||
if s.conversationCache != nil {
|
||||
s.conversationCache.InvalidateMessagePages(conversationID)
|
||||
|
||||
@@ -471,18 +471,7 @@ func (s *groupService) broadcastMemberJoinNotice(groupID string, targetUserID st
|
||||
Status: model.MessageStatusNormal,
|
||||
Category: model.CategoryNotification,
|
||||
}
|
||||
// 从 Redis 获取下一个 seq
|
||||
if s.conversationCache != nil {
|
||||
seq, seqErr := s.conversationCache.GetNextSeqWithGroup(context.Background(), conv.ID, true)
|
||||
if seqErr != nil {
|
||||
zap.L().Warn("get next seq failed, skipping system message",
|
||||
zap.String("convID", conv.ID),
|
||||
zap.Error(seqErr),
|
||||
)
|
||||
return
|
||||
}
|
||||
msg.Seq = seq
|
||||
}
|
||||
// seq 由 CreateMessageWithSeq 在事务内原子分配
|
||||
if err := s.messageRepo.CreateMessageWithSeq(msg); err != nil {
|
||||
zap.L().Warn("保存入群提示消息失败",
|
||||
zap.String("component", "broadcastMemberJoinNotice"),
|
||||
@@ -492,6 +481,9 @@ func (s *groupService) broadcastMemberJoinNotice(groupID string, targetUserID st
|
||||
)
|
||||
} else {
|
||||
savedMessage = msg
|
||||
if s.conversationCache != nil {
|
||||
s.conversationCache.SyncConvSeq(context.Background(), conv.ID, msg.Seq)
|
||||
}
|
||||
s.invalidateConversationCachesAfterSystemMessage(conv.ID)
|
||||
}
|
||||
} else {
|
||||
@@ -1396,18 +1388,7 @@ func (s *groupService) MuteMember(userID string, groupID string, targetUserID st
|
||||
Category: model.CategoryNotification,
|
||||
}
|
||||
|
||||
// 从 Redis 获取下一个 seq
|
||||
if s.conversationCache != nil {
|
||||
seq, seqErr := s.conversationCache.GetNextSeqWithGroup(context.Background(), conv.ID, true)
|
||||
if seqErr != nil {
|
||||
zap.L().Warn("get next seq failed, skipping system message",
|
||||
zap.String("convID", conv.ID),
|
||||
zap.Error(seqErr),
|
||||
)
|
||||
return nil
|
||||
}
|
||||
msg.Seq = seq
|
||||
}
|
||||
// seq 由 CreateMessageWithSeq 在事务内原子分配
|
||||
|
||||
if err := s.messageRepo.CreateMessageWithSeq(msg); err != nil {
|
||||
zap.L().Warn("保存禁言消息失败",
|
||||
@@ -1421,6 +1402,9 @@ func (s *groupService) MuteMember(userID string, groupID string, targetUserID st
|
||||
zap.String("msgID", msg.ID),
|
||||
zap.Int64("seq", msg.Seq),
|
||||
)
|
||||
if s.conversationCache != nil {
|
||||
s.conversationCache.SyncConvSeq(context.Background(), conv.ID, msg.Seq)
|
||||
}
|
||||
s.invalidateConversationCachesAfterSystemMessage(conv.ID)
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -2,7 +2,6 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"with_you/internal/cache"
|
||||
@@ -88,21 +87,17 @@ func (s *MessageService) SendMessage(ctx context.Context, senderID, receiverID s
|
||||
Status: model.MessageStatusNormal,
|
||||
}
|
||||
|
||||
// 从 Redis 获取下一个 seq
|
||||
if s.conversationCache != nil {
|
||||
seq, seqErr := s.conversationCache.GetNextSeqWithGroup(context.Background(), conv.ID, false)
|
||||
if seqErr != nil {
|
||||
return nil, fmt.Errorf("failed to get next seq for conversation %s: %w", conv.ID, seqErr)
|
||||
}
|
||||
msg.Seq = seq
|
||||
}
|
||||
|
||||
// 使用事务创建消息并更新seq
|
||||
// 事务内原子分配 seq + 创建消息(DB 是 seq 唯一来源)
|
||||
err = s.messageRepo.CreateMessageWithSeq(msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 写透 seq 到 Redis,保持缓存与 DB 一致
|
||||
if s.conversationCache != nil {
|
||||
s.conversationCache.SyncConvSeq(context.Background(), conv.ID, msg.Seq)
|
||||
}
|
||||
|
||||
// 新消息会改变分页结果,先失效分页缓存,避免读到旧列表
|
||||
if s.conversationCache != nil {
|
||||
s.conversationCache.InvalidateMessagePages(conv.ID)
|
||||
|
||||
Reference in New Issue
Block a user