手记

Postgres 能替代 Redis 作为缓存吗?

Twitter | LinkedIn | YouTube | Instagram
本文也可以在 YouTube 上观看!

今天,我决定在 Twitter 上询问大家最先想到的消息队列系统是什么。令我惊讶的是,其中一个回答是:Postgres。

我打开了链接,不仅惊讶于可以将 Postgres 用作消息代理的可能性:

“使用 Postgres 作为消息队列,用 SKIP LOCKED 替代 Kafka(如果你只需要一个消息队列)。” — Stephan Schmidt

但同样……通过使用Postgres作为缓存来替代Redis的可能性:

“使用Postgres而不是Redis进行缓存,并使用UNLOGGED表和TEXT作为JSON数据类型。使用存储过程,或者像我这样做,使用ChatGPT为你编写它们,为数据添加并强制执行过期日期,就像在Redis中一样”。 — Stephan Schmidt

而且让我感到惊讶的原因是在我学习 Redis 的过程中,我经常听到很多人(来自 Redis 的人)提倡的一个观点是“Redis 是一个数据库,因此它应该作为你的主数据库”。

这其实是有道理的。Redis 是一个真正的数据库,只是恰好也非常适合用作缓存。它之所以非常适合用作缓存,是因为它非常快。极其快速。快到可以在一秒钟内执行数百万次操作。

而且……读到 Postgres,我最喜欢的的关系型数据库,现在可以替代 Redis,我最喜欢的非关系型数据库,这真的让我感到世界都颠倒了。毕竟,我应该用 Postgres 替换 Redis 还是用 Redis 替换 Postgres?

但在考虑这个问题之前,我想先了解:Postgres 作为缓存是否真的是个好主意? 它是否真的可以替代 Redis?这就是我今天想要探究的问题。

我分享过的那篇文章,后来我发现它在 Twitter 上很火,这篇文章是由 Stephan Schmidt 写的。

Stephan 不仅主张用 Postgres 替代 Redis,他还主张用 Postgres 替代一切。在他看来,这样可以减少复杂性并加快速度。

“只需使用Postgres处理一切(如何减少复杂性并更快地推进)” — Stephan Schmidt

然而,他并不是唯一一个主张用其他东西替代 Redis 的人,实际上有几个人也做了同样的事情:

首先,我为什么要用 Postgres 替换 Redis 呢?

Stephan 已经给出了两个原因:减少复杂性及更快的更改。但还有更多吗?

使用 Postgres 作为缓存并不是最常见的情况,但在某些场景下这样做是有意义的。让我们来看看这些场景:

统一技术栈

Postgres 是最流行的数据库之一。它是免费的,是开源的,而且很有可能你今天已经在你的应用程序中使用它了。

将其用作缓存可以简化你的技术栈,通过减少管理多个数据库系统的需要来实现。

熟悉的界面

Postgres 支持复杂的查询和索引。这使得在缓存层直接处理高级数据检索和转换任务变得更加容易。如果您的团队已经熟练使用 SQL,那么使用 SQL 进行缓存逻辑处理将非常有利。这种情况很可能是存在的。

成本

在某些情况下,使用现有的Postgres资源进行缓存可能比部署单独的缓存解决方案(如Redis)更具成本效益。将Postgres同时用于主存储和缓存可以更好地利用资源,特别是在基础设施预算有限的环境中。

我应该从缓存服务中期待什么?

传统的缓存服务,如 Redis,具有一系列增强应用程序性能和可扩展性的功能。为了理解 Postgres 是否真的可以替代 Redis,我们需要了解这些功能是什么。因此,这里有一些我们应该期望缓存服务具备的关键方面:

性能

缓存服务的主要目标是通过加快数据访问速度来提升应用程序的性能。

高性能缓存解决方案可以处理高吞吐量的工作负载,并提供亚毫秒级的响应时间,显著加快了数据检索的过程。

过期

通过为缓存数据设置过期时间,我们可以确保在指定时间段后自动从缓存中移除过时的数据。确保我们的应用程序不会使用过时的数据,这是缓存服务的另一个重要方面。

驱逐政策

缓存服务通常将数据存储在内存中,而内存的历史上容量有限。因此,设置淘汰策略可以让我们自动移除不常使用的数据,为新条目腾出空间。

键值存储

在大多数缓存服务的核心,数据是以键值对的形式存储的。这种简单而强大的模型使得快速检索数据成为可能,从而可以高效地存储和访问频繁使用的数据。

简而言之,你希望缓存服务能够让你更快地访问数据,并且返回尽可能最新数据。

如何将Postgres变成缓存?

根据 Martin Heinz 在他的博客 [你不需要专用的缓存服务——Postgres作为缓存] 中所写,你也可以从 Postgres 获取我之前提到的几乎所有内容。

斯蒂芬和马丁都说,我们可以使用UNLOGGED 表将 Postgres 转变为缓存服务。从现在开始我将展示的几乎所有示例都来自马丁的出版物。

未记录表和预写日志

Postgres中的未记录表是一种防止特定表生成WAL(预写日志)的方法。

WAL 会确保所有对数据库所做的更改在实际写入数据库文件之前都被记录下来。这有助于维护数据完整性,特别是在发生崩溃或断电的情况下。

Fun fact: Redis 提供了一个类似的机制叫做追加只写文件(AOF),它不仅提供了一种持久化 Redis 数据的机制,还通过记录所有在 Redis 中执行的操作来实现类似的功能。当我们使用 Redis 作为主数据库时,会开启 AOF,而当我们使用 Postgres 作为缓存时,会关闭(特定表的)WAL。

关闭WAL意味着提升性能

对于每次数据修改,Postgres 必须将变更写入 WAL 和数据文件中,这使得所需的写入操作数量翻倍。

除此之外,为了确保每个提交的事务都被物理地写入磁盘,WAL 被设计为强制执行磁盘刷新(fsync)。频繁的磁盘刷新操作会影响性能,因为它们会引入等待磁盘确认数据安全写入的延迟。

也意味着放弃持久化

关于未记录表的主要一点是,它们不是持久化的。

这是因为 Postgres 使用 WAL 来重放并应用自上次检查点以来所做的任何更改。如果没有这个日志记录,数据库无法通过重放 WAL 记录恢复到一致的状态。不过,这是否可以被认为是从缓存中预期的行为呢?

    CREATE UNLOGGED TABLE cache (  
        id serial PRIMARY KEY,  
        key text UNIQUE NOT NULL,  
        value jsonb,  
        inserted_at timestamp);  

    CREATE INDEX idx_cache_key ON cache (key);
带存储过程的过期处理

马丁和斯特凡都说可以通过使用存储过程来实现过期。而这就开始了复杂性。

存储过程可能会很复杂,事实上,Stephan 更进一步建议我们可以使用 ChatGPT 来为我们编写它们,暗示它们确实可能很复杂。

    CREATE OR REPLACE PROCEDURE expire_rows (retention_period INTERVAL) AS  
    $  
    BEGIN  
        DELETE FROM cache  
        WHERE inserted_at < NOW() - retention_period;  

        COMMIT;  
    END;  
    $ LANGUAGE plpgsql;  

    CALL expire_rows('60 minutes'); -- 这将删除一小时之前插入的行

事实是,大多数现代应用程序不再依赖存储过程,而且现在很多软件开发者都反对使用它们。

通常,这样做的原因是为了避免业务逻辑渗入我们的数据库。除此之外,随着存储过程数量的增长,管理和理解它们可能会变得复杂。

此外,我们还需要按照计划调用这些存储过程。为此,我们需要使用一个名为pg_cron的扩展。

安装扩展后,我们仍然需要创建我们的调度器:

    -- 创建一个每小时运行一次的计划任务  
    SELECT cron.schedule('0 * * * *', $CALL expire_rows('1小时');$);  

    -- 列出所有已安排的任务  
    SELECT * FROM cron.job;

复杂度在增加,不是吗?

使用存储过程进行驱逐

Stephan 在他的文章中甚至没有提到驱逐,而 Martin 则表示这可能是可选的,因为过期可以控制大小。

如果你仍然希望启用驱逐策略,他建议在表中添加一个名为 last_read_timestamp 的列,并定期运行另一个存储过程以实现“最近最少使用”(LRU)的驱逐策略。

    CREATE OR REPLACE PROCEDURE lru_eviction(eviction_count INTEGER) AS  
    $  
    BEGIN  
        DELETE FROM cache  
        WHERE ctid IN (  
            SELECT ctid  
            FROM cache  
            ORDER BY last_read_timestamp ASC  
            LIMIT eviction_count  
        );  

        COMMIT;  
    END;  
    $ LANGUAGE plpgsql;  

    -- 调用该过程以移除指定数量的行  
    CALL lru_eviction(10); -- 这将移除最近最少访问的10行

Redis 内置了八种驱逐策略。如果你需要为你的“Postgres 缓存”设置另一种驱逐策略,只需询问 ChatGPT。

性能方面呢?

性能才是最重要的,对吧?毕竟,我们通常想要一个缓存服务的原因就是为了更快地访问数据。

Greg Sabino Mullane 在他的文章 [PostgreSQL 未记录表——看,没有 WAL!] 中,对比了 Postgres 中 UNLOGGED 和 LOGGED 表的性能。他的数据显示,向 UNLOGGED 表写入数据的速度是向 LOGGED 表写入数据速度的两倍。具体来说:

未记录表:
延迟: 2.059 毫秒
TPS: 485,706168

已记录表:
延迟: 5.949 毫秒
TPS: 168,087557

读取性能呢?

而且这里有个关键点。Postgres 的性能优化策略依赖于 shared buffers

共享缓冲区将经常访问的数据和索引直接存储在内存中,使其能够快速访问并减少从磁盘读取的需要。这可以提高查询性能和已记录表及未记录表的数据访问速度。

确实,未记录表可能会驻留在这些缓冲区中,但如果它们变得太大或内存受限,它们会被写入磁盘。因此,未记录表主要提升写入速度,而不是读取速度。

为了证明这一点,我使用 pgbench 进行了一个快速实验。你可以在这里看到我是如何做的

并且结果显示,普通表和未记录表的性能实际上非常相似。读取这两种类型的表平均耗时约为 0.650 毫秒。具体来说:

未记录表:
延迟: 0.679 毫秒
TPS: 14,724,204

已记录表:
延迟: 0.627 毫秒
TPS: 15,946,025

这个结果强化了这样一个理解,即未记录表主要提升了写入性能。对于读取操作,未记录表的性能优势并不明显,因为无论是已记录表还是未记录表,都能从Postgres的缓存和优化策略中获益相似。

与 Redis 相比,性能如何?

除了对 Postgres 进行基准测试外,我还对 Redis 进行了实验。你可以在这里查看实验的详细信息。 Redis 的结果显示,在读写操作方面具有显著的性能优势:

阅读:
延迟 (p50): 0.095 毫秒
每秒请求数 (RPS): 892.857,12

写入:
延迟 (p50): 0.103 毫秒
每秒请求数 (RPS): 892.857,12

性能对比显示,Redis 在写入和读取操作中都明显优于 Postgres:

Redis 实现了 0.095 毫秒的延迟,比 Postgres 的未记录表观察到的 0.679 毫秒延迟快约 85%。

它还能够处理更高的请求率,每秒处理892.857,12个请求,而Postgres每秒只能处理15.946,025个事务。

在谈到写入操作时,从明显更高的吞吐量和更低的延迟可以看出,Redis 提供了更优越的性能。

如果我在内存中运行 Postgres 呢?

在本文的审阅过程中,Xebia 的一位同事 Maksym Fedorov 说道:

“如果现在创建的无日志表位于对应于内存映射文件的表空间中,我猜我们会看到完全不同的数字。”

为了测试这一点,我在将 Postgres 数据持久化在内存中运行了基准测试。出人意料的是,结果并没有任何改善。 基准测试显示:

阅读:
延迟: 0.652 毫秒
每秒请求数 (TPS): 15.329,776954

经过进一步的研究,我了解到即使数据存储在RAM中,从Postgres的共享缓冲区中访问数据仍然会产生额外的成本。这些成本来自于管理锁和其他内部过程,这些过程对于数据完整性和并发访问是必要的。

Postgres 总是先检查数据是否在共享缓冲区中。如果不在,它会将数据从 tmpfs 文件系统复制到共享缓冲区中,然后再提供服务,即使数据库是持久化在内存中的也是如此。

是否应该用 Postgres 替换 Redis?

基于这项研究,如果你需要一个缓存服务来提升写入性能,可以使用Postgres的未记录表进行优化。然而,虽然未记录表的写入性能优于记录表,但仍然不及Redis。

使用缓存服务的主要原因是提高数据检索时间。未记录表不会提升读取性能,而 Redis 在读取操作方面表现出极其快速的性能。

此外,Redis 还有助于防止大量低成本查询冲击你的数据库,这是 UNLOGGED 表无法提供的优势。Redis 还提供了内置功能,如过期时间、驱逐策略等,这些功能在 Postgres 中实现起来较为复杂。

虽然对于某些人来说管理Postgres可能看起来更简单,但将Postgres变成缓存并不能提供专用缓存服务的优势。同时,Redis易于学习、部署和使用。

为了更快的性能和简洁性,选择一个真正的缓存服务如 Redis 是一个明确的选择。

希望你喜欢读这篇故事!我做这个研究的时候确实玩得很开心!

特别感谢 Maksym FedorovJoão Paulo Gomes,和 Hernani Fernandes 审阅本文。

保持好奇!
贡献

写作需要时间和精力。 我热爱写作和分享知识,但我也有账单需要支付。如果你喜欢我的工作,请 考虑通过Buy Me a Coffee捐赠:https://www.buymeacoffee.com/RaphaelDeLio

或者通过发送BitCoin到: 1HjG7pmghg3Z8RATH4aiUWr156BGafJ6Zw

关注我的社交媒体

保持联系,跟我一起深入探索Redis的世界吧!关注我在所有主要社交平台上的旅程,获取独家内容、技巧和讨论。

Twitter | LinkedIn | YouTube | Instagram

0人推荐
随时随地看视频
慕课网APP