@@ -29,6 +29,14 @@ var acquireConcurrencyScript = redis.NewScript(`
` )
const cardTrafficSyncLockTTL = 10 * time . Minute
const pollingFallbackOperationTimeout = 5 * time . Second
// pollingFallbackContext 创建轮询兜底操作使用的独立短超时上下文。
// Asynq 任务 ctx 超时后,释放并发计数、释放锁、重入队仍必须尽量完成,
// 否则会造成并发计数短期卡住,甚至卡已出队但未重新入队。
func pollingFallbackContext ( ) ( context . Context , context . CancelFunc ) {
return context . WithTimeout ( context . Background ( ) , pollingFallbackOperationTimeout )
}
// PollingBase 轮询共享基类
// 封装并发控制、卡缓存、重入队、配置间隔查询等公共方法,所有 Handler 共享
@@ -86,10 +94,13 @@ func (b *PollingBase) acquireConcurrency(ctx context.Context, taskType string) b
}
// releaseConcurrency 释放并发信号量
func ( b * PollingBase ) releaseConcurrency ( ctx context . Context , taskType string ) {
func ( b * PollingBase ) releaseConcurrency ( _ context . Context , taskType string ) {
ctx , cancel := pollingFallbackContext ( )
defer cancel ( )
currentKey := constants . RedisPollingConcurrencyCurrentKey ( taskType )
if err := b . redis . Decr ( ctx , currentKey ) . Err ( ) ; err != nil {
b . logger . Warn ( "释放并发计数失败" , zap . Error ( err ) )
b . logger . Warn ( "释放并发计数失败" , zap . String ( "task_type" , taskType ) , zap . Error( err ) )
}
}
@@ -106,24 +117,43 @@ func (b *PollingBase) acquireCardTrafficSyncLock(ctx context.Context, cardID uin
}
// releaseCardTrafficSyncLock 释放卡流量同步锁。
func ( b * PollingBase ) releaseCardTrafficSyncLock ( ctx context . Context , cardID uint ) {
func ( b * PollingBase ) releaseCardTrafficSyncLock ( _ context . Context , cardID uint ) {
if b . redis == nil {
return
}
ctx , cancel := pollingFallbackContext ( )
defer cancel ( )
if err := b . redis . Del ( ctx , constants . RedisCardTrafficSyncLockKey ( cardID ) ) . Err ( ) ; err != nil {
b . logger . Warn ( "释放卡流量同步锁失败" , zap . Uint ( "card_id" , cardID ) , zap . Error ( err ) )
}
}
// requeueCardAt 使用独立短超时上下文执行真正的 ZADD 重入队。
func ( b * PollingBase ) requeueCardAt ( cardID uint , taskType string , nextCheckAt time . Time ) error {
ctx , cancel := pollingFallbackContext ( )
defer cancel ( )
if err := b . queueMgr . Requeue ( ctx , cardID , taskType , nextCheckAt ) ; err != nil {
b . logger . Error ( "重入队失败,卡可能暂停轮询" ,
zap . Uint ( "card_id" , cardID ) , zap . String ( "task_type" , taskType ) , zap . Error ( err ) )
return err
}
return nil
}
// requeueCard 将卡按匹配配置间隔重新入队分片 Sorted Set
// ⚠️ 关键: Lua 脚本原子出队后卡已从队列移除,若并发满时直接 return 会导致卡永久丢失
// 调用方必须在 acquireConcurrency 返回 false 时调用此方法入队后再返回
func ( b * PollingBase ) requeueCard ( ctx context . Context , cardID uint , taskType string ) error {
func ( b * PollingBase ) requeueCard ( _ context . Context , cardID uint , taskType string ) error {
ctx , cancel := pollingFallbackContext ( )
card , err := b . getCardWithCache ( ctx , cardID )
cancel ( )
if err != nil {
b . logger . Warn ( "重入队:获取卡信息失败" , zap . Uint ( "card_id" , cardID ) , zap . Error ( err ) )
b . logger . Warn ( "重入队:获取卡信息失败, 使用30秒兜底间隔 " ,
zap . Uint ( "card_id" , cardID ) , zap . String ( "task_type" , taskType ) , zap . Error ( err ) )
// 获取卡失败时立即重入队(下次仍然尝试处理)
return b . queueMgr . Requeue ( ctx , cardID , taskType , time . Now ( ) . Add ( 30 * time . Second ) )
return b . requeueCardAt ( cardID , taskType , time . Now ( ) . Add ( 30 * time . Second ) )
}
intervals := b . configMgr . MergedTaskIntervals ( card )
@@ -131,7 +161,7 @@ func (b *PollingBase) requeueCard(ctx context.Context, cardID uint, taskType str
// 兜底:配置未加载时延迟 30 秒重入队,防止卡从轮询永久消失
b . logger . Warn ( "无匹配轮询配置, 30秒后重入队( 配置可能未加载) " ,
zap . Uint ( "card_id" , cardID ) , zap . String ( "task_type" , taskType ) )
return b . queueMgr . Requeue ( ctx , cardID , taskType , time . Now ( ) . Add ( 30 * time . Second ) )
return b . requeueCardAt ( cardID , taskType , time . Now ( ) . Add ( 30 * time . Second ) )
}
info , ok := intervals [ taskType ]
if ! ok || info . Interval <= 0 {
@@ -139,5 +169,5 @@ func (b *PollingBase) requeueCard(ctx context.Context, cardID uint, taskType str
return nil
}
nextCheckAt := time . Now ( ) . Add ( time . Duration ( info . Interval ) * time . Second )
return b . queueMgr . Requeue ( ctx , cardID , taskType , nextCheckAt )
return b . requeueCardAt ( cardID , taskType , nextCheckAt )
}