加分布式锁
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m0s

This commit is contained in:
Break
2026-06-09 10:54:13 +08:00
parent 4ccbf13197
commit 6a70713b30
5 changed files with 88 additions and 16 deletions

View File

@@ -28,6 +28,8 @@ var acquireConcurrencyScript = redis.NewScript(`
return current
`)
const cardTrafficSyncLockTTL = 10 * time.Minute
// PollingBase 轮询共享基类
// 封装并发控制、卡缓存、重入队、配置间隔查询等公共方法,所有 Handler 共享
type PollingBase struct {
@@ -91,6 +93,28 @@ func (b *PollingBase) releaseConcurrency(ctx context.Context, taskType string) {
}
}
// acquireCardTrafficSyncLock 获取卡流量同步锁,避免轮询与手动刷新重复统计同一上游读数。
func (b *PollingBase) acquireCardTrafficSyncLock(ctx context.Context, cardID uint) (bool, error) {
if b.redis == nil {
return true, nil
}
locked, err := b.redis.SetNX(ctx, constants.RedisCardTrafficSyncLockKey(cardID), "1", cardTrafficSyncLockTTL).Result()
if err != nil {
return false, err
}
return locked, nil
}
// releaseCardTrafficSyncLock 释放卡流量同步锁。
func (b *PollingBase) releaseCardTrafficSyncLock(ctx context.Context, cardID uint) {
if b.redis == nil {
return
}
if err := b.redis.Del(ctx, constants.RedisCardTrafficSyncLockKey(cardID)).Err(); err != nil {
b.logger.Warn("释放卡流量同步锁失败", zap.Uint("card_id", cardID), zap.Error(err))
}
}
// requeueCard 将卡按匹配配置间隔重新入队分片 Sorted Set
// ⚠️ 关键Lua 脚本原子出队后卡已从队列移除,若并发满时直接 return 会导致卡永久丢失
// 调用方必须在 acquireConcurrency 返回 false 时调用此方法入队后再返回

View File

@@ -62,7 +62,19 @@ func (h *PollingCarddataHandler) Handle(ctx context.Context, t *asynq.Task) erro
}
defer h.base.releaseConcurrency(ctx, constants.TaskTypePollingCarddata)
card, err := h.base.getCardWithCache(ctx, cardID)
locked, lockErr := h.base.acquireCardTrafficSyncLock(ctx, cardID)
if lockErr != nil {
h.base.logger.Warn("获取卡流量同步锁失败,延迟重入队", zap.Uint("card_id", cardID), zap.Error(lockErr))
return h.base.queueMgr.Requeue(ctx, cardID, constants.TaskTypePollingCarddata, time.Now().Add(5*time.Second))
}
if !locked {
h.base.logger.Info("卡流量同步正在执行,延迟重入队", zap.Uint("card_id", cardID))
return h.base.queueMgr.Requeue(ctx, cardID, constants.TaskTypePollingCarddata, time.Now().Add(5*time.Second))
}
defer h.base.releaseCardTrafficSyncLock(ctx, cardID)
h.base.invalidateCardCache(ctx, cardID)
card, err := h.iotCardStore.GetByID(ctx, cardID)
if err != nil {
if isNotFound(err) {
return nil
@@ -105,25 +117,18 @@ func (h *PollingCarddataHandler) Handle(ctx context.Context, t *asynq.Task) erro
if updateErr := h.iotCardStore.UpdateFields(ctx, cardID, updates); updateErr != nil {
h.base.logger.Error("更新卡流量信息失败", zap.Uint("card_id", cardID), zap.Error(updateErr))
h.base.updateStats(ctx, constants.TaskTypePollingCarddata, false, time.Since(startTime))
return h.base.requeueCard(ctx, cardID, constants.TaskTypePollingCarddata)
}
go h.base.insertDataUsageRecord(context.Background(), cardID, flowIncrementMB, now)
cacheUpdates := map[string]any{}
if reading, ok := updates["last_gateway_reading_mb"]; ok {
cacheUpdates["last_gateway_reading_mb"] = reading
if refreshedCard, loadErr := h.iotCardStore.GetByID(ctx, cardID); loadErr == nil {
writeCardToCache(h.base, constants.RedisPollingCardInfoKey(cardID), refreshedCard)
} else {
h.base.invalidateCardCache(ctx, cardID)
h.base.logger.Warn("刷新卡缓存失败", zap.Uint("card_id", cardID), zap.Error(loadErr))
}
if isCrossMonth {
// 跨月时同步更新缓存中的本月开始日期和本月用量,避免下次从缓存读取时再次误判跨月
currentMonthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
cacheUpdates["current_month_start_date"] = currentMonthStart.Unix()
if flowIncrementMB > 0 {
cacheUpdates["current_month_usage_mb"] = flowIncrementMB
} else {
cacheUpdates["current_month_usage_mb"] = float64(0)
}
}
h.base.updateCardCache(ctx, cardID, cacheUpdates)
if flowIncrementMB > 0 && h.usageService != nil {
if deductErr := h.usageService.DeductDataUsage(ctx, constants.AssetTypeIotCard, cardID, flowIncrementMB); deductErr != nil {