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:
109
internal/cache/conversation_cache.go
vendored
109
internal/cache/conversation_cache.go
vendored
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
"with_you/internal/model"
|
"with_you/internal/model"
|
||||||
|
redisPkg "with_you/internal/pkg/redis"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
@@ -31,6 +32,14 @@ redis.call('SETNX', seqKey, initVal)
|
|||||||
return redis.call('INCR', seqKey)
|
return redis.call('INCR', seqKey)
|
||||||
`)
|
`)
|
||||||
|
|
||||||
|
// syncSeqLua 写透缓存:确保 Redis seq >= DB seq,防止冷启动后读到过期值
|
||||||
|
var syncSeqLua = redis.NewScript(`
|
||||||
|
local current = tonumber(redis.call('GET', KEYS[1]))
|
||||||
|
if current == nil or current < tonumber(ARGV[1]) then
|
||||||
|
redis.call('SET', KEYS[1], ARGV[1])
|
||||||
|
end
|
||||||
|
`)
|
||||||
|
|
||||||
// CachedConversation 带缓存元数据的会话
|
// CachedConversation 带缓存元数据的会话
|
||||||
type CachedConversation struct {
|
type CachedConversation struct {
|
||||||
Data *model.Conversation // 实际数据
|
Data *model.Conversation // 实际数据
|
||||||
@@ -713,86 +722,58 @@ func (c *ConversationCache) ComputeAllUnreadCount(ctx context.Context, userID st
|
|||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNextSeq 获取会话的下一个 seq 值(原子递增)
|
// GetNextSeq 获取会话的下一个 seq 值(单次 Redis INCR,原子无竞态)
|
||||||
// 启用 seq 预分配时使用本地缓冲区 + Redis Lua 分配;否则使用旧 INCR 逻辑
|
|
||||||
func (c *ConversationCache) GetNextSeq(ctx context.Context, convID string) (int64, error) {
|
func (c *ConversationCache) GetNextSeq(ctx context.Context, convID string) (int64, error) {
|
||||||
// 优先使用 seq 预分配
|
// 优先使用 SeqBufferManager(已简化为单次 INCR)
|
||||||
if c.seqBufferMgr != nil && c.seqBufferMgr.IsEnabled() {
|
if c.seqBufferMgr != nil && c.seqBufferMgr.IsEnabled() {
|
||||||
return c.seqBufferMgr.GetNextSeq(ctx, convID, false)
|
return c.seqBufferMgr.GetNextSeq(ctx, convID, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 降级到旧逻辑:使用 Lua 脚本原子执行 INCR + 首次初始化对齐,避免竞态
|
|
||||||
seqKey := MessageSeqKey(convID)
|
seqKey := MessageSeqKey(convID)
|
||||||
|
|
||||||
// 尝试用 Lua 脚本原子递增(key 存在时 INCR,不存在时返回 0 触发冷启动)
|
// 尝试 Redis INCR(key 存在时递增,不存在时返回 0 触发冷启动)
|
||||||
if lc, ok := c.cache.(*LayeredCache); ok && lc.IsEnabled() {
|
if lc, ok := c.cache.(*LayeredCache); ok && lc.IsEnabled() {
|
||||||
rdb := lc.GetRedisClient()
|
rdb := lc.GetRedisClient()
|
||||||
if rdb != nil {
|
if rdb != nil {
|
||||||
result, err := nextSeqLua.Run(ctx, rdb.GetClient(), []string{seqKey}).Int64()
|
result, err := nextSeqLua.Run(ctx, rdb.GetClient(), []string{seqKey}).Int64()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if result > 0 {
|
if result > 0 {
|
||||||
// key 已存在,INCR 成功
|
|
||||||
_ = c.cache.Expire(ctx, seqKey, c.settings.SeqTTL)
|
_ = c.cache.Expire(ctx, seqKey, c.settings.SeqTTL)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
// result == 0: 冷启动,key 不存在,需从 DB 初始化
|
// 冷启动:从 DB 加载 last_seq,SETNX+INCR 原子初始化
|
||||||
if c.repo != nil {
|
return c.initSeqFromDB(ctx, convID, seqKey, rdb)
|
||||||
conv, dbErr := c.repo.GetConversationByID(convID)
|
|
||||||
if dbErr != nil {
|
|
||||||
return 0, fmt.Errorf("failed to load conversation for seq init: %w", dbErr)
|
|
||||||
}
|
|
||||||
initVal := conv.LastSeq // SETNX 使用 last_seq,INCR 返回 last_seq + 1
|
|
||||||
initResult, initErr := initSeqLua.Run(ctx, rdb.GetClient(), []string{seqKey}, initVal).Int64()
|
|
||||||
if initErr != nil {
|
|
||||||
// 初始化失败,降级到非原子路径
|
|
||||||
zap.L().Warn("initSeqLua failed, falling back", zap.Error(initErr))
|
|
||||||
} else {
|
|
||||||
_ = c.cache.Expire(ctx, seqKey, c.settings.SeqTTL)
|
|
||||||
return initResult, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Lua 脚本失败,降级到非原子路径
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 非原子降级路径(Redis 不可用或 Lua 不支持)
|
// Redis 不可用,降级到 DB(SELECT FOR UPDATE 保证原子性)
|
||||||
newSeq, err := c.cache.Incr(ctx, seqKey)
|
if c.msgRepo != nil {
|
||||||
if err != nil {
|
if dbSeq, dbErr := c.msgRepo.GetNextSeq(convID); dbErr == nil {
|
||||||
// Redis 不可用时降级到 DB
|
return dbSeq, nil
|
||||||
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,需要从 DB 补齐
|
|
||||||
// 注意:此路径存在竞态窗口,优先使用上面的 Lua 原子路径
|
|
||||||
if newSeq == 1 && c.repo != nil {
|
|
||||||
conv, err := c.repo.GetConversationByID(convID)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("failed to load conversation for seq init: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if conv.LastSeq > 0 {
|
|
||||||
alignedSeq, err := c.cache.IncrBySeq(ctx, seqKey, conv.LastSeq)
|
|
||||||
if err != nil {
|
|
||||||
return conv.LastSeq + 1, nil // 降级返回 DB 值
|
|
||||||
}
|
|
||||||
newSeq = alignedSeq
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return 0, fmt.Errorf("seq allocation failed for %s: no redis or db available", convID)
|
||||||
// 每次 INCR 后刷新 TTL,防止 seq 计数器过期导致从头开始
|
|
||||||
_ = c.cache.Expire(ctx, seqKey, c.settings.SeqTTL)
|
|
||||||
|
|
||||||
return newSeq, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNextSeqWithGroup 获取会话的下一个 seq 值(支持群聊标识,用于 seq 预分配步长区分)
|
// initSeqFromDB 冷启动:从 DB 加载 last_seq,用 SETNX+INCR 原子初始化
|
||||||
|
func (c *ConversationCache) initSeqFromDB(ctx context.Context, convID, seqKey string, rdb *redisPkg.Client) (int64, error) {
|
||||||
|
if c.repo == nil {
|
||||||
|
return 0, fmt.Errorf("conversation repository not configured for seq init")
|
||||||
|
}
|
||||||
|
conv, dbErr := c.repo.GetConversationByID(convID)
|
||||||
|
if dbErr != nil {
|
||||||
|
return 0, fmt.Errorf("load conversation for seq init: %w", dbErr)
|
||||||
|
}
|
||||||
|
result, initErr := initSeqLua.Run(ctx, rdb.GetClient(), []string{seqKey}, conv.LastSeq).Int64()
|
||||||
|
if initErr != nil {
|
||||||
|
return 0, fmt.Errorf("initSeqLua failed: %w", initErr)
|
||||||
|
}
|
||||||
|
_ = c.cache.Expire(ctx, seqKey, c.settings.SeqTTL)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNextSeqWithGroup 获取会话的下一个 seq 值(isGroup 参数保留兼容,简化后不区分步长)
|
||||||
func (c *ConversationCache) GetNextSeqWithGroup(ctx context.Context, convID string, isGroup bool) (int64, error) {
|
func (c *ConversationCache) GetNextSeqWithGroup(ctx context.Context, convID string, isGroup bool) (int64, error) {
|
||||||
if c.seqBufferMgr != nil && c.seqBufferMgr.IsEnabled() {
|
if c.seqBufferMgr != nil && c.seqBufferMgr.IsEnabled() {
|
||||||
return c.seqBufferMgr.GetNextSeq(ctx, convID, isGroup)
|
return c.seqBufferMgr.GetNextSeq(ctx, convID, isGroup)
|
||||||
@@ -800,6 +781,22 @@ func (c *ConversationCache) GetNextSeqWithGroup(ctx context.Context, convID stri
|
|||||||
return c.GetNextSeq(ctx, convID)
|
return c.GetNextSeq(ctx, convID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SyncConvSeq 写透缓存:将 DB 刚分配的 seq 同步到 Redis,确保 Redis seq >= DB seq
|
||||||
|
func (c *ConversationCache) SyncConvSeq(ctx context.Context, convID string, seq int64) {
|
||||||
|
seqKey := MessageSeqKey(convID)
|
||||||
|
|
||||||
|
if lc, ok := c.cache.(*LayeredCache); ok && lc.IsEnabled() {
|
||||||
|
rdb := lc.GetRedisClient()
|
||||||
|
if rdb != nil {
|
||||||
|
_ = syncSeqLua.Run(ctx, rdb.GetClient(), []string{seqKey}, seq).Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.seqBufferMgr != nil && c.seqBufferMgr.rdb != nil {
|
||||||
|
_ = syncSeqLua.Run(ctx, c.seqBufferMgr.rdb, []string{seqKey}, seq).Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// 消息缓存方法
|
// 消息缓存方法
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
286
internal/cache/seq_buffer.go
vendored
286
internal/cache/seq_buffer.go
vendored
@@ -3,7 +3,6 @@ package cache
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
@@ -12,139 +11,27 @@ import (
|
|||||||
redisPkg "with_you/internal/pkg/redis"
|
redisPkg "with_you/internal/pkg/redis"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SeqBufferConfig seq 预分配配置
|
const seqKeyTTL = 24 * time.Hour
|
||||||
|
|
||||||
|
// SeqBufferConfig seq 预分配配置(保留结构以兼容配置文件,实际不再使用缓冲)
|
||||||
type SeqBufferConfig struct {
|
type SeqBufferConfig struct {
|
||||||
Enabled bool `mapstructure:"enabled"`
|
Enabled bool `mapstructure:"enabled"`
|
||||||
PrivateBufferSize int `mapstructure:"private_buffer_size"` // 私聊预分配步长
|
PrivateBufferSize int `mapstructure:"private_buffer_size"`
|
||||||
GroupBufferSize int `mapstructure:"group_buffer_size"` // 群聊预分配步长
|
GroupBufferSize int `mapstructure:"group_buffer_size"`
|
||||||
LockTimeoutMs int `mapstructure:"lock_timeout_ms"` // 分布式锁超时(ms)
|
LockTimeoutMs int `mapstructure:"lock_timeout_ms"`
|
||||||
MaxRetries int `mapstructure:"max_retries"` // 冷启动重试次数
|
MaxRetries int `mapstructure:"max_retries"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// localSeqBuffer 本地 seq 缓冲区
|
// SeqBufferManager seq 分配管理器(简化版:每条消息一个 Redis INCR,无本地缓冲)
|
||||||
type localSeqBuffer struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
curr int64 // 当前已分配到的 seq
|
|
||||||
last int64 // 本地缓冲区的上界
|
|
||||||
}
|
|
||||||
|
|
||||||
// SeqBufferManager seq 预分配管理器
|
|
||||||
type SeqBufferManager struct {
|
type SeqBufferManager struct {
|
||||||
rdb *redis.Client // 原始 Redis 客户端(用于执行 Lua 脚本)
|
rdb *redis.Client
|
||||||
config SeqBufferConfig
|
config SeqBufferConfig
|
||||||
repo ConversationRepository
|
repo ConversationRepository
|
||||||
msgRepo MessageRepository
|
msgRepo MessageRepository
|
||||||
buffers sync.Map // convID -> *localSeqBuffer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// mallocLua 预分配 Lua 跟踪脚本
|
// NewSeqBufferManager 创建 seq 分配管理器
|
||||||
// KEYS[1] = seq_buf:{convID} (Hash: CURR/LAST/LOCK/TIME)
|
|
||||||
// KEYS[2] = msg_seq:{convID} (String, 兼容旧 INCR key)
|
|
||||||
// ARGV[1] = bufferSize
|
|
||||||
// ARGV[2] = lockTimeoutMs (毫秒)
|
|
||||||
// ARGV[3] = current timestamp (毫秒)
|
|
||||||
//
|
|
||||||
// 返回:
|
|
||||||
// status=1, startSeq, endSeq — 成功分配
|
|
||||||
// status=2 — 冷启动(key 不存在)
|
|
||||||
// status=0 — 获取锁失败
|
|
||||||
var mallocLua = redis.NewScript(`
|
|
||||||
local bufKey = KEYS[1]
|
|
||||||
local seqKey = KEYS[2]
|
|
||||||
local bufSize = tonumber(ARGV[1])
|
|
||||||
local lockMs = tonumber(ARGV[2])
|
|
||||||
local nowMs = tonumber(ARGV[3])
|
|
||||||
|
|
||||||
-- 尝试获取分布式锁
|
|
||||||
local lockVal = redis.call('HGET', bufKey, 'LOCK')
|
|
||||||
if lockVal then
|
|
||||||
local lockTs = tonumber(lockVal)
|
|
||||||
if lockTs > 0 and (nowMs - lockTs) < lockMs then
|
|
||||||
return {0, 0, 0}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
redis.call('HSET', bufKey, 'LOCK', nowMs)
|
|
||||||
redis.call('HSET', bufKey, 'TIME', nowMs)
|
|
||||||
|
|
||||||
local curr = tonumber(redis.call('HGET', bufKey, 'CURR'))
|
|
||||||
local last = tonumber(redis.call('HGET', bufKey, 'LAST'))
|
|
||||||
|
|
||||||
if curr == nil or last == nil then
|
|
||||||
-- 冷启动:key 不存在
|
|
||||||
redis.call('HDEL', bufKey, 'LOCK')
|
|
||||||
return {2, 0, 0}
|
|
||||||
end
|
|
||||||
|
|
||||||
if curr < last then
|
|
||||||
-- 本地缓冲区仍有余量,直接分配一个 seq
|
|
||||||
local newCurr = redis.call('HINCRBY', bufKey, 'CURR', 1)
|
|
||||||
redis.call('HDEL', bufKey, 'LOCK')
|
|
||||||
return {1, newCurr, last}
|
|
||||||
end
|
|
||||||
|
|
||||||
-- 缓冲区耗尽,扩展 LAST
|
|
||||||
local newLast = last + bufSize
|
|
||||||
redis.call('HSET', bufKey, 'LAST', newLast)
|
|
||||||
-- 推进 CURR 防止下一个调用者获得相同的 start seq
|
|
||||||
redis.call('HSET', bufKey, 'CURR', curr + 1)
|
|
||||||
-- 同步更新 msg_seq(兼容旧 INCR key,确保 READ 的值不回退)
|
|
||||||
local oldSeq = tonumber(redis.call('GET', seqKey))
|
|
||||||
if oldSeq and oldSeq > newLast then
|
|
||||||
newLast = oldSeq + bufSize
|
|
||||||
redis.call('HSET', bufKey, 'LAST', newLast)
|
|
||||||
end
|
|
||||||
redis.call('SET', seqKey, newLast)
|
|
||||||
redis.call('HDEL', bufKey, 'LOCK')
|
|
||||||
return {1, curr + 1, newLast}
|
|
||||||
`)
|
|
||||||
|
|
||||||
// initLua 冷启动初始化 Lua 跟踪脚本
|
|
||||||
// KEYS[1] = seq_buf:{convID}
|
|
||||||
// KEYS[2] = msg_seq:{convID}
|
|
||||||
// ARGV[1] = startSeq (从 DB 获取的 conv.LastSeq)
|
|
||||||
// ARGV[2] = bufferSize
|
|
||||||
// ARGV[3] = lockTimeoutMs
|
|
||||||
// ARGV[4] = current timestamp (毫秒)
|
|
||||||
var initLua = redis.NewScript(`
|
|
||||||
local bufKey = KEYS[1]
|
|
||||||
local seqKey = KEYS[2]
|
|
||||||
local startSeq = tonumber(ARGV[1])
|
|
||||||
local bufSize = tonumber(ARGV[2])
|
|
||||||
local lockMs = tonumber(ARGV[3])
|
|
||||||
local nowMs = tonumber(ARGV[4])
|
|
||||||
|
|
||||||
-- 创建 seq_buf Hash
|
|
||||||
redis.call('HSET', bufKey, 'CURR', startSeq)
|
|
||||||
redis.call('HSET', bufKey, 'LAST', startSeq + bufSize)
|
|
||||||
redis.call('HSET', bufKey, 'LOCK', 0)
|
|
||||||
redis.call('HSET', bufKey, 'TIME', nowMs)
|
|
||||||
|
|
||||||
-- 同步更新 msg_seq(兼容旧 INCR key)
|
|
||||||
local oldSeq = tonumber(redis.call('GET', seqKey))
|
|
||||||
if oldSeq and oldSeq > (startSeq + bufSize) then
|
|
||||||
redis.call('HSET', bufKey, 'LAST', oldSeq + bufSize)
|
|
||||||
redis.call('SET', seqKey, oldSeq + bufSize)
|
|
||||||
else
|
|
||||||
redis.call('SET', seqKey, startSeq + bufSize)
|
|
||||||
end
|
|
||||||
|
|
||||||
return {1, startSeq + 1, startSeq + bufSize}
|
|
||||||
`)
|
|
||||||
|
|
||||||
// NewSeqBufferManager 创建 seq 预分配管理器
|
|
||||||
func NewSeqBufferManager(rdb *redis.Client, cfg SeqBufferConfig, repo ConversationRepository, msgRepo MessageRepository) *SeqBufferManager {
|
func NewSeqBufferManager(rdb *redis.Client, cfg SeqBufferConfig, repo ConversationRepository, msgRepo MessageRepository) *SeqBufferManager {
|
||||||
if cfg.PrivateBufferSize <= 0 {
|
|
||||||
cfg.PrivateBufferSize = 50
|
|
||||||
}
|
|
||||||
if cfg.GroupBufferSize <= 0 {
|
|
||||||
cfg.GroupBufferSize = 200
|
|
||||||
}
|
|
||||||
if cfg.LockTimeoutMs <= 0 {
|
|
||||||
cfg.LockTimeoutMs = 5000
|
|
||||||
}
|
|
||||||
if cfg.MaxRetries <= 0 {
|
|
||||||
cfg.MaxRetries = 3
|
|
||||||
}
|
|
||||||
return &SeqBufferManager{
|
return &SeqBufferManager{
|
||||||
rdb: rdb,
|
rdb: rdb,
|
||||||
config: cfg,
|
config: cfg,
|
||||||
@@ -153,160 +40,67 @@ func NewSeqBufferManager(rdb *redis.Client, cfg SeqBufferConfig, repo Conversati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetNextSeq 获取下一个 seq(从本地缓冲区分配,耗尽时向 Redis 预分配新区间)
|
// GetNextSeq 获取下一个 seq(单次 Redis INCR,原子且无竞态)
|
||||||
func (m *SeqBufferManager) GetNextSeq(ctx context.Context, convID string, isGroup bool) (int64, error) {
|
func (m *SeqBufferManager) GetNextSeq(ctx context.Context, convID string, _ bool) (int64, error) {
|
||||||
bufSize := m.config.PrivateBufferSize
|
|
||||||
if isGroup {
|
|
||||||
bufSize = m.config.GroupBufferSize
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 检查本地缓冲区
|
|
||||||
bufVal, ok := m.buffers.Load(convID)
|
|
||||||
if ok {
|
|
||||||
buf := bufVal.(*localSeqBuffer)
|
|
||||||
buf.mu.Lock()
|
|
||||||
if buf.curr < buf.last {
|
|
||||||
seq := buf.curr + 1
|
|
||||||
buf.curr = seq
|
|
||||||
buf.mu.Unlock()
|
|
||||||
return seq, nil
|
|
||||||
}
|
|
||||||
buf.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 本地缓冲区耗尽或不存在,向 Redis 预分配
|
|
||||||
startSeq, endSeq, err := m.allocateFromRedis(ctx, convID, bufSize)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("allocate from redis: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 更新本地缓冲区(CAS 防止并发分配时丢失区间)
|
|
||||||
newBuf := &localSeqBuffer{curr: startSeq, last: endSeq}
|
|
||||||
for {
|
|
||||||
existing, loaded := m.buffers.LoadOrStore(convID, newBuf)
|
|
||||||
if !loaded {
|
|
||||||
// 首次插入,直接使用
|
|
||||||
return startSeq, nil
|
|
||||||
}
|
|
||||||
// 已有缓冲区,检查是否被其他 goroutine 刷新
|
|
||||||
oldBuf := existing.(*localSeqBuffer)
|
|
||||||
oldBuf.mu.Lock()
|
|
||||||
if oldBuf.curr < oldBuf.last {
|
|
||||||
// 旧缓冲区仍有余量,直接使用(丢弃本次分配,避免区间泄漏)
|
|
||||||
seq := oldBuf.curr + 1
|
|
||||||
oldBuf.curr = seq
|
|
||||||
oldBuf.mu.Unlock()
|
|
||||||
return seq, nil
|
|
||||||
}
|
|
||||||
// 旧缓冲区耗尽,替换为新分配的区间
|
|
||||||
m.buffers.Store(convID, newBuf)
|
|
||||||
oldBuf.mu.Unlock()
|
|
||||||
return startSeq, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// allocateFromRedis 向 Redis 预分配 seq 区间
|
|
||||||
func (m *SeqBufferManager) allocateFromRedis(ctx context.Context, convID string, bufferSize int) (startSeq, endSeq int64, err error) {
|
|
||||||
bufKey := SeqBufferKey(convID)
|
|
||||||
seqKey := MessageSeqKey(convID)
|
seqKey := MessageSeqKey(convID)
|
||||||
nowMs := time.Now().UnixMilli()
|
|
||||||
|
|
||||||
for i := 0; i < m.config.MaxRetries; i++ {
|
// 1. 尝试 INCR(key 存在时直接递增,原子操作)
|
||||||
result, err := mallocLua.Run(ctx, m.rdb, []string{bufKey, seqKey}, bufferSize, m.config.LockTimeoutMs, nowMs).Int64Slice()
|
result, err := nextSeqLua.Run(ctx, m.rdb, []string{seqKey}).Int64()
|
||||||
if err != nil {
|
if err == nil {
|
||||||
// Redis 不可用,降级到 DB
|
if result > 0 {
|
||||||
zap.L().Warn("seq malloc lua failed, falling back to DB", zap.String("convID", convID), zap.Error(err))
|
m.rdb.Expire(ctx, seqKey, seqKeyTTL)
|
||||||
if m.msgRepo != nil {
|
return result, nil
|
||||||
if dbSeq, dbErr := m.msgRepo.GetNextSeq(convID); dbErr == nil {
|
|
||||||
return dbSeq, dbSeq, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0, 0, fmt.Errorf("lua script error: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
status := result[0]
|
|
||||||
switch status {
|
|
||||||
case 1:
|
|
||||||
// 成功分配
|
|
||||||
return result[1], result[2], nil
|
|
||||||
case 2:
|
|
||||||
// 冷启动,需要从 DB 初始化
|
|
||||||
startSeq, endSeq, initErr := m.initFromDB(ctx, convID, bufferSize)
|
|
||||||
if initErr != nil {
|
|
||||||
return 0, 0, fmt.Errorf("init from db: %w", initErr)
|
|
||||||
}
|
|
||||||
return startSeq, endSeq, nil
|
|
||||||
case 0:
|
|
||||||
// 获取锁失败,短暂等待后重试
|
|
||||||
time.Sleep(time.Duration(50) * time.Millisecond)
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
|
// result == 0: 冷启动,需从 DB 初始化
|
||||||
|
return m.initAndIncr(ctx, convID, seqKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 所有重试失败,降级到 DB
|
// Redis 不可用,降级到 DB
|
||||||
zap.L().Warn("seq malloc retries exhausted, falling back to DB", zap.String("convID", convID))
|
zap.L().Warn("seq INCR failed, falling back to DB", zap.String("convID", convID), zap.Error(err))
|
||||||
if m.msgRepo != nil {
|
if m.msgRepo != nil {
|
||||||
if dbSeq, dbErr := m.msgRepo.GetNextSeq(convID); dbErr == nil {
|
if dbSeq, dbErr := m.msgRepo.GetNextSeq(convID); dbErr == nil {
|
||||||
return dbSeq, dbSeq, nil
|
return dbSeq, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return 0, 0, fmt.Errorf("malloc retries exhausted")
|
return 0, fmt.Errorf("seq allocation failed for %s: %w", convID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// initFromDB 冷启动时从 DB 加载 LastSeq 并初始化 Redis seq_buf Hash
|
// initAndIncr 冷启动:从 DB 加载 last_seq,然后用 SETNX+INCR 原子初始化
|
||||||
func (m *SeqBufferManager) initFromDB(ctx context.Context, convID string, bufferSize int) (startSeq, endSeq int64, err error) {
|
func (m *SeqBufferManager) initAndIncr(ctx context.Context, convID, seqKey string) (int64, error) {
|
||||||
if m.repo == nil {
|
if m.repo == nil {
|
||||||
return 0, 0, fmt.Errorf("conversation repository not configured")
|
return 0, fmt.Errorf("conversation repository not configured for seq init")
|
||||||
}
|
}
|
||||||
|
|
||||||
conv, err := m.repo.GetConversationByID(convID)
|
conv, err := m.repo.GetConversationByID(convID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, fmt.Errorf("load conversation: %w", err)
|
return 0, fmt.Errorf("load conversation for seq init: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
dbLastSeq := conv.LastSeq
|
initVal := conv.LastSeq
|
||||||
if dbLastSeq <= 0 {
|
result, err := initSeqLua.Run(ctx, m.rdb, []string{seqKey}, initVal).Int64()
|
||||||
dbLastSeq = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
bufKey := SeqBufferKey(convID)
|
|
||||||
seqKey := MessageSeqKey(convID)
|
|
||||||
nowMs := time.Now().UnixMilli()
|
|
||||||
|
|
||||||
result, err := initLua.Run(ctx, m.rdb, []string{bufKey, seqKey}, dbLastSeq, bufferSize, m.config.LockTimeoutMs, nowMs).Int64Slice()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, 0, fmt.Errorf("init lua script error: %w", err)
|
return 0, fmt.Errorf("initSeqLua failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
zap.L().Info("seq buffer cold-start initialized",
|
m.rdb.Expire(ctx, seqKey, seqKeyTTL)
|
||||||
zap.String("convID", convID),
|
return result, nil
|
||||||
zap.Int64("dbLastSeq", dbLastSeq),
|
|
||||||
zap.Int64("startSeq", result[1]),
|
|
||||||
zap.Int64("endSeq", result[2]),
|
|
||||||
)
|
|
||||||
|
|
||||||
return result[1], result[2], nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InvalidateBuffer 清除本地缓冲区(会话 seq 被外部重置时调用)
|
// InvalidateBuffer no-op(保留接口兼容)
|
||||||
func (m *SeqBufferManager) InvalidateBuffer(convID string) {
|
func (m *SeqBufferManager) InvalidateBuffer(convID string) {}
|
||||||
m.buffers.Delete(convID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEnabled 是否启用预分配
|
// IsEnabled 是否启用
|
||||||
func (m *SeqBufferManager) IsEnabled() bool {
|
func (m *SeqBufferManager) IsEnabled() bool {
|
||||||
return m.config.Enabled && m.rdb != nil
|
return m.config.Enabled && m.rdb != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSeqBufferManagerFromCache 从 ConversationCache 的 Cache 接口创建 SeqBufferManager
|
// NewSeqBufferManagerFromCache 从 ConversationCache 的 Cache 接口创建 SeqBufferManager
|
||||||
// 当 wire 中不方便注入 redis.Client 时,可通过 Cache 接口获取底层 Redis 连接
|
|
||||||
func NewSeqBufferManagerFromCache(cacheBackend Cache, cfg SeqBufferConfig, repo ConversationRepository, msgRepo MessageRepository) *SeqBufferManager {
|
func NewSeqBufferManagerFromCache(cacheBackend Cache, cfg SeqBufferConfig, repo ConversationRepository, msgRepo MessageRepository) *SeqBufferManager {
|
||||||
redisCache, ok := cacheBackend.(*RedisCache)
|
redisCache, ok := cacheBackend.(*RedisCache)
|
||||||
if !ok {
|
if !ok {
|
||||||
zap.L().Warn("SeqBuffer: cache is not RedisCache, seq pre-allocation disabled")
|
zap.L().Warn("SeqBuffer: cache is not RedisCache, seq pre-allocation disabled")
|
||||||
return &SeqBufferManager{config: cfg, repo: repo, msgRepo: msgRepo}
|
return &SeqBufferManager{config: cfg, repo: repo, msgRepo: msgRepo}
|
||||||
}
|
}
|
||||||
|
|
||||||
rdb := redisCache.GetRedisClient().GetClient()
|
rdb := redisCache.GetRedisClient().GetClient()
|
||||||
return NewSeqBufferManager(rdb, cfg, repo, msgRepo)
|
return NewSeqBufferManager(rdb, cfg, repo, msgRepo)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -431,40 +431,43 @@ func (r *messageRepository) GetNextSeq(conversationID string) (int64, error) {
|
|||||||
return nextSeq, err
|
return nextSeq, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateMessageWithSeq 创建消息并更新seq(事务操作)
|
// CreateMessageWithSeq 创建消息并在事务内原子分配 seq
|
||||||
// msg.Seq 应由调用方通过 Redis INCR 预分配
|
// DB 是 seq 的唯一真相来源:SELECT FOR UPDATE 锁行 → last_seq+1 → 写入消息 → 更新 last_seq
|
||||||
|
// msg.Seq 会在事务内被设置,调用方无需预先分配
|
||||||
func (r *messageRepository) CreateMessageWithSeq(msg *model.Message) error {
|
func (r *messageRepository) CreateMessageWithSeq(msg *model.Message) error {
|
||||||
return r.db.Transaction(func(tx *gorm.DB) 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 {
|
if err := tx.Create(msg).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新会话last_seq:仅当新 seq 更大时才更新,防止并发写入导致 last_seq 回退
|
// 4. 更新会话 last_seq 和 last_msg_time
|
||||||
result := tx.Model(&model.Conversation{}).
|
if err := tx.Model(&model.Conversation{}).
|
||||||
Where("id = ? AND last_seq < ?", msg.ConversationID, msg.Seq).
|
Where("id = ?", msg.ConversationID).
|
||||||
Updates(map[string]any{
|
Updates(map[string]any{
|
||||||
"last_seq": msg.Seq,
|
"last_seq": msg.Seq,
|
||||||
"last_msg_time": gorm.Expr("CURRENT_TIMESTAMP"),
|
"last_msg_time": gorm.Expr("CURRENT_TIMESTAMP"),
|
||||||
})
|
}).Error; err != nil {
|
||||||
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 {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 新消息到达后,自动恢复被"仅自己删除"的会话
|
// 5. 新消息到达后,自动恢复被"仅自己删除"的会话
|
||||||
if err := tx.Model(&model.ConversationParticipant{}).
|
return tx.Model(&model.ConversationParticipant{}).
|
||||||
Where("conversation_id = ?", msg.ConversationID).
|
Where("conversation_id = ?", msg.ConversationID).
|
||||||
Update("hidden_at", nil).Error; err != nil {
|
Update("hidden_at", nil).Error
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -479,20 +479,18 @@ func (s *chatServiceImpl) SendMessage(ctx context.Context, senderID string, conv
|
|||||||
Status: model.MessageStatusNormal,
|
Status: model.MessageStatusNormal,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 Redis 获取下一个 seq
|
// seq 由 CreateMessageWithSeq 在事务内原子分配
|
||||||
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
|
// 使用事务创建消息并更新seq
|
||||||
if err := s.repo.CreateMessageWithSeq(message); err != nil {
|
if err := s.repo.CreateMessageWithSeq(message); err != nil {
|
||||||
return nil, fmt.Errorf("failed to save message: %w", err)
|
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
|
// 发送者自动已读:将发送者的 lastReadSeq 更新到消息 seq
|
||||||
_ = s.repo.UpdateLastReadSeq(conversationID, senderID, message.Seq)
|
_ = s.repo.UpdateLastReadSeq(conversationID, senderID, message.Seq)
|
||||||
if s.conversationCache != nil {
|
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) {
|
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 err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, errors.New("会话不存在,请重新创建会话")
|
return nil, errors.New("会话不存在,请重新创建会话")
|
||||||
@@ -1073,19 +1071,17 @@ func (s *chatServiceImpl) SaveMessage(ctx context.Context, senderID string, conv
|
|||||||
Status: model.MessageStatusNormal,
|
Status: model.MessageStatusNormal,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 Redis 获取下一个 seq(SaveMessage 方法)
|
// seq 由 CreateMessageWithSeq 在事务内原子分配
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.repo.CreateMessageWithSeq(message); err != nil {
|
if err := s.repo.CreateMessageWithSeq(message); err != nil {
|
||||||
return nil, fmt.Errorf("failed to save message: %w", err)
|
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 {
|
if s.conversationCache != nil {
|
||||||
s.conversationCache.InvalidateMessagePages(conversationID)
|
s.conversationCache.InvalidateMessagePages(conversationID)
|
||||||
|
|||||||
@@ -471,18 +471,7 @@ func (s *groupService) broadcastMemberJoinNotice(groupID string, targetUserID st
|
|||||||
Status: model.MessageStatusNormal,
|
Status: model.MessageStatusNormal,
|
||||||
Category: model.CategoryNotification,
|
Category: model.CategoryNotification,
|
||||||
}
|
}
|
||||||
// 从 Redis 获取下一个 seq
|
// seq 由 CreateMessageWithSeq 在事务内原子分配
|
||||||
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
|
|
||||||
}
|
|
||||||
if err := s.messageRepo.CreateMessageWithSeq(msg); err != nil {
|
if err := s.messageRepo.CreateMessageWithSeq(msg); err != nil {
|
||||||
zap.L().Warn("保存入群提示消息失败",
|
zap.L().Warn("保存入群提示消息失败",
|
||||||
zap.String("component", "broadcastMemberJoinNotice"),
|
zap.String("component", "broadcastMemberJoinNotice"),
|
||||||
@@ -492,6 +481,9 @@ func (s *groupService) broadcastMemberJoinNotice(groupID string, targetUserID st
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
savedMessage = msg
|
savedMessage = msg
|
||||||
|
if s.conversationCache != nil {
|
||||||
|
s.conversationCache.SyncConvSeq(context.Background(), conv.ID, msg.Seq)
|
||||||
|
}
|
||||||
s.invalidateConversationCachesAfterSystemMessage(conv.ID)
|
s.invalidateConversationCachesAfterSystemMessage(conv.ID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1396,18 +1388,7 @@ func (s *groupService) MuteMember(userID string, groupID string, targetUserID st
|
|||||||
Category: model.CategoryNotification,
|
Category: model.CategoryNotification,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 Redis 获取下一个 seq
|
// seq 由 CreateMessageWithSeq 在事务内原子分配
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.messageRepo.CreateMessageWithSeq(msg); err != nil {
|
if err := s.messageRepo.CreateMessageWithSeq(msg); err != nil {
|
||||||
zap.L().Warn("保存禁言消息失败",
|
zap.L().Warn("保存禁言消息失败",
|
||||||
@@ -1421,6 +1402,9 @@ func (s *groupService) MuteMember(userID string, groupID string, targetUserID st
|
|||||||
zap.String("msgID", msg.ID),
|
zap.String("msgID", msg.ID),
|
||||||
zap.Int64("seq", msg.Seq),
|
zap.Int64("seq", msg.Seq),
|
||||||
)
|
)
|
||||||
|
if s.conversationCache != nil {
|
||||||
|
s.conversationCache.SyncConvSeq(context.Background(), conv.ID, msg.Seq)
|
||||||
|
}
|
||||||
s.invalidateConversationCachesAfterSystemMessage(conv.ID)
|
s.invalidateConversationCachesAfterSystemMessage(conv.ID)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"with_you/internal/cache"
|
"with_you/internal/cache"
|
||||||
@@ -88,21 +87,17 @@ func (s *MessageService) SendMessage(ctx context.Context, senderID, receiverID s
|
|||||||
Status: model.MessageStatusNormal,
|
Status: model.MessageStatusNormal,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从 Redis 获取下一个 seq
|
// 事务内原子分配 seq + 创建消息(DB 是 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
|
|
||||||
err = s.messageRepo.CreateMessageWithSeq(msg)
|
err = s.messageRepo.CreateMessageWithSeq(msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 写透 seq 到 Redis,保持缓存与 DB 一致
|
||||||
|
if s.conversationCache != nil {
|
||||||
|
s.conversationCache.SyncConvSeq(context.Background(), conv.ID, msg.Seq)
|
||||||
|
}
|
||||||
|
|
||||||
// 新消息会改变分页结果,先失效分页缓存,避免读到旧列表
|
// 新消息会改变分页结果,先失效分页缓存,避免读到旧列表
|
||||||
if s.conversationCache != nil {
|
if s.conversationCache != nil {
|
||||||
s.conversationCache.InvalidateMessagePages(conv.ID)
|
s.conversationCache.InvalidateMessagePages(conv.ID)
|
||||||
|
|||||||
Reference in New Issue
Block a user