Redis从入门到实战

redis笔记


1、Redis的安装

1.1 win安装

1、官网下载安装包:https://redis.io/

在这里插入图片描述

2、解压

3、Redis启动

Redis和MySQL相同也是典型的C/S架构的系统,所以我们需要先启动服务器端然后再使用Redis的客户端登录Redis服务器端。

4、服务端启动

进入到解压后的目录,双击redis-server.exe或是在cmd窗口下执行redis-server redis.windows.conf

当然,要是长时间需要使用到redis启动等命令,那么也可以配置一个环境变量

在这里插入图片描述

5、客户端启动

Redis默认没有密码,设置密码方式为,将下图用红色框圈出的文件使用NotePad++打开然后将requirepass的注释打开,然后再其后面写上自己项设定的密码即可,我已经密码设置为123456所以上文客户端登录可以直接使用123456作为密码登录。

在这里插入图片描述

在这里插入图片描述

远程服务上执行命令

如果需要在远程 redis 服务上执行命令,同样我们使用的也是 redis-cli 命令。

语法:redis-cli –h IP地址 –p 端口 –a 密码

如果是登录远程redis需要使用-h参数,我们本次登录本机redis则不需要使用-h参数。

本地客户端登录用redis-cli 密码登陆(redis-cli -a password)

或者双击redis-cli.exe后,执行auth 密码登录

6、Redis关闭

第一种关闭方式:

非正常关闭,比如断电,直接杀死Redis进程等方式。这种方式容易造成数据丢失。

第二种关闭方式

(正常关闭、数据保存)通过客户端使用shutdown命令关闭redis服务。

1.2 Linux下使用Docker安装(推荐)

1、安装Docker即Docker的基础使用

Docker基础使用

2、执行以下指令

首次使用Docker会下载redis,可能会很慢,可以配置阿里云代理等方式

docker run -p 6379:6379 --name redis --privileged=true -v /app/redis/redis.conf:/etc/redis/redis.conf -v /app/redis/data:/data -d redis redis-server /etc/redis/redis.conf 

命令解释:
docker run -p 6379:6379 #端口映射
--name redis #设置容器名字
--privileged=true #开启权限
-v /app/redis/redis.conf:/etc/redis/redis.conf #配置容器卷,用于数据持久化和配置读取
-v /app/redis/data:/data 
-d redis redis-server /etc/redis/redis.conf #读取我们修改好的配置

3、Redis配置文件

随便找个redis配置文件,或者是使用win安装中下载下来的配置文件,复制或是复制文件中内容,放到Linux /app/redis/redis.conf即可

4、设置Redis自启动(可选)

docker update redis --restart=always

5、客户端连接到redis

在Linux下连接redis

[root@centos100 redis]# docker exec -it redis /bin/bash
root@3b6932343d28:/data# redis-cli
127.0.0.1:6379> auth 123456
OK
后续就可以直接执行Redis的指令

在win连接到redis

如果你下载了win安装redis的安装包,那么,进入到解压后的目录执行cmd,执行redis-cli –h IP地址 –p 端口 –a 密码命令,也可以连接到Linux下的redis

如果没有下载,也可以使用redis的图形用户工具连接,如RedisDesktopManager

Redis集群也会使用Docker安装,当然要是想跟深入了解可以看Docker高级篇中详细记录了集群安装及其思路

1.3 Centos安装

1、win网页下载好tar包后,将文件上传到centos中

2、使用tar -zxvf redis-x.x.x.tar.gz进行解压

3、可以发现,生成了一个redis文件夹,进入文件夹可以看到,里面存在一个Makefile文件

4、安装gcc

yum install -y gcc

5、进入解压目录后执行以下命令

make MALLOC=libc

6、编译完成后执行以下命令

make install PREFIX=/usr/redis

7、进入/usr/redis/bin执行./redis-server,启动服务

8、新增一个窗口/usr/redis/bin,进入,执行./redis-cli -h 127.0.0.1 -p 6379连接到服务

redis细节

1、redis启动服务的细节

注意:使用./redis-server方式启动,使用的是redis-server这个shell脚本的默认配置

2、如何在启动redis时使用自定义的配置

在默认redis安装完成之后,在安装目录没有任何配置文件,需要在源码目录中复制配置文件到安装目录

a.复制配置文件

cp ~/redis-6.2.6/redis.conf .

此时,使用ls就可以看到复制过来的配置文件

b.进入bin目录加载配置

./redis-server ../redis.conf

使用redis.conf配置文件启动服务

c.修改redis.conf文件中port 7000

使用./redis-server ../redis.conf启动服务

d.执行./redis-cli -h 127.0.0.1 -p 7000连接到服务

3、redis中库的概念

库:用来存放数据的一个基本单元,一个库可以存放ket-value键值对,redis中每一个库都有一个唯一的编号,从0开始

库的个数:默认情况下为16个库,即编号0-15,默认使用0号库

4、redis客户端使用中文时

./redis-cli -p 7000 --raw

2、常用命令

2.1 库操作

Redis默认16个数据库,类似数组下标从0开始,初始默认0号库

命令说明
select 切换数据库,例:select 3
dbsize查看当前数据库的key数量
flushdb清空当前库
flushall清空所有库

2.2 Redis键(Key)

keys *查看当前库的所有数据,查找符合条件的key,如还可以:
keys h?llo 匹配hello,hallo,hbllo
keys h*llo 匹配heeeeeello,0-多个字符
keys h[ae]llo 匹配hello,hallo,但是不匹配hbllo
exists key判断某个可以是否存在,判断多个key时,只要有一个存在就返回1
type key查看key是什么类型
del key删除一个或多个key数据
unlink key根据value选择非阻塞是删除(仅将keys从keyspace元数据中删除,真正的删除会在后续异步操作)
ttl key查看key还有多少秒过期,-1表示永不过期,-2表示已经过期,也代表key不存在
expire key 1010秒钟:给定key的过期时间
move key db将当前数据库的key移动到指定的db中
rename key newkey修改key为newkey,当key不存在时返回错误

2.3 字符串(String)

string类型是二进制安全的,这意味着Redis的string可以包含任何数据,如jpg图片或者反序列化对象。

string是Redis最基本的数据类型,一个Redis中的字符串value最多可以是512M

set 添加键值对

在这里插入图片描述
NX:当前数据库中key不存在时,可以将key-value添加到数据库

XX:当前数据库key存在时,可以将key-value添加到数据库,与NX互斥

EX:key超时秒数

PX:key的超时毫秒数,与EX互斥

命令说明
get 查询对应键值对
append 将给定的value追加到原值的末尾,返回追加后的字符串长度
strlen 获得值的长度
setnx 只有key不存在时,设置key的值
incr 将key中储存的值增1,只能对数字值使用,如果为空,新增值为1,返回增加后的结果
decr 将key中储存的值减1,只能对数字值使用,如果为空,新减值为-1,返回减少后的结果
incrby/decrby <步长>将key存储的数字增减。自定义步长
mset …同时设置一个或是多个key-value
mset …同时获取一个或是多个key
msetnx …同时设置一个或是多个key-value,当且仅当所有给定的key都不存在,当一个不成功,都不成功
getrange <起始位置> <结束位置>获取值的范围,类似java中的sbstring,前包、后包
setrange <起始位置> 用value覆盖key所存储的字符串,从起始位置开始(索引从0开始)
setex <过期时间> 设置键值的同时,设置过期时间
getset 以旧值取代新值

2.4 列表(List)

Redis列表是简单地字符串列表,按照插入顺序,你可以添加一个元素到头部或是尾部

它的底层实际是一个双向链表,对两端的操作性能跟高,通过索引下标操作中间节点的性能可能会很差

内存模型
在这里插入图片描述

lpush/rpush …从左边/右边插入一个值
lpop/rpop 从左边/右边吐出一个值。值在健在,值无键亡
rpoplpuch 从key1中右边取出一个值插入到key2左边
lrange 按索引下标获得元素(从左到右)
lrange mylist 0 -1 #表示获取所有值
lindex 按照索引位置获取元素(从左到右)
llen 获取列表长度
linsert before 在value后面插入newvalue
lrem 从左边删除n个value
lset 将列表key中index位置的值替换成value
lpushx/rpushx …同lpush,但是要保证这个key存在
ltrim 保留列表中指定区间的元素

2.5 集合(set)

Set是Redis的string类型的无序集合,它的底层是value为null的hash表,所以添加、删除、查找的时间的复杂度都接近O(1)

内存模型
在这里插入图片描述

命令说明
sadd …将一个或多个member元素加入到集合key中,已经存在的member忽略
smembers 取出给集合的所有值
sismember 判断集合key中是否含有该value值,有则为1,没有为0
scard 返回该集合的元素个数
srem …删除集合中的某些元素
spop 随机从集合中取出一个值
srandmember 随机从集合中取出n个值,不会从集合中删除
smove value把集合中一个值从一个集合移动到另一个集合,比武时同种类型
sinter 返回两个集合中交集部分
sunion 返回两个集合中并集部分
sdiff 返回集合中差集部分(key1中的不包含key2中的),去掉第一个集合中,其他集合中存在的元素
spop 随机返回一个元素,并从集合中删除

2.6 ZSet

特点:可排序的set集合、无序、不可重复

内存模型
在这里插入图片描述

命令说明
zadd添加一个有序集合元素
zcard返回集合中元素个数
zrange 升序/zrevrange 降序返回一个范围内的元素
zrangebyscore按照分数查找一个范围内的,还支持分页查询
zrank返回排名
zrevrank返回倒序排名
zscord显示某个元素的分数
zrem删除某个元素
zincrby给某个特定元素加分

2.7 Hash类型

redis key(string) value(map)

Map<String,Map<String,value>>

特点:

value是map结构,key无序

内存模型

在这里插入图片描述

命令说明
hset设置一个key-value对
hget获取一个key对应的value
hgetall获取所有key-value键值对
hdel删除某个键值对
hexists判断是否存在这个key
hkeys获取所有key
hvals获取所有value
hmset一次性设置多个key-value
hmget一次性获取多个value
hsetnx设置一个不存在的key的值
hincrby为value做假发运算
hincrbyfloat为value加上浮点值

3、持久化机制

Redis官方提供了两种持久化机制:

  • 快照(Snopshot)
  • AOF(Append only file) 只追加日志文件

3.1 快照

1、特点

这种方式可以某一时刻的所有数据都写入磁盘中,当然这也是redis默认的持久化方式,保存的文件是以.rdb形式结尾的文件,因此,也被称之为RDB方式

在这里插入图片描述

2、快照的生成方式
  • 客户端方式:save和bgsave命令
  • 服务器配置自动触发

1、客户端方式之bgsave

​ 客户端可以使用bgsave来创建一个快照,当接收到用户bgsave命令时,redis会调用fork创建一个子进程,然后子进程负责进行持久化,而父进程则继续处理请求。

名词解释: fork当一个进程创建子进程的时候,底层的操作系统会创建该进程的一个副本,在类unix系统中创建子进程的操作会进行优化:在刚开始的时候,父子进程共享相同内存,直到父进程或子进程对内存进行了写之后,对被写入的内存的共享才会结束服务。

在这里插入图片描述
2、客户端方式之save

​ 客户端还可以使用save命令来创建一个快照,接收到命令的redis服务器在快照创建完毕之前不会再接收客户端的其他命令

在这里插入图片描述

3、服务器配置方式之自动配置

​ 如果用户在redis.conf配置文件中设置了save选项,redis会在满足save选项条件之后自动触发一次bgsave命令,如果设置多个save选项,当任意一个save选项条件满足,redis都会自动触发一次bgsave命令

在这里插入图片描述

4、服务器接收到客户端shutdown命令

​ 当redis通过shutdown指令接受到关闭服务器请求时,会执行一个save命令,阻塞所有客户端,不再接受客户端发送的任何指令,并且在save执行结束后关闭服务端

3、配置生成快照的名称和位置
# 1、配置修改快照的名称
dbfilename dump.rdb
# 2、配置生成位置
dir ./

3.2 AOF 只追加日志文件

1、特点

这种方式将客户端执行的写命令记录到日志文件中,AOF持久化会将被执行的写命令写到AOF的文件末尾,以此来记录数据发生的变化,因此只要redis从头到尾执行一次AOF文件中的所有写命令,就可以恢复AOF文件记录的数据集

在这里插入图片描述

2、开启AOF持久化

在redis的默认配置中没有开启AOF持久化,需要在配置文件中进行配置

 appendonly no	->	appendonly yes	#开启AOF持久化

appendfilename "appendonly.aof"		#指定配置文件名称和位置

在这里插入图片描述

3、日志生成频率

1、always【谨慎使用】

说明:每个redis写命令都要同步写入硬盘,严重降低redis速度

解释:如果用户使用了always选项,那么每个redis写命令都会被写入硬盘,从而将发生系统崩溃时出现的数据丢失减到最少;遗憾的是,因为这种同步策略需要对硬盘进行大量的写入操作,所以redis处理命令的速度会受到硬盘性能的限制

注意:转盘式硬盘在这种频率下208左右个命令/s ﹔固态硬盘(SSD)几百万个命令/s 

警告∶使用SSD用户请谨慎使用always选项,这种模式不断写入少量数据的做法有可能会引发严重的写入放大问题,导致将固态硬盘的寿命从原来的几年降低为几个月。

2、everysec【推荐】

说明:每秒执行一次同步显式的将多个写命令同步到磁盘

解释:为了兼顾数据安全和写入性能,用户可以考虑使用everysec选项,让redis每秒一次的频率对AOF文件进行同步; redis每秒同步一次AOF文件时性能和不使用任何持久化特性时的性能相差无几,而通过每秒同步一次AOF文件, redis可以保证,即使系统崩溃,用户最多丢失—秒之内产生的数据。

3、no【不推荐】

说明:由操作系统决定何时同步

解释:最后使用no选项,将完全有操作系统决定什么时候同步AOF日志文件,这个选项不会对redis性能带来影响但是系统崩溃时,会丢失不定数量的数据,另外如果用户硬盘处理写入操作不够快的话,当缓冲区被等待写入硬盘数据填满时, redis会处于阻塞状态,并导致redis的处理命令请求的速度变慢。

在这里插入图片描述

3.3 AOF文件的重写

1、AOF存在的问题

​ AOF同时也带了一些问题,持久化文件会越来越大。例如我们调用incr test命令100次,文件必须保存全部的100次命令,,其实有99条都是多余的。因为要恢复数据库的状态其实只需要一条set test 100就够了,为了压缩AOF持久化文件,redis为我们提供了AOF重写机制。

2、AOF重写

用来一定程度上减小AOF文件大小

3、触发重写的方式

a.客户端方式触发重写

执行bgrewritreaof

b.服务器配置方式自动触发

配置redis.conf中的auto-aof-rewrite-percentage选项

如果设置auto-aof-rewrite-percentage 100auto-aof-rewrite-min-size 64mb,并启用AOF持久化时,那么当AOF文件大于64mb时,并且AOF文件的体积大了至少一倍(100%)时,会自动触发,如果触发过于频繁,用户可以考虑将auto-aof-rewrite-percentage设置为更大。

如:64mb --> 20mb --> 40mb --> 18mb --> 36mb --> 26mb --> 52mb

auto-aof-rewrite-min-size为重写阈值,而auto-aof-rewrite-percentage 100控制重写频率。

在这里插入图片描述

3.4 重写原理

重写AOF文件的操作,并没有读取旧的AOF文件,而是将整个内存中的数据用命令的方式重写了一个新的AOF文件,替换里原有的文件,这点类似快照。

重写流程:
1. redis调用fork ,现在有父子两个进程子进程根据内存中的数据库快照,往临时文件中写入重建数据库状态的命令
2、父进程继续处理client请求,除了把写命令写入到原来的aof文件中。同时把收到的写命令缓存起来。这样就能保证如果子进程重写失败的话并不会出问题。
3.当子进程把快照内容写入已命令方式写到临时文件中后,子进程发信号通知父进程。然后父进程把缓存的写命令也写入到临时文件。
4.现在父进程可以使用临时文件替换老的aof文件,并重命名,后面收到的写命令也开始往新的aof文件中追加。

在这里插入图片描述

3.5 持久化总结

  • 两种持久化方案,既可以同时使用(aof),也可以单独使用,在某些情况下也可以都不使用,具体使用哪种方案取决于用户的数据和应用决定。

  • 无论使用AOF还是快照机制进行持久化,将内存中的数据写入到磁盘都是有必要的,处理持久化外,用户还应该对持久化的文件进行备份(最好备份在多个不同地方)。

4、Java操作Redis

1、 引入依赖
<!-- 引入jedis依赖 -->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.1.1</version>
</dependency>
2、创建Jedis对象
//1、创建Jedis客户端对象
Jedis jedis = new Jedis("192.168.75.100", 7000);//1、关闭防火墙 2、开启远程连接
//2、选择一个库,默认使用0号库
jedis.select(0);

Set<String> keys = jedis.keys("*");
keys.forEach(System.out::println);

//清空所有库
jedis.flushAll();
//清空当前库
jedis.flushDB();

//3、释放资源
jedis.close();
3、操作key
private Jedis jedis;

@Before
public void before(){
    this.jedis = new Jedis("192.168.75.100", 7000);
}

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

@Test
public void testKeys(){
    //删除一个
    jedis.del("name");
    //删除多个
    //jedis.del("name","age");

    //判断key是否存在
    boolean name = jedis.exists("name");
    System.out.println(name);

    //设置一个超时时间
    //long age = jedis.expire("age", 100);
    //System.out.println("age = " + age);
    //查看key超时时间
    //long ttl = jedis.ttl("age");
    long ttl = jedis.ttl("age");
    System.out.println("ttl = " + ttl);

    //随机获取一个key
    String s = jedis.randomKey();
    System.out.println("s = " + s);

    //修改名称
    jedis.rename("age","newage");

}
4、操作String
//set
jedis.set("name","zhangsan");
//get
jedis.get("name");

//mset
jedis.mset("aa","张三","bb","李四");
//mget
List<String> mget = jedis.mget("aa", "name", "bb");
mget.forEach(v -> System.out.println("v = " + v));

//getset
String set = jedis.getSet("name", "小明");
System.out.println("set = " + set);

//.......
5、操作List
//lpush
jedis.lpush("lists","aaa");
//lrage
List<String> lists = jedis.lrange("lists", 0, -1);
lists.forEach(v-> System.out.println("v = " + v));
//lpop
jedis.lpop("lists");

jedis.linsert("lists", ListPosition.BEFORE,"aaa","ccc");

//.......

5、SpringBoot操作Redis

​ Spring Boot Dath Redis 中提供了RedisTemplate和StringRedisTemplate,其中StringRedisTemplate是RedisTemplate的子类,两个方法基本一致,不同之处主要体现在操作的数据类型不同,RedisTemplate中的两个泛型都是object,意味着存储的key和value都可以是一个对象,而StringRedisTemplate的两个泛型都是String,意味着stringRedisTemplate的key和value都只能是字符串。

​ 注意:使用RedisTemplate默认是将对象序列化到Redis中,所以放入的对象都要实现对象序列化接口

5.1 环境准备

1、引入依赖
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、配置文件
#redis
spring.redis.host=192.168.75.100	#要连接的ip地址
spring.redis.port=7000				#端口
spring.redis.database=0				#要使用哪个库

5.2 使用StringRedisTemplate

//启动springboot应用
//指向主启动类
@SpringBootTest(classes = RedisDemo02Application.class)
@RunWith(SpringRunner.class)
public class TestRedisTemplate {
    //注入StringRedisTemplate
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    //操作key相关'
    @Test
    public void testKey(){
        //删除key
        //stringRedisTemplate.delete("name");
        //判断key是否存在
        Boolean hasKey = stringRedisTemplate.hasKey("name");
        System.out.println("hasKey = " + hasKey);
        //判断一个key对应类型
        DataType type = stringRedisTemplate.type("name");
        System.out.println("type = " + type);
        //获取所有key
        Set<String> keys = stringRedisTemplate.keys("*");
        keys.forEach(v-> System.out.println("v = " + v));
        //获取key超时时间 -1 永不超时 -2 key不存在 >=0 超时时间
        Long expire = stringRedisTemplate.getExpire("name");
        System.out.println("expire = " + expire);
        //修改key名称
        //如果不存在会报错
		//stringRedisTemplate.rename("name","newName");
        stringRedisTemplate.move("newName",3);
    }

    //操作redis中的字符串
    @Test
    public void testString(){
        //opsForValue 操作的就是String类型
        stringRedisTemplate.opsForValue().set("name","张三");
        String name = stringRedisTemplate.opsForValue().get("name");
        System.out.println("name = " + name);
        //设置key超时时间 60s
        stringRedisTemplate.opsForValue().set("code","3478",60, TimeUnit.SECONDS);
        //追加
        stringRedisTemplate.opsForValue().append("name","是一个好人");
    }

    //操作list类型
    @Test
    public void testList(){
        //创建一个列表,插入一个元素
        stringRedisTemplate.opsForList().leftPush("lists","aaa");
        //创建一个列表,插入多个元素
        stringRedisTemplate.opsForList().leftPushAll("lists","bbb","ccc","ddd");
        ArrayList<String> list= new ArrayList<>();
        list.add("小张");
        list.add("小明");
        //创建一个列表,插入list中的值
        stringRedisTemplate.opsForList().leftPushAll("lists",list);
        //查看列表
        List<String> lists = stringRedisTemplate.opsForList().range("lists", 0, -1);
        assert lists != null;
        lists.forEach(v-> System.out.println("v = " + v));
    }

    //操作set
    @Test
    public void testSets(){
        stringRedisTemplate.opsForSet().add("sets","zhangsan","张三","xiaoming");
        Set<String> sets = stringRedisTemplate.opsForSet().members("sets");
        assert sets != null;
        sets.forEach(v -> System.out.println("v = " + v));
        Long sets1 = stringRedisTemplate.opsForSet().size("sets");
        System.out.println("sets1 = " + sets1);
    }

    //操作zset
    @Test
    public void testZset(){
        //创建并放入元素
        stringRedisTemplate.opsForZSet().add("zsets","xiaoming",10);

        Set<String> zsets = stringRedisTemplate.opsForZSet().range("zsets", 0, -1);
        zsets.forEach(v-> System.out.println("v = " + v));

        Set<ZSetOperations.TypedTuple<String>> zsets1 = stringRedisTemplate.opsForZSet().rangeByScoreWithScores("zsets", 0, 100);
        zsets1.forEach(v-> System.out.println("v = " + v));
    }

    //操作Hash类型
    @Test
    public void testHash(){
        stringRedisTemplate.opsForHash().put("hashs","name","zhangsan");
        String o = (String) stringRedisTemplate.opsForHash().get("hashs", "name");
        List<Object> hashs = stringRedisTemplate.opsForHash().values("hashs");
        hashs.forEach(System.out::println);

        HashMap<String, String> hashMap = new HashMap<>();
        hashMap.put("age","19");
        hashMap.put("gender","man");
        //插入多个值
        stringRedisTemplate.opsForHash().putAll("hashs",hashMap);
        //获取多个值
        List<Object> values = stringRedisTemplate.opsForHash().multiGet("hashs", Arrays.asList("name", "age"));
        System.out.println("values = " + values);
    }
}

5.3 使用RedisTemplate

创建一个User类

要实现序列化接口

@Data
//可是使用链式的set
@Accessors(chain = true)
public class User implements Serializable {
    private String id;
    private String name;
    private Integer age;
    private Date bir;
}

直接使用RedisTemplate创建一个User对象存储到Redis中

@Test
public void testRedisTemplate(){
    User value = new User();
    value.setId(UUID.randomUUID().toString()).setName("张三").setAge(21).setBir(new Date());
    redisTemplate.opsForValue().set("user", value);
}

在这里插入图片描述

在这里插入图片描述

使用RedisTemplate操作

在这里插入图片描述

可以发现,直接使用RedisTemplate,key-value都会使用到jdk的序列化,经过序列化后的结果在终端上难以查看

RedisTemplate对象中key-value都是使用的JdkSerializationRedisSerializer,因为这种序列化方式,key-value都支持Object

在这里插入图片描述

因此,常常我们在使用之前都会修改key的序列化方式

redisTemplate.setKeySerializer(new StringRedisSerializer());

User value = new User();
value.setId(UUID.randomUUID().toString()).setName("张三").setAge(21).setBir(new Date());
redisTemplate.opsForValue().set("user", value);

User user = (User) redisTemplate.opsForValue().get("user");
System.out.println("user = " + user);

在这里插入图片描述

但是,又会遇到下一个问题:那就是hash类型下,有两个key,那么,刚刚才设置了一个key的序列化,另一个要怎么办呢?

redisTemplate.setHashKeySerializer(new StringRedisSerializer());
//设置key的序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());

User value = new User();
value.setId(UUID.randomUUID().toString()).setName("张三").setAge(21).setBir(new Date());
redisTemplate.opsForValue().set("user", value);
List<String> list = new ArrayList<>();
list.add("aaa");
list.add("李四");
redisTemplate.opsForList().leftPushAll("lists",list);
redisTemplate.opsForSet().add("sets",user);
redisTemplate.opsForZSet().add("zsets",user,10);
redisTemplate.opsForHash().put("hashs","user",user);

5.4 Bound API

spring data 为了方便我们对于redis更友好的操作,因此有了bound api

当要对某个key多次操作,那么stringRedisTemplate.opsForValue().这段代码要重复多次,因此Bound API对这种情况也有了更友好的操作

在这里插入图片描述

使用bound api

stringRedisTemplate.opsForValue().set("name","zhangsan");
stringRedisTemplate.opsForValue().append("name","是一个好孩子");
stringRedisTemplate.opsForValue().get("name");

上下代码效果等同

BoundValueOperations<String, String> nameOperations = stringRedisTemplate.boundValueOps("name");
nameOperations.set("lisi");
nameOperations.append("真好看");
nameOperations.get();

5.5 总结

1、针对key-value都是String的情况,使用StringRedisTemplate。

2、针对处理key-value中有对象的情况,使用RedisTemplate,在使用时记得序列化。

3、针对一个key有多次操作的情况,使用boundXxxOps()的api可以简化书写。

6、Redis应用场景

1、利用redis中字符串类型的超时功能,可以用来存储手机验证码。

2、利用redis中字符串类型完成具有时效性的业务功能(如:淘宝/12306订单超时)

3、完成分布式集群系统中的session共享

4、利用zset类型(可排序、分页),实现排行榜

5、分布式缓存

6、存储认证之后的token认证

7、解决分布式集群中分布式锁问题(lua脚本)

7、分布式缓存

7.1 Redis分布式缓存(一)

1、什么是缓存

​ 定义:就是计算机内存中一段数据

2、内存中数据的特点

​ a.读写快

​ b.断电立即丢失

3、缓存解决了什么问题

​ a.提高网站的吞吐量,提高网站运行效率

​ b.一定程度上减轻原始数据库的访问压力

4、既然缓存可以提高效率,那么项目中所有数据都加入到缓存中岂不是更好?

​ 使用缓存时,一定是数据库中数据极少发生修改,更多用于查询信息这种情况

5、什么是本地缓存?什么是分布式缓存?

​ 本地缓存:存在应用服务器内存中的数据称之为本地缓存(local cache)

​ 分布式缓存:存储在当前应用服务器内存之外的数据称之为分布式缓存(distribute cache)

​ 集群:将同一个服务的多个节点,放在一起,对系统提供服务的过程被称之为集群

​ 分布式:由多个不同的服务集群共同对系统提供服务,这个系统称之为分布式系统(distribute system)

6、利用mybatis自身的本地缓存结合redis实现分布式缓存

​ a.mybatis中应用缓存(二级缓存) SqlSessionFaction级别缓存,所有会话共享

​ b.如何开启(二级缓存)

​ mapper.xml中加入<catch/>标签 --> 本地缓存

​ c.查看cache标签缓存实现,默认使用PerpetualCache

在这里插入图片描述

在这里插入图片描述

​ 分析源码可知,我们只需要重写put/get方法即可

在这里插入图片描述

​ 结论:

​ 1、Mybatis默认使用org.apache.ibatis.cache.impl.PerpetualCache

​ 2、默认缓存使用的是HashMap

​ d.自定义RedisCache实现

​ 1、通过源码可以看到,可以自定义Cache实现类,并对里面的方法进行重写

​ 2、使用RedisCache

自定义RedisCache实现

//自定义cache实现
public class RedisCache implements Cache {
    ///当前放入缓存的mapper的namespace
    private final String id;

    //必须存在有id的构造参数
    public RedisCache(String id) {
        this.id = id;
    }

    //返回cache的唯一标识
    @Override
    public String getId() {
        return this.id;
    }

    //缓存中放入值
    @Override
    public void putObject(Object key, Object value) {
        //通过工具类获取RedisTemplate
        RedisTemplate redisTemplate = getRedisTemplate();

        //使用redis中的hash作为缓存存储模型
        redisTemplate.opsForHash().put(id.toString(), key.toString(), value);

    }

    //获取缓存中的值
    @Override
    public Object getObject(Object key) {
        //通过工具类获取RedisTemplate
        RedisTemplate redisTemplate = getRedisTemplate();

        //根据key从redis中获取数据
        return redisTemplate.opsForHash().get(id.toString(), key.toString());
    }

    //删除缓存中的值
    //注意:这个方法为mybatis的保留方法,后续版本可能实现
    @Override
    public Object removeObject(Object key) {
        return null;
    }

    //增删改都需要执行这个方法,清空缓存
    @Override
    public void clear() {
        //通过工具类获取RedisTemplate
        RedisTemplate redisTemplate = getRedisTemplate();

        //清空缓存
        redisTemplate.delete(id.toString());
    }

    //用来计算缓存数量
    @Override
    public int getSize() {
        //通过工具类获取RedisTemplate
        RedisTemplate redisTemplate = getRedisTemplate();
        //获取hash中key数量
        return redisTemplate.opsForHash().size(id.toString()).intValue();
    }

    //自定义获取热第三Template
    private RedisTemplate getRedisTemplate() {
        //通过工具类获取RedisTemplate
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return null;
    }
}

测试:

@SpringBootTest(classes = RedisDemo02Application.class)
@RunWith(SpringRunner.class)
public class TestUserService {
    @Autowired
    private UserService userService;

    @Test
    public void testUser(){
        List<User> all = userService.findAll();
        all.forEach(System.out::println);
        userService.findAll();
    }

    @Test
    public void testFindById(){
        System.out.println(userService.finaById("1"));
        System.out.println(userService.finaById("1"));
    }

    @Test
    public void testDeleteById(){
        userService.deleteById("1");
    }

    @Test
    public void testInsert(){
        User user = new User();
        user.setName("xxc").setAge(21).setBir(new Date());
        userService.save(user);
    }

    @Test
    public void testUpdate(){
        User user = new User();
        user.setName("xxc").setAge(24).setBir(new Date()).setId("011e6608-1de1-4388-b1ce-6e50c9375cdb");
        userService.update(user);
    }

}

根据测试可以发现,当执行增删改都会执行清空缓存的方法

7.2 Redis分布式缓存(二)

此时,其实还存在一个问题,那么就是:

1、清空缓存时,只能清除自己这个namespace(redis中,以namespace为key)的键值对,会造成以下问题:

a.如果项目中表查询之间没有任何关联查询,那么现在使用的这种缓存方式没有问题

b.现有缓存在有表连接查询过程中一定会有问题

2、如何解决这个问题

:用来将多个具有关联关系查询的缓存放在一起进行处理

例如:

<!--关联关系缓存处理-->
<cache-ref namespace="com.xxc.redisdemo02.dao.UserDao"/>

在这里插入图片描述

7.3 Redis分布式缓存(三)

1、缓存优化策略

对放入redis的可以key优化:key的长度不能太长:

-1781060725:4117791490:com.xxc.redisdemo02.dao.UserDao.findAll:0:2147483647:select * from t_user:SqlSessionFactoryBean

尽可能将key设计的更简短?

算法:MD5处理(加密)

​ 特点:1、一切文件字符串经过MD5处理之后,都会生成32位16进制的字符串

​ 2、不同内容的文件经过MD5进行加密,加密结果一定不一致(问:a.txt和b.txt怎么判断内容是否相同。答:MD5加密,如果一致,那么内容 相同)

​ 3、相同内容经过MD5生成结果始终一致

推荐:在redis整合MyBatis过程中,进行MD5加密

//自定义cache实现
public class RedisCache implements Cache {
    ///当前放入缓存的mapper的namespace
    private final String id;

    //必须存在有id的构造参数
    public RedisCache(String id) {
        this.id = id;
    }

    //返回cache的唯一标识
    @Override
    public String getId() {
        return this.id;
    }

    //缓存中放入值
    @Override
    public void putObject(Object key, Object value) {

        //通过工具类获取RedisTemplate
        RedisTemplate redisTemplate = getRedisTemplate();

        //使用redis中的hash作为缓存存储模型
        redisTemplate.opsForHash().put(id.toString(), getKeyToMd5(key.toString()), value);

    }

    //获取缓存中的值
    @Override
    public Object getObject(Object key) {
        //通过工具类获取RedisTemplate
        RedisTemplate redisTemplate = getRedisTemplate();

        //根据key从redis中获取数据
        return redisTemplate.opsForHash().get(getKeyToMd5(key.toString()), key.toString());
    }

    //删除缓存中的值
    //注意:这个方法为mybatis的保留方法,后续版本可能实现
    @Override
    public Object removeObject(Object key) {
        return null;
    }

    //增删改都需要执行这个方法,清空缓存
    @Override
    public void clear() {
        //通过工具类获取RedisTemplate
        RedisTemplate redisTemplate = getRedisTemplate();

        //清空缓存
        redisTemplate.delete(id.toString());
    }

    //用来计算缓存数量
    @Override
    public int getSize() {
        //通过工具类获取RedisTemplate
        RedisTemplate redisTemplate = getRedisTemplate();
        //获取hash中key数量
        return redisTemplate.opsForHash().size(id.toString()).intValue();
    }

    //自定义获取热第三Template
    private RedisTemplate getRedisTemplate() {
        //通过工具类获取RedisTemplate
        RedisTemplate redisTemplate = (RedisTemplate) ApplicationContextUtils.getBean("redisTemplate");
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return null;
    }

    private String getKeyToMd5(String keyString){
        return DigestUtils.md5DigestAsHex(keyString.getBytes());
    }
}

2、面试相关概念

1)什么是缓存穿透?

​ 定义:客户端查询了一个数据库中没有的数据记录,导致缓存在这种情况无法利用,称之为缓存穿透

MyBatis中的cache是解决了缓存穿透问题的,将数据库中没有查询到的结果也进行缓存(null)

在这里插入图片描述

2)什么是缓存雪崩?

​ 定义:在系统运行的某一时刻,突然系统中缓存全部失效,恰好在这一时刻涌来了大量的用户请求,导致所有模块化缓存无法运用,导致极端情况,数据库阻塞或挂起

​ 缓存存储时:业务系统非常大、模块多、业务数据不同,不同模块在放入缓存时,都会设置一个缓存的超时时间

​ 解决方案:1、缓存永久存储(不推荐) 2、针对不同的业务数据一定要设置不同的超时时间

3)什么是缓存击穿?

​ 定义:如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。

​ 解决方案:1、不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间

​ 2、互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者 默认值。

4)有没有遇到过?如何解决?

8、Redis主从复制

主从复制架构仅仅用来解决数据的冗余备份,从节点仅仅用来同步数据

无法解决:master节点出现故障转移

主从复制架构图

在这里插入图片描述

搭建主从复制

# 1.准备三台机器并修改配置
- master:
	port 6379
	bind 0.0.0.0
	
- slave1:
	port 6380
	bind 0.0.0.0
	slaveof masterip masterport
	
- slave2:
	port 6381
	bind 0.0.0.0
	slaveof masterip masterport
	
# 启动3台机器进行测试
cd /usr/redis/bin
	./redis-server /root/master/redis.conf
	./redis-server /root/slave1/redis.conf
	./redis-server /root/slave2/redis.conf

slave节点只可读数据,不能增删改数据

当主节点挂掉之后,slave节点也会持续给主节点发送心跳包,主节点再次启动了之后,slave节点会再次连接上主节点

只解决了数据的冗余备份,如果主节点宕机了还是存在着问题,因此就存在了哨兵机制

9、Redis中的哨兵机制

9.1 哨兵机制

Sentinel(哨兵)是Redis的高可用性解决方案∶由一个或多个Sentinel实例组成的Sentinel系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器。简单的说哨兵就是带有自动故障转移功能的主从架构。

无法解决:1、单节点并发压力 2、单节点内存和物理磁盘上限

9.2 架构原理

在这里插入图片描述

9.3 哨兵架构搭建

1、 在主节点上创建哨兵机制
	在Master对应的redis.conf同目录下创建sentinel.conf,名字绝对不能错
	
2、 配置哨兵,在sentinel.conf文件中填入以下内容
	sentinel monitor 被监控数据库名字(自己起名字) ip port 1(哨兵数量)
	超过半数以上哨兵检测到master宕机了,才会执行主从切换的功能 
	
3、 启动哨兵模式进行测试
	redis-sentinel /root/sentinel/sentinel.conf
	
哨兵默认端口 26379

9.4 使用SpringBoot中操作哨兵

#redis单节点
#spring.redis.host=192.168.75.100
#spring.redis.port=7000
#spring.redis.database=0

#Redis sentinel配置
#master写得是哨兵监听的那个名称(对应配置文件中配置的名称)
spring.redis.sentinel.master=mymaster
#连接的不再是一个具体的主机,而是多个哨兵节点
spring.redis.sentinel.nodes=192.168.75.100:26379

注意,如果连接过程中出现错误,可能是没有开启远程连接权限

因此在sentinel配置文件中添加bind=0.0.0.0即可

10、Redis集群

10.1 集群

Redis在3.0后开始支持Cluster(模式)模式,目前redis的集群支持节点的自动发现,支持slave-master选举和容错,支持在线分片(sharding shard )等特性。

10.2 集群架构图

PING --> PONG机制

在这里插入图片描述

10.3 集群细节

1、所有Redis节点之间彼此互联(PING-PING机制),内部使用二进制协议优化传输速度和带宽

2、节点的fail是通过集群中超过半数的节点检测失效才生效

3、客户端与redis直连,不需要中间proxy层,客户端不需要连接集群所有节点,连接集群中任意一个可用节点即可

4、redis-cluster把所有的物理节点映射到[0-16383]slot上,cluster负责node<->slot<->value

在这里插入图片描述

10.4 集群搭建

判断一个是集群中的节点是否可用,是集群中的所用主节点选举过程,如果半数以上的节点认为当前节点挂掉,那么当前节点就是挂掉了,所以搭建redis集群时建议节点数最好为奇数,搭建集群至少需要三个主节点,三个从节点,至少需要6个节点。

1、准备环境安装ruby以及redis集群依赖
	yum install -y ruby rubygems
	gem install Redis
2、在机器上创建7个目录
	mkdir 7000 7001 7002 7003 7004 7005 7006
3、复制配置文件到每个目录
	[root@centos100 ~]# cp redis-6.2.6/redis.conf 7000
	[root@centos100 ~]# cp redis-6.2.6/redis.conf 7001
	[root@centos100 ~]# cp redis-6.2.6/redis.conf 7002
	[root@centos100 ~]# cp redis-6.2.6/redis.conf 7003
	[root@centos100 ~]# cp redis-6.2.6/redis.conf 7004
	[root@centos100 ~]# cp redis-6.2.6/redis.conf 7005
	[root@centos100 ~]# cp redis-6.2.6/redis.conf 7006
4、修改配置文件
port 7000								//修改端口
bind 0.0.0.0							//开启远程连接
cluster-enabled yes						//开启集群模式
cluster-config-file nodes-port.conf		//集群节点配置文件
cluster-node-timeout 5000				//集群节点超时时间
appendonly yes							//开启aof持久化
5、指定不同目录下的文件启动七个节点
[root@centos100 bin]# ./redis-server /root/7000/redis.conf
[root@centos100 bin]# ./redis-server /root/7001/redis.conf
[root@centos100 bin]# ./redis-server /root/7002/redis.conf
[root@centos100 bin]# ./redis-server /root/7003/redis.conf
[root@centos100 bin]# ./redis-server /root/7004/redis.conf
[root@centos100 bin]# ./redis-server /root/7005/redis.conf
[root@centos100 bin]# ./redis-server /root/7006/redis.conf
6、查看进程

在这里插入图片描述

1、创建集群
1、复制集群操作脚本到bin目录下
[root@centos100 src]# cp ./redis-trib.rb /usr/redis/bin

2、创建集群
./redis-trib.rb --replicas 1 192.168.75.100:7000 192.168.75.100:7001 192.168.75.100:7002 192.168.75.100:7003 192.168.75.100:7004 192.168.75.100:7005 192.168.75.100:7006

我这边使用上面这条命令报错了:

在这里插入图片描述

根据提示修改了命令:

./redis-cli --cluster create 192.168.75.100:7000 192.168.75.100:7001 192.168.75.100:7002 192.168.75.100:7003 192.168.75.100:7004 192.168.75.100:7005 192.168.75.100:7006 --cluster-replicas 1

如此就成功了

输入命令后,在这里停止了,输入yes回车后,创建好了集群;

在这里插入图片描述

2、查看集群状态
# 查看集群状态 check [原始集群中任意一个节点] 
./redis-cli --cluster check 192.168.75.100:7000

# 集群节点状态说明
主节点:
	主节点存在hash slots,且主节点的hash slots没有交叉
	主节点不能删除
	一个主节点可以有多个从节点
	主节点宕机时,多个副本之间自动选举主节点
从节点:
	从节点没有hash slots
	从节点可以删除
	从节点不负责数据的写,只负责数据的同步
3、添加主节点
添加主节点 add-node --slave [新加入节点] [集群中任意节点]
./redis-cli --cluster add-node 192.168.75.100:7006 192.168.75.100:7000

注意:
 	1、该节点必须以集群模式启动
 	2、默认情况下该节点是以master节点形式添加
4、添加从节点
1、添加从节点 add-node --slave [新加入加点] [集群中任意节点]
./redis-cli --cluster add-node --slave 192.168.75.100:7007 192.168.75.100:7000

注意:
	当添加副本节点时没有指定主节点,redis会随机给副本节点较少的主节点添加当前副本节点

2、为确定的master添加主节点 add-node --slave --master-id master节点id [新加入加点] [集群中任意节点]
./redis-cli --cluster add-node --slave --master-id 41a5da77b40241896c2234a1ff23cf1d679a1e33 192.168.75.100:7007 192.168.75.100:7000
这条命令我试过,没成功,花了些时间找到了以下解法:
使用1提供的方法将主从节点都添加进集群中,然后使用下面这个设置主从关系来解决

设置主从关系
./redis-cli -p 7007 cluster replicate 41a5da77b40241896c2234a1ff23cf1d679a1e33
解析:
7007:从节点ip
41a5da77b40241896c2234a1ff23cf1d679a1e33:主节点

5、删除副本节点
1、删除节点 del-node [集群中任意节点] [删除节点id]
./redis-cli --cluster del-node 192.168.75.100:7000 41a5da77b40241896c2234a1ff23cf1d679a1e33

注意:
	被删除的节点一定是从节点或是hash slots没有被分配到的节点
6、集群在分片
1、再分片 reshard [集群中任意节点]
./redis-cli --cluster reshard 192.168.75.100:7000

PS:学习过程中遇到的坑

1、使用win直接连接redis时,连接不成功

1、注释掉配置文件redis.conf中的

默认情况下redis拒绝所有的客户端远程连接,因此需要修改配置

bind 127.0.0.1			#该配置会导致,其他ip登录被拒绝。
改为:
bind 0.0.0.0			#允许一切用户端连接	

2、修改配置文件redis.conf中的

此配置会使redis目前处于受保护模式,不允许非本地客户端链接

protected-mode no

修改为

protected-mode yes

3、关闭虚拟机的防火墙

bash sudo systemctl stop firewalld.service 

2、改了以上配置,任然链接不了,出现以下错误

DENIED Redis is running in protected mode because protected mode is enabled, no bind address was specified, no authentication password is requested to clients. In this mode connections are only accepted from the loopback interface. If you want to connect from external computers to Redis you may adopt one of the following solutions: 1) Just disable protected mode sending the command 'CONFIG SET protected-mode no' from the loopback interface by connecting to Redis from the same host the server is running, however MAKE SURE Redis is not publicly accessible from internet if you do so. Use CONFIG REWRITE to make this change permanent. 2) Alternatively you can just disable the protected mode by editing the Redis configuration file, and setting the protected mode option to 'no', and then restarting the server. 3) If you started the server manually just for testing, restart it with the '--protected-mode no' option. 4) Setup a bind address or an authentication password. NOTE: You only need to do one of the above things in order for the server to start accepting connections from the outside.

使用谷歌翻译

拒绝 Redis 在保护模式下运行,因为启用了保护模式,没有指定绑定地址,也没有向客户端请求身份验证密码。在此模式下,仅接受来自环回接口的连接。如果您想从外部计算机连接到 Redis,您可以采用以下解决方案之一: 
1) 只需禁用保护模式,通过从服务器的同一主机连接到 Redis 从环回接口发送命令 'CONFIG SET protected-mode no'正在运行,但是如果这样做,请确保 Redis 不能从 Internet 公开访问。使用 CONFIG REWRITE 使此更改永久生效。 
2) 或者,您可以通过编辑 Redis 配置文件并将保护模式选项设置为“否”来禁用保护模式,然后重新启动服务器。 
3) 如果您手动启动服务器只是为了测试,请使用“--protected-mode no”选项重新启动它。 
4) 设置绑定地址或认证密码。注意:您只需执行上述操作之一,服务器即可开始接受来自外部的连接。

按照翻译过来的方式修改即可

3、XShell免费版一次性只能开4的连接,继续开会重新启动一个新窗口,这里推荐FinalShell

这边使用了FinallShell 离线激活步骤,适用MAC/WIN提供的方法

4、ruby安装问题

使用yum install -y ruby rubygems安装ruby,使用ruby -v查看ruby版本,可以看到是2.0.0,因此在使用gem install -l ./redis-4.7.1.gem时,会报错,提示ruby版本大于2.4,因此,还要做以下操作

1、安装基本工具

yum install ruby

2、查看当前版本

ruby -v

效果:
ruby 2.0.0p648 (2015-12-16) [x86_64-linux]
3、安装yum源

yum install centos-release-scl-rh

4、安装指定版本的ruby

yum install rh-ruby24 -y

5、加载环境变量

scl enable rh-ruby24 bash #加载环境变量

6、查看当前ruby版本

ruby -v

效果:
ruby 2.4.6p354 (2019-04-01 revision 67394) [x86_64-linux]
7、这里你会看到ruby版本已经是高于2.0.0以上的版本了,但是这里有个坑,当你重启虚拟机的时候,你再执行ruby -v会发现莫名其妙的又变成了2.0.0.
莫慌,执行以下命令:

vim /etc/profile.d/rh-ruby23.sh

把下面内容粘贴进去

#!/bin/bash
source /opt/rh/rh-ruby24/enable
export X_SCLS="`scl enable rh-ruby24 'echo $X_SCLS'`"
export PATH=$PATH:/opt/rh/rh-ruby24/root/usr/local/bin

写完别忘了 :wq 保存
8、最后

scl  enable  rh-ruby23 bash  #加载环境变量
ruby -v

5、rubygems安装

gem下载地址,下载好后上传到centos上,之后使用gem install -l ./redis-4.7.1.gem安装即可

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值