Redis开发与运维读书笔记-第十章-集群

Redis Cluster是Redis的分布式解决方案,在3.0版本正式推出,有效地解 决了Redis分布式方面的需求。当遇到单机内存、并发、流量等瓶颈时,可以采用Cluster架构方案达到负载均衡的目的。

一.数据分布

1 数据分布理论
分布式数据库首先要解决把整个数据集按照分区规则映射到多个节点的问题,即把数据集划分到多个节点上,每个节点负责整体数据的一个子集。 需要重点关注的是数据分区规则,常见的分区规则有哈希分区和顺序分区两种

由于Redis Cluster采用哈希分区规则,这里我们重点讨论哈希分区,常见的哈希分区规则有几种,下面分别介绍。

1.节点取余分区
使用特定的数据,如Redis的键或用户ID,再根据节点数量N使用公式: hash(key)%N计算出哈希值,用来决定数据映射到哪一个节点上。这种方案存在一个问题:当节点数量变化时,如扩容或收缩节点,数据节点映射关系需要重新计算,会导致数据的重新迁移。这种方式的突出优点是简单性,常用于数据库的分库分表规则,一般采 用预分区的方式,提前根据数据量规划好分区数,比如划分为512或1024张表,保证可支撑未来一段时间的数据量,再根据负载情况将表迁移到其他数据库中。扩容时通常采用翻倍扩容,避免数据映射全部被打乱导致全量迁移的情况.

翻倍扩容迁移约50%数据

2.一致性哈希分区
一致性哈希分区(Distributed Hash Table)实现思路是为系统中每个节 点分配一个token,范围一般在0~232,这些token构成一个哈希环。数据读写 执行节点查找操作时,先根据key计算hash值,然后顺时针找到第一个大于 等于该哈希值的token节点,如下图所示。关于一致性哈希分区可以查找更多资源了解.

一致性哈希数据分布

这种方式相比节点取余最大的好处在于加入和删除节点只影响哈希环中相邻的节点,对其他节点无影响。但一致性哈希分区存在几个问题:
·加减节点会造成哈希环中部分数据无法命中,需要手动处理或者忽略这部分数据,因此一致性哈希常用于缓存场景。
·当使用少量节点时,节点变化将大范围影响哈希环中数据映射,因此这种方式不适合少量数据节点的分布式方案。
·普通的一致性哈希分区在增减节点时需要增加一倍或减去一半节点才能保证数据和负载的均衡。
正因为一致性哈希分区的这些缺点,一些分布式系统采用虚拟槽对一致性哈希进行改进,比如Dynamo系统。

3.虚拟槽分区
虚拟槽分区巧妙地使用了哈希空间,使用分散度良好的哈希函数把所有 数据映射到一个固定范围的整数集合中,整数定义为槽(slot)。这个范围 一般远远大于节点数,比如Redis Cluster槽范围是0~16383。槽是集群内数据管理和迁移的基本单位。采用大范围槽的主要目的是为了方便数据拆分和集 群扩展。每个节点会负责一定数量的槽,如下图所示。
当前集群有5个节点,每个节点平均大约负责3276个槽。由于采用高质 量的哈希算法,每个槽所映射的数据通常比较均匀,将数据平均划分到5个 节点进行数据分区。Redis Cluster就是采用虚拟槽分区,下面就介绍Redis数据分区方法。

槽集合与节点关系

2 Redis数据分区

Redis Cluser采用虚拟槽分区,所有的键根据哈希函数映射到0~16383整 数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部 分槽以及槽所映射的键值数据,如下图所示:

使用CRC16(key)&16383将键映射到槽上

Redis虚拟槽分区的特点:
·解耦数据和节点之间的关系,简化了节点扩容和收缩难度。
·节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据。
·支持节点、槽、键之间的映射查询,用于数据路由、在线伸缩等场景。
数据分区是分布式存储的核心,理解和灵活运用数据分区规则对于掌握 Redis Cluster非常有帮助。

3 集群功能限制

Redis集群相对单机在功能上存在一些限制,需要开发人员提前了解,在使用时做好规避。限制如下:
1)key批量操作支持有限。如mset、mget,目前只支持具有相同slot值的 key执行批量操作。对于映射为不同slot值的key由于执行mget、mget等操作可能存在于多个节点上因此不被支持。
2)key事务操作支持有限。同理只支持多key在同一节点上的事务操 作,当多个key分布在不同的节点上时无法使用事务功能。
3)key作为数据分区的最小粒度,因此不能将一个大的键值对象如 hash、list等映射到不同的节点。
4)不支持多数据库空间。单机下的Redis可以支持16个数据库,集群模 式下只能使用一个数据库空间,即db0。
5)复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构。

二.搭建集群

1.我们先安装一个redis实例,然后拓展为集群:

  • CentOS7操作系统下Redis的安装与配置

1.下载

wget http://download.redis.io/releases/redis-3.0.7.tar.gz

2.解压

tar -zxvf redis-3.0.7.tar.gz

3.编译与安装

cd redis-3.0.7
如果这里报指令错误,请安装gcc环境:yum -y install gcc automake autoconf 
libtool make
make
make install PREFIX=/usr/local/redis-3.0.7

4.启动服务器

cd /usr/local/redis-3.0.7
bin/redis-server 如果是已经进入bin目录,要输入./redis-server
# 直接启动为前台启动,控制台会被占用
# 可以使用Ctrl+C强制关闭服务

5.配置后台启动

  • 将源码中redis.conf拷贝到/usr/local/redis目录
  • vi命令中“/daemonize” 查找 daemonize,小写n查找下一个,大写N查找上一个
  • 并将"daemonize no"行改为"daemonize yes"
  • 允许远程连接,注释#bind 127.0.0.1(protected-mode no)
  • 修改连接密码,requirepass
cp /root/redis-3.0.7/redis.conf ./
vim redis.conf

6.后台启动

bin/redis-server redis.conf

7.测试

bin/redis-cli
redis> set foo bar
OK
redis> get foo
"bar"

8.关闭服务器

# 推荐使用
bin/redis-cli shutdown
# 不推荐使用
kill -9 10452

CentOS7操作系统下Redis集群的搭建

安装ruby环境

yum install ruby
yum install rubygems

 安装ruby脚本运行使用的包:

# 离线安装
gem install redis-3.0.7.gem
# 在线安装
gem install redis -v 3.0.7
如下为离线安装实例:
[root@localhost ~]# gem install redis-3.0.0.gem 
Successfully installed redis-3.0.0
1 gem installed
Installing ri documentation for redis-3.0.0...
Installing RDoc documentation for redis-3.0.0...
[root@localhost ~]# cd redis-3.0.0/src
[root@localhost src]# ll *.rb(注意这个文件稍后要拷贝到rediscluster那个文件夹中)
-rwxrwxr-x. 1 root root 48141 Apr  1  2015 redis-trib.rb

创建6台服务器,将6台的端口号修改9001——9006

1./usr/soft(我自己的软件安装目录)创建一个rediscluster文件夹(用来创建集群),将上面已经安装好的redis文件夹(注意是安装目录)复制六份到这个文件夹

上图是我的目录结构,先把redis-3.0.7复制一份到rediscluster里面并改名为redis1
指令:cp /usr/soft/redis-3.0.7  /usr/soft/rediscluster
然后改文件夹名字为redis1,可以用notepad++等工具,也可以用指令:mv redis-3.0.7 redis1
然后记住先将redis1里面的数据文件dump.rdb删除,然后打开redis.conf之后修改三处: cluster-enabled yes前的注释去掉,port分别改为9001,再将密码先注释掉requirepass(# requirepass xxxxxx),之后保存(用指令的话先vi redis.conf进入文件,然后键盘i修改,修改后按esc,再按shift+:,之后wq!保存退出)
然后再把redis1复制五份,分别命名为redis2-redis6,之后进去把各自port修改为9002-9006

上图中-R表示递归复制(复制文件夹下的所有文件及文件夹)

4. 如果是阿里云ECS服务器的话,请将端口号+10000添加到安全组

如:7001需要加入安全组,17001也需要加入安全组
  • 自定义shell脚本启动6台服务器
先进入rediscluster文件夹,然后用指令touch startup.sh创建文件,之后通过vi startup.sh进入文件,然后编辑写入如下内容(注意文件的相对路径,因为我的路径结构是如下的,rediscluster下有redis1-6,startup.sh然后有redis1-6文件夹下有bin,bin内部有redis.confredis-server,所以我的脚本文件应该按照如下写:

cd /usr/soft/rediscluster/redis1
bin/redis-server  
bin/redis.conf
cd ../redis2

cd /usr/soft/rediscluster/redis1
bin/redis-server  bin/redis.conf
cd ../redis2
bin/redis-server bin/redis.conf

cd ../redis3

bin/redis-server bin/redis.conf

cd ../redis4

bin/redis-server bin/redis.conf

cd ../redis5

bin/redis-server bin/redis.conf

cd ../redis6

bin/redis-server bin/redis.conf
自定义的启动脚本完成后保存并退出,来到rediscluster文件夹下,然后输入./startup.sh 运行脚本即可全部启动(关闭的脚本类似)

(redisclustertouch创建shutdown.sh,然后写入如下内容)

cd /usr/soft/rediscluster/redis1

bin/redis-cli -p 9001 shutdown

bin/redis-cli -p 9002 shutdown

bin/redis-cli -p 9003 shutdown

bin/redis-cli -p 9004 shutdown

bin/redis-cli -p 9005 shutdown

bin/redis-cli -p 9006 shutdown

然后保存退出,记得用cat -A shutdown.sh来看下写入的指令是不是尾部以$(换行结束),如果是就对了,否则会报错(注意用windows记事本写的脚本会在尾部加上其他我们看不到的符号,要注意检查下)

  • 运行如下代码搭建集群环境(注意这个时候要把redis-trib.rb这个离线安装文件-------一般在redis的解压目录的文件夹下的src中拷贝到rediscluster),之后运行如下指令
  • ./redis-trib.rb create --replicas 1 
    10.31.166.22:9001 
    10.31.166.22:9002 
    10.31.166.22:9003 
    10.31.166.22:9004 
    10.31.166.22:9005 
    10.31.166.22:9006
  • 集群创建成功的两张截图

  • 客户端如何连接集群中的机器
# -p 端口号
# -c 开启reidis cluster模式,连接redis cluster节点时候使用
bin/redis-cli -p 7004 -c
  • 往集群节点存入数据进行测试,查看数据到底存入到哪个节点

redis集群中内置了16384个哈希槽,当需要往集群中存放键值对的时候,redis先对key使用CRC16算法算出一个结果,然后拿这个结果对16384求余,这样每个key都会对应一个编号为0-16383之间的哈希槽,redis会根据节点数量大致均等的将哈希槽映射到不同的节点上

三.集群伸缩

1 伸缩原理
Redis集群提供了灵活的节点扩容和收缩方案。在不影响集群对外服务的情况下,可以为集群添加节点进行扩容也可以下线部分节点进行缩容,Redis集群可以实现对节点的灵活上下线控制。其中原理可抽象为槽和对应数据在不同节点之间灵活移动。首先来看我们之前搭建 的集群槽和数据与节点的对应关系,如下图所示。

槽和数据与节点的对应关系

三个主节点分别维护自己负责的槽和对应的数据,如果希望加入1个节点实现集群扩容时,需要通过相关命令把一部分槽和数据迁移给新节点,如下图所示:

槽和相关数据迁移到新节点

图中每个节点把一部分槽和数据迁移到新的节点6385,每个节点负责的槽和数据相比之前变少了从而达到了集群扩容的目的。

2 扩容集群

扩容是分布式存储最常见的需求,Redis集群扩容操作可分为如下步骤:
1)准备新节点。
2)加入集群。
3)迁移槽和数据。

1.准备新节点
需要提前准备好新节点并运行在集群模式下,新节点建议跟集群内的节点配置保持一致,便于管理统一。准备好配置后启动两个节点命令如下:

redis-server conf/redis-6385.conf 
redis-server conf/redis-6386.conf

启动后的新节点作为孤儿节点运行,并没有其他节点与之通信,集群结构如下图所示。

集群内节点和孤儿节点

2.加入集群
新节点依然采用cluster meet命令加入到现有集群中。在集群内任意节点 执行cluster meet命令让6385和6386节点加入进来,命令如下:

127.0.0.1:6379> cluster meet 127.0.0.1 6385 
127.0.0.1:6379> cluster meet 127.0.0.1 6386

新节点加入后集群结构如下图所示。

新节点6385和6386加入集群

集群内新旧节点经过一段时间的ping/pong消息通信之后,所有节点会发 现新节点并将它们的状态保存到本地。

新节点刚开始都是主节点状态,但是由于没有负责的槽,所以不能接受任何读写操作。对于新节点的后续操作我们一般有两种选择:
·为它迁移槽和数据实现扩容。
·作为其他主节点的从节点负责故障转移。

3.迁移槽和数据

加入集群后需要为新节点迁移槽和相关数据,槽在迁移过程中集群可以正常提供读写服务,迁移过程是集群扩容最核心的环节

(1)槽迁移计划
槽是Redis集群管理数据的基本单位,首先需要为新节点制定槽的迁移计划,确定原有节点的哪些槽需要迁移到新节点。迁移计划需要确保每个节点负责相似数量的槽,从而保证各节点的数据均匀。例如,在集群中加入 6385节点,如下图所示。加入6385节点后,原有节点负责的槽数量从 6380变为4096个。

新节点加入的槽迁移计划

槽迁移计划确定后开始逐个把槽内数据从源节点迁移到目标节点

(2)迁移数据
数据迁移过程是逐个槽进行的,每个槽数据迁移的流程如下图所示。

槽和数据迁移流程


流程说明:
1)对目标节点发送cluster setslot{slot}importing{sourceNodeId}命令,让目标节点准备导入槽的数据。
2)对源节点发送cluster setslot{slot}migrating{targetNodeId}命令,让源节点准备迁出槽的数据。
3)源节点循环执行cluster getkeysinslot{slot}{count}命令,获取count个 属于槽{slot}的键。
4)在源节点上执行migrate{targetIp}{targetPort}""0{timeout}keys{keys...} 命令,把获取的键通过流水线(pipeline)机制批量迁移到目标节点,批量 迁移版本的migrate命令在Redis3.0.6以上版本提供,之前的migrate命令只能 单个键迁移。对于大量key的场景,批量键迁移将极大降低节点之间网络IO次数。
5)重复执行步骤3)和步骤4)直到槽下所有的键值数据迁移到目标节点。
6)向集群内所有主节点发送cluster setslot{slot}node{targetNodeId}命令,通知槽分配给目标节点。为了保证槽节点映射变更及时传播,需要遍历发送给所有主节点更新被迁移的槽指向新节点。

(3)添加从节点
扩容之初我们把6385、6386节点加入到集群,节点6385迁移了部分槽和数据作为主节点,但相比其他主节点目前还没有从节点,因此该节点不具备故障转移的能力。
这时需要把节点6386作为6385的从节点,从而保证整个集群的高可用。 使用cluster replicate{masterNodeId}命令为主节点添加对应从节点,注意在集 群模式下slaveof添加从节点操作不再支持。如下所示:

127.0.0.1:6386>cluster replicate 1a205dd8b2819a00dd1e8b6be40a8e2abe77b756

从节点内部除了对主节点发起全量复制之外,还需要更新本地节点的集群相关状态,查看节点6386状态确认已经变成6385节点的从节点:

127.0.0.1:6386>cluster nodes 
475528b1bcf8e74d227104a6cf1bf70f00c24aae 
127.0.0.1:6386 myself,slave 1a205dd8b2    
819a00dd1e8b6be40a8e2abe77b756 0 0 8 connected 
1a205dd8b2819a00dd1e8b6be40a8e2abe77b756 
127.0.0.1:6385 master - 0 1469779083513 9     
connected 0-1365 4096 5462-6826 10923-12287 ...

到此整个集群扩容完成.

3 收缩集群

收缩集群意味着缩减规模,需要从现有集群中安全下线部分节点。安全下线节点流程如下图所示:

节点安全下线流程

流程说明:
1)首先需要确定下线节点是否有负责的槽,如果是,需要把槽迁移到其他节点,保证节点下线后整个集群槽节点映射的完整性。
2)当下线节点不再负责槽或者本身是从节点时,就可以通知集群内其他节点忘记下线节点,当所有的节点忘记该节点后可以正常关闭。

1.下线迁移槽
下线节点需要把自己负责的槽迁移到其他节点,原理与之前节点扩容的 迁移槽过程一致,收缩正好和扩容迁移方向相反,6381变为源节点,其他主节点变为目标 节点,源节点需要把自身负责的4096个槽均匀地迁移到其他主节点上,下线节点槽迁出完成后,剩下的步骤需要让集群忘记该节点。

2.忘记节点

由于集群内的节点不停地通过Gossip消息彼此交换节点状态,因此需要通过一种健壮的机制让集群内所有节点忘记下线的节点。也就是说让其他节 点不再与要下线节点进行Gossip消息交换。Redis提供了clusterforget{downNodeId}命令实现该功能

四.故障转移

Redis集群自身实现了高可用。高可用首先需要解决集群部分失败的场景:当集群内少量节点出现故障时通过自动故障转移保证集群可以正常对外提供服务。下面介绍故障转移的细节,分析故障发现和替换故障节点的过程。

1 故障发现
当集群内某个节点出现问题时,需要通过一种健壮的方式保证识别出节 点是否发生了故障。Redis集群内节点通过ping/pong消息实现节点通信,消息不但可以传播节点槽信息,还可以传播其他状态如:主从状态、节点故障等。因此故障发现也是通过消息传播机制实现的,主要环节包括:主观下线 (pfail)和客观下线(fail)。
·主观下线:指某个节点认为另一个节点不可用,即下线状态,这个状态并不是最终的故障判定,只能代表一个节点的意见,可能存在误判情况。
·客观下线:指标记一个节点真正的下线,集群内多个节点都认为该节点不可用,从而达成共识的结果。如果是持有槽的主节点故障,需要为该节点进行故障转移。
1.主观下线
集群中每个节点都会定期向其他节点发送ping消息,接收节点回复pong 消息作为响应。如果在cluster-node-timeout时间内通信一直失败,则发送节 点会认为接收节点存在故障,把接收节点标记为主观下线(pfail)状态。流程图如下图所示:

主观下线识别流程

流程说明:
1)节点a发送ping消息给节点b,如果通信正常将接收到pong消息,节 点a更新最近一次与节点b的通信时间。
2)如果节点a与节点b通信出现问题则断开连接,下次会进行重连。如 果一直通信失败,则节点a记录的与节点b最后通信时间将无法更新。
3)节点a内的定时任务检测到与节点b最后通信时间超高cluster-nodetimeout时,更新本地对节点b的状态为主观下线(pfail)。
主观下线简单来讲就是,当cluster-note-timeout时间内某节点无法与另一 个节点顺利完成ping消息通信时,则将该节点标记为主观下线状态。每个节 点内的cluster State结构都需要保存其他节点信息,用于从自身视角判断其他节点的状态

Redis集群对于节点最终是否故障判断非常严谨,只有一个节点认为主观下线并不能准确判断是否故障

2.客观下线

当某个节点判断另一个节点主观下线后,相应的节点状态会跟随消息在 集群内传播。ping/pong消息的消息体会携带集群1/10的其他节点状态数据,当接受节点发现消息体中含有主观下线的节点状态时,会在本地找到故障节 点的ClusterNode结构,保存到下线报告链表中,通过Gossip消息传播,集群内节点不断收集到故障节点的下线报告。当半数以上持有槽的主节点都标记某个节点是主观下线时,触发客观下线流程。

2 故障恢复

故障节点变为客观下线后,如果下线节点是持有槽的主节点则需要在它的从节点中选出一个替换它,从而保证集群的高可用。下线主节点的所有从节点承担故障恢复的义务,当从节点通过内部定时任务发现自身复制的主节 点进入客观下线时,将会触发故障恢复流程.

1.资格检查
每个从节点都要检查最后与主节点断线时间,判断是否有资格替换故障 的主节点。如果从节点与主节点断线时间超过cluster-node-time*cluster-slave660.validity-factor,则当前从节点不具备故障转移资格。参数cluster-slavevalidity-factor用于从节点的有效因子,默认为10。
2.准备选举时间
当从节点符合故障转移资格后,更新触发故障选举的时间,只有到达该时间后才能执行后续流程.这里之所以采用延迟触发机制,主要是通过对多个从节点使用不同的延迟选举时间来支持优先级问题。复制偏移量越大说明从节点延迟越低,那么它应该具有更高的优先级来替换故障主节点
3.发起选举
当从节点定时任务检测到达故障选举时间(failover_auth_time)到达后,发起选举流程如下:

(1)更新配置纪元

配置纪元是一个只增不减的整数,每个主节点自身维护一个配置纪元 (clusterNode.configEpoch)标示当前主节点的版本,所有主节点的配置纪元都不相等,从节点会复制主节点的配置纪元。整个集群又维护一个全局的配 置纪元(clusterState.current Epoch),用于记录集群内所有主节点配置纪元 的最大版本

配置纪元会跟随ping/pong消息在集群内传播,当发送方与接收方都是主 节点且配置纪元相等时代表出现了冲突,nodeId更大的一方会递增全局配置纪元并赋值给当前节点来区分冲突.

配置纪元的主要作用:
·标示集群内每个主节点的不同版本和当前集群最大的版本。
·每次集群发生重要事件时,这里的重要事件指出现新的主节点(新加入的或者由从节点转换而来),从节点竞争选举。都会递增集群全局的配置纪元并赋值给相关主节点,用于记录这一关键事件。
·主节点具有更大的配置纪元代表了更新的集群状态,因此当节点间进行ping/pong消息交换时,如出现slots等关键信息不一致时,以配置纪元更大的一方为准,防止过时的消息状态污染集群。
配置纪元的应用场景有:
·新节点加入。
·槽节点映射冲突检测。
·从节点投票选举冲突检测。

(2)广播选举消息
在集群内广播选举消息(FAILOVER_AUTH_REQUEST),并记录已发送过消息的状态,保证该从节点在一个配置纪元内只能发起一次选举。消息 内容如同ping消息只是将type类型变为FAILOVER_AUTH_REQUEST。
4.选举投票
只有持有槽的主节点才会处理故障选举消息 (FAILOVER_AUTH_REQUEST),因为每个持有槽的节点在一个配置纪元
内都有唯一的一张选票,当接到第一个请求投票的从节点消息时回复 FAILOVER_AUTH_ACK消息作为投票,之后相同配置纪元内其他从节点的选举消息将忽略。
投票过程其实是一个领导者选举的过程,如集群内有N个持有槽的主节 点代表有N张选票。由于在每个配置纪元内持有槽的主节点只能投票给一个 从节点,因此只能有一个从节点获得N/2+1的选票,保证能够找出唯一的从节点。
Redis集群没有直接使用从节点进行领导者选举,主要因为从节点数必 须大于等于3个才能保证凑够N/2+1个节点,将导致从节点资源浪费。使用集群内所有持有槽的主节点进行领导者选举,即使只有一个从节点也可以完成选举过程。
当从节点收集到N/2+1个持有槽的主节点投票时,从节点可以执行替换 主节点操作,例如集群内有5个持有槽的主节点,主节点b故障后还有4个, 当其中一个从节点收集到3张投票时代表获得了足够的选票可以进行替换主节点操作
5.替换主节点
当从节点收集到足够的选票之后,触发替换主节点操作:
1)当前从节点取消复制变为主节点。
2)执行clusterDelSlot操作撤销故障主节点负责的槽,并执行 clusterAddSlot把这些槽委派给自己。
3)向集群广播自己的pong消息,通知集群内所有的节点当前从节点变为主节点并接管了故障主节点的槽信息。

小结:

1)Redis集群数据分区规则采用虚拟槽方式,所有的键映射到16384个槽中,每个节点负责一部分槽和相关数据,实现数据和请求的负载均衡。
2)集群伸缩通过在节点之间移动槽和相关数据实现。扩容时根据槽迁移计划把槽从源节点迁移到目标节点,源节点负责的槽相比之前变少从而达到集群扩容的目的,收缩时如果下线的节点有负责的槽需要迁移到其他节 点,再通过cluster forget命令让集群内其他节点忘记被下线节点。
3)集群自动故障转移过程分为故障发现和故障恢复。节点下线分为主观下线和客观下线,当超过半数主节点认为故障节点为主观下线时标记它为客观下线状态。从节点负责对客观下线的主节点触发故障恢复流程,保证集群的可用性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值