17611538698
info@21cto.com

我是如何用 PostgreSQL 替换 Redis 的

数据库 0 37 1天前
图片

21CTO导读:各位朋友们,越来越多的应用开始倾向于技术栈的简洁,特别是PostgreSQL已经将众多NoSQL技术特性融合一身的情况下。有类似的场景可以参考本文。

各位朋友,我的 Web 应用技术栈很典型:
PostgreSQL 用于持久化数据
Redis 用于缓存、发布/订阅和后台定时Job
可以看到,一共有两个数据库实例,就会有两件事需要管理,会存在两个故障点。
后来我意识到:现在的PostgreSQL 可以做到 Redis 能做的一切。
我操作了几次,最后彻底移除了Redis。我把它记录了一下,并做了总结,希望各位能少走弯路。
事情经过是这样的。

设置:我使用 Redis 的目的

在技术栈更改之前,Redis 主要处理以下三件事:
1. 缓存(占整个使用量的 70%)
// Cache API responsesawaitredis.set(`user:${id}`,JSON.stringify(user),'EX',3600);
2. 发布/订阅(占使用量的 20%)
// Real-time notificationsredis.publish('notifications',JSON.stringify({userId,message}));
3. 后台作业队列(使用率 10%)
// Using Bull/BullMQqueue.add('send-email',{to,subject,body});
我所面临的痛点:
需要备份两个数据库
Redis 使用内存(规模化时成本很高)
Redis 持久化机制非常复杂。
Postgres 和 Redis 之间的网络跳跃成本

是什么原因来替换掉 Redis

原因一:成本比较高

下面是我的 Redis 服务器配置:
AWS ElastiCache:每月 45 美元(2GB)
升级到 5GB 流量每月需花费 110 美元。
所用的PostgreSQL:
已付费使用的 RDS:每月 50 美元(20GB 存储空间)
增加 5GB 数据流量:每月 0.50 美元
潜在能够节省成本:每月约 100 美元左右。

原因二:运营复杂性

使用 Redis 所要做的工作:
Postgres 备份(Postgres backup) ✅
Redis备份(Redis backup) ❓ (RDB? AOF? Both?)
Postgres监控(Postgres monitoring) ✅
Redis监控(Redis monitoring)❓
Postgres失败恢复(Postgres failover) ✅
Redis 守护/集群(Redis Sentinel/Cluster) ❓
不使用 Redis 后的工作:
Postgres备份(Postgres backup) ✅
Postgres监控(Postgres monitoring )✅
Postgres失败恢复(Postgres failover) ✅
我们可以看到,少了不只一个活动组件。

原因三:数据一致性

经典的性能问题:
// 更新数据库awaitdb.query('UPDATE users SET name = $1 WHERE id = $2',[name,id]);// 删除无效缓存awaitredis.del(`user:${id}`);// ⚠️ 处理Redis宕机逻辑// ⚠️处理修复失败逻辑// 缓存与数据同步逻辑
而Postgres 的功能通过事务将上面的问题全部解决了。

PostgreSQL 特性 #1:使用未记录表进行缓存

Redis:
await redis.set('session:abc123',JSON.stringify(sessionData),'EX',3600);
PostgreSQL:
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 ));
插入(INSERT)操作:
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)操作:
SELECT value FROM cacheWHERE key = $1ANDexpires_at>NOW();
清理操作(定期运行):
DELETE FROM cache WHEREexpires_at<NOW();
何为未记录数据?
下面,来解释什么是未记录的表:
跳过预写式日志(WAL)
写入速度快得多。
不要在崩溃后保留数据(非常适合缓存!)
最终表现:
Redis SET: 0.05ms
Postgres UNLOGGED INSERT: 0.08ms
距离足够近,完全可以缓存。

PostgreSQL 特性 #2:带有 LISTEN/NOTIFY 的发布/订阅


接下来的故事,就更加精彩了。
PostgreSQL 数据库具有原生的发布/订阅功能,但大多数的开发人员并不太了解。
Redis 的发布/订阅
// Publisherredis.publish('notifications',JSON.stringify({userId:123,msg:'Hello'}));// Subscriberredis.subscribe('notifications');redis.on('message',(channel,message)=>{console.log(message);});
PostgreSQL 的发布与订阅
-- PublisherNOTIFY notifications,'{"userId": 123, "msg": "Hello"}';// Subscriber (Node.js with pg)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);});
性能对比:
Redis 发布/订阅延迟:1-2毫秒
Postgres 通知延迟:2-5毫秒
Postgres速度稍微慢了些,但是它:
无需额外基础设施
可用于交易
可以与查询结合使用

现实世界的例子

我的日志管理应用程序需要实时的日志流
使用 Redis来处理:
// When new log arrivesawait db.query('INSERT INTO logs ...');await redis.publish('logs:new',JSON.stringify(log));// Frontend listensredis.subscribe('logs:new');
问题是:若有两个操作。如果发布失败怎么办?
使用 PostgreSQL:
CREATE FUNCTION notify_new_log()RETURN STRIGGERAS$BEGIN PERFORM pg_notify('logs_new',row_to_json(NEW)::text);RETURN NEW;END;$LANGUAGE plpgsql;CREATET RIGGER log_inserted AFTER INSERT ON logs FOREACH ROW EXECUTE FUNCTION notify_new_log();
现在已经是原子操作了。插入和通知要么同时发生,要么什么都不发生。
// Frontend (via SSE)app.get('/logs/stream',async( req,res)=>{ constclient=awaitpool.connect(); res.write Head(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`); });});
结果:无需 Redis 即可实现实时日志流传输。

PostgreSQL 特性 #3:带 SKIP LOCKED 的工作队列


Redis(使用 Bull/BullMQ):
queue.add('send-email',{to,subject,body});queue.process('send-email',async(job)=>{    await sendEmaill(job.data);});
PostgreSQL:
CREATE TABLE jobs(id BIG SERIAL 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_queue ON jobs(queue,scheduled_at)WHERE attempts
进入队列:
INSERT INTOjobs(queue,payload)VALUES('send-email','{"to""user@example.com""subject""Hi"}');
工作进程(出队):
WITH next_job AS(SELECT id FROM jobsWHERE queue=$1AND attemptsAND scheduled_at<=NOW()ORDER BY scheduled_atLIMIT1FOR UPDATE SKIPLOCKED)UPDATE jobsSET attempts=attempts+1FROM next_jobWHERE jobs.id=next_job.idRETURN ING*;
一个神奇之处:FOR UPDATE SKIP LOCKED
这使得PostgreSQL成为一个无锁队列
多个工作可以同时拉取任务。
任何工作都不会被处理两次。
若某个工作进程崩溃,该任务将再次可用。
表现在:
Redis BRPOP: 0.1ms
Postgres SKIP LOCKED: 0.3ms
对于大多数工作负载而言,差异可以忽略不计。

PostgreSQL 特性 #4:速率限制


Redis(经典速率限制器):
const key=`ratelimit:${userId}`;const count=await redis.incr(key);if (count===1){await redis.expire(key,60);// 60 seconds}if (count>100){throw new Error('Rate limit exceeded');}
PostgreSQL:
CREATE TABLE rate_limits(user_id INT PRIMARY KEY,request_count INT DEFAULT 0,window_start TIMESTAMPTZ DEFAULT NOW());-- Check and incrementWITH current AS(SELECTrequest_count,CASEWHEN window_start
或者,我们用窗口函数更加简单:
CREATE TABLE api_requests(user_id INT NOT NULL,created_at TIMESTAMPTZ DEFAULT NOW());-- Check rate limitSELECT COUNT(*FROM api_requests WHERE user_id=$1 AND created_at>NOW()-INTERVAL'1 minute';-- If under limit, insertINSERT INTO api_requests(user_id) VALUES($1);-- Cleanup old requests periodicallyDELETE FROM api_requests WHERE created_at<span minutes';<="" section="">
Postgres 的优势在于:
  • 需要基于复杂的逻辑进行速率限制(而不仅仅是计数)。
  • 希望将速率限制数据与业务逻辑放在同一事务中
  • Redis 的优势时刻在于:
  • 需要亚毫秒级速率限制
  • 极高的吞吐量(每秒数百万个请求)

PostgreSQL 特性 #5:使用 JSONB 的会话


Redis:
await redis.set(`session:${sessionId}`,JSON.stringify(sessionData),'EX',86400);
PostgreSQL:
CREATE TABLE sessions(id TEXT PRIMARYKEY,data JSONB NOTNULL,expires_at TIMESTAMPTZ NOT NULL);CREATE INDEX idx_sessions_expires ON sessions(expires_at);--Insert/UpdateINSERTINTOsessions(id,data,expires_at)VALUES($1,$2,NOW()+INTERVAL'24 hours')ON CONFLICT(id) DO UPDATE SETdata=EXCLUDED.data,expires_at=EXCLUDED.expires_at;-- ReadSELECT data FROM sessions WHERE id=$1 AND expires_at>NOW();
附加内容:JSONB 运算符
可以在会话内部进行查询:
-- Find all sessions for a specific userSELECT * FROM sessionsWHERE data->>'userId'='123';-- Find sessions with specific roleSELECT * FROM sessionsWHEREdata->'user'->>'role'='admin';
如果用 Redis 是做不到这一点的。

真实的基准测试

我使用生产数据集运行了行之有效的基准测试:
测试设置
硬件:
AWS RDS db.t3.medium(2 个虚拟 CPU,4GB 内存)
数据集:
100 万条缓存条目,1 万个会话
工具:
pgbench(自定义脚本)
结果如下:
目标
RedisPostgreSQL不同之处
缓存集0.05毫秒0.08毫秒速度降低 60%
缓存获取0.04毫秒0.06毫秒速度降低 50%
发布/订阅1.2毫秒3.1毫秒速度降低 158%
队列推送0.08毫秒0.15毫秒速度降低 87%
队列弹出0.12毫秒0.31毫秒速度降低 158%

可以看到,PostgreSQL 速度确实比较慢……但是:
  • 所有操作均在 1 毫秒以内。
  • 消除到 Redis 的网络跳转
  • 降低基础设施复杂性
  • 联合行动(真正的胜利)
场景:插入数据 + 使缓存失效 + 通知订阅者
使用 Redis:
await db.query('INSERT INTO posts ...');// 2msawait redis.del('posts:latest');// 1ms (network hop)await redis.publish('posts:new',data);// 1ms (network hop)// Total: ~4ms
使用 PostgreSQL:
BEGIN;INSERT INTO posts...;-- 2msDELETE FROM cache WHERE key='posts:latest';-- 0.1ms (same connection)NOTIFY posts_new,'...';-- 0.1ms (same connection)COMMIT;-- Total: ~2.2ms
PostgreSQL 在合并操作时速度更快。

何时保留 Redis


万事需要谨慎为之,也不是所有应用都替换。如果你的应用符合以下条件,请勿替换为 Redis:
1. 你需要极致的性能
Redis: 100,000+ ops/sec (single instance)
Postgres: 10,000-50,000 ops/sec
如果你每秒执行数百万次缓存读取操作,那就继续使用 Redis。
2. 你正在使用 Redis 特有的数据结构
Redis 具有如下更精细的数据结构:
已排序集合(排行榜)
HyperLogLog(唯一计数估计)
地理空间指数
流媒体(高级发布/订阅)
Postgres虽然也有类似的功能,但比较“笨拙”:
-- Leaderboard in Postgres (slower)SELECT user_id,scoreFROM leaderboardORDER BY score DESCLIMIT 10;-- vs RedisZREV RANGE leaderboard 09 WITH SCORES
3. 若你有单独的缓存层需求
如果你的系统架构要求使用单独的缓存层(例如微服务),请保留 Redis。

迁移战略

不要想一夜之间就彻底放弃 Redis。以下是我的做法:
第一阶段:并排进行练习(第 1 周)
// Write to bothawait redis.set(key,value);await pg.query('INSERT INTO cache ...');// Read from Redis (still primary)le tdata=await redis.get(key);
监控:比较命中率和延迟。
第二阶段:从Postgres读取数据(第二周)
// Try Postgres firstlet data=await pg.query('SELECT value FROM cache WHERE key = $1',[key]);// Fallback to Redisif (!data){data=await redis.get(key);}
监控它们的:错误率、性能。
第三阶段:仅写入Postgres数据库(第三周)
// Only write to Postgresawaitpg.query('INSERT INTO cache ...');
监控:一切正常吗?
第四阶段:尝试移除 Redis(第四周)
三个月后的结果
我省下了:
✅ 每月 100 美元(不再使用 ElastiCache)
✅ 备份复杂性降低 50%
✅ 少监控一项服务
✅ 更简单的部署(少一个依赖项)
我失去了什么:
❌ 缓存操作延迟约为 0.5 毫秒
❌ Redis 的奇特数据结构(其实并不需要)
我会再次这样做吗?是的,就这种使用场景而言。
我会向所有人推荐吗?不会。

决策矩阵


如果符合以下条件,可以将 Redis 替换为 Postgres:
✅ 你使用 Redis 只是为了简单的缓存/会话管理。
✅ 缓存命中率低于 95%(写入次数过多)
✅ 您需要事务一致性
✅ 您可以接受操作速度慢 0.1-1 毫秒。
✅ 你们团队规模小,运营资源有限。
如果符合以下条件,则保留 Redis:
❌ 你需要每秒 10 万次以上的操作。
❌ 您使用了 Redis 数据结构(有序集合等)。
❌ 您拥有专门的运维团队
❌ 亚毫秒级延迟至关重要
❌ 你正在进行地理复制。

相关资源:
PostgreSQL 特性:
监听/通知文档(https://www.postgresql.org/docs/current/sql-notify.html)
跳过锁定(https://www.postgresql.org/docs/current/sql-select.html#SQL-FOR-UPDATE-SHARE)
未记录表(https://www.postgresql.org/docs/current/sql-createtable.html)
相关工具:
  • pgBouncer
    - pgBoucer数据库连接池
    (https://www.pgbouncer.org/)
  • pg_stat_statements
    - 性能查询工具
    https://www.postgresql.org/docs/current/pgstatstatements.html)
  • 其他解决方案:
  • Graphile Worker
    - 基于 Postgres 的作业队列(https://github.com/graphile/worker)
  • pg-boss
    - 另一个 Postgres 队列(https://github.com/timgit/pg-boss)

说点最有用的


我用 PostgreSQL 替换了 Redis,原因是:
  • 缓存 → 未记录表
  • 发布/订阅 → 收听/通知
  • 作业队列 → 跳过锁定
  • SESSION会话 → JSONB 表
结果:
  • 每月节省 100 美元
  • 降低操作复杂性
  • 稍慢一些(0.1-1毫秒),但可以接受。
  • 保证交易一致性
何时这样做:
  • 中小型应用
  • 简单的缓存需求
  • 希望减少活动组件
  • 何时不应这样做:
  • 高性能要求(每秒 10 万次以上操作)
  • 使用 Redis 特有的功能
  • 拥有专门的运维团队

各位同学是否用 Postgres 替换了 Redis(或者也可以反过来)?

你的体验如何?欢迎在评论区分享基准测试结果。

作者:行动中的大雄

评论

我要赞赏作者

请扫描二维码,使用微信支付哦。

分享到微信