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

@@ -431,40 +431,43 @@ func (r *messageRepository) GetNextSeq(conversationID string) (int64, error) {
return nextSeq, err
}
// CreateMessageWithSeq 创建消息并更新seq事务操作
// msg.Seq 应由调用方通过 Redis INCR 预分配
// CreateMessageWithSeq 创建消息并在事务内原子分配 seq
// DB 是 seq 的唯一真相来源SELECT FOR UPDATE 锁行 → last_seq+1 → 写入消息 → 更新 last_seq
// msg.Seq 会在事务内被设置,调用方无需预先分配
func (r *messageRepository) CreateMessageWithSeq(msg *model.Message) error {
return r.db.Transaction(func(tx *gorm.DB) error {
// 创建消息seq 已由调用方预分配)
// 1. 锁定会话行,读取当前 last_seq
var conv model.Conversation
query := tx.Where("id = ?", msg.ConversationID)
if tx.Dialector.Name() == "postgres" {
query = query.Clauses(clause.Locking{Strength: "UPDATE"})
}
if err := query.First(&conv).Error; err != nil {
return fmt.Errorf("lock conversation for seq: %w", err)
}
// 2. 原子分配 seq
msg.Seq = conv.LastSeq + 1
// 3. 创建消息
if err := tx.Create(msg).Error; err != nil {
return err
}
// 更新会话last_seq:仅当新 seq 更大时才更新,防止并发写入导致 last_seq 回退
result := tx.Model(&model.Conversation{}).
Where("id = ? AND last_seq < ?", msg.ConversationID, msg.Seq).
// 4. 更新会话 last_seq 和 last_msg_time
if err := tx.Model(&model.Conversation{}).
Where("id = ?", msg.ConversationID).
Updates(map[string]any{
"last_seq": msg.Seq,
"last_msg_time": gorm.Expr("CURRENT_TIMESTAMP"),
})
if result.Error != nil {
return result.Error
}
// 当 last_seq >= msg.Seq 时(并发写入已更新),仍需刷新 last_msg_time
if err := tx.Model(&model.Conversation{}).
Where("id = ? AND last_seq >= ?", msg.ConversationID, msg.Seq).
Update("last_msg_time", gorm.Expr("CURRENT_TIMESTAMP")).Error; err != nil {
}).Error; err != nil {
return err
}
// 新消息到达后,自动恢复被"仅自己删除"的会话
if err := tx.Model(&model.ConversationParticipant{}).
// 5. 新消息到达后,自动恢复被"仅自己删除"的会话
return tx.Model(&model.ConversationParticipant{}).
Where("conversation_id = ?", msg.ConversationID).
Update("hidden_at", nil).Error; err != nil {
return err
}
return nil
Update("hidden_at", nil).Error
})
}