PG is all you need

PostgreSQL 统一数据架构:用一个数据库覆盖十个场景

核心论点

PostgreSQL 的扩展生态已覆盖全文搜索、向量检索、时序数据、缓存、消息队列、Pub/Sub、限流、Session、文档存储、地理空间等场景。在绝大多数业务规模下,PostgreSQL 可以作为唯一数据存储,替代 Elasticsearch、Pinecone、Redis、MongoDB、Kafka、InfluxDB 等专用系统。

衡量维度:

  • 功能覆盖:10 个需求场景的 Postgres 原生或扩展实现方案
  • 性能对比:各场景下 Postgres vs 专用方案的 benchmark 数据
  • 工程成本:基础设施、运维、开发效率的量化对比
  • 适用边界:Postgres 方案的限制条件和不适用场景

适用读者:需要做技术选型的架构师、技术负责人、中小团队 CTO。

不适用场景:百万级 QPS 热 key 缓存、PB 级日志检索、跨区域实时复制等需要专用系统的极端规模。


多数据库架构的工程代价

典型的"为每个需求选专用工具"技术栈:

  • Elasticsearch 负责搜索
  • Pinecone 负责向量检索
  • Redis 负责缓存
  • MongoDB 负责文档存储
  • Kafka 负责消息队列
  • InfluxDB 负责时序数据
  • PostgreSQL 负责关系型存储

这意味着 7 套备份策略、7 套监控面板、7 套安全审计、7 组凭证轮换。每增加一个系统,工程复杂度不是线性增长,而是乘法增长。

SLA 算术:三个 99.9% 可用性的系统组合后可用性为 99.7%,相当于每年 26 小时停机(单系统为 8.7 小时)。每增加一个系统,故障概率相乘。

数据一致性:跨系统数据同步需要编写同步任务、对账逻辑、补偿机制。这些胶水代码本身会成为故障源。

认知负担:团队需要同时掌握 SQL、Redis 命令、Elasticsearch Query DSL、MongoDB 聚合管道、Kafka 协议、InfluxDB 方言——这不是专业化,而是碎片化。


需求场景对比总览

需求场景专用方案Postgres 方案底层算法
全文搜索Elasticsearchpg_textsearchBM25
向量搜索Pineconepgvector + pgvectorscaleHNSW / DiskANN
时序数据InfluxDBTimescaleDB时间分区 + 压缩
缓存RedisUNLOGGED 表 + JSONB内存 KV 存储
Pub/SubRedis / KafkaLISTEN/NOTIFY发布订阅
任务队列Redis + BullFOR UPDATE SKIP LOCKED原子认领
限流Redis滑动窗口 + UPSERT计数器 / 窗口
SessionRedisUNLOGGED 表 + JSONBKV 过期存储
文档存储MongoDBJSONB文档索引
地理空间专用 GISPostGISR-Tree(2001 年至今行业标准)

这些不是简化实现。多数 Postgres 扩展使用与专用方案相同甚至更优的算法,由同一批研究者开发,经过多年生产验证。


性能基准对比

单操作延迟对比(Redis vs PostgreSQL)

测试环境:AWS RDS db.t3.medium(2 vCPU, 4GB RAM),100 万条缓存记录,1 万条 session,工具为 pgbench。

操作RedisPostgreSQL差异
Cache SET0.05ms0.08ms+60%
Cache GET0.04ms0.06ms+50%
Pub/Sub1.2ms3.1ms+158%
Queue push0.08ms0.15ms+87%
Queue pop0.12ms0.31ms+158%

单操作维度 PostgreSQL 均慢于 Redis,但所有操作仍在亚毫秒到个位毫秒级别。

组合操作延迟对比

实际业务中,单次请求往往涉及多个操作。以"写入数据 + 失效缓存 + 通知订阅者"为例:

Redis 架构(跨系统调用):

INSERT INTO posts ...             2.0ms
→ 网络跳转 Redis DEL 缓存         1.0ms
→ 网络跳转 Redis PUBLISH          1.0ms
总计                              ≈ 4.0ms(无事务保证)

PostgreSQL 架构(同一事务):

BEGIN;
INSERT INTO posts ...;                         -- 2.0ms
DELETE FROM cache WHERE key = 'posts:latest';  -- 0.1ms
NOTIFY posts_new, '...';                       -- 0.1ms
COMMIT;
-- 总计 ≈ 2.2ms(ACID 事务保证)

组合操作场景下,PostgreSQL 因省去网络跳转反而更快,且具备事务一致性保证。

扩展 vs 专用方案

扩展对比方案性能数据
pgvectorscalePinecone99% 召回率下 p95 延迟低 28 倍,成本降低 75%
TimescaleDBInfluxDB性能持平或更优,支持完整 SQL
pg_textsearchElasticsearch相同的 BM25 排序算法

现代 PostgreSQL 扩展生态

这些扩展并非实验性项目,均已在生产环境长期运行:

扩展起始年份生产验证时间代表用户
PostGIS200124 年OpenStreetMap, Uber
全文搜索200817 年Postgres 核心内置
JSONB201411 年支持 ACID 的文档存储
TimescaleDB20178 年GitHub 21K+ star
pgvector20214 年GitHub 19K+ star

超过 48,000 家公司使用 PostgreSQL,包括 Netflix、Spotify、Uber、Reddit、Instagram、Discord。

AI 浪潮催生的新一代扩展:

扩展替代方案功能
pgvectorscalePinecone, QdrantDiskANN 算法,延迟低 28 倍,成本降低 75%
pg_textsearchElasticsearch原生 BM25 排序
pgai外部 AI pipeline数据变更时自动同步 embedding

快速安装

CREATE EXTENSION pg_textsearch;    -- BM25 全文搜索
CREATE EXTENSION vector;           -- 向量搜索
CREATE EXTENSION vectorscale;      -- 高性能向量索引
CREATE EXTENSION ai;               -- AI embedding 自动同步
CREATE EXTENSION timescaledb;      -- 时序数据
CREATE EXTENSION pgmq;             -- 消息队列
CREATE EXTENSION pg_cron;          -- 定时任务
CREATE EXTENSION postgis;          -- 地理空间

实现方案与代码示例

搜索类

全文搜索(替代 Elasticsearch)

扩展:pg_textsearch(BM25 排序)

对比方案:Elasticsearch(独立 JVM 集群、映射配置、同步 pipeline)、Algolia(按搜索次数计费、外部 API 依赖)。

Postgres 方案使用与 Elasticsearch 相同的 BM25 排序算法,无需额外基础设施。

CREATE TABLE articles (
  id SERIAL PRIMARY KEY,
  title TEXT,
  content TEXT
);

-- 创建 BM25 索引
CREATE INDEX idx_articles_bm25 ON articles USING bm25(content)
WITH (text_config = 'english');

-- BM25 评分搜索
SELECT title, -(content <@> 'database optimization') as score
FROM articles
ORDER BY content <@> 'database optimization'
LIMIT 10;

向量搜索(替代 Pinecone)

扩展:pgvector + pgvectorscale

对比方案:Pinecone(最低 $70/月、独立基础设施)、Qdrant / Milvus / Weaviate(额外运维负担)。

pgvectorscale 使用微软研究院的 DiskANN 算法,在 99% 召回率下延迟和吞吐量均优于 Pinecone。

CREATE EXTENSION vector;
CREATE EXTENSION vectorscale CASCADE;

CREATE TABLE documents (
  id SERIAL PRIMARY KEY,
  content TEXT,
  embedding vector(1536)
);

-- DiskANN 高性能索引
CREATE INDEX idx_docs_embedding ON documents USING diskann(embedding);

-- 相似度查询
SELECT content, embedding <=> '[0.1, 0.2, ...]'::vector as distance
FROM documents
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;

使用 pgai 自动同步 embedding(数据变更时自动重新生成,无需外部同步任务):

SELECT ai.create_vectorizer(
  'documents'::regclass,
  loading => ai.loading_column(column_name => 'content'),
  embedding => ai.embedding_openai(model => 'text-embedding-3-small', dimensions => '1536')
);

混合搜索(BM25 + 向量)

在 Elasticsearch + Pinecone 架构中实现混合搜索需要两次 API 调用、结果合并、错误处理。在 Postgres 中是一条 SQL。

-- 加权混合
SELECT
  title,
  -(content <@> 'database optimization') as bm25_score,
  embedding <=> query_embedding as vector_distance,
  0.7 * (-(content <@> 'database optimization')) +
  0.3 * (1 - (embedding <=> query_embedding)) as hybrid_score
FROM articles
ORDER BY hybrid_score DESC
LIMIT 10;

RAG 倒数排名融合(RRF)

WITH bm25 AS (
  SELECT id, ROW_NUMBER() OVER (ORDER BY content <@> $1) as rank
  FROM documents LIMIT 20
),
vectors AS (
  SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> $2) as rank
  FROM documents LIMIT 20
)
SELECT d.*,
  1.0 / (60 + COALESCE(b.rank, 1000)) +
  1.0 / (60 + COALESCE(v.rank, 1000)) as score
FROM documents d
LEFT JOIN bm25 b ON d.id = b.id
LEFT JOIN vectors v ON d.id = v.id
WHERE b.id IS NOT NULL OR v.id IS NOT NULL
ORDER BY score DESC LIMIT 10;

模糊搜索

扩展:pg_trgm(Postgres 内置)

CREATE EXTENSION pg_trgm;
CREATE INDEX idx_name_trgm ON products USING GIN (name gin_trgm_ops);

-- 容错搜索:即使拼错也能匹配
SELECT name FROM products
WHERE name % 'posgresql'
ORDER BY similarity(name, 'posgresql') DESC;

数据存储类

时序数据(替代 InfluxDB)

扩展:TimescaleDB

对比方案:InfluxDB(独立数据库、Flux 查询语言、有限 SQL 支持)、Prometheus(适合指标监控,不适合应用数据)。

TimescaleDB 提供自动时间分区、高达 90% 压缩率、连续聚合,同时保持完整 SQL 兼容。

CREATE EXTENSION timescaledb;

CREATE TABLE metrics (
  time TIMESTAMPTZ NOT NULL,
  device_id TEXT,
  temperature DOUBLE PRECISION
);

SELECT create_hypertable('metrics', 'time');

-- 时间桶聚合
SELECT time_bucket('1 hour', time) as hour, AVG(temperature)
FROM metrics
WHERE time > NOW() - INTERVAL '24 hours'
GROUP BY hour;

-- 自动过期策略
SELECT add_retention_policy('metrics', INTERVAL '30 days');

-- 压缩策略(节省 90% 存储)
ALTER TABLE metrics SET (timescaledb.compress);
SELECT add_compression_policy('metrics', INTERVAL '7 days');

文档存储(替代 MongoDB)

特性:原生 JSONB + GIN 索引

Postgres 的 JSONB 提供与 MongoDB 类似的文档存储能力,同时具备 ACID 事务保证。

CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  data JSONB
);

INSERT INTO users (data) VALUES ('{
  "name": "Alice",
  "profile": {"bio": "Developer", "links": ["github.com/alice"]}
}');

-- 嵌套字段查询
SELECT data->>'name', data->'profile'->>'bio'
FROM users
WHERE data->'profile'->>'bio' LIKE '%Developer%';

-- JSON 字段索引
CREATE INDEX idx_users_email ON users ((data->>'email'));

地理空间(替代专用 GIS)

扩展:PostGIS(2001 年至今,行业标准)

CREATE EXTENSION postgis;

CREATE TABLE stores (
  id SERIAL PRIMARY KEY,
  name TEXT,
  location GEOGRAPHY(POINT, 4326)
);

-- 查找 5km 内的门店
SELECT name, ST_Distance(location, ST_MakePoint(-122.4, 37.78)::geography) as meters
FROM stores
WHERE ST_DWithin(location, ST_MakePoint(-122.4, 37.78)::geography, 5000);

基础设施类(替代 Redis)

缓存(UNLOGGED 表)

原理:UNLOGGED 表跳过 Write-Ahead Log(WAL),写入性能接近 Redis,但数据库崩溃后表数据会被清空。这对缓存场景是可接受的——缓存数据本身就是可重建的。

性能:Redis SET 0.05ms vs Postgres UNLOGGED INSERT 0.08ms。

Redis 实现:

await redis.set(`user:${id}`, JSON.stringify(user), 'EX', 3600);
const user = JSON.parse(await redis.get(`user:${id}`));

Postgres 实现:

CREATE UNLOGGED TABLE cache (
  key TEXT PRIMARY KEY,
  value JSONB NOT NULL,
  expires_at TIMESTAMPTZ NOT NULL
);

CREATE INDEX idx_cache_expires ON cache(expires_at);

-- 写入(upsert)
INSERT INTO cache (key, value, expires_at)
VALUES ($1, $2, NOW() + INTERVAL '1 hour')
ON CONFLICT (key) DO UPDATE
  SET value = EXCLUDED.value,
      expires_at = EXCLUDED.expires_at;

-- 读取
SELECT value FROM cache
WHERE key = $1 AND expires_at > NOW();

-- 过期清理(配合 pg_cron 定时执行)
DELETE FROM cache WHERE expires_at < NOW();

完整 Node.js 封装:

class PostgresCache {
  constructor(pool) { this.pool = pool; }

  async get(key) {
    const result = await this.pool.query(
      'SELECT value FROM cache WHERE key = $1 AND expires_at > NOW()', [key]
    );
    return result.rows[0]?.value;
  }

  async set(key, value, ttlSeconds = 3600) {
    await this.pool.query(
      `INSERT INTO cache (key, value, expires_at)
       VALUES ($1, $2, NOW() + make_interval(secs => $3))
       ON CONFLICT (key) DO UPDATE
         SET value = EXCLUDED.value, expires_at = EXCLUDED.expires_at`,
      [key, value, ttlSeconds]
    );
  }

  async delete(key) {
    await this.pool.query('DELETE FROM cache WHERE key = $1', [key]);
  }

  async cleanup() {
    await this.pool.query('DELETE FROM cache WHERE expires_at < NOW()');
  }
}

Pub/Sub(LISTEN/NOTIFY)

原理:PostgreSQL 内置发布/订阅机制,支持在事务中发送通知,保证原子性。

性能:Redis Pub/Sub 1–2ms vs Postgres NOTIFY 2–5ms。

限制条件(架构师须知):

  • payload 上限 8000 字节,大消息需存表后只传 ID
  • 消息不持久化,不支持回放——适用于实时通知,不适用于需要持久化和重消费的消息流
  • LISTEN 需要长连接,pgBouncer transaction mode 下不可用(需使用 session mode 或直连)

Redis 实现:

// Publisher
redis.publish('notifications', JSON.stringify({ userId: 123, msg: 'Hello' }));

// Subscriber
redis.subscribe('notifications');
redis.on('message', (channel, message) => {
  console.log(JSON.parse(message));
});

Postgres 实现:

-- 发布
SELECT pg_notify('notifications', '{"userId": 123, "msg": "Hello"}');
// Subscriber (Node.js)
const client = new Client({ connectionString: process.env.DATABASE_URL });
await client.connect();
await client.query('LISTEN notifications');

client.on('notification', (msg) => {
  const payload = JSON.parse(msg.payload);
  console.log(payload);
});

触发器驱动的自动通知:插入与通知在同一事务中,保证原子性。

CREATE FUNCTION notify_new_log() RETURNS TRIGGER AS $$
BEGIN
  PERFORM pg_notify('logs_new', row_to_json(NEW)::text);
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER log_inserted
AFTER INSERT ON logs
FOR EACH ROW EXECUTE FUNCTION notify_new_log();

SSE 实时流推送示例:

app.get('/logs/stream', async (req, res) => {
  const client = await pool.connect();
  res.writeHead(200, {
    'Content-Type': 'text/event-stream',
    'Cache-Control': 'no-cache',
  });
  await client.query('LISTEN logs_new');
  client.on('notification', (msg) => {
    res.write(`data: ${msg.payload}\n\n`);
  });
});

完整 Node.js 封装:

class PostgresPubSub {
  constructor(pool) { this.pool = pool; this.listeners = new Map(); }

  async publish(channel, message) {
    await this.pool.query('SELECT pg_notify($1, $2)', [channel, JSON.stringify(message)]);
  }

  async subscribe(channel, callback) {
    const client = await this.pool.connect();
    await client.query(`LISTEN ${channel}`);
    client.on('notification', (msg) => {
      if (msg.channel === channel) callback(JSON.parse(msg.payload));
    });
    this.listeners.set(channel, client);
  }

  async unsubscribe(channel) {
    const client = this.listeners.get(channel);
    if (client) {
      await client.query(`UNLISTEN ${channel}`);
      client.release();
      this.listeners.delete(channel);
    }
  }
}

任务队列(FOR UPDATE SKIP LOCKED)

原理FOR UPDATE SKIP LOCKED 实现无锁队列——多个 Worker 并发拉取任务,不会重复处理,不会互相阻塞。Worker 崩溃后,任务自动变为可用。

性能:Redis BRPOP 0.1ms vs Postgres SKIP LOCKED 0.3ms。

Redis 实现(Bull/BullMQ):

queue.add('send-email', { to, subject, body });
queue.process('send-email', async (job) => {
  await sendEmail(job.data);
});

Postgres 实现:

CREATE TABLE jobs (
  id BIGSERIAL PRIMARY KEY,
  queue TEXT NOT NULL,
  payload JSONB NOT NULL,
  attempts INT DEFAULT 0,
  max_attempts INT DEFAULT 3,
  scheduled_at TIMESTAMPTZ DEFAULT NOW(),
  created_at TIMESTAMPTZ DEFAULT NOW()
);

CREATE INDEX idx_jobs_pending ON jobs(queue, scheduled_at)
WHERE attempts < max_attempts;

-- 入队
INSERT INTO jobs (queue, payload)
VALUES ('send-email', '{"to": "user@example.com", "subject": "Hi"}');

-- Worker 认领(原子操作)
WITH next_job AS (
  SELECT id FROM jobs
  WHERE queue = $1
    AND attempts < max_attempts
    AND scheduled_at <= NOW()
  ORDER BY scheduled_at
  LIMIT 1
  FOR UPDATE SKIP LOCKED
)
UPDATE jobs
SET attempts = attempts + 1
FROM next_job
WHERE jobs.id = next_job.id
RETURNING jobs.*;

替代方案:pgmq 扩展提供了更完整的消息队列抽象,适合需要 exactly-once 语义的场景。原生 SKIP LOCKED 适合简单任务队列。

完整 Node.js 封装:

class PostgresQueue {
  constructor(pool) { this.pool = pool; }

  async enqueue(queue, payload, scheduledAt = new Date()) {
    await this.pool.query(
      'INSERT INTO jobs (queue, payload, scheduled_at) VALUES ($1, $2, $3)',
      [queue, payload, scheduledAt]
    );
  }

  async dequeue(queue) {
    const result = await this.pool.query(
      `WITH next_job AS (
        SELECT id FROM jobs
        WHERE queue = $1 AND attempts < max_attempts AND scheduled_at <= NOW()
        ORDER BY scheduled_at LIMIT 1
        FOR UPDATE SKIP LOCKED
      )
      UPDATE jobs SET attempts = attempts + 1
      FROM next_job WHERE jobs.id = next_job.id
      RETURNING jobs.*`,
      [queue]
    );
    return result.rows[0];
  }

  async complete(jobId) {
    await this.pool.query('DELETE FROM jobs WHERE id = $1', [jobId]);
  }

  async fail(jobId, error) {
    await this.pool.query(
      `UPDATE jobs SET attempts = max_attempts,
       payload = payload || jsonb_build_object('error', $2) WHERE id = $1`,
      [jobId, error.message]
    );
  }
}

限流(滑动窗口 + UPSERT)

Redis 实现:

const key = `ratelimit:${userId}`;
const count = await redis.incr(key);
if (count === 1) await redis.expire(key, 60);
if (count > 100) throw new Error('Rate limit exceeded');

Postgres 实现:

CREATE TABLE rate_limits (
  user_id INT PRIMARY KEY,
  request_count INT DEFAULT 0,
  window_start TIMESTAMPTZ DEFAULT NOW()
);

-- 原子性检查并递增
WITH current AS (
  SELECT request_count,
    CASE
      WHEN window_start < NOW() - INTERVAL '1 minute' THEN 1
      ELSE request_count + 1
    END AS new_count
  FROM rate_limits WHERE user_id = $1
  FOR UPDATE
)
UPDATE rate_limits
SET request_count = (SELECT new_count FROM current),
    window_start = CASE
      WHEN window_start < NOW() - INTERVAL '1 minute' THEN NOW()
      ELSE window_start
    END
WHERE user_id = $1
RETURNING request_count;

Postgres 方案的优势:限流逻辑可以与业务逻辑在同一事务中执行,保证一致性。Redis 方案在需要复杂限流条件(如按用户等级、接口类型组合限流)时需要多次网络调用。

Redis 方案的优势:亚毫秒延迟、百万级 QPS 吞吐。

Session 存储(UNLOGGED 表 + JSONB)

Redis 实现:

await redis.set(`session:${sessionId}`, JSON.stringify(sessionData), 'EX', 86400);

Postgres 实现:

CREATE UNLOGGED TABLE sessions (
  id TEXT PRIMARY KEY,
  data JSONB NOT NULL,
  expires_at TIMESTAMPTZ NOT NULL
);

CREATE INDEX idx_sessions_expires ON sessions(expires_at);

-- 写入 / 更新
INSERT INTO sessions (id, data, expires_at)
VALUES ($1, $2, NOW() + INTERVAL '24 hours')
ON CONFLICT (id) DO UPDATE
  SET data = EXCLUDED.data, expires_at = EXCLUDED.expires_at;

-- 读取
SELECT data FROM sessions WHERE id = $1 AND expires_at > NOW();

Postgres 的独特优势——支持 Session 内部的结构化查询(Redis 做不到):

-- 查找某用户的所有活跃 session
SELECT * FROM sessions WHERE data->>'userId' = '123' AND expires_at > NOW();

-- 查找所有管理员 session
SELECT * FROM sessions WHERE data->'user'->>'role' = 'admin' AND expires_at > NOW();

定时任务(替代 Cron)

扩展:pg_cron

CREATE EXTENSION pg_cron;

SELECT cron.schedule('cleanup', '0 * * * *',
  'DELETE FROM cache WHERE expires_at < NOW()');

SELECT cron.schedule('rollup', '0 2 * * *',
  'REFRESH MATERIALIZED VIEW CONCURRENTLY daily_stats');

高级查询

图遍历(递归 CTE)

WITH RECURSIVE org_tree AS (
  SELECT id, name, manager_id, 1 as depth
  FROM employees WHERE id = 42

  UNION ALL

  SELECT e.id, e.name, e.manager_id, t.depth + 1
  FROM employees e
  JOIN org_tree t ON e.manager_id = t.id
  WHERE t.depth < 10
)
SELECT * FROM org_tree;

PostgreSQL 18 的关键性能改进

PostgreSQL 18(2025 年 9 月发布)引入了两项对缓存架构有直接影响的特性。

Asynchronous I/O(io_uring 集成)

PostgreSQL 18 引入了新的 I/O 子系统,基于 Linux io_uring 实现异步 I/O,提升顺序扫描、bitmap heap scan、vacuum 等操作的性能。

核心变化:此前 PostgreSQL 的 I/O 是同步的——发起一次读请求,等待返回,再发起下一次。AIO 允许同时发起多个 I/O 请求,充分利用 SSD 的并行能力。

实测对比(dashboard 聚合查询,30 天数据,每日百万级调用):

SELECT date, sum(events)
FROM activity
WHERE user_id = $1
AND date > now() - interval '30 days'
GROUP BY date;
场景PostgreSQL 17PostgreSQL 18(AIO)提升
冷缓存12–18ms6–8ms~2x
热 buffer4–6ms1.8–2.5ms~2x

启用配置(5 行):

io_method = 'io_uring'
effective_io_concurrency = 200
maintenance_io_concurrency = 50
shared_buffers = 16GB
io_combine_limit = 512kB

前提条件:Linux kernel >= 5.10、SSD 存储、云厂商支持 io_uring。HDD 或不支持 AIO 的环境无法获得该提升。

B-tree Skip Scan

此前,组合索引 (a, b, c) 在查询条件不包含前缀列 a 时无法有效利用。PostgreSQL 18 支持 skip scan:自动跳过前缀列,对每个 distinct 值做范围扫描。

-- 索引
CREATE INDEX idx_orders_composite ON orders (user_id, status, created_at);

-- 查询不包含 user_id
SELECT * FROM orders
WHERE status = 'pending' AND created_at > '2025-01-01'
ORDER BY created_at DESC LIMIT 50;
版本执行方式延迟
PostgreSQL 17全表扫描或次优路径~45ms
PostgreSQL 18Skip scan 命中索引~8ms

案例:移除 Redis 缓存层

背景

一个典型的 Web 应用架构:Application → Redis(缓存层)→ PostgreSQL(持久层)。Redis 集群月费 $340,缓存逻辑约 800+ 行,分布在 12 个文件中。

问题量化

升级到 PostgreSQL 18 并启用 AIO 后,团队对生产查询做了延迟对比:

Redis 缓存的实际加权延迟计算(cache hit rate 73%):

加权延迟 = hit_rate × (Redis GET + 序列化) + miss_rate × (DB查询 + 写Redis)
         = 0.73 × (0.1 + 0.3 + 0.8) + 0.27 × (15 + 0.3 + 0.8)
         = 0.73 × 1.2 + 0.27 × 16.1
         = 0.876 + 4.347
         ≈ 5.2ms

(其中 0.1ms Redis GET,0.3ms 反序列化,0.8ms 网络 RTT,15ms DB 查询,0.3ms 序列化,0.8ms Redis SET RTT)

PostgreSQL 18 直连:1.8–2.5ms(热 buffer)。

当 cache hit rate 不够高(< 95%)时,Redis 缓存层引入的序列化开销和网络 RTT 会侵蚀其延迟优势。

三年成本复盘

成本项计算金额
Redis 集群$340/月 × 36 个月$12,240
缓存失效 Bug23 次事故 × 平均 7 小时 × $150/h$24,150
合计(可量化部分) $36,390

未量化但真实存在的成本:每个新功能需要设计缓存策略、每次 schema 变更需要同步缓存 key、双套监控和 on-call 手册的维护。

迁移后架构

Application
  ├── Redis(Session / 限流 / 排行榜 / 分布式锁)
  └── PostgreSQL 18(持久化存储 + 缓存 + 队列 + Pub/Sub)

Redis 仅保留其确有不可替代优势的场景,其余全部收敛到 PostgreSQL。


迁移策略

建议分四个阶段执行,每阶段约一周,全程监控关键指标。

Phase 1:双写

同时写入 Redis 和 Postgres,仍从 Redis 读取。监控:两端 hit rate 和延迟差异。

// 双写
await redis.set(key, value);
await pg.query('INSERT INTO cache ...', [key, value]);

// 仍从 Redis 读取
let data = await redis.get(key);

Phase 2:读切换

优先从 Postgres 读取,Redis 作为 fallback。监控:错误率、延迟分布。

let data = await pg.query('SELECT value FROM cache WHERE key = $1', [key]);
if (!data.rows[0]) {
  data = await redis.get(key);  // fallback
}

Phase 3:停止写 Redis

只写入 Postgres。监控:业务指标是否正常。

Phase 4:关闭 Redis

下线 Redis 实例。观察 1–2 周。


适用边界与限制条件

PostgreSQL 方案不适用的场景

场景原因建议
P99 < 2ms 硬性要求Postgres 延迟波动大于 Redis保留 Redis
百万 QPS 热 keyRedis 单实例 100K+ ops/sec,Postgres 10K–50K保留 Redis
Sorted Set / HyperLogLogRedis 原生数据结构,Postgres 无直接等价物保留 Redis
大规模消息流(需持久化 + 回放)LISTEN/NOTIFY 不持久化、不支持重消费使用 Kafka
PB 级日志全文检索Elasticsearch 的分布式架构更适合使用 Elasticsearch
跨区域实时复制Redis 的 geo-replication 更成熟保留 Redis

PostgreSQL 扩展方案的已知限制

特性限制应对方案
UNLOGGED 表崩溃后数据清空仅存储可重建数据(缓存、Session)
LISTEN/NOTIFYpayload 上限 8KB大消息存表,只传 ID
LISTEN/NOTIFY不持久化,不支持回放需要持久化用 pgmq 或 Kafka
LISTEN/NOTIFY需要长连接pgBouncer transaction mode 下不可用,需 session mode 或直连
AIO (io_uring)需要 Linux >= 5.10 + SSDHDD 或旧内核无法启用

决策矩阵

适合用 Postgres 替代 Redis 的条件:使用 Redis 做简单缓存/Session、cache hit rate < 95%、需要事务一致性、可接受 0.1–1ms 额外延迟、小团队运维资源有限。

应保留 Redis 的条件:需要 100K+ ops/sec、依赖 Redis 特有数据结构、有专职运维团队、亚毫秒延迟为刚性需求、需要 geo-replication。


性能调优参考

postgresql.conf 关键参数

shared_buffers = 4GB            # RAM 的 25%
effective_cache_size = 12GB     # RAM 的 75%
work_mem = 50MB                 # 复杂查询内存
maintenance_work_mem = 512MB    # VACUUM 内存
io_method = 'io_uring'          # PG18 异步 I/O
effective_io_concurrency = 200  # SSD 并发 I/O

连接池

const { Pool } = require('pg');
const pool = new Pool({
  max: 20,
  idleTimeoutMillis: 30000,
  connectionTimeoutMillis: 2000,
});

索引优化

CREATE INDEX CONCURRENTLY idx_cache_key ON cache(key) WHERE expires_at > NOW();
CREATE INDEX CONCURRENTLY idx_jobs_pending ON jobs(queue, scheduled_at)
  WHERE attempts < max_attempts;

自动维护

-- 缓存表高频清理
ALTER TABLE cache SET (autovacuum_vacuum_scale_factor = 0.1);
ALTER TABLE sessions SET (autovacuum_vacuum_scale_factor = 0.1);

-- 手动维护(如未启用 autovacuum)
VACUUM ANALYZE cache;
VACUUM ANALYZE jobs;
VACUUM ANALYZE sessions;

总结

PostgreSQL 的扩展生态覆盖了搜索、向量、时序、缓存、队列、Pub/Sub、限流、Session、文档、地理空间等场景,且多数扩展使用与专用方案相同的底层算法。PostgreSQL 18 的 AIO 和 Skip Scan 进一步缩小了与 Redis 在缓存场景下的性能差距。

对于绝大多数业务规模,单一 PostgreSQL 实例可以替代多个专用系统,换取:

  • 统一的查询语言和事务模型
  • 单一的备份、监控、安全审计流程
  • 更低的基础设施成本和运维复杂度
  • 更高的组合操作性能(省去跨系统网络开销)

在真正遇到性能瓶颈之前,不建议引入额外的数据系统。引入的判断依据应当是实测 benchmark,而非架构模式的惯性。


引用

最后修改于:2026年03月02日 15:47

添加新评论