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...

相关推荐
Lee川12 分钟前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川4 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i6 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有6 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有6 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫7 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫7 小时前
Handler基本概念
面试
Wect8 小时前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼8 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼8 小时前
Next.js 企业级落地
前端·javascript·面试