refactor(messaging): shift sequence generation responsibility to database transactions
All checks were successful
Build Backend / build (push) Successful in 1m59s
Build Backend / build-docker (push) Successful in 1m20s

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:
2026-05-25 14:51:46 +08:00
parent 2748c80095
commit 2084473fb5
6 changed files with 145 additions and 376 deletions

View File

@@ -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 {