← 中文资源

简体中文版本

PostgreSQL 滑动窗口限流

用数据库实现滑动窗口限流:单条 CTE 链原子完成清扫过期、统计用量、条件插入三步,剩余额度与重置时间一并产出。

阅读英文原文

示例说明

无 Redis 依赖的限流同样可以严谨:滑动窗口按时间点计数,六十秒内的权重总和即当前用量。判定与写入必须原子——CTE 链把清扫旧记录、统计现用量、条件插入本次尝试合并为一条语句,数据库的快照隔离保证并发请求各自看到一致的窗口状态。

技术要点

  • CTE 链原子化清扫加判定加记录
  • 滑窗按时间点计数无边界突变
  • 剩余额度与重置时间随查询产出

代码解读

pruned 删除一分钟前的过期行顺便减表体积;recent 汇总当前主体的用量;attempt 的 INSERT 带 WHERE NOT EXISTS——用量已达上限时插入零行,allowed 零即拒绝。第二个查询输出剩余额度与窗口重置时刻,正是限流响应头的两个标准字段。索引按主体加时间覆盖滑窗扫描。

使用建议

高 QPS 场景数据库限流是兜底方案,主路径用 Redis。表要定期清理防膨胀(清扫已内建)。权重字段支持重接口扣双份的精细策略。

代码示例

-- PostgreSQL 滑动窗口限流:一条语句原子清理旧记录并判额度
CREATE TABLE rate_limit_buckets (
  subject_key text NOT NULL,
  occurred_at timestamptz NOT NULL DEFAULT now(),
  weight int NOT NULL DEFAULT 1
);

CREATE INDEX idx_rate_subject_time
  ON rate_limit_buckets (subject_key, occurred_at);

-- 原子判定 + 清扫 + 记录:单语句完成限流
WITH pruned AS (
  DELETE FROM rate_limit_buckets
  WHERE occurred_at < now() - interval '60 seconds'
  RETURNING 1 AS removed
),
recent AS (
  SELECT subject_key, SUM(weight) AS used
  FROM rate_limit_buckets
  WHERE subject_key = 'api:user-42'
    AND occurred_at >= now() - interval '60 seconds'
  GROUP BY subject_key
),
attempt AS (
  INSERT INTO rate_limit_buckets (subject_key, weight)
  SELECT 'api:user-42', 1
  WHERE NOT EXISTS (
    SELECT 1 FROM recent WHERE used >= 100
  )
  RETURNING occurred_at
)
SELECT
  (SELECT count(*) FROM attempt) AS allowed,
  (SELECT used FROM recent) AS current_usage,
  (SELECT count(*) FROM pruned) AS pruned_rows;

-- 剩余额度与重置时间:响应头的原料
SELECT
  100 - SUM(weight) AS remaining,
  min(occurred_at) + interval '60 seconds' AS window_resets_at
FROM rate_limit_buckets
WHERE subject_key = 'api:user-42'
  AND occurred_at >= now() - interval '60 seconds';