拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Redis发布与订阅:陪玩系统实时推送的轻量级实践

做陪玩平台最烦的事情就是陪玩师那边刚上线玩家下一秒就下了单结果陪玩师压根没收到提醒等发现的时候订单早就被别人接了。一开始我总以为是手机通知没弹出来后来查了日志才发现服务端推送这块压根就没做好——玩家下单了系统得把消息推给匹配的陪玩师这个推的动作如果全靠数据库轮询或者客户端自己刷接口那延迟和压力都顶不住。后来我把系统里这块实时功能重写了一遍核心就用到了Redis的发布订阅机制Pub/Sub也就是标题里说的redis发布与订阅整体效果好了很多。这篇东西就把我当时的实现思路、代码结构和踩过的坑记录下来给想搞明白陪玩系统里实时消息怎么做的朋友一个参考。这套方案适合谁看如果你正在写陪玩、约拍、代练、上门服务这类C2C交易平台而且系统里已经有Redis了不想为了推送个通知就单独引一套消息队列那Redis发布订阅就是最轻量、最性价比的做法。我会把从业务场景分析、频道设计、Spring Boot里的代码实现、序列化配置到线上问题排查全串一遍尽量做到拿着这篇东西就能自己复现出来。1. 先从业务场景说起陪玩系统里到底哪里需要实时推送1.1 一个典型的陪玩下单流程哪里需要“推送”先把业务场景立起来。陪玩系统说白了是个双边平台一头是陪玩师上架自己的游戏技能和服务时间另一头是玩家按需下单买陪玩时长。我在系统里跑通的主流程是这样玩家在搜索页筛出符合条件的陪玩师查看陪玩师的档期、技能标签、价格。选好了提交订单支付成功订单状态变成“待接单”。系统要把这个“新订单”消息立即通知到陪玩师端。陪玩师收到通知选择接受订单进入“已接单”然后开始陪玩。开黑过程中要语音开黑、共享屏幕这其实是另一套实时音视频体系跟本条消息推送无关。结束后订单结算双方互评。这里面最核心的实时节点就是第3步支付成功以后怎么让陪玩师第一时间知道“你有个新订单了”。如果时效性差比如隔了5秒钟陪玩师才看到通知玩家可能已经取消退款了。之前我试过让客户端每3秒拉一次订单接口订单量一大就撑不住数据库压力上来接口延迟飙升用户体验更差。除了订单通知还有几个地方也天然需要发布订阅大厅公屏聊天玩家和陪玩师在一个公共房间里聊游戏心得、找队友消息要广播给当前房间的所有人。陪玩师上下线状态玩家关注列表里要实时显示陪玩师“在线/在玩/离线”不能每次刷新页面才变。私聊新消息提醒玩家和陪玩师谈价格、约时间没进聊天页时要收到“你有一条新消息”的角标提醒。订单状态流转已接单、已开始、已完成这些状态变了交易双方都要立刻知道。这些场景有个共同特点一次产生多方接收而且要求秒级触达。这种模型恰好就是发布订阅最舒服的适用区。1.2 为什么不选轮询、长连接、消息队列偏偏用Redis发布订阅其实我当时纠结过四类方案客户端轮询、WebSocket长连接、独立消息队列RabbitMQ/Kafka、Redis发布订阅。一个个说下我当时的取舍逻辑。客户端轮询最简单但是最笨。客户端每隔几秒请求一次接口查“有没有新订单”服务端被无效请求占满。陪玩师刷一下、玩家刷一下并发一高接口全白热化。而且实时性永远受轮询间隔限制你说3秒轮询一次那最坏延迟就是3秒这个延迟在“抢订单”场景里不可接受。WebSocket长连接实时性确实好能做到真正的服务端主动推送。但它的问题在于连接管理成本高握手、心跳、断线重连、分布式环境下的会话路由一套做下来没个一周搞不定而且很多场景下客户端用的不是WebSocket而是小程序或纯HTTP环境兼容性也麻烦。独立消息队列RabbitMQ、Kafka都是成熟的消息中间件自带消息持久化、消费确认、重试机制可靠性和功能完整性都碾压Redis的发布订阅。但问题也很现实——对于一个中小体量的陪玩系统专门搭一套消息队列集群运维成本和资源占用都高尤其Redis本来就在跑为了“通知”这个功能再引一套重型中间件性价比很低。Redis发布订阅轻量、毫秒级延迟、支持一对多广播写法也简单几行代码就能把消息从服务端推给所有订阅了某个频道的客户端。缺点是不持久化、没有ACK机制但这个缺点在某些场景下其实没那么致命后面我会单独讲。当时我拍板选Redis发布订阅还有一个很现实的原因Redis本身已经是系统基础设施了缓存、分布式锁、排行榜都靠它不需要额外部署任何东西只需要多用两个命令发布订阅的运维成本几乎为零。2. Redis发布订阅的原理与核心概念2.1 三个角色发布者、频道、订阅者Redis发布订阅的模型特别容易理解我用一个生活例子来类比。你把Redis想象成一家广播电台频道Channel就是一个个不同的广播频率。订阅者Subscriber就是听众拿着收音机调到某个频率上发布者Publisher就是主持人对着麦克风说话。主持人说的话所有在这个频率上收听的人都能同时听到不在这个频率上的人完全感受不到。而且主持人根本不需要知道听众是谁、有多少人他只需要对着麦克风讲就完了。对应回技术实现发布者调用PUBLISH channel message命令往指定频道发一条消息。订阅者调用SUBSCRIBE channel命令订阅一个或多个频道。Redis把消息推送给所有订阅了这个频道的客户端。没有订阅者的频道消息发出去就没了不会存下来等以后有人订。另外Redis还有一个模式订阅Pattern Subscribe用通配符匹配频道。比如订阅order:*那order:create、order:accept、order:finish这些频道的消息都能收到。这个特性在陪玩系统里非常有用一个监听器就能处理所有订单相关的消息。2.2 发布订阅的生命周期、消息形态和三个特殊表现这里有几个绕不开的特性理解不透后面就是各种莫名其妙的问题。第一Redis发布订阅是“即发即弃”fire-and-forget模式。消息不会写入Redis的任何数据结构里频道本身也不存储历史消息。你在频道上发一条消息如果当前没有任何订阅者这条消息就直接丢了之后也不会补发。这一点和消息队列有本质区别Kafka里消息会在分区里保留一段时间但Redis的Pub/Sub不行。第二消息推送是即时的延迟在毫秒级别。因为Redis是单线程处理命令的PUBLISH命令往频道上一发布订阅这个频道的每个客户端连接都会立刻收到这条消息中间没有任何批处理或者积压缓冲。第三Redis发布订阅是一对多广播。一条消息发出来所有订阅者都会收到同一份不是竞争消费。打个比方订单通知发给10个陪玩师10个陪玩师都能收到这条“新订单”推送而不是只有1个人收到。所以它天然适合做广播场景但不适合做任务分发/负载均衡——如果我要把订单平均分给10个陪玩师每人只处理属于自己的那部分就需要消息队列里那种“消费组”的概念而发布订阅做不到。第四特别容易忽略的一点订阅行为是有状态的。客户端一旦执行SUBSCRIBE命令这个连接就进入了“订阅模式”在这个连接上只能执行订阅相关的命令SUBSCRIBE/UNSUBSCRIBE/PING等不能再执行GET、SET这些普通命令否则会报错。这一点在使用连接池的时候尤其容易踩坑后面代码部分会专门强调。3. 陪玩系统里Redis发布订阅的落地设计3.1 频道命名规范与消息协议设计动手写代码之前先把频道和消息格式定下来。这两个是后续所有功能的地基一开始没设计好改起来特别痛。频道命名方面我按“业务域:事件类型:版本号”的格式来命名。之所以加版本号是因为Redis的频道结构是扁平的改协议没有升级机制只能靠频道名区分。我系统里实际用到的几个频道如下order:notify:v1—— 订单通知频道推送新订单、订单状态变更等消息。lobby:chat:v1—— 大厅公屏聊天频道。user:status:v1—— 陪玩师上下线状态频道。im:message:v1—— 私聊新消息提醒频道。消息内容统一用JSON格式而且内部再包一层“信封”方便扩展。我定义的消息结构大概是这样的第一层是消息类型type、数据data、时间戳ts其中data里再放具体的业务字段。比如新订单通知的消息体{ type: new_order, data: { orderId: CK20250612001, playerId: 10086, playerName: 小明, gameName: 王者荣耀, duration: 60, price: 120.00 }, ts: 1749720000000 }这样做的好处有几个所有消息长一个样监听端只要反序列化一次信封再根据type字段分发到不同的业务处理器代码结构统一。调试方便用Redis客户端直接往频道里发一条JSON就能模拟消息不用跑完整业务流程。ts时间戳可以用来排查消息延迟曾经有一次我发现订单通知晚了2秒就是靠ts定位到是网络传输问题。3.2 工程结构怎么划分监听器要拆几个在Spring Boot工程里我按“配置类-发布服务-监听器-业务处理器”四层来组织代码。redis-pubsub/ ├── config/ │ └── RedisPubSubConfig.java # 监听器容器、序列化配置 ├── publisher/ │ ├── OrderMessagePublisher.java # 订单相关消息发布 │ └── LobbyMessagePublisher.java # 公屏消息发布 ├── listener/ │ ├── OrderNotifyListener.java # 订单频道监听 │ ├── LobbyChatListener.java # 公屏聊天监听 │ └── UserStatusListener.java # 用户状态监听 ├── handler/ │ ├── MessageDispatcher.java # 消息分发器按type分发 │ └── OrderMessageHandler.java # 具体处理订单消息这里有个设计心得监听器不要和业务逻辑混在一起。监听器的职责是“从Redis拿到原始消息反序列化然后交给业务处理器”它本身不应该处理订单状态流转、不应该写数据库、不应该调第三方接口。把监听器和业务处理器拆开以后一个是方便单测另一个是方便以后切消息队列——如果平台发展大了要换Kafka只需要重写监听器业务处理器完全不动。监听器拆几个这个看业务量。最开始我图省事只写了一个全局监听器订阅所有频道结果每个频道都要去做一层白名单判断代码混乱出了问题也不知道是哪个频道的消息触发的。后来我改成按“业务域”拆监听器订单一个、公屏一个、用户状态一个。每个监听器只关心自己领域的频道。3.3 三个典型场景的频道与推送策略把三个核心场景串起来看一下实际的消息流转。场景一新订单通知。玩家支付成功以后订单服务调用发布服务往order:notify:v1频道发一条type为new_order的消息。所有订阅了这个频道的在线陪玩师连接都会收到客户端拿到消息后弹出浮窗“您有一个新订单”。这里有个点要注意不该把订单的全部敏感数据都推进这条消息里推orderId就够了真正的订单详情由客户端根据orderId再拉接口。这样能减少消息体积也避免价格字段在推送链路里被篡改或者泄露。场景二大厅公屏聊天。这个和订单通知完全不同公屏消息量很大而且没有精确的接收者所有在大厅里的人都要看到。我按照房间ID来设计频道粒度每个大厅聊天室对应一个频道比如lobby:chat:v1然后聊天室ID拼在后面作为区分。用户发言时服务端把消息发布到对应频道的推送带上所有在线订阅者就能收到。比轮询舒服太多了。场景三陪玩师上下线。这个场景我做得比较轻陪玩师登录成功后自动订阅user:status:v1下线时退订同时发布一条上下线状态消息。关注了这位陪玩师的玩家端通过服务端关联关系也会收到对应提醒。因为状态变更不像聊天那么频繁这个频道不会撑爆Redis。4. 配置与核心代码实现4.1 引入依赖与配置文件我是在Spring Boot 2.7.x spring-data-redis环境下做的实现Maven里需要引入spring-boot-starter-data-redis。如果用的是Spring Boot 3.x依赖名不变只是切换到了Jakarta命名空间代码整体差异不大。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency配置文件里主要是连接信息注意要配置连接池因为监听器容器会占用连接。下面是application.yml里的关键配置spring: redis: host: 127.0.0.1 port: 6379 password: yourpassword database: 0 timeout: 3000ms lettuce: pool: max-active: 16 max-idle: 8 min-idle: 2 max-wait: 3000ms这里有个关键点Redis发布订阅监听器会独占一个连接持续阻塞等待消息。如果项目里还有其他操作Redis的逻辑连接池必须开大一点否则监听器把连接占完业务操作Redis就会排队等着拿连接拖慢接口响应。4.2 RedisMessageListenerContainer的配置这是整个发布订阅的“心脏”。RedisMessageListenerContainer负责管理订阅连接、把收到的消息分发给对应的监听器。我先看config包里的配置类Configuration public class RedisPubSubConfig { Bean public RedisMessageListenerContainer redisMessageListenerContainer( RedisConnectionFactory connectionFactory, OrderNotifyListener orderNotifyListener, LobbyChatListener lobbyChatListener, UserStatusListener userStatusListener) { RedisMessageListenerContainer container new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); // 关键容器默认不会做序列化需要用Topic 消息监听适配器来指定消息转换器 container.addMessageListener(orderNotifyListener, new PatternTopic(order:notify:v1)); container.addMessageListener(lobbyChatListener, new PatternTopic(lobby:chat:v1)); container.addMessageListener(userStatusListener, new PatternTopic(user:status:v1)); return container; } }PatternTopic支持通配符我用的都是精确频道名但如果你订阅order:*那后续扩展订单状态变更频道时就不需要改代码。通配符模式有个注意点PatternTopic匹配的消息里第一个元素是原始频道名监听器处理时要按这个来区分消息来源。然后定义监听器容器要用的消息转换器。默认的情况下RedisMessageListenerContainer处理消息是把消息的body直接按字节数组交付给监听器所以要设置消息转换器让容器帮忙把字节转成字符串或者对象。这里我推荐直接用StringRedisSerializer消息体统一是JSON字符串收到后在监听器里再用Jackson反序列化成对象。这样最省心不要再去做乱七八糟的序列化。Bean public RedisMessageListenerContainer redisMessageListenerContainer( RedisConnectionFactory connectionFactory) { RedisMessageListenerContainer container new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.setMessageListener(new MessageListenerAdapter(new OrderNotifyListener(), new Jackson2JsonRedisSerializer(Object.class))); // 具体监听器注册方式根据需求定 container.addMessageListener(new MessageListenerAdapter(orderNotifyListener), new PatternTopic(order:notify:v1)); return container; }4.3 发布消息的核心代码发布消息非常简单核心就是redisTemplate.convertAndSend(channel, message)。我用单独的Publisher类封装了一下方便统一处理消息序列化和日志记录。Component public class OrderMessagePublisher { private static final Logger log LoggerFactory.getLogger(OrderMessagePublisher.class); private final StringRedisTemplate redisTemplate; public OrderMessagePublisher(StringRedisTemplate redisTemplate) { this.redisTemplate redisTemplate; } public void publishNewOrder(NewOrderNotifyMessage message) { String channel order:notify:v1; String payload JSON.toJSONString(message); // Fastjson或Jackson都可以 redisTemplate.convertAndSend(channel, payload); log.info(发布新订单通知, channel{}, orderId{}, channel, message.getOrderId()); } }这里有一个特别实操的点发布用StringRedisTemplate不要用RedisTemplate。原因很简单StringRedisTemplate默认的序列化器就是StringRedisSerializer发布出去的字符串到Redis是纯文本用redis-cli的SUBSCRIBE命令肉眼直接能看到消息内容。如果用RedisTemplate默认的JdkSerializationRedisSerializer那发出去的是Java序列化后的二进制对象redis-cli里看到的是一堆乱码调试非常痛苦。我一直是用StringRedisTemplate做推送序列化交给JSON工具。4.4 监听器的代码实现与消息分发监听器要实现MessageListener接口重写onMessage方法。注意onMessage方法签名有两个参数message是整个消息对象包含body、channel两个部分pattern是模式订阅时匹配到的pattern精确订阅时为null。我实际代码里是这样写的Component public class OrderNotifyListener implements MessageListener { private final MessageDispatcher dispatcher; public OrderNotifyListener(MessageDispatcher dispatcher) { this.dispatcher dispatcher; } Override public void onMessage(Message message, byte[] pattern) { String channel new String(message.getChannel(), StandardCharsets.UTF_8); String body new String(message.getBody(), StandardCharsets.UTF_8); log.info(收到订单频道消息, channel{}, body{}, channel, body); dispatcher.dispatch(channel, body); } }Channel在Message对象里就是byte[]类型需要用UTF-8转成字符串。body也是byte[]用String构造方法转成字符串。千万注意字符集默认平台编码在Windows上可能是GBK转出来中文全乱码所以写代码时一定要显式传StandardCharsets.UTF_8。收到消息后我进入一个统一的消息分发器按之前的type字段分发给具体的业务处理方法Component public class MessageDispatcher { private final OrderMessageHandler orderMessageHandler; // ... 其他handler public void dispatch(String channel, String messageBody) { MessageEnvelope envelope JSON.parseObject(messageBody, MessageEnvelope.class); String type envelope.getType(); switch (type) { case new_order: orderMessageHandler.handleNewOrder(envelope.getData()); break; case order_status_change: orderMessageHandler.handleOrderStatusChange(envelope.getData()); break; // ... 其他消息类型 default: log.warn(未知消息类型, type{}, type); } } }这个模式的好处是以后每增加一种消息类型只需要在Dispatcher里加一个case分支然后在对应的Handler里面写业务逻辑监听器完全不用动。4.5 消息体反序列化的坑Java泛型丢失问题这里必须单独拎出来讲因为这是我实际开发中花了大半天才搞定的问题。如果需要把data转换成一个具体的Java对象直接用JSON.parseObject(jsonString, XXX.class)是没有问题的。但如果我是想先解析成MessageEnvelope再从envelope里取data转成具体对象就一定会碰到泛型问题。比如data字段在MessageEnvelope里定义为Object类型从JSON里解析出来以后它实际上是一个JSONObject类型直接强转成NewOrderNotifyMessage会报ClassCastException。解决办法是把data也设计成JSON字符串或者解析的时候用TypeReference指定泛型。我当时是这么处理的信封设计里data本身就是个JSONObject然后在dispatch里面直接通过envelope.getData().toJavaObject(NewOrderNotifyMessage.class)来做转换这样最干净。如果你用的是Jackson可以这么写JsonNode root objectMapper.readTree(messageBody); String type root.get(type).asText(); JsonNode dataNode root.get(data); NewOrderNotifyMessage msg objectMapper.treeToValue(dataNode, NewOrderNotifyMessage.class);不要偷懒直接拿整个messageBody去反序列化成NewOrderNotifyMessage因为信封外面还包着type和ts两个字段反序列化的时候会抛UnrecognizedPropertyException。4.6 Redis Desktop Manager里怎么验证发布订阅功能代码写完以后怎么快速验证最直接的方式就是用客户端工具连接Redis然后手动执行SUBSCRIBE和PUBLISH命令验证。我用的是Redis Desktop Manager新版本里也有命令行工具连接上以后可以直接操作。在Redis Desktop Manager的Console里执行SUBSCRIBE order:notify:v1回车之后会进入等待消息的状态界面会显示订阅成功。然后再开另一个连接或者同一个客户端里再开一个tab也可以直接用redis-cliPUBLISH order:notify:v1 {\type\:\new_order\,\data\:{\orderId\:\TEST001\,\playerId\:1}}如果配置没问题订阅的那个连接会马上收到这条消息屏幕上有返回。这样就能验证频道连通性不需要启动整个系统去扣完完整整的订单流程。用可视化客户端验证有个技巧Redis Desktop Manager的Console不支持多条命令的并发操作如果在一个tab里先SUBSCRIBE就没办法在同一tab再敲PUBLISH命令。所以一定要开两个连接窗口一个订阅一个发布。另外如果你在Redis Desktop Manager里设置过多个数据库database:0、database:1注意订阅和发布必须在同一个数据库下否则也收不到。5. 线上问题排查与避坑实录5.1 连接被占满订阅和读写互相影响第一次把发布订阅部署到测试环境跑了一下午突然发现业务接口大量超时。排查了半天最后是通过Redis的CLIENT LIST命令看到所有连接都处于subscribed状态业务读写的连接全被订阅连接占满了。原因是RedisMessageListenerContainer默认需要为每个订阅者单独建立连接如果项目里同时注册了很多监听器每个监听器都占一条长连接连接池不够用业务操作就抢不到连接了。解决方法是配置连接池时调大max-active同时把监听器合并——不要给每个业务场景都单独注册一个Listener可以把一组频道合并到一个监听器里。5.2 序列化格式不一致收到消息全是乱码这个坑特别经典。我一开始图省事发布端用了StringRedisTemplate监听端却给监听器配了Jackson序列化器结果onMessage里收到的原始body还能正常解析但日志里打印出来全是乱码这不是数据丢了是字节流被错误解析。我的最终方案发布和订阅两端统一用StringRedisTemplate JSON字符串不配置任何自定义消息转换器让消息在Redis里以纯文本形式传送。监听器从message.getBody()拿到byte[]以后一律new String(body, StandardCharsets.UTF_8)转字符串再去用Jackson反序列化。这个方案的优点是链路清晰、日志可读、调试方便缺点是反序列化需要自己写几行代码但这点成本换来的稳定性非常值。5.3 订阅消息丢失服务重启期间发的消息没了这是发布订阅最容易被诟病的地方也是它的本质特性。Redis的Pub/Sub是即发即弃的如果服务端没有订阅者在线这期间发生的所有消息都会丢失。对于订单通知这种“必须送达”的消息光靠发布订阅是不够的。我的处理方式是“发布订阅落库兜底”双通道。发布订阅负责实时推送但同一条消息也会写入MySQL的message_log表。如果客户端长时间没收到推送比如断网重连以后客户端主动调一次“拉取未读消息”的接口把落库的消息拉回来。这样实时性由Redis保证可靠性由MySQL兜底两边互补。这也是我要给所有要用发布订阅做核心业务的人的真心话发布订阅不是消息队列不要试图用它来承载绝对不丢失的可靠性消息。5.4 多实例部署时消息重复处理幂等性必须做陪玩系统一般不会单机部署至少也是两台应用服务器。这时候如果两台服务器都订阅了同一个频道一条消息发出来两台都会收到于是订单通知的处理逻辑会被执行两次可能会导致重复推送、重复发短信、甚至重复扣费。解决这个问题的核心思路是幂等。我在订单消息处理里加了一个基于Redis的分布式锁同一个orderId在处理前先尝试加锁如果加锁失败说明已经有其他实例在处理了当前实例直接丢弃这条消息。加锁的key可以用处理过的业务主键来做锁的过期时间不要太长2-3秒即可因为订单通知的处理逻辑本身很快。// 伪代码消息处理器中的幂等控制 String lockKey lock:order:notify: orderId; boolean locked redisTemplate.opsForValue().setIfAbsent(lockKey, 1, Duration.ofSeconds(3)); if (!locked) { log.info(重复订单通知消息, orderId{}, 忽略处理, orderId); return; } // 业务处理逻辑...5.5 频道数量膨胀与内存问题当客户端比较多、频道又按房间维度拆分时频道数量会膨胀得非常快。大厅聊天室按房间ID建频道房间一多Redis内存里保存的是“频道与订阅者的映射关系”虽然单个订阅关系占不了多少内存但架不住量大。我的处理经验是最大限度复用频道不搞太细的频道粒度。大厅公屏聊天虽然按房间拆开更精确但实际用下来发现如果房间ID非常多这些频道的调度对Redis连接的管理也是个负担。后来我把公屏聊天改成按业务域建一个频道消息里带上roomId字段客户端收到消息后用本地逻辑过滤一遍只显示自己房间的消息。这个方案在消息量不大的情况下性能损失可以忽略但对连接管理带来的简化是非常明显的。如果消息量真的大到要按房间拆分频道那建议直接上专业的IM组件或消息队列别再硬扛Redis了。6. 发布订阅之外的另一种选择Stream与消息队列对比6.1 Redis Pub/Sub vs Redis Stream vs MQTT vs 消息队列很多人在热词里会搜到Redis Stream或者MQTT也会纠结到底该用哪套。我把四个方面放在一张表里看起来最直观对比项Redis Pub/SubRedis StreamMQTTRabbitMQ/Kafka消息持久化无离线即丢有可回溯消费有遗嘱/保留消息取决于Broker有按策略保留消费确认ACK无有有QoS机制有重复消费一定会重复广播可控制取决于QoS可控制实现复杂度极低几行代码中等需要管理消费组高需要部署Broker高需要部署集群延迟毫秒级毫秒级毫秒级毫秒级运维成本零复用Redis零复用Redis中要部署EMQX等高要维护集群MQTT其实跟Redis的定位不同MQTT是物联网领域的轻量级消息协议有Broker做消息的路由和保留支持遗嘱消息适合网络波动大的IoT设备。如果你要做的不是陪玩系统而是硬件上报数据MQTT是更专业的选择。但放在Web服务器场景里为了用MQTT还要单独部署一个Broker就有点杀鸡用牛刀了。Kafka/RabbitMQ在可靠性方面碾压Redis但如果只是订单通知、公屏聊天这种可以容忍偶发丢失的实时推送部署一套Kafka集群的成本比Redis发布订阅高了一个量级而且只有一两台机器的时候Kafka的性能压根跑不出来。6.2 我的选型建议什么阶段用什么方案如果你的系统还在起步阶段Redis本身就是标配那直接上Redis发布订阅是最合理的开发成本低、迭代快。如果业务量上来了对消息的可靠性要求越来越高比如支付通知、订单状态通知不能丢但又不是海量消息那就在发布订阅外面加落库兜底不换技术栈。如果消息量非常大、客户端连接数多或者需要消息回溯、消费确认这时候才真正需要换消息队列。我给的建议是先确认瓶颈到底在哪里而不是看到别人用Kafka就跟着用。我用发布订阅跑到一万多在线用户的时候Redis的CPU占用率也不过5%左右离需要换技术栈还有很大距离。7. 关于这套方案我的最终实践心得写了这么多最后说点掏心窝子的体会。Redis发布订阅这套方案在陪玩、约拍、代练这种C2C实时交易场景里确实是目前性价比最高的实时推送实现方式。一句话总结它的定位它适合做广播通知不适合做可靠消息投递。你把它的边界搞清楚用在对的地方就非常好用用错了地方硬拿它当消息队列使就会踩到消息丢失、重复消费这些让人头疼的坑。我在这个项目里学到最重要的经验不是怎么写RedisMessageListenerContainer而是“实时推送”这个需求本身要分两层看第一层是消息的即时触达这部分交给Redis发布订阅第二层是消息的可靠兜底这部分必须靠落库和客户端主动拉取来保证。两层配合才能既快又稳缺一个都不行。刚开始写这套功能的时候我也天真地以为“推送出去就完事了”直到线上真的丢了几个订单通知、被运营追着问罪才老老实实把落库兜底补上。技术选型的本质不是选一个完美的工具而是搞清楚你手里的工具到底适合解决什么问题然后再去构建一套能把短板补上的完整方案。最后再分享一个小技巧我在排查发布订阅问题时最常用的三条指令PUBSUB CHANNELS查看当前活动的频道、PUBSUB NUMSUB查看频道的订阅者数量、CLIENT LIST查看客户端连接状态。这三个命令能帮你快速判断消息到底发到了哪个频道、有多少人在听、连接是不是还活着。建议把这几个命令贴在工位旁边排查问题的时候翻出来用比啥都管用。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门