Redis学习记录+面试题

NoSql的特点:

  • 方便扩展,数据之间没有关系,很好扩展
  • 大数据高性能,是一种细粒度的缓存
  • 数据类型多样,不需要事先设计数据库,随取随用

NoSql的四大分类

  1. KV键值对 Redis
  2. 文档型数据库 MongoDB
  3. 列存储数据库 HBase
  4. 图关系数据库 Neo4j

Redis

Remote Dictionary Server, 远程字典服务。支持网络、可基于内存和持久型的日志型。

作用:

  1. 内存存储,持久化
  2. 可用于高速缓存
  3. 发布订阅系统,队列
  4. 地图信息分析
  5. 计数器、记时器,浏览量

Redis的安装
Redis中文官网

要和系统的gcc版本一样 Redis6+需要gcc5+
因此用的Redis5.0.13

make
make test
make install

测试成功
在这里插入图片描述
需要修改一些redis-config的配置

ps -ef | grep redis

然后检查redis服务器是否启动成功
在这里插入图片描述
如果需要进不去,可能你的redis已经开启了,因此需要关掉 使用kill 端口号
在这里插入图片描述

启动redis 配置ip和端口号

redis-cli -h 127.0.0.1 -p 6379

退出Redis

shutdown
exit

在这里插入图片描述

Redis是单线程为什么这么快

Redis是基于内存操作的,性能瓶颈是机器的内存和网络带宽。将所有的数据全部放在内存中,使用单线程去操作执行效率就是最高的,而多线程再执行过程中,需要CPU的上下文切换,是耗时的,而单线程多次读写都是在一个CPU上。

Redis是C语言写的,使用跳表,链表,动态字符串,压缩列表来实现redis的数据结构,从而导致效率更高。

Redis的八种数据类型

String

对于字符串的一些处理

set key1 v1 ==>  ok
get key1 ==> "v1"
append key1 hello ==> "7" ==> "v1hello"
strlen key1 ==> 7

getrange key1 0 2 ==> "v1h"
getrange key1 0 -1 ==> "v1hello" -1 代表尾部

set key1 hello_world ==> "hello_world"
setrange key1 1 x ==> 11 ==> "hxllo_world"
set key1 hello_world ==> "hello_world"
setrange key1 1 xx ==> 11 ==> "hxxlo world" 要注意这两个不同的地方


对于数字,比如可以用在粉丝数,浏览量

set view1 1 ==> ok
set view1 2 ==> ok
incr views ==> views = 3
decr views ==> views = 2

incrby views 2 ==> views = 4

对象

set user:1 {username:zhangsan,age:20} ==> ok
mget user:1 ==> 1) "{username:zhangsan,age:20}" 得到一条数据 这个数据是json数据
getset key8 value8 ==> nil
getset key8 value8 ==> "value8"
getset key8 value9 ==> "value8"
getkey8 ==> "value9" 先返回的是value8,但是实际内存中已经是value9

List

在Redis中可以把list用作栈、队列、阻塞队列。

l开头一般代表队列左侧执行,r代表队列右侧执行。

从左边进,当作栈,后进先出

127.0.0.1:6379> lpush list value1
(integer) 1
127.0.0.1:6379> lpush list value2
(integer) 2
127.0.0.1:6379> lpush list value3
(integer) 3
127.0.0.1:6379> lrange list 0 -1 		这里-1代表尾部
1) "value3"
2) "value2"
3) "value1"
127.0.0.1:6379> rpush list value0
(integer) 4
127.0.0.1:6379> lrange list 0 -1
1) "value3"
2) "value2"
3) "value1"
4) "value0"
在1的左边加, 或者在1的右边加
[3 2 1] ====> [3 2 1 0 ]

从左侧弹出

127.0.0.1:6379> lpop list 
"value3"
127.0.0.1:6379> lrange list 0 -1
1) "value2"
2) "value1"
3) "value0"
127.0.0.1:6379> rpop list
"value0"
127.0.0.1:6379> lrange list 0 -1
1) "value2"
2) "value1"

根据索引获取值,获取长度

127.0.0.1:6379> lindex list 1
"value1"
127.0.0.1:6379> llen list
(integer) 2

移除列表中的数值

127.0.0.1:6379> lpush list value3
(integer) 3
127.0.0.1:6379> lpush list value4
(integer) 4
127.0.0.1:6379> lpush list value5
(integer) 5
127.0.0.1:6379> lrange list 0 -1
1) "value5"
2) "value4"
3) "value3"
4) "value2"
5) "value1"
127.0.0.1:6379> lrem list 2 value4
(integer) 1
127.0.0.1:6379> lrange list 0 -1
1) "value5"
2) "value3"
3) "value2"
4) "value1"

小结:

  • list实际是一个链表,前后都可以插入
  • 如果key不存在,就创建一个新的链表
  • 如果演出了所有的值,空链表,也代表不存在
  • 在两边插入或者改动值,效率是最高的

Set

sset set xxxx xxx
smembers set 集合中所有的元素
sismember set xxx 集合中是否存在xxx
smove set newset xxx 移动集合中的元素到newset中
sdiff set1 set2 差集
sniter set1 set2 交集
sunion set1 set2 并集

Hash

hset hash1 field1 value1 创建一个hash的名字叫hash1 存储了一个(field1,value1)
hget hash1 field1 得到在hash1中field1所对应的value值
hgetall hash1 获取所有hash
hdel 删除
hlen 获取长度
hexist 是否存在
hkeys hash1 获取所有在hash1中的key
hvalues hash1 获取所有在hash1中的value

hash适合存储一些经常变动的对象信息,string更适合存储字符串

ZSet

适用于做排行榜

zadd zset score value 在zset中加入分数为score的value值,会根据score进行排序
zrange zset 0 -1 获取所有的值
zrevrange zset 0 -1 反序排
zrangebyscore zset 15 22 WITHSCORES 按照一定条件进行排

特殊数据类型

geoadd china:city 1163.40 39.90 beijing
geodist 两地的距离
hyperloglog
bitmap

Redis的事务和SpringBoot集合

参考Redis事务详解

Redis单条命令是保证原子性的,但是事物不保证原子性

解释:

Redis事务提供了一种“将多个命令打包, 然后一次性、按顺序地执行”的机制, 并且事务在执行的期间不会主动中断 —— 服务器在执行完事务中的所有命令之后, 才会继续处理其他客户端的其他命令

因此Redis的一个事物从开始到执行会经历

开始事务 、 命令入队 和 执行事务

multi 开启事务
exec 提交事务 
discard 取消事务,放弃执行事务块内所有命令
watch 监视一个key,如果在事务exec之前,key被其他命令(来自其他客户端)改动,那么事务被打断
错误命令:如果遇到了异常,所有的命令都不会被执行
语法错误:当条命令抛异常、但是其他命令会被执行

监视Watch

悲观锁:无论什么都会加锁,影响效率
乐观锁,在更新数据的时候需要判断是否在这个时期修改过监视的数据。就比如在更新的时候需要判定一下这个即将要被修改的值和内存中存的原来的值是否一致,如果不一致,就说明在这期间有其他线程访问了数据,很有可能就会引发线程安全问题。

使用Watch

在这里插入图片描述

Watch命令的实现

在每个代表数据库结构类型中都保存了一个watched_keys字典

在这里插入图片描述
该字典中的key:被监视的key
该字典中的value:一个链表,保存了所有监视这个键的客户端

就比如key1正在被client2、client5、client1监视。

假设此时一个客户端client10086要监视key1,key2;key1,key2的链表会被修改如下
在这里插入图片描述

  • watch的触发

一旦某个key进行了修改命令(比如set、del、sadd…)那么就检查watched_keys字典是否有其他的客户端在监视已经被命令修改的key。

如果有的话,就说明其他客户端也正在对key操作,并且将监视这个key的所有客户端的REDIS_DIRTY_CAS打开,表明事务安全性已经被破坏。

如果在EXEC命令,触发事务执行时,服务器对所有客户端进行检查。

  • 有一个REDIS_DIRTY_CAS打开,服务器放弃执行,事务失败
  • REDIS_DIRTY_CAS都没有打开,所有监视的键都安全,服务器执行事务

Jedis

使用java操作redis,jedis是redis官方推荐java的开发工具

添加依赖

<dependency>
   <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.2.0</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.75</version>
</dependency>
Jedis jedis = new Jedis("127.0.0.1", 6379);
        jedis.flushDB();
        //开启事务
        Transaction multi = jedis.multi();
        //一系列命令的集合
        try {
            multi.set("user1","{'username':'zhangsan'}");
            multi.set("user2","{'username':'lisi'}");
            int i = 1/0;
            multi.exec();
        }catch (Exception e){
            e.printStackTrace();
            //事务回滚
            multi.discard();
        }finally {
            String user1 = jedis.get("user1");
            String user2 = jedis.get("user2");
            System.out.println(user1);
            System.out.println(user2);
            jedis.close();
        }

在Spring Boot中整合Redis

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

在这里插入图片描述
在SpringBoot中默认就是lettuce

自动加载RedisTemplate的原理

首先在Spring项目启动之前,就会先搜索META-INF下的spring.factories
在这里插入图片描述
该文件就包括了启动项目,需要自动注入的组件
在这里插入图片描述
可以看到redis包含了自动加载

@Configuration(
    proxyBeanMethods = false
)
@ConditionalOnClass({RedisOperations.class})
@EnableConfigurationProperties({RedisProperties.class})
@Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class})
public class RedisAutoConfiguration {
	public RedisAutoConfiguration() {
    }
	@Bean
    @ConditionalOnMissingBean(
        name = {"redisTemplate"}
    )
    @ConditionalOnSingleCandidate(RedisConnectionFactory.class)
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}
生成的redisTemplate

该自动配置文件就包含了一些是否RedisOperations.class存在的条件注解,还有绑定了RedisProperties.class配置文件
而该配置类就包含了这些信息,如果需要配置redis的相关设定,就以spring.redis在application配置文件中配置

@ConfigurationProperties(
    prefix = "spring.redis"
)
public class RedisProperties {
    private int database = 0;
    private String url;
    private String host = "localhost";
    private String username;
    private String password;
    private int port = 6379;
    private boolean ssl;
    private Duration timeout;
}

存一个对象的话,需要实现序列化接口,转成Json字符串存

而在redis存的时候,会在前面加上一些转义字符,这些字符是用redisTemplate就会有且能识别,但是别的就不行(因为在这里被jdk序列化了)

可以自定义redisTemplate进行序列化

也可以直接使用RedisTemplate<string,string>或者StringRedisTemplate就不会被转义

一般来说存value都用json字符串来存

结合上述所说的事务,可以直接用redisTemplate开启事务

在这里插入图片描述

Redis的配置

配置在**/usr/local/bin/redis-config**目录下

打开配置文件 vim redis.conf

### 单位
# Redis configuration file example.
#
# Note that in order to read the configuration file, Redis must be
# started with the file path as first argument:
#
# ./redis-server /path/to/redis.conf

# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.

包含

也就是一个配置文件可以包含多种配置文件,就能够方便在各个文件中写好自己的配置,再导入,方便管理

################################## INCLUDES ###################################

# Include one or more other config files here.  This is useful if you
# have a standard template that goes to all Redis servers but also need
# to customize a few per-server settings.  Include files can include
# other files, so use this wisely.
#
# Notice option "include" won't be rewritten by command "CONFIG REWRITE"
# from admin or Redis Sentinel. Since Redis always uses the last processed
# line as value of a configuration directive, you'd better put includes
# at the beginning of this file to avoid overwriting config change at runtime.
#
# If instead you are interested in using includes to override configuration
# options, it is better to use include as the last line.
#
# include /path/to/local.conf
# include /path/to/other.conf

网络

# bind 127.0.0.1
protected-mode no 不开启保护模式
port 6379

通用

daemonize yes 守护进程
pidfile /var/run/redis_6379.pid 一旦开启就会写入一个pid在后台中
logfile “” 	日志文件的位置
database 16 数据库数量默认16

快照SnapShooting

内存中的数据集需要打包到磁盘上,也就是快照存储

save 900 1 		如果900s内,至少有1个key进行了修改,进行持久化操作
save 300 10  	如果300s内,至少有10个key进行了修改。进行持久化操作
save 60 10000
top-writes-on-bgsave-error yes  如果持久化出现错误,是否还需要工作
rdbcompression yes

安全

requirepass 123456 设置密码为123456

内存

volatile-lru -> Evict using approximated LRU among the keys with an expire set.

append only 模式配置AOF配置

appendonly on 默认是不开启AOF模式的,默认是使用rdb方式持久化,大部分情况下,rdb完全够用。
appendfilename "appendonly.aof" 持久化文件的名字

Redis持久化

Redis是内存数据库,如果不将内存中的数据库保存到磁盘,一旦服务器的进程退出,服务器的数据库的状态就会消失,从而Redis提供了持久化功能

RDB (Redis Database)

在这里插入图片描述

Redis首先会创建一个fork子进程进行持久化,会先将数据写入一个临时文件中,等到持久化过程结束了,在用新的RDB文件替换旧的RDB文件。

在整个过程中,主进程不进行任何的IO操作,这样就保证饿了极高的性能。

RDB唯一的缺点就是最后一次持久化的数据可能丢失。【比如断电,内存中的数据就丢失了,生成的新的RDB文件也就不能写入到磁盘中】

RDB保存的文件叫做dump.rdb

触发保存的命令

save 阻塞
bgsave 非阻塞
flushall 刷新也会也会保存

总结:

优点: 适合大规模的数据恢复,对数据的完整性要求不高

缺点:需要一定的时间间隔进行操作,如果redis宕机,最后一次修改的数据就会丢失

AOF (append only file)

AOF持久化以独立日志的方式记录每次写命令,比如说aof文件中就包含了多条

set k1 v1
set k1 v2
...

然后在Redis重启时执行AOF命令以达到恢复数据的目的,AOF的主要作用就是解决数据持久化的实时性。

在配置文件中的体现

appendfsync everysec 每秒执行一次sync,最坏的情况就是丢失这1s的数据

  • 使用需要开启appendonly yes

  • 如果自己手动修改了aof文件,可能会导致错位,那么客户端就不能正常的链接了,因此需要修复AOF文件

    Redis提供了工具

    redis-check-aof --fix appendonly.aof
    

AOF保存的文件叫做appendonly.aof
在这里插入图片描述
aof有重写功能,只会保留最后一条需改的命令(这样就保证了aof文件的体积)

对比

  • RDB持久化方式能够在指定的时间间隔内对数据进行快照存储
  • AOF持久化方式记录每次对服务器写操作,当服务器重启时就会重新执行这些命令恢复原始数据。
  • 如果只需进行缓存,比如说这些数据在服务器运行的是否存在即可,就不需要持久化到磁盘上,但是对于热点数据也需要持久化
  • 是否能同时开启两种持久化方式
  • 当Redis重启的时候会优先加载AOF文件来恢复原始数据,因为AOF文件保存的数据集要比RDB文件保存的数据集要完整
  • RDB的数据不实时,但是适合备份数据,因为AOF不断变化(不断的在aof文件末尾加入写命令)

回归配置文件

在看redis.conf文件就能够看到为什么存在save 900 1 这条规则,也就是15分钟备份一次就够了

对于开启AOF后,最恶劣的情况也只是会丢失两秒的数据,然而开启AOF的代价就是

  1. 持续的IO(一直在往aof中写),
  2. aof writer的最后将rewrite过程中产生的新数据写到新文件中造成的阻塞是不可避免的。

因此需要尽量减少AOF rewrite的频率。

发布订阅模式、主从复制和哨兵模式

Redis的订阅发布

使用场景:

  1. 实时消息系统
  2. 实时聊天
  3. 订阅、关注系统都可以,比如微博关注了某一个用户

简单的用订阅发布,用于稍微复杂的场景用MQ

发送者pub发消息,订阅者sub接受消息

redis客户端可以订阅任意数量的频道(可以理解为公众号)

在这里插入图片描述
命令

public
subscribe
psubscribe
...

主从复制

实现高可用的基础

概念

master节点以写为主
slave以读为主

默认情况下,每台Redis服务器都是主节点,且一个主节点可以有多个从节点或者没有从节点,但是一个slave只能有一个master

主从复制的作用

  1. 数据冗余:实现了数据的热备份,比如master挂了,数据也能够在slave中获取
  2. 故障恢复:master出现问题,slave可以提供服务,实现快速的故障恢复,实际也是一种服务的冗余
  3. 负载均衡:在上述的基础上,配合读写分离,可以由master提供写服务,slave提供读服务。由此,Redis数据应用应该连接在master,读数据的时候应该连接slave,分担服务器负载。尤其是在写少读多的场景中,如果能够实现多个slave分担负载,那么就能够提高Redis的并发量

单台Redis最大使用内存不应该超过20g,可以通过负载均衡这种增强

比如在电商网站上,就可以用到如上的架构,读多写少

复制原理

在这里插入图片描述

最主要的就是master会给slave发送自己服务器的rdb文件

  • 如果遇到rdb文件很大,比如2GB,需要向他的多个slave发送rdb,那么显然负载很大。

  • 解决方式:

    发给了一个slave后,接受到的slave再发送给其他的slave,但是这也面临一个问题,就是先得到的rdb的slave1的数据肯定比其他的数据多,所以这样就存在数据的滞后性

哨兵模式

master挂了 slave成为master,则就需要哨兵模式

哨兵模式能够后台监控master是否故障,如果故障就根据**投票数(投哨兵节点)**将slave转化成master
在这里插入图片描述
但哨兵此时是单节点,如果哨兵挂了,因此需要多哨兵

当有多个哨兵节点,需要选出一个哨兵节点,来去进行主从切换,因此选择的不是slave,而是一个sentinel。
在这里插入图片描述

哨兵原理

多哨兵模式(布置奇数节点,为了投票)

  1. 一个哨兵认为master挂了,作为主观下线
  2. 其他的哨兵也去判定master是否挂了,并且达到一定的值就为客观下线
  3. 进行选举,决定一个哨兵进行failover(故障转移)
  4. 哨兵会判断哪一个slave有最新数据
    根据时一个队列中的offset越大,就代表数据越新
  5. leader哨兵解决完成谁是那个master后,发布订阅模式,让其余哨兵监控的slave都进行主从切换
    在这里插入图片描述

Redis集群

动态水平伸缩,Redis3.0推出了Redis Cluster,实现可扩展性

  • 采用无中心结构
  • 多个节点自动进行数据粉片
  • 支持节点的动态添加和移除
  • 自动故障转移

哈希槽

2的14次方个哈希槽,每个key通过CRC16校验16383取模来决定放哪个槽。

Cluster中过的每个节点负责一部分的hash槽 (hash slot)

在这里插入图片描述
在这里插入图片描述

面试题

1.Redis有什么特点?

key-value的内存数据库

  • 支持持久化,可以将内存中的数据持久化到磁盘
  • 支持多种数据结构
  • 支持数据的备份RDB,还有主从模式的备份AOF
  • 高性能 读速度11w/s 写8.1w/s
  • 支持事务

2. Redis的数据类型

一共有8种
5种基本数据类型:String 、Hash、List、Set、ZSet
3种特殊类型:geospatial、hyperloglog、bitmap

3. Redis和Memcache的区别

  • memcache数据存储在内存中,断电即失
  • memcache支持的数据结构只有简单的string,而redis有多种

4. Redis是单线程的?

数据放在内存中,单线程执行效率最高,多线程执行反而需要进行CPU上下文切换,是一个耗时操作,因此会导致执行性能下降

5.Redis的持久化?

RDB:使用数据集快照的方式以一定的时间点,所有的数据先写入一个临时文件,持久化到硬盘中,持久化结束后,用这个临时文件替换上次持久化的文件,实现快照存储,如果想恢复之前的数据,使用之前的持久化文件,达到数据恢复的目的。

优点:

  • 只有一个文件dump.rdb方便持久化
  • Redis会单独fork一个子进程进行持久化,主进程不进行任何的IO,只负责写。
  • 在数据较多的时候,比AOF的效率要高,并且适合用于数据备份
    缺点
  • 最后一次持久化的数据可能会丢失

AOF:以独立日志的方式记录每次写命令(不包含读),并在Redis重启时执行AOF文件的命令达到恢复数据的目的。主要解决数据持久化的实时性(RDB并不实时)

优点

  • 数据安全,选择不同的同步策略,1s就更新一次,这样数据丢失就只有1s
  • 自动修复功能 redis-check-aof

缺点

  • AOF文件比RDB文件大,且恢复速度慢,因为文件中存入的大多数是命令
  • 数据多时,效率低于RDB

如何防止AOF文件过大?

  • 执行AOF重写,对同一个数据的重复的写操作,只保留最后一次的修改记录

6.Redis的主从复制

主从复制就是将一台Redis服务器的数据复制到其他的Redis的服务器,前者为master,后者是slave

作用:

  • 数据冗余:数据的热备份,(更新备份在从节点上)
  • 故障修复:当主节点出现故障,从节点还可以提供服务
  • 负载均衡:主节点只提供写,而从提供读
  • 高可用的基石:主从复制是哨兵模式的基础

复制的原理:

  • 从节点成功连接主节点后,会发送一个sync的同步命令,主节点接收到后,启动后台的存盘进程(生成rdb文件),收集所有修改数据库的命令,将整个数据文件传送给从节点,完成一次完全同步

全量复制:从节点接受到数据文件后,将其存盘文件都加载到内存中
增量复制:主节点继续将收集到的修改命令传递给从节点,完成同步。

7.哨兵模式

为了解决手动切换主节点。

哨兵是一个独立的进程

  1. 哨兵监视到主节点挂了,此时为主观下线
  2. 通知其他的哨兵也开始去查看是否主节点挂了,达到了一定的值,就为客观下线
  3. 投票选举由哪一个哨兵节点去选主节点,生成一个leader
  4. leader从而选择一个从节点成为主节点。
  5. 选主的条件,数据多的,连接快的。。。。

8. 缓存穿透、缓存击穿、缓存雪崩

缓存穿透

用户需要查询一个数据,缓存中没有,数据库也没有。但是用户非得去查询这个数据,于是就向数据库中发送大量请求,造成很大的压力

  • 解决方法
    • 布隆过滤器:一种数据结构,对所有可能查询的参数以hash存储,先在控制层校验,不符合规定就丢弃,这样避免过多访问数据库
    • 缓存空对象:当存储层没有找到数据,即返回控对象也将缓存起来。

缓存击穿

当一个key非常热点,在不断的扛着高并发,集中对这个热点数据进行访问,当这个key失效的瞬间(缓存中的key突然过期了),请求直接数据库,给数据库就会造成很大的压力。

  • 解决方案
    • 设置热点数据永不过期
    • 加分布式锁:保证每个key同时只有一个线程去查询后端服务

缓存雪崩:

某个时间段,缓存集中失效

  • 解决方法:
    • 增加Redis集群的数量
    • 设置缓存过期时间的时候错峰设置
    • 限流降级:在缓存失效的时候,通过加锁和队列控制写缓存的线程数量
    • 数据预热:在正式部署之前,将数据预先访问一遍,让缓存失效的时间均匀一点。

9.Redis的使用场景

  1. 会话缓存:如单点登录,使用Redis模拟session,SSO系统生成一个token,将用户信息存在Redis当中,并设置过期时间。
  2. 全页缓存
  3. 排行榜和计数器(zset的score来进行排行)
  4. 发布/订阅:比如聊天系统
  5. 热点数据

10.如何保证缓存的一致性

读数据的流程:

  • 首先先去Redis缓存中读取,没有再去MySQL中读取,读取到的数据更新到Redis中作为下一次的缓存。

因此此时就会存在Redis和Mysql数据不一致的情况,不能保证原子性。

但是能够保证最终的一致性,即缓存失效后,从数据库更新缓存。

11.集群

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值