Redis与PostgreSQL缓存性能终极对决:7千次/秒真的够用吗?

原文:xuanhu.info/projects/it...

Redis与PostgreSQL缓存性能终极对决:7千次/秒真的够用吗?

概述

在当今的软件开发中,缓存已成为提升应用性能的关键技术。虽然Redis被公认为缓存领域的王者,但近年来出现了一种有趣的观点:是否可以用PostgreSQL替代Redis进行缓存? 本文将通过严谨的基准测试,深入分析这两种方案的性能差异,并探讨在实际项目中的适用场景。

🧪 实验设计与环境配置

测试环境架构

实验采用Kubernetes集群部署,确保环境一致性和可重复性:

  • 缓存节点:单独节点运行Redis或PostgreSQL,限制为2CPU核心和8GB内存
  • 应用节点:运行HTTP服务器,处理缓存请求
  • 压测节点:使用k6进行性能测试,模拟真实负载

软件版本与配置

组件 版本 配置
PostgreSQL 17.6 默认配置,使用unlogged表
Redis 8.2 默认配置
HTTP服务器 Go 1.21+ 自定义实现

测试数据集

为确保测试的真实性,我们预先向两个系统各插入3000万条缓存记录,使用UUID作为键。测试期间,我们会生成现有键的子集来模拟缓存命中和未命中场景。

🖥️ 核心代码实现

缓存接口设计

首先定义统一的缓存接口,这是实现可替换缓存方案的关键:

go 复制代码
// 定义缓存未命中的标准错误
var ErrCacheMiss = errors.New("cache miss")

// Cache接口定义Get和Set方法
type Cache interface {
    Get(ctx context.Context, key string) (string, error)
    Set(ctx context.Context, key string, value string) error
}

// Session结构体表示要缓存的数据
type Session struct {
    ID string
}

HTTP服务器实现

go 复制代码
// serveHTTP函数启动HTTP服务器并注册处理程序
func serveHTTP(c Cache) {
    http.HandleFunc("/get", getHandler(c))  // 注册GET请求处理
    http.HandleFunc("/set", setHandler(c))  // 注册SET请求处理

    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"  // 默认端口
    }

    fmt.Println("Server starting on http://0.0.0.0:" + port)

    server := &http.Server{Addr: "0.0.0.0:" + port}

    // 启动服务器goroutine
    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            fmt.Println("Error starting server:", err)
        }
    }()

    // 优雅关闭处理
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt)
    <-quit

    fmt.Println("Shutting down server...")

    if err := server.Close(); err != nil {
        fmt.Println("Error shutting down server:", err)
    }
}

Redis缓存实现

使用官方Redis客户端库实现缓存接口:

go 复制代码
// RedisCache结构体包装Redis客户端
type RedisCache struct {
    client *redis.Client
}

// NewRedisCache创建新的Redis缓存实例
func NewRedisCache() *RedisCache {
    redisURL := os.Getenv("REDIS_URL")
    if redisURL == "" {
        redisURL = "localhost:6379"  // 默认连接地址
    }

    fmt.Println("Connecting to Redis at", redisURL)

    // 创建Redis客户端配置
    client := redis.NewClient(&redis.Options{
        Addr:     redisURL,
        Password: "",  // 无密码
        DB:       0,   // 默认数据库
    })

    return &RedisCache{
        client: client,
    }
}

// Get方法从Redis获取键值
func (r *RedisCache) Get(ctx context.Context, key string) (string, error) {
    val, err := r.client.Get(ctx, key).Result()
    if err == redis.Nil {
        return "", ErrCacheMiss  // 键不存在返回缓存未命中
    }
    if err != nil {
        return "", err  // 其他错误
    }
    return val, nil  // 返回找到的值
}

// Set方法向Redis设置键值
func (r *RedisCache) Set(ctx context.Context, key string, value string) error {
    return r.client.Set(ctx, key, value, 0).Err()  // 0表示无过期时间
}

PostgreSQL缓存实现

使用pgx连接池实现PostgreSQL缓存:

go 复制代码
// PostgresCache结构体包装数据库连接池
type PostgresCache struct {
    db *pgxpool.Pool
}

// NewPostgresCache创建新的PostgreSQL缓存实例
func NewPostgresCache() (*PostgresCache, error) {
    pgDSN := os.Getenv("POSTGRES_DSN")
    if pgDSN == "" {
        pgDSN = "postgres://user:password@localhost:5432/mydb"  // 默认连接字符串
    }

    // 解析连接配置
    cfg, err := pgxpool.ParseConfig(pgDSN)
    if err != nil {
        return nil, err
    }

    // 配置连接池参数
    cfg.MaxConns = 50   // 最大连接数
    cfg.MinConns = 10   // 最小连接数

    // 创建连接池
    pool, err := pgxpool.NewWithConfig(context.Background(), cfg)
    if err != nil {
        return nil, err
    }

    // 创建unlogged缓存表(不写WAL日志,提高性能)
    _, err = pool.Exec(context.Background(), `
        CREATE UNLOGGED TABLE IF NOT EXISTS cache (
            key VARCHAR(255) PRIMARY KEY,  -- 主键字段
            value TEXT                      -- 值字段
        );
    `)
    if err != nil {
        return nil, err
    }

    return &PostgresCache{
        db: pool,
    }, nil
}

// Get方法从PostgreSQL获取键值
func (p *PostgresCache) Get(ctx context.Context, key string) (string, error) {
    var content string
    // 执行查询并扫描结果
    err := p.db.QueryRow(ctx, `SELECT value FROM cache WHERE key = $1`, key).Scan(&content)
    if err == pgx.ErrNoRows {
        return "", ErrCacheMiss  // 无结果返回缓存未命中
    }
    if err != nil {
        return "", err  // 其他错误
    }
    return content, nil  // 返回找到的值
}

// Set方法向PostgreSQL设置键值(使用UPSERT操作)
func (p *PostgresCache) Set(ctx context.Context, key string, value string) error {
    // 使用ON CONFLICT处理键冲突
    _, err := p.db.Exec(ctx, 
        `INSERT INTO cache (key, value) VALUES ($1, $2) 
         ON CONFLICT (key) DO UPDATE SET value = $2`, 
        key, value)
    return err
}

📊 性能测试方法论

测试场景设计

为了全面评估两种缓存方案的性能,我们设计了三种测试场景:

  1. 读取测试:80%概率命中现有键,20%概率未命中
  2. 写入测试:90%概率插入新键,10%概率更新现有键
  3. 混合测试:80%读取操作,20%写入操作

每种测试运行2分钟,记录每秒操作数、延迟分布以及资源使用情况。

性能指标

我们关注以下关键性能指标:

  • 吞吐量:每秒处理的请求数(QPS)
  • 延迟:平均延迟、中位数延迟、P95和P99延迟
  • 资源使用:CPU和内存占用情况
  • 可扩展性:随着负载增加的性能变化

📈 测试结果分析

读取性能对比

xychart-beta title "读取性能对比 - QPS(越高越好)" x-axis [Redis, PostgreSQL] y-axis "每秒请求数" 0 --> 10000 bar [8350, 7425] line [8350, 7425]

在纯读取测试中,Redis表现出色,达到了约8350 QPS,而PostgreSQL也达到了7425 QPS。虽然Redis领先约12.5%,但PostgreSQL的表现仍然令人印象深刻。

资源使用情况

  • Redis:CPU使用约1280m(限制2000m),内存稳定在3800MB
  • PostgreSQL:CPU达到2000m限制,内存使用约5000MB

写入性能对比

xychart-beta title "写入性能对比 - QPS(越高越好)" x-axis [Redis, PostgreSQL] y-axis "每秒请求数" 0 --> 8000 bar [7920, 6100] line [7920, 6100]

在写入测试中,Redis继续保持领先,达到7920 QPS,PostgreSQL为6100 QPS,差距约为29.8%。

资源使用情况

  • Redis:CPU使用约1280m,内存增长到4300MB(由于新键插入)
  • PostgreSQL:CPU达到2000m限制,内存增长到5500MB

混合读写性能对比

xychart-beta title "混合读写性能对比 - QPS(越高越好)" x-axis [Redis, PostgreSQL] y-axis "每秒请求数" 0 --> 8500 bar [8150, 6950] line [8150, 6950]

在混合工作负载下,Redis处理8150 QPS,PostgreSQL处理6950 QPS,差距约为17.3%。

延迟对比分析

xychart-beta title "平均延迟对比(毫秒,越低越好)" x-axis [Redis, PostgreSQL] y-axis "延迟(ms)" 0 --> 5 bar [1.2, 2.8] line [1.2, 2.8]

Redis在延迟方面表现更好,平均延迟为1.2ms,而PostgreSQL为2.8ms。对于需要极低延迟的应用,这一差异可能很重要。

🧠 Unlogged表的性能影响

PostgreSQL的unlogged表不写入预写日志(WAL),这可以显著提高写入性能。我们对比了使用普通表和unlogged表的性能差异:

xychart-beta title "Unlogged表 vs 普通表性能对比" x-axis ["写入-unlogged", "写入-普通", "混合-unlogged", "混合-普通"] y-axis "QPS" 0 --> 7000 bar [6100, 4200, 6950, 6250]

结果显示,unlogged表对写入性能提升显著(提高约45%),对混合工作负载也有明显改善(提高约11%),但对读取性能影响很小。

🔧 PostgreSQL性能优化建议

根据测试结果和社区反馈,以下优化可以进一步提升PostgreSQL作为缓存的性能:

1. 配置调优

使用pgtune等工具生成优化配置:

ini 复制代码
# 示例优化配置
max_connections = 100
shared_buffers = 2GB
effective_cache_size = 6GB
maintenance_work_mem = 512MB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100
random_page_cost = 1.1
effective_io_concurrency = 200
work_mem = 5242kB
min_wal_size = 1GB
max_wal_size = 4GB

2. 索引优化

虽然我们使用主键索引,但可以考虑以下优化:

sql 复制代码
-- 添加并发索引创建(不影响业务)
CREATE INDEX CONCURRENTLY IF NOT EXISTS cache_key_idx ON cache (key);

-- 定期清理和重建索引
REINDEX TABLE CONCURRENTLY cache;

3. 分区和表空间优化

对于超大规模缓存,可以考虑分区:

sql 复制代码
-- 按键哈希分区
CREATE TABLE cache (
    key VARCHAR(255),
    value TEXT
) PARTITION BY HASH (key);

-- 创建分区子表
CREATE TABLE cache_0 PARTITION OF cache FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE cache_1 PARTITION OF cache FOR VALUES WITH (MODULUS 4, REMAINDER 1);
CREATE TABLE cache_2 PARTITION OF cache FOR VALUES WITH (MODULUS 4, REMAINDER 2);
CREATE TABLE cache_3 PARTITION OF cache FOR VALUES WITH (MODULUS 4, REMAINDER 3);

4. 连接池优化

使用PgBouncer或连接池中间件减少连接开销:

ini 复制代码
# PgBouncer配置示例
[databases]
mydb = host=127.0.0.1 port=5432 dbname=mydb

[pgbouncer]
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20

🌐 网络架构考虑因素

测试中发现了几个关键架构洞察:

网络跳数的影响

如果缓存需要经过多个网络跳点,Redis和PostgreSQL之间的性能差异可能会减小。在设计系统架构时,应尽量减少网络延迟:

graph TD A[客户端] --> B[负载均衡器] B --> C[应用服务器] C --> D[缓存层] subgraph "低延迟架构" E[客户端] --> F[本地缓存代理] F --> G[缓存层] end

数据局部性优势

对于已在使用PostgreSQL的应用,使用同一数据库作为缓存可以提供更好的数据局部性,减少系统复杂度。

📋 实践建议与决策指南

何时选择Redis

以下情况推荐使用Redis作为缓存:

  1. 极致性能需求:需要最低延迟和最高吞吐量
  2. 丰富数据结构:需要使用Redis特有的数据结构(如集合、有序集合等)
  3. 内置缓存功能:需要TTL、LRU自动淘汰等内置功能
  4. 高并发场景:预计每秒请求数超过10,000
  5. 内存充足:有足够内存容纳整个缓存数据集

何时选择PostgreSQL

以下情况可以考虑使用PostgreSQL作为缓存:

  1. 系统简化:希望减少外部依赖,保持架构简单
  2. 已有PostgreSQL:项目中已使用PostgreSQL,且缓存需求不大
  3. 数据持久性:需要缓存数据具备更强的持久性和一致性
  4. 复杂查询:可能需要对缓存数据进行复杂查询或分析
  5. 资源受限:项目初期,资源投入有限

混合方案

对于大型项目,可以考虑混合方案:

graph TB A[客户端] --> B[应用层] B --> C[本地缓存] B --> D[Redis分布式缓存] B --> E[PostgreSQL持久缓存] C -->|缓存未命中| D D -->|缓存未命中| E

🚀 实现缓存TTL功能

如果选择PostgreSQL作为缓存但需要TTL功能,可以通过以下方式实现:

1. 添加过期时间字段

sql 复制代码
ALTER TABLE cache ADD COLUMN expires_at TIMESTAMP DEFAULT NULL;
CREATE INDEX cache_expires_idx ON cache (expires_at);

2. 定期清理任务

设置定时任务清理过期键:

sql 复制代码
-- 使用pg_cron扩展定期清理
SELECT cron.schedule('0 * * * *', $$DELETE FROM cache WHERE expires_at < NOW()$$);

3. 查询时过滤过期键

go 复制代码
func (p *PostgresCache) Get(ctx context.Context, key string) (string, error) {
    var content string
    err := p.db.QueryRow(ctx, 
        `SELECT value FROM cache WHERE key = $1 AND (expires_at IS NULL OR expires_at > NOW())`, 
        key).Scan(&content)
    // ... 错误处理逻辑
}

🔮 未来扩展与替代方案

其他缓存解决方案

除了Redis和PostgreSQL,还有其他值得考虑的缓存方案:

  1. KeyDB:Redis的多线程替代品,兼容Redis协议
  2. Dragonfly:新一代高性能缓存系统
  3. SQLite:轻量级嵌入式数据库,适合边缘计算场景
  4. 内存表:使用MySQL或PostgreSQL的内存表功能

读写分离架构

对于超高负载场景,可以考虑读写分离:

graph LR A[写入请求] --> B[主数据库] B --> C[复制] C --> D[只读副本1] C --> E[只读副本2] C --> F[只读副本3] G[读取请求] --> H[负载均衡器] H --> D H --> E H --> F

🎯 总结

性能表现

  1. Redis确实更快:在所有测试场景中,Redis的性能都优于PostgreSQL,吞吐量高出12-30%,延迟降低50%以上
  2. PostgreSQL表现令人惊喜:即使使用默认配置,PostgreSQL也能达到7425 QPS,相当于每天处理6.41亿次请求
  3. 资源消耗:Redis在CPU使用上更加高效,而PostgreSQL需要更多CPU资源来达到类似性能

实践意义

  1. 适用于大多数应用:对于99%的应用,PostgreSQL的缓存性能已经足够,无需引入Redis
  2. 架构简化价值:减少系统组件可以降低运维复杂度、提高开发效率和系统稳定性
  3. 成本考虑:使用现有PostgreSQL实例作为缓存可以节省硬件和许可成本

建议

不要盲目追求极致性能,而是根据实际需求做出技术选择。对于大多数Web应用、API服务和中小型企业应用,使用PostgreSQL作为缓存是完全可行的方案。当性能真正成为瓶颈时,再考虑引入Redis或其他专用缓存解决方案。

总结

通过本次深入的性能对比和分析,我们全面了解了Redis和PostgreSQL在缓存场景下的表现。虽然Redis在纯粹的性能指标上领先,但PostgreSQL提供了一个简单且足够高效的替代方案,特别适合那些已经使用PostgreSQL且不希望增加系统复杂性的项目。

最重要的是,选择技术方案时应该基于实际需求而不是盲目追求性能指标。7425 QPS的性能意味着PostgreSQL可以处理绝大多数应用的缓存需求,而保持系统简单性带来的好处往往超过微小的性能提升。

无论选择哪种方案,定义清晰的缓存接口和抽象层都是明智的做法,这为未来的扩展和变更提供了灵活性。在软件开发中,简单性和可维护性往往比极致的性能更重要。

原文:xuanhu.info/projects/it...

相关推荐
帧栈3 小时前
开发避坑指南(61):Redis持久化失败:RDB快照因磁盘问题无法保存解决方案
数据库·redis·缓存
海梨花3 小时前
关于Java的几个小问题
java·面试
晨港飞燕4 小时前
Websocket+Redis实现微服务消息实时同步
redis·websocket·微服务
gsfl4 小时前
redis单线程模型
数据库·redis·缓存
倔强青铜三5 小时前
苦练Python第58天:filecmp模块——文件和目录“找不同”的利器
人工智能·python·面试
倔强青铜三5 小时前
苦练Python第59天:tempfile模块,临时文件自动删!再也不用手动清理到怀疑人生
人工智能·python·面试
Q741_1475 小时前
C++ 位运算 高频面试考点 力扣 371. 两整数之和 题解 每日一题
c++·算法·leetcode·面试·位运算
mpHH7 小时前
babelfish for postgresql 分析--babelfishpg_tds--doing
数据库·postgresql
Aniugel7 小时前
渐进式 Web 应用(PWA)
面试·pwa