feat(cache): implement database fallback for conversation sequence management
All checks were successful
Build Backend / build (push) Successful in 2m2s
Build Backend / build-docker (push) Successful in 1m23s

Improve the reliability of conversation sequence generation by implementing a fallback mechanism to the database when Redis is unavailable or when keys are missing.

- Add `GetNextSeq` to `MessageRepository` and implement it in `MessageRepositoryAdapter`.
- Update `ConversationCache` to fallback to DB in `GetConvMaxSeq`, `GetConvMaxSeqBatch`, and `GetNextSeq`.
- Ensure Redis TTL is refreshed after every `INCR` operation to prevent sequence counter expiration.
- Refactor service layers to handle sequence generation errors more explicitly.
- Add `IsEnabled` helper to `LayeredCache` for cleaner availability checks.
This commit is contained in:
2026-05-15 13:08:22 +08:00
parent 570b2d7afe
commit 6c14309624
6 changed files with 58 additions and 38 deletions

View File

@@ -221,6 +221,7 @@ type MessageRepository interface {
GetMessages(convID string, page, pageSize int) ([]*model.Message, int64, error)
GetMessagesAfterSeq(convID string, afterSeq int64, limit int) ([]*model.Message, error)
GetMessagesBeforeSeq(convID string, beforeSeq int64, limit int) ([]*model.Message, error)
GetNextSeq(convID string) (int64, error)
}
// ============================================================
@@ -588,22 +589,29 @@ func (c *ConversationCache) GetUserReadSeqs(ctx context.Context, userID string,
return result, nil
}
// GetConvMaxSeq 获取会话的最大 seq复用 msg_seq 计数器,读取当前值
// GetConvMaxSeq 获取会话的最大 seq优先 Redis降级 DB
func (c *ConversationCache) GetConvMaxSeq(ctx context.Context, convID string) (int64, error) {
seqKey := MessageSeqKey(convID)
redisCache, ok := c.cache.(*RedisCache)
if !ok {
return 0, ErrKeyNotFound
if ok {
rdb := redisCache.GetRedisClient().GetClient()
nKey := normalizeKey(seqKey)
val, err := rdb.Get(ctx, nKey).Int64()
if err == nil {
return val, nil
}
}
rdb := redisCache.GetRedisClient().GetClient()
nKey := normalizeKey(seqKey)
val, err := rdb.Get(ctx, nKey).Int64()
if err != nil {
return 0, ErrKeyNotFound
// Redis 未命中或不可用,降级到 DB
if c.repo != nil {
conv, err := c.repo.GetConversationByID(convID)
if err == nil && conv.LastSeq > 0 {
return conv.LastSeq, nil
}
}
return val, nil
return 0, ErrKeyNotFound
}
// GetConvMaxSeqBatch 批量获取多个会话的 maxSeqMGet 减少网络往返)
@@ -637,6 +645,12 @@ func (c *ConversationCache) GetConvMaxSeqBatch(ctx context.Context, convIDs []st
result := make(map[string]int64, len(convIDs))
for i, val := range vals {
if val == nil {
// Redis 未命中,降级到 DB
if c.repo != nil {
if conv, err := c.repo.GetConversationByID(convIDs[i]); err == nil && conv.LastSeq > 0 {
result[convIDs[i]] = conv.LastSeq
}
}
continue
}
var seq int64
@@ -699,10 +713,17 @@ func (c *ConversationCache) GetNextSeq(ctx context.Context, convID string) (int6
newSeq, err := c.cache.Incr(ctx, seqKey)
if err != nil {
// Redis 不可用时降级到 DB拿到值后回写 Redis 以便下次命中缓存
if c.msgRepo != nil {
if dbSeq, dbErr := c.msgRepo.GetNextSeq(convID); dbErr == nil {
c.cache.Set(seqKey, dbSeq, c.settings.SeqTTL)
return dbSeq, nil
}
}
return 0, fmt.Errorf("redis incr seq failed: %w", err)
}
// 首次调用时 Redis key 不存在INCR 从 0 返回 1
// 首次调用时 Redis key 不存在INCR 从 0 返回 1,需要从 DB 补齐
if newSeq == 1 && c.repo != nil {
conv, err := c.repo.GetConversationByID(convID)
if err != nil {
@@ -710,17 +731,17 @@ func (c *ConversationCache) GetNextSeq(ctx context.Context, convID string) (int6
}
if conv.LastSeq > 0 {
// INCR 已返回 1需要补齐 delta 使总值为 last_seq + 1
alignedSeq, err := c.cache.IncrBySeq(ctx, seqKey, conv.LastSeq)
if err != nil {
return conv.LastSeq + 1, nil // 降级返回 DB 值
}
newSeq = alignedSeq
}
_ = c.cache.Expire(ctx, seqKey, c.settings.SeqTTL)
}
// 每次 INCR 后刷新 TTL防止 seq 计数器过期导致从头开始
_ = c.cache.Expire(ctx, seqKey, c.settings.SeqTTL)
return newSeq, nil
}

View File

@@ -352,8 +352,13 @@ func (c *LayeredCache) ZCard(ctx context.Context, key string) (int64, error) {
return c.redis.ZCard(ctx, key)
}
// IsEnabled 返回 Redis 是否可用
func (c *LayeredCache) IsEnabled() bool {
return c.enabled && c.redis != nil
}
func (c *LayeredCache) Incr(ctx context.Context, key string) (int64, error) {
if !c.enabled || c.redis == nil {
if !c.IsEnabled() {
return 0, ErrKeyNotFound
}
c.local.Delete(key)

View File

@@ -74,3 +74,8 @@ func (a *MessageRepositoryAdapter) CreateMessage(msg *model.Message) error {
func (a *MessageRepositoryAdapter) UpdateConversationLastSeq(convID string, seq int64) error {
return a.repo.UpdateConversationLastSeq(convID, seq)
}
// GetNextSeq 实现 MessageRepository 接口
func (a *MessageRepositoryAdapter) GetNextSeq(convID string) (int64, error) {
return a.repo.GetNextSeq(convID)
}