Redis Pub/Sub 使用与底层原理全解析:从基础操作到源码剖析

1. 引言

在现代互联网应用中,实时通信和事件驱动架构越来越普遍。例如,聊天系统、实时监控平台、日志聚合系统以及任务队列,都需要高效、低延迟的消息传递机制。**Redis Pub/Sub(Publish/Subscribe,发布/订阅)**作为 Redis 提供的轻量级消息通信方案,能够满足这些需求。它通过发布者(Publisher)发送消息,订阅者(Subscriber)接收消息,实现解耦的消息广播体系。

Redis Pub/Sub 的核心特点包括:

  • 低延迟、高吞吐:消息直接在内存中传递,无需额外消息队列组件,延迟极低,适合实时性要求高的场景。

  • 轻量级:不需要复杂的消息存储和消费确认机制,系统开销小。

  • 多语言支持:几乎所有主流编程语言都提供了 Redis Pub/Sub 客户端库。

然而,Pub/Sub 也存在一些固有限制:

  • 消息不可持久化:如果订阅者未在线,消息将会丢失。

  • 订阅阻塞:订阅操作会阻塞客户端,影响并发处理。

  • 不保证消息顺序:在高并发环境下,消息可能出现乱序。

因此,理解 Pub/Sub 的使用方法与底层实现逻辑,对于设计高性能、可靠的实时系统至关重要。


1.1 Pub/Sub 流程概览(文字版图示)

下面用文字表示 Redis Pub/Sub 的消息流动过程:

+----------------+       SUBSCRIBE       +----------------+
|  Subscriber 1  |<--------------------->|   Redis Server |
+----------------+                       +----------------+
       |                                      ^
       |                                      |
       |                                      |
+----------------+                            |
|  Subscriber 2  |<---------------------------+
+----------------+
       ^
       |
       | PUBLISH "message"
       |
+----------------+
|   Publisher    |
+----------------+

流程说明:

  1. 订阅阶段:客户端(Subscriber)发送 SUBSCRIBE channel 请求,Redis 在内部维护一个订阅者列表。

  2. 发布阶段:发布者(Publisher)向 Redis 发送 PUBLISH channel "message"

  3. 广播阶段:Redis 遍历订阅者列表,将消息广播到所有订阅了该频道的客户端。

  4. 接收阶段:订阅者收到消息,并可进行相应处理(如更新界面、触发事件等)。

这个简单的流程概览展示了 Pub/Sub 的核心机制:发布者无需关心具体订阅者,订阅者无需关心消息来源,实现了高度解耦的通信模型。

2.1 Pub/Sub 概念与核心原理

Pub/Sub(发布/订阅)模式是消息中间件中的一种通信模式,其核心思想是:

  • 发布者(Publisher):负责发送消息,不关心谁会接收。

  • 订阅者(Subscriber):订阅感兴趣的频道(channel),等待接收消息。

  • 频道(Channel):消息的传递载体,用于标识消息主题。

Redis Pub/Sub 的实现原理:

  1. 订阅维护:Redis 使用内部字典(dict)维护每个频道对应的订阅者列表,同时每个客户端也维护自己订阅的频道列表。

  2. 消息广播:当发布者发送消息时,Redis 根据频道查找订阅者列表,并遍历列表将消息发送给每个客户端。

  3. 模式订阅:通过 PSUBSCRIBE,客户端可以使用通配符匹配多个频道,实现批量订阅。

内部结构示意(文字版):

channel_dict {
    "news": [client1, client3],
    "chat": [client2, client4],
    ...
}

client_channels {
    client1: ["news"],
    client2: ["chat"],
    client3: ["news"],
    client4: ["chat", "news"]
}

2.2 使用场景与应用案例

Redis Pub/Sub 常见场景包括:

场景说明示例
实时通信适合聊天室、即时消息系统WebSocket 消息推送
事件驱动系统内部事件广播用户注册通知、日志告警
日志聚合将不同服务日志汇总到监听服务ELK 数据采集
任务队列轻量级任务分发定时任务、异步执行

提示:对于需要消息持久化或保证顺序的任务队列,Pub/Sub 并不适合,需要使用 Redis Streams 或 Kafka。


2.3 基础操作命令详解

2.3.1 SUBSCRIBE / UNSUBSCRIBE

  • SUBSCRIBE channel1 channel2 ...
    订阅一个或多个频道,客户端会阻塞等待消息。

  • UNSUBSCRIBE channel1 channel2 ...
    取消订阅一个或多个频道。

Java 示例:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;

public class RedisPubSubExample {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);

        JedisPubSub subscriber = new JedisPubSub() {
            @Override
            public void onMessage(String channel, String message) {
                System.out.println("Received message: " + message + " from channel: " + channel);
            }
        };

        System.out.println("Subscribing to channel 'news'...");
        jedis.subscribe(subscriber, "news");
        // 程序会阻塞在此等待消息
    }
}

运行说明

  1. 程序连接本地 Redis。

  2. 订阅 news 频道。

  3. 当其他客户端向 news 频道发布消息时,会触发 onMessage 回调并打印内容。


2.3.2 PUBLISH

  • PUBLISH channel message
    向指定频道发送消息,所有订阅该频道的客户端都会接收到。

Java 示例:

import redis.clients.jedis.Jedis;

public class RedisPublish {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);

        String channel = "news";
        String message = "Hello Redis Pub/Sub!";
        long receivers = jedis.publish(channel, message);

        System.out.println("Message sent to " + receivers + " subscribers.");
    }
}

运行说明

  • 发布消息后,Redis 返回接收到该消息的订阅者数量。

  • 所有订阅 news 的客户端都会收到消息。


2.4 模式订阅与通配符规则

Redis 支持 模式订阅(Pattern Subscription),允许客户端通过通配符订阅多个频道:

  • PSUBSCRIBE pattern1 pattern2 ...
    支持 * 匹配任意字符,例如 news.* 可以匹配 news.sportsnews.tech

Java 示例:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;

public class RedisPatternSubscribe {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);

        JedisPubSub subscriber = new JedisPubSub() {
            @Override
            public void onPMessage(String pattern, String channel, String message) {
                System.out.println("Pattern matched: " + pattern + ", channel: " + channel + ", message: " + message);
            }
        };

        System.out.println("Pattern subscribing to 'news.*'...");
        jedis.psubscribe(subscriber, "news.*");
    }
}

匹配规则说明

通配符含义
*匹配任意字符,包括空字符
?匹配单个字符
[]匹配指定字符集合中的单个字符

模式订阅适合批量监听类似频道,但也可能增加 Redis 内存开销,因为每个模式也会维护订阅者列表。

3. 消息结构与序列化机制

Redis Pub/Sub 的消息系统虽然轻量,但涉及到客户端与服务器之间的通信格式、消息存储结构以及序列化解析机制,这对理解性能和扩展性非常重要。


3.1 消息格式与传输过程

Redis Pub/Sub 的消息在内部是以 RESP 协议(Redis Serialization Protocol) 传输的。每条消息包含以下核心信息:

  1. 消息类型:标识该消息是订阅确认、普通消息或模式消息。

  2. 频道名称:消息所属的频道。

  3. 消息内容:实际发送的数据。

消息类型示意(RESP 协议表示)

  • 普通消息(来自 PUBLISH):

    *3
    $7
    message
    $4
    news
    $21
    Hello Redis Pub/Sub!
    

解释:

  • *3 表示数组长度为 3。

  • $7 表示字符串长度为 7,内容为 message(消息类型)。

  • $4 表示频道长度为 4,内容为 news

  • $21 表示消息内容长度为 21,内容为 Hello Redis Pub/Sub!

  • 模式消息(来自 PSUBSCRIBE):

    *4
    $8
    pmessage
    $6
    news.*
    $4
    news
    $21
    Hello Redis Pub/Sub!
    


3.2 序列化与解析方式

Redis Pub/Sub 本身 不对消息内容进行序列化,消息是以字符串或二进制形式原样发送的。因此:

  • 文本数据:直接发送 UTF-8 编码字符串。

  • 对象/复杂数据:需要客户端进行序列化(如 JSON、ProtoBuf、Kryo 等)。

  • 二进制数据:可以直接发送字节数组。

Java 示例:发送和接收 JSON 对象

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
import com.google.gson.Gson;

class NewsMessage {
    String title;
    String content;

    public NewsMessage(String title, String content) {
        this.title = title;
        this.content = content;
    }
}

public class RedisJsonPubSub {
    public static void main(String[] args) {
        Gson gson = new Gson();
        Jedis jedis = new Jedis("localhost", 6379);

        // 订阅者线程
        new Thread(() -> {
            Jedis subscriberJedis = new Jedis("localhost", 6379);
            subscriberJedis.subscribe(new JedisPubSub() {
                @Override
                public void onMessage(String channel, String message) {
                    NewsMessage news = gson.fromJson(message, NewsMessage.class);
                    System.out.println("Received news: " + news.title + " - " + news.content);
                }
            }, "news");
        }).start();

        // 发布者发送 JSON 对象
        NewsMessage news = new NewsMessage("Redis Pub/Sub", "消息结构与序列化机制解析");
        String jsonMessage = gson.toJson(news);
        long receivers = jedis.publish("news", jsonMessage);
        System.out.println("Message sent to " + receivers + " subscribers.");
    }
}

运行说明

  1. 订阅者使用 subscribe 阻塞接收消息,并将接收到的 JSON 字符串反序列化为对象。

  2. 发布者将对象序列化为 JSON 并通过 publish 发送。

  3. 这样可以保证消息内容复杂对象在客户端间安全传递。


关键点总结

  • Redis Pub/Sub 只传输原始字符串/字节数组,不会进行自动序列化。

  • 对象或复杂类型消息需要应用层进行序列化和反序列化。

  • RESP 协议定义了消息的传输格式,保证客户端和服务器之间解析一致。

  • 模式订阅消息(PSUBSCRIBE)额外返回匹配模式信息。

4. Redis Pub/Sub 源码解析

理解 Redis Pub/Sub 的源码,对于深入掌握其性能特性、优化方案及潜在问题非常关键。下面从三个方面进行解析:订阅者列表维护机制、消息广播流程、核心命令实现。


4.1 订阅者列表维护机制

Redis 使用 字典(dict)和链表 来维护 Pub/Sub 订阅关系,核心数据结构如下(来自 server.h 中定义):

/* channel -> list of clients */
dict *pubsub_channels;  

/* client -> list of patterns */
list *pubsub_patterns;  

结构说明

  1. pubsub_channels

    • key:频道名称(channel)

    • value:一个链表,存放所有订阅该频道的客户端指针

  2. pubsub_patterns

    • 存储客户端订阅的模式(pattern),每个元素包含客户端指针和模式字符串

流程示意(文字版)

pubsub_channels:
  "news" -> [client1, client3]
  "chat" -> [client2]

pubsub_patterns:
  client4 -> ["news.*"]

当客户端发送 SUBSCRIBEPSUBSCRIBE 时,Redis 会更新这两个数据结构。


4.2 消息广播流程

当发布者调用 PUBLISH channel message 时,Redis 的广播流程如下:

  1. 查找订阅者

    • 根据 channelpubsub_channels 中查找对应链表

  2. 向客户端发送消息

    • 遍历链表,对每个订阅者调用 addReply 将消息压入客户端输出缓冲区

  3. 模式匹配广播

    • 遍历 pubsub_patterns,使用通配符匹配函数 stringmatchlen 检查模式是否匹配该频道

    • 匹配成功的客户端也发送消息

伪代码流程:

publishCommand(channel, message) {
    /* 普通订阅 */
    clients = dictFetch(pubsub_channels, channel)
    for each client in clients:
        addReply(client, message)

    /* 模式订阅 */
    for each patternClient in pubsub_patterns:
        if stringmatch(patternClient.pattern, channel):
            addReply(patternClient.client, message)
}

通过这种方式,Redis 实现了高效的广播机制,时间复杂度约为 O(N),N 为订阅该频道的客户端数量。


4.3 核心函数剖析

4.3.1 subscribeCommand

位于 src/pubsub.c,处理客户端订阅请求。

主要步骤:

  1. 遍历命令参数,逐个频道订阅

  2. 更新客户端结构 c->pubsub_channels

  3. 更新服务器字典 server.pubsub_channels

  4. 给客户端返回订阅确认消息

关键代码片段(简化):

void subscribeCommand(client *c) {
    for (int j = 1; j < c->argc; j++) {
        sds channel = c->argv[j]->ptr;
        dictAdd(server.pubsub_channels, channel, c);
        listAddNodeTail(c->pubsub_channels, channel);
        addReply(c, shared.mbulklen[3]); // 发送确认消息
    }
}

核心点:dictAdd 将客户端加入频道链表,listAddNodeTail 记录客户端自己的订阅列表,确保双向管理。


4.3.2 publishCommand

处理消息发布,核心步骤如下:

  1. 查找 pubsub_channels 获取普通订阅者

  2. 向订阅者发送消息(addReply

  3. 遍历 pubsub_patterns,匹配模式并发送消息

  4. 返回接收到消息的客户端数量

关键代码片段(简化):

void publishCommand(client *c) {
    sds channel = c->argv[1]->ptr;
    robj *message = c->argv[2];

    int receivers = 0;

    list *clients = dictFetch(server.pubsub_channels, channel);
    for (client *sub = clients->head; sub != NULL; sub = sub->next) {
        addReply(sub, message);
        receivers++;
    }

    for (patternClient *pc = server.pubsub_patterns->head; pc != NULL; pc = pc->next) {
        if (stringmatchlen(pc->pattern, channel)) {
            addReply(pc->client, message);
            receivers++;
        }
    }

    addReplyLongLong(c, receivers); // 返回接收数量
}

4.4 小结

  • 订阅关系双向维护channel -> clientsclient -> channels,方便快速广播和取消订阅。

  • 广播流程高效:普通订阅和模式订阅分开处理,使用链表和字典保证 O(1) 插入与 O(N) 广播。

  • 核心函数简洁明了subscribeCommandpublishCommand 都非常直观,但在高并发场景下要注意输出缓冲区可能阻塞。

通过理解源码,可以为后续性能优化(如 Lua 脚本结合 Pub/Sub、异步处理客户端消息)打下坚实基础。

5. 性能与限制分析

Redis Pub/Sub 以其轻量级和低延迟著称,但在高并发和复杂场景下仍存在性能瓶颈和功能限制。本章将详细解析这些问题,并提供优化思路。


5.1 吞吐量与延迟瓶颈

1. 单线程瓶颈

Redis Pub/Sub 与 Redis 核心一样,采用 单线程事件循环ae.c + epoll/kqueue/select)处理客户端命令。

  • 发布消息时,publishCommand 会遍历订阅列表,将消息写入客户端输出缓冲区。

  • 如果订阅者数量多或者输出缓冲区满,会导致阻塞,影响吞吐量。

影响因素:

因素描述
订阅者数量N 个订阅者 → 广播复杂度 O(N)
客户端输出缓冲区阻塞或慢速客户端会拖慢发布速度
消息大小大消息会增加 I/O 压力

小结:Pub/Sub 适合订阅者数量有限、消息体较小、延迟敏感的场景。


2. 网络与 I/O 压力

  • Redis 使用异步 socket 写入客户端缓冲区,但缓冲区溢出时会阻塞事件循环。

  • 高速消息发布可能导致慢订阅者阻塞,出现延迟抖动。

优化策略:

  1. 合理拆分频道:将不同类型消息分散到多个频道,避免单频道订阅者过多。

  2. 客户端消费快:确保订阅者及时消费消息,避免输出缓冲区积压。

  3. 使用 Redis Cluster:在多节点分片中分散负载,提高整体吞吐量。


5.2 消息丢失与持久化缺失

Redis Pub/Sub 是 内存级的实时广播机制,存在以下限制:

  1. 消息不可持久化

    • 如果订阅者不在线,发布的消息会直接丢失。

  2. 客户端断开

    • 突然断开或网络抖动,会导致客户端错过消息。

  3. Redis 重启

    • 内存消息无法恢复。

解决方案:

方案优缺点
使用 Redis Streams消息持久化、可回溯、支持消费者组,但延迟略高
外部队列(Kafka、RabbitMQ)强持久化、高吞吐,但系统复杂度高
客户端缓存订阅者在重连时从缓存拉取未处理消息,但需要额外逻辑

核心思路:Pub/Sub 适合 实时性强、可容忍丢失的场景;如果业务对可靠性要求高,应结合 Streams 或队列。


5.3 优化策略

1. 限制订阅者数量

  • 避免单频道订阅者过多,可将频道拆分为更细粒度的子频道。

  • 对模式订阅(PSUBSCRIBE)慎用,匹配计算可能消耗 CPU。

2. 控制消息体大小

  • 小消息可提高广播效率。

  • 对大对象建议先序列化为二进制或 JSON,并压缩传输。

3. 异步处理发布

  • 可使用 Lua 脚本pipeline 批量发送消息,减少网络往返。

  • 结合多线程客户端消费,避免阻塞发布者线程。

4. 客户端端优化

  • 使用非阻塞 I/O 的客户端库,及时消费输出缓冲区。

  • 对慢订阅者可以设置输出缓冲区上限,超限则断开,保证整体吞吐量。

5. Redis Cluster 与分片

  • 将不同频道分配到不同节点,降低单节点压力。

  • 对大规模消息系统,可结合 Cluster 提高横向扩展能力。


5.4 小结

  • 优点:延迟低、使用简单、内存直通,适合实时通信。

  • 限制:消息不可持久化、订阅阻塞、单节点吞吐量受限。

  • 优化方向:拆分频道、控制消息大小、异步发布、客户端优化、集群扩展。

6. 高级特性与扩展应用

随着业务复杂度增加,Pub/Sub 的一些高级特性和扩展应用变得尤为重要。本章将从三个方面深入分析。


6.1 与 Redis Streams 对比

Redis Streams 是 Redis 5.0 引入的 持久化消息队列机制,与 Pub/Sub 在设计目标和使用场景上存在明显差异:

特性Pub/SubRedis Streams
消息持久化不持久化,内存级广播持久化,消息保存在内存和 AOF/RDB
消费者模型无状态订阅者消费者组,可回溯、消费确认
消息顺序不保证顺序保证顺序,可顺序消费
延迟极低稍高(磁盘或内存操作)
适用场景实时通信、事件通知任务队列、可靠日志收集

总结

  • 对实时性要求高、允许丢失的场景,用 Pub/Sub 即可。

  • 对可靠性和顺序要求高的场景,应使用 Redis Streams。

  • 可以将 Pub/Sub 与 Streams 结合:实时广播 + 持久化存储。


6.2 Lua 脚本优化 Pub/Sub

在高并发场景下,通过 Lua 脚本可以批量发布消息或结合业务逻辑处理,减少客户端与 Redis 交互次数:

示例场景:一次性向多个频道发布消息

import redis.clients.jedis.Jedis;

public class RedisLuaPubSub {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);

        String luaScript =
                "for i=1,#KEYS do " +
                "   redis.call('PUBLISH', KEYS[i], ARGV[1]) " +
                "end " +
                "return #KEYS";

        String[] channels = {"news", "chat", "alerts"};
        Object result = jedis.eval(luaScript, java.util.Arrays.asList(channels), java.util.Collections.singletonList("System update"));
        System.out.println("Message sent to " + result + " channels.");
    }
}

运行说明

  1. Lua 脚本在 Redis 内部执行,减少了多次网络往返。

  2. 一次调用即可向多个频道广播消息。

  3. 返回值表示实际广播的频道数量。

适合 批量发布、事件广播、或需要在发布前进行业务逻辑判断的场景。


6.3 多语言客户端示例(Java)

虽然本博客只提供 Java 示例,但在实际开发中,Pub/Sub 的跨语言支持非常广泛。

6.3.1 高级 Java 示例:聊天室广播

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;

public class ChatRoomPubSub {
    public static void main(String[] args) {
        Jedis jedis = new Jedis("localhost", 6379);

        // 聊天室订阅者
        new Thread(() -> {
            Jedis subscriberJedis = new Jedis("localhost", 6379);
            subscriberJedis.subscribe(new JedisPubSub() {
                @Override
                public void onMessage(String channel, String message) {
                    System.out.println("[" + channel + "] " + message);
                }
            }, "room:1", "room:2"); // 多频道订阅
        }).start();

        // 发布消息
        jedis.publish("room:1", "Hello Room 1!");
        jedis.publish("room:2", "Hello Room 2!");
        jedis.publish("room:1", "User2 joined Room 1.");
    }
}

关键点

  • 支持一次订阅多个频道,方便聊天室等场景。

  • 消息按频道广播,客户端接收到后可直接更新界面或业务逻辑。

  • 可结合 Lua 脚本实现群发或消息过滤。


6.4 高级应用总结

  • 实时消息分发:结合频道策略,可实现聊天室、推送通知、监控事件广播。

  • 批量操作优化:Lua 脚本减少网络交互,提升高并发性能。

  • 跨语言协作:Java、Python、Node.js 等客户端均可订阅同一频道,实现异构系统实时通信。

  • 与 Streams 联动:Pub/Sub 可作为即时广播层,Streams 作为可靠消息存储层,实现“实时 + 持久化”架构。

7. 常见问题与解决方案

Redis Pub/Sub 轻量、高效,但在复杂或高并发环境下,开发者可能会遇到一些问题。本章总结了最常见的三类问题,并提供解决策略。


7.1 订阅阻塞问题

问题描述

  • Redis 的 SUBSCRIBEPSUBSCRIBE 会阻塞客户端,直到客户端取消订阅或断开连接。

  • 阻塞意味着客户端无法同时执行其他命令,可能导致业务逻辑阻塞。

解决方案

  1. 独立线程订阅

    • 将订阅逻辑放在独立线程中,避免阻塞主业务线程。

      new Thread(() -> {
          Jedis jedis = new Jedis("localhost", 6379);
          jedis.subscribe(new JedisPubSub() {
              @Override
              public void onMessage(String channel, String message) {
                  System.out.println("Received: " + message);
              }
          }, "news");
      }).start();
      

  2. 使用非阻塞客户端

    • 例如 Jedis 3.x 可以结合异步库或 Lettuce 客户端,利用事件驱动处理消息。


7.2 消息乱序问题

问题描述

  • 高并发下,尤其是多个发布者同时向同一频道发送消息时,订阅者接收的顺序可能与发送顺序不完全一致。

  • 这是由于 单线程事件循环 + 网络传输延迟 导致的。

解决方案

  1. 消息带序号

    • 发布消息时附加自增序号,订阅者收到后根据序号处理顺序。

      String message = "1|Hello";
      jedis.publish("news", message);
      

  2. 使用 Redis Streams

    • 如果对顺序要求严格,应考虑使用 Streams,支持顺序消费和消费者组。


7.3 客户端断开与重连

问题描述

  • 客户端网络抖动或重启会导致断开,未处理消息会丢失。

  • 重连后需要重新订阅频道,否则无法接收后续消息。

解决方案

  1. 自动重连逻辑

    • 在客户端实现重连机制,并在重连后重新订阅频道。

      while (true) {
          try (Jedis jedis = new Jedis("localhost", 6379)) {
              jedis.subscribe(new JedisPubSub() {
                  @Override
                  public void onMessage(String channel, String message) {
                      System.out.println(message);
                  }
              }, "news");
          } catch (Exception e) {
              System.out.println("Disconnected, retrying...");
              try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
          }
      }
      

  2. 结合消息缓存

    • 对重要消息,可在发布端或中间层缓存一定时间,客户端重连后拉取未接消息。

    • 可结合 Redis Streams 实现“即时 + 持久化”架构。


7.4 小结

问题原因解决方案
订阅阻塞SUBSCRIBE 阻塞线程独立线程订阅、异步客户端
消息乱序高并发 + 网络延迟消息序号、使用 Streams
客户端断开网络或客户端异常自动重连、缓存或 Streams

核心思路:Pub/Sub 保证实时性,但不保证可靠性和顺序。对于关键业务,可结合 Streams 或其他队列,同时在客户端加入重连、缓存和序号机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

探索java

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值