ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

中间件核心原理与实战:Redis做中间件及消息队列选型

中间件核心原理与实战:Redis做中间件及消息队列选型 1. 先搞清楚中间件到底是个什么东西1.1 中间件不是“中间的一个软件”很多同学第一次听到“中间件”这个名词第一反应是是不是就是把一个软件放在两个软件中间这个理解方向是对的但不够准确。我见过太多人把中间件等同于“消息队列”一聊中间件就只想到Kafka、RabbitMQ其实中间件这个家族远比想象中庞大。按照行业里比较认可的说法中间件是介于操作系统/网络层与应用业务逻辑之间的独立软件层它的核心职责是解决分布式系统中的共性难题跨进程通信、数据缓存、异步削峰、服务协调、流量治理等。说白了业务代码里不该重复写的那部分底层逻辑被单独抽出来成了中间件。Redis做中间件、Kafka做中间件、Nginx做中间件、ZooKeeper做中间件甚至ShardingSphere这种数据库分片中间件统统属于这个范畴。我打个比方你开了一家餐厅后厨炒菜应用逻辑顾客点菜外部请求如果每个服务员都要自己冲到后厨去催菜、传菜后厨就乱套了。中间件就是那个站在窗口传菜的传菜员菜品先送过来它替你排序、暂存、再送到对应餐桌。传菜员不仅解决了“谁传给谁”的问题还能在高峰时段帮后厨缓冲压力——这就是中间件的本质解耦、缓冲、通信、协调。1.2 中间件到底解决什么问题从实用角度说中间件解决的三大问题是通信协议统一、异步解耦、数据状态共享。先看通信协议统一。在没有消息中间件之前服务A要调用服务B最简单的方式是HTTP接口直连。这听起来没什么问题但一旦服务数量上去了A要对接B、C、D每个服务的协议、超时设置、重试策略都不一样你会在业务代码里写出一堆又臭又长的HTTP客户端封装。换成一个消息中间件大家只跟中间件建立连接发送方把消息丢进Topic消费方按自己的速度去拉协议只需要对齐中间件这一层就行。再看异步解耦。典型场景是下单送积分用户下完单订单系统要调积分系统、短信系统、物流系统。如果这些全都同步调用下单接口的耗时会从50毫秒变成500毫秒而且任何一个下游系统挂了下单就失败。引入消息中间件之后订单系统只负责写一条“下单成功”的消息积分系统、短信系统自己去订阅。下单接口响应时间瞬间降下来下游系统挂了也不影响主流程消息先攒着等它恢复再消费。最后是数据状态共享。多个服务实例共同访问同一份数据如果各自维护本地缓存数据一致性就崩了。Redis这种缓存中间件或者说ZooKeeper/etcd这种协调中间件就是用来做“跨进程共享状态”的。这三个问题几乎每个互联网项目都会遇到所以中间件才成了后端工程师绕不开的必修课。1.3 中间件家族的分类图谱我在面试候选人的时候经常让人列出自己用过的中间件然后按类别归类。很多人能列出一堆名字但分不清它们之间的定位差异。这里我按常用场景给中间件分个类类别典型代表核心职责缓存中间件Redis、Memcached数据缓存、分布式锁、限流计数消息中间件Kafka、RabbitMQ、RocketMQ异步解耦、削峰填谷、事件通知数据库中间件ShardingSphere、MyCat、ProxySQL分库分表、读写分离、SQL拦截服务协调中间件ZooKeeper、etcd、Consul服务注册发现、分布式协调、配置管理流量网关/接入中间件Nginx、OpenResty、APISIX流量入口、路由转发、限流熔断RPC通信中间件Dubbo、gRPC、Thrift服务间远程调用、服务治理日志/链路中间件ELK、SkyWalking、Pinpoint日志采集分析、分布式链路追踪这里面后端日常开发打交道最多的就是缓存中间件和消息中间件而且Redis这两头都沾——它既能做缓存也能在某些场景下充当消息中间件。这也正好对应了热搜词里“redis做中间件”这个说法。后面我用独立章节展开讲。2. 为什么Redis能成为最常说的那个中间件2.1 Redis凭什么这么特别Redis做中间件能火成这样核心原因是它把“快”和“灵活”这两个特性结合得太好了。先看快。Redis是基于内存的键值存储读写速度能达到每秒十万到二十万次级别延迟通常低于1毫秒。它为什么能做到这么快这得从它的底层设计说起。Redis是单线程模型这里的“单线程”指的是处理网络请求和执行命令的过程是单线程的好处是避免了多线程上下文切换和锁竞争的开销。同时它用了IO多路复用技术Linux上的epoll一个线程就能同时盯着成千上万个客户端连接哪个连接有数据过来了就去处理哪个而不是傻等一个连接。你可以类比成一个大厨一个人管着十个灶眼哪个锅冒气了就去炒哪个锅但炒菜之间不需要切换衣服也不需要担心两个锅同时起火抢厨具。再看灵活。Redis支持的数据结构太丰富了String、Hash、List、Set、ZSet、Bitmap、HyperLogLog、Geo、Stream。这意味着它不只是个缓存还能做排行榜ZSet、做去重计数Set、做UV统计HyperLogLog、做附近的人Geo、做消息流Stream。一个中间件能覆盖这么多场景自然成了后端项目的“标配件”。2.2 缓存中间件的关键机制旁路缓存与过期策略Redis最常见的角色是缓存中间件。缓存的基本思路很简单把数据放到Redis里查询时先查Redis查不到再查数据库。这里面有一个必须掌握的方案叫Cache Aside Pattern旁路缓存模式。读请求过来时先查缓存缓存命中直接返回缓存没命中查数据库然后把结果写回Redis再返回给调用方。写请求的处理要稍微谨慎一些通常是先更新数据库再删除Redis里的对应缓存。为什么不是先更新缓存因为并发环境下容易出问题两个线程同时写数据库里最后的值和缓存里最后的值可能来自不同的线程数据就永久不一致了。而删除缓存的做法最坏情况就是下一次读请求重新查一次库不会有长时间不一致。缓存过期策略同样关键。Redis默认支持两种主流的过期淘汰策略惰性删除和定期删除。惰性删除是当某个key被访问时才检查它是否过期定期删除是每隔一段时间主动扫一批带过期时间的key。这两种策略结合的结果是过期的key不一定会立刻被物理删除所以内存占用量可能暂时降不下来。这里我建议在生产环境显式配置参数比如maxmemory和maxmemory-policy而不是让Redis无限使用内存。我之前接手过一个项目Redis没设maxmemory结果数据量涨到物理内存的85%系统开始频繁swap接口响应从5毫秒飙到500毫秒排查了很久才发现是内存问题。2.3 Redis做消息中间件Stream和Pub/Sub到底怎么用热搜词里提到“redis做中间件”我估计很多人说的就是用Redis当消息队列用。这个做法在特定场景下是合理的。Redis做消息队列有三种流派第一种是List BRPOP。List本身是双向链表LPUSH往队尾塞BRPOP阻塞式地从队头取。用这两个命令就能实现一个阻塞队列。优点是简单直接缺点是消息没有确认机制消费者取走消息后如果崩溃消息就丢了。第二种是Pub/Sub发布订阅。优点是实时推送适合广播通知场景。缺点也很明显消息不持久化消费方不在线就收不到消息而且消费者断线重连期间的消息完全丢失。拿它做核心消息通道我建议慎之又慎。第三种是Redis 5.0引入的Stream。Stream是一个带消息ID的持久化日志结构支持消费者组Consumer Group每个消费者组维护独立的消费游标支持ACK确认。这就解决了List和Pub/Sub的核心痛点。如果你不想引入Kafka或RabbitMQ业务量又不大用Redis Stream完全可以撑住一天几十万条消息的规模。我举个实际使用案例一个技术社区项目里用户发帖后需要更新搜索索引、更新热帖榜、发送站内通知。这三个操作都不是核心链路我用Redis Stream建了一个topic发帖服务把事件写入三个消费者组各取所需。Redis本身已经部署了不需要额外维护一套消息中间件成本几乎为零。等后续消息量涨到每秒几千条再平滑迁移到Kafka也不迟。不过要提醒一句Redis Stream没有Kafka那样的分区级并行消费能力也没有成熟的消息轨迹追踪和死信队列管理。一旦消息量级上来还是要换专业消息中间件。选型问题我放在下一节讲。3. 消息中间件选型Kafka、RabbitMQ、RocketMQ怎么选3.1 三大消息中间件的核心模型差异很多人在选消息中间件时第一反应是去对比吞吐量数据这其实是个误区。选型首先要看的是它的消息模型和自己的业务场景对不对得上。Kafka的核心模型是“分区日志”。每个Topic被分成多个分区每个分区内部是有序的日志文件。生产端按Key哈希写入某个分区消费端以消费者组为单位一个分区的消息只会被组内的一个消费者实例消费。这个模型带来的能力是高吞吐顺序写磁盘、零拷贝传输、分区有序、消费位点自主控制。Kafka的定位就是大数据管道和日志收集天生适合海量数据、高吞吐场景。RabbitMQ是传统的消息代理核心模型是Exchange交换机 Queue。生产端不直接发消息到队列而是发给Exchange由Exchange通过路由规则Direct、Topic、Fanout把消息分发到不同的Queue。RabbitMQ的强项是灵活的路由能力和丰富的消息特性延迟队列、死信队列、消息TTL、优先级队列都有成熟插件支持。它基于Erlang VM运行并发能力不错但吞吐量跟Kafka不是一个量级。RocketMQ是中间模型Topic 消费组跟Kafka类似但做了很多业务向的增强消息事务、定时/延时消息、消息轨迹、消息重试队列。它在金融、电商这种追求消息一定不能丢的场景里用得多。还有一点必须搞清楚消费模型。Kafka是拉模式消费者主动去Broker拉取消息由消费者自己控制速率。RabbitMQ支持推拉两种模式但默认走推送。推模式延迟低但消费者处理不过来时容易堆积拉模式更稳但实现上要注意长轮询的设计。没有绝对的好坏只有匹配不匹配。3.2 选型对照表与场景决策下面这张表是我在实际项目中反复权衡之后整理出来的选型对照表直接拿去用对比维度KafkaRabbitMQRocketMQ单机吞吐量百万级/秒万级/秒十万级/秒消息延迟毫秒级略高微秒级毫秒级消息可靠性高幂等ACK高非常高事务消息消费模式拉模式推/拉模式拉模式延迟消息支持不支持原生插件支持原生支持事务消息不支持弱原生支持运维复杂度中高低中适用场景日志管道、大数据流处理、流量削峰企业内部系统、业务消息通知、RPC解耦电商订单、金融交易、可靠消息场景我个人的决策逻辑很简单业务消息量小、要求低延迟比如OA系统的审批通知选RabbitMQ省心省力日志采集、数据管道每秒几十万条写入选Kafka电商订单链路交易消息一条都不能丢还要支持订单超时取消这种延时场景选RocketMQ。有一种场景我会特别提醒避开Kafka需要对每条消息做精细的路由分发比如根据消息里的地域字段把消息发给不同地区的服务实例。Kafka的分区机制不支持这种路由你只能把地域拼进Key让哈希把同类消息分到同一分区灵活性远不如RabbitMQ的Exchange。用错模型会非常别扭而且后期很难改。3.3 部署和运维上的实际差异选型不能只看功能还要看你有多少人能养这个中间件。我见过不少团队在技术选型时只比功能点上线之后才发现没能力运维。Kafka部署牵扯到ZooKeeper虽然新版本KRaft模式可以去掉ZK但很多团队还在用ZK版本或KRaft控制器生产环境至少三台Broker起步还要规划磁盘、分区副本数、日志保留策略。它本身不提供管理界面通常要另外部署Kafka UI工具对监控的要求也高。团队里如果没有一个熟悉Kafka运维的人我劝你不要轻易上。RabbitMQ部署是最温和的一个Erlang虚拟机线程跑起来就完事自带Web管理界面队列状态、连接数、消费者情况一目了然。它内存优化得当的话一个2核4G的虚拟机都能跑得很稳。适合中小团队长期维护。RocketMQ的管理依赖NameServer自带控制台功能全面但部署组件比RabbitMQ多比Kafka稍好一些。运维上还有一个隐形坑消息中间件的版本兼容性。Kafka的Broker、客户端、ZooKeeper三者之间有严格的版本匹配关系升级的时候要先核对兼容矩阵否则会踩到一堆玄学错误。我踩过最狠的一次就是客户端和Broker版本不兼容导致消息重复消费排查到凌晨才定位出来。4. 从零落地一套缓存中间件完整实操记录4.1 场景设定与架构设计理论聊了这么多我们落到一个具体实操场景一个电商商品详情页的查询链路。这是我去年给一个项目做的架构优化正好可以完整还原出来。原始方案是直接查MySQL一个商品详情接口峰值QPS到了6000左右MySQL扛不住慢查询了。优化目标很明确把商品详情信息缓存到Redis让大部分请求在缓存层直接返回。架构上分了四层接入层Nginx做负载均衡应用层是Java服务缓存层是Redis主从集群数据层是MySQL。查询链路先查Redis命中直接返回未命中则查MySQL然后把数据回填到Redis并设置过期时间再返回。这也就是前面说的Cache Aside模式。4.2 缓存中间件的核心参数与配置要点配置Redis实例时除了基本的端口和绑定地址我强烈建议按下面这套参数来调。特别是maxmemory不配置等于裸奔。maxmemory 4gb maxmemory-policy allkeys-lru appendonly yes appendfsync everysec lazyfree-lazy-eviction yes先说maxmemory-policy。Redis提供了八种淘汰策略我用得最多的是allkeys-lru当内存达到上限时从所有key里挑选最近最少使用的key淘汰。商品详情缓存这种业务天然适合LRU因为热度差异大冷数据淘汰掉完全没问题。再说appendonly和appendfsync。开启AOF持久化appendfsync设成everysec意思是每秒刷一次盘。这么设的原因很简单RDB快照默认是隔一段时间才落一次盘宕机会丢掉几分钟数据AOF每秒刷盘最多丢一秒对大多数缓存场景完全可接受。如果把appendfsync设成always每一条写命令都刷盘安全性是提升了但性能会降到原来的三分之一左右不划算。lazyfree-lazy-eviction这个参数是我后来才配的。它是异步释放内存模式。之前清理一个超大key时Redis主线程被阻塞了接近两秒所有请求全部超时。开启lazyfree之后删除动作放到后台线程执行主线程不卡了。4.3 高可用方案与一致性保障单节点Redis一旦挂了整个缓存层就瘫痪了所有请求瞬间压到数据库上。这个风险必须提前规避。我的高可用方案是标准的“一主二从三哨兵”一个Master节点负责写两个Slave节点同步复制数据三个Sentinel哨兵进程负责监控和自动故障转移。当Master宕机时Sentinel集群会选举出一个新的Master并通知其他Slave切换主从关系。对应用层来说整个过程体验是无感知的只是Redis连接需要自动重连。这里要讲清楚一个核心概念Redis主从复制是异步的。Master写成功后数据还没同步到Slave之前Master挂了数据就丢了。所以Redis能保证的是高可用不是强一致。如果你的业务场景里数据一秒都不能丢应该考虑的是用数据库的事务而不是把Redis当成唯一数据源。Redis做缓存数据库做持久化存储这个职责边界要画清楚。我在这个项目里还给商品详情缓存设计了“过期时间加随机抖动”的策略。假设所有商品缓存设的都是5分钟过期刚好某个整点大批量到期缓存失效引发的请求洪峰会让数据库直接打崩。解决办法很简单把过期时间设成“基础值随机数”比如300秒加0到60秒的随机值。这样缓存过期时间错开数据库压力曲线平滑得多。序列化方式也值得提一句。Java项目里我是用Jackson把商品详情对象序列化成JSON字符串存进Redis查询时反序列化回来。前期图方便用过JDK原生的序列化存进去的数据带了大量的类信息体积膨胀三到五倍浪费了不少内存。如果你们用的是Protobuf序列化大小可以压到JSON的五分之一遇到超大对象时可以重点考虑。4.4 一套可复用的落地模板模板这东西只要有一次实战烂在肚子里后面就是复制粘贴改改名。这里我梳理一套可复用的Redis缓存落地模板适用范围包括但不限于商品详情、账户信息、用户配置、热点新闻。第一步评估数据是否需要缓存。判断标准是读多写少且数据一致性要求不是极高。像库存余额这种读取和写入同样频繁、数据一天都不能错的数据尽量不要缓存或者只缓存热点数据并做严格过期保护。第二步确定缓存粒度。是缓存整个对象还是对象的字段级商品详情这种整体读取的对象级缓存合适如果业务里只需要读取商品价格一个字段单独缓存价格即可。第三步设计Key规范。我习惯用“业务域:实体类型:ID[:子ID]”三段式。比如电商项目里商品缓存的Key是mall:product:10086订单超时状态是mall:order:10086:status。这样出了问题也可以直接在Redis里用SCAN定位数据。第四步设置过期时间和淘汰策略。按前面说的随机抖动方案写入代码里完成过期时间计算。第五步配置监控。至少要有三个指标Redis内存使用率、命中率、慢查询数量。命中率低于80%就要检查缓存策略是不是有问题慢查询超过100毫秒就要看是不是有大key或者阻塞命令。这套模板我已经在三个项目里复用过了每次落地基本只需要半天时间而且是带着监控和压测一起做的。5. 中间件实战中常见的问题与排查技巧5.1 缓存穿透、击穿、雪崩三个高频坑的区分与应对这三个词经常被混为一谈但本质完全不同处理方式也天差地别。缓存穿透指的是请求一个数据库里根本不存在的数据比如商品ID传了一个负数或者不存在的ID缓存里没有数据库里也没有每次请求都会打到数据库。要是有人恶意循环请求这种不存在的key数据库会被空转打垮。应对方案是布隆过滤器一个用位数组表示数据集合的过滤器判断“这个key肯定不存在”很快如果有大量的穿透请求先在布隆过滤器这一层拦截。另一种做法是缓存空值把查不到的数据的key也缓存起来设一个很短的过期时间比如60秒减轻数据库压力。缓存击穿指的是一个非常热门的key在缓存过期的瞬间大量并发请求同时落到数据库上。一个key的失效拖垮整个数据库这个局面很常见。应对方案是互斥锁查询数据库并回填缓存的操作加锁同一时间只放一个线程去查库其他线程等缓存回填之后直接读缓存。还有一种是逻辑过期不给key设置实际过期时间而是在value里存一个过期时间戳发现数据逻辑过期时只放一个线程去刷新缓存其他线程先返回旧数据。逻辑过期方案对并发友好牺牲的是短暂的数据时效性。缓存雪崩是大量的key在同一时间段集中失效导致请求全部落到数据库。前面说的过期时间加随机抖动就是对付雪崩的常规操作。还有一种做法是做多级缓存在Redis前面加一层本地缓存比如Caffeine即使Redis挂了本地缓存还能扛住一部分流量。5.2 中间件连接被耗尽的排查思路生产环境最容易碰到的问题是接口突然大面积超时错误日志里报“Cannot get connection from pool”。这是Redis连接池被耗尽的表现。排查思路按这个顺序走先看Redis当前连接数命令是CLIENT LIST统计一下有没有可疑的客户端连接。很多时候是项目里的工具类每次new了一个连接但没归还导致连接泄漏连接数只增不减。这时候重点排查代码里有没有try/catch/finally里遗漏的归还连接操作。再看慢查询Redis的slowlog get 10可以列出最近的慢命令。慢查询会占用连接如果队列里的请求都在等Redis响应连接池马上被打满。曾经在生产环境遇到过有人用KEYS命令匹配线上大量key直接阻塞了Redis主线程几十秒导致所有请求超时。最后要确认的是你的连接池是不是配置合理。Java的Jedis或Lettuce连接池maxTotal我一直建议按“预估QPS乘以单次请求平均耗时”来算再留30%的余量。比如单次缓存操作平均耗时10毫秒QPS是8000那8千米的高峰只需要大约8000乘以0.01等于80个连接保守配置120个就够了。那是说不要无脑地把maxTotal配成2000连接池太大会造成连接浪费反而拖垮Redis。5.3 消息重复消费的排查套路消息中间件用得多了一定会遇到重复消费。因为消息系统普遍不保证只投递一次而是“至少一次”投递消费端要自己做幂等处理。我遇到的最经典的一次订单系统消费支付结果消息给用户加积分。消息重复投递了两次积分就加了两遍用户投诉说积分不对。排查时发现问题不出在消息中间件而是消费端没有做幂等控制。解决方案是在消费端引入幂等机制最简单的是用Redis做一个去重消费消息时先以“业务ID消息ID”为key在Redis里SETNX如果是第一次执行key写入成功正常处理处理完成后保留这个key并设置过期时间为24小时如果SETNX失败说明这条消息在24小时内处理过直接跳过。像“给用户加积分”这种累加型操作最好的方式是改成幂等更新语句在数据库里做原子操作而不是先查再算再加。这里要强调一个原则消息中间件只管投递不管业务幂等。真正避免重复影响的边界在业务应用层。5.4 中间件问题排查速查表这段时间踩过的坑多了我习惯把常规排查思路整理成一张速查表贴在团队文档里遇到问题直接对照找方向。现象可能原因首选排查命令/手段Redis连接池满连接泄漏/慢查询阻塞CLIENT LIST、slowlog get缓存命中率骤降大量key集中过期/代码改key格式info stats里的keyspace_hits、检查过期配置Redis内存暴涨大key写入/无淘汰策略memory usage key、redis-cli --bigkeys消息大量堆积消费者消费能力不足/消费者宕机查看消费组Lag、检查消费端日志消息重复消费无幂等机制/消费者超时重投检查消费日志消息ID、确认幂等设计消费速率突然为0消费者被阻塞/连接异常查看消费者组状态、检查服务心跳Kafka分区不均匀消息Key分布严重倾斜kafka-consumer-groups.sh 查看分区Lag接口超时DB正常中间件连接耗尽用链路追踪定位每一步耗时表格不能解决所有问题但排查的第一步是确定方向方向对了问题就解决了一半。6. 学习路线与对应场景的实践建议6.1 从“会调用API”到“理解内部原理”的进阶路径后台刚入门的新手接触中间件的方式通常是会用Spring的RedisTemplate调几个方法会用KafkaTemplate发个消息。这个阶段不算真正掌握中间件因为出了问题无法定位更不要说做优化。我对想系统学中间件的人建议按三步走。第一步先把基本功补齐。中间件的底层都离不开三项基础知识计算机网络TCP/IP、HTTP、Socket通信、操作系统进程、线程、内存、IO模型、数据结构哈希表、跳表、链表、B树。Redis的快离不开IO多路复用Kafka的高吞吐离不开零拷贝和磁盘顺序写ZooKeeper的ZAB协议离不开TCP连接管理。没有这些基础你读任何中间件源码都会像看天书。第二步找一个真实的业务场景把中间件用起来。比如做一个社区项目引入Redis存热门帖子引入Kafka做异步通知。用起来之后主动做两件事第一件事是看监控把命中率、延迟、内存变化全部记录成曲线观察规律第二件事是故意制造故障比如手动把Redis停了看系统怎么表现把消费者的处理逻辑里sleep一秒模拟慢消费看消息堆积是什么样。这个过程帮助极大因为你在安全环境里见过这些故障未来线上出问题了才不会慌。第三步挑一个最常用的中间件精读源码。我推荐先从Redis开始因为它的源码量相对可控而且核心数据结构、事件循环、持久化机制都是教科书级别的实现。读的时候带着问题去读Redis为什么用单线程还能这么快它的跳跃表是怎么维护多级索引的AOF重写是怎么避免阻塞的6.2 我踩过最深的坑希望你一次都不要踩最后分享几个经验都是我在生产环境用真金白银买来的教训。第一不要在中间件上“炫技”。我见过有人为了提高吞吐量把消费端改成批量拉取再批量处理结果消息处理成功了一半另一半在服务重启时全部重投订单状态直接乱了。复杂方案带来的收益可能很小但引入的复杂度会成倍增加。第二上线中间件之前一定做压测。没有压测数据你连自己集群的容量都不知道平时跑得好好的购物节大促一来就崩这是完全可预期的灾难。第三任何中间件都有它擅长的边界跨边界硬用会非常难受。Redis的Stream能做消息队列但它不具备Kafka的分区并行扩展能力RabbitMQ能支撑上万的吞吐但它不适合做海量日志管道。选型时要先定位场景再选组件。如果只让我说一条最重要的经验那就是把中间件当作你系统里一个有生命的组件它有自己的特性、自己的脾气你需要熟悉它、监控它、敬畏它而不是只会调用它。数据结构、网络协议这些底层知识和线上踩过的每一个坑最终都会成为你做技术判断时的底气。中间件这条路没有捷径但走进去之后你处理复杂系统问题的能力会上一个明显的台阶。
返回列表