Postgres缓存实战
参考链接
- PostgreSQL 内存表可选项 - unlogged table-阿里云开发者社区
- PostgreSQL如何实现缓存表?
- Temporary tables vs unlogged tables performance in PostgreSQL? - Stack Overflow
- You Don't Need a Dedicated Cache Service - PostgreSQL as a Cache | Martin Heinz | Personal Website & Blog
- PostgreSQL Unlogged Tables - Look Ma, No WAL! | Crunchy Data Blog
- PostgreSQL temporary table cache in memory – iTecNote
- PostgreSQL and Temporary Tables - DEV Community
- PostgreSQL TEMPORARY TABLE和UNLOGGED TABLE, 临时表、全局临时表和 Unlogged 表_pg 库 创建临时表 temporary preserve rows-CSDN博客
- Postgresql Checkpoint 原理 | 学习笔记
- 深入理解PostgreSQL中的Checkpoints-百度开发者中心
- Postgresql 主备原理及部署 | 记录美好生活
- PostgreSQL -使用unlogged table表问题-天翼云开发者社区 - 天翼云
前言:
在初创公司中简化技术栈、减少组件、加快开发速度、降低风险并提供更多功能特性的方法之一就是 “一切皆用 Postgres” 。Postgres 能够取代许多后端技术,包括 Kafka、RabbitMQ、ElasticSearch,Mongo和 Redis ,至少到数百万用户时都毫无问题。
- 使用 Postgres 替代 Redis 作为缓存,使用 UNLOGGED Table 并用 TEXT 类型存储 JSON 数据,并使用存储过程来添加并强制执行过期时间,正如 Redis 所做的那样。
- 使用 Postgres 作为消息队列,采用 SKIP LOCKED来代替Kafka(如果你只需要消息队列的能力)。
- 使用加装了 TimescaleDB扩展的 Postgres 作为数据仓库。
- 使用 PostgreSQL 的 JSONB 类型来存储、索引、搜索 JSON 文档,从而替代 MongoDB。
- 使用加装 pg_cron 扩展的 Postgres 作为定时任务守护程序,在特定时间执行特定任务,例如发送邮件,或向消息队列中添加事件。
- 使用 Postgres + PostGIS 执行 地理空间查询。
- 使用 Postgres 进行全文搜索,加装 ParadeDB 替代 ElasticSearch。
- 使用 Postgres 在数据库中生成JSON,免去服务器端代码编写,直接供 API 使用。
- 使用 GraphQL适配器,也可以让 PostgreSQL 提供 GraphQL 服务。
我已明言,一切皆可Postgres。
PostgreSQL 在9.1 中推出了一种特殊的表:UNLOGGED TABLE
,使用UNLOGGED TABLE
最大的特点是涉及到表的更新,删除等操作不会记录WAL 日志,这样可以大大的提高性能。
Postgres的可玩性太多,本文我们主要来探讨Postgres使用UNLOGGED TABLE
实现实现缓存服务功能。
传统的缓存服务特性
- 过期-能够设置缓存数据的过期时间,使缓存不会存储过时的信息
- 内存回收-在缓存已满时删除不常用的
- 缓存失效-在数据更改时覆盖数据
- 性能-使用缓存的主要原因是避免数据库查询缓慢。与缓存相比,SQL数据库的写入速度通常也较慢
- 非持久性-缓存服务通常具有有限的或没有持久性的
- 键值存储
如果我们要使用Postgres来实现缓存服务,那么也必定要具备以上传统缓存服务的特性。并且使用Potgres作为缓存服务时,相比其他第三方组件时也有如下优点:
- 熟悉的界面-使用SQL和常见的PostgreSQL客户端库,使其更容易集成到应用程序中
- 成本-无需设置和维护其他服务。这降低了运营成本。您也不需要Redis/Memcached/。。。维护专家
UNLOGGED TABLE 特性
- UNLOGGED TABLE 不会记录WAL日志
- UNLOGGED TABLE 在集群环境下,只会在主节点上有数据,不会复制到备节点;即不支持分布式缓存(写入速度快,备库无数据,只有结构。)
- 当数据库crash后,数据库重启时自动清空unlogged table的数据。
- 正常关闭数据库,再启动时,unlogged table有数据。
- temporary tables are cached in process private memory, governed by the
temp_buffers
parameter, while unlogged tables are cached inshared_buffers
UNLGGED TABLE 缓存应用实战
创建缓存表
使用
UNLOGGED TABLE
作为缓存表
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);
和普通表创建唯一不同的就是多了UNLOGGED
关键字;这里使用jsonb
类型来存储数据,根据适合自己的场景你也可以使用text
、varchar
或者hstore
等类型。这里的inserted_at
字段主要的作用是用户缓存的过期校验。最后我们也创建了一个索引来提高查询的性能。
缓存管理(过期、失效、回收)
如上所述,我们期望缓存服务的一个特性是能够使记录过期。要在PostgreSQL中做到这一点,我们可以创建一个定期删除旧行的存储过程:
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'); -- 删除插入时间距离当前时间超过1小时的记录
我们需要定期的去调用这个存储过程,使得过期记录及时被删除掉。我们可以使用Postgres的pg_cron
扩展,需要在操作系统层面安装这个插件(参考链接),然后再数据库中创建这个扩展。Docker安装
安装完成之后(CREATE EXTENSION pg_cron;
),可以使用如下命令定期调用存储过程:
-- Create a schedule to run the procedure every hour
SELECT cron.schedule('0 * * * *', $$CALL expire_rows('1 hour');$$);
-- List all scheduled jobs
SELECT * FROM cron.job;
如果你没有安装这个扩展,你也可以选择创建一个触发器,在每次插入数据的时候检测过期记录并删除。(不推荐)
CREATE OR REPLACE FUNCTION expire_rows_func (retention_hours integer) RETURNS void AS
$$
BEGIN
DELETE FROM cache
WHERE inserted_at < NOW() - (retention_hours || ' hours')::interval;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION expire_rows_func_trigger() RETURNS trigger AS
$$
BEGIN
PERFORM expire_rows_func (1);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER cache_cleanup_trigger
AFTER INSERT ON cache
FOR EACH ROW
EXECUTE FUNCTION expire_rows_func_trigger();
当然,实际的过期/清除时间取决于你的数据和用例。
这是过期,但内存回收(删除旧数据为新记录腾出空间)怎么办?
由于到过期应该会降低大小,我认为内存回收是可选的,但我们也可以实现这一点——我们可以添加last_read时间戳列,该列将在每次读取时更新。然后,我们可以每隔一段时间运行一次存储过程来清理最近未使用的行,从而为我们提供一个LRU缓存。您可以决定是否值得在每次读取时更新行。
这样,我们创建了一个简单的缓存,它具有快速写入、快速读取、键值存储、比传统缓存服务更好的持久性、缓存过期、清理和失效,而无需部署另一个(昂贵的)服务。
性能
到目前为止,我主要只提到使用PostgreSQL作为缓存有多好,但显然也有缺点。其中之一是性能,这将比专门构建的优化缓存服务(略)差。更糟糕的是,这取决于您的数据和使用模式(读重或写重操作、数据大小、查询类型等)。将UNLOGGED表与Memcached或Redis进行基准测试和比较超出了本文的范围,但如果您想测试性能(并且应该),您可以从生成一些数据开始:
INSERT INTO cache (key, value, inserted_at)
VALUES
('key1', '{"field1": "value1", "field2": "value2"}', NOW() - INTERVAL '1 hour'),
('key2', '{"field1": "value3", "field2": "value4"}', NOW() - INTERVAL '2 hours'),
('key3', '{"field1": "value5", "field2": "value6"}', NOW() - INTERVAL '3 hours'),
('key4', '{"field1": "value7", "field2": "value8"}', NOW() - INTERVAL '4 hours'),
('key5', '{"field1": "value9", "field2": "value10"}', NOW() - INTERVAL '5 hours');
-- Insert more data
INSERT INTO cache (key, value, inserted_at)
SELECT 'key' || s,
('{"field1": "value' || s || '"}')::jsonb,
NOW() - (s || ' hours')::interval
FROM generate_series(1, 25) AS s;
然后分析插入和查询语句的性能:
EXPLAIN ANALYZE SELECT * FROM cache WHERE key = 'key1';
EXPLAIN ANALYZE INSERT INTO cache (key, value, inserted_at)
VALUES ('new_key', '{"field1": "new_value1", "field2": "new_value2"}', NOW());
我还建议阅读这篇关于UNLOGGED
表的好文章,它说明了有关该功能的更多细节,包括一些性能比较。
结合SpringBoot使用
- java - Can hibernate autogenerate an unlogged table? - Stack Overflow
- Solved-Can hibernate autogenerate an unlogged table?-Hibernate
在SpringBoot JPA 中
UNLOGGED TABLE
的使用和普通表的使用基本一致,唯一需要注意的是使用UNLOGGED TABLE
时,需要自己手动建表,不能依赖JPA的自动建表;因为自动建表的类型是普通表而非UNLOGGED TABLE
,其他的用法均和普通表一致。
个人观点
在我看来,大多数时候,你不需要额外的服务或特殊的数据库。有一个原因是,为什么多数的新的高级数据库是在好的旧SQL数据库之上实现的。可以看PostgreSQL派生数据库的列表。更不用说像Timescale和其他许多数据库了,它们实际上只是PostgreSQL,上面点缀了一些额外的功能。虽然专门构建、优化的解决方案有其用武之地,但最好考虑为每一件小事运行额外服务的利弊,如缓存、调度程序、矢量数据库等。也许,只是也许,使用一个工具处理多件事,节省成本和开销超过了额外服务提供的少数好处。