一、环境
操作系统: Ubuntu 22.04.4 LTS
内核版本: 6.8.0-117-generic
辅助工具: 1panel v2.1.13
虚拟环境: Docker v29.5.2
管理工具: pgAdmin 4
二、扩展安装与使用
1. PostGIS
bash
docker run -d \
--name postgis-container \
-e POSTGRES_PASSWORD=yourpassword \
-p 5432:5432 \
postgis/postgis:17-3.4
本质是给 PostgreSQL 增加空间存储、空间计算、空间索引的扩展插件
具体的数据类型和相关函数见:PostGIS 3.5.2dev 手册 - PostGIS 空间数据库
2. TimescaleDB
bash
# 创建并运行容器
docker run -d --name timescaledb \
-p 5432:5432 \
-e POSTGRES_PASSWORD=your_password \
-e POSTGRES_DB=your_database \
-e POSTGRES_USER=your_user \
timescale/timescaledb:latest-pg17
超表是 TimescaleDB 的核心,本质是普通表 + 自动时间分区,数据按时间范围拆分为多个 Chunk(默认 7 天),查询时自动跳过无关 Chunk,性能远优于单表。
sql
-- 步骤1:创建普通表(必须含时间字段,建议 TIMESTAMPTZ)
CREATE TABLE sensor_data (
time TIMESTAMPTZ NOT NULL, -- 时间主键(分区键)
sensor_id TEXT NOT NULL, -- 设备/传感器ID
temperature DOUBLE PRECISION NULL, -- 温度
humidity DOUBLE PRECISION NULL, -- 湿度
location TEXT NULL -- 位置
);
-- 步骤2:转为超表(按 time 分区,1天1个Chunk)
SELECT create_hypertable(
'sensor_data', -- 表名
'time', -- 分区字段(必须是 TIMESTAMPTZ)
chunk_time_interval => INTERVAL '1 day', -- 分区粒度(1天)
if_not_exists => TRUE -- 避免重复创建报错
);
-- 启用超表压缩(按时间自动压缩旧 Chunk)
ALTER TABLE sensor_data SET (
timescaledb.compress = true,
timescaledb.compress_segmentby = 'sensor_id' -- 按设备ID分组压缩
);
-- 创建保留策略:保留30天数据,自动删除更早数据
SELECT add_retention_policy('sensor_data', INTERVAL '30 days');
-- 查看保留策略
SELECT * FROM timescaledb_information.retention_policies;
3. zhparser
bash
docker run --name pgzhparser -d -e POSTGRES_PASSWORD=somepassword -p 5432:5432 zhparser/zhparser:bookworm-16
sql
create extension zhparser;
CREATE TEXT SEARCH CONFIGURATION testzhcfg (PARSER = zhparser);
ALTER TEXT SEARCH CONFIGURATION testzhcfg ADD MAPPING FOR n,v,a,i,e,l WITH simple;
SELECT * FROM ts_parse('zhparser', 'hello world! 2010年保障房建设在全国范围内获全面启动');
SELECT * FROM ts_parse('zhparser', '人工智能改变世界');
SELECT to_tsvector('testzhcfg', '人工智能改变世界');
4. PGMQ
bash
docker run -d --name pgmq-postgres \
-e POSTGRES_PASSWORD=postgres \
-p 5432:5432 \
ghcr.io/pgmq/pg18-pgmq:v1.10.0
常见用法:
pgmq.send
sql
-- 发送消息
select * from pgmq.send('my_queue', '{"hello": "world"}');
-- 发送消息带上headers信息
select * from pgmq.send('my_queue', '{"hello": "world"}', '{"trace_id": "abc123"}');
-- 发送一条延迟5s的消息,可用于超时取消/定时任务等场景
select * from pgmq.send('my_queue', '{"hello": "world"}', 5);
-- 发送一条延迟一天的消息
select * from pgmq.send('my_queue', '{"hello": "world"}', CURRENT_TIMESTAMP + INTERVAL '1 day');
-- 发送消息,带headers信息且延迟10s
select * from pgmq.send('my_queue', '{"hello": "world"}', '{"priority": "high"}', 10);
pgmq.send_batch
sql
-- 批量发送消息
select * from pgmq.send_batch('my_queue',
ARRAY[
'{"hello": "world_0"}',
'{"hello": "world_1"}'
]::jsonb[]
);
-- 批量发送消息带headers信息
select * from pgmq.send_batch('my_queue',
ARRAY['{"hello": "world_0"}', '{"hello": "world_1"}']::jsonb[],
ARRAY['{"trace_id": "abc"}', '{"trace_id": "def"}']::jsonb[]
);
-- 批量发送消息,且均延迟5s
select * from pgmq.send_batch('my_queue',
ARRAY[
'{"hello": "world_0"}',
'{"hello": "world_1"}'
]::jsonb[],
5
);
-- 批量发送消息,且均延迟1天
select * from pgmq.send_batch('my_queue',
ARRAY[
'{"hello": "world_0"}',
'{"hello": "world_1"}'
]::jsonb[],
CURRENT_TIMESTAMP + INTERVAL '1 day'
);
pgmq.send_topic
sql
-- 创建队列并绑定topic, # 匹配0到多个分段, * 匹配一个分段
select pgmq.create('logs_all');
select pgmq.create('logs_errors');
select pgmq.bind_topic('logs.#', 'logs_all');
select pgmq.bind_topic('logs.*.error', 'logs_errors');
-- 该消息会发送到两个队列 logs_all,logs_errors
select pgmq.send_topic('logs.api.error', '{"message": "API failed"}');
-- 带headers信息
select pgmq.send_topic('logs.db.error', '{"message": "DB error"}', '{"severity": "high"}', 0);
-- 带延迟5s
select pgmq.send_topic('logs.api.info', '{"message": "Request received"}', 5);
pgmq.send_batch_topic
pgmq.send_topic的批量版本
pgmq.bind_topic
sql
-- 创建队列
select pgmq.create('error_logs');
-- 绑定topic匹配
select pgmq.bind_topic('logs.*.error', 'error_logs');
select pgmq.bind_topic('alerts.#', 'error_logs');
-- 绑定是幂等的
select pgmq.bind_topic('logs.*.error', 'error_logs');
pgmq.unbind_topic
sql
-- 解绑topic匹配
select pgmq.unbind_topic('logs.*.error', 'error_logs');
-- 解绑成功返回true,本身未绑定返回false
select pgmq.unbind_topic('nonexistent.pattern', 'error_logs');
pgmq.test_routing
sql
-- 会返回该topic匹配了哪些,将会向哪些队列发送消息
select * from pgmq.test_routing('logs.api.error');
pattern | queue_name | compiled_regex
----------------+--------------+---------------------------
logs.# | logs_all | ^logs\..*$
logs.*.error | error_logs | ^logs\.[^.]+\.error$
pgmq.read
sql
-- 从队列my_queue读取2条消息,延迟10s可见
select * from pgmq.read('my_queue', 10, 2);
pgmq.read_with_poll
同pgmq.read,但如果队列里无消息会等待,默认5s
pgmq.pop
sql
-- 读取并删除2条消息
select * from pgmq.pop('my_queue', 2);
pgmq.delete
sql
-- 删除msg_id=5的消息
select pgmq.delete('my_queue', 5);
-- 批量删除
select * from pgmq.delete('my_queue', ARRAY[2, 3]);
pgmq.purge
sql
-- 删除整个队列的消息,返回删除的消息数量
select * from pgmq.purge_queue('my_queue');
pgmq.archive
sql
-- 归档msg_id=1的消息,从主队列移动到归档表,用于追溯重放等功能
SELECT * FROM pgmq.archive('my_queue', 1);
-- 批量版本
SELECT * FROM pgmq.archive('my_queue', ARRAY[1, 2]);
pgmq.create
sql
-- 创建消息队列
select from pgmq.create('my_queue');
-- 创建单表消息队列
select from pgmq.create_non_partitioned('my_queue');
-- 创建分区消息队列,需要另外安装pg_partman扩展,适合大数据量,高吞吐,冷热分离
select from pgmq.create_partitioned(
'my_partitioned_queue',
'100000',
'10000000'
);
-- 创建unlogged表队列,该类型无法恢复无法同步无法集群
select pgmq.create_unlogged('my_unlogged');
-- 转换为分区消息队列
select from pgmq.convert_archive_partitioned('my_queue', '10000', '100000');
pgmq.drop_queue
sql
-- 正式队列和归档队列均会删除
select * from pgmq.drop_queue('my_unlogged');
pgmq.list_queues
列出所有队列
pgmq.metrics
sql
-- 获取队列的信息
select * from pgmq.metrics('my_queue');
-- 所有队列信息
select * from pgmq.metrics_all();
queue_name | queue_length | newest_msg_age_sec | oldest_msg_age_sec | total_messages | scrape_time
------------+--------------+--------------------+--------------------+----------------+-------------------------------
my_queue | 16 | 2445 | 2447 | 35 | 2023-10-28 20:23:08.406259-05
5. UNLOGGED表 + pg_cron
对模拟redis的思路说明:
用 PostgreSQL 的 UNLOGGED 表 ( cache_kv )替代 Redis,核心就一张 KV 表 + 两个 PL/pgSQL 函数 + pg_cron 定时任务。
UNLOGGED 表不写 WAL,写入性能更高,且崩溃后数据丢失更接近 Redis 的语义。
过期策略模拟
被动删除 :查询时检查过期 → WHERE 子句过滤。
定期清理 :pg_cron 每 30 秒执行一次 DELETE FROM cache_kv WHERE expire_time < NOW()。
与redis的优缺对比:
优点:
- 零额外组件成本,不用单独维护 Redis
- 数据天然可持久化
- 支持完整 SQL 查询,功能远超 Redis
- 不用担心里存容量爆炸
- 权限、事务、安全直接复用 PG 能力
缺点:
- 性能差距巨大:Redis ≈ 10 万~100 万 QPS,PG≈1 万~5 万 QPS
- 延迟更高
- 内存效率远不如 Redis
- redis其他的数据结构需要另外实现
docker-compose.yml
XML
version: "3.9"
services:
postgres:
build:
context: .
dockerfile: Dockerfile
container_name: pedis
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: Aa123123
POSTGRES_DB: postgres
volumes:
- pgdata:/var/lib/postgresql/17/main
- ./init-cron.sql:/docker-entrypoint-initdb.d/init-cron.sql:ro
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
volumes:
pgdata:
Dockerfile
bash
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
# 安装必要工具并添加 PostgreSQL 17 官方源
RUN apt-get update \
&& apt-get install -y curl ca-certificates gnupg lsb-release \
&& curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor -o /usr/share/keyrings/postgresql-archive-keyring.gpg \
&& echo "deb [signed-by=/usr/share/keyrings/postgresql-archive-keyring.gpg] http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list \
&& apt-get update \
&& apt-get install -y postgresql-17 postgresql-client-17 \
&& apt-get update && apt-get upgrade -y && apt-get -y install postgresql-17-cron \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# 设置 PostgreSQL 环境变量
ENV PGDATA=/var/lib/postgresql/17/main
ENV PATH="/usr/lib/postgresql/17/bin:${PATH}"
# 先修改配置文件(启动前生效,避免需要 restart)
RUN sed -i "s/#listen_addresses = 'localhost'/listen_addresses = '*'/" /etc/postgresql/17/main/postgresql.conf \
&& echo "host all all 0.0.0.0/0 md5" >> /etc/postgresql/17/main/pg_hba.conf \
&& echo "shared_preload_libraries = 'pg_cron'" >> /etc/postgresql/17/main/postgresql.conf \
&& echo "cron.database_name = 'postgres'" >> /etc/postgresql/17/main/postgresql.conf
# 初始化数据库并启动
RUN pg_ctlcluster 17 main start \
&& pg_ctlcluster 17 main stop
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 5432
ENTRYPOINT ["/entrypoint.sh"]
bash
#!/bin/bash
set -e
# 启动 PostgreSQL 服务
pg_ctlcluster 17 main start
# 根据环境变量更新 postgres 用户密码
if [ -n "$POSTGRES_PASSWORD" ]; then
su - postgres -c "psql -c \"ALTER USER postgres PASSWORD '$POSTGRES_PASSWORD';\"" 2>/dev/null || true
fi
# 执行初始化 SQL 脚本(pg_cron 定时任务等)
if [ -f /docker-entrypoint-initdb.d/init-cron.sql ]; then
su - postgres -c "psql -f /docker-entrypoint-initdb.d/init-cron.sql" 2>/dev/null || true
fi
# 保持容器运行并监听信号
tail -f /var/log/postgresql/postgresql-17-main.log &
PID=$!
# 使用 trap 优雅停止
trap "pg_ctlcluster 17 main stop; kill $PID 2>/dev/null; exit 0" SIGTERM SIGINT
wait $PID
init-cron.sql
sql
CREATE EXTENSION IF NOT EXISTS pg_cron;
-- 模拟 Redis KV 存储表 (UNLOGGED: 不写WAL, 性能更高, 崩溃丢数据符合缓存语义)
DROP TABLE IF EXISTS cache_kv;
CREATE UNLOGGED TABLE cache_kv (
k VARCHAR(255) PRIMARY KEY,
v TEXT NOT NULL,
expire_time TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_cache_kv_expire_time ON cache_kv(expire_time);
-- 模拟 Redis GET:查不到或过期则清除残留
CREATE OR REPLACE FUNCTION cache_get(p_key VARCHAR)
RETURNS TEXT AS $$
DECLARE
result TEXT;
BEGIN
SELECT v INTO result
FROM cache_kv
WHERE k = p_key AND (expire_time IS NULL OR expire_time > NOW());
IF result IS NULL THEN
DELETE FROM cache_kv WHERE k = p_key;
END IF;
RETURN result;
END;
$$ LANGUAGE plpgsql;
-- 模拟 Redis TTL:返回剩余秒数(-1:永久, -2:不存在/已过期)
CREATE OR REPLACE FUNCTION cache_ttl(p_key VARCHAR)
RETURNS BIGINT AS $$
DECLARE
exp TIMESTAMP;
BEGIN
SELECT expire_time INTO exp FROM cache_kv WHERE k = p_key;
IF exp IS NULL THEN
RETURN -2;
END IF;
IF exp <= NOW() THEN
DELETE FROM cache_kv WHERE k = p_key;
RETURN -2;
END IF;
RETURN EXTRACT(EPOCH FROM (exp - NOW()))::BIGINT;
END;
$$ LANGUAGE plpgsql;
-- 每30秒定期清理过期数据
SELECT cron.schedule(
'cleanup_expired_cache',
'*/30 * * * *',
$$DELETE FROM cache_kv WHERE expire_time < NOW()$$
);
SELECT cron.schedule(
'daily_maintenance',
'0 3 * * *',
$$VACUUM ANALYZE cache_kv$$
);
GRANT USAGE ON SCHEMA cron TO postgres;