我有个大胆的想法,用 PostgreSQL 代替 Redis

我有个大胆的想法,用 PostgreSQL 代替 Redis 2024年底Ruby on Rails社区出了一条在圈外人看来波澜不惊、在圈内人看来近乎疯狂的新闻。Rails 8发布。Redis——这个过去十年间几乎所有Rails应用都在用的、以”快”著称的、久经考验的键值存储——被从默认技术栈中正式移除。取而代之的是三个名字听起来像廉价平替的库SolidQueue任务队列、SolidCache缓存、SolidCable消息推送。它们全部运行在应用已有的关系数据库上。PostgreSQL、MySQL、SQLite随便哪个。对你没看错。不是用一个更快的专用中间件替代Redis。是直接用关系数据库替代Redis。这件事的冲击力在于它正面挑战了过去十五年Web开发的一条基本共识通用工具打不过专用工具。消息队列用RabbitMQ或SidekiqRedis全文搜索用Elasticsearch缓存用Redis关系数据库老老实实做持久化——这是写在每一份架构设计文档第一章的铁律。Rails 8的设计者显然不这么认为。37signals——Rails的创造者和维护者——每天处理两千万个后台任务全部跑在MySQL上。没有Redis。注意不是”没有Redis也能勉强凑合”是”删掉Redis之后系统反而更简单了性能完全够用”。一个问题自然浮出水面专用中间件的隐性成本到底有多大Redis的账单远不止云服务商每个月扣掉的那笔钱。部署Redis意味着维护一套独立的服务器软件——版本升级、安全补丁、运行监控。要选定持久化策略——RDB快照还是AOF日志还是两个都要要配置内存上限和淘汰策略。要维护网络连通性和防火墙规则。要搭建高可用集群。出了问题时要在两个语义完全不同的数据存储之间来回跳转——一边是SQL一边是Redis的专有命令。还要准备两套备份方案而且两套都得定期测试——测试过没有还没完。如果用的是Sidekiq配合Rails的ActiveJob定时任务还需要额外挂载sidekiq-cron或whenever——又一个gem又一个依赖又一个可能出问题的地方。Redis本身是卓越的软件。问题从来不在Redis身上。问题在于”引入一个专用中间件”这个决策动作本身就携带了一组不可消除的附属成本。每增加一个技术组件就多一套配置、一套监控、一套备份策略、一组故障模式。这些成本不是一次性的——它们在系统的整个运行周期中持续产生持续消耗注意力持续放大排查问题的难度。到这里一个更深的问题出现了如果Redis这么快、这么可靠关系数据库怎么可能替代它答案藏在一个发布已近十年的SQL特性里。PostgreSQL 9.5为SELECT语句的FOR UPDATE子句增加了一个选项SKIP LOCKED。FOR UPDATE创建行级排他锁SKIP LOCKED则跳过所有当前已被锁定的行。两个词放在一起效果极其微妙又极其关键SELECT * FROM solid_queue_ready_executions WHERE queue_name default ORDER BY priority DESC, job_id ASC LIMIT 1 FOR UPDATE SKIP LOCKED;一个空闲的工作进程执行这条查询。PostgreSQL返回一条未被其他进程锁定的、优先级最高的就绪任务。多个工作进程同时发出这条查询数据库层面保证每个进程拿到不同的任务。没有锁竞争。没有等待。没有阻塞。这就一把解决了数据库任务队列的”不可能三角”——并发争用。过去基于数据库的任务队列之所以被判死刑核心死因就是多个工作进程争抢同一任务时产生的锁冲突导致的吞吐量塌方。SKIP LOCKED拆掉了这个瓶颈。SolidQueue的整个架构围绕三张表展开。所有任务元数据存入solid_queue_jobs——任务名、Ruby类、开始时间、结束时间。默认永久保留出了什么事都有据可查。尚未到执行时间的调度任务放在solid_queue_scheduled_executions里等待。可以立即执行的任务进入solid_queue_ready_executions工作进程从这里认领。三张表上高频插入和删除但PostgreSQL的MVCC机制配合自带的autovacuum完全可以消化不需要特殊调优。几类独立进程协同运作工作进程按可配置间隔轮询ready表——高优先级队列最快可达0.1秒一次调度进程每秒轮询scheduled表把到期任务移入ready队列定时任务管理器按cron配置周期入队监督进程监控所有进程的心跳自动重启崩溃的子进程。每一种进程操作不同的表使用不同的轮询间隔互不干扰。数据库通过原生ACID事务完成所有协调。不需要外部锁服务。不需要分布式协调器。不需要ZooKeeper。这就是SolidQueue最巧妙的设计决策——把并发协调从应用层交还给数据库层。而数据库在过去二十年里已经为这项任务做了足够多的准备。还有更狠的。有些功能在RedisSidekiq生态里是要付费的。Sidekiq Enterprise——起步价每年1699美元——提供的并发限制功能SolidQueue直接内置了class ProcessUserOnboardingJob ApplicationJob limits_concurrency to: 1, key: -(user) { user.id }, duration: 15.minutes def perform(user) # ... end end这行配置保证同一个用户同时最多只有一个此类任务在运行。底层用两张额外的表实现——solid_queue_semaphores跟踪并发限制solid_queue_blocked_executions存放等待信号量的任务。任务完成释放信号量调度进程自动激活下一个等待者。全数据库原生零外部依赖。定时任务同样不用额外集成sidekiq-cron。SolidQueue原生支持cron风格配置一个YAML文件搞定production: cleanup_old_sessions: class: CleanupSessionsJob schedule: every day at 2am queue: maintenance send_daily_digest: class: DailyDigestJob schedule: every day at 9am queue: mailers实现方式相当巧妙——每次执行定时任务时调度进程同步将下一次触发时间写入计划。即便调度进程崩溃重启时间表也是确定性的——”每天9点”永远解析为当天9点不受重启时刻影响。这个思路借鉴自GoodJob项目天然抗崩溃。监控同样原生。Mission Control Jobs一个免费开源的任务管理面板挂载只需一行路由配置。队列维度的实时状态、失败任务的完整堆栈、批量重试和丢弃、定时任务时间线、队列吞吐量图表——开箱即得。但真正重要的是另一件事。所有数据都在数据库里。排查失败任务时SQL直接上SELECT j.queue_name, COUNT(*) as failed_count FROM solid_queue_failed_executions fe JOIN solid_queue_jobs j ON j.id fe.job_id WHERE fe.created_at NOW() - INTERVAL 1 hour GROUP BY j.queue_name;没有外部工具的上下文切换。没有额外的查询语言。就是SQL——团队已经会用的语言已经熟悉的工具已经在用的数据库客户端。好现在来面对那个绕不开的问题能撑住多大量先看一个参照点。2013年Shopify工程师John Duff在Big Ruby大会上分享了他们的数字每秒833个请求平均响应时间72毫秒53台服务器1172个工作进程。十二年前的那个时间点在那个规模下Redis级别的基础设施确确实实是必要的。但37signals提供了一个更近的参照每天两千万个任务换算下来大约每秒230个全部跑在MySQL上没有Redis。再做一道简单的算术。Nate Berkopec在2015年给过一个被称为”1000 RPM公式”的简洁模型所需应用实例数 请求速率req/sec× 平均响应时间sec。假设一个典型应用每分钟100个请求平均响应200毫秒——约每秒1.67个请求乘以0.2秒等于0.083个应用实例。只用了8%的一个实例容量。离”必须上Redis”的临界点还有很远的距离。一个粗略的经验边界每秒不到100个任务或者能接受100毫秒以上的任务延迟——关系数据库绰绰有余。每秒100到1000个任务——两边都测让数据做决定。每秒持续超过1000个任务或者延迟容忍度低于1毫秒——Redis依然是正确答案。到这里拼图开始成形了。为什么一个被奉为行业标准的技术选型——”专用中间件优于通用数据库”——在被推到足够远之后反而出现了反转答案或许不在于技术本身而在于”最佳实践”这个词的传播机制。技术圈的”最佳实践”通常是头部公司的经验结晶。2013年的Shopify每秒833个请求选择RedisSidekiq在当时的约束下完全正确。问题在于这个选择被社区内化成了”标准做法”之后很少有人停下来问一句自己的应用真的到了Shopify 2013年的水位吗还有一层更隐蔽的心理机制在起作用。使用一个”大家都用”的技术栈自带一种隐性保险——出了问题”选型没错行业公认方案”。而选择”非标”方案则需要持续的辩护成本——每次技术评审都要解释为什么没选Redis。两种力量同向叠加把专用中间件的实际采用率推到了远超必要范围的区间。同时还有一个被长期低估的变量关系数据库自身的进化速度。PostgreSQL 9.5的SKIP LOCKED发布于2016年。同年的PostgreSQL已经有了JSONB、全文搜索、窗口函数。今天的PostgreSQL能做的事十年前被认为只有专用工具才能做——但大多数人的认知地图还停留在十年前。这套机制需要一个名字。可以称之为——”中间件税”。不是在云厂商账单上出现的那种税。是每一套额外配置、每一轮备份演习、每一次跨存储调试、每一个”为什么数据对不上”的深夜电话在系统整个生命周期中持续征收的隐性税收。税率看似很低但终身征收。而且绝大多数缴纳者根本没有意识到自己在缴纳。更微妙的是这笔税并不由技术决策者独立判断是否需要缴纳——它被”行业惯例”预扣了。不交这笔税需要主动申请豁免而申请豁免本身就产生了解释成本。做大家都在做的事永远比解释为什么不做更容易。这不仅仅关乎Redis或Rails。任何技术选型决策都面临同一组力量头部公司的经验下沉为行业标准行业标准遮蔽了个体判断的空间而打破遮蔽本身需要付出额外的认知和说服成本。这三步构成的正反馈循环才是”中间件税”真正的征收机制。回到开头的那条新闻。Rails 8抛弃Redis这件事的冲击力在于它暴露了一个被长期忽视的事实行业最佳实践的适用范围远比它被实际运用的范围窄得多。而那些落在适用范围之外的项目一直以来都在为自己不需要的能力付费。不只是钱。是系统的每一个配置项、每一次跨存储调试、每一套没有人测试过的备份策略。