MySQL高级篇来了!索引优化?分库分表?通通安排!!

个人学习笔记box 专栏收录该内容
25 篇文章 0 订阅

第二部分、MySQL高级

13、Linux安装MySQL

  1. 检查是否安装了MySQL

    rpm -qa|grep mysql
    
  2. 若有 使用rpm -e mysqlxxx或者yum remove mysql一一卸载

  3. rpm按顺序安装四个文件

    image-20200505201648800

    安装client时可能遇到这个报错

    image-20200708195937641

  4. 查看是否安装成功

    image-20200505202119143

  5. 启动Mysql服务

    systemctl start mysql # 启动服务
    systemctl stop mysql # 停止服务
    
  6. 然后获取临时密码(存放位置:/var/logs/mysqld.log中)

    cat /var/log/mysqld.log|grep password
    

    image-20200505210012333

  7. 使用临时密码登录后,进入首先需要修改密码

    SET PASSWORD = PASSWORD('newpassword'); 
    # 或者使用
    ALTER USER USER() IDENTIFIED BY 'newpassword';
    
  8. 默认的密码策略要求严格(在默认密码的长度最小值为 4 ,由 大/小写字母各一个 + 阿拉伯数字一个 + 特殊字符一个),可以进行修改降低

    set global validate_password_policy=LOW; # 将密码验证强度将为低
    set global validate_password_length=6; # 将密码长度设置为6
    
  9. 初次进行远程连接会被拒绝报错1130,是因为只允许localhost连接

    mysql>use mysql;
    
    mysql>select 'host' from user where user='root';
    
    mysql>update user set host = '%' where user ='root';
    
    mysql>flush privileges;
    
    mysql>select 'host'   from user where user='root';
    
  10. 开启服务开机自启

    systemctl enable mysqld # 设置自启
    
    ntsysv # 查看服务自启状态
    
    image-20200505212212381

14、配置文件

配置文件在Windows下是my.ini,在Linux中是 my.cnf位置:/etc/my.cnf

以下是默认配置

image-20200505212525080

路径解释
/var/lib/mysqlmysql数据文件的存放路径,datadir
/usr/share/mysql配置文件目录
/usr/bin/mysql*mysql命令存放目录
  1. 修改字符集编码

    mysql> show variables like '%char%'; # 查看字符集编码
    

    image-20200505215622870

    修改配置文件:

    # 在mysqld下添加
    character-set-server = utf8mb4
    collation-server = utf8mb4_general_ci
    # 在mysql下添加
    default-character-set = utf8mb4
    

    重新启动Mysql服务,然后重新建库,建表就可以使用中文了。

  2. 其他主要配置项、配置文件

    • 二进制日志:log-bin(主要用于主从复制)

    • 错误日志:log-error

      默认是关闭的,记录严重警告和错误记录,以及每次启动和关闭的详细信息。

    • 查询日志

      默认关闭,记录查询的SQL语句,开启会降低Mysql的整体性能,有利于我们排查慢SQL;

    • 数据文件

      Windows系统:安装目录下的data文件夹中存放数据库的数据文件

      Linux:/var/lib/mysql 目录下,使用ls -lF|grep ^d目录即可查看所有的库

      MyIASM引擎:

      • .frm:表结构

      • .myd:表数据

      • .myi:表索引

        image-20200509222823432

      InnoDB引擎:由于InnoDB的索引和其数据文件是一体的,所以只有两个文件

      • .frm(在Mysql8.0+后移除)

      • .ibd(数据/索引文件)

        image-20200509222744059

15、MySQL Server分层

image-20200509224414960
  • 连接层
  • 服务处
  • 引擎层
  • 存储层

Mysql是可拔插的,可高度定制化的。在应对不同场景时发挥稳定。

插件式的存储引擎架构将查询处理举额其它的系统任务以及数据的存储提取相分离。

可以根据业务的需求和实际情况来选择合适的存储引擎。

16、MySQL引擎

mysql中show engines可以查看所有的引擎,以及当前默认使用的engine

image-20200509225419795

show variables like ‘%storage_engine%’可以获取当前使用存储引擎的信息

image-20200509225650262

MyISAM和InnoDB对比

对比项MyISAMInnoDB
外键不支持支持
事务不支持支持
行/表锁表锁,即使操作一条记录,就会锁住整张表。不适合高并发操作。行锁,操作时只锁定某一行,对其他行不影响,适合高并发操作
缓存只缓存索引,不缓存真实数据。不仅缓存索引,还缓存真实数据,对内存要求较高,内存大小对性能有决定性影响。
表空间大(由于同时存放了索引和数据)
侧重点性能,适合用于查找事务
默认安装YESYES

Alibaba使用的MySQL

  • Percona为MySQL数据库服务器进行了改进,在功能和性能上较MySQL有着很显著的提升。该版本提升了在高负载情况下的InnoDB的性能,衍生出来一种新的存储引擎:XtraDB.
  • XtraDB完全可以代替InnoDB,并且它在高并发和性能方面表现更加出色。
  • 阿里巴巴大部分MySQL数据库其实就是使用的Percona的原型(XtraDB)并加以修改。
  • AliSQL+AliRedis 已经进行了开源

17、索引优化分析

慢SQL形成的原因

  • SQL写的烂

  • 索引失效(建了索引,但是由于SQL语句不合理索引没用上)

    创建索引:

    # 单值索引
    create idx_user_name on `user`(name); 
    
    # 联合索引
    create (idx_user_name, idx_user_age) on `user`(name,age);
    

    删除索引

    drop index [index_name] on `table_name`
    

    查看索引

    infomation_schema是MySQL的元数据库,所有的元数据都存放在这个库中,其中statistics表存放着所有索引的相关信息。

    show index from `table_name`
    
  • 关联查询(join)太多

  • 服务器调优及各种参数的设置(缓冲,线程数等)

机器执行SQL的顺序:

from `table_name`
on 'join_condition'
join_type join `join_table`
where 'select_condition'
group by 'group_by_list'
having 'having_condition'
select
distinct `select_list`
order by 'order_by_condition'
limit 'offset,rows'

img

索引选择

  1. 复合索引的选择优先于单值索引

  2. 每张表的索引数量不超过5个

  3. 索引并不是越多越好,索引也会占用空间!

    • 推荐创建索引的情况

      1. 主键自动建立索引(InnoDB自动创建)
      2. 频繁作为查询条件的字段建立索引
      3. 查询中与其他表关联的字段(外键关系)建立索引
      4. 查询中排序的字段(Order BY),建立索引会大大调高排序的速度
      5. 查询中统计或者分组的字段(Group BY)
    • 不推荐建立索引的情况

      1. 频繁修改的字段不推荐建立索引

      2. 表记录较少(<2000)

      3. 频繁增删改的字段

      4. 选择性不高的字段

        选择性:列数据的基数/列数据的总记录数 (0,1]

性能分析

MySQL Query Optimizer(查询优化分析器)

MySQL服务内置,通过计算分析系统中收集到的统计信息,自动优化SQL语句达到MySQL认为最优的执行计划。(但是不一定是DBA认为的最优)

Explain关键字

可以模拟优化器执行SQL查询语句,从而知道MySQL是如何处理我们的SQL语句的。方便我们对SQL语句性能进行分析。

image-20200510222455343

使用这个关键字能获取那些信息?

  • 表的读取顺序
  • 数据读取操作的操作类型
  • 那些索引可以被使用
  • 用到了那些索引
  • 表之间的引用
  • 每张表多少行被优化器查询

性能分析表字段解析

  • id:

    select查询的序列号,包含一组数字,表示查询中执行select子句或操作表的顺序。

    • id相同:表示执行顺序从上到下依次执行。
    • id不同:id递增,一般常见在嵌套子查询,之间有父子关系,id越大执行优先级越高,越先被执行。
    • id相同、不同同时存在:结合前两条的规则,先按照优先级执行,然后相同id再顺序执行。
  • select_type

    查询的类型

    1. SIMPLE: 查询语句中不包含子查询和union
    2. PRIMARY: 当查询中包含若干子查询,最外层的查询则为PRIMARY
    3. SUBQUERY: 在Select和Where列表中包含了子查询,或子查询本身
    4. DERIVED: 在from列表中包含的子查询,被标记未DERIVED(衍生),MySQL会递归执行这些子查询,将结果放在临时表中。
    5. UNION: 若第二个Select出现在union之后,会被标记为UNION, 若union出现在from列表的子查询中,外层select会被标记会DERIVED.
    6. UNION_RESULT: 从union表获取结果的select会被标记为UNION_RESUL
  • table

    执行过程中查询了哪张表。

  • type

    查询访问表的访问类型

    从优到劣:

    system > const > eq_ref > ref > range > index > ALL

    优化至少达到range

    • system: 表只有一行记录(相当于系统表),是const的特例。
    • const: 表示通过一次索引就定位到数据,用于比较primary key或者unique索引。因为只匹配一行数据,速度很快。如将主键置于where条件中,MySQL就可以将查询转化为一个常量。
    • eq_ref: 使用索引,且索引的每个key,有且仅有唯一一条记录与之匹配,常见于primary key和unique索引。
    • ref: 非唯一性索引扫描,使用索引,但是满足条件的可能是多条记录,属于扫描和查找的结合体。
    • range: 只检索给定范围的行,使用一个索引来选择行。常见于between、in、<、>查询。
    • index: 全索引扫描。虽然和ALL一样是读全表,但是index只扫描索引树,速度要比ALL快。
    • ALL: 遍历全表。(记录较大时,ALL需要进行优化)
  • possible_key

    列出所有可能用到的索引。

  • key

    实际使用的索引。NULL:未使用索引

    若查询中使用了覆盖索引,则该索引仅出现在key列表中。

    • 通常开发人员会根据查询的where条件来创建合适的索引,但是优秀的索引设计应该考虑到整个查询。其实mysql可以使用索引来直接获取列的数据。**如果索引的叶子节点包含了要查询的数据,那么就不用回表查询了,也就是说这种索引包含(亦称覆盖)所有需要查询的字段的值,我们称这种索引为覆盖索引。**即当我们的select列表中的字段包含在了索引的字段中,就会用到覆盖索引。

      注意:如果要使用覆盖索引,一定要保证查询选择字段是能够被索引的字段覆盖的而避免使用select *;但是如果将所有字段一起创建索引则会导致索引文件变大,同时降低了查询性能。

  • ken_len

    表示使用中使用的字节数,可以通过该列计算查询中使用的索引长度。在不损失精确性的情况下,长度越小也好。

    key_len显示的是值为索引字段的最大可能长度,并非实际使用的长度,是通过表定义计算得出的,而不表检索得出的。

  • ref

    索引的哪一列被使用,显示哪些列或者常量被使用查找索引上的key

    const:表示是常量,一般出现在条件中等值匹配一个定值时,例如 where stu.name=‘zs’;

  • row

    根据表统计信息及索引的选用情况,大致估算出定位数据需要读取的记录行数。越小越好

  • Extra

    一些额外信息:

    • Using filesort(危险):表示mysql对数据使用了一个外部的索引排序,而不是按照表内的索引顺序进行读取。==MySQL中无法利用索引完成的排序称作文件内排序==一般出现在使用了order BY和groupBY的查询中。
    • Using temporary(十分危险):表示建立了临时表保存中间结果。常见于order BY和group BY.

    所以在使用group by时,尽量保证groupBY的字段与索引的字段保持相同顺序和数量。

    • USING index:表示查询语句中用到了覆盖索引,减少了回表操作,速度和效率提高。
      • 如果同时出现了using where 表示索引被用于执行索引键值的查找。
      • 如果没有出现using where 表示索引用于读取数据而非执行查找动作。常见于没有where条件的
    • Using where: 使用了where条件语句
    • Impossible where: where条件存在逻辑错误。

18、索引性能测试

如何快速进行批量数据的创建?

  • Java多线程插入
  • 批量数据脚本(SQL编程)

自建函数、存储过程

CREATE TABLE `dept`
(
    `id`       INT(11) not NULL auto_increment,
    `deptName` VARCHAR(30) DEFAULT NULL,
    `address`  VARCHAR(40) DEFAULT NULL,
    ceo        int     NULL,
    PRIMARY KEY (`id`)
) ENGINE = INNODB
  auto_increment = 1
  DEFAULT charset = utf8;

CREATE TABLE `emp`
(
    `id`    INT(11) not NULL auto_increment,
    `empno` INT     not NULL,
    `name`  varchar(20) default null,
    `age`   int(3)      default null,
    deptId  int(11)     default null,
    primary key (`id`)
#     constraint `fk_dept_id` foreign key (deptId) references `dept`(id);
) engine = INNODB
  auto_increment = 1
  default charset = utf8;

# mysql的二进制日志 默认关闭
show variables like 'log_bin_trust_function_creators';
# 全局开启
set global log_bin_trust_function_creators = 1;

# 随机生成字符串
delimiter $$
create function rand_string(n int) returns varchar(255)
begin
    declare char_str varchar(100) default
        'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
    declare return_str varchar(255) default '';
    declare i int default 0;
    while i < n
        do
            # 每次取出一个字母拼接到返回字符串中
            set return_str = concat(return_str, substr(char_str, 1 + floor(RAND() * 52), 1));
            set i = i + 1;
        end while;
    return return_str;
end $$

# 随机生成编号
delimiter $$
create function rand_num(from_num int, to_num int) returns int(11)
begin
    declare return_num int(11) default 0;
    set return_num = from_num + floor(RAND() * (to_num - from_num + 1));
    return return_num;
end $$

# 设置用户变量进行测试
set @test_str = rand_string(13);
select @test_str;
# 删除用户变量
set @test_str = null;

# 删除函数:drop function rand_string;

# 创建存储过程 创建员工数据
delimiter $$
create procedure insert_emp(start int, max_num int)
begin
    # 计数器
    declare count int default 0;
    # 关闭自动提交
    set AUTOCOMMIT = false;
    repeat
        set count = count + 1;
        # 插入数据
        insert into emp(empno, name, age, deptId)
        VALUES (start + count,
                rand_string(6),
                rand_num(20, 50),
                rand_num(1, 10000));
    until count = max_num end repeat;
    # 提交
    commit;
end $$

# 创建存储过程 生成部门信息
delimiter $$
create procedure insert_dept(max_num int)
begin
    declare count int default 0;
    set autocommit = false;
    repeat
        set count = count + 1;
        insert into dept(deptName, address, ceo)
        VALUES (rand_string(8),
                rand_string(10),
                rand_num(1,500000));
    until count=max_num end repeat;
    commit;
end $$

delimiter ;
call insert_dept(10000);
call insert_emp(10000,500000);

# 清空表的数据,并重置自增列
truncate table emp;

# 查看自增计算器
select AUTO_INCREMENT
from information_schema.TABLES
where TABLE_NAME = 'dept';

游标使用,删除表的除主索引以外的其他索引

# 创建存储过程,删除表的全部索引
delimiter $$
create procedure proc_drop_index(dbname varchar(200), tablename varchar(200))
begin
    declare done int default 0;
    declare ct int default 0;
    declare _index varchar(200) default '';
    # 创建游标
    declare _cur cursor for (
        select index_name
        from information_schema.STATISTICS
        where TABLE_SCHEMA = dbname
          and TABLE_NAME = tablename
          and SEQ_IN_INDEX = 1
          and INDEX_NAME <> 'PRIMARY'
    );
    declare continue handler for not found set done = 2;
    open _cur;
    # 获取当前游标指向的索引名
    fetch _cur into _index;
    while _index <> '' do
        # 拼接sql
        set @sql = concat('"drop index ',_index,' on ',tablename);
        # sql预编译
        prepare real_sql from @`sql`;
        execute real_sql;
        deallocate prepare real_sql;
        set _index = '';
        fetch _cur into _index;
    end while;
    close _cur;
end $$;

索引正确使用(最左前缀原则)

场景一:全列匹配

对索引的字段全匹配,且都使用等值匹配(‘=’或 in) Y

# 创建 age+deptId 的复合索引
create index idx_age_deptId on emp(age,deptId);
/*
 使用索引前:execution: 103 ms, fetching: 21 ms
 使用索引后:execution: 5 ms, fetching: 24 ms
 */
explain select sql_no_cache * from emp where age = 30 and deptId = 9988;

image-20200513164633152

# 使用 in 也是可以使用到索引的
explain select sql_no_cache * from emp where age in (20,21,22,23,24) and deptId = 9988;

image-20200513164658878

当where条件中字段有能够完全匹配索引的字段的时候,可以使用索引。

这些条件的顺序可以随意调整,mysql的优化器会为我们自动优化顺序然后选择最佳的索引。

场景二、最左前缀匹配

where条件中有一个或几个字段使用了索引中最左边开始一个或连续多个字段时会使用部分索引

注意:索引创建的顺序是可以自定义选择的。

# 场景二: 最左前缀匹配
create index idx_empno_deptId_age on emp(empno,deptId,age);

# empno deptId 匹配 索引的最左前缀  可以使用部分索引 
explain select * from emp where empno = 123213 and deptId = 4235;

image-20200513170140098

这里可以结合场景一,其实全匹配只是最左匹配的一种特殊情况。

遗留问题:如果查询条件中跳过索引中的字段还能不能用索引呢?

场景三、查询条件使用了索引的最左前缀的部分字段(中间条件缺失)

where条件子句中,使用到了索引的部分字段,但是中间有字段缺失,只有部分使用索引

create index idx_empno_deptId_age on emp(empno,deptId,age);

explain select sql_no_cache * from emp where empno = 123123 and age in (23,24,25,26);
explain select sql_no_cache * from emp where empno = 123123 ;

image-20200513171905808

这两行的性能分析结果相同,索引也就只被用于empno字段。

分析:

单一个empno字段是可以满足最左前缀的条件的,但是empno+age(中间掉了deptId无法满足最左前缀),所以where条件中最长的最左前缀就是empno,所以只有empno用到了索引。

解决办法:

  • 建empno+age的辅助索引

  • 将中间缺失的条件填坑

    填坑方式一般采用in,当缺失条件的字段可选值很少的时候,使用in对字段做指定范围匹配,查询的type会变为range

    explain select sql_no_cache * from emp where empno = 100085 and deptId in (2342,4324,4324) and age in (23,24,25,26);
    

    这样就既能保证全表索引,还能兼顾索引效率

    当可选值很多的时候,就会使得in中的需要填写的值变多,此时最好还是使用建辅助索引吧

场景四、查询条件没有使用索引字段的最左列(第一列)

当查询的条件中,直接略过索引的最左字段,这样是不满足最左前缀原则的,所以不会使用索引

# 场景四:不使用最左前缀
explain select sql_no_cache * from emp where deptId = 4455 and age in (23,24,25,26);

image-20200513174019803

场景五、匹配某列的字符串前缀

当再使用模糊查询时候,使用字符串通配符时,通配符不出现在字符串的开头(即匹配字符串的前缀),可以使用索引

create index idx_name on emp(name);
# 场景五 字符串前缀匹配
explain select sql_no_cache * from emp where name like 'aa%';
explain select sql_no_cache * from emp where name like 'aa_';

image-20200513231330855

例如这样的字符串前缀匹配,都是可以使用索引的。

# 反例
explain select sql_no_cache * from emp where name like '_aa%';
explain select sql_no_cache * from emp where name like '%aa%';

image-20200513231532499

这种开头就使用通配符的模糊查询是无法使用索引的噢

场景六、使用范围查询

当查询条件中用到了范围查询(<、>等),索引中第一个使用范围查询字段之后的所有字段都不能使用索引。

create index idx_empno_deptId_age on emp(empno,deptId,age);
# 场景六 使用范围查询
explain select sql_no_cache * from emp where empno = 123123 and deptId < 5566 and age in (23,24,25,26,27);
explain select sql_no_cache * from emp where empno = 123123 and deptId < 5566

这两条sql的性能分析结果使用的索引长度的一致的,也就证实了 age字段并没有使用索引,两次的查询type都是range也可以进一步证明。

image-20200513232250757

注意点:

虽说是范围查询但是between…and… 和 in不在范畴内噢

# 全字段使用索引
explain select sql_no_cache * from emp where empno = 123123 and deptId between 2233 and 4455 and age in (23,24,25,26,27);

解决方案:

  1. 在创建索引之前分析,将有可能使用范围查询的字段放在索引的末端。

场景七、查询条件中含有表达式或者函数

一旦查询条件中出现了表达式、函数会使得索引失效,慎重!

# 查询条件使用表达式 索引失效
explain
select sql_no_cache *
from emp
where empno + 1 = 288200;

# 查询使用函数 索引失效
explain
select sql_no_cache *
from emp
where left(name, 3) like 'gyc';

image-20200513233628542

提醒:不等于(<>、!=)也是表达式的一种,使用后也会是索引失效,同样 xx is not null也会使得索引不可用,而is null却可以

场景八、查询条件发生数据类型转换

当查询条件中出现数据类型的转换,可能导致索引失效。

# 场景八、数据类型转换 
# int转varchar 索引失效
explain
select sql_no_cache *
from emp
where name = 123424;

# varchar 转 int 索引可用
explain
select sql_no_cache *
from emp
where empno = '123424';

int转varchar:索引失效image-20200513234310948

varchar转int :索引可用

image-20200513234418508

场景九、使用了groupby

知识补充:

当我们使用select * 时使用groupby 会发生报错,关于ONLY_FULL_GROUP_BY。

MySQL 5.7.5及以上功能依赖检测功能。如果启用了ONLY_FULL_GROUP_BY SQL模式(默认情况下),MySQL将拒绝select列表,HAVING条件或ORDER BY列表的查询引用在GROUP BY子句中既未命名的非集合列,也不在功能上依赖于它们。

ONLY_FULL_GROUP_BY的意思是:对于GROUP BY聚合操作,如果在SELECT中的列,没有在GROUP BY中出现,那么这个SQL是不合法的,因为列不在GROUP BY从句中,也就是说查出来的列必须在group by后面出现否则就会报错,或者这个字段出现在聚合函数里面。

explain
select deptId, name
from emp
where name like 'fa%'
group by deptId, name

MySQL要求如果使用groupBY,那么select中的列,必须全部出现在groupBy中,虽然可以使用索引,但是会触发Using Tempoary和Using Filesort 而在前面的性能分析结果表的字段分析中,也提到这样个额外信息的出现表示我们的sql是需要进行优化的。

19、使用索引建议

  1. 在创建索引时,我们尽量选择那些选择性较高的组合或者列。

    关于选择性(selectivity)

    计算选择性:使用列/组合列 不重复的值的数量 ÷ 列/组合列 的总记录数

    一般类似ID这种全局唯一的字段,选择性为1,

    而例如sex这个字段 不重复的值只有两个,随着总记录数的增加,选择性也会降低。age也是如此,不重复的值顶多120个,而随着记录数的增加也会随之降低。

    但是如果将(sex,age)进行组合,选择性又会有些许提高。

    有一种与索引选择性相关的索引优化策略:前缀索引,使用列中的值的前缀作为索引的key,选择性接近全值列索引,且减少了空间的开销,同时兼顾了速度。

  2. 在使用组合索引时,将选择性高的字段放在前面,便于提高过滤效率,提高查询速度。

  3. 选择组合索引时,在保证索引尽可能短的同时,尽量多包含where中可能用到的字段。

  4. 选择组合索引时,考虑使用范围查询的字段,放在索引的后面位置,避免使其他字段索引失效。

  5. 注意SQL编写,结合以上九个场景高效使用索引。

20、多表联合查询优化

当我们使用多表关联查询时,有连接发起表,和连接表。在执行过程中它们担任着驱动表和被驱动表的角色(取决于连接表的方式left join、right join、inner join);

当我们两张表都不创建索引的时候

explain select * from class right join book on class.card = book.card;

image-20200516133821469

两张表都是使用全表扫描,相当于做笛卡尔积然后筛选取出符合条件的记录。效率十分低,此时执行过程可以表示为:

image-20200516133122636

首先是驱动表中确定一个记录,然后对被驱动表全表扫描,直到对驱动表完成全表扫描。可以看出对驱动表的全表扫描是无法避免的,但是我们可以想办法避免对被驱动表进行全表扫描。

此时我们要分清我们写的联表查询SQL中哪个表是驱动表,哪个是被驱动表。

# left join 
# 连接发起表是驱动表即 A
# 连接表是被驱动表即 B
A left join B on xxx

# right join 与left join刚好相反
# 连接表是驱动表即 B
# 连接发起表是被驱动表即 A
A right join B on xxx

# inner join 稍后再议

我们以左连接为例子,创建索引

注意:创建索引的字段应该是两表的关联字段

按照我们刚才的理想优化方式,给book(也就是我们的被驱动表)的card字段建上索引

create index idx_book_card on book(card);

explain select * from class right join book on class.card = book.card;

执行结果:

image-20200516133728637

明显看出当被驱动表使用索引后,减少了扫描的行数,执行速度也会相应提升,此时执行过程可以表示为:

image-20200516134126547

此时虽然对于驱动表的全表扫描无法避免,但是在对被驱动表进行查询时候使用索引,大大加快了数据定位的速度。那如果我们对驱动表也加上索引呢?

在被驱动加上索引的基础上为驱动表也加上索引

create index idx_class_card on class(card);

explain select sql_no_cache * from class left join book on class.card = book.card;

image-20200516134443437

从结果中可以看到,即使是使用了索引,扫描的行数依旧没有变,相对于之前没有实质性的变化,也就进一步证明对驱动表的全表扫描是无法避免的

再来看看inner join

image-20200516135948369

起初我们两张表都有索引,所以默认以inner join的发起表(class)作为驱动表。book 作为被驱动表。

当我们把book表上关联字段的索引去掉以后,神奇的事情发送了——此时我们的class变为了被驱动表。

这在我们使用left/right join时是不可能发生的,驱动表是被驱动表是人为写好的, 所以当我们去掉被驱动表的索引后,就是这种情况:

image-20200516140324862

这就涉及到MySQL自动优化的机制:

当使用inner join时候,驱动表与被驱动表并不是指定不变,MySQL会选取关联字段有索引的表作为被驱动表,而没有索引的表也就自动变为驱动表,以此来提高效率。

当都有关联字段索引的时候,默认前表是驱动表。

当两张表关联字段都没有索引,选择表记录少的表作为驱动表。

联表查询使用子查询

那么到这里我们要注意一个问题:当我们的联表查询中出现了子查询的时候,就要考虑一下子查询的位置,使用子查询会出现多趟查询,(MySQL5.7对部分子查询做了优化,有时即使使用子查询也显示一趟完成,即Id列全相同)。

由于我们使用子查询得到的结果是存放在一张虚表里面,虚表是无法建立索引的!所以尽量将子查询放在驱动表的位置,将被驱动表的位置留给可以创建索引的表。

此外如果能使用直接关联多表完成的查询尽量使用直接关联,不用子查询!

21、子查询优化

尽量不要使用not in 或者 not exists可以使用left join on xxx代替

几个SQL练习(使用之前创建的dept和emp表的缩小版t_dept和t_emp)

  1. 列出自己CEO年龄比自己小的人员

    # 优化前
    explain
    select *
    from t_emp e1
    where e1.age > (
        select e2.age
        from t_emp e2,
             t_dept d
        where e2.empno = d.ceo
          and e1.deptId = d.id
    );
    
    # 优化后
    explain
    select e1.name '员工姓名', e1.age '员工年龄', e2.name 'CEO姓名', e2.age 'CEO年龄'
    from t_emp e1
             left join t_dept d on e1.deptId = d.id
             left join t_emp e2 on d.ceo = e2.empno
    where e1.age > e2.age;
    
  2. 列出所有年龄低于自己部门平均年龄的人员

    explain
    select e1.name '员工姓名', e1.age '员工年龄', d.avg_age '部门平均年龄'
    from t_emp e1
             inner join
         (
             select deptId, avg(age) avg_age
             from t_emp e2
             where e2.empno is not null
             group by e2.deptId
         ) d
         on e1.deptId = d.deptId
    where e1.age < d.avg_age;
    
    # 进行优化
    # 1. 给 groupby的字段增加索引 
    # 2. 由于使用了 联表查询,且其中有一个虚拟表,应该将其防止在驱动表的位置,要对e1表中 deptId、age创建索引,根据索引建立规则 索引顺序是:deptId+age
    create index idx_deptid on t_emp (deptId);
    create index idx_deptid_age on t_emp (deptId, age);
    
  3. 列出至少有2个年龄大于40岁成员的部门

    #优化前
    explain
    select *
    from t_dept d
    where (
              select count(*)
              from t_emp e
              where e.deptId = d.id
                and e.age > 40
          ) > 2;
    
    # 优化后
    explain
    select d.id 部门ID, d.deptName 部门名, count(*) '年龄大于40的员工数'
    from t_emp e1
             inner join t_dept d
                        on e1.deptId = d.id
    where e1.age > 40
    group by d.id, e1.age
    having count(*) > 2;
    
    # 继续优化
    # 由于inner join中dept 受主键索引的影响被选为被驱动表,但是实际上两张表都没有索引的情况下,应该选择记录数较少的 dept表作为驱动表。
    
    # 使用 straight_join 指定固定的驱动表和被驱动表,其他地方和inner join相同,将记录少的dept表放到前面做驱动表,emp做被驱动表,然后给被驱动表建索引
    explain
    select d.id '部门ID', d.deptName '部门名', count(*) '年龄大于40的员工数'
    from t_dept d
             straight_join  t_emp e1
                        on e1.deptId = d.id
    where e1.age > 40
    group by d.deptName ,d.id
    having count(*) > 2;
    
    # 创建索引
    create index idx_deptId_age on t_emp (deptId, age);
    create index idx_deptName on t_dept (deptName);
    
  4. 列出至少有2个非CEO成员的部门

    # 优化前
    explain
    select d2.deptName, d2.id, count(*)
    from t_emp e1
    inner join t_dept d2 on e1.deptId = d2.id
    left join t_dept d on e1.empno = d.ceo
    where d.id is null
    group by d2.deptName, d2.id
    having count(*) >= 2;
    
    # 优化
    #1.还是inner join中由于d2 中id的主键索引被选择称为了被驱动表,使用straight_join手动设置
    select d2.deptName, d2.id, count(*)
    from t_dept d2
    straight_join t_emp e1 on d2.id = e1.deptId
    left join t_dept d on e1.empno = d.ceo
    where d.id is null
    group by d2.deptName, d2.id
    having count(*) >= 2;
    # 现在将记录数少的 d2选为了驱动表,接下来就是关于索引的创建
    
    # groupby中 deptName 需要创建一个索引
    create index idx_deptName on t_dept (deptName);
    # 在straight_join中 e1是被驱动表 关联字段deptId要建索引
    create index idx_deptId on t_emp (deptId);
    # 在left join中 d是被驱动表,所以关联字段ceo要创建索引
    create index idx_ceo on t_dept (ceo);
    
  5. 列出全部成员,并增加一列备注是否为CEO(是:不是)

    # 条件语句的  两种写法
    explain
    select e.empno '员工ID', e.name '员工姓名', if(isnull(d.id),'否','是') '是否为CEO'
    from t_emp e
             left join t_dept d on e.empno = d.ceo;
    
    explain
    select e.empno '员工ID', e.name '员工姓名', (case when d.id is null then '否' else '是' end) '是否为CEO'
    from t_emp e
             left join t_dept d on e.empno = d.ceo;
             
    # 优化
    # 既然出现了关联查询,那么就可以对被驱动表的关联字段建立索引
    create index idx_ceo on t_dept (ceo);
    
  6. 列出所有部门,并增加一列 ’老鸟or菜鸟‘ 若部门中平均年龄 >50 为老鸟,其余为菜鸟

    explain
    select d.deptName '部门名', d.id '部门id', avg(e.age) '部门平均年龄', if(avg(e.age)>35,'老鸟','菜鸟') '老鸟or菜鸟'
    from t_dept d
    inner join t_emp e on d.id = e.deptId
    group by d.deptName, d.id;
    
    # 同样的优化手段,先调整好驱动表和被驱动表,然后增加索引
    # 直接通过增加索引变换 两表的角色
    create index idx_deptid on t_emp (deptId);
    # 为groupby 字段增加索引
    create index idx_deptnam on t_dept (deptName);
    
  7. 显示每个部门年龄最大的员工

    explain
    select e2.deptId '部门id', e2.name '员工姓名', e2.empno '员工编号', e3.maxage '年龄'
    from t_emp e2
             inner join (
        select e1.deptId, max(e1.age) maxage
        from t_emp e1
        where e1.deptId is not null
        group by e1.deptId
    ) e3
    on e3.deptId = e2.deptId and e2.age = e3.maxage;
    
    # 出现了联表查询 并且联表是子查询 话不多说子查询放驱动表, 被驱动表项建上索引
    create index idx_deptid_age on t_emp (deptId, age);
    

22、排序分组优化

OrderBy

前面在索引优化分析中的索引选择中提到,对OrderBy中的字段建立索引会大大调高排序速度!

没有索引的情况下:

image-20200516163615882

分析结果中出现了Using fileSort表示MySQL并没有使用我们的索引进行排序,而是自建一个外部索引进行排序,所以我们应当避免出现Using fileSort,尽可能让MySQL使用我们自己建立的索引,以下分析几种情况说明如何避免出现Using fileSort

情况一:使用了过滤条件 VS 未使用

首先我们创建索引

create index idx_age_deptid on emp (age, deptId);

执行未使用过滤的查询:

explain
select sql_no_cache *
from emp
order by age, deptId;

image-20200516163834417

结果没有变化,我增加一个过滤条件:

explain
select sql_no_cache *
from emp
where age = 34
order by age, deptId;

image-20200516164433724

继续使用其他条件:

explain
select sql_no_cache *
from emp
where deptId = 2244
order by age, deptId;

image-20200516164732011

有点问题,明明使用了条件过滤为什么还是出现了Using filesort,再尝试:

explain
select sql_no_cache *
from emp
where age = 34 and deptId = 2244
order by age, deptId;

image-20200516164937075

奇了怪,现在又消失了(待会再说),然后我们尝试以下limit:

explain
select sql_no_cache *
from emp
order by age, deptId
limit 30;

image-20200516164624495

综合上面五次尝试:可以总结出一个大概的规律:

当使用了条件过滤时就可以消灭掉Using filesort使用我们自己的索引

这里的过滤条件就包括了

  • where查询条件过滤
  • limit分页查询过滤

但是对于where查询条件好像并非所有的都可以,个人猜测规律:

当where的条件中但凡有一个字段用到了索引(为排序字段的建立的索引)就能消除Using filesort,反之如果条件所有的字段都没有用到索引势必出现Using filesort。

奇怪现象:

因为我们索引是(age,deptid),那么age单个字段条件查询肯定是能够用上索引的,所以没有出现Usingfilesort

而单个deptid 是不满足索引的左前缀原则的,所以没有用上索引,所以出现了Usingfilesort

情况二:OrderBy中字段顺序与索引顺序不同,或者出现了非索引字段

一样我们先创建索引:

create index idx_age_deptid_name on emp (age, deptId, name);

先测试执行顺序相同的:

explain
select sql_no_cache *
from emp
where age = 34
order by age, deptId, name;

image-20200516171951844

没有出现,我来调整一下顺序:

explain
select sql_no_cache *
from emp
where age = 34
order by age, name, deptId;

image-20200516172101960

它来了它来了,我们还原顺序,然后使用一个非索引字段替换其中一个

explain
select sql_no_cache *
from emp
where age = 34
order by age, deptId, empno;

image-20200516172313347

还是一样,我们再还原,减去其中一个字段:

# 情况一
explain
select sql_no_cache *
from emp
where age = 34
order by deptId,name;

#情况二
explain
select sql_no_cache *
from emp
where age = 34
order by age,name;

# 情况三
explain
select sql_no_cache *
from emp
order by deptId, name
limit 20

情况一image-20200516172553492

情况二

image-20200516172814038

情况三

image-20200516173549116

结合上面的测试结果总结:

  1. OrderBy中的字段一定要保证和索引创建时的字段顺序相同。

    可能会疑问,前面索引不是说MySQL可以帮我们调整吗?但是自动调整的前题是查询结果不会变化,OrderBy中字段顺序变化就可能会引起结果顺序变化,但是也不是绝对的,比如如果列的值是统一的话,还是可以为我们进行优化的

    explain
    select sql_no_cache *
    from emp
    where age = 34
    order by deptId, name, age;
    # 这里age 固定是34! 所以age的位置变化不会导致结果变化,所以结果就是:
    

    image-20200516174752149

  2. OrderBy中字段最好也保证索引字段的最左前缀原则。

  3. 索引字段中部分字段出现在where条件中且是等值查询,也可以等效出现在OrderBy中,然后结合第二条(示例:最后一次测试的三种情况)

  4. OrderBy中不要出现非索引字段。

这里对于第三条额外解释一下:

# 我们的索引顺序是 age + deptId + name

explain
select sql_no_cache *
from emp
where age = 34
order by deptId,name;
# 

以上这种情况 没有出现 Using filesort 原因就是age作为了where条件且是等值查询,(换成范围查询也会出现Usingfilesort,但是可以解决)如果将age加入orderby中,age这一列也是固定的34,所以这种where的等值判断的字段可以视为加入了orderby中,也就符合第二条规则。

前面说范围查询,出现Using filesort,解决方式就是:显式将此字段按顺序加到OrderBy中就可以:

explain
select sql_no_cache *
from emp
where age = 34 and deptId in (2323,2422) # deptId 使用了范围查询
order by deptId,name; # 显式加入OrderBy中

image-20200516175129112

情况三:同升/降序 VS 混合使用

还是使用idx_age_deptid_name作为索引

由于默认OrderBy是使用升序(ASC),可以直接跳过

explain
select sql_no_cache *
from emp
where age = 34
order by age ASC , deptId ASC , name;

explain
select sql_no_cache *
from emp
where age = 34
order by age DESC , deptId DESC , name DESC ;

同升序image-20200516175933721

同降序image-20200516180319904

将其中一个改为降序,就变为了升降序混合使用

explain
select sql_no_cache *
from emp
where age = 34
order by age DESC , deptId , name;

image-20200516180033671

难道混合使用没有影响?我们再加一个降序试试:

explain
select sql_no_cache *
from emp
where age = 34
order by age DESC , deptId DESC , name;

image-20200516180137316

其实并不是,原因是第二次测试中我们的age是固定值 34 所以升降序对它没有影响,所以产生了错觉。

总结:

  • 同为升序或者同为降序,可以消除Using filesort
  • 升序和降序混用,会出现Using filesort

注意:升降序对于查询结果中固定不变的列来说,是无效的。

filesort算法

双路排序

MySQL 4.1 之前使用双路排序,两次扫描磁盘,最终得到数据,读取行指针和ORDER BY列,对他们进行排序,然后扫描已经排好序的列表,按照列表中的值重新从列表中读取对数据输出。也就是==从磁盘读取排序字段,在buffer进行排序,再从磁盘读取其他字段。==文件的磁盘IO非常耗时的,所以在Mysql4.1之后,出现了第二种算法,就是单路排序。

单路排序

从磁盘读取查询所需要的所有列,按照ORDER BY在buffer对它进行排序,然后扫描排序后的列表进行输出,它的效率更快一些,避免了第二次读取数据。并且把随机IO变成了顺序IO,但是它会使用更多的空间,因为它把每一行都保存在了内存里

  1. 由于以上两种排序都是在buffer中进行排序,都有超出sort_buffer容量的风险,而相对来言,单路排序是去整条记录进行排序,所以风险更大,要增大一些sort_buffer_size,在1M~8M之间调整。
  2. 提高max_length_for_sort_data(1024~8192间调整),会增加使用改进算法的概率,但是如果设置太高,数据总容量超出sort_buffer的概率也会增加,会导致高的磁盘I/O活动和低的处理器利用率。

GroupBy

与OrderBy使用大致相同,唯一区别是,在没有条件过滤的情况下也能用到索引。两者可以进行对比学习。

覆盖索引

当以上的优化都无法进行或应用时,我们还有最后的招就是使用覆盖索引,前面也有提到并讲解了什么是覆盖索引。

当我们的select列表中的字段,能够被某个索引中的字段覆盖时,即使查询没有可用的索引,也会使用使用这个索引作为覆盖索引,同时避免了回表操作,直接通过索引的key获取值。所以跟select * 说再见把

23、查询截取分析

慢查询日志

需要通过MySQL配置开启,默认是关闭的,将查询时间超过预定时间的sql语句以日志的形式输出导文件中

show variables like '%slow_query_log%':用这个命令查看慢查询日志的状态,以及日志文件的存放位置

image-20200517101942774

image-20200517102413489

配置使用:

  1. 开启慢查询日志

    set global slow_query_log = true;
    
  2. 配置过滤值

    超时时间:默认是10s

    使用show variables like 'long_query_time%';查看

    image-20200517102917295

    修改成0.1s

    set long_query_time = 0.1;
    
  3. 然后执行几个SQL后,查看我们的日志文件

    image-20200517104128696

    每一个框对应一次慢查询的记录,但是实际开发中这些日志量很大,不可能人为分析,需要机器为我们继续筛选处理数据。

  4. mysql提供了日志分析工具:mysqldumpslow

    image-20200517110019937

mysqldumpslow -s ct -t 3 -a /var/lib/mysql/localhost-slow.log

意思是 统计查询次数©、查询时间(t),然后取出前3条(-t 3),关闭‘N’/‘S’替换(-a)

image-20200517110609092

show processlist

image-20200517111056989

显示连接进程

24、视图

将一段查询sql封装成一个虚拟表。

只保存SQL逻辑,不保存结果。

作用

  • 当一段查询的结果被频繁使用时候使用视图,提高SQL的复用性

创建视图:create view xxx as

create view view_test_06
as
select xx
from xx
where xx
...

image-20200517112413404

更新视图:

create or replace view view_test_06
as

可以在已有的视图的基础上,进行更新。

注意:MySQL5.5中创建视图的sql中不允许from后有子查询

25、主从复制(重难点)

过程以及特点

img

  1. Binarylog 是主机生成的二进制日志文件。
  2. 从机通过I/O读取这个二进制日志文件,然后写入到自己的Relaglog(也叫中继日志)中,同时保存下主机的信息。
  3. 然后从机的SQL线程再去读取中继日志(Relaylog),然后执行以达到更新/同步 数据库的效果。

注意

  1. MySQL的主从复制与Redis的主从复制不同,Redis主从复制,是将主机中所有的内容进行“复制”,而MySQL的从机只会保存当连接到主机之后,所有主机的修改操作。
  2. 由于多次的文件IO操作,会导致主从机之间会有短暂的延迟

其他原则不变:

  • 每个slave只能有一个master
  • 每个slave只能有一个且是唯一的服务器ID
  • 每个master可以有多个slave

我们这里使用Docker来搭建主从复制的环境

  1. 从镜像仓库pull下来mysql:5.7

    docker pull mysql:5.7
    
    # run mysql
    docker run -it -p 3301:3306 -d -e MYSQL_ROOT_PASSWORD=123456 --name mysql_master  mysql:5.7
    
    # into shell
    docker exec -it mysql_master /bin/bash
    
  2. 运行起来以后,我们进入其命令行,发现是没有vim编辑器的

    # yum安装也是没有的 只能使用apt
    
    # 直接按照会报 '找不到可用的安装包' 先对apt进行更新
    apt-get update
    # 再次执行安装,使用海外的镜像仓库下载可能会有些慢。
    apt-get install vim
    
  3. 然后修改一下mysql的配置文件(/etc/mysql目录下)

    其中的配置文件是多个配置文件组合而成的

    image-20200517172328297其中基本参数比如:datadir,socket,pidfile都配置好了(在/etc/mysql/mysql.conf.d/mysql.cnf中)

    image-20200517172501438

    我们需要在my.cnf中增加配置一些字符编码的配置即可

    image-20200517172628014

  4. 配置完成后,我们重启一下容器的mysql,这会导致docker关闭我们的容器。

    # 重启mysql服务,容器会被docker关闭
    service mysql restart
    
    # 重新启动容器
    docker start mysql_master
    # 重新进入命令行,前面提到了怎么进入,这里省略
    ...
    
  5. 尝试连接(本地and远程)

    本地连接:mysql -h主机号 -P端口号(3301) -uroot -p 然后输入密码如果能进入就可以。

    远程连接通过ip和我们使用的端口3301连接即可。

    查看一下字符编码修改情况:

    image-20200517173239190

  6. 为了避免我们反复配置,我们将这个已经配置好的mysql容器提交 成为新的镜像供我们自己使用

    docker commit mysql_master imageName
    

    此后我们直接使用我们提交的镜像创建主从机就可以,也可以将主机,从机配置好,再次提交,但是会占用一些空间。

提示:

  • 如果因为修改配置文件而导致容器无法启动,可以通过docker start 的-a选项,查看错误信息。

  • 然后通过docker inspect 容器名,可以找到容器的文件路径

image-20200517175910949

在这个目录下 /diff/etc/mysql/mysql.cnf中就存放着你之前修改的配置文件,撤销你刚才的修改,然后重新启动就OK了。

具体步骤

  1. 修改主从机的配置文件(my.ini/my.cnf)

    主机

    1. 设置日志文件存放位置

      log-bin=xxxx/mysqlbin

      注意点:

      当使用docker配置时候,务必保证对此路径下文件夹的读写权限,最好是777;

      否者会报错启动失败:

      image-20200517181457355

    2. 设置好server-id,保证全局唯一

    3. 设置那些表输出日志,那些表不输入日志

      binlog-ignore-db=xx:复制时候忽略的表

      binlog-do-db=xx:仅复制的表

      原则上两者配置一个就可以。

    4. 配置日志输出格式

      binlog_format=STATEMENT:标准日志输出格式

      存在问题是,由于延迟原因,当主机中使用now()函数,在从机中执行时两者结果不同,就会造成数据不一致问题。

      或者binlog_format=ROW:行模式

      直接复制主机中已有的数据行,但是在大片数据更新的时候显得效率极低。

      binglog_format=MIXED:混合模式

      当数据中使用函数生成的,直接复制,其他使用日志生成,但是也存在问题是当sql中使用环境变量的时候,就无能为力了。

    从机

    1. server-id,配置为全局唯一

    2. 开启中继日志

      relay-log=mysql-relay

    修改主从机的配置文件后,均要重新启动服务。

  2. 主机授权

    在主机上为从机创建一个可以用于复制用户。

    # 授权所有库的所有表的复制权限,给'slave'@'%'(所有远程连接) 用户密码是123123 
    grant replication slave on *.* to 'slave'@'%' identified by '123123'
    

    查看主机状态show status master

    image-20200517190737058

    每一次对主机修改,position都会变化,也就证明从机确实是从绑定主机开始复制之后的数据库内容的。

    而每一次主机服务重启就会重新创建一个mysqlbin文件,序号递增。

    所以在主从复制环境搭建时,尽量不要对主机数据库进行修改,以免数据不一致

  3. 从机‘拜大哥’

    change master to master_host = '172.17.0.2',# 容器内部的ip
    	master_port = 3306, # 容器内部的端口
        master_user = 'slave', # 这个用户对应我们在主机授权的用户
        master_password = '123123', # 密码同上
        master_log_file = 'mysqlbin.000005', # 对应我们要读取的二进制文件名
        master_log_pos = 154; # 对应起始位置
    

    里面的参数均对应我们在主机中的设置,以及主机的状态。

    当想要解除主从关系时使用reset master即可

    当我们使用docker搭建时host和port应该填写docker容器的IP而非宿主机的ip!

    docker inspect 容器名可以查看

    或者docker inspect --format='{{.NetworkSettings.IPAddress}}' 容器名

    image-20200517201007491

  4. 启动从机start slave

    使用show slave status \G命令可以查看到一下内容证明连接成功

    image-20200517201802082

    • Slave_IO_Running和Slave_SQL_Running 必须全为YES

    这里可以看到错误信息

    image-20200517194957976

    • Slave_IO_Running出现Connecting一般有一下几种情况:
      1. 网络不通——检查ip,端口注意注意 一定是容器的ip和端口
      2. 连接的用户密码不对
      3. master_log_pos不对

    image-20200517193612203

    这个原因就是ip和端口填写的是宿主机的而非容器的。

    我们可以随时停止从机的复制stop slave,然后查看状态

    image-20200517202036759

  5. 测试

    保证我们配置的要进行复制的库没有在主从复制之前被创建,如果在此之前被创建,从机是无法复制到的。一定在主从搭建好以后对指定库进行操作。

    对其他非指定主从复制的库进行操作也不会被从机读取到噢。

    image-20200517211531699

    当我们对主机进行修改时,可以通过主机的status中的Position的变化来判断我们的操作,是否被写入到了二进制日志中,同时再看从机的Real_Master_Log_Pos是否也变化来判断从机是否正确读取到了主机上的二进制日志。

    image-20200517211328178image-20200517211344806

    image-20200517212235488

    Bingo!!完成!

参考博客:https://www.cnblogs.com/songwenjie/p/9371422.html

从库只读设置

上面主从复制搭建完毕后,发现使用从库还是可以对数据库进行修改,就这就涉及了后面的读写分离。垂直拆分,水平拆分。

slave上配置只读,在配置文件中的mysqld中配置read_only=1

注意

  • read_only=1只读模式,可以限定普通用户进行数据修改的操作,但不会限定具有super权限的用户(如超级管理员root用户)的数据修改操作。

  • 如果想保证super用户也不能写操作,就可以就需要执行给所有的表加读锁的命令 “flush tables with read lock;”。

26、MyCat

官网:http://www.mycat.org.cn/

概述

是什么

  • 数据库中间件(Java程序和MySQL之间的中间件)
  • 国人开发!骄傲!前身是阿里的cobar

干什么的

  1. 读写分离

    应对大流量访问,将读操作的压力分到从机上,写操作由主机完成。

  2. 数据分片

    • 水平拆分(分表)
    • 垂直拆分(分库)

    分到多台机器,降低单台机器的压力

  3. 多数据源整合

    • nosql:redis等
    • dbms: oracle、mysql

原理

  • 拦截+转发

    img

Linux下安装MyCat

从官网下载tar.gz压缩包解压安装即可

image-20200517221122762

  1. 将解压文件拷贝到/usr/local目录下

image-20200517221904054

  1. 修改配置文件3个(conf目录下)

    • schema.xml :定义逻辑库,表,分片节点等内容

      1. 当中是无关的table

        image-20200517222952687

        将其删除

      2. 然后为schema设置数据结点dataNode=‘dn1’

        image-20200517223344184

      3. 修改dataNode信息并删除多余的dataNode

        将dataHost的localhost改一下避免引起歧义,database改为我们刚才主从复制选择的库

        然后将多余的数据结点dn2,dn3删掉

        image-20200517223612563

      4. 配置一下dataHost信息

        image-20200517224810378

        时间开发中,mycat与我们的数据库是分机器部署的,所以尽量使用远程连接。

        这里的ip还是使用容器的ip; 不过宿主机ip启动也可以。

      5. 配置完成

        image-20200517225041423

    • rule.xml :定义分片规则 后面学习分片时候进行配置。

    • server.xml :定义用户以及相关系统变量,例如端口等。

      image-20200517222405589
      1. 为了与mysql区别,将root用户改名为mycat

    上面所有配置完成之后,我们再启动前测试一下我们在逻辑库中配置的两台mysql主机的访问情况如何

    如果均能正常登录就没有问题。

  2. 启动mycat

    两种启动方式,首先进入mycat/bin目录下

    • 控制台(前台)启动:./mycat console
    • 后台启动:./mycat start

    启动报错:拒绝连接,ip配置错误,由于是国人写的,且是java编写所以排错很容易。

    image-20200517231735779

    成功:

    image-20200517231345599

  3. 登录

    • 后台管理窗口(9066):mysql -u用户名 -p密码 -h mycat的主机号 -P 9066
    • 数据窗口(8066):mysql -u用户名 -p密码 -h mycat的主机号 -P8066

    这里的用户名和密码参考server.xml

    image-20200517232613026

    成功登录后:

    image-20200517232753918

    我是不是走错片场了??这是mysql? 其实只是mycat仿造出来的mysql,也就是我们之前所说的逻辑库。

    看一下有什么库:

    image-20200517233011084

    这刚好对应server.xml中配置的,我们再来看看这个库中又哪些表:

    image-20200517233312211

    奇迹发生,这就是我们配置给mycat的我们MySQL主从复制的库中创建的表。现在我们通过mycat,也能够对其进行操作。数据可以正常读取出来:

    image-20200517233504881

读写分离

现在我们执行一个操作,使得主从机中数据库内容不一致

insert into test(test) values(@@hostname);
# 将主机名插入,由于每个机器的主机名是不一致的,就造成了主从机数据不一致情况。

image-20200517234705596

我们再来看mycat查出来的数据:

image-20200517234851368

MyCat显示的是master的数据库表信息。证明我们的读写分离没有开启!!

我们要重新回到schema.xml进行修改 dataHost中的balance参数

image-20200517235205588

balance=“0”(默认): 表示不启用读写分离

balance=“1”: 表示全部的readHost,以及stand by writeHost(备选写主机,只有一台主机是用于写的)都参与select的负载均衡

例如:双主双从(M1->S1,M2->S2)再正常情况下,M1和M2是互为主备,只有M1执行写操作,M2就是备选写主机即Stand by writeHost,故 M2,S1,S2都参与select的负载均衡。

balance=“2”:所有的读操作再 writeHost和readHost上随机分发。

balence=“3”:所有的读操作分发给readHost执行,writeHost不参与承担。

我们配置balance=“2”,看MyCat是怎样变化的,修改配置文件后要重启mycat哦!

image-20200518000529802

发现再次使用MyCat来查询数据就是在主从库之间来回随机切换。

分库

当单库的数据量达到了瓶颈时后就需要对单库中的表分解出来分到不同的库中。

分库原则:

当两个库在同一台机器上时,时可以进行跨库join的,但是一旦分库一般是放在不同的机器上的,就不能进行跨库join了,所以:将有可能进行关联查询的表尽量放在同一个库中,例如订单表与商品信息表

分库操作并不是我们人去去干预数据的库表,而是通过MyCat进行拆分。修改schema.xml。

image-20200518140106237

  1. 在schema中配置一个table 并设置数据节点dn2
  2. 配置数据节点dn1,dn2,由于我们是进行分库,不再使用主从复制,而是将它们两个主机,所以两个dataNode的库名保证一致!不要写主从复制的库。dataHost使用不用的host,达到不同主机的效果。
  3. 修改dataHost的host1,增加host2
    • 由于不使用读写分离,将balance改回0
    • 没有读写分离也就没有读主机和写主机之分,所以直接删掉readHost
    • 两个dataHost中的writeHost的ip分别对应两台主机的IP。
<schema name="TESTDB" checkSQLschema="false" sqlMaxLimit="100" dataNode='dn1' >
    <table name = 'customer' dataNode = 'dn2'></table>
</schema>

<dataNode name="dn1" dataHost="host1" database="order" />
<dataNode name="dn2" dataHost="host2" database="order" />

<dataHost name="host1" maxCon="1000" minCon="10" balance="0"
          writeType="0" dbType="mysql" dbDriver="native" switchType="1"  slaveThreshold="100">
    <heartbeat>select user()</heartbeat>
    <!-- can have multi write hosts -->
    <writeHost host="hostM1" url="172.17.0.2:3306" user="root"
               password="123456">
    </writeHost>
</dataHost>

<dataHost name="host2" maxCon="1000" minCon="10" balance="0"
          writeType="0" dbType="mysql" dbDriver="native" switchType="1"  slaveThreshold="100">
    <heartbeat>select user()</heartbeat>
    <!-- can have multi write hosts -->
    <writeHost host="hostM1" url="172.17.0.3:3306" user="root"
               password="123456">
    </writeHost>
</dataHost>

修改完配置文件先别急着启动,我们配置中的order库在两个主机上都还没有,我需要分别创建一个干净的order库。

然后启动MyCat,进去发现:

image-20200518141819337

show tables;看到有一张表,但实际上我们并没有创建这张表。

由于我们在配置文件中写了这个customer表是放在dn2数据节点下的,也就是172.17.0.3主机(后面简称主机B)上的,所以当我们在MyCat中创建这个表的时候,对应的SQL语句会被拦截然后转发到主机B上执行,其余的表创建一律发给172.17.0.2主机(后面简称主机A)执行。

image-20200518142707168

image-20200518143715928

分表

数据量大的表

怎么分?按什么规则分

尽量保证分出的表的数据量平均,访问频率相近,才可以达到减轻单库压力的目的。

首先依然是配置 schema.xml,由于是分表,所以表应该是在多个主机(dataNode)上都有,并且要指定一下分表的规则(rule)

image-20200518151922985

<table name = 'orders' dataNode = 'dn1,dn2' rule = 'mod_rule' ></table>

原本这order张表只在dn1上,我们现在计划把他拆分到两台主机上,dataNode有两个,并且指定了拆分规则rule=‘mod_rule’。

我们通过使用customer_id对节点数取模(mod),就可以实现根据customer_id进行分表,对应的MyCat中的算法是mod-long。

保存,然后我们要去我们最开始使用MyCat提到的三个配置文件之二: rule.xml

image-20200518152552625

这个配置文件中,提供了很多预备的分表规则,并且指明了使用的算法,以及算法的class文件:

image-20200518152749382

如果我们需要自定义分表规则,只需要使用其中已有的算法,然后对rule中的相关参数设置成为我们自己的就行。

<!--自定义算法 rule_mod -->
<tableRule name="rule_mod">
    <rule>
        <!--根据哪个字段分表-->
        <columns>customer_id</columns>
        <!--使用的修改后的算法-->
        <algorithm>mod-long</algorithm>
    </rule>
</tableRule>

<!--对原有进行修改-->
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
    <!-- 节点数量 修改为2 -->
    <property name="count">2</property>
</function>

修改完成,在启动前,还要去dn2把指定拆分的表(orders)建上,否者MyCat启动会报错。

启动成功以后,我们使用MyCat进行数据插入测试:

INSERT INTO orders(id,order_type,customer_id,amount)
VALUES
(1,101,100,100100),
(2,101,100,100300),
(3,101,101,120000),
(4,101,101,103000),
(5,102,101,100400),
(6,102,100,100020);

插入过程注意,我们用于分表的字段必须要在表信息中标出来,否则插入报错。

image-20200518155750453

果然是通过customer_id取模进行分表操作的。

联合查询的表

问题来了:如果我现在要对orders表进行联表查询能成吗?

image-20200518161837956

事实证明使用MyCat进行跨库join是不行的,那么为什么会报这个诡异的错误呢?

image-20200518162521315

问题就在于虽然对orders进行了分表,但是进行联表查询,其中一方并没有这个表就直接报错。

**解决方式:**将进行关联的表也进行分表!

那么问题又来了:怎么分才不会导致关联查询漏掉结果呢?

如果分表后,dn1中的od表和dn2中odt表进行关联查询也是可以得到结果的,但是由于跨库join无法实现,就会漏掉数据导致查询结果不准确。

最好的办法就是,让关联表通过 关联字段绑定,你去哪我就去哪,这样就可以解决这个问题了。

比如例子中,odt和od 通过odt.orderid和 od.id产生关联关系,那么由于我们对od进行了分表,那么分表后每个od.id也就有固定的去向,对应的odt.orderid屁颠屁颠跟着跑就对了。

我们来看看在schema.xml中这种配置如何实现:

image-20200518163854953

<table name = 'orders' dataNode = 'dn1,dn2' rule = 'mod_rule' >
            <childTable name = 'orders_detail' primaryKey = 'id' joinKey = 'order_id' parentKey = 'id'/>
</table>

就是在之前分表的标签中加一个<childTable>标签,并指定表名(name),主键名(primaryKey),自身用于和父表进行关联的字段名(joinKey),以及父表中对应的关联字段名(parentKey)。

同样启动之前先去把dn2中缺失的表补上。

启动然后使用MyCat插入odt数据,查看结果:

image-20200518165214217

这样就完成了对关联表的分表,我们查询一下:

image-20200518165359812

解决了联合查询漏数据的问题,即使是分了库分了表。

全局通用的表

还有一些表,其中的数据是全局共享的,但是进行了分库,我们需要将这些表在每个库中都放置一份完整的。这就是全局表。一般这种表的数据量不会很大,会产生冗余但是影响并不大。

看看全局表在schema.xml配置:

image-20200518170657787

<table name = 'dict_order_type' dataNode = 'dn1,dn2' type = 'global'></table>

为table设置type属性为‘global’即可;

然后去补建上这张表。启动MyCat并插入数据,查看结果:

image-20200518171607025

两个库中都有这个表,并且数据的统一的。

全局序列

分表之后为了使我们的主键值不重复,需要MyCat为我们生成全局序列,之前分布式中生成全局唯一序列的方式有UUID、雪花算法等。

MyCat提供的生成方式:

  • 本地文件生成,将计数器存放在本地文件中(稳定性不高,不推荐)

  • 时间戳方式(18位,太长占用空间)

  • 数据库方式

    创建一个数据库,专门用于统计全局序列,每次提供若干个序列供MyCat分配,一旦MyCat宕机,备用机上线,再次从数据库中获取新批次,之前没有用完的直接丢弃,保证了稳定性和可用性。

  • 自主生成

    使用Redis的单线程特点,每次插入执行一次INCR,也可以保证序列唯一。

数据库方式

  1. 建表

    create table MYCAT_SEQUENCE (
        name varchar(50) not null,
        current_value int not null ,
        increment int not null default 100,
        primary key (name)
    )engine = innoDB;
    
  2. 官方给出的函数

    #取当前squence的值
    DROP FUNCTION IF EXISTS mycat_seq_currval;
    DELIMITER $$
    CREATE FUNCTION mycat_seq_currval(seq_name VARCHAR(50))RETURNS VARCHAR(64) CHARSET 'utf8'
    BEGIN
        DECLARE retval VARCHAR(64);
        SET retval='-999999999,NULL';
        SELECT CONCAT(CAST(current_value AS CHAR),',',CAST(increment AS CHAR)) INTO retval FROM
            MYCAT_SEQUENCE WHERE NAME = seq_name;
        RETURN retval;
    END$$
    DELIMITER ;
    
    #设置 sequence 值
    DROP FUNCTION IF EXISTS mycat_seq_setval;
    DELIMITER $$
    CREATE FUNCTION mycat_seq_setval(seq_name VARCHAR(50),VALUE INTEGER) RETURNS VARCHAR(64) CHARSET 'utf8'
    BEGIN
        UPDATE MYCAT_SEQUENCE SET current_value = VALUE    WHERE NAME = seq_name;
        RETURN mycat_seq_currval(seq_name);
    END$$
    DELIMITER ;
    
    #取下一个sequence的值
    DROP FUNCTION IF EXISTS mycat_seq_nextval;
    DELIMITER $$
    CREATE FUNCTION mycat_seq_nextval(seq_name VARCHAR(50)) RETURNS VARCHAR(64) CHARSET 'utf8'
    BEGIN
        UPDATE MYCAT_SEQUENCE SET current_value = current_value + increment
        WHERE NAME = seq_name;
        RETURN mycat_seq_currval(seq_name);
    END$$
    DELIMITER ;
    

    若报错1418:

    show variables like '%log_bin_trust%';
    # 为off再执行这条语句,将其打开然后创建函数。
    SET GLOBAL log_bin_trust_function_creators = 1;
    
  3. 表中插入数据

    insert into MYCAT_SEQUENCE(name, current_value) VALUES 
    ('ORDERS',400000,100);
    # 统计列是 orders
    # 当前计数值是 400000
    # 每次下发100个(步长)
    
  4. 修改MyCat配置文件

    • sequence_db_conf.properties

      ORDERS=dn1,指向我们计数的数据库节点。

    • server.xml

      <property name="sequnceHandlerType">1</property>

      0:文件方式

      1:数据库方式

      2:时间戳方式(默认)

      3:自主生成

  5. 在MyCat中向orders表插入数据

    INSERT INTO orders(id,order_type,customer_id,amount)
    VALUES
    (next value for MYCATSEQ_ORDERS,102,100,100900);
    ...
    

    image-20200518181044365

    对应的MYCAT_SEQUENCE中数据也产生了变化:

    image-20200518181152399

  6. 模拟宕机:重启MyCat,再次插入数据:

    image-20200518181321721

    直接放弃了上一批次未用完的数据,重新分配新的批次;

    image-20200518181414460

  • 0
    点赞
  • 1
    评论
  • 6
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

©️2021 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值