redis源码阅读笔记-主从复制

文章预读:

  • 当为socket描述符fd新建可写事件,回调函数callbak,则当前服务器aewait等待时,必然会监听到该描述符可写,立马执行回调函数。
  • rdbSaveRio,可以看做讲信息保存在fd中,当这个fd是文件描述符时,即保存文件,当fd是socket描述符时,即将信息发送给fd指向的客户端。

主从复制小知识:

  • 前提:A为主节点,B为从节点,只有这样一对主从关系
  • 当B是空的从节点,B请求A主从复制,A后台子进程立马保存该时刻快照,将快照发送给B从节点。B节点能保证与生成快照那个时间点的A节点保存数据一致性,若是在生成快照之后,A节点新增/修改/删除了数据,则会将数据发送给从节点,并将数据备份在主节点的复制积压缓冲区中。该过程即为全量复制。
  • 当B和A已经全量复制过了,A、B数据保持一致着,若B节点断线了一会儿,意味着在这个时间段内,A、B节点数据可能不一致,当B重新连上时,会去请求A把该时间段复制积压缓存区新增的信息同步给B节点,复制积压缓冲区是个循环队列,大小默认1MB,当断线时间段A新增信息大于1MB时,请求增量同步无效,只能进行全量同步。否则,即为部分同步。

1、B-client客户端slaveof请求

从节点B-client 终端发起slaveof 192.168.1.1 6379命令

2、B-server响应,重置B从节点服务器

节点B-server对slaveof命令响应操作,对节点B进行初始化设置,清空所有之前可能已主从复制占用的空间。slaveofCommand

节点B的状态就以下三种情况讨论:
状态1:B节点是个单机节点,未与其他节点建立主从关系
状态2:B节点已建立/正在建立主从关系,B是主节点
状态3:B节点已建立/正在建立主从关系,B是从节点

replicationSetMaster,初始化B-server节点。

  • 设置masterhost和mastertport
  • B-server处于状态2,关闭所有连接该B-server的从节点,清空repl_backlog复制积压缓冲区
  • B-server处于状态3,删除与主节点连接的client,清空主节点缓存。cancelReplicationHandshake清空主从建立过程中占用的开销
  • 设置主从复制状态repl_state = REPL_STATE_CONNECT
	server.repl_state = REPL_STATE_CONNECT;
	server.master_repl_offset = 0;
	server.repl_down_since = 0;

回复B-client终端ok

3、B-server定时检查,socket非阻塞连接主节点

B-server时间事件,周期性执行serverCron,默认100ms执行一次(1000/server.hz)。

run_with_period(1000) replicationCron();//每1000ms时,定时调用主从复制检查函数

replicationCron函数,检测到状态为REPL_STATE_CONNECT

if (server.repl_state == REPL_STATE_CONNECT) {
        serverLog(LL_NOTICE,"Connecting to MASTER %s:%d",
            server.masterhost, server.masterport);
        if (connectWithMaster() == C_OK) {
            serverLog(LL_NOTICE,"MASTER <-> SLAVE sync started");
        }
    }

connectWithMaster函数,非阻塞连接masterhost、masterport节点。socket连接成功,为该fd新增可读可写事件,回调函数syncWithMaster,设置以下状态。为新的fd监听事件新增可写状态,监听到事件可写,进入回调函数。

	aeCreateFileEvent(server.el,fd,AE_READABLE|AE_WRITABLE,syncWithMaster,NULL) ==AE_ERR
    server.repl_transfer_lastio = server.unixtime;//该值在replicationCron中,定时检测主从是否保持连接/是否超时
    server.repl_transfer_s = fd;//连接的socket
    server.repl_state = REPL_STATE_CONNECTING;

4、主从复制连接准备工作

syncWithMaster函数,为主从复制准备前提。
以下从节点发送的命令都是内联命令:

4.1 发送PING
  • >>>>处于REPL_STATE_CONNECTING状态,删除可写事件,向主节点发送"PING\r\n",发送完毕进入等待PONG接收状态
  • <<<<主节点回复"+PONG",消息走向,readQueryFromClient->processInputBuffer->processCommand->pingCommand
  • <<<<处于REPL_STATE_RECEIVE_PONG状态,等待监听事件可读,进入syncWithMaster。读取主节点响应,更新repl_transfer_lastio值,防止replicationCron定时检测主从连接超时。
    响应消息正确,进入auth待发送状态。
    响应不正确,关闭套接字,删除事件,更新状态为REPL_STATE_CONNECT,走流程3.
4.2 发送AUTH,验证身份

从节点设置了masterauth主节点授权密码,发送AUTH命令,否则进入下一个状态。

  • >>>>处于REPL_STATE_SEND_AUTH状态,向主节点发送"AUTH password",发送完毕进入AUTH接收状态
  • <<<<主节点回复"+OK",更新从节点客户端authenticated已授权,消息走向,readQueryFromClient->processInputBuffer->processCommand->authCommand
    在主节点,会为每一个从节点维护一个redisClient客户端,用于保存从节点客户端信息
  • <<<<处于REPL_STATE_RECEIVE_AUTH状态,等待监听事件可读,进入syncWithMaster。读取主节点响应,更新repl_transfer_lastio值,防止replicationCron周期性检测超时。
    响应消息正确,进入下一个状态。
    响应不正确,关闭套接字,删除事件,更新状态为REPL_STATE_CONNECT,走流程3.
4.3 发送端口号、IP地址、能力
  • >>>>处于【REPL_STATE_SEND_PORT】/【REPL_STATE_SEND_IP】/【REPL_STATE_SEND_CAPA】状态,向主节点发送【“REPLCONF listening-port 6379”】/【“REPLCONF ip-address ip”】/【REPLCONF capa eop】,发送完毕,进入接收状态
  • <<<<主节点回复"+OK",保存从节点【端口号slave_listening_port】/【ip地址slave_ip】/【能力slave_capa=SLAVE_CAPA_EOF】,消息走向,readQueryFromClient->processInputBuffer->processCommand->replconfCommand

SLAVE_CAPA_EOF能力,即支持EOF流形式,代表主从复制选择方式为无盘socket方式。后面会讲到,数据复制有两种形式,文件保存在主节点本地,发送文件,和直接基于socket发送。

  • <<<<处于【REPL_STATE_RECEIVE_PORT】/【REPL_STATE_RECEIVE_IP】/【REPL_STATE_RECEIVE_CAPA】状态,等待监听事件可读,进入syncWithMaster。读取主节点响应,更新repl_transfer_lastio值,防止replicationCron周期性检测超时。
    响应消息正确,进入下一个状态。
    响应不正确,忽略该影响,进入下一个状态。
4.4 发送PSYNC,请求全量同步/部分同步
  • >>>>处于REPL_STATE_SEND_PSYNC状态,server.cached_master缓存存在,就进行部分同步,向主节点发送"PSYNC replrund reploff";否则进行全量同步,向主节点发送"PSYNC ?-1"。进入接收状态。

第一次进入该函数,server.cached_master是空的,只有当主从节点完全复制成功后,当从节点,突然断线,会将从节点信息保存在cached_master中,当从节点再次连上时,从缓存信息保留的中断复制点请求增量同步。

   server.repl_master_initial_offset = -1;
   if (server.cached_master) {
       psync_runid = server.cached_master->replrunid;
       snprintf(psync_offset,sizeof(psync_offset),"%lld", server.cached_master->reploff+1);
       serverLog(LL_NOTICE,"Trying a partial resynchronization (request %s:%s).", psync_runid, psync_offset);
   } else {
       serverLog(LL_NOTICE,"Partial resynchronization not possible (no cached master)");
       psync_runid = "?";
       memcpy(psync_offset,"-1",3);
   }
  • <<<<主节点回复"+OK",为需要进行全量/部分同步的从节点客户端设置状态,消息走向,readQueryFromClient->processInputBuffer->processCommand->syncCommand

(1)判断是否能进行全量/部分同步

	if (c->flags & CLIENT_SLAVE) return;
    if (server.masterhost && server.repl_state != REPL_STATE_CONNECTED) {
        addReplyError(c,"Can't SYNC while not connected with my master");
        return;
    }
    //从客户端回复缓冲区不能用偶数据
    if (clientHasPendingReplies(c)) {
        addReplyError(c,"SYNC and PSYNC are invalid with pending output");
        return;
    }

(2)判断全量还是部分同步masterTryPartialResynchronization
部分同步条件:从节点传递的第二个参数psync_offset,在一个临界区间内。设置状态,给从节点回复"+CONTINUE\r\n"。

	c->flags |= CLIENT_SLAVE;
	c->replstate = SLAVE_STATE_ONLINE;
	c->repl_ack_time = server.unixtime;
	c->repl_put_online_on_ack = 0;
	buflen = snprintf(buf,sizeof(buf),"+CONTINUE\r\n");
	if (write(c->fd,buf,buflen) != buflen) {
	    freeClientAsync(c);
	    return C_OK;
	}
	psync_len = addReplyReplicationBacklog(c,psync_offset);

循环复制缓冲区大小1Mb。psync_offset<缓冲区目前接收最大的字节数下标,且psync_offset> 缓冲区接收最大字节数下标-缓冲区接收最大字节数(大于1024 * 1024时取1024*1024)。addReplyReplicationBacklog,将从节点缺失部分,psync_offset~缓冲区接收最大字节数下标该区间,放入客户端发送缓冲区,发送给客户端。

否则,进行全量同步,设置状态。

	 c->replstate = SLAVE_STATE_WAIT_BGSAVE_START;
	 if (server.repl_disable_tcp_nodelay)
	     anetDisableTcpNoDelay(NULL, c->fd); /* Non critical if it fails. */
	 c->repldbfd = -1;
	 c->flags |= CLIENT_SLAVE;
	 listAddNodeTail(server.slaves,c);

全量同步有三种情况

  • a. 后台有运行中的bgsave进程,且有盘复制
    在所有子节点中,找到运行bgsave进程的slave子节点C。如果当前客户端也为有盘复制,即可以使用该bgsave运行结束后的存储文件,只需要复制子节点C客户端回复缓冲区数据和并将主节点循环积压缓冲区数据发送到从节点客户端,调用replicationSetupSlaveForFullResync更改状态,回复客户端"FULLRESYNC runid offset\r\n"。如果该客户端为socket流式复制,则等待后台bgsave结束后,再运行bgsave,该情况并未回复客户端。
	copyClientOutputBuffer(c,slave);
	replicationSetupSlaveForFullResync(c,slave->psync_initial_offset);
  • b. 后台有运行中的bgsave进程,socket流复制
    等待bgsave结束,再一次运行bgsave。
  • c.无后台进程,socket流式复制,先暂时不进行BGSAVE,而是在定时函数replicationCron中在执行,这样可以等到更多的从节点,以减少执行BGSAVE的次数。
    无后台进程,有盘复制,则调用startBgsaveForReplication开始进行BGSAVE操作。对于有盘复制,对于所有处于SLAVE_STATE_WAIT_BGSAVE_START状态的从节点,调用replicationSetupSlaveForFullResync更改状态,回复客户端"FULLRESYNC runid offset\r\n"。
	//根据从节点socket复制还是有盘复制,进行bgsave
	if (socket_target)
	    retval = rdbSaveToSlavesSockets();
	else
	    retval = rdbSaveBackground(server.rdb_filename);
	 if (!socket_target) {
        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            client *slave = ln->value;
            if (slave->replstate == `SLAVE_STATE_WAIT_BGSAVE_START`) {
                    replicationSetupSlaveForFullResync(slave,
                            getPsyncInitialOffset());//即server.master_repl_offset;
            }
        }
    }
  • <<<<处于REPL_STATE_RECEIVE_PSYNC状态,等待监听事件可读,进入syncWithMaster。读取主节点响应,删除fd回调函数为syncWithMaster可读事件。
    部分复制:将缓存节点恢复到master
void replicationResurrectCachedMaster(int newfd) {
    server.master = server.cached_master;
    server.cached_master = NULL;
    server.master->fd = newfd;
    server.master->flags &= ~(CLIENT_CLOSE_AFTER_REPLY|CLIENT_CLOSE_ASAP);
    server.master->authenticated = 1;
    server.master->lastinteraction = server.unixtime;
    server.repl_state = REPL_STATE_CONNECTED;

    /* Re-add to the list of clients. */
    listAddNodeTail(server.clients,server.master);
    if (aeCreateFileEvent(server.el, newfd, AE_READABLE,
                          readQueryFromClient, server.master)) {
        serverLog(LL_WARNING,"Error resurrecting the cached master, impossible to add the readable handler: %s", strerror(errno));
        freeClientAsync(server.master); /* Close ASAP. */
    }

    /* We may also need to install the write handler as well if there is
     * pending data in the write buffers. */
    if (clientHasPendingReplies(server.master)) {
        if (aeCreateFileEvent(server.el, newfd, AE_WRITABLE,
                          sendReplyToClient, server.master)) {
            serverLog(LL_WARNING,"Error resurrecting the cached master, impossible to add the writable handler: %s", strerror(errno));
            freeClientAsync(server.master); /* Close ASAP. */
        }
    }
}

全量复制,跟新repl_master_runid,repl_master_initial_offset,缓存节点清空,所有从节点断开,清空循环积压缓冲区。
为当前fd添加可读事件,回调函数readSyncBulkPayload。打开临时文件,保存各种状态。

	server.repl_state = REPL_STATE_TRANSFER;
    server.repl_transfer_size = -1;
    server.repl_transfer_read = 0;
    server.repl_transfer_last_fsync_off = 0;
    server.repl_transfer_fd = dfd;
    server.repl_transfer_lastio = server.unixtime;
    server.repl_transfer_tmpfile = zstrdup(tmpfile);

5、bgsave运行完成,发送rdb文件

主节点要发送RDB文件,但是回复完”+FULLRESYNC”就再也没有操作了。而子节点创建了监听主节点写RDB文件的事件,等待主节点来写,才调用readSyncBulkPayload()函数来处理。这又有问题了,到底主节点什么时候发送RDB文件呢?如果不是主动执行,那么一定就在周期性函数内被执行。调用关系如下:

serverCron()->backgroundSaveDoneHandler()->updateSlavesWaitingBgsave();
void backgroundSaveDoneHandler(int exitcode, int bysignal) {
    switch(server.rdb_child_type) {
    case RDB_CHILD_TYPE_DISK:
        backgroundSaveDoneHandlerDisk(exitcode,bysignal);
        break;
    case RDB_CHILD_TYPE_SOCKET:
        backgroundSaveDoneHandlerSocket(exitcode,bysignal);
        break;
    default:
        serverPanic("Unknown RDB child type.");
        break;
    }
}

backgroundSaveDoneHandler无论时处理有盘复制的还是socket流复制,处理流程都是,更新该方式相关的一些状态值,清理释放无关的空间,比如server.rdb_child_pid=-1,后台bgsave进程结束,接下来,都需要调用updateSlavesWaitingBgsave去处理从节点客户端。
从节点客户端,无非两种状态 (1)SLAVE_STATE_WAIT_BGSAVE_START(2)SLAVE_STATE_WAIT_BGSAVE_END

  • 状态(1),则需要重新进行后台bgsave,当前bgsave不满足要求。前面提到过,为什么会需要重新bgsave,因为后台bgsave为有盘/流式复制,只要当前客户端要求流式复制,则必须等待后台执行完bgsave,重新调用startBgsaveForReplication,见全量同步情况c。新一轮的bgsave复制方式,尽可能选择当前主节点所有从节点slave_capa能力最小的那个,尽可能减少后台bgsve调用次数。
  • 状态(2)后台bgsave结束,流式复制结束,文件已发送完毕,需要发送客户端输出缓冲区数据,更新状态为SLAVE_STATE_ONLINE
	slave->replstate = SLAVE_STATE_ONLINE;
	slave->repl_put_online_on_ack = 1;
	slave->repl_ack_time = server.unixtime; /* Timeout otherwise. */

后台bgsave结束,有盘复制,则需要打开本地临时文件描述符,删除当前fd可写事件,新增可写事件,回调函数sendBulkToSlave,毫无疑问,该函数用于主节点向从节点同步rdb文件信息。

	if ((slave->repldbfd = open(server.rdb_filename,O_RDONLY)) == -1 ||
	    redis_fstat(slave->repldbfd,&buf) == -1) {
	    freeClient(slave);
	    serverLog(LL_WARNING,"SYNC failed. Can't open/stat DB after BGSAVE: %s", strerror(errno));
	    continue;
	}
	slave->repldboff = 0;
	slave->repldbsize = buf.st_size;
	slave->replstate = SLAVE_STATE_SEND_BULK;
	slave->replpreamble = sdscatprintf(sdsempty(),"$%lld\r\n",
	    (unsigned long long) slave->repldbsize);
	
	aeDeleteFileEvent(server.el,slave->fd,AE_WRITABLE);
	if (aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE, sendBulkToSlave, slave) == AE_ERR) {
	    freeClient(slave);
	    continue;
	}

小总结:
当从节点发起PSYNC命令时,与主节点建立起通信。此时将从节点客户端加入到从列表中。在主从复制过程中,新加入的从节点客户端i需要经历这四个阶段,s1、s2、s3、s4:
REDIS_REPL_WAIT_BGSAVE_START、REDIS_REPL_WAIT_BGSAVE_END、REDIS_REPL_SEND_BULK和REDIS_REPL_ONLINE
当s4阶段缓冲区数据发送完成时,从节点新建redisClient结构,名称master,与socket绑定,状态为connected。此时主从复制已完成。


在这里插入图片描述

6、文件同步

1、主节点将文件发送给从节

有盘复制:

执行sendBulkToSlave发送文件给客户端,当文件发送完毕,则立刻发送缓冲区数据。

	//先发送文件大小
	if (slave->replpreamble) {
        nwritten = write(fd,slave->replpreamble,sdslen(slave->replpreamble));
        if (nwritten == -1) {
            serverLog(LL_VERBOSE,"Write error sending RDB preamble to slave: %s",
                strerror(errno));
            freeClient(slave);
            return;
        }
        // 更新已经写到网络的字节数
        server.stat_net_output_bytes += nwritten;
        // 保留未写的字节,删除已写的字节
        sdsrange(slave->replpreamble,nwritten,-1);
        // 如果已经写完了,则释放replpreamble
        if (sdslen(slave->replpreamble) == 0) {
            sdsfree(slave->replpreamble);
            slave->replpreamble = NULL;
            /* fall through sending data. */
        } else {
            return;
        }
    }

无盘复制:

在rdbSaveRio之后,数据已经都发送给客户端了,当数据都发送完,不能立刻发送缓冲区数据,设置了该标识slave->repl_put_online_on_ack = 1。

2、从节点接收文件信息
监听到可读,执行回调函数readSyncBulkPayload,有盘复制和无盘复制都由该函数加载数据,保存在本地文件。

  • 有盘复制,则根据文件大小,将socket缓冲区数据接收到写入文件缓冲区buf中,最大一次接收4096个字节,当接受到文件大小的字节数时,代表接收文件结束。无盘复制,每次都接收4096个字节数据,对比最后40个字节数据,判断接收文件是否结束。接收文件结束,在从节点新建master信息。
	void replicationCreateMasterClient(int fd) {
    // 创建一个client
    server.master = createClient(fd);
    // 设置为主节点client的状态
    server.master->flags |= CLIENT_MASTER;
    // client认证通过
    server.master->authenticated = 1;
    // 服务器复制状态:和主节点保持连接
    server.repl_state = REPL_STATE_CONNECTED;
    // 主节点PSYNC的偏移量拷贝给client保存的复制偏移量
    server.master->reploff = server.repl_master_initial_offset;
    // 拷贝主节点的运行ID给client
    memcpy(server.master->replrunid, server.repl_master_runid,
        sizeof(server.repl_master_runid));
    /* If master offset is set to -1, this master is old and is not
     * PSYNC capable, so we flag it accordingly. */
    // 如果主节点的偏移量是-1,那么将client设置为CLIENT_PRE_PSYNC,适用于旧的Redis版本,执行执行SYNC命令
    if (server.master->reploff == -1)
        server.master->flags |= CLIENT_PRE_PSYNC;
}

文件接收完毕后,会先删除当前socket回调函数sendBulkToSlave的可读事件,当创建master客户端时,则会新增可读回调函数readQueryFromClient的可读事件,用于处理主节点传输过来的命令。

7、命令传播

1、有盘复制
当文件发送完毕,则立刻调用putSlaveOnline将数据发送给客户端,从节点执行readQueryFromClient,处理命令。

	void putSlaveOnline(client *slave) {
    // 设置从节点的状态为ONLINE
    slave->replstate = SLAVE_STATE_ONLINE;
    // 不设置从节点的写处理器
    slave->repl_put_online_on_ack = 0;
    // 设置通过ack命令接收到的偏移量所用的时间
    slave->repl_ack_time = server.unixtime; /* Prevent false timeout. */
    // 重新设置文件的可写事件的处理程序为sendReplyToClient
    if (aeCreateFileEvent(server.el, slave->fd, AE_WRITABLE,
        sendReplyToClient, slave) == AE_ERR) {
        serverLog(LL_WARNING,"Unable to register writable event for slave bulk transfer: %s", strerror(errno));
        freeClient(slave);
        return;
    }
    // 更新当前状态良好的从节点的个数
    refreshGoodSlavesCount();
    serverLog(LL_NOTICE,"Synchronization with slave %s succeeded",
        replicationGetSlaveName(slave));
}

2、无盘复制

  • 无盘复制,为什么不能在文件保存完毕后,立马发送缓冲区数据呢?因为无盘复制发送文件,再发送缓冲区数据,你不知道文件发送是什么时候结束的。回顾一下,无盘复制完毕后,设置了repl_put_online_on_ack 该参数,那么肯定就与发送缓冲区数据有关。
  • 无盘接收文件结束后,从节点就会定期向主节点同步复制状况。
   // 定期发送ack给主节点,旧版本的Redis除外,每1s一次
   if (server.masterhost && server.master &&
       !(server.master->flags & CLIENT_PRE_PSYNC))
       replicationSendAck();

再看看主节点收到ack命令后怎么处理:

	 else if (!strcasecmp(c->argv[j]->ptr,"ack")) {
     // 从节点使用REPLCONF ACK通知主机到目前为止处理的复制偏移量。 这是一个内部唯一的命令,普通客户端不应该使用它
     long long offset;
     // client不是从节点,直接返回
     if (!(c->flags & CLIENT_SLAVE)) return;
     // 获取offset
     if ((getLongLongFromObject(c->argv[j+1], &offset) != C_OK))
         return;
     // 设置从节点通过ack命令接收到的偏移量
     if (offset > c->repl_ack_off)
         c->repl_ack_off = offset;
     // 通过ack命令接收到的偏移量所用的时间
     c->repl_ack_time = server.unixtime;
     // 如果这是一个无盘复制,我们需要在接收到第一个ACK时确实将从节点设置为在线状态
     // (这确认从节点在线并准备好获取更多的数据)
     // 将从节点设置为在线状态
     if (c->repl_put_online_on_ack && c->replstate == SLAVE_STATE_ONLINE)
         putSlaveOnline(c);
     /* Note: this command does not reply anything! */
     return;

当repl_put_online_on_ack为1,SLAVE_STATE_ONLINE状态时,就会调用putSlaveOnline将缓冲区数据发送给从节点。

8、主从复制完毕,主节点实时同步数据

不多啰嗦,直接看主节点call函数,可以理解为,当主节点每执行一条命令,看脏键值(即数据)是否变化,当变化时,则需要将该命令同步给从节点,如下:

if (propagate_flags != PROPAGATE_NONE)
       propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags);

9、主从连接状态定时检测

replicationCron
1、主从未连接成功

  • 从节点发送PING、IP、PORT、CAPA、PSYNC超时,未收到主节点"+OK\r\n",超时,则删除连接的socket客户端,状态设置为REPL_STATE_CONNECT,等待下一次重新连接。
  • 主节点后台bgsave时,定时发送从节点"\n",保持主从连接状态
  • 从节点REPL_STATE_TRANSFER状态,连接超时,既没有收到"\n"也没收到文件信息,则删除连接的socket客户端,删除本地文件,状态设置为REPL_STATE_CONNECT,等待下一次重新连接。
    2、主从成功连接
  • 主节点每隔10s给从节点发送PING命令,保持主从连接状态
  • 从节点每个1s发送当前复制偏移量
  • 主从交互(即上述两个都没执行)超时,从节点释放master客户端

释放master客户端,执行replicationCacheMaster,会先将master信息保存在缓存master中,方便下次主从连上时,进行部分同步。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值