lua-scripts.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. /**
  2. * Redis Lua 脚本集合
  3. *
  4. * 用于保证 Redis 操作的原子性
  5. */
  6. /**
  7. * 原子性检查并发限制 + 追踪 Session(TC-041 修复版)
  8. *
  9. * 功能:
  10. * 1. 清理过期 session(5 分钟前)
  11. * 2. 检查 session 是否已追踪(避免重复计数)
  12. * 3. 检查当前并发数是否超限
  13. * 4. 如果未超限,追踪新 session(原子操作)
  14. *
  15. * KEYS[1]: provider:${providerId}:active_sessions
  16. * ARGV[1]: sessionId
  17. * ARGV[2]: limit(并发限制)
  18. * ARGV[3]: now(当前时间戳,毫秒)
  19. *
  20. * 返回值:
  21. * - {1, count, 1} - 允许(新追踪),返回新的并发数和 tracked=1
  22. * - {1, count, 0} - 允许(已追踪),返回当前并发数和 tracked=0
  23. * - {0, count, 0} - 拒绝(超限),返回当前并发数和 tracked=0
  24. */
  25. export const CHECK_AND_TRACK_SESSION = `
  26. local provider_key = KEYS[1]
  27. local session_id = ARGV[1]
  28. local limit = tonumber(ARGV[2])
  29. local now = tonumber(ARGV[3])
  30. local ttl = 300000 -- 5 分钟(毫秒)
  31. -- 1. 清理过期 session(5 分钟前)
  32. local five_minutes_ago = now - ttl
  33. redis.call('ZREMRANGEBYSCORE', provider_key, '-inf', five_minutes_ago)
  34. -- 2. 检查 session 是否已追踪
  35. local is_tracked = redis.call('ZSCORE', provider_key, session_id)
  36. -- 3. 获取当前并发数
  37. local current_count = redis.call('ZCARD', provider_key)
  38. -- 4. 检查限制(排除已追踪的 session)
  39. if limit > 0 and not is_tracked and current_count >= limit then
  40. return {0, current_count, 0} -- {allowed=false, current_count, tracked=0}
  41. end
  42. -- 5. 追踪 session(ZADD 对已存在的成员只更新时间戳)
  43. redis.call('ZADD', provider_key, now, session_id)
  44. redis.call('EXPIRE', provider_key, 3600) -- 1 小时兜底 TTL
  45. -- 6. 返回成功
  46. if is_tracked then
  47. -- 已追踪,计数不变
  48. return {1, current_count, 0} -- {allowed=true, count, tracked=0}
  49. else
  50. -- 新追踪,计数 +1
  51. return {1, current_count + 1, 1} -- {allowed=true, new_count, tracked=1}
  52. end
  53. `;
  54. /**
  55. * 批量检查多个供应商的并发限制
  56. *
  57. * KEYS: provider:${providerId}:active_sessions (多个)
  58. * ARGV[1]: sessionId
  59. * ARGV[2...]: limits(每个供应商的并发限制)
  60. * ARGV[N]: now(当前时间戳,毫秒)
  61. *
  62. * 返回值:数组,每个元素对应一个供应商
  63. * - {1, count} - 允许
  64. * - {0, count} - 拒绝(超限)
  65. */
  66. export const BATCH_CHECK_SESSION_LIMITS = `
  67. local session_id = ARGV[1]
  68. local now = tonumber(ARGV[#ARGV])
  69. local ttl = 300000 -- 5 分钟(毫秒)
  70. local five_minutes_ago = now - ttl
  71. local results = {}
  72. -- 遍历所有供应商 key
  73. for i = 1, #KEYS do
  74. local provider_key = KEYS[i]
  75. local limit = tonumber(ARGV[i + 1]) -- ARGV[2]...ARGV[N-1]
  76. -- 清理过期 session
  77. redis.call('ZREMRANGEBYSCORE', provider_key, '-inf', five_minutes_ago)
  78. -- 获取当前并发数
  79. local current_count = redis.call('ZCARD', provider_key)
  80. -- 检查限制
  81. if limit > 0 and current_count >= limit then
  82. table.insert(results, {0, current_count}) -- 拒绝
  83. else
  84. table.insert(results, {1, current_count}) -- 允许
  85. end
  86. end
  87. return results
  88. `;
  89. /**
  90. * 追踪 5小时滚动窗口消费(使用 ZSET)
  91. *
  92. * 功能:
  93. * 1. 清理 5 小时前的消费记录
  94. * 2. 添加当前消费记录(带时间戳)
  95. * 3. 计算当前窗口内的总消费
  96. * 4. 设置兜底 TTL(6 小时)
  97. *
  98. * KEYS[1]: key:${id}:cost_5h_rolling 或 provider:${id}:cost_5h_rolling
  99. * ARGV[1]: cost(本次消费金额)
  100. * ARGV[2]: now(当前时间戳,毫秒)
  101. * ARGV[3]: window(窗口时长,毫秒,默认 18000000 = 5小时)
  102. *
  103. * 返回值:string - 当前窗口内的总消费
  104. */
  105. export const TRACK_COST_5H_ROLLING_WINDOW = `
  106. local key = KEYS[1]
  107. local cost = tonumber(ARGV[1])
  108. local now_ms = tonumber(ARGV[2])
  109. local window_ms = tonumber(ARGV[3]) -- 5 hours = 18000000 ms
  110. -- 1. 清理过期记录(5 小时前的数据)
  111. redis.call('ZREMRANGEBYSCORE', key, '-inf', now_ms - window_ms)
  112. -- 2. 添加当前消费记录(member = timestamp:cost,便于调试和追踪)
  113. local member = now_ms .. ':' .. cost
  114. redis.call('ZADD', key, now_ms, member)
  115. -- 3. 计算窗口内总消费
  116. local records = redis.call('ZRANGE', key, 0, -1)
  117. local total = 0
  118. for _, record in ipairs(records) do
  119. -- 解析 member 格式:"timestamp:cost"
  120. local cost_str = string.match(record, ':(.+)')
  121. if cost_str then
  122. total = total + tonumber(cost_str)
  123. end
  124. end
  125. -- 4. 设置兜底 TTL(6 小时,防止数据永久堆积)
  126. redis.call('EXPIRE', key, 21600)
  127. return tostring(total)
  128. `;
  129. /**
  130. * 查询 5小时滚动窗口当前消费
  131. *
  132. * 功能:
  133. * 1. 清理 5 小时前的消费记录
  134. * 2. 计算当前窗口内的总消费
  135. *
  136. * KEYS[1]: key:${id}:cost_5h_rolling 或 provider:${id}:cost_5h_rolling
  137. * ARGV[1]: now(当前时间戳,毫秒)
  138. * ARGV[2]: window(窗口时长,毫秒,默认 18000000 = 5小时)
  139. *
  140. * 返回值:string - 当前窗口内的总消费
  141. */
  142. export const GET_COST_5H_ROLLING_WINDOW = `
  143. local key = KEYS[1]
  144. local now_ms = tonumber(ARGV[1])
  145. local window_ms = tonumber(ARGV[2]) -- 5 hours = 18000000 ms
  146. -- 1. 清理过期记录
  147. redis.call('ZREMRANGEBYSCORE', key, '-inf', now_ms - window_ms)
  148. -- 2. 计算窗口内总消费
  149. local records = redis.call('ZRANGE', key, 0, -1)
  150. local total = 0
  151. for _, record in ipairs(records) do
  152. local cost_str = string.match(record, ':(.+)')
  153. if cost_str then
  154. total = total + tonumber(cost_str)
  155. end
  156. end
  157. return tostring(total)
  158. `;
  159. /**
  160. * 追踪 24小时滚动窗口消费(使用 ZSET)
  161. *
  162. * 功能:
  163. * 1. 清理 24 小时前的消费记录
  164. * 2. 添加当前消费记录(带时间戳)
  165. * 3. 计算当前窗口内的总消费
  166. * 4. 设置兜底 TTL(25 小时)
  167. *
  168. * KEYS[1]: key:${id}:cost_daily_rolling 或 provider:${id}:cost_daily_rolling
  169. * ARGV[1]: cost(本次消费金额)
  170. * ARGV[2]: now(当前时间戳,毫秒)
  171. * ARGV[3]: window(窗口时长,毫秒,默认 86400000 = 24小时)
  172. *
  173. * 返回值:string - 当前窗口内的总消费
  174. */
  175. export const TRACK_COST_DAILY_ROLLING_WINDOW = `
  176. local key = KEYS[1]
  177. local cost = tonumber(ARGV[1])
  178. local now_ms = tonumber(ARGV[2])
  179. local window_ms = tonumber(ARGV[3]) -- 24 hours = 86400000 ms
  180. -- 1. 清理过期记录(24 小时前的数据)
  181. redis.call('ZREMRANGEBYSCORE', key, '-inf', now_ms - window_ms)
  182. -- 2. 添加当前消费记录(member = timestamp:cost,便于调试和追踪)
  183. local member = now_ms .. ':' .. cost
  184. redis.call('ZADD', key, now_ms, member)
  185. -- 3. 计算窗口内总消费
  186. local records = redis.call('ZRANGE', key, 0, -1)
  187. local total = 0
  188. for _, record in ipairs(records) do
  189. -- 解析 member 格式:"timestamp:cost"
  190. local cost_str = string.match(record, ':(.+)')
  191. if cost_str then
  192. total = total + tonumber(cost_str)
  193. end
  194. end
  195. -- 4. 设置兜底 TTL(25 小时,防止数据永久堆积)
  196. redis.call('EXPIRE', key, 90000)
  197. return tostring(total)
  198. `;
  199. /**
  200. * 查询 24小时滚动窗口当前消费
  201. *
  202. * 功能:
  203. * 1. 清理 24 小时前的消费记录
  204. * 2. 计算当前窗口内的总消费
  205. *
  206. * KEYS[1]: key:${id}:cost_daily_rolling 或 provider:${id}:cost_daily_rolling
  207. * ARGV[1]: now(当前时间戳,毫秒)
  208. * ARGV[2]: window(窗口时长,毫秒,默认 86400000 = 24小时)
  209. *
  210. * 返回值:string - 当前窗口内的总消费
  211. */
  212. export const GET_COST_DAILY_ROLLING_WINDOW = `
  213. local key = KEYS[1]
  214. local now_ms = tonumber(ARGV[1])
  215. local window_ms = tonumber(ARGV[2]) -- 24 hours = 86400000 ms
  216. -- 1. 清理过期记录
  217. redis.call('ZREMRANGEBYSCORE', key, '-inf', now_ms - window_ms)
  218. -- 2. 计算窗口内总消费
  219. local records = redis.call('ZRANGE', key, 0, -1)
  220. local total = 0
  221. for _, record in ipairs(records) do
  222. local cost_str = string.match(record, ':(.+)')
  223. if cost_str then
  224. total = total + tonumber(cost_str)
  225. end
  226. end
  227. return tostring(total)
  228. `;