第七节 Redis

Redis数据结构服务器

1. redis 是个啥?

  1. redis(Remote Dictionary Server)远程字典服务,是一个内存 NoSql 数据库,即非关系型数据库。
  2. redis是,存储 key-value 形式的数据。
  3. redis 中的 key-value 中 的 value 比较强大,它的value可以不仅仅是一个byte[] (zookeeper只能存byte[])
    • redis 的 value可以有结构 :可以是一个List,也可以是一个hash(简单的理解为:value里也可以存key-value),也可以是set…所以redis经常被称作为----数据结构服务器。

1.1 为什么需要redis

  1. 纵观现在互联网的发展趋势,电商几乎独占天下,电商业务的特点:
    • CURD 的操作中,80%是查询业务, 20%是新增,修改,删除业务。
    • 数据不会频繁的变更 (电商中的店小二只维护商品的价格)。
  2. 上面说了这么多,之所以选择redis就是,因为它的快。
    • 相比于mySql来说,redis基于 内存操作的,这样好处就是减少了磁盘IO的操作。
    • 合理的数据编码,如根据需求能存储相应的数据类型,存储的 5种数据类型。
    • 并且有自己的虚拟机制。
    • 它是单线程操作,避免了频繁的线程切换。

2. Redis 安装

2.1 安装前准备

  1. 因为redis安装需要 依赖c语言环境,所以需要gcc进行编译。

    • 如果下载最新版本的redis redis -6.0.6的话就会需要 Linux, gcc编译版本在5.0以上,这里实验使用的是 redis-3.2.8.tar.gz gcc版本在 4就可以使用。
    • gcc --version 查看gcc版本。
      在这里插入图片描述
  2. 然后上传 redis-3.2.8.tar.gz 到Linux目录下/usr/java ,并解压该tar包。

    • 进入/usr/java/redis-3.2.8/src 执行默认有gcc的话,在该目录下执行make 会产生相应的库文件。
      在这里插入图片描述

    • make之后 ,在使用make install 即确认安装。将可以执行文件放到相应目录。
      在这里插入图片描述

  3. 注意: make install 默认会安装在 /usr/local/bin,也可以自己指定目录make PREFIX=/usr/java/redis install中间加路径即可。
    在这里插入图片描述

2.2 启动 redis

1 . 启动 redis 服务端- 服务器端启动 /usr/local/bin 执行 redis-server
在这里插入图片描述
2. 客户端启动: 然后再启动一个连接/usr/local/bin 目录下执行 redis-cli客户端启动。

  • 注意,有时候set 中文时会有乱码,中文乱码问题 就需要启动时 redis-cli --raw

    在这里插入图片描述
    4. 测试客户端连接。 输入 ping 即可
    在这里插入图片描述

3. redis 存储数据结构

3.1 支持的数据类型

  1. redis可以存储类型 key-value形式。其实还有3种特殊类型但是一般不刻意使用。
    • 3 种特殊类型: Geo,HyperLogLog,Bitmaps。

在这里插入图片描述

3.2 命令行的基本操作 String set list 为例

3.2.1 String 命令行操作
  1. 既然是存储就离不开增删改查,以String类型为例子。
    • set key value 存放数据 ,如果添加的key值相同,则覆盖value值。
    	// 添加数据  set key value
    		set name zhangsan 
    		
    
    • get key 查看value值
    	// 查看value值  get key
    	get name
    
    • keys * 显示所有key值
    	// 显示所有key值  keys *
    	keys *
    
    • del key 删除key值,相应的数据也会删除。
    	// 删除key  del key
    	del name
    
    • dbsize 显示key总数。 如,有多少个键值对。
    	// 查看当前key总数  dbsize
    	dbsize
    
    • rename key newkey 重命名key
    	// 将key值重命名  rename key newkey
    	rename name name1
    
    • exists key 检查键值是否存在
    	// 检查键值是否存在 exists key
    	exists name
    
    • type key 显示键的类型
    	// 查看键的类型 type key
    	type name
    
3.2.2 List 命令行操作
  1. 添加List操作,lpush/rpush 明白什么是从左侧添加? 什么是从右侧添加?

    • 添加操作从,左边开始添加 lpush key value1[value2...]
    • 添加操作从,右边开始添加 rpush key value 1[value2...]
   // 1.往redis中添加list 从左边开始添加 
	   lpush grils liuyifei yangmi angelbaby zhaoliying 
	   
  //  2.往redis中添加list 从右侧开始添加
  		rpush boys liudehua zhangxueyou guofucheng liming 
  1. 查询操作

    • 根据索引查询 lindex key index
    • 通过获取list起始元素开始查询 lrange key start index stop index
    • 显示 列表长度 llen key
    //1. 根据索引查询 
    	lindex girls 0  //左插入,返回结果 "zhaoliying"
    	lindex boys 0 // 右插入,返回结果  "liudehua"
    	
    //2. 通过元素获取
    	lrange girls 0 -1 //0~-1指获取全部  
    	lrange girls 0 1  // zhaoliying  angelbaby 
    // 3.显示列表长度
    	llen boys // 4
    	
    
  2. 删除操作,每次只删除一个。

    • 从左边删除 lpop key
    • 从右边删除rpop key
    //1.从左边删除一个 
    	lpop girls // zhaoliying
    //2.从右边删除第一个
    	rpop girls // liuyifei
    
  3. 删除操作 ,删除指定元数 。

    • 假设list 列表里面有 10个yangmi 元数 lrem girls count yangmi
    // 删除指定元素 count 指定数量
    lrem girls 8 yangmi // 即删除list 中 8个yangmi
    
  4. 修改操作
    - 修改指定索引 lset key index value

     // 修改索引 0 的元素为ruhua
     lset girls 0 ruhua
    
3.2.3 set 命令行操作
  1. 添加元素 不存储相同的集合元素。

    • sadd key value1 [value2] ... 添加元素,
    	// 添加元素 
    	sadd friends a b c d d d e // 只能添加一个d
    
  2. 显示出集合所有元素。 smembers key显示无序!

    • 获取成员集合成员个数 scard key
     // 1.显示出集合中所有元素
     	smembers friends //列出所有 b c d e a 因为是无序!
     //2 .获取成员个数
      	scard friends // 5
    
  3. 删除集合中元素 srem key value 1 [ value2]...

    • 随机删除元素并返回删除元素 spop key
     // 1 删除一个或者多个
     	srem friends a b c // 删除 a b c
     // 2.随机删除元素
     	spop friends // 随机删除 并返回删除元素
    
  4. 集合之间的操作。

    • 返回集合之间的交集sinter key1[key2]...
     //1.返回多个集合之间的交集
     sinter friends friendss // 
    
    • 返回集合之间的并集sunion key1 key2 ...
    //1.返回集合之间的并集,即集合合并去除重复。
    	sunion friends friendss //相当于合并集合
    
    • 返回集合之间的 差集sdiff key1 key2 ...
    //1.取集合之间的差集,即去除相同元素
    sdiff friends friendss //去除同类元素
    

3.3 flushall 和 flushdb

  1. 二者区别在于 “程序员删库跑路事件”

    	// 清空所有数据库
    1. flushall  
    	 // 清空当前数据库
    2. flushdb 
    
    

4. Jedis客户端Api的示范

4.1 存String类型

  1. 因为是用底层c开发的,就需要java开发程序去连接,这里我们选择的是jedis java连接redis开发,首先需要导入pom.xml 依赖文件。
    • 可以使用中央仓库,但是需要网络 中央仓库
    • pom.xml 。
    <dependencies>
                <dependency>
                    <groupId>redis.clients</groupId>
                    <artifactId>jedis</artifactId>
                    <version>2.9.0</version>
                </dependency>
        </dependencies>
    
  1. 使用 java 操作Api 连接redis操作。
    • 注意修改 redis.conf 文件,配置ip 权限保护,后台启动。
    • 并且重启之后需要启动服务时读取该文件,redis-server /usr/java/redis-3.2.8/redis.conf
      在这里插入图片描述
public class Redis_Client {
    Jedis jedis =null;
    @Before
    public void init() {
        /* 1.通过jedis对象创建连接。
            new jedis(host,prot)即可
         */
        jedis = new Jedis("192.168.150.140", 6379);
        String ping = jedis.ping();
        System.out.println(ping); //返回 pong
    }

    @Test
    public void testSet(){
        String data = jedis.set("name", "刘亦菲");
        System.out.println(data);
    }
    @Test
    public void testGet(){
        String name = jedis.get("name");
        System.out.println(name);
    }
}

4.2 存储对象

  1. 使用redis存储对象。
    • 访问电商京东,思考一下,那些商品存储在什么地方 ? 每次都需要访问数据库么?
      在这里插入图片描述
      在这里插入图片描述
public class Product_redis {

    @Test
    public void setProduct() throws IOException {
        Jedis jedis = new Jedis("192.168.150.140", 6379);
        Product p = new Product("001","明星同款空调房披肩外搭女秋冬季米灰刘-亦菲同款 190*65cm",89);
        /*
            将对象转成字节数组  object to bytearray
            set(byte[],byte[])
         */
        ByteArrayOutputStream bs = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bs);
        oos.writeObject(p);
        jedis.set(p.getP_id().getBytes(),bs.toByteArray());
        jedis.close();//关闭
    }

    @Test
    public void getProduct() throws IOException, ClassNotFoundException {
        Jedis jedis = new Jedis("192.168.150.140", 6379);
        byte[] bytes = jedis.get("001".getBytes());
        /*
        //打印的是字节数组,不是我们想要的。
        for (byte b:bytes){
            System.out.println(b);
        }
         */

        /*需要将对象进行反序列化 bytearray to object
         */
        ByteArrayInputStream bi = new ByteArrayInputStream(bytes);
        ObjectInputStream ois = new ObjectInputStream(bi);
        //转换类型将 object 转为product
        Product p=(Product)ois.readObject();
        System.out.println(p.getP_id()+" ----"+p.getTitle()+"-----"+p.getPrice());
        jedis.close();
    }
}

  • Product 对象。
public class Product implements Serializable { //实现序列化
    private String  p_id; //商品id
    private String title; //商品标题
    private double price; //价格

    public Product(String p_id, String title, double price) {
        this.p_id = p_id;
        this.title = title;
        this.price = price;
    }

    public Product() {
    }

    public String getP_id() {
        return p_id;
    }

    public void setP_id(String p_id) {
        this.p_id = p_id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public double getPrice() {
        return price;
    }

    public void setPrice(double price) {
        this.price = price;
    }
}

4.3 存List类型

  1. 针对客户端的List操作。
public class Redis_Client {
    Jedis jedis =null;
    @Before
    public void init() {
        /* 1.通过jedis对象创建连接。
            new jedis(host,prot)即可
         */
        jedis = new Jedis("192.168.150.140", 6379);
        String ping = jedis.ping();
        System.out.println(ping); //返回 pong
    }

    /*
        list操作

      */
    @Test
    public void testList(){
        //1.从左侧开始插入
        Long lpush = jedis.lpush("nba", "kobe", "jordan", "rose", "ai", "curry");
        System.out.println(lpush); //返回长度
        //2.从右侧开始插入
        jedis.rpush("wnba","canglaoshi","xiaozhelaoshi","guanyuelaoshi");

    }

    @Test
    public void testLrange(){
        List<String> nba = jedis.lrange("nba", 0, -1);
        System.out.println(nba); //[curry, ai, rose, jordan, kobe]
    }
    @Test
    public void testPop(){
        String rnba = jedis.rpop("nba");
        System.out.println("从右边删除:" +rnba); //kobe
       String lnba = jedis.lpop("nba");
       System.out.println("从左边删除:" +lnba); //curry
    }

    @Test
    public void  testListSet(){
        String lset = jedis.lset("nba", 0, "caixukun");
        System.out.println(lset); // //[caixukun, ai, rose, jordan, kobe]
    }
}

4.4 存set类型

  1. set 集合 java API 操作。
public class Redis_Client {
    Jedis jedis =null;
    @Before
    public void init() {
        /* 1.通过jedis对象创建连接。
            new jedis(host,prot)即可
         */
        jedis = new Jedis("192.168.150.140", 6379);
        String ping = jedis.ping();
        System.out.println(ping); //返回 pong
    }


    /*
        set操作
     */

    @Test
    public void test_Set(){
        //1. 添加操作
        Long sadd = jedis.sadd("num", "a", "b", "c", "d", "e");
        //2.获取成员个数
        Long num = jedis.scard("num");
        System.out.println("set集合中value个数"+num);

        //3.显示所有
        Set<String> word = jedis.smembers("num");
        for (String n : word){
            System.out.println("循环遍历个数:" +n);
        }

        //4. 删除
        Long srem = jedis.srem("num", "e", "a");
        System.out.println("删除的set集合元素"+srem);
    }

}

  1. 集合之间操作。
    • 集合之间交集。
    • 集合之间并集。
    • 集合之间去除相同。
public class Redis_Client {
    Jedis jedis =null;
    @Before
    public void init() {
        /* 1.通过jedis对象创建连接。
            new jedis(host,prot)即可
         */
        jedis = new Jedis("192.168.150.140", 6379);
        String ping = jedis.ping();
        System.out.println(ping); //返回 pong
    }


    @Test
    public void test_SetandSet(){
        //1.取交集
        Set<String> sinter = jedis.sinter("num", "num1");
        System.out.println("取交集:"+sinter);

        //2.取并集
        Set<String> sunion = jedis.sunion("num", "num1");
        System.out.println("取并集:"+sunion);

        //3.取差集 前面的集合为主,去除包含后面的元素。
        Set<String> sdiff = jedis.sdiff("num1", "num");
        System.out.println("取差集:"+sdiff);
    }
}

4.5 存hash类型

  1. 存key-value形式的hash值。 key - field(字段属性) - value
    • 添加数据
    • 查询一个,查询全部
    • 增量数据
    • 判断是否存在
    • 删除
	public class Redis_Hash {
    private  Jedis jedis = null;
    @Before
    public void  init (){
         jedis = new Jedis("192.168.150.140", 6379);
        System.out.println(jedis);
    }

    @Test
    public void  testHash(){
        /*
            1.添加数据,  hset(String var1, String field, String var3);

         */
        jedis.hset("cart","苹果","1");
        jedis.hset("cart","香蕉","3");
        jedis.hset("cart","橘子","5");

        /*1.1
            添加多个key value
         */
        Map<String, String> hash = new HashMap();
        hash.put("猕猴桃","1");
        hash.put("冬枣","2");
        hash.put("青瓜","5");
        jedis.hmset("cart",hash);

        /*2. 获取数据
                hget(key,fild); //第一个key值 第二个是value中的 key
         */
        String hget = jedis.hget("cart", "苹果");
        System.out.println(hget);

        /*2.1 返回多个
            hgetAll(key),通过key返回里面的value(key -value)。
         */
        Map<String, String> cart = jedis.hgetAll("cart");
        System.out.println(cart);//{青瓜=5, 猕猴桃=1, 苹果=1, 香蕉=3, 橘子=5, 冬枣=2}


        /*3. 向cart中 冬枣 添加4个
                      青瓜 减掉3个
                hincrBy(key,field,value)
         */
        jedis.hincrBy("cart","冬枣",4);
        jedis.hincrBy("cart","青瓜",-3);


        Map<String, String> cart1 = jedis.hgetAll("cart");
        System.out.println(cart1);//{青瓜=2, 猕猴桃=1, 苹果=1, 香蕉=3, 橘子=5, 冬枣=6}

        /*4. 删除hash表中的一个字段
         */
        jedis.hdel("cart","橘子");
        Map<String, String> cart2 = jedis.hgetAll("cart");
        System.out.println(cart2);//{青瓜=2, 猕猴桃=1, 苹果=1, 香蕉=3, 冬枣=6}

        /*5.判断是否存在hash值
                hexists(key,field)
         */
        Boolean hexists = jedis.hexists("cart", "橘子");
        System.out.println(hexists);//false
    }
}

4.6 存带排序的Zset集合类型

  1. set集合无序,如果想排序需要通过代码实现,但是redis中有封装号的有序set。
public class Redis_Zset {

    private Jedis jedis =null;
    @Before
    public void init(){
         jedis = new Jedis("192.168.150.140", 6379);
        System.out.println(jedis);
    }

    /**
     * 带排序的set集合
     *      例如:qq音乐排行榜
     *      收听 万单位 人名
     */
    @Test
    public void test_Zset(){
        // 1.添加数据
        jedis.zadd("music",300,"周杰伦");
        jedis.zadd("music",80,"林俊杰");
        jedis.zadd("music",90,"王力宏");
        jedis.zadd("music",10,"蔡徐坤");


        //2.获取数据 升序
        Set<String> music = jedis.zrange("music", 0, -1);
        System.out.println(music);

        //2.1 获取数据 降序 reverse
        Set<String> revmusic = jedis.zrevrange("music", 0, -1);
        System.out.println(revmusic);


        //3. 取某个元素的索引 从升序中取
        Long zrank = jedis.zrank("music", "王力宏");
        System.out.println(zrank);

        //3.1 取某个元素索引 从降序中取
        Long zrevrank = jedis.zrevrank("music", "王力宏");
        System.out.println(zrevrank);

        //4.返回某个成员的分数值。
        Double music1 = jedis.zscore("music","王力宏");
        System.out.println(music1); //90

		 //5.删除元素
        Long zrem = jedis.zrem("music", "林俊杰");
        System.out.println(zrem);

		//6. 对有序集合上进行增量
        Double zincrby = jedis.zincrby("music", 3, "周杰伦");
        System.out.println(zincrby);
    }
}

[蔡徐坤, 林俊杰, 王力宏, 周杰伦]
[周杰伦, 王力宏, 林俊杰, 蔡徐坤]
2
1
90.0
1
303.0

5 统计场景案例

  1. 查看电影观看次数。
    • 提示: zrangeByScoreWithScores() 方法使用。
/**
    客户端采集
 */
public class Movie_Customer {

    public static void main(String[] args) throws InterruptedException {
        //1.连接客户端
        Jedis jedis = new Jedis("192.168.150.140", 6379);

        //2. 创建一个数组里面存储电影
        String[] movies  = {"战狼","战狼2","复仇者联盟","复仇者联盟2","复仇者联盟3","复仇者联盟4"};

        //3.随机访问观看
        Random random = new Random();
        while (true){
            //4 随机挑一个电影观看
            String movie = movies[random.nextInt(movies.length)];

            /*4.1
                放入redis中,后面会统计观看
                    相当于更新 观看点击次数,每次进行加1 在挑选的基础上
             */
            jedis.zincrby("movie",1,movie);

            //5. 看时间不定,假设在看电影
            System.out.println("电影观看中-------");
            Thread.sleep(300);
        }
    }
}

/**
    显示观看排行榜
 */
public class Movie_Box {

    public static void main(String[] args) throws InterruptedException {
        Jedis jedis = new Jedis("192.168.150.140", 6379);

        while (true){

//            //1.显示 redis数据,即显示观看次数 ,选择升序还是降序!
//            Set<String> movie = jedis.zrevrange("movie", 0, -1);//显示全部
//            System.out.println(movie);

            // 1.1 需要显示members 和 key值 zrangeByScoreWithScores 指定区间
            Set<Tuple> movie = jedis.zrangeByScoreWithScores("movie", 0, 100, 0, 10);
            for (Tuple t : movie){
                System.out.println(t.getElement()+"---"+t.getScore());
            }


            //2.显示查看, 在15秒查询5次。
            Thread.sleep(1500);
            System.out.println("------------------------");
        }
    }
}

6 话题

6.1 redis的两种持久化方案

  1. redis是基于内存的存储key-value服务器,可想而知,内存不是磁盘,一旦断电或者宕机就会造成数据的缺失。所以它不同于MemoryCache 它提供了持久化的能力。
    • RDB 相当于快照来保存。利用一个进程fork,遍历整个hashtable 然后将整个db dump都保存下来。没有实时性
    • AOF 采用日志的形式来记录每个写操作, 追加到文件中,重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。 默认是不开启的。
  1. 至于怎么选择持久化看业务需求,也可以二者同时使用。

6.2 redis 和数据库 双写一致性问题

  1. redis作用就是缓存,提高读的效率操作,它只是应用和数据库 之间的一个读操作的缓存层目的就是减少数据库的IO,客户端读数据有缓存就从缓存中读取,没有缓存就从数据库中读取,然后将读取的数据写入缓存中,
  2. 这是就会出现的问题,一份数据同时保存在redis和数据库中,当数据发声变化时,需要更新 redis和数据库,因为更新操作是有先后顺序的,不像mysql中的多表事务操作,可以满足ACID的特性,此时就会出现数据一致性的问题。一致性问题如果具体在细分的话,还可以分为最终一致性和强一致性。
  3. 数据库和缓存双写,就必然会出现这样的问题,不一致问题。前提是如果对数据有强一致性要求就不能放缓存。
  4. 只能说从方案上处理,最多能降低不一致发生的概率,可以使用消息队列保证一致性,如redismq等组件。

6.3 缓存穿透问题

  1. 所谓穿透, 可以理解为,去缓存中访问缓存不存在的数据,这样缓存中没有,从而就会向数据库中访问数据,这样压力都会指向持久层,根本在于,当查询缓存中数据没有命中, 由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,进而给数据库带来压力。
    • 如: 用户想要查询一个数据,发现redis内存数据库没有,也就是缓存未命中,于是向持久层数据库查询。发现也没有,于是本次查询结束。
    • 当很多的用户都发起这个相同的操作,缓存都没有命中 (抢演唱会门票) 于是都去请求了持久层数据库,这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
    • 黑客攻击缓存中不存在的数据,也会造成上述表现。
  2. 如果会造成这样的结果就失去了缓存的意义,其redis本身就是为了解决数据库压力而使用的。
  • 解决办法:
    1. 使用互斥锁,缓存失效时,先获取锁在请求数据库,没有锁就休眠时间在去请求,这样可以缓解数据库压力。
    1. 使用 布隆过滤器{布隆过滤器是一种数据结构,布隆过滤器对所有可能查询的参数以hash形式存储,在控制层先进行校验,不符合则丢弃,从而避免了对底层存储系统的查询压力} 快速判断数据是否存在。内部维护一系列合法有效的key,然后迅速做出判断,请求的key是否合法,如果合法则有效访问,如果不合法就直接返回。

6.4 缓存雪崩问题

  1. redis 缓存中可以设置有效时间,此时相当于大批的缓存失效,而此时查询量又很大,结果造成了数据库的压力倍增,从而导致数据库连接异常。
  2. redis故障也能引起雪崩问题, 可以使用redis高可用解决。
  • 解决办法:
    1. 给缓存的失效时间,让过期时间离散一点,不在同一个时间点上大量过期,例如:在上一个随机值,避免集体失效。
    1. 使用锁机制,限流,这样也可以降低访问。
    1. 双缓存机制, A 缓存, B 缓存, 一个设置有时间,另一个不设置过去时间。问题是需要自己做缓存的同步操作。 例如: 从a缓存读取数据,有就返回,如果没有在去B中读取, 这时在同步A和B的数据。相当于提前做了缓存预热。

6.5 热key问题

  1. 在redis中有些key是访问比较高的,如: 音乐排行榜 ,新闻热点 。这种访问比较高的称之为热key。
    • 由于请求量特别大,不断的请求服务器操作,可能会导致主机资源不足,从而影响正常的服务。
  • 解决方案:
    1. 凭经验判断哪些可能成为热key,然后把热key分散到不同的服务器上。
    1. 在客户端进行统计收集:这个方式就是在操作redis之前,加入一行代码进行数据统计。那么这个数据统计的方式有很多种,也可以是给外部的通讯系统发送一个通知信息。
    1. 做二级缓存,将热key缓存到本地jvm中即可。不走redis读取。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

吴琼老师

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值