PostgreSQL - 与 Redis 的结合使用:缓存策略优化

PostgreSQL - 与 Redis 的结合使用:缓存策略优化
在这里插入图片描述
👋 大家好,欢迎来到我的技术博客!
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕PostgreSQL这个话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!

文章目录

PostgreSQL - 与 Redis 的结合使用:缓存策略优化 💡

在现代高并发、低延迟的应用系统中,数据库性能往往是系统的瓶颈之一。PostgreSQL 作为一款功能强大、稳定可靠的开源关系型数据库,被广泛应用于各类业务场景。然而,面对海量请求和复杂查询,即便是经过精心调优的 PostgreSQL 实例,也可能难以满足毫秒级响应的需求。

此时,引入内存数据库如 Redis 作为缓存层,成为提升系统整体性能的关键手段。通过将热点数据缓存在 Redis 中,可以显著减少对 PostgreSQL 的直接访问,降低数据库负载,同时大幅提升应用响应速度。

本文将深入探讨 PostgreSQL 与 Redis 结合使用的缓存策略优化,涵盖缓存架构设计、常见模式(如 Cache-Aside、Read-Through、Write-Through 等)、缓存一致性保障、缓存穿透/雪崩/击穿问题的解决方案,并辅以 Java 代码示例Mermaid 架构图,帮助开发者构建高性能、高可用的数据访问层。


为什么需要缓存?🤔

PostgreSQL 虽然支持索引、分区、物化视图等多种优化手段,但其本质仍是基于磁盘存储的关系型数据库。即使使用 SSD,一次磁盘 I/O 通常也需要几百微秒到几毫秒。而 Redis 作为基于内存的键值存储系统,读写延迟通常在 亚毫秒级别(<1ms),吞吐量可达每秒数十万次操作。

📊 性能对比(典型值):PostgreSQL 单行查询(带索引):1–5 msRedis GET/SET 操作:0.1–0.5 ms内存访问 vs 磁盘访问:快 10⁵ 倍以上

因此,对于高频读取、低频更新的“热点数据”(如用户信息、商品详情、配置项等),将其缓存到 Redis 中,可极大减轻 PostgreSQL 压力,提升用户体验。


缓存架构设计:PostgreSQL + Redis 的典型拓扑 🏗️

在典型的三层架构中,缓存层通常位于应用服务与数据库之间:

Hit

Miss

Client

Application Service

Cache?

Redis

PostgreSQL

应用首先尝试从 Redis 获取数据(缓存命中),若未命中(缓存未命中),则回源到 PostgreSQL 查询,并将结果写入 Redis 供后续请求使用。

这种模式称为 Cache-Aside(旁路缓存),是目前最主流的缓存使用方式。


缓存策略详解 🧠

1. Cache-Aside(旁路缓存)✅

这是最常用、最灵活的缓存策略。应用负责管理缓存的读写逻辑:

  • 读操作:先查 Redis,命中则返回;未命中则查 PostgreSQL,再写入 Redis。
  • 写操作:先更新 PostgreSQL,再删除 Redis 中的对应缓存(或更新)。
⚠️ 注意:写操作通常选择 删除缓存 而非更新缓存,原因如下:更新缓存可能引发脏数据(如并发写)删除更简单,且下次读会自动加载最新数据避免缓存与数据库不一致的窗口期过长
Java 示例:Cache-Aside 实现
@ServicepublicclassUserService{@AutowiredprivateUserRepository userRepository;// JPA Repository for PostgreSQL@AutowiredprivateRedisTemplate<String,User> redisTemplate;privatestaticfinalStringUSER_CACHE_PREFIX="user:";publicUsergetUserById(Long id){String key =USER_CACHE_PREFIX+ id;// Step 1: Try to get from RedisUser user = redisTemplate.opsForValue().get(key);if(user !=null){return user;}// Step 2: Cache miss, query PostgreSQL user = userRepository.findById(id).orElse(null);if(user !=null){// Step 3: Write back to Redis with TTL redisTemplate.opsForValue().set(key, user,Duration.ofMinutes(10));}return user;}publicvoidupdateUser(User user){// Step 1: Update PostgreSQL userRepository.save(user);// Step 2: Delete cache (not update!)String key =USER_CACHE_PREFIX+ user.getId(); redisTemplate.delete(key);}}
🔒 线程安全提示:上述代码在单机环境下基本可用,但在分布式系统中,需考虑并发问题(见后文“缓存一致性”部分)。

2. Read-Through(读穿透)🔄

在 Read-Through 模式中,应用只与缓存层交互,缓存层负责在未命中时自动从数据库加载数据。这种方式将缓存逻辑封装在缓存客户端或中间件中。

虽然 Spring Data Redis 不直接提供 Read-Through 抽象,但可通过自定义 CacheManager 或使用 Caffeine + Redis 二级缓存实现。

示例:Spring Cache + Redis(简化版 Read-Through)
@ServicepublicclassCachedUserService{@AutowiredprivateUserRepository userRepository;@Cacheable(value ="users", key ="#id")publicUsergetUserById(Long id){return userRepository.findById(id).orElse(null);}@CacheEvict(value ="users", key ="#user.id")publicvoidupdateUser(User user){ userRepository.save(user);}}

配合 RedisCacheManager 配置:

@Configuration@EnableCachingpublicclassCacheConfig{@BeanpublicRedisCacheManagercacheManager(RedisConnectionFactory connectionFactory){RedisCacheConfiguration config =RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofMinutes(10)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(newGenericJackson2JsonRedisSerializer()));returnRedisCacheManager.builder(connectionFactory).cacheDefaults(config).build();}}
✅ 优点:代码简洁,逻辑解耦
❌ 缺点:灵活性较低,难以处理复杂缓存逻辑(如空值缓存、批量加载等)

3. Write-Through(写穿透)✍️

Write-Through 要求每次写操作同时更新缓存和数据库,确保两者同步。该模式适用于对一致性要求极高的场景,但会增加写延迟。

由于 Redis 本身不支持事务性写入到 PostgreSQL,通常需由应用层保证原子性(如使用分布式事务,但成本高),因此 实际生产中较少采用纯 Write-Through

更常见的做法是 Write-Behind(写回):先更新缓存,异步批量同步到数据库。但此模式存在数据丢失风险,一般用于日志、统计等非关键数据。


缓存一致性:如何避免脏数据?🧼

缓存与数据库的一致性是缓存系统的核心挑战。理想情况下,我们希望“强一致性”,但受限于 CAP 定理,在分布式系统中通常只能做到 最终一致性

常见问题:先更新 DB 还是先删缓存?

考虑以下两种顺序:

  1. 先更新 DB,再删除缓存(推荐)
  2. 先删除缓存,再更新 DB
场景分析(并发写+读)

假设初始状态:DB=100,Cache=100

方案1:先更新 DB,再删缓存

  • 线程A:更新 DB → 200
  • 线程B:读请求,Cache miss → 读 DB=200 → 写 Cache=200
  • 线程A:删缓存
  • 最终:DB=200,Cache=200 ✅

但若线程B在A删缓存前就读取了旧值:

  • 线程A:更新 DB → 200
  • 线程B:读请求,Cache miss → 读 DB=200 → (此时A未删缓存)
  • 线程A:删缓存
  • 线程B:写 Cache=200
  • 最终一致 ✅

方案2:先删缓存,再更新 DB

  • 线程A:删缓存
  • 线程B:读请求,Cache miss → 读 DB=100(旧值)→ 写 Cache=100
  • 线程A:更新 DB → 200
  • 最终:DB=200,Cache=100 ❌ 脏数据!

因此,推荐“先更新数据库,再删除缓存”

🔗 更深入讨论可参考 Martin Kleppmann 的博客

延迟双删(Double Delete)策略

为应对上述极端并发场景,可采用“延迟双删”:

  1. 删除缓存
  2. 更新数据库
  3. 延迟 N 毫秒后再次删除缓存
publicvoidupdateUserWithDoubleDelete(User user){String key =USER_CACHE_PREFIX+ user.getId(); redisTemplate.delete(key);// 第一次删除 userRepository.save(user);// 更新 DB// 延迟删除(防止并发读加载旧值)CompletableFuture.runAsync(()->{try{Thread.sleep(500);// 根据业务调整 redisTemplate.delete(key);}catch(InterruptedException e){Thread.currentThread().interrupt();}});}
⚠️ 缺点:增加复杂度,且不能 100% 保证一致性,仅降低概率。

使用消息队列解耦(最终一致性)

更健壮的做法是:更新 DB 后,发送消息到 Kafka/RabbitMQ,由消费者异步删除缓存。

RedisCacheWorkerKafkaPostgreSQLAppRedisCacheWorkerKafkaPostgreSQLAppUPDATE userSend "user.updated" eventConsume eventDELETE user:id

此方案优势:

  • 解耦缓存与 DB 操作
  • 失败可重试,保证最终一致性
  • 避免应用线程阻塞

缓存三大难题:穿透、雪崩、击穿 🛡️

即使有了缓存,仍需防范以下三类典型问题。

1. 缓存穿透(Cache Penetration)🕳️

定义:查询一个不存在的数据,由于缓存不命中,每次都穿透到数据库。

危害:恶意攻击者可构造大量无效 ID 请求,压垮数据库。

解决方案

  • 布隆过滤器(Bloom Filter):在缓存前加一层布隆过滤器,快速判断 key 是否可能存在。
  • 缓存空值(Null Cache):对查询结果为 null 的 key,也缓存一个特殊值(如 "NULL"),并设置较短 TTL。
Java 示例:缓存空值
publicUsergetUserById(Long id){String key =USER_CACHE_PREFIX+ id;Object cached = redisTemplate.opsForValue().get(key);if(cached !=null){if("NULL".equals(cached)){returnnull;// 明确表示不存在}return(User) cached;}User user = userRepository.findById(id).orElse(null);if(user !=null){ redisTemplate.opsForValue().set(key, user,Duration.ofMinutes(10));}else{// 缓存空值,防止穿透 redisTemplate.opsForValue().set(key,"NULL",Duration.ofSeconds(60));}return user;}
🔗 布隆过滤器原理可参考 RedisBloom 模块文档

2. 缓存雪崩(Cache Avalanche)❄️

定义:大量缓存同时过期,导致瞬间所有请求打到数据库。

原因:缓存 TTL 设置相同,或 Redis 宕机。

解决方案

  • 随机 TTL:在基础 TTL 上增加随机偏移(如 10min ± 2min)
  • 永不过期 + 异步刷新:缓存不设 TTL,后台任务定期更新
  • 多级缓存:本地缓存(Caffeine) + Redis,降低 Redis 压力
  • 限流降级:Hystrix/Sentinel 保护数据库
Java 示例:随机 TTL
privateDurationgetRandomTtl(int baseMinutes,int randomRange){int ttl = baseMinutes *60+newRandom().nextInt(randomRange *60);returnDuration.ofSeconds(ttl);}// 使用 redisTemplate.opsForValue().set(key, user,getRandomTtl(10,5));// 10±5 分钟

3. 缓存击穿(Cache Breakdown)🎯

定义:某个热点 key 在过期瞬间,大量并发请求同时穿透到数据库。

与雪崩区别:击穿是单个 key,雪崩是大量 key。

解决方案

  • 互斥锁(Mutex Lock):只有一个线程回源,其他等待
  • 逻辑过期(Logical Expiry):缓存中存储过期时间,后台异步更新
Java 示例:互斥锁(Redis 分布式锁)
publicUsergetUserByIdWithMutex(Long id){String key =USER_CACHE_PREFIX+ id;String lockKey ="lock:"+ key;// Step 1: Try cacheUser user = redisTemplate.opsForValue().get(key);if(user !=null)return user;// Step 2: Acquire distributed lockBoolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey,"1",Duration.ofSeconds(10));if(Boolean.TRUE.equals(locked)){try{// Double-check inside lock user = redisTemplate.opsForValue().get(key);if(user !=null)return user;// Load from DB user = userRepository.findById(id).orElse(null);if(user !=null){ redisTemplate.opsForValue().set(key, user,Duration.ofMinutes(10));}else{ redisTemplate.opsForValue().set(key,"NULL",Duration.ofSeconds(60));}}finally{ redisTemplate.delete(lockKey);// Release lock}}else{// Wait and retry (or return stale data)Thread.sleep(50);returngetUserByIdWithMutex(id);// Recursive retry}return user;}
⚠️ 注意:递归重试可能导致栈溢出,生产环境建议用循环 + 最大重试次数。

缓存序列化与数据结构设计 🧩

序列化选择

Redis 存储的是字节数组,需将 Java 对象序列化。常见方案:

方案优点缺点
JDK 原生简单体积大、跨语言不兼容
JSON (Jackson)可读、通用性能略低
Protobuf高效、紧凑需定义 schema
Kryo快速、小体积非线程安全

推荐使用 Jackson JSON,兼顾可读性与通用性:

@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactory factory){RedisTemplate<String,Object> template =newRedisTemplate<>(); template.setConnectionFactory(factory);Jackson2JsonRedisSerializer<Object> serializer =newJackson2JsonRedisSerializer<>(Object.class);ObjectMapper mapper =newObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL,JsonAutoDetect.Visibility.ANY); mapper.activateDefaultTyping(mapper.getPolymorphicTypeValidator(),ObjectMapper.DefaultTyping.NON_FINAL); serializer.setObjectMapper(mapper); template.setValueSerializer(serializer); template.setHashValueSerializer(serializer); template.afterPropertiesSet();return template;}

数据结构优化

  • 单对象SET user:123 "{...}"
  • 列表/分页:使用 ZSET 存储带分数的 ID 列表,配合 Pipeline 批量获取详情
  • 关联查询:避免缓存 JOIN 结果,改用 ID 列表 + 批量查询
示例:分页缓存
// 缓存用户ID列表(按注册时间排序) redisTemplate.opsForZSet().add("user:by_register_time", userId, timestamp);// 分页获取Set<String> userIds = redisTemplate.opsForZSet().reverseRange("user:by_register_time", offset, offset + limit -1);// 批量获取用户详情(使用 Pipeline)List<User> users = userIds.stream().map(id ->getUserById(Long.valueOf(id)))// 内部走缓存.collect(Collectors.toList());

监控与运维:让缓存可观测 📈

没有监控的缓存如同盲人摸象。关键指标包括:

  • 缓存命中率hit_rate = hits / (hits + misses),目标 >95%
  • 缓存大小与内存使用
  • 慢查询日志
  • TTL 分布

Spring Boot Actuator + Micrometer

@ComponentpublicclassCacheMetrics{privatefinalCounter cacheHitCounter;privatefinalCounter cacheMissCounter;publicCacheMetrics(MeterRegistry registry){this.cacheHitCounter =Counter.builder("cache.hits").tag("cache","user").register(registry);this.cacheMissCounter =Counter.builder("cache.misses").tag("cache","user").register(registry);}publicUsergetUserById(Long id){// ... logic ...if(cached !=null){ cacheHitCounter.increment();}else{ cacheMissCounter.increment();}return user;}}

通过 /actuator/metrics 可查看指标,并集成 Prometheus + Grafana 可视化。

🔗 参考 Micrometer 官方文档

高级技巧:缓存预热与懒加载 🌱

缓存预热(Cache Warming)

在系统启动或低峰期,主动加载热点数据到缓存,避免冷启动时大量穿透。

@PostConstructpublicvoidwarmUpCache(){List<Long> hotUserIds =getHotUserIds();// 从配置或分析得出 hotUserIds.parallelStream().forEach(id ->getUserById(id));}

懒加载 + 异步刷新

对非核心数据,可采用“懒加载 + 后台刷新”策略:

  • 首次访问时加载
  • 后台定时任务刷新缓存,避免用户感知延迟
@Scheduled(fixedRate =300_000)// 每5分钟publicvoidrefreshHotUsers(){List<User> hotUsers = userRepository.findHotUsers();for(User user : hotUsers){String key =USER_CACHE_PREFIX+ user.getId(); redisTemplate.opsForValue().set(key, user,Duration.ofMinutes(15));}}

安全与资源管理 🔐

Redis 安全配置

  • 启用密码认证(requirepass
  • 限制 IP 访问(防火墙)
  • 禁用危险命令(rename-command FLUSHDB ""
  • 使用 TLS 加密连接

内存淘汰策略

当 Redis 内存不足时,需配置合适的淘汰策略:

  • allkeys-lru:所有 key LRU 淘汰(推荐)
  • volatile-lru:仅带 TTL 的 key LRU 淘汰
  • noeviction:不淘汰,写入报错

配置示例(redis.conf):

maxmemory 4gb maxmemory-policy allkeys-lru 

总结:最佳实践清单 ✅

  1. 优先使用 Cache-Aside 模式,写操作“先更新 DB,再删缓存”
  2. 缓存空值 防止穿透,随机 TTL 防止雪崩,互斥锁 防止击穿
  3. 序列化选择 JSON,兼顾可读性与通用性
  4. 监控命中率,目标 >95%
  5. 避免大 Key / 热 Key,合理分片
  6. 使用 Pipeline / 批量操作 提升吞吐
  7. 缓存与 DB 保持最终一致性,必要时引入消息队列
  8. 预热 + 异步刷新 提升用户体验

结语 🌟

PostgreSQL 与 Redis 的结合,是构建高性能 Web 应用的经典组合。缓存不是银弹,但合理使用能带来数量级的性能提升。关键在于理解业务场景、权衡一致性与性能、并持续监控优化。

正如数据库大师 Michael Stonebraker 所言:“One size never fits all.” 缓存策略亦如此——没有放之四海而皆准的方案,只有最适合你业务的实践。

希望本文能为你在缓存之路上提供清晰的指引。Happy coding! 💻✨

🔗 延伸阅读:Redis 官方最佳实践PostgreSQL 性能调优指南Designing Data-Intensive Applications(《数据密集型应用系统设计》)

🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨

Read more

小米 “养龙虾”:手机 Agent 落地,智能家居十年困局被撬开

小米 “养龙虾”:手机 Agent 落地,智能家居十年困局被撬开

3月6日,小米正式推出国内首个手机端类 OpenClaw Agent 应用 ——Xiaomi miclaw,开启小范围邀请封测。这款被行业与网友戏称为小米 “开养龙虾” 的新品,绝非大模型浪潮下又一款语音助手的常规升级,而是基于自研 MiMo 大模型、具备系统级权限、全场景上下文理解能力的端侧智能体。 作为深耕智能家居领域的行业媒体,《智哪儿》始终认为:智能家居行业过去十年的迭代,始终没能跳出 “被动执行” 的底层困局。而 miclaw 的落地,不止是小米在端侧 AI 赛道的关键落子,更是为整个智能家居行业的底层逻辑重构,提供了可落地的参考范本。需要清醒认知的是,目前该产品仍处于小范围封测阶段,复杂场景执行成功率、端侧功耗表现、第三方生态适配进度等核心体验,仍有待大规模用户实测验证。本文将结合具象场景、量化数据与多维度视角,客观拆解 miclaw 的突破价值、现实挑战,以及它对智能家居行业的长期影响。 01 复盘行业困局:智能家居十年 始终困在 “被动执行”

By Ne0inhk
Flutter 三方库 discord_interactions 的鸿蒙化适配指南 - 在 OpenHarmony 打造高效的社交机器人交互底座

Flutter 三方库 discord_interactions 的鸿蒙化适配指南 - 在 OpenHarmony 打造高效的社交机器人交互底座

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 discord_interactions 的鸿蒙化适配指南 - 在 OpenHarmony 打造高效的社交机器人交互底座 在现代社交应用与办公协同工具的开发中,集成强大的机器人(Bot)交互能力是提升活跃度的关键。discord_interactions 库为 Flutter 开发者提供了一套完整的、遵循 Discord 官方协议的交互模型,涵盖了从 Slash Commands(斜杠命令)到 Webhook 签名验证的核心功能。本文将深入解析如何在 OpenHarmony(鸿蒙)环境下,结合鸿蒙的安全机制与网络特性,完美适配 discord_interactions 到你的鸿蒙应用中。 前言 随着鸿蒙系统(HarmonyOS)进入原生应用开发的新纪元,跨平台社交工具的适配需求日益增长。discord_interactions 作为一个纯

By Ne0inhk

Flutter 三方库 eip55 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、符合 Web3 标准的以太坊地址校验与防串改引擎

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 eip55 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、符合 Web3 标准的以太坊地址校验与防串改引擎 在鸿蒙(OpenHarmony)系统的区块链钱包应用、数字资产管理工具(如鸿蒙版 NFT 浏览器)或需要处理加密货币转账的场景中,如何确保用户输入的以太坊(Ethereum)地址既符合基本格式,又通过了大小写混合的校验和(Checksum)验证,防止因为单个字符手误导致的资产永久丢失?eip55 为开发者提供了一套工业级的、基于 EIP-55 提案的地址转换与验证方案。本文将深入实战其在鸿蒙 Web3 安全基座中的应用。 前言 什么是 EIP-55?它是由以太坊创始人 Vitalik Buterin 提出的地址校验和提案。通过在地址字符串中引入特定的。大小写混合模式(基于 Keccak-256 哈希)

By Ne0inhk
【CANN】Pi0机器人大模型 × 昇腾A2 测评

【CANN】Pi0机器人大模型 × 昇腾A2 测评

【CANN】Pi0机器人大模型 × 昇腾A2 测评 * 写在最前面 🌈你好呀!我是 是Yu欸🚀 感谢你的陪伴与支持~ 欢迎添加文末好友🌌 在所有感兴趣的领域扩展知识,不定期掉落福利资讯(*^▽^*) 写在最前面 版权声明:本文为原创,遵循 CC 4.0 BY-SA 协议。转载请注明出处。 Pi0机器人VLA大模型测评 哈喽大家好呀!我是 是Yu欸。 最近人形机器人和具身智能真的太火了,大家都在聊 Pi0、聊 VLA 大模型。但是,兄弟们,不管是搞科研还是做落地,咱们始终绕不开一个问题——算力。 今天,我们一起把当下最火的 Pi0 机器人视觉-语言-动作大模型,完完整整地部署在国产算力平台上,也就是华为的昇腾 Atlas 800I A2 服务器上。 在跑通仓库模型的基础上,我们做一次性能测评。 我们要测三个最核心的指标:

By Ne0inhk