fix: 停复机指令队列审查问题
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m24s

- 开关开启但分发器心跳缺失时回退为当场执行,避免指令无人消费
- 待确认指令先查上游状态再判断;执行期间被覆盖时把待确认带到新指令
- 同方向失败退避中重复写入不提前执行时间
- 领取容量把执行上限与本秒剩余 QPS 分开计算
- 队列复机共用单卡复机锁;执行上下文取消后按结果未知确认
- 消费者并行停止;读取最近调用时间失败时不覆盖;成功但终结日志失败按成功处理

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
邹进
2026-10-26 00:43:58 +09:00
parent 934a3a27a1
commit a4e3fe4d8f
9 changed files with 188 additions and 79 deletions

View File

@@ -19,7 +19,7 @@ const (
)
// srWriteLua 写入或覆盖停复机指令:方向相反时清零失败与待确认,递增版本,放入账户停复机队列。
// 指令执行中(存在处理中标记)时只更新指令内容与版本,由收尾按版本保留;待确认中且方向不变时保持确认时间。
// 指令执行中(存在处理中标记)时只更新指令内容与版本,由收尾按版本保留;待确认中、或同方向失败退避中时保持原执行时间。
//
// KEYS[1] 指令 Hash;KEYS[2] 账户停复机队列;KEYS[3] 处理中标记。
// ARGV:action、reason、scenes、source、carrier、now_ms、card_id。返回新版本号。
@@ -31,7 +31,9 @@ end
redis.call('HSET', KEYS[1], 'action', ARGV[1], 'reason', ARGV[2], 'scenes', ARGV[3], 'source', ARGV[4], 'carrier', ARGV[5], 'at', ARGV[6])
local ver = redis.call('HINCRBY', KEYS[1], 'ver', 1)
if redis.call('EXISTS', KEYS[3]) == 1 then return ver end
if redis.call('HGET', KEYS[1], 'pending') == '1' then
local backingOff = prev == ARGV[1] and tonumber(redis.call('HGET', KEYS[1], 'fail') or '0') > 0
if redis.call('HGET', KEYS[1], 'pending') == '1' or backingOff then
-- 待确认或同方向失败退避中:不提前执行时间(ZADD NX 只补缺失)。
redis.call('ZADD', KEYS[2], 'NX', tonumber(ARGV[6]), ARGV[7])
else
redis.call('ZADD', KEYS[2], 'LT', tonumber(ARGV[6]), ARGV[7])
@@ -43,7 +45,7 @@ return ver
// 待确认的指令只查状态,不受最小间隔约束。指令已不存在的成员直接移出队列。
//
// KEYS[1] 账户停复机队列;KEYS[2] 账户停复机执行中计数。
// ARGV:now_ms、lease_ms、容量(执行上限与剩余 QPS 的较小值)、token 前缀、单卡最小间隔 ms。
// ARGV:now_ms、lease_ms、执行上限、token 前缀、单卡最小间隔 ms、本秒剩余 QPS(-1 表示不按 QPS 限制)。
// 返回 {过期数, card_id, token, ver, action, pending, ...}。
const srClaimLua = `
local now = tonumber(ARGV[1])
@@ -51,9 +53,11 @@ local lease = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])
local prefix = ARGV[4]
local minInterval = tonumber(ARGV[5])
local budget = tonumber(ARGV[6])
local expired = redis.call('ZREMRANGEBYSCORE', KEYS[2], '-inf', now)
local result = {expired}
local capacity = limit - redis.call('ZCARD', KEYS[2])
if budget >= 0 and budget < capacity then capacity = budget end
if capacity <= 0 then return result end
local ids = redis.call('ZRANGEBYSCORE', KEYS[1], '-inf', now, 'LIMIT', 0, capacity * 2 + 10)
local taken = 0
@@ -99,7 +103,8 @@ return result
// 否则 done=1 删除指令并移出队列,done=0 写入字段并按 next_ms 重新排队(返回 1)。
//
// KEYS[1] 指令 Hash;KEYS[2] 处理中标记;KEYS[3] 执行权;KEYS[4] 执行中计数;KEYS[5] 账户停复机队列。
// ARGV:token、card_id、领取时版本、done(1/0)、next_ms、now_ms、set 字段对数 n、字段1、值1、…、删除字段…。
// ARGV:token、card_id、领取时版本、done(1/0)、next_ms、now_ms、本次结果未知(1/0)、set 字段对数 n、字段1、值1、…、删除字段…。
// 版本已变大且本次结果未知时,把待确认带到新指令上:新指令执行前先查状态,避免上游已生效而库里未知。
const srFinishLua = `
if redis.call('GET', KEYS[2]) ~= ARGV[1] then return 0 end
redis.call('DEL', KEYS[2])
@@ -108,6 +113,7 @@ redis.call('ZREM', KEYS[4], ARGV[1])
local cardId = ARGV[2]
if (redis.call('HGET', KEYS[1], 'ver') or '0') ~= ARGV[3] then
if redis.call('EXISTS', KEYS[1]) == 1 then
if ARGV[7] == '1' then redis.call('HSET', KEYS[1], 'pending', '1') end
redis.call('ZADD', KEYS[5], tonumber(ARGV[6]), cardId)
else
redis.call('ZREM', KEYS[5], cardId)
@@ -119,8 +125,8 @@ if ARGV[4] == '1' then
redis.call('ZREM', KEYS[5], cardId)
return 1
end
local n = tonumber(ARGV[7])
local idx = 8
local n = tonumber(ARGV[8])
local idx = 9
for i = 1, n do
redis.call('HSET', KEYS[1], ARGV[idx], ARGV[idx + 1])
idx = idx + 2