闭关修炼(十九)Mysql优化


mysql如何调优?这是面试时常问的问题,主要有这些方面:

  • 范式设计
  • 数据库分表分库
  • 定位慢查询
  • 索引优化
  • 索引原理
  • SQL优化
  • 数据库读写分离
  • 零碎的知识:having、存储过程、触发器、函数

mysql如何实现优化?

  1. 数据库设计合理(需要经验)
  2. 添加索引(普通索引、主键索引、唯一索引、全文索引)
  3. 分表分库(取模分表、水平分割、垂直分割)
  4. 读写分离
  5. 存储过程
  6. 配置mysql最大连接数(my.ini)
  7. 服务器升级(硬件)
  8. 随时清理碎片(小细节)
  9. sql语句调优(核心)

数据库三大范式

数据设计目的是减少冗余量

三大范式是什么?

1F

原子约束

表示每列不可再分

是否保证原子约束看业务需求

2F

每个表必须有主关键字(Primary key),其他数据元素与主关键字一一对应。

关键字:保证唯一,主键

在订单表中,如下,id和orderNum都是唯一的

idorderNumname

项目中为了安全性不允许直接用id作为订单号,而是新增一列orderNum,在Rpc远程调用使用id,外部接口用oderNum

分布式系统解决并发生成订单号问题,也就是如何保证幂等性?
分布锁(不怎好),一般做法是提前将订单号生成号,存放在redis中。需要时候直接去redis中去取

3F

表中的所有数据元素不但要能唯一地被主关键字所标识,而且它们之间还必须相互独立,不存在其他的函数关系。

核心思想是不存在冗余

比如course表,这其中class_id和class_name就存在函数关系,属于冗余数据

iduser_idclass_idclass_name
111第一期
221第一期
312第二期

修改的方法是再建立一张表存放class_id和class_name的对应关系

class_idclass_name
1第一期
2第二期
3第三期

数据库不一定要完全遵循3范式


分表分库

什么时候分表分库?

分库:

垂直分割:电商项目中将一个项目进行拆分,拆成多个小项目,每个小项目有自己单独的数据库,项目间就互不影响。比如会员数据库,订单数据库,支付数据库


分表:

水平分割:分表规则根据业务需求,如存放日志的表按每年存放,根据年份进行分割。腾讯QQ号根据位数分表。会员系统根据手机号前三位分表。


水平分割取模算法:

保证分表后数量均匀。

user表有id,我们希望将user分成三张表user0 user1 user2,id%3,根据取模结果分到三张表中

实现取模分表算法需要专门有一张表存放userid的数量


水平分割取模算法案例

建表,建立三张user分表和一张专门存放userid的uuid表

-- phpMyAdmin SQL Dump
-- version 5.0.2
-- https://www.phpmyadmin.net/
--
-- 主机: 127.0.0.1
-- 生成日期: 2021-01-19 07:22:06
-- 服务器版本: 10.4.14-MariaDB
-- PHP 版本: 7.4.10

SET SQL_MODE = "NO_AUTO_VALUE_ON_ZERO";
START TRANSACTION;
SET time_zone = "+00:00";


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8mb4 */;

--
-- 数据库: `test`
--

-- --------------------------------------------------------

--
-- 表的结构 `user0`
--

CREATE TABLE `user0` (
  `id` int(11) NOT NULL,
  `name` varchar(20) NOT NULL,
  `pwd` varchar(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- 表的结构 `user1`
--

CREATE TABLE `user1` (
  `id` int(11) NOT NULL,
  `name` varchar(20) NOT NULL,
  `pwd` varchar(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- 表的结构 `user2`
--

CREATE TABLE `user2` (
  `id` int(11) NOT NULL,
  `name` varchar(20) NOT NULL,
  `pwd` varchar(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- --------------------------------------------------------

--
-- 表的结构 `uuid`
--

CREATE TABLE `uuid` (
  `id` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

--
-- 转储表的索引
--

--
-- 表的索引 `uuid`
--
ALTER TABLE `uuid`
  ADD PRIMARY KEY (`id`);

--
-- 在导出的表使用AUTO_INCREMENT
--

--
-- 使用表AUTO_INCREMENT `uuid`
--
ALTER TABLE `uuid`
  MODIFY `id` int(11) NOT NULL AUTO_INCREMENT;
COMMIT;

/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;

导入jdbc jar包

<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>8.0.19</version>
</dependency>

连接JDBC


import java.sql.*;

public class JDBCUtil {

    public static final String URL = "jdbc:mysql://localhost:3306/test";
    public static final String USER = "root";
    public static final String PASSWORD = "";

    public static Connection getConnection() throws SQLException {
        return DriverManager.getConnection(URL, USER, PASSWORD);
    }
}

写Service层

import java.sql.*;

public class UserService {
    private Connection conn = JDBCUtil.getConnection();

    public UserService() throws SQLException {
    }

    public int insertAndReturnParameterKey(String sql) {
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        int id = -1;
        try {
            pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
            pstmt.executeUpdate();
            rs = pstmt.getGeneratedKeys();
            if (rs.next()) {
                id = rs.getInt(1);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return id;
    }

    public boolean register(String userName, String password) throws SQLException {
        try {
            String insertUUidSql = "insert into uuid values(null)";
            // 生成user_id
            int userId = insertAndReturnParameterKey(insertUUidSql);
            System.out.println(userId);
            // 确定存放在哪张表
            String tableName = "user" + userId % 3;
            // 插入到表中
            String insertUserSql = String.format("insert into %s values(%s, \"%s\", \"%s\")",
                    tableName, userId, userName, password);
            System.out.println(insertUserSql);
            PreparedStatement ptmt = conn.prepareStatement(insertUserSql);
            ptmt.execute();

            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    public String get(int userId) throws SQLException {
        // 确定存放在哪张表
        String tableName = "user" + userId % 3;
        // 查询
        String selectSql = String.format("select * from %s where id = %d",
                tableName, userId);
        PreparedStatement ptmt = conn.prepareStatement(selectSql);
        //执行
        ResultSet rs = ptmt.executeQuery();
        String resultSet = "";
        while(rs.next()){

            int id = rs.getInt("id");
            String name = rs.getString("name");

            resultSet += id + " : "+name + '\n';
        }

        return resultSet;
    }

}

测试

import org.junit.Test;

public class TestRegister {

    @lombok.SneakyThrows
    @Test
    public void test(){
        new UserService().register("jack", "12345");
        String result = new UserService().get(8);
        System.out.println(result);
    }
}

测试结果
在这里插入图片描述
在项目中这样做其实有很大的缺点:

不能分页查询,查询非常受限制,可以建立视图将三张表又合为一张表稍微缓解一下,所以一般做法还是一张主表存放所有数据,再根据业务需求进行分表(如果是用id查就用分表进行查询)

项目中会用mycar进行分表

还有就是阿里云的rds云数据库自带分表分库,底层自动解决高并发问题。


定位慢查询

什么是慢查询?

超过指定时间的SQL语句查询称为“慢查询”

mysql默认慢查询默认是10s,其实超过1s就应该开始考虑如何做优化了

先了解相关的sql语句

查看mysql状态
show status

查看试图连接mysql的次数
show status like 'connections'

查看慢查询次数
show status like 'slow_queries'

查询慢查询时间,默认是10s
show VARIABLES LIKE 'long_query_time'

修改慢查询时间,改成1s
set long_query_time = 1

如何定位慢查询是哪条语句呢?

开启慢查询日志

# 安全模式开启
set global  SQL_SAFE_UPDATES = 1;
# 开启慢查询日志
set global slow_query_log='ON';
# 设置慢查询时间为1秒
set global long_query_time=1;

查看修改结果

# 显示sql安全模式类型
show variables like 'SQL_SAFE_UPDATES';
# 查看慢查询设置状态
show variables like 'slow_query%';
# 查看慢查询时间
show variables like 'long_query_time'

执行一条sql语句,模拟慢查询

select sleep(2)

我的慢查询日志名为SKY-20200921MQP-slow.log,那这个日志存放在哪里呢?
在这里插入图片描述

打开根目录下的bin文件夹下的my.ini,datadir就是日志存放的位置
在这里插入图片描述

打开日志,里面就能看到慢查询的sql语句
在这里插入图片描述

linux系统也是差不多的,具体操作可以看别人的博客,思想都是一样的

https://www.cnblogs.com/afeige/p/10896389.html


在【mysql根目录\data\数据库名】文件夹下,MYI文件为索引文件,MYD为数据结构文件,frm为表结构文件


执行计划

当我们需要了解SQL语句在数据库中是如何扫描表、如何使用索引时,可以使用MySQL提供的explain/desc命令输出执行计划

在sql语句前加explain

例子:
在这里插入图片描述
各个字段含义如下:

  • id :select 查询序列号

  • select_type:查询语句类型
    SIMPLE(简单SELECT,不使用UNION或子查询等)
    PRIMARY(查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY)
    UNION(UNION中的第二个或后面的SELECT语句)
    DEPENDENT UNION(UNION中的第二个或后面的SELECT语句,取决于外面的查询)
    UNION RESULT(UNION的结果)
    SUBQUERY(子查询中的第一个SELECT)
    DEPENDENT SUBQUERY(子查询中的第一个SELECT,取决于外面的查询)
    DERIVED(派生/衍生表的SELECT, FROM子句的子查询)
    UNCACHEABLE SUBQUERY(一个子查询的结果不能被缓存,必须重新评估外链接的第一行)
    UNCACHEABLE UNION(UNION查询的结果不能被缓存)

  • table:查询涉及的表或衍生表

  • type :判断索引的依据
    system(表中只有一条数据,相当于系统表; 这个类型是特殊的 const 类型
    const(主键或者唯一索引的常量查询,表格最多只有1行记录符合查询,通常const使用到主键或者唯一索引进行定值查询)
    ref(普通索引,此类型通常出现在多表的 join 查询, 针对于非唯一或非主键索引, 或者是使用了最左前缀规则索引的查询
    eq_ref: (除了system和const类型之外,效率最高的连接类型;唯一索引扫描,对于每个索引键,表中只有一条记录与之对应;常用于主键或唯一索引扫描)
    equ_ref(用于唯一索引查询,对每个索引键,表中只有一条或零条记录与之匹配)
    range(表示使用索引范围查询, 通过索引字段范围获取表中部分数据记录. 这个类型通常出现在 =, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, IN() 操作中)
    index(扫描索引树,如果索引是复合索引,并且复合索引列满足select所需的所有数据,则仅扫描索引树)
    ALL(全表扫描,没有任何索引可以使用时。这是最差的情况,应该避免)

  • possible_keys:指示MySQL可以从中选择查找此表中的行的索引。

  • key:MySQL查询实际使用到的索引。

  • key_len:表示索引中使用的字节数(只计算利用索引作为index key的索引长度,不包括用于group by/order by的索引长度)
    一般地,key_len 等于索引列类型字节长度,例如int类型为4 bytes,bigint为8 bytes;
    如果是字符串类型,还需要同时考虑字符集因素,例如utf8字符集1个字符占3个字节,gbk字符集1个字符占2个字节
    若该列类型定义时允许NULL,其key_len还需要再加 1 bytes
    若该列类型为变长类型,例如 VARCHAR(TEXT\BLOB不允许整列创建索引,如果创建部分索引也被视为动态列类型),其key_len还需要再加 2 bytes

  • ref:显示该表的索引字段关联了哪张表的哪个字段

  • rows:根据表统计信息及选用情况,大致估算出找到所需的记录或所需读取的行数,数值越小越好

  • filtered:返回结果的行数占读取行数的百分比,值越大越好

  • extra:包含不适合在其他列中显示但十分重要的额外信息。常见的值如下
    use filesort(MySQL会对数据使用非索引列进行排序,而不是按照索引顺序进行读取;若出现改值,应优化索引)
    use temporary(使用临时表保存中间结果,比如,MySQL在对查询结果排序时使用临时表,常见于order by和group by;若出现改值,应优化索引)
    use index(表示select操作使用了索引覆盖,避免回表访问数据行,效率不错)
    use where(where子句用于限制哪一行 )

索引

mysql优化的核心就得谈到索引

设计表中一般都会建立索引,提高查询效率,原理是b叉树的折半查找

索引分类

  • 主键索引
  • 唯一索引
  • 组合索引
  • 全文索引
  • 普通索引

主键索引:一种特殊的唯一索引,不允许有空值。primary key

添加主键索引例子1:

# 创建索引方式1
create table aaa1
( id int primary key);

添加主键索引例子2:

# 创建索引方式2
create table aaa
( id int);
# 设置成为主键
ALTER TABLE `aaa`
  ADD PRIMARY KEY (`id`);

普通索引:这是最基本的索引,它没有任何限制。

创建方式1:

ALTER TABLE users 
  ADD INDEX index_users( id )

创建方式2:

CREATE INDEX index_users 
  ON users (column_name)

唯一索引:与普通索引类似,不同的就是:索引列的值必须唯一,但允许有空值。

创建方式1:

ALTER TABLE users
  ADD UNIQUE ( id )

创建方式2:

CREATE UNIQUE INDEX index_users
  ON users(id)

注意:unique字段可以为NULL,并且可以有多个NULL,但是如果是具体内容则不能重复,不能有重复的空字符串‘’。


全文索引:FULLTEXT INDEX用于全文搜索。

试想在1M大小的文件中搜索一个词,可能需要几秒,在100M的文件中可能需要几十秒,如果在更大的文件中搜索那么就需要更大的系统开销,这样的开销是不现实的。

所以在这样的矛盾下出现了全文索引技术,有时候有人叫倒排文档技术。

只有InnoDB和 MyISAM存储引擎支持全文索引(仅适用于 CHAR, VARCHAR和 TEXT列)

创建方式:

ALTER TABLE users 
  ADD FULLTEXT ( id )

例子:

# 创建表
CREATE TABLE articles (
       id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY,
       title VARCHAR(200),
       body TEXT,
       FULLTEXT (title,body)
     )engine=myisam charset utf8;

# 插入数据
INSERT INTO articles (title,body) VALUES
     ('MySQL Tutorial','DBMS stands for DataBase ...'),
     ('How To Use MySQL Well','After you went through a ...'),
     ('Optimizing MySQL','In this tutorial we will show ...'),
     ('1001 MySQL Tricks','1. Never run mysqld as root. 2. ...'),
     ('MySQL vs. YourSQL','In the following database comparison ...'),
     ('MySQL Security','When configured properly, MySQL ...');

# 全文索引的错误用法,这样用不会生效
select * from articles where body like '%DataBase%'

# 正确用法
select * from articles where match(title,body) against ('DataBase')

实际项目中不会采用全文索引,InnoDB不支持全文索引

在mysql中全文索引只针对MyISAM生效(MyISAM是MySQL的默认数据库引擎)

一般用第三方搜索引擎框架如slor、es来代替全文索引

mysql自己提供的fulltext针对英文生效

处理中文使用方法是 match(字段名…) against(‘关键字’)

对一些常用词和字符,就不会创建,这些词,称为停止词.比如(a,b,mysql,the)

select match(title,body) against (‘database’) from articles;(输出每行和database关键字的匹配度)
在这里插入图片描述


组合索引:即一个索包含多个列

新增dept表:

create table dept(deptno int unsigned ,dname varchar(32),loc varchar(32))engine=myisam;

创建组合索引

alter table dept add index dname_loc (dname,loc);

注意:

  • 如果不是使用第一部分,则不会使用索引进行搜索

下面这条sql就不会使用到索引

explain select * from dept where loc='aaa'

而使用组合索引的第一部分会使用索引

explain select * from dept where loc='aaa'

在这里插入图片描述


注意:

  • 模糊查询 like语句有百分号开头索引会失效,而模糊查询 like百分号结尾索引又有效,所以建议不要使用like ‘%xxx’百分号开头的语句,会全表扫描

  • 如果条件中有or,要求使用的所有字段,都必须建立索引,才能用索引扫描,建议不使用or

  • 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来。否则不使用索引。也就是,如果列是字符串类型,就一定要用 ‘’ 把他包括起来

  • 如果mysql估计使用全表扫描要比使用索引快,则不使用索引。

  • 判断是否为空时,不要使用=null,要写is null才会用索引进行查找


索引的优缺点:

优点:提高程序效率

缺点:增加删除较慢,改变索引文件开销较大,增加内存占用

什么字段适合加索引?

查询次数比较多,值有很多不同,where后条件经常用到的字段


索引底层原理

在MySQL中,索引属于存储引擎级别的概念,不同存储引擎对索引的实现方式是不同的。

InnoDB引擎使用B+Tree作为索引结构

https://www.cnblogs.com/boothsun/p/8970952.html

B-tree(多路搜索树,并不是二叉的)是一种常见的数据结构。使用B-tree结构可以显著减少定位记录时所经历的中间过程,从而加快存取速度。按照翻译,B 通常认为是Balance的简称。这个数据结构一般用于数据库的索引,综合效率较高。

一棵m阶的B-Tree有如下特性:

  1. 每个节点最多有m个孩子。
  2. 除了根节点和叶子节点外,其它每个节点至少有Ceil(m/2)个孩子。
  3. 若根节点不是叶子节点,则至少有2个孩子
  4. 所有叶子节点都在同一层,且不包含其它关键字(键值)信息
  5. 每个非终端节点包含n个关键字信息(P0,P1,…Pn, k1,…kn)
  6. 关键字的个数n满足:ceil(m/2)-1 <= n <= m-1
  7. ki(i=1,…n)为关键字,且关键字升序排序。
  8. Pi(i=1,…n)为指向子树根节点的指针。P(i-1)指向的子树的所有节点关键字均小于ki,但都大于k(i-1)

B-Tree中的每个节点根据实际情况可以包含大量的关键字信息和分支,如下图所示为一个3阶的B-Tree:

在这里插入图片描述
B+Tree是在B-Tree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是用B+Tree实现其索引结构。

B+Tree相对于B-Tree有几点不同:

  1. 非叶子节点只存储键值信息。
  2. 所有叶子节点之间都有一个链指针。
  3. 数据记录都存放在叶子节点中。

将上一节中的B-Tree优化,由于B+Tree的非叶子节点只存储键值信息,假设每个磁盘块能存储4个键值及指针信息,则变成B+Tree后其结构如下图所示:
在这里插入图片描述
索引的缺点:B树在插入删除新的数据记录会破坏B-Tree的性质,在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持B-Tree性质,更新索引文件的时间开销增加。


SQL语句优化

一部分在索引的注意里提到了,还有一些sql优化事项

① 使用group by 分组查询是,默认做全表扫描,并且分组后,还会排序,可能会降低速度,
在group by 后面增加 order by null 就可以防止排序.
explain select * from emp group by deptno order by null;

② 有些情况下,可以使用连接来替代子查询。因为使用join,MySQL不需要在内存中创建临时表。
select * from dept, emp where dept.deptno=emp.deptno; [简单处理方式]
select * from dept left join emp on dept.deptno=emp.deptno; [左外连接,更好一些]

③ 对查询进行优化,要尽量避免全表扫描,首先应考虑在 where 及 order by 涉及的列上建立索引

④ 应尽量避免在 where 子句中对字段进行 null 值判断,否则将导致引擎放弃使用索引而进行全表扫描

最好不要给数据库留 NULL,尽可能的使用 NOT NULL 填充数据库.

备注、描述、评论之类的可以设置为 NULL,其他的,最好不要使用 NULL,也就是说要设定默认值。

不要以为 NULL 不需要空间,比如:char(100) 型,在字段建立时,空间就固定了, 不管是否插入值(NULL 也包含在内),都是占用 100 个字符的空间的,如果是 varchar 这样的变长字段, null 不占用空间。

⑤ 条件搜索where语句不要使用≥,而改用>,大于等于要做两次全表扫描

尽量避免where子句中使用!=,<>操作符,引擎将放弃使用索引进而全表扫描

⑥ 如果条件中有or,要求使用的所有字段,都必须建立索引,才能用索引扫描,建议尽量不使用or

⑦ 尽量不使用in 和 not in,也会进行全表扫描,索引都会失效

⑧ like字句不用%开头

⑨ 查询量十分大时,请使用缓存、分表、分页技术。


Mysql搜索引擎

MySQL数据引擎常使用的存储引擎有 myisam / innodb/ memory,其中innodb使用的最多

MyISAM 存储: 如果表对事务要求不高,同时是以查询和添加为主的,我们可以考虑使用myisam存储引擎. ,比如 bbs 中的 发帖表,回复表。

INNODB 存储: 有事务机制,保证数据一致性,对事务要求高,保存的数据都是重要数据,如订单表,账号表建议使用INNODB

Memory 存储:比如我们数据变化频繁,不需要入库,同时又频繁的查询和修改,我们考虑使用memory, 速度极快. (如果mysql重启的话,数据就不存在了)

MyISAM 和 INNODB的区别:

  1. 事务安全(MyISAM不支持事务,INNODB支持事务)
  2. 查询和添加速度(MyISAM批量插入速度快)
  3. 支持全文索引(MyISAM支持全文索引,INNODB不支持全文索引)
  4. 批量添加(MyiSAM效率高)
  5. 锁机制(MyISAM是表锁,innodb是行锁)
  6. 外键 MyISAM 不支持外键, INNODB支持外键. (在PHP开发中,通常不设置外键,通常是在程序中保证数据的一致)

各引擎区别总结
在这里插入图片描述


定时清理碎片化

如果数据库的存储引擎是myisam,请一定记住要定时进行碎片整理

举例:

# 创建表
create table test100(id int unsigned ,name varchar(32))engine=myisam;
# 插入数据
insert into test100 values(1,’aaaaa’);
insert into test100 values(2,’bbbb’);
insert into test100 values(3,’ccccc’);
# 成倍复制数据
insert into test100 select id,name from test100;
insert into test100 select id,name from test100;
insert into test100 select id,name from test100;
insert into test100 select id,name from test100;
insert into test100 select id,name from test100;
insert into test100 select id,name from test100;
# 执行前去查看MYD文件大小,和执行后MYD文件大小进行比对,发现是没有变化的
delete from test100 where id = 3;
# 我们应该定义对myisam进行整理
optimize table test100;

注意: 企业项目不会真正删除数据,而是做标识进行软删除。真正删除数据需要备份容灾,数据迁移。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值