公司的后台管理有一个搜索功能,搜订单备注里的关键词。刚开始数据量小,MySQL 的 LIKE '%keyword%' 还能跑。后来订单涨到几百万条,一个模糊搜索要跑 8 秒。更倒霉的是,这个搜索请求直接打在主库上——索引走不了,全表扫描,CPU 飙到 90%,其他正常业务也跟着慢。 LIKE '%xxx%' 是数据库的噩梦。前导通配符导致索引完全失效,只能全表扫描。MySQL 虽然也有 FULLTEXT 索引,但中文分词烂、不支持复杂排序、而且仍然在主库上消耗资源。 今天聊聊怎么用 Canal 把 MySQL 的数据实时同步到 Elasticsearch,把搜索的活从主库彻底拆出来。 架构:主库只管写,搜索交给 ES MySQL(主库) Elasticsearch │ │ │ INSERT/UPDATE/DELETE │ ├──── Canal 监听 binlog ──────────→├─ 增量同步 │ │ │ 业务读写 │ 全文搜索 │ (走主库) │ (走 ES) Canal 伪装成 MySQL 的 Slave,订阅 binlog 的变更事件。任何 INSERT、UPDAT....
数据库连接池突发流量防护:Wait Timeout 导致请求堆积?动态扩容 + 排队熔断策略!
朋友公司大促期间,数据库连接池突然打满。所有需要数据库的请求都排队等连接,connection-timeout 设了 30 秒——等了 30 秒拿到连接的人还得用,30 秒拿不到的人直接报错。问题是那些等了 28 秒终于拿到连接的人,数据库早已经被前面的人压垮了,拿到连接也没用。400 个请求同时排队,300 个超时报错,100 个拿到连接但数据库响应时间从 5ms 飚到 500ms。 这不是数据库的问题,是连接池在突发流量面前没有自保能力。今天聊聊怎么用动态扩容和排队熔断,让连接池在洪峰到来时不崩溃。 问题:连接池是漏斗,不是水坝 连接池的设计初衷是"复用连接,避免频繁建连"。但它有一个致命假设:总有空闲连接可用。 突发流量一来,连接瞬间打满,后续请求只能排队。排队的请求占着 Tomcat 线程,Tomcat 线程也满了——连锁反应。 请求 → 连接池 → 没空连接 → 排队等 ↓ 排队占着 Tomcat 线程 ↓ Tomcat 线程池也满了 ↓ 新请求直接被拒 ↓ 整个服务不可用 连接池从"漏斗"变成了"堵点"。 方案一:动态扩容,峰值时多借几个连接 HikariCP 的 ....
Redis 内存碎片化治理:used_memory 低却 OOM?内存碎片整理 + 对象池优化
朋友公司的 Redis 集群监控上 used_memory 才 4GB,但 used_memory_rss 已经到了 8GB——是 used 的两倍。运维加了内存,过两个月又一样。最离谱的是有一次 used_memory 只有 2GB,Redis 却报了 OOM。排查发现内存碎片率 3.5,8GB 的物理内存被碎片占了一半多。 这就是 Redis 的内存碎片问题。used_memory 是你存进去的有用数据,used_memory_rss 是 Redis 实际向操作系统申请的内存。两者之间的差,就是碎片。 今天聊聊怎么用 Redis 自带的内存碎片整理和对象复用,把碎片率降到 1.1 以下。 碎片怎么来的 Redis 的内存分配是 jemalloc 管理的。当你反复写入和删除不同大小的 Key,就会出现这样的场景: 内存布局(简化): |AAAA| 空闲 |BB| 空闲 |CCCC| 空闲 |DD| 空闲 |... 删掉的 Key 留下的空隙不够大,放不下新的 Key。新 Key 只能往后申请新内存。结果就是 used 没涨多少,rss 一直涨。 碎片率计算公式: mem_fra....
跨库事务死锁自动恢复:锁等待超时回滚失败?分布式死锁检测 + 智能重试机制!
朋友公司的订单系统连了两个库——订单库和库存库。一笔订单创建需要在订单库 INSERT、在库存库 UPDATE,同一个事务里跨两库操作。高峰期出现了死锁:事务 A 锁了订单表的行等库存表的锁,事务 B 锁了库存表的行等订单表的锁。两个事务互相等待,40 秒后被 MySQL 的 innodb_lock_wait_timeout 强杀。问题是强杀后抛出的异常只说了"锁等待超时",没说是谁锁了谁——运维排查不到哪个事务导致了死锁。 单库死锁 MySQL 自己能检测和回滚,跨库死锁 MySQL 管不了——因为在它看来就是两个独立的事务各等各的。今天聊聊怎么在应用层做跨库死锁检测,并配合智能重试自动恢复。 跨库死锁是怎么发生的 单库死锁 MySQL 的 InnoDB 引擎自己就能处理:检测到环 → 选一个代价最小的回滚。但跨两个库的时候,场景是这样的: 事务 A: 事务 B: BEGIN BEGIN UPDATE orders SET ... UPDATE inventory SET ... WHERE id=1001 WHERE sku='XYZ' (锁住订单库的行) (锁住库存库的行) ....
分布式事务重试幂等控制:网络抖动导致重复扣款?业务流水号 + 状态机拦截!
公司的支付系统出了个大事故。一笔订单因为网络抖动,支付回调超时了,系统重试了扣款。结果扣了两笔——用户投诉,财务对账发现了差异,运营逐笔人工退款。查日志发现,不是网络不通,是"半通"——第一次扣款请求发出去了,银联处理成功了,但响应在回来的路上丢了。系统认为扣款失败,又发了一次。 这种"网络半通"是分布式系统里最危险的情况——操作已经执行了,但调用方不知道。不加重试怕丢,加多了怕重。今天聊聊怎么用业务流水号加状态机,让重试在"安全"的边界内进行。 问题的本质:操作不是天然幂等的 HTTP 的 POST 不是幂等的,数据库的 INSERT 不是幂等的,银行的扣款接口更不是幂等的。如果不做任何处理,同一个扣款请求发两次,钱就会扣两笔。 分布式系统里有太多场景需要重试——网络抖动、超时、服务暂时不可用。重试本身是合理的,问题在于你拿什么来判断"这次重试的请求,上一次有没有已经成功过了"。 答案就是业务流水号。不是数据库自增 ID,不是 UUID,而是一个由业务方生成、全链路唯一、可用来判断"这条请求我见没见过"的标识。 业务流水号:请求的唯一身份证 业务流水号的核心原则:由发起方生成,....
本地消息表性能瓶颈:高频写入拖垮主库?异步批量落盘 + 分表策略优化!
公司用本地消息表做分布式事务的最终一致性。每笔订单创建后往消息表里插一条"待发送"记录,后台定时任务轮询发送。业务量上来以后,消息表单表积压了几千万条数据,每次 INSERT 都要等几十毫秒,DB 连接池打满,主库 CPU 飙到 80%。订单创建都跟着变慢了。 本地消息表本来是为了解耦,结果自己成了瓶颈。今天聊聊怎么把消息表的高频写入从主库的业务路径上剥离,用异步批量落盘加分表来扛住海量消息。 问题到底出在哪 本地消息表最朴素的实现是业务代码里直接 INSERT: @Transactional public void createOrder(Order order) { orderMapper.insert(order); // 订单入库 messageMapper.insert(new Message( // 消息入库 —— 跟订单在同一个事务 order.getId(), "ORDER_CREATED", "PENDING" )); } 这种写法,消息的 INSERT 跟业务 SQL 在同一个事务里。单看一条没问题,但当每秒几千个订单同时创建,消息表的 INSERT 就变成了主....
Spring Cloud Gateway 动态路由热更新零宕机:配置下发瞬间 502?双缓冲路由表 + 健康检查切换!
公司用 Nacos 做配置中心,Gateway 路由也放 Nacos 里动态刷新。运维改了一条路由规则,配置一刷新,Gateway 就开始间歇性 502——大概持续了 2 到 3 秒。排查发现是路由表在热更新的时候,旧的被清空了、新的还没加载完,中间有一个空窗期。请求进来找不到路由,全部 502。 热更新本来是为了不重启,结果还是搞出了服务中断。问题不在热更新本身,而在更新的姿势不对——路由表是"先清空再加载"而不是"先准备好再切换"。今天聊聊怎么用双缓冲让路由切换做到真正的零宕机。 问题出在哪里:单路由表的空窗期 Spring Cloud Gateway 的 RouteDefinitionRouteLocator 在接收到 Nacos 配置变更时,默认行为是: 收到新配置 │ ├─ 清空旧路由表 ├─ 逐条解析新路由 ├─ 校验路由合法性 └─ 加载完成 从"清空"到"加载完成"之间,路由表是空的。这段时间进来的请求,全部 502。加载几十条路由可能只要 100ms,但如果路由规则复杂、或者有外部依赖校验,这个窗口可能拉到秒级。 双缓冲:新路由加载完再切过去 双缓冲的思路很朴素....
网关全局请求 ID 生成:日志排查靠猜?X-Request-ID 统一注入 + 全链路透传!
公司线上出了个支付异常,三四个微服务都有日志,但各打各的,谁也不知道哪条日志和哪条日志是同一个请求。运维在 ELK 里翻了一个小时,靠着时间戳和 userId 硬猜,最后猜错了版本,回滚到错误的代码上又出了一波事故。要是每条日志头上都有一个贯穿全链路的请求 ID,一秒就能搜出这个请求的完整轨迹。 X-Request-ID 不是什么新技术,但落地起来细节不少。谁生成、怎么传、下游怎么接、日志怎么打,任何一个环节断了,链路就断了。今天把这些细节串起来。 谁负责生成 Request ID 生成责任只有一个:第一个接收到请求的网关。 客户端不生成,因为你不能信任客户端传上来的值——重复、太短、甚至包含注入字符。 如果客户端已经传了 X-Request-ID(比如从另一个系统透传过来的),网关应当尊重它,直接转发。但如果客户端没传,网关必须生成一个。 请求到达 Gateway │ ├─ 检查 Header: X-Request-ID │ ├─ 有值 → 直接用(跨系统透传) │ └─ 没值 → 生成新 ID │ └─ 写入 Header → 转发到下游微服务 ID 格式建议用 UUID 去横....
Spring Cloud Gateway 路由节点健康检查盲区:实例假在线导致 502?主动探测 + 权重动态剔除!
公司微服务上了 Spring Cloud Gateway,一切正常直到有一天运维发现某个下游服务挂了,Gateway 还是把流量往那台死掉的实例上发,前端一阵 502。查了注册中心,Nacos 上那台实例的状态还是 UP。原来是服务进程虽然活着,但业务线程池被耗尽了,所有请求都在排队超时。Nacos 的心跳包是单独的线程处理的,业务线程池死了不影���心跳,所以注册中心一直认为它是健康的。 这就是"假在线"——健康检查过了不代表服务能用。今天聊聊怎么在 Gateway 层做主动业务探测,把假在线的节点从路由表里动态踢出去。 注册中心健康检查的盲区 Nacos、Eureka 这些注册中心判断服务是否健康,靠的是心跳。客户端定期发一个心跳包给注册中心,注册中心收到了就认为服务活着。 但心跳包只能证明网络没断、进程没死。以下场景心跳都是正常的: 业务线程池打满,所有请求都在排队超时 数据库连接池耗尽,每次数据库查询都失败 依赖的下游全挂了,返回的全是 500 死锁,只有心跳线程还在工作 这些情况下,注册中心说服务是 UP,Gateway 就把流量发过去,结果全是 502 或 500。 ....
RabbitMQ 消费者内存暴涨防护:未 ACK 消息堆积撑爆 JVM?Prefetch 限制 + 自动降级策略!
隔壁组一个 RabbitMQ 消费者服务,每隔几天就 OOM 重启一次。查了 heap dump,发现 Delivery 对象占了 80% 的堆内存——全是未 ACK 的消息。根因是他们用的是默认的自动 ACK,后来改成了手动 ACK 防止消息丢失,但忘了设 prefetch。RabbitMQ 一股脑把所有消息都推给了 Consumer,Consumer 处理不过来,消息在堆内存里越堆越多,直到撑爆。 RabbitMQ 的 Push 模式有个很容易忽略的坑:如果你不设 prefetch,Broker 会用最快的速度把所有消息推给 Consumer。 Consumer 处理慢了,消息就在堆内存里排队等处理。每条消息至少占几百字节,10 万条消息就是几十 MB,轻松打满 JVM。 今天聊聊怎么用 prefetch 限流和自动降级,让 Consumer 不会把自己撑死。 问题出在哪:Push 模式的无限制投递 RabbitMQ 默认的行为是:Consumer 一连接上,Broker 就把当前队列里所有的消息一口气推过去。Consumer 处理不过来没关系——消息已经在你的 JVM 堆里了。....
WebSocket 优雅停机与连接迁移:服务发版用户频繁掉线?平滑过渡 + 状态保持方案!
公司的在线客服系统用的是 WebSocket。每次发版滚动更新,旧 Pod 一停,挂在上面的几千个 WebSocket 连接全断。前端虽然做了自动重连,但重连期间用户发了一条消息,没收到回复,以为客服不理他,直接给了差评。更糟糕的是,客服正在输入的内容也丢了——WebSocket 断了,会话状态没了。 WebSocket 跟 HTTP 不一样。HTTP 是无状态的,请求断了重试一次就好。WebSocket 是长连接,一旦断了,连接上的状态全丢。发版又是必然事件——你不能为了 WebSocket 永远不发版。 今天聊聊怎么让 WebSocket 在服务发版时平滑过渡,不让用户感知到断线。 WebSocket 发版的三个痛点 滚动更新时,K8s 或者运维平台会给旧 Pod 发 SIGTERM 信号,然后等一段时间(默认 30 秒)后 SIGKILL。WebSocket 没有 HTTP 那样的负载均衡重试机制,Pod 一死连接直接断。 三个痛点: 连接断开——旧 Pod 停了,上面的 WebSocket 连接瞬间全断。用户端要么看到连接断开提示,要么消息发不出去。 状态丢失——客服正在输入....
RocketMQ Tag/SQL92 过滤实战:客户端拉取无效消息浪费带宽?Broker 端精准过滤,网络开销降 70%!
公司做物流系统,订单状态变更通过 RocketMQ 广播。刚开始只有一个消费者,所有消息照单全收。后来业务拆分,多了十几个微服务各自订阅感兴趣的消息。问题来了——每个服务都从 Broker 拉全量消息,然后自己过滤。一天几千万条消息,每个服务实际需要的不到 10%,90% 拉过来就扔了。内网带宽跑满,消息堆积,消费延迟越来越大。 这个问题特别容易被忽视。因为消息队列用起来太简单了,Producer 发、Consumer 收,中间不用管。但当你有了十几个 Consumer 各自只需要不同子集的消息时,全量拉取就是在用带宽换便利。 今天聊聊 RocketMQ 的 Tag 和 SQL92 两种 Broker 端过滤方式,把过滤逻辑从消费端前移到 Broker,让不需要的消息根本不出 Broker 的门。 客户端过滤的问题在哪 默认的 Push Consumer 模式,客户端从 Broker 拉消息的流程是这样的: Broker ──拉取──→ Consumer(全量消息) │ ├─ 需要的消息(10%)→ 处理 └─ 不需要的消息(90%)→ 丢弃 看起来没什么问题?你丢你的,又不占硬盘....
规则执行上下文串号排查:并发请求下变量互相覆盖?ThreadLocal 精准隔离 + 清理钩子!
公司的风控系统出了个诡异的 bug。用户 A 在页面上看到自己被拒绝了,但查日志发现规则里引用的是用户 B 的订单金额。两个人完全不相干,数据却串了。排查了两天,最后定位到 QLExpress 的 DefaultContext 被当成了单例 Bean,所有请求共享同一个 context 对象,并发请求下变量互相覆盖。 这种 bug 的特征很典型——低并发时一切正常,压测或者高峰期才出现,而且数据是"偶尔串"而不是"一直错"。如果你问 QA 能不能复现,他们的回答多半是"有时候能有时候不能"。这就是并发问题的经典症状。 今天聊聊怎么在规则引擎的场景下,用 ThreadLocal 把上下文彻底隔离开,再加一个清理钩子防止内存泄露。 问题怎么发生的 先还原一下事故现场。很多人在用 QLExpress 的时候,为了省事会把 DefaultContext 注入成 Spring Bean: @Component public class RuleService { // ❌ 单例 Bean!所有请求共享 private final DefaultContext<String, Object....
脚本引擎 Metaspace OOM 防护:动态规则频繁加载导致内存泄漏?ClassLoader 隔离 + 定时回收!
公司有个规则引擎服务,每天运营要更新几百条风控规则。QLExpress 每次执行规则都会编译生成一个匿名类,然后装进 JVM 的 Metaspace。运行了两周之后,服务开始频繁 Full GC,再后来直接 Metaspace OOM 崩了。重启能续命两周,但规则数量只增不减,两周变成十天,十天变成一周,最后每天都得重启。 这个问题在脚本引擎场景下几乎必现。GroovyShell、QLExpress、Aviator,甚至 Nashorn,只要是"动态编译 → 生成类 → 装载到 JVM"的模式,都会往 Metaspace 里塞东西。而且 Metaspace 默认没有上限——它只会一直涨,直到物理内存耗尽。 今天聊聊怎么用 ClassLoader 隔离 + 定时回收,让 Metaspace 不炸。 Metaspace 里到底装了什么 Java 8 以前叫 PermGen,Java 8 以后改名为 Metaspace。换了个名字,但干的活一样——存类的元数据。 你写的每一个类,编译后的字节码,字段名、方法名、注解信息、常量池,全部放在这里。普通的 Java 类在应用启动时加载一次,之后不....
QLExpress 规则单元测试框架:上线前自动跑 1000 条用例,拦截逻辑错误!
运营在后台配了一条规则:orderAmount > 10000 && userLevel == 'VIP' 标记为大额订单。配完觉得没问题,直接上线。半小时后客服被用户打爆——所有 VIP 用户的订单都被拦截了,不管金额大小。排查下来发现是规则里多打了个 >,本来应该是 <。就一个字符,损失了几十万。 规则引擎的优点是灵活——运营可以随时改规则,不用开发介入。但灵活的另一面是危险:没有编译器帮你检查,一个手误就上线了。 今天聊聊怎么给规则加上自动化测试——让 1000 条用例在规则上线前跑一遍,逻辑错误当场拦截。 规则引擎的测试为什么难做 传统代码的测试很成熟:JUnit、Mockito、覆盖率工具,流水线上跑一下,绿了就上线。但规则引擎面前,这套流程用不上。 规则不是 Java 代码。它是运营在后台配的一段表达式字符串,比如: productPrice * 0.8 > competitorPrice && stockQty < 100 这段东西没有编译期检查。你没法给它写 JUnit,因为在 Java 眼里它只是一个字....
