Redis学习笔记

Reids笔记

文章目录

一、NoSQL简述:

1.1、什么是nosql

​ NoSql,叫非关系型数据库,它的全名Not only sql。为了解决高并发、高可用、高可扩展,大数据存储等一系列问题而产生的数据库解决方案,就是NoSql。它不能替代关系型数据库,只能作为关系型数据库的一个良好补充。

1.2、NOSQL和关系型数据库的比较

优点:
1)成本:nosql数据库简单易部署,基本都是开源软件,不需要像使用oracle那样花费大量成本购买使用,相比关系型数据库价格便宜。
2)查询速度:nosql数据库将数据存储于缓存之中,关系型数据库将数据存储在硬盘中,自然查询速度远不及nosql数据库。
3)存储数据的格式:nosql的存储格式是key,value形式、文档形式、图片形式等等,所以可以存储基础类型以及对象或者是集合等各种格式,而数据库则只支持基础类型。
4)扩展性:关系型数据库有类似join这样的多表查询机制的限制导致扩展很艰难。
缺点:
1)维护的工具和资料有限,因为nosql是属于新的技术,不能和关系型数据库10几年的技术同日而语。
2)不提供对sql的支持,如果不支持sql这样的工业标准,将产生一定用户的学习和使用成本。
3)不提供关系型数据库对事务的处理。

1.3、场景应用:
1.3.1、适用场景:
  • 海量数据的读写

  • 对数据高并发的读写

  • 对数据高可拓展性

1.3.2、不适合场景:
  • 需要事务支持

二、Redis相关简介

2.1、什么是Redis

Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库,官方提供测试数据,50个并发执行100000个请求,读的速度是110000次/s,写的速度是81000次/s ,且Redis通过提供多种键值数据类型来适应不同场景下的存储需求,目前为止Redis支持的键值数据类型如下:

​ 1) 字符串类型 string

​ 2) 哈希类型 hash

​ 3) 列表类型 list

​ 4) 集合类型 set

​ 5) 有序集合类型 sortedset

2.2、Redis的应用场景

1).缓存(数据查询、短连接、新闻内容、商品内容等等)

2).聊天室的在线好友列表

3).任务队列。(秒杀、抢购、12306等等)

4).应用排行榜

5).网站访问统计

6).数据过期处理(可以精确到毫秒)

7).分布式集群架构中的session分离

2.3、如何理解Redis是单线程+多路IO复用

​ 做某件事情需要一定的时间,可以监视这件事,我们可以做其他的事。多路IO复用有select,poll,epoll这些模式。select监测数量能力有限。poll监测数量没有限制,但是需要一个一个核查。配epoll监测数量没有限制,也不需要一个一个核查,直接看是否有一个正确的标识。

2.4、常用五大数据类型
常见操作:

Redis键(key)

1.keys * 查看当前库所有key 

2.exists key 判断某个key是否存在

3.type key 查看你的key是什么类型

4.del key 删除指定的key数据

5.expire key time 为给定的key设置time秒的过期时间

6.ttl key 查看还有多少秒过去, -1标识永不过期 -2表示已过期

7.select 命令切换数据库

8.dbsize 查看当前数据库的key的数量

9.flushdb 清空当前库 (慎用)
2.4.1、Redis字符串(String)

​ String是Redis最基本的类型,key-value

​ String类型是二进制安全的。意味着Redis的String可以包含任何数据,如果图片或者序列化的对象。一个Redis中字符串value最多可以是512M。

常见操作:

1.set   <key><value>添加键值对

2.get   <key>查询对应键值

3.append  <key><value>将给定的<value> 追加到原值的末尾

4.strlen  <key>获得值的长度

5.setnx  <key><value>只有在 key 不存在时    设置 key 的值

6.incr  <key>
将 key 中储存的数字值增1
只能对数字值操作,如果为空,新增值为1

7.decr  <key>
将 key 中储存的数字值减1
只能对数字值操作,如果为空,新增值为-1

8.incrby / decrby  <key><步长>将 key 中储存的数字值增减。自定义步长。

9.mset  <key1><value1><key2><value2>  ..... 
同时设置一个或多个 key-value对  

10.mget  <key1><key2><key3> .....
同时获取一个或多个 value  

11.msetnx <key1><value1><key2><value2>  ..... 
同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在。 原子性,有一个失败则都失败

12.setex  <key><过期时间><value>
设置键值的同时,设置过期时间,单位秒。



​ 数据结构:

​ String的数据结构为简单动态字符串(Simple Dynamic String,缩写SDS)。是可以修改的字符串,内部结构实现上类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配 在这里插入图片描述

​ 如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M。

2.4.2、Redis列表(List)

​ 单键多值

​ Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)。

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

在这里插入图片描述

常见操作:

1.lpush/rpush  <key><value1><value2><value3> .... 从左边/右边插入一个或多个值。

2.lpop/rpop  <key>从左边/右边吐出一个值。值在键在,值光键亡。

3.lrange <key><start><stop>
按照索引下标获得元素(从左到右)
lrange mylist 0 -1   0左边第一个,-1右边第一个,(0-1表示获取所有)

4.lindex <key><index>按照索引下标获得元素(从左到右)

5.llen <key>获得列表长度 

6.linsert <key>  before <value><newvalue>在<value>的后面插入<newvalue>插入值

7.lset<key><index><value>将列表key下标为index的值替换成value


数据结构:

List的数据结构为快速链表quickList。

首先在列表元素较少的情况下会使用一块连续的内存存储,这个结构是ziplist,也即是压缩列表。

它将所有的元素紧挨着一起存储,分配的是一块连续的内存。当数据量比较多的时候才会改成quicklist。

为普通的链表需要的附加指针空间太大,会比较浪费空间。比如这个列表里存的只是int类型的数据,结构上还需要两个额外的指针prev和next。 在这里插入图片描述

Redis将链表和ziplist结合起来组成了quicklist。也就是将多个ziplist使用双向指针串起来使用。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余。

2.4.3、Redis集合(Set)

​ Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。

​ Redis的Set是string类型的无序集合。它底层其实是一个value为null的hash表,所以添加,删除,查找的复杂度都是O(1)。

​ 一个算法,随着数据的增加,执行时间的长短,如果是O(1),数据增加,查找数据的时间不变

常见操作:

1.sadd <key><value1><value2> ..... 
将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略

2.smembers <key>取出该集合的所有值。

3.sismember <key><value>判断集合<key>是否为含有该<value>值,有1,没有0

4.scard<key>返回该集合的元素个数。

5.srem <key><value1><value2> .... 删除集合中的某个元素。

6.spop <key>随机从该集合中吐出一个值。

7.srandmember <key><n>随机从该集合中取出n个值。不会从集合中删除 。

8.smove <source><destination>value把集合中一个值从一个集合移动到另一个集合

9.sinter <key1><key2>返回两个集合的交集元素。

10.sunion <key1><key2>返回两个集合的并集元素。

11.sdiff <key1><key2>返回两个集合的差集元素(key1中的,不包含key2中的)

数据结构:

Set数据结构是dict字典,字典是用哈希表实现的。

Java中HashSet的内部实现使用的是HashMap,只不过所有的value都指向同一个对象。Redis的set结构也是一样,它的内部也使用hash结构,所有的value都指向同一个内部值。

2.4.4、Redis哈希(Hash)

​ Redis hash 是一个键值对集合。

​ Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象

​ 类似Java里面的Map<String,Object>

​ 用户ID为查找的key,存储的value用户对象包含姓名,年龄,生日等信息,如果用普通的key/value结构来存储

主要有以下存储方式:

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

常见操作:

1.hset <key><field><value>给<key>集合中的  <field>键赋值<value>

2.hget <key1><field>从<key1>集合<field>取出 value 

3.hmset <key1><field1><value1><field2><value2>... 批量设置hash的值

4.hexists<key1><field>查看哈希表 key 中,给定域 field 是否存在。 

5.hkeys <key>列出该hash集合的所有field

6.hvals <key>列出该hash集合的所有value

7.hincrby <key><field><increment>为哈希表 key 中的域 field 的值加上增量 1   -1

8.hsetnx <key><field><value>将哈希表 key 中的域 field 的值设置为 value ,当且仅当域 field 不存在 .

数据结构:

​ Hash类型对应的数据结构是两种:ziplist(压缩列表),hashtable(哈希表)。当field-value长度较短且个数较少时,使用ziplist,否则使用hashtable。

2.4.5、Redis有序集合Zset(sorted set)

​ Redis有序集合zset与普通集合set非常相似,是一个没有重复元素的字符串集合。

​ 不同之处是有序集合的每个成员都关联了一个**评分(**score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分可以是重复了 。

​ 因为元素是有序的, 所以你也可以很快的根据评分(score)或者次序(position)来获取一个范围的元素。

​ 访问有序集合的中间元素也是非常快的,因此你能够使用有序集合作为一个没有重复成员的智能列表。

常见操作:

1.zadd  <key><score1><value1><score2><value2>…
将一个或多个 member 元素及其 score 值加入到有序集 key 当中。

2.zrange <key><start><stop>  [WITHSCORES]   
返回有序集 key 中,下标在<start><stop>之间的元素
带WITHSCORES,可以让分数一起和值返回到结果集。

3.zrangebyscore key minmax [withscores] [limit offset count]
返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。 

4.zrevrangebyscore key maxmin [withscores] [limit offset count]               
同上,改为从大到小排列。 

5.zincrby <key><increment><value>      为元素的score加上增量

6.zrem  <key><value>删除该集合下,指定值的元素 

7.zcount <key><min><max>统计该集合,分数区间内的元素个数 

8.zrank <key><value>返回该值在集合中的排名,从0开始。

数据结构:

​ SortedSet(zset)是Redis提供的一个非常特别的数据结构,一方面它等价于Java的数据结构Map<String, Double>,可以给每一个元素value赋予一个权重score,另一方面它又类似于TreeSet,内部的元素会按照权重score进行排序,可以得到每个元素的名次,还可以通过score的范围来获取元素的列表。

zset底层使用了两个数据结构:

(1)hash,hash的作用就是关联元素value和权重score,保障元素value的唯一性,可以通过元素value找到相应的score值。

(2)跳跃表,跳跃表的目的在于给元素value排序,根据score的范围获取元素列表。

​ 什么是跳跃表:

  • 有序集合在生活中比较常见,例如根据成绩对学生排名,根据得分对玩家排名等。对于有序集合的底层实现,可以用数组、平衡树、链表等。数组不便元素的插入、删除;平衡树或红黑树虽然效率高但结构复杂;链表查询需要遍历所有效率低。Redis采用的是跳跃表。跳跃表效率堪比红黑树,实现远比红黑树简单。

  • 实例:

    • 对比有序链表和跳跃表,从链表中查询出51
      

      (1) 有序链表

      在这里插入图片描述

      要查找值为51的元素,需要从第一个元素开始依次查找、比较才能找到。共需要6次比较。

      (2) 跳跃表

在这里插入图片描述

          从第2层开始,1节点比51节点小,向后比较。

          21节点比51节点小,继续向后比较,后面就是NULL了,所以从21节点向下到第1层

          在第1层,41节点比51节点小,继续向后,61节点比51节点大,所以从41向下

          在第0层,51节点为要查找的节点,节点被找到,共查找4次。

           

          从此可以看出跳跃表比有序链表效率要高

三、Redis配文件介绍

3.1、目录:…/redis.conf
3.2、Units单位

​ 配置大小单位,开头定义了一些基本的度量单位,只支持bytes,不支持bit

大小写不敏感

在这里插入图片描述

3.3、INCLUDES包含

在这里插入图片描述

​ 类似jsp中的include,多实例的情况可以把公用的配置文件提取出来

3.4、网络相关配置
3.4.1、bind

​ 默认情况bind=127.0.0.1只能接受本机的访问请求

​ 不写的情况下,无限制接受任何ip地址的访问

​ 生产环境肯定要写你应用服务器的地址;服务器是需要远程访问的,所以需要将其注释掉

​ 如果开启了protected-mode,那么在没有设定bind ip且没有设密码的情况下,Redis只允许接受本机的响应

在这里插入图片描述

保存配置,停止服务,重启启动查看进程,不再是本机访问了。

在这里插入图片描述

3.4.2、protected-mode

​ 将本机访问保护模式设置no

在这里插入图片描述

3.4.3、Port

​ 端口号,默认 6379

在这里插入图片描述

3.4.4、tcp-backlog

​ 设置tcp的backlog,backlog其实是一个连接队列,backlog队列总和=未完成三次握手队列 + 已经完成三次握手队列。

在高并发环境下你需要一个高backlog值来避免慢客户端连接问题。

注意Linux内核会将这个值减小到/proc/sys/net/core/somaxconn的值(128),所以需要确认增大/proc/sys/net/core/somaxconn和/proc/sys/net/ipv4/tcp_max_syn_backlog(128)两个值来达到想要的效果

在这里插入图片描述

3.4.5、timeout

​ 一个空闲的客户端维持多少秒会关闭,0表示关闭该功能。即永不关闭

在这里插入图片描述

3.4.6、tcp-keepalive

​ 对访问客户端的一种心跳检测,每个n秒检测一次。

​ 单位为秒,如果设置为0,则不会进行Keepalive检测,建议设置成60

在这里插入图片描述

3.5、GENERAL通用
3.5.1、daemonize

​ 是否为后台进程,设置为yes

​ 守护进程,后台启动

在这里插入图片描述

3.5.2、pidfile

​ 存放pid文件的位置,每个实例会产生一个不同的pid文件

在这里插入图片描述

3.5.3、loglevel

​ 指定日志记录级别,Redis总共支持四个级别:debug、verbose、notice、warning,默认为notice

​ 四个级别根据使用阶段来选择,生产环境选择notice 或者warning

在这里插入图片描述

3.5.4、logfile

​ 日志文件名称

在这里插入图片描述

3.5.5、databases 16

设定库的数量 默认16,默认数据库为0,可以使用SELECT 命令在连接上指定数据库id

在这里插入图片描述

3.6、SECURITY安全

​ 3.6.1、设置密码

在这里插入图片描述

3.7、 LIMITS限制
3.7.1、maxclients

​ Ø 设置redis同时可以与多少个客户端进行连接。

​ Ø 默认情况下为10000个客户端。

​ Ø 如果达到了此限制,redis则会拒绝新的连接请求,并且向这些连接请求方发出“max number of clients reached”以作回应。

在这里插入图片描述

3.7.2、maxmemory

​ Ø 建议必须设置,否则,将内存占满,造成服务器宕机

​ Ø 设置redis可以使用的内存量。一旦到达内存使用上限,redis将会试图移除内部数据,移除规则可以通过maxmemory-policy来指定。

​ Ø 如果redis无法根据移除规则来移除内存中的数据,或者设置了“不允许移除”,那么redis则会针对那些需要申请内存的指令返回错误信息,比如SET、LPUSH等。

​ Ø 但是对于无内存申请的指令,仍然会正常响应,比如GET等。如果你的redis是主redis(说明你的redis有从redis),那么在设置内存使用上限时,需要在系统中留出一些内存空间给同步队列缓存,只有在你设置的是“不移除”的情况下,才不用考虑这个因素。

在这里插入图片描述

3.7.3、maxmemory-policy

​ Ø volatile-lru:使用LRU算法移除key,只对设置了过期时间的键;(最近最少使用)

​ Ø allkeys-lru:在所有集合key中,使用LRU算法移除key

​ Ø volatile-random:在过期集合中移除随机的key,只对设置了过期时间的键

​ Ø allkeys-random:在所有集合key中,移除随机的key

​ Ø volatile-ttl:移除那些TTL值最小的key,即那些最近要过期的key

​ Ø noeviction:不进行移除。针对写操作,只是返回错误信息

在这里插入图片描述

3.7.4、maxmemory-samples

​ Ø 设置样本数量,LRU算法和最小TTL算法都并非是精确的算法,而是估算值,所以你可以设置样本的大小,redis默认会检查这么多个key并选择其中LRU的那个。

​ Ø 一般设置3到7的数字,数值越小样本越不准确,但性能消耗越小。

在这里插入图片描述

四、Redis的发布和订阅

4.1、什么是发布和订阅

​ Redis 发布订阅 (pub/sub) 是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。

​ Redis 客户端可以订阅任意数量的频道。

4.2、Redis的发布和订阅

1、客户端可以订阅频道如下图

在这里插入图片描述

2、当给这个频道发布消息后,消息就会发送给订阅的客户端

在这里插入图片描述

4.3、实现过程

​ 1、 打开一个客户端订阅channel1

127.0.0.1:6379> subscribe channel1
Reading messages... (press Ctrl-C to quit)

2、打开另一个客户端,给channel1发布消息hello

127.0.0.1:6379> publish channel1 Max
(integer) 1

3、打开第一个客户端可以看到发送的消息

127.0.0.1:6379> subscribe channel1
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "channel1"
3) (integer) 1
1) "message"
2) "channel1"
3) "Max"

五、Jedis

​ Jedis: 一款java操作redis数据库的工具。类似于JDBC

5.1、Jedis测试
import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.ListPosition;

import java.util.Set;

/**
 * 如果通过IP访问则必须做的操作有
 * 1.redis.conf文件中修改网络相关配置
 * 服务器是需要远程访问,所以注释掉 bind 127.0.0.1
 * 2.修改保护模式
 * protected_mode no
 * 3.重启服务
 */
public class Jedis_Test {

    public static void main(String[] args) {
// 创建Jedis对象
        Jedis jedis = new Jedis("localhost", 6379);

//  创建Jedis测试
        String ping = jedis.ping();
        System.out.println(ping);

    }
}
5.2、Jedis中Key常用操作
 @Test
    public void Redis_keyDemo() {

        // 创建Jedis对象
        Jedis jedis = new Jedis("localhost", 6379);

        // 切换选择1号数据库
        jedis.select(1);

        // 向一号库添加一些数据
//       String
        jedis.set("K1", "V1");
//        List
        jedis.lpush("name", "Tom");
//        Hash
        jedis.hset("school", "tjse", "info");
//		  set
        jedis.sadd("age", "20");
//        Zset
        jedis.zadd("sex", 100, "sex");

        //查看当前库的所有key
        Set<String> keys = jedis.keys("*");
        for (String key : keys
        ) {
            System.out.println(key);
        }

        //判断某个key是否存在
        System.out.println(jedis.exists("k1"));

        //查看当前key是什么类型
        System.out.println(jedis.type("sex"));

        //删除指定key的数据
        System.out.println(jedis.del("K1"));

        //为给定的key设置time秒过期时间
        System.out.println(jedis.expire("name", 1000));


        /**
         *  -1 标识永不过期   -2 表示已经过期
         */
        System.out.println("*****************");
        //查看TTl(生存时间值)
        System.out.println(jedis.ttl("name"));
        System.out.println("*****************");

        //查看当前数据的key的数量
        System.out.println(jedis.dbSize());

        //清空当前库(慎用)
//        jedis.flushDB();

    }

5.3、Jedis中String常用操作
    @Test
    public void Redis_StringDemo() {
// 创建对象
        Jedis jedis = new Jedis();
// 选择2号数据库
        jedis.select(2);

// 添加String类型数据
        jedis.set("id", "1");
        jedis.set("k1", "v1");
        jedis.set("k2", "v2");
        jedis.set("k3", "v3");

// 获取键对应的值
        System.out.println(jedis.get("id"));

//  给定k进行追加值
        System.out.println(jedis.append("k1", "appendInfo"));

//  获取k对应值的长度
        System.out.println(jedis.strlen("k1"));

//  只有在key不存在时,设置成功key的值
        System.out.println(jedis.setnx("k3", "v3"));
        System.out.println(jedis.setnx("k4", "v4"));

//   对应k存储的数字增加 或减少固定步长 1
        System.out.println(jedis.incr("id"));
        System.out.println(jedis.decr("id"));

//  对应k存储的数字增加,或减少特定步长数据
        jedis.incrBy("id",10);
        jedis.decrBy("id",9);

//  同时设置多个键值对数据
        jedis.mset("name","tom","sex","man","age","18");

//   同时获取多个value
        System.out.println(jedis.mget("name", "sex", "age"));

//   设置键值的同时,设置过期时间,单位秒
        System.out.println(jedis.setex("k6", 10, "k6"));
    }

5.4、Jedis中List常用操作
 @Test
    public void Redis_ListDemo(){

        Jedis jedis = new Jedis();

        jedis.select(3);


//       List操作:从左边/右边插入一个或多个值。
        jedis.lpush("k1", "v1", "v2", "v3");
        jedis.rpush("k2", "s1", "s2", "s3");

//       从左边/右边吐出一个值。值在键在,值光键亡。

        System.out.println(jedis.lpop("k1"));
        System.out.println(jedis.rpop("k1"));

//       标获得元素(从左到右)
        System.out.println(jedis.lrange("k2", 0, -1));

//       按照索引下标获得元素(从左到右)
        System.out.println(jedis.lindex("k2", 1));

//       获取列表长度
        System.out.println(jedis.llen("k2"));

//       在固定值后面插入新值
        System.out.println(jedis.linsert("k2", ListPosition.BEFORE, "s2", "ss2"));

//       将列表key下标为index的值替换成value
        System.out.println(jedis.lset("k2", 0, "ss1"));
    }
5.5、Jedis中Set常用操作
   @Test
    public void Reids_SetDemo() {

        Jedis jedis = new Jedis("localhost", 6379);

        jedis.select(4);

//    将一个或多个 member 元素加入到集合 key 中,已经存在的 member 元素将被忽略
        jedis.sadd("k1", "v1", "v2", "v3");
        jedis.sadd("k2", "v3", "v4", "v5");

//    取出该集合的所有值。
        Set<String> k1 = jedis.smembers("k1");
        Iterator<String> iterator = k1.iterator();
        while (iterator.hasNext()) {
            System.out.println(iterator.next());
        }

//      判断集合<key>是否为含有该<value>值,有1,没有0
        System.out.println(jedis.sismember("k1", "v4"));

//      返回该集合的元素个数。
        System.out.println(jedis.scard("k1"));

//       删除集合中的某个元素。
        System.out.println(jedis.srem("k1", "v1"));

//        随机从该集合中吐出一个值。
        System.out.println(jedis.spop("k1"));

//        随机从该集合中取出n个值。不会从集合中删除
        System.out.println(jedis.srandmember("k1", 3));

//       返回两个集合的交集元素。
        System.out.println(jedis.sinter("k1", "k2"));

//        返回两个集合的并集元素。
        System.out.println(jedis.sunion("k1", "k2"));

//        返回两个集合的差集元素(key1中的,不包含key2中的)
        System.out.println(jedis.sdiff("k1", "k2"));


    }
5.6、Jedis中Hash常用操作
   @Test
    public void Redis_HashDemo(){

        Jedis jedis = new Jedis("localhost", 6379);

        jedis.select(5);

//     给<key>集合中的  <field>键赋值<value>
        jedis.hset("k1","name","tom");
        jedis.hset("k2","age","18");
        jedis.hset("k3","1","v1");

//      批量设置hash的值
        HashMap<String, String> map = new HashMap<>();
        map.put("name","Gern");
        map.put("sex","man");
        map.put("age","18");

        System.out.println(jedis.hmset("k3", map));

//      从<key1>集合<field>取出 value
        jedis.hget("k1","name");

//       列出该hash集合的所有field
        Set<String> set = jedis.hkeys("k3");
        for (String sets : set
             ) {
            System.out.println(sets);
        }

//       列出该hash集合的所有value
        List<String> list = jedis.hvals("k3");
        for (String lists: list
        ) {
            System.out.println(lists);
        }

//        为哈希表 key 中的域 field 的值加上增量 1   -1
       jedis.hincrBy("k3","1",1);
        jedis.hincrBy("k3","1",-1);

    }

5.7、Redis中Zset常用操作
    @Test
    public void Redis_ZsetDemo(){
        Jedis jedis = new Jedis("localhost", 6379);

        jedis.select(6);


//        将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
        jedis.zadd("k1",1,"v1");
        jedis.zadd("k1",2,"v2");
        jedis.zadd("k1",3,"v3");
        jedis.zadd("k1",4,"v4");

//      返回有序集 key 中,下标在<start><stop>之间的元素
//       带WITHSCORES,可以让分数一起和值返回到结果集。

        Set<String> set = jedis.zrange("k1", 0, -1);
        for (String sets: set
             ) {
            System.out.println(sets);
        }

        Set<Tuple> Sets = jedis.zrangeWithScores("k1", 0, -1);

        for (Tuple Set:Sets
             ) {
            System.out.println(Set);
        }

//   返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。有序集成员按 score 值递增(从小到大)次序排列。
        System.out.println(jedis.zrangeByScoreWithScores("k1", 2, 3));


//      统计该集合,分数区间内的元素个数
        System.out.println(jedis.zcount("k1", 0, 3));

//        返回该值在集合中的排名,从0开始。
        System.out.println(jedis.zrank("k1", "v3"));

//        zrem  <key><value>删除该集合下,指定值的元素
        System.out.println(jedis.zrem("k1", "v2"));


        System.out.println("******************");
//        为元素的score加上增量
        System.out.println(jedis.zincrby("k1", 100, "v4"));
        System.out.println("******************");

    }

六、Redis6的事务操作

6.1、事务的定义

​ redis事务是一个单独的隔离操作:事务中的所有命令队徽序列化,按顺序地执行,事务在执行的过程中,不会被其他客户端发来的命令请求所打断。

​ redis事务的只要作用就是串联多喝命令防止别的命令插队。

6.2、事务的三个命令 (Multi、Exec、discard)

从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,Redis会将之前的命令队列中的命令依次执行。组队的过程中可以通过discard来放弃组队。

在这里插入图片描述

当事务中某个命令出现了错误,执行时整个所有队列都会被取消。

在这里插入图片描述

如果执行阶段某个命令报出了错误,则只有报错的命令不会被执行,而其他的命令都会执行,不会回滚。在这里插入图片描述

6.3、Redis事务的三特性
  • 单独的隔离操作

事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。

  • 没有隔离级别的概念

队列中的命令没有提交之前都不会实际被执行,因为事务提交前任何指令都不会被实际执行

  • 不保证原子性

事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

复习内容:Mysql事务特性

​ 1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。

2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。

3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。

4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

七、事务冲突

7.1、什么是事务冲突

​ 当不同行为去操作同一数据时,数据发生异常的错误信息。

例子如下:

一个请求想给金额减8000

一个请求想给金额减5000

一个请求想给金额减1000

在这里插入图片描述

如上情况与事实不符。

7.2、事务冲突的解决方式
7.2.1、悲观锁

在这里插入图片描述

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁表锁等,读锁写锁等,都是在做操作之前先上锁。

7.2.2、乐观锁

在这里插入图片描述

乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis就是利用这种check-and-set机制实现事务的。

7.2.3、监控事务 WATCH key [key …]

在执行multi之前,先执行watch key1 [key2],可以监视一个(或多个) key ,如果在事务**执行之前这个(**或这些) key 被其他命令所改动,那么事务将被打断。

客户端1:
127.0.0.1:6379> set test 100
OK
127.0.0.1:6379> key*
(error) ERR unknown command 'key*'
127.0.0.1:6379> keys *
1) "test"
127.0.0.1:6379> watch test
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incrby test 5
QUEUED
127.0.0.1:6379> exec
1) (integer) 105
127.0.0.1:6379>
客户端2:
127.0.0.1:6379> watch test
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incrby test 10
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379>

这里两个客户端同时设定事务监控,也同时开启Multi进入队列。但其中一个客户端1进行服务的exec,则客户端2进行提交后则得到是一个空值。

使用unwatch进行监控取消。取消 WATCH 命令对所有 key 的监视。如果在执行 WATCH 命令之后,EXEC 命令或DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。

八、持久化(RDB和AOF)

8.1、RDB (Redis DataBase)
8.1.1、什么是RDB

在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是Snapshot快照方式,它恢复时是将快照文件直接读到内存里

8.1.2、备份是如何执行的

​ Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能 如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。

8.1.3、save和bgsave
  • save:save时只管保存,其他不管、全部阻塞,手动保存,不建议使用
  • bgsave:Redis会在后台异步进行快照操作,快照同时还可以响应客户端请求。
8.1.4、RDB持久化流程

在这里插入图片描述

8.1.5、RDB优点
-		适合大规模的数据恢复
-		对数据完整性和一致性要求不高更适合使用
-		节省磁盘空间
-		回复速度快
8.1.6、RDB缺点
  • Fork的时候,内存中的数据被克隆了一份,大致2倍的膨胀性需要考虑
  • 虽然Redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能。
  • 在备份周期在一定间隔时间做一次备份,所以如果Redis意外down掉的话,就会丢失最后一次快照后的所有修改。
8.2、AOF(Append Only File)
8.2.1、AOF是什么

​ 以日志的形式来记录每个写操作(增量保存),将Redis执行过的所有写指令记录下来(读操作不记录), 只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis 重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。 它是默认不开启的、可以在redis.conf中配置文件名称,默认为 appendonly.aof。

8.2.2、AOF和RDB的优先级

AOF和RDB同时开启,系统默认取AOF的数据(数据不会存在丢失)

8.2.3、AOF启动/恢复

​ AOF的备份机制和性能虽然和RDB不同, 但是备份和恢复的操作同RDB一样,都是拷贝备份文件,需要恢复时再拷贝到Redis工作目录下,启动系统即加载。

  • 正常恢复

​ 修改默认的appendonly no,改为yes, 将有数据的aof文件复制一份保存到对应目录(查看目录:config get dir) 恢复:重启redis然后重新加载

  • 异常恢复

修改默认的appendonly no,改为yes

​ 如遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof–fix appendonly.aof进行恢复 备份被写坏的AOF文件 恢复:重启redis,然后重新加载

8.2.4、AOF同步频率设置

appendfsync always

始终同步,每次Redis的写入都会立刻记入日志;性能较差但数据完整性比较好

appendfsync everysec

每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能丢失。

appendfsync no

redis不主动进行同步,把同步时机交给操作系统。

8.2.5、AOF持久化流程
  • 客户端的请求写命令会被append追加到AOF缓冲区内;

  • AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;

  • AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;

  • Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;

在这里插入图片描述

8.2.6、AOP优点
  • ​ 备份机制更稳健,丢失数据概率更低。

  • 可读的日志文本,通过操作AOF稳健,可以处理误操作。

8.2.7、AOP缺点
  • 比起RDB占用更多的磁盘空间。

  • 恢复备份速度要慢

  • 每次读写都要同步的化,有一定的性能压力

8.3、总结
8.3.1、使用推荐

​ 官方推荐两个都启用。

​ 如果对数据不敏感,可以选单独用RDB。

​ 不建议单独用 AOF,因为可能会出现Bug。

​ 如果只是做纯内存缓存,可以都不用。

8.3.2、概述
  • RDB持久化方式能够在指定的时间间隔能对你的数据进行快照存储

  • AOF持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来恢复原始的数据,AOF命令以redis协议追加保存每次写的操作到文件末尾.

  • Redis还能对AOF文件进行后台重写,使得AOF文件的体积不至于过大

  • 只做缓存:如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.

  • 同时开启两种持久化方式

  • 在这种情况下,当redis重启的时候会优先载入AOF文件来恢复原始的数据, 因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集要完整.

  • RDB的数据不实时,同时使用两者时服务器重启也只会找AOF文件。那要不要只使用AOF呢?

    ​ 建议不要,因为RDB更适合用于备份数据库(AOF在不断变化不好备份), 快速重启,而且不会有AOF可能潜在的bug,留着作为一个万一的手段。

九、主从复制

9.1、什么是主从复制

​ 主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(master),后者称为从节点(slave);数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave以读为主。

​ 默认情况下,每台Redis服务器都是主节点;且一个主节点可以有多个从节点(或没有从节点),但一个从节点只能有一个主节点。

9.2、主从复制作用
  • 数据冗余:主从复制实现了数据的热备份,是持久化之外的一种数据冗余方式。

  • 故障恢复:当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复;实际上是一种服务的冗余。

  • 负载均衡:在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写Redis数据时应用连接主节点,读Redis数据时应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个从节点分担读负载,可以大大提高Redis服务器的并发量。

  • 高可用基石:除了上述作用以外,主从复制还是哨兵和集群能够实施的基础,因此说主从复制是Redis高可用的基础。

9.3、实现过程
9.3.1、安装多个redis

在这里插入图片描述

分别设置3个配置文件redis.conf的内容,修改配置文件中的如下内容:

​ 1.注释掉#bind 127.0.0.1

#bind 127.0.0.1

​ 2.关闭保护模式

protected-mode no

​ 3.修改端口号

port 6380   
#其他配置需更改端口号

​ 4.后台启动

daemonize yes

​ 5. 开启主节点授权,如果主节点需要密码的话,这里一定要开启授权才可以,123456为主节点密码

masterauth 123456

​ 6.开启密码

requirepass 123456
9.3.2、建立复制

主从复制的开启,完全是客户端的行为,不需要主节点做任何事情。

​ 1.启动服务端:各子节点连接服务端

#例如
D:\Redis\Redis-x64-3.2.100 -  6380>redis-cli -p 6380
127.0.0.1:6380>

​ 2. 从节点服务端启动命令:

slaveof <masterip> <masterport> 
#例如  slaveof localhost 6379

​ 3. 6380作为6379的从节、从节点是只读的,不能写入数据。

127.0.0.1:6380> set k1 v1
(error) READONLY You can't write against a read only slave.
127.0.0.1:6380>

​ 4. 查询节点信息

127.0.0.1:6380> role
1) "slave"
2) "localhost"
3) (integer) 6379
4) "connected"
5) (integer) 1513

​ 5. 查看主从复制信息

#主节点视角
127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=::,port=6381,state=online,offset=1765,lag=1
slave1:ip=::,port=6380,state=online,offset=1765,lag=1
master_repl_offset:1765
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:2
repl_backlog_histlen:1764
127.0.0.1:6379>
#从节点视角
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:localhost
master_port:6379
master_link_status:up
master_last_io_seconds_ago:6
master_sync_in_progress:0
slave_repl_offset:1793
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

6.主节点下线

127.0.0.1:6379> shutdown
#此时,主从复制的状态,从节点还是能够正常读取数据的,但是不能自动升级为主节点。
127.0.0.1:6380> info replication
# Replication
role:slave
master_host:localhost
master_port:6379
master_link_status:down
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:1961
master_link_down_since_seconds:jd
slave_priority:100
slave_read_only:1
connected_slaves:0
master_repl_offset:0
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
#两个从节点状态一致,主节点都为down的状态,从节点可用,但是不能连接主节点,从节点也不会自动升级为主节点。除非手动断开复制,从节点才会升级为主节点。如果重新启动主节点,从节点又会重新连接主节点。

7.断开复制

127.0.0.1:6380> slaveof no one
OK
127.0.0.1:6380> role
1) "master"
2) (integer) 0
3) (empty list or set)
#通过slaveof <masterip> <masterport>命令建立主从复制关系以后,可以通过slaveof no one断开。需要注意的是,从节点断开复制后,不会删除已有的数据,只是不再接受主节点新的数据变化。
从节点执行slaveof no one后,又变回为主节点。

9.4、哨兵模式
9.4.1、什么是哨兵模式

​ Redis Sentinel,即Redis哨兵,在Redis 2.8版本开始引入。**哨兵的核心功能是主节点的自动故障转移。**下面是Redis官方文档对于哨兵功能的描述:

  1. 监控(Monitoring):哨兵会不断地检查主节点和从节点是否运作正常。

  2. 自动故障转移(Automatic failover):当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。

  3. 配置提供者(Configuration provider):客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。

  4. 通知(Notification):哨兵可以将故障转移的结果发送给客户端。

​ 其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移;而配置提供者和通知功能,则需要在与客户端的交互中才能体现。

9.4.2、哨兵模式架构

​ sentinel可以让redis实现主从复制,当一个集群中的master失效之后,sentinel可以选举出一个新的master用于自动接替master的工作,集群中的其他redis服务器自动指向新的master同步数据。一般建议sentinel采取奇数台,防止某一台sentinel无法连接到master导致误切换。其结构如下:

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

​ 它由两部分组成,哨兵节点和数据节点:

  • 哨兵节点:哨兵系统由一个或者多个哨兵节点组成,哨兵节点是特殊的redis节点,不存储数据。

  • 数据节点:主节点和从节点都是数据节点。

9.4.3、哨兵模式的配置

​ 1.编写配置

​ 分别在3个redis中编写sentinel.conf

在这里插入图片描述

内容如下:

# 当前Sentinel服务运行的端口
port 26379
# Sentinel去监视一个名为mymaster的主redis实例,
# 这个主实例的IP地址为本机地址127.0.0.1,端口号为6379,
# 而将这个主实例判断为失效至少需要2个Sentinel进程的同意,只要同意Sentinel的数量不达标,自动failover就不会执行
sentinel monitor mymaster 127.0.0.1 6379 2
# down-after-milliseconds指定了Sentinel认为Redis实例已经失效所需的毫秒数。
# 当实例超过该时间没有返回PING,或者直接返回错误,那么Sentinel将这个实例标记为主观下线。
# 只有一个Sentinel进程将实例标记为主观下线并不一定会引起实例的自动故障迁移:只有在足够数量的Sentinel都将一个实例标记为主观下线之后,实例才会被标记为客观下线。
# 这时自动故障迁移才会执行
sentinel down-after-milliseconds mymaster 5000
# parallel-syncs指定了在执行故障转移时,最多可以有多少个从Redis实例在同步新的主实例,
# 在从Redis实例较多的情况下这个数字越小,同步的时间越长,完成故障转移所需的时间就越长
sentinel parallel-syncs mymaster 1
# 如果在failover-timeout该时间(ms)内未能完成failover操作,则认为该failover失败
sentinel failover-timeout mymaster 15000

第二个sentinel.conf,只需调整当前sentinel的端口号为26380,其余不变。

第三个sentinel.conf,只需调整当前sentinel的端口号为26381,其余不变。

2.启动哨兵

D:\Redis\Redis-x64-3.2.100 -  6380>redis-server sentinel.conf --sentinel

其他同理。

3.节点和哨兵信息:

在这里插入图片描述

4.启动哨兵客户端,查看哨兵信息:主节点名称,ip地址,端口号,2个从节点,3个哨兵。

在这里插入图片描述

5.主节点shutdown,从节点自动升级为主节点

在这里插入图片描述

从节点自动升级为主节点:从哨兵的日志信息中可以看到:

在这里插入图片描述

6380升级为主节点:

在这里插入图片描述

6381为从节点:

在这里插入图片描述

重新启动6379节点,则6379作为6380的从节点:

在这里插入图片描述

十、Redis集群

注:引用
版权声明:本文为CSDN博主「最爱喝酸奶」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/miss1181248983/article/details/90056960

十一、Redis应用问题

11.1、缓存穿透
11.1.1、问题描述

在这里插入图片描述

​ 用户通过提高访问量,或者出现很多非正常url访问,提高web服务器的压力。由于是错误访问,导致缓存未命中,让服务器一致查询数据,但数据库提供不了数据。导致服务器宕机。

补充:什么是redis的命中率

​ 命中:可以直接通过缓存获取到需要的数据。

​ 不命中:无法直接通过缓存获取到想要的数据,需要再次查询数据库或者执行其它的操作。原因可能是由于缓存中根本不存在,或者缓存已经过期。

​ 通常来讲,缓存的命中率越高则表示使用缓存的收益越高,应用的性能越好(响应时间越短、吞吐量越高),抗并发的能力越强。

​ 由此可见,在高并发的互联网系统中,缓存的命中率是至关重要的指标。

11.1.2、解决方式

(1) **对空值缓存:**如果一个查询返回的数据为空(不管是数据是否不存在),我们仍然把这个空结果(null)进行缓存,设置空结果的过期时间会很短,最长不超过五分钟

(2) 设置可访问的名单(白名单):

使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmap里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。

(3) 采用布隆过滤器:(布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。

布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)将所有可能存在的数据哈希到一个足够大的bitmaps中,一个一定不存在的数据会被 这个bitmaps拦截掉,从而避免了对底层存储系统的查询压力。

(4) **进行实时监控:**当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务

11.2、缓存击穿
11.2.1、问题描述

​ key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。

在这里插入图片描述

11.2.2、解决方案

(1)预先设置热门数据:在redis高峰访问之前,把一些热门数据提前存入到redis里面,加大这些热门数据key的时长

(2)实时调整:现场监控哪些数据热门,实时调整key的过期时长

(3) 加锁 缺点降低了整体系统效率

(4)后台刷新:后台定义一个job(定时任务)专门主动更新缓存数据.比如,一个缓存中的数据过期时间是30分钟,那么job每隔29分钟定时刷新数据(将从数据库中查到的数据更新到缓存中).

11.3、缓存雪崩
11.3.1、问题描述

​ 在极短的时间段、查询大量key的集中过期情况,数据访问压力集中在数据库,导致服务器宕机,

​ 缓存雪崩与缓存击穿的区别在于这里针对很多key缓存,后者则是某一个key

11.3.2、解决方案

(1)构建多级缓存架构:**nginx缓存 + redis缓存 +其他缓存(ehcache等)

(2) 使用锁或者队列

​ 用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。不适用高并发情况

(3) 设置过期标志更新缓存

​ 记录缓存数据是否过期(设置提前量),如果过期会触发通知另外的线程在后台去更新实际key的缓存。

(4)将缓存失效时间分散开

​ 比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值