RabbitMQ 消息可靠性保障

消息可靠性保障:三道防线全覆盖 📖 前置阅读:本文假设读者已掌握 SpringBoot RabbitMQ 的基本操作(RabbitTemplate 发送、@RabbitListener 消费、手动 ACK)。如果还不熟悉,建议先阅读 SpringBoot RabbitMQ 全操作指南。 一、⚡ 问题切入:消息去哪儿了? 先看一段日常的订单处理代码: // 下单成功后发消息 @Service public class OrderService { @Transactional public void createOrder(OrderRequest req) { orderMapper.insert(req.toOrder()); // 1. 写 MySQL rabbitTemplate.convertAndSend( // 2. 发消息 "order.exchange", "order.created", req); } } 表面看起来没问题。但消息真的被消费了吗?在以下任何一个环节都可能丢: Producer → [网络] → RabbitMQ → [网络] → Consumer ① 发送丢失 ② Broker 宕机丢失 ③ 消费失败丢失 环节 丢失原因 后果 ① 生产者 → Broker 网络断连、Exchange 不存在、消息路由失败 消息根本没进队列 ② Broker 存储 RabbitMQ 进程崩溃、服务器断电 内存中的消息全部丢失 ③ Consumer 消费 消费者处理到一半挂了、代码异常没 ACK 消息被取走但实际没处理完 这三个环节必须逐一设防——RabbitMQ 提供了完整的机制,但需要生产者、Broker、消费者三端配合。 ...

十一月 4, 2022 · 8 分钟 · 1512 字 · yaomingye

SpringBoot RabbitMQ 全操作指南

SpringBoot 集成 RabbitMQ:从发送到消费 📖 前置阅读:本文假设读者已理解 RabbitMQ 的核心概念(Exchange、Queue、Binding、RoutingKey)和四种交换机类型。如果还不熟悉,建议先阅读前两篇: RabbitMQ 核心概念与 AMQP 协议 交换机类型完全指南 Part 1:概念与前置 1.1 本文目标 前两篇用 RabbitMQ 原生 Java Client 写了所有代码——channel.basicPublish、channel.basicConsume、手动 basicAck。理解底层是正确的,但真正进项目时,Spring AMQP 帮我们做了 90% 的重复工作。 读完这篇会掌握: 用 RabbitTemplate 一行代码发消息(替代 channel.basicPublish 那一大堆) 用 @RabbitListener 注解收消息(替代手动 basicConsume + DeliverCallback) 用 Jackson2JsonMessageConverter 自动序列化/反序列化 Java 对象 用 @Bean + 声明式配置 管理 Exchange/Queue/Binding(替代每次启动时 channel.exchangeDeclare) 三种交换机在 Spring 中的完整示例代码(Direct / Fanout / Topic) 手动 ACK 的配置和坑 1.2 前置条件 前置项 具体要求 验证命令 JDK 17+(8+ 也兼容) java -version Maven 3.6+ mvn -v SpringBoot 3.x(文中用 3.2) mvn dependency:tree | grep spring-boot RabbitMQ 3.12+(management 版) docker ps | grep rabbitmq 前置知识 前两篇的 Exchange/Queue/Binding/RoutingKey 概念 — 确认 RabbitMQ 在跑: ...

十一月 3, 2022 · 13 分钟 · 2677 字 · yaomingye

RabbitMQ 交换机类型完全指南

四种交换机:路由机制完全解析 📖 前置阅读:本文假设读者已理解上一篇中 Exchange、Queue、Binding、RoutingKey 的概念。如果还不清楚,建议先阅读 RabbitMQ 核心概念与 AMQP 协议。 一、⚡ 问题切入:同一条消息,为什么有人收到有人收不到? 上一篇结尾发了第一条 RabbitMQ 消息——消息发出去,消费者收到了。但实际业务远比这个复杂: 订单创建后,所有下游服务(短信、邮件、风控、日志)都要收到通知 商品价格变更后,只有关注了这个商品的搜索服务需要重建索引 用户行为日志中,一部分是购买行为(需要发优惠券),一部分是浏览行为(只需要统计) 这些需求的本质是路由——同一批消息,不同消费者按不同规则接收不同子集。RabbitMQ 用 Exchange(交换机)来承担这个角色。 Exchange 有四种类型。它们唯一的不同是如何匹配 RoutingKey 和 BindingKey: flowchart TD classDef startEnd fill:#701a4c,stroke:#e11d48,stroke-width:2px,color:#fce7f3,font-weight:bold; classDef condition fill:#2a1147,stroke:#a855f7,stroke-width:1.5px,color:#ede9fe,font-weight:bold; classDef process fill:#1e1e24,stroke:#6b7280,stroke-width:1.5px,color:#e5e7eb; classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold; classDef highlight fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold; P([Producer\n消息 + RoutingKey]) --> EX{Exchange 类型?} EX -->|"Direct"| D[精确匹配\nRoutingKey == BindingKey] EX -->|"Fanout"| F[忽略 RoutingKey\n广播所有绑定队列] EX -->|"Topic"| T[通配符匹配\n* 单段 / # 多段] EX -->|"Headers"| H[消息头属性匹配\nx-match: all / any] class P startEnd; class EX condition; class D,F,T,H highlight; 去管理界面 Exchanges 页面点开一个 Exchange,看到 type 字段的值就是这四种之一。 ...

十一月 2, 2022 · 8 分钟 · 1549 字 · yaomingye

RabbitMQ 核心概念与 AMQP 协议

核心概念与 AMQP 协议 一、⚡ 问题切入:同步处理为什么不行? 先看一个电商系统里最常见的下单流程: @Service public class OrderService { @Transactional public Order createOrder(CreateOrderRequest request) { // 1. 扣减库存 inventoryService.deduct(request.getProductId(), request.getQuantity()); // 2. 创建订单 Order order = orderMapper.insert(request); // 3. 发送下单成功短信——这一步是同步的 smsService.sendOrderConfirm(request.getUserId(), order.getId()); // 4. 发送下单成功邮件——这一步也是同步的 emailService.sendOrderConfirm(request.getUserId(), order.getId()); // 5. 写入操作日志 operationLogService.record("CREATE_ORDER", order.getId()); return order; } } 一次下单请求,用户要等库存扣减、订单入库、短信发送、邮件发送、日志写入全部完成才能收到响应。短信调用运营商接口,邮件走 SMTP,日志写入数据库——这三步加起来可能要 500ms ~ 2s。用户在前端点完"提交订单"后盯着屏幕转圈,体验糟糕。 有人会说:“那简单,开个线程异步执行不就行了?” // 线程池异步——似乎解决了问题 executorService.submit(() -> smsService.sendOrderConfirm(userId, orderId)); executorService.submit(() -> emailService.sendOrderConfirm(userId, orderId)); 但这引入了一连串新问题: ...

十一月 1, 2022 · 7 分钟 · 1466 字 · yaomingye

Redis + Caffeine 双层缓存:降级与容错

Redis + Caffeine 双层缓存 📖 前置阅读:本文是缓存架构的进阶级文章,假设读者已经掌握了 Redis 的基础操作和 Caffeine 本地缓存的 API。如果还不熟悉,建议先阅读: SpringBoot Redis 全操作指南 —— Redis 实战篇 Caffeine 核心与 SpringBoot 集成 —— Caffeine 入门篇 一、⚡ 问题切入:凌晨三点,Redis 挂了 凌晨三点,Redis 内存用满——大量的 TTL 同时到期 + 新一波定时任务写入,导致内存 OOM,Redis 进程被系统 kill。你的服务所有缓存请求全部报错,瞬间全部穿透到 MySQL,数据库连接池耗尽,整个系统不可用。 值班群炸了。你翻日志发现——服务启动时所有 @Cacheable 都配置了 Redis,Redis 一挂连个兜底的都没有。 Redis 是高可用的——有哨兵(Sentinel)、有集群(Cluster),官方说可用性能到 99.99%。但 99.99% 意味着一年有将近 1 小时的不可用时间。这 1 小时如果发生在双十一,后果就不是"维护了一次",而是"事故"。 本地缓存的价值不只是"快",更是 Redis 挂了时的最后一道防线。就算 Redis 是全宇宙最高可用的服务,网络也可能抖——交换机故障、机房间专线断掉、Kubernetes 网络策略变更——这些事情的发生概率比 Redis 自身故障高得多。 本篇要解决的问题:构建 Redis(远程)+ Caffeine(本地)双层缓存架构,把 Redis 的不可用当成"迟早会发生的事"来设计,而不是寄望于它不会发生。 📌 真实场景:数据字典——最简单的双层缓存 在进入复杂架构之前,先看一个真实项目里怎么用 Spring Cache + Caffeine + Redis 做双层缓存。不是所有场景都需要 200 行的 TieredCacheManager——有时候 3 行配置 + 1 个注解就够了。 ...

十月 31, 2022 · 11 分钟 · 2341 字 · yaomingye

Caffeine 本地缓存核心与 SpringBoot 集成

Caffeine 核心与 SpringBoot 集成 📖 前置阅读:本文假设读者已了解 Redis 基本操作和 Spring Cache 注解(@Cacheable/@CachePut/@CacheEvict)。如果还不熟悉 Redis 系列,建议先阅读 SpringBoot Redis 全操作指南。 一、⚡ 问题切入:Redis 再快也是远程调用 回顾一下,一个典型的 Redis 缓存查询是: // Redis 缓存读 User user = (User) redisTemplate.opsForValue().get("user:1001"); if (user != null) return user; // 缓存未命中,查 MySQL user = userMapper.selectById(1001L); redisTemplate.opsForValue().set("user:1001", user, 30, TimeUnit.MINUTES); return user; Redis 延迟一般在 0.5ms ~ 2ms——相比 MySQL 的 3ms ~ 10ms 已经快很多了。但这个延迟不是免费的:每次 Redis 查询都是一次网络往返(RTT)。同机房内 RTT 大约 0.1ms,跨机房可能到 2ms 甚至更久。 在高 QPS 下,这 0.5ms × 10 万次查询 = 50 秒的累计时间,还不算序列化/反序列化的 CPU 开销。而且 Redis 不是永远不会挂——网络抖动、内存满了、主从切换,任何一个都可能让 Redis 临时不可用。 ...

十月 30, 2022 · 6 分钟 · 1073 字 · yaomingye

MongoDB 索引、事务与性能调优

生产落地的三件套:索引、事务与调优 📖 前置阅读:本文是 MongoDB 系列的生产调优篇,假设读者已经掌握了 MongoDB 文档模型、SpringBoot 操作和聚合管道。如果还没有,建议先阅读前三篇: MongoDB 核心概念:文档模型、BSON 与查询操作符全解析 —— 介绍篇 SpringBoot MongoDB 全操作指南 —— 实战篇 MongoDB 聚合管道深入 —— 进阶篇 一、⚡ 问题切入:查询怎么越来越慢了? 用户表单数据从 10 万增长到 500 万,“按手机号查询表单记录"从 5ms 变成了 800ms。你用 explain() 一看——stage: "COLLSCAN",全表扫描。 这场景和 MySQL 一样——数据量大了没建索引,写什么数据库都快不了。但 MongoDB 的索引有一些 MySQL 没有的类型,还有 Schema 设计的决策(嵌入还是引用?要不要用事务?)直接影响性能。 本篇要解决的问题:怎么设计索引、怎么看懂 explain、怎么选嵌入还是引用、什么时候用事务——以及遇到性能问题时从哪下手排查。 二、📊 索引类型与策略 2.1 单字段索引与复合索引 底子和 MySQL 一样——B-Tree。创建一个索引语法几乎一样: // 单字段索引 db.users.createIndex({ email: 1 }) // 1 = 升序,-1 = 降序(单字段索引中不重要) // 复合索引(多个字段组合) db.orders.createIndex({ userId: 1, createTime: -1 }) // 查看所有索引 db.orders.getIndexes() 复合索引的 ESR 规则——这是 MongoDB 复合索引最重要的设计原则: ...

十月 29, 2022 · 6 分钟 · 1237 字 · yaomingye

MongoDB 聚合管道深入

聚合管道实战:从入门到精通 📖 前置阅读:本文是 MongoDB 系列的进阶篇,假设读者已经掌握了 MongoDB 文档模型和 SpringBoot 基本操作。如果还没有,建议先阅读前两篇: MongoDB 核心概念:文档模型、BSON 与查询操作符全解析 —— 介绍篇 SpringBoot MongoDB 全操作指南 —— 实战篇 一、⚡ 问题切入:查询能查出结果,但分析不出来 先看一个业务需求。你有一个电商订单 Collection: // 一条订单文档 { _id: ObjectId("..."), userId: 1001, total: NumberDecimal("6999.00"), status: "paid", items: [ { productName: "华为Mate60 Pro", category: "手机", price: NumberDecimal("6999.00"), quantity: 1 } ], createTime: ISODate("2024-01-15T10:30:00Z") } 产品经理要你给出以下数据: 每个用户的总消费金额 每月订单量趋势 销量最高的 10 个商品分类 每个用户的平均客单价 哪些商品经常一起购买(关联分析) 用 find 可以查出原始数据,但统计计算全得拉到 Java 内存里自己算——5 万条订单光加载到内存就需要 2 秒,再算就 5 秒起步。 这就是聚合管道的用武之地——把计算下推到 MongoDB 服务器端完成,只返回结果,不传原始数据。 二、🧱 聚合管道是什么 2.1 核心概念 聚合管道(Aggregation Pipeline) 是一组按顺序执行的数据处理阶段(Stage)。每个 Stage 接收上一阶段的输出,做一次数据变换,把结果传给下一阶段。 flowchart LR classDef stage fill:#450a0a,stroke:#dc2626,stroke-width:1.5px,color:#fecaca,font-weight:bold; classDef data fill:#052e16,stroke:#16a34a,stroke-width:1.5px,color:#bbf7d0,font-weight:bold; classDef result fill:#0f172a,stroke:#3b82f6,stroke-width:2px,color:#bfdbfe,font-weight:bold; COLLECTION[(orders\n50000 文档)] COLLECTION --> S1[$match\n过滤: status=paid] S1 --> S2[$group\n分组: 按 userId] S2 --> S3[$sort\n排序: 总金额降序] S3 --> S4[$limit\n取前 10] S4 --> RESULT([10 条聚合结果]) class S1,S2,S3,S4 stage; class COLLECTION data; class RESULT result; 关键点: ...

十月 28, 2022 · 8 分钟 · 1662 字 · yaomingye

SpringBoot MongoDB 全操作指南

SpringBoot 集成 MongoDB:CRUD 到聚合全操作 📖 前置阅读:本文假设读者已了解 MongoDB 的文档模型、BSON 数据类型和 mongosh 基础操作。如果还不熟悉,建议先阅读 MongoDB 核心概念:文档模型、BSON 与查询操作符全解析。 本文按照"先搞懂操作 → 教程版完整实现 → 生产版架构模式 → 验证排错“的顺序组织。如果你只想快速上手 MongoTemplate 的 CRUD,读完 Part 1 后直接看 Part 2 即可;如果你想理解真实项目中 MongoDB 是怎么用的,需要完整读完。 Part 1:先搞懂要做什么 一、目标说明 这篇文章的目标:让读者在一篇文章内学会 SpringBoot 项目中所有常用的 MongoDB 操作,读完就能直接写到项目里。 具体来说,读完这篇文章会掌握: 用 @Document / @Id / @Field 注解定义 MongoDB 文档映射 用 MongoTemplate 执行 CRUD、复杂查询、更新操作 用 MongoRepository 做声明式查询(方法命名 + @Query) 聚合管道的 Java 写法初探 一个完整的"用户自定义表单"功能从零到一的实现 二、前置条件 前置项 具体要求 验证命令 JDK 17+(文中用 17,8+ 均兼容) java -version Maven 3.6+ mvn -v SpringBoot 3.x(文中用 3.2.0) mvn dependency:tree | grep spring-boot MongoDB 7.0(6.x 也兼容文中所有操作) mongosh --eval "db.version()" 前置知识 SpringBoot 基础、MongoDB 核心概念(第一篇) — Part 2:教程版 —— 从零掌握 MongoDB 全部操作 下面每一节都给出了完整的、可运行的代码。整个教程版使用同一个技术栈:Spring Boot 3.x + spring-boot-starter-data-mongodb,所有操作通过 MongoTemplate 和 MongoRepository 完成。 ...

十月 27, 2022 · 11 分钟 · 2206 字 · yaomingye

MongoDB 核心概念

MongoDB 核心概念:文档模型、BSON 与查询操作符全解析 一、⚡ 问题切入:MySQL 为什么不适合这个场景? 先看一个典型的系统设计需求。你正在开发一个 SaaS 平台的"用户自定义表单"功能——每个客户可以自己创建表单,定义不同的字段: 客户A:报名表 → 姓名、手机号、紧急联系人姓名、紧急联系人电话、是否过敏(是/否) 客户B:问卷表 → 昵称、年龄、兴趣爱好(多选)、详细简历(文本)、作品链接 客户C:订单表 → 商品名、单价、数量、收货地址(省/市/区/详细)、发票抬头、纳税人识别号 用 MySQL 做这件事,摆在面前的有三条路: 方案一:一张宽表 CREATE TABLE form_data ( id BIGINT PRIMARY KEY, field_1 VARCHAR(200), -- 姓名/昵称/商品名 field_2 VARCHAR(200), -- 手机号/年龄/单价 field_3 VARCHAR(200), -- 紧急联系人/兴趣爱好/数量 -- ... 预留 50 个字段 field_50 VARCHAR(200) ); 客户A的"紧急联系人电话"存在 field_4,客户B的"作品链接"存在 field_5,客户C的"纳税人识别号"存在 field_7。字段名没有任何业务含义,查询时只能对着文档翻"field_4 到底存了什么"。SQL 写成: SELECT * FROM form_data WHERE field_1 = '张三' AND field_2 = '13800000000'; 这已经不是在写代码了,是在玩解谜游戏。 ...

十月 26, 2022 · 8 分钟 · 1575 字 · yaomingye
Cat Radio