Redis入门之基本命令、数据类型和持久化

1、什么是NOSQL

NOSQL(Not Only Sql),意为不仅仅是sql,泛指非关系型数据库。

2、为什么NOSQL

随着互联网的兴起,传统的关系型数据库在应对动态网站,特别是超大规模、高并发的动态网站就显得力不从心了,暴露了很多难以克服的问题,如:商城网站中对商品数据的频繁查询对热搜商品的排行统计等等,虽然可以实现这些功能,但是在性能上不容乐观,NOSQL这个技术门类的出现,很好的解决了这个问题,他告诉这个世界不仅仅是sql

3、NOSQL的四大分类

3.1 键值(key-value)数据库

# 说明
这一类的数据库会使用到一个哈希表,这个表中有一个键和一个指针指向特定的数据。
# 特点
- 简单、易部署
- 但是在只对部分值进行更新时,key/value就显得效率低下了
# 相关产品
Redis
SSDB
...

3.2 列存储数据库

# 说明
这类数据库用来应对分布式存储的海量数据。
# 特点
键仍然存在,但是它指向了多个列,这些列是由列家族来安排的。
# 相关产品
HBase、Riak

3.3 文档型数据库

# 说明
该类型的数据模型是版本化的文档、半结构化的文档以特定的格式进行存储,比如JSON.
# 特点
以文档形式存储
# 相关产品
MongoDB

3.4 图形(graph)数据库

# 说明
使用灵活的图形模型,可以扩展到多个服务器上。
# 相关产品
InfoGrid

4、NOSQL的应用场景

  • 数据模型比较简单
  • 需要灵活性更强的IT系统
  • 对数据库性能要求较高
  • 不需要高度的数据一致性(NOSQL产品对事务的支持不是特别良好)

5、什么是Redis

Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache and message broker.

Redis是一个开源的、基于BSD协议,通过内存存储,被用作数据库、缓存和消息中间件。

6、Redis的特点

  • Redis是一个高性能key/value的内存数据库;
  • Redis支持丰富的数据类型(String、List、Set、ZSet、Hash);
  • Redis支持持久化(内存数据持久化到硬盘中);
  • Redis单进程、单线程(没有线程安全问题,但是依旧保持高效率,可用于分布式锁)

7、Redis的安装

# 下载压缩包
http://download.redis.io/releases/redis-4.0.11.tar.gz
# 安装gcc(redis是c编写的,我们要提供c的依赖)
yum install -y gcc
# 解压缩
tar -zxvf redis-4.0.11.tar.gz 
# 进入解压缩后的目录,执行如下命令进行编译
make MALLOC=libc
# 编译完成后执行如下
make install PREFIX=/usr/redis/(指定安装目录)
# 进入安装目录下的bin目录,执行如下
./redis-server(启动服务端)
# 然后复制会话,执行如下
./redis-cli -h localhost -p 6379(启动客户端,-h指定服务端地址,-p指定端口)
./redis-cli -h localhost -p 6379 --raw(如果有显示中文的需要)
# 测试,在客户端执行如下
set name zs(键是name,值是value)
get name(获取键为name的值)

8、Redis的细节

8.1 Redis启动服务的细节

如果我们直接使用./redis-server启动的话使用的是这个shell脚本的默认配置。
那么如何才能使用指定的配置启动呢?我们在解压目录中找到redis.conf配置文件,我们将它拷贝到我们的安装目录:

cp redis.conf /usr/redis/

接着,我们打开并编辑redis.conf,找到port 6379,修改为port 7000

vi /usr/redis/redis.conf

最后指定配置文件进行启动

./redis-server ../redis.conf

8.2 Redis中库的概念

库(database):用于存放数据的一个基本单元,库中存放着key-value键值对,每一个库都有一个自己的编号,从0开始。
默认库的个数是16,编号0-15,默认使用0号库。
如果想要切换库的话可以使用select dbid(库的编号),比如:select 1

8.3 Redis中清除库

  • flushdb:清空当前库
  • flushall:清空所有库

9、Redis的相关指令

9.1 操作key相关指令

# 1、del指令
删除指定的一个或多个key,不存在的key会被忽略
del name age(返回删除的key的数量)
# 2、exists指令
判断一个或多个key是否存在,返回存在的个数
exists name age
# 3、expire指令
默认key的生存时间为永久,我们可以给它设置生存时间
expire name 10(生存时间为10秒)
# 4、keys指令
查找匹配指定模式pattern的key
keys *(查找所有key)
keys h*llo(查找首尾分别是h和llo的key)
keys h?llo(查找首尾分别是h和llo并且中间只有一个字符的key)
keys h[ae]llo(查找首尾分别是h和llo并且中间是a或者e的key)
# 5、move指令
将当前库中的key移动到其它库
move key dbid
比如:move name 1(将名字为name的key移动到1号库)
# 6、pexpire指令
和expire指令类似,用于设置生存时间,单位为毫秒
pexpire age 5000(生存时间为5000毫秒也就是5秒)
# 7、ttl指令
用于返回指定key的剩余生存时间
ttl key(如果生存时间为永久返回-1,如果key不存在返回-2,其它的则返回剩余秒数)
还有一个pttl指令则是返回剩余的毫秒数
# 8、randomkey指令
随机返回当前数据库中的某个key,如果数据库为空,则返回nil
# 9、rename指令
给库中存储的key重新命名
rename key newkey
# 10、type指令
获取key所存储的值的类型
type key

10、Redis的数据类型

10.1 String数据类型

# 设置新的key/value
set key value  =>  set name zs
# 获取指定key存储的value
get key  =>  get name
# 同时设置多个key/value
mset key value ... key value  =>  mset name zs age 23 addr jx
# 同时获取多个key对应的值
mget name age addr
# 获取原始key的值,并设置新值
getset name ls
# 获取key所对应的value的长度
strlen key  =>  strlen name
# 向指定key所对应的值追加内容,返回追加后value的长度
append key value  =>  append name isgood 
# 截取value的内容
getrange key start end  =>  getrange name 3 -1(-1表示末尾)
# 在设置key/value时设置生存时间
setex key seconds value  =>  setex name 10 zs(ex是expire的缩写)
还有一个psetex指令,功能类似,单位是毫秒
# key不存在则执行添加操作,存在不进行操作
setnx key value  =>  setnx name zs
还有一个msetnx指令功能类似,不过它是进行批量添加的,并且它是原子性的,只要其中有一个key已存在,则不会进行添加操作
msetnx name zs age 23 addr jx
# 给数值类型的value进行减法操作
decr key  =>  decr age(每次减1)
decrby key step  =>  decrby age 10(每次减10)
# 给数值类型的value进行加法操作
incr key  =>  incr age(每次加1)
incrby key step  =>  incrby age 10(每次加10)
incrbyfloat key step  =>  incrbyfloat age 0.123(与浮点数进行加法运算)

10.2 List数据类型

特点:元素有序、可以重复

10.2.1 List数据类型的内存模型

在这里插入图片描述

10.2.1 List相关指令

# 在列表的左边添加元素(如果列表存在)或者创建一个列表并从左往右添加元素(列表不存在)
lpush namelist zs ls ww
还有一个lpushx指令,功能和lpush一样,但是前提是key(列表)必须存在
# 在列表的右边添加元素(如果列表存在)或者创建一个列表并从右往左添加元素(列表不存在)
rpush namelist zs ls ww
还有一个rpushx指令,功能和rpush一样,但是前提是key(列表)必须存在
# 获取列表指定范围内的元素
lrange key start end  =>  lrange namelist 0 -1(-1表示末尾)
# 移除并返回列表左边第一个元素
lpop key  =>  lpop namelist
# 移除并返回列表右边第一个元素
rpop key  =>  rpop namelist
# 获取列表中元素的个数
llen key  =>  llen namelist
# 修改指定索引的值(索引必须存在)
lset namelist 0 asx
# 获取指定索引的值
lindex namelist 0
# 删除元素
lrem key count value  =>  lrem namelist 3 zs(删除列表中最多三个“zs”)
# 保留指定区间内的元素
ltrim key start end  =>  ltrim namelist 0 1
# 插入元素
linsert key before|after value1 value2  =>  linsert namelist before zs ww(在从左边数第一个“zs”前插入“ww”)

10.3 Set数据类型

10.3.1 特点

元素无序、不可重复

10.3.2 内存模型

在这里插入图片描述

10.3.3 相关命令

# 向set中添加元素(set存在)或者创建并向set集合中添加元素(set不存在)
sadd nameset zs ww zl ls zs
# 查询集合中所有的元素(无序)
smembers nameset
# 获取set集合的元素个数
scard nameset
# 随机移除一个(默认)或者多个元素并返回
spop nameset(移除一个)
spop nameset 2(移除两个)
# 将元素从一个集合移动到另一个集合
smove source(源set) destination(目标set) value(元素)  => smove nameset nameset2 zs
# 移除指定的一个或者多个元素
srem key value1 value2 ...  =>  srem nameset zs ww
# 判断元素是否存在
sismember key value  =>  sismember nameset zs
# 获取一个(默认)或者多个元素
srandmember key =>  srandmember nameset(随机返回一个)
srandmember key count  =>  srandmember nameset 2(随机返回两个)
# 返回集合中不同于指定的其它集合内元素的元素(简而言之,返回的不能是其它集合有的)
sdiff key1,key2...  =>  sdiff nameset1,nameset2
# 求交集,返回多个集合共有的元素
sinter key1,key2...  =>  sinter nameset1,nameset2
# 求并集,返回多个集合的所有元素(去除重复)
sunion key1,key2...  =>  sunion nameset1,nameset2

10.4 ZSet数据类型

10.4.1 特点

可排序的、不可重复的set集合

10.4.2 内存模型

在这里插入图片描述

10.4.3 常用命令

# 向zset中添加元素(zset存在)或者创建并向zset中添加元素
zadd key score1(分数) value1 score2 value2...  =>  zadd namezset 10 zs 20 ww 30 ls
# 获取元素个数
zcard namezset
# 返回一个范围内的元素
zrange key start end [withscores]  => zrange namezset 0 -1 withscores(按照分数升序显示,并且会显示分数)
zrevrange key start end [withscores]  =>  zrevrange namezset 0 -1 withscores
# 返回指定分数范围内的元素
zrangebyscore key startscore endscore [withscores] =>  zrangebyscore namezset 10 20(返回10-20分之间的元素)
# 返回某个元素的排名
zrank key value  =>  zrank namezset ls(按分数升序的排名)
zrevrank key value  =>  zrevrank namezset ls(按分数倒序的排名)
# 返回指定元素的分数
zscore key value  =>  zscore namezset ls
# 移除某个元素
zrem key value  =>  zrem namezset ww
# 给某个元素加分
zincrby key increment value  =>  zincrby namezset 1 ls(给ls加1分)

10.5 Hash数据类型

10.5.1 特点

value是一个map结构(key-value),key是无序的。

10.5.2 内存模型

在这里插入图片描述

10.5.3 相关命令

# 向hash中存入一个key/value(hash存在,不存在则是新建)
hset key(bigkey) field(smallkey) value  =>  hset personhash name zs(personhash是整个hash的key,name是其中一对键值对的key)
# 根据key获取value
hget bigkey smallkey  =>  hget personhash name
# 获取所有的键值对
hgetall key  =>  hgetall personhash
# 删除一个键值对
hdel bigkey smallkey  =>  hdel personhash name
# 判断是否存在某个键值对
hexists bigkey smallkey  =>  hexists personhash name
# 获得所有的键
hkeys key  =>  hkeys personhash
# 获得所有的值
hvals key  =>  hvals personhash
# 同时设置多个key/value
hmset personhash name zs age 23 addr jx
# 同时获取多个key/value
hmget personhash name age addr
# 设置一个不存在的key的值
hsetnx bigkey smallkey value  =>  hsetnx personhash bir 2020-11-10(nx表示not exist)
# 加法操作
hincrby personhash age 1
# 浮点加法
hincrbyfloat personhash age 1.22

10.6 补充:redis可视化工具RDM(redis-desktop-manager)

  • RDM的安装和使用
  • 注意:在连接时如果无法成功连接,则可能是因为没有开启redis的远程连接。我们需要修改redis.conf文件,找到bind 127.0.0.1,将它修改为bind 0.0.0.0允许任意客户端连接。

11、持久化机制

Redis官方为我们提供了两种持久化方法将数据从内存保存到硬盘。

  • 快照(Snapshot):保存redis某一时刻的数据状态。
  • AOF(Append Only File):只追加日志文件,就是将所有的写命令记录到日志文件中。

11.1 快照(snapshot)

11.1.1 特点

可以将某一时刻的数据保存到磁盘中,这是redis默认开启的持久化方式,生成的文件以.rdb结尾(默认生成的文件在bin目录下,叫做dump.rdb),所以也叫作RDB方式,redis会在启动时自动加载快照内保存的数据。

11.1.2 快照的生成方式

  • 客户端:bgsave指令和save指令
  • 服务端配置自动触发
# 客户端方式之bgsave
当在客户端使用bgsave指令时,redis会使用fork来创建一个子进程,然后子进程负责将快照写入到磁盘中,而父进程则继续处理请求。

在这里插入图片描述

# 客户端方式之save
当使用save指令时,将由主进程来负责生成快照以及将快照写入到磁盘(不会创建子进程),此时服务端不会响应其它的命令,也就是说此时redis处于阻塞状态,无法对外提供服务。

在这里插入图片描述

# 服务端方式之满足配置自动触发
当用户在`redis.conf`配置文件中配置了save选项,当满足其要求时便会触发bgsave命令,如果配置了多个save选项,则只要有一个满足,便会触发bgsave。下面是`redis.conf`中的相关配置:
################################ SNAPSHOTTING  ################################
#
# Save the DB on disk:
#
#   save <seconds>(秒数) <changes>(改变次数)
#
#   Will save the DB if both the given number of seconds and the given
#   number of write operations against the DB occurred.
#
#   In the example below the behaviour will be to save:
#   after 900 sec (15 min) if at least 1 key changed
#   after 300 sec (5 min) if at least 10 keys changed
#   after 60 sec if at least 10000 keys changed
#
#   Note: you can disable saving completely by commenting out all "save" lines.
#
#   It is also possible to remove all the previously configured save
#   points by adding a save directive with a single empty string argument
#   like in the following example:
#
#   save ""

save 900 1(900秒也就是15min内有至少一个改变则会触发bgsave)
save 300 10(300秒也就是5min至少有10次改变触发bgsave)
save 60 10000(60秒也就是1min至少有10000次改变触发bgsave)

# 服务端方式之接收客户端shutdown指令
当redis接收到shutdown(关闭服务器)指令,会执行一个save命令

11.2 AOF只追加日志文件

11.2.1 快照缺陷

我们知道,只要满足配置的要求,就会自动触发bgsave命令,但是这里会有一个很致命的缺陷,假如在保存完上一次快照之后、还未达到下一次保存快照的要求之前宕机了,那么这一段时间内的数据就会丢失,所以redis在此基础上增加了AOF。

11.2.2 特点

AOF会将我们客户端执行的所有写命令记录到日志文件中,以此来记录数据的变化,我们的redis只需要从头到尾执行一次便可以恢复原来的数据状态。
在这里插入图片描述

11.2.3 开启AOF持久化

redis的默认配置是没有开启AOF的,我们需要在配置文件redis.conf进行相关配置:

# 在redis.conf中找到APPEND ONLY MODE
appendonly no  =>  appendonly yes
appendfilename "appendonly.aof"//这个是AOF日志文件的名字,可自行进行修改

11.2.4 日志追加频率

所谓的日志追加频率指的是redis多久向日志文件中追加写命令记录,redis为我们提供了三种策略:

# always(谨慎使用)
说明:每个写命令都会被同步写入到磁盘中,会严重影响redis的速度。
解释:如果使用always选项的话,每个写命令都会被写到磁盘中,可以将数据的丢失减少到最小;但是这种策略需要频繁进行写操作,redis执行命令的速度会受到磁盘性能的影响。
# everysec(推荐)
说明:每秒执行一次同步将写命令记录到日志文件中。
解释:这种策略可以兼顾数据安全和写入性能两方面,既不会因为磁盘性能而影响到redis执行命令的速度,并且最多丢失一秒钟内的数据。
# no(不推荐)
说明:由操作系统决定何时执行同步
解释:这种策略虽然不会影响redis的性能,但是系统崩溃时,会丢失不定量的数据,万一操作系统一年之后才决定执行同步呢?或者说下一次会一次性写入几个G的写命令到日志文件?
# 修改日志同步频率
在redis.conf中找到appendfsync everysec进行修改即可

11.2.5 AOF文件的重写

1.AOF带来的问题

因为每一条写命令都会被记录到日志文件中,长此以往,日志文件的体积将会越来越大。如果我们执行了100条set name 24,那么日志文件就会记录100条,其实恢复数据状态只需要一条就够了,为了压缩aof持久化文件的体积,redis为我们提供了AOF重写机制。

2.AOF重写

用于在某种程度上减小日志文件的体积

3.重写触发的方式
# 客户端方式触发重写
在客户端使用BGREWRITEAOF指令
# 服务器配置方式触发重写
我们可以在redis.conf中找到这两个东西:
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
这两个表示当日志文件的体积大于64mb,并且体积是原来体积的一倍(100%)时触发重写。
64->20->40->27->54....
4.重写原理

重写aof文件并没有读取旧的aof文件,而是将整个内存中的数据库的内容用命令的方式重写了一个新的aof文件,替换原有的aof文件,这点和快照相类似。
在这里插入图片描述

# 重写流程
1. redis使用fork产生子进程,子进程会将当前数据库的数据状态以命令的方式保存到临时文件中;
2. 父进程会继续处理客户端请求,将写命令记录到旧的临时文件中,并且缓存这些写命令,这样就能保证即使子进程重写失败,也能恢复到正常的数据状态;
3. 等到子进程重写完毕,就会向父进程发出信号,然后父进程就会把缓存的写命令也写入到临时文件中;
4. 最后父进程就可以使用临时文件去替换旧的aof文件了。

12、java操作redis

12.1 简单使用

首先需要引入依赖:

compile group: 'redis.clients', name: 'jedis', version: '2.9.0'

接着编写测试代码

public static void main(String[] args) {
        //创建一个Jedis对象(参数是ip和端口号)
        Jedis jedis = new Jedis("xxx",6379);
        //选择库,默认是0号库
        jedis.select(0);
        //获取所有的key
        Set<String> keys = jedis.keys("*");
        keys.forEach(key-> System.out.println(key));
        //清空库
        jedis.flushDB();
        //jedis.flushAll();
        //释放资源
        jedis.close();
    }

12.2 相关操作

public class RedisTest01 {

    private static Jedis jedis;

    @Before
    public void before() {
        jedis = new Jedis("192.168.35.128",6379);
        jedis.select(0);
    }

    //测试key相关
    @Test
    public void testKey() {
        //获取所有的key
        Set<String> keys = jedis.keys("*");
        keys.forEach(key-> System.out.println(key));
        //判断一个或多个key是否存在
        System.out.println(jedis.exists("name", "age"));//2 存在的个数
        //删除一个或者多个key
        System.out.println(jedis.del("addr"));//1
        //设置超时时间
        jedis.expire("name",100);
        //获取一个随机的key
        System.out.println(jedis.randomKey());//name
    }
    
     //测试String类型
    @Test
    public void testString() {
        //set
        jedis.set("name","zs");
        //get
        System.out.println(jedis.get("name"));
        //mset:设置多个
        jedis.mset("age","23","addr","jx");
        //mget:获取多个
        List<String> list = jedis.mget("age", "addr");
        for (String s : list) {
            System.out.println(s);
        }
        //getSet:获取原值并设置
        System.out.println(jedis.getSet("name", "ls"));
    }

    @After
    public void after() {
        jedis.close();
    }
}

13、SpringBoot整合Redis

13.1 介绍

Spring Boot Data Redis为我们提供了RedisTemplateStringRedisTemplateStringRedisTemplateRedisTemplate的子类,方法大致相同,区别在于,RedisTemplate的两个泛型都是Object,意味着key和value都可以是对象,而StringRedisTemplate的两个泛型都是String,意味着key和value只能是字符串。

  • 注意点:RedisTemplate默认是将对象序列化到redis中,所以需要实现序列化接口

13.2 环境搭建

创建一个SpringBoot项目并引入相关依赖

compile group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis'

在配置文件中编写redis相关

# 配置redis
spring.redis.host=xxx.xxx.xxx.xxx
spring.redis.port=6379
spring.redis.database=0

13.3 测试StringRedisTemplate

@SpringBootTest(classes = Redisdemo02Application.class)
@RunWith(SpringRunner.class)
public class StringRedisTemplateTest {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void testString() {
        stringRedisTemplate.opsForValue().set("name","zs");
        stringRedisTemplate.opsForValue().set("age","23",100, TimeUnit.SECONDS);//设置超时时间为100秒
        stringRedisTemplate.opsForValue().append("name"," is stupid");//追加内容
        stringRedisTemplate.opsForValue().decrement("age",10);//减10
        stringRedisTemplate.opsForValue().getAndSet("name","ls is smart");//获取原值并设置
    }

    @Test
    public void testList() {
        //stringRedisTemplate.opsForList().leftPush("list","zs");//左边添加
        //stringRedisTemplate.opsForList().rightPush("list","ls");//右边添加
        List<String> list = stringRedisTemplate.opsForList().range("list", 0, -1);//获取所有的value
        list.forEach(s-> System.out.println(s));
        String s1 = stringRedisTemplate.opsForList().index("list", 2);//获取指定索引处的元素
        System.out.println(s1);
        String s2 = stringRedisTemplate.opsForList().leftPop("list");//移除左边第一个元素
        System.out.println(s2);
        String s3 = stringRedisTemplate.opsForList().rightPop("list");//移除右边第一个元素
        System.out.println(s3);
        stringRedisTemplate.opsForList().remove("list",3,"zs");//最多删除列表中三个zs
        System.out.println(stringRedisTemplate.opsForList().size("list"));//获取列表长度
    }
}

13.4 RedisTemplate测试

RedisTemplateStringRedisTemplate的父类,他存储的key和value都是Object,并且在存储时会对key和value进行序列化操作,所以key和value所在类必须实现序列化接口(Serializable):

@SpringBootTest(classes = Redisdemo02Application.class)
@RunWith(SpringRunner.class)
public class RedisTemplateTest {
    @Autowired
    private RedisTemplate redisTemplate;

    //测试String
    @Test
    public void testString() {
        redisTemplate.opsForValue().set("name","zs",100, TimeUnit.SECONDS);//\xac\xed\x00\x05t\x00\x04name  key经过了序列化
        System.out.println(redisTemplate.opsForValue().get("name"));
    }

    //测试Object
    @Test
    public void testObject() {
        User user = new User();
        user.setName("zs");
        user.setAge(23);
        redisTemplate.opsForValue().set("user",user);//\xac\xed\x00\x05t\x00\x04user
        User u = (User) redisTemplate.opsForValue().get("user");
        System.out.println(u.getName());//zs
    }

    //序列化方案
    @Test
    public void testSerializer() {
        //RedisTemplate默认使用JDK序列化方案,我们一般会将key的序列化方案设置为String类型序列化,这样也方便在终端查看
        redisTemplate.opsForHash().put("map","name","zs");//hash类型
        System.out.println(redisTemplate.getKeySerializer());//获取大key的序列化方案 JdkSerializationRedisSerializer
        System.out.println(redisTemplate.getHashKeySerializer());//获取小key的序列化方案 JdkSerializationRedisSerializer
        //修改序列化方案  String类型序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    }
}

13.5 Bound Api

为什么要有Bound Api(绑定api),我们可以发现,每次我们使用RedisTemplate或者StringRedisTemplate进行操作时都需要带上key,假如我们需要连续多次对同一个key进行操作的话,显然很不友好,所以Spring Data为我们提供了绑定api,它可以让我们去绑定一个key,并且在后续操作时不需要带上key。

    //bound api
    @Test
    public void testBoundApi() {
        BoundHashOperations hashOperations = redisTemplate.boundHashOps("user");
        hashOperations.put("name","zs");//不需要带上大key user
        hashOperations.put("age",23);

        System.out.println(hashOperations.get("name"));//zs
    }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值