数据库入门之学习《MySQL实战45讲》第41讲-第45讲笔记

《MySQL实战45讲》第41讲 怎么最快地复制一张表?

怎么在两张表中拷贝数据

如果可以控制对源表的扫描行数和加锁范围很小的话,我们简单地使用 insert … select 语句即可实现。当然,为了避免对源表加读锁,更稳妥的方案是先将数据写到外部文本文件,然后再写回目标表。这时,有两种常用的方法。

mysqldump 方法

一种方法是,使用 mysqldump 命令将数据导出成一组 INSERT 语句。你可以使用下面的命令:

mysqldump -h$host -P$port -u$user --add-locks=0 --no-create-info --single-transaction  --set-gtid-purged=OFF db1 t --where="a>900" --result-file=/client_tmp/t.sql

把结果输出到临时文件。

  • –single-transaction 的作用是,在导出数据的时候不需要对表 db1.t 加表锁,而是使用 START
    TRANSACTION WITH CONSISTENT SNAPSHOT 的方法;
  • –add-locks 设置为 0,表示在输出的文件结果里,不增加" LOCK TABLES t WRITE;" ;
  • –no-create-info 的意思是,不需要导出表结构;
  • –set-gtid-purged=off 表示的是,不输出跟 GTID 相关的信息;
  • –result-file 指定了输出文件的路径,其中 client 表示生成的文件是在客户端机器上的。

通过这条 mysqldump 命令生成的 t.sql 文件中就包含了如图所示的 INSERT 语句。
在这里插入图片描述
然后,你可以通过下面这条命令,将这些 INSERT 语句放到 db2 库里去执行。

mysql -h127.0.0.1 -P13000  -uroot db2 -e "source /client_tmp/t.sql"
导出 CSV 文件

MySQL 提供了下面的语法,用来将查询结果导出到服务端本地目录:

select * from db1.t where a>900 into outfile '/server_tmp/t.csv';

得到.csv 导出文件后,你就可以用下面的 load data 命令将数据导入到目标表 db2.t 中。

load data infile '/server_tmp/t.csv' into table db2.t;
物理拷贝方法

在 MySQL 5.6 版本引入了可传输表空间的方法,可以通过导出 + 导入表空间的方式,实现物理拷贝表的功能。

小结
  1. 物理拷贝的方式速度最快,尤其对于大表拷贝来说是最快的方法。如果出现误删表的情况,用备份恢复出误删之前的临时库,然后再把临时库中的表拷贝到生产库上,是恢复数据最快的方法。但是,这种方法的使用也有一定的局限性: 1 必须是全表拷贝,不能只拷贝部分数据;2 需要到服务器上拷贝数据,在用户无法登录数据库主机的场景下无法使用;3 由于是通过拷贝物理文件实现的,源表和目标表都是使用 InnoDB 引擎时才能使用。
  2. 用 mysqldump 生成包含 INSERT 语句文件的方法,可以在 where 参数增加过滤条件,来实现只导出部分数据。这个方式的不足之一是,不能使用 join 这种比较复杂的 where 条件写法。
  3. 用 select … into outfile 的方法是最灵活的,支持所有的 SQL 写法。但,这个方法的缺点之一就是,每次只能导出一张表的数据,而且表结构也需要另外的语句单独备份。

后两种方式都是逻辑备份方式,是可以跨引擎使用的。

《MySQL实战45讲》第42讲 grant之后要跟着flush privileges吗?

在 MySQL 里面,grant 语句是用来给用户赋权的。grant 之后要马上跟着执行一个flush privileges 命令,才能使赋权语句生效。

创建一个用户的命令:

create user 'ua'@'%' identified by 'pa';

这条语句的逻辑是创建一个用户’ua’@’%’,密码是 pa。注意,在 MySQL 里面,用户名 (user)+ 地址(host) 才表示一个用户,因此 ua@ip1 和 ua@ip2 代表的是两个不同的用户。

这条命令做了两个动作:

  1. 磁盘上,往 mysql.user 表里插入一行,由于没有指定权限,所以这行数据上所有表示权限的字段的值都是 N;
  2. 内存里,往数组 acl_users 里插入一个 acl_user 对象,这个对象的 access 字段值为 0。
全局权限

全局权限,作用于整个 MySQL 实例,这些权限信息保存在 mysql 库的 user 表里。如果我要给用户 ua 赋一个最高权限的话,语句是这么写的:

grant all privileges on *.* to 'ua'@'%' with grant option;

这个 grant 命令做了两个动作:

  1. 磁盘上,将 mysql.user 表里,用户’ua’@’%'这一行的所有表示权限的字段的值都修改为‘Y’;
  2. 内存里,从数组 acl_users 中找到这个用户对应的对象,将 access 值(权限位)修改为二进制的“全 1”。

需要注意:

  1. grant 命令对于全局权限,同时更新了磁盘和内存。命令完成后即时生效,接下来新创建的连接会使用新的权限。
  2. 对于一个已经存在的连接,它的全局权限不受 grant 命令的影响。

如果要回收上面的 grant 语句赋予的权限,你可以使用下面这条命令:

revoke all privileges on *.* from 'ua'@'%';
db 权限

如果要让用户 ua 拥有库 db1 的所有权限,可以执行下面这条命令:

grant all privileges on db1.* to 'ua'@'%' with grant option;

基于库的权限记录保存在 mysql.db 表中,在内存里则保存在数组 acl_dbs 中。这条 grant 命令做了如下两个动作:

  1. 磁盘上,往 mysql.db 表中插入了一行记录,所有权限位字段设置为“Y”;
  2. 内存里,增加一个对象到数组 acl_dbs 中,这个对象的权限位为“全 1”。

每次需要判断一个用户对一个数据库读写权限的时候,都需要遍历一次 acl_dbs 数组,根据 user、host 和 db 找到匹配的对象,然后根据对象的权限位来判断。也就是说,grant 修改 db 权限的时候,是同时对磁盘和内存生效的。

grant 操作对于已经存在的连接的影响,在全局权限和基于 db 的权限效果是不同的。

表权限和列权限

表权限定义存放在表 mysql.tables_priv 中,列权限定义存放在表 mysql.columns_priv 中。这两类权限,组合起来存放在内存的 hash 结构 column_priv_hash 中。

这两类权限的赋权命令如下

create table db1.t1(id int, a int);

grant all privileges on db1.t1 to 'ua'@'%' with grant option;
GRANT SELECT(id), INSERT (id,a) ON mydb.mytbl TO 'ua'@'%' with grant option;

跟 db 权限类似,对这两类权限的操作,也会马上影响到已经存在的连接。

flush privileges 命令会清空 acl_users 数组,然后从 mysql.user 表中读取数据重新加载,重新构造一个 acl_users 数组。也就是说,以数据表中的数据为准,会将全局权限内存数组重新加载一遍。同样地,对于 db 权限、表权限和列权限,MySQL 也做了这样的处理。

也就是说,如果内存的权限数据和磁盘数据表相同的话,不需要执行 flush privileges。而如果我们都是用 grant/revoke 语句来执行的话,内存和数据表本来就是保持同步更新的。因此,正常情况下,grant 命令之后,没有必要跟着执行 flush privileges 命令。

flush privileges 使用场景

当数据表中的权限数据跟内存中的权限数据不一致的时候,flush privileges 语句可以用来重建内存数据,达到一致状态。这种不一致往往是由不规范的操作导致的,比如直接用 DML 语句操作系统权限表。

小结

grant 语句会同时修改数据表和内存,判断权限的时候使用的是内存数据。因此,规范地使用 grant 和 revoke 语句,是不需要随后加上 flush privileges 语句的。

grant super on *.* to 'ua'@'%' identified by 'pa';

这条命令加了 identified by ‘密码’, 语句的逻辑里面除了赋权外,还包含了:

  1. 如果用户’ua’@’%'不存在,就创建这个用户,密码是 pa;
  2. 如果用户 ua 已经存在,就将密码修改成 pa。

《MySQL实战45讲》第43讲 要不要使用分区表?

手动分表和分区表

我们使用分区表的一个重要原因就是单表过大。那么,如果不使用分区表的话,我们就是要使用手动分表的方式。

手工分表的逻辑,也是找到需要更新的所有分表,然后依次执行更新。在性能上,这和分区表并没有实质的差别。分区表和手工分表,一个是由 server 层来决定使用哪个分区,一个是由应用层代码来决定使用哪个分表。因此,从引擎层看,这两种方式也是没有差别的。其实这两个方案的区别,主要是在 server 层上。

分区策略
  1. MyISAM 分区表使用的分区策略,我们称为通用分区策略,每次访问分区都由 server 层控制。其具有比较严重的性能问题。
  2. 从 MySQL 5.7.9 开始,InnoDB 引擎引入了本地分区策略。这个策略是在 InnoDB 内部自己管理打开分区的行为。

从 MySQL 8.0 版本开始,就不允许创建 MyISAM 分区表了,只允许创建已经实现了本地分区策略的引擎。目前来看,只有 InnoDB 和 NDB 这两个引擎支持了本地分区策略。

小结

如果从 server 层看的话,一个分区表就只是一个表。

  1. MySQL 在第一次打开分区表的时候,需要访问所有的分区;
  2. 在 server 层,认为这是同一张表,因此所有分区共用同一个 MDL 锁;
  3. 在引擎层,认为这是不同的表,因此 MDL 锁之后的执行过程,会根据分区表规则,只访问必要的分区。
分区表的应用场景

分区表的一个显而易见的优势是对业务透明,相对于用户分表来说,使用分区表的业务代码更简洁。还有,分区表可以很方便的清理历史数据。

《MySQL实战45讲》第44讲 答疑文章(三):说一说这些好问题

Simple Nested Loop Join 的性能问题

BNL 算法和Simple Nested Loop Join 算法都是要判断 M*N 次(M 和 N 分别是 join 的两个表的行数),但是 Simple Nested Loop Join 算法的每轮判断都要走全表扫描,因此性能上 BNL 算法执行起来会快很多。

BNL 算法的执行逻辑是:首先,将驱动表的数据全部读入内存 join_buffer 中,这里 join_buffer 是无序数组;然后,顺序遍历被驱动表的所有行,每一行数据都跟 join_buffer 中的数据进行匹配,匹配成功则作为结果集的一部分返回。

Simple Nested Loop Join 算法的执行逻辑是:顺序取出驱动表中的每一行数据,到被驱动表去做全表扫描匹配,匹配成功则作为结果集的一部分返回。

Simple Nested Loop Join 算法,其实也是把数据读到内存里,然后按照匹配条件进行判断,为什么性能差距会这么大呢?

  1. 在对被驱动表做全表扫描的时候,如果数据没有在 Buffer Pool 中,就需要等待这部分数据从磁盘读入;从磁盘读入数据到内存中,会影响正常业务的 Buffer Pool 命中率,而且这个算法天然会对被驱动表的数据做多次访问,更容易将这些数据页放到 Buffer Pool 的头部
  2. 即使被驱动表数据都在内存中,每次查找“下一个记录的操作”,都是类似指针操作。而 join_buffer 中是数组,遍历的成本更低。

所以说,BNL 算法的性能会更好。

《MySQL实战45讲》第45讲 自增id用完怎么办?

表定义自增值 id

MySQL 里有很多自增的 id,每个自增 id 都是定义了初始值,然后不停地往上加步长。虽然自然数是没有上限的,但是在计算机里,只要定义了表示这个数的字节长度,那它就有上限。比如,无符号整型 (unsigned int) 是 4 个字节,上限就是 2的32次方-1。

表定义的自增值达到上限后的逻辑是:再申请下一个 id 时,得到的值保持不变。

2的32次方-1(4294967295)不是一个特别大的数,对于一个频繁插入删除数据的表来说,是可能会被用完的。因此在建表的时候你需要考察你的表是否有可能达到这个上限,如果有可能,就应该创建成 8 个字节的 bigint unsigned。

InnoDB 系统自增 row_id

如果你创建的 InnoDB 表没有指定主键,那么 InnoDB 会给你创建一个不可见的,长度为 6 个字节的 row_id。

写入表的 row_id 是从 0 开始到 2的48次方-1。达到上限后,下一个值就是 0,然后继续循环。在 InnoDB 逻辑里,申请到 row_id=N 后,就将这行数据写入表中;如果表中已经存在 row_id=N 的行,新写入的行就会覆盖原有的行。

Xid

Xid 在 MySQL 中是用来对应事务的。那么,Xid 在 MySQL 内部是怎么生成的呢?MySQL 内部维护了一个全局变量 global_query_id,每次执行语句的时候将它赋值给 Query_id,然后给这个变量加 1。如果当前语句是这个事务执行的第一条语句,那么 MySQL 还会同时把 Query_id 赋值给这个事务的 Xid。

而 global_query_id 是一个纯内存变量,重启之后就清零了。所以你就知道了,在同一个数据库实例中,不同事务的 Xid 也是有可能相同的。但是 MySQL 重启之后会重新生成新的 binlog 文件,这就保证了,同一个 binlog 文件里,Xid 一定是惟一的。

虽然 MySQL 重启不会导致同一个 binlog 里面出现两个相同的 Xid,但是如果 global_query_id 达到上限后,就会继续从 0 开始计数。从理论上讲,还是就会出现同一个 binlog 里面出现相同 Xid 的场景。

Innodb trx_id

Xid 是由 server 层维护的。InnoDB 内部使用 Xid,就是为了能够在 InnoDB 事务和 server 之间做关联。但是,InnoDB 自己的 trx_id,是另外维护的。trx_id 实际上就是事务 id(transaction id)。

InnoDB 内部维护了一个 max_trx_id 全局变量,每次需要申请一个新的 trx_id 时,就获得 max_trx_id 的当前值,然后并将 max_trx_id 加 1。

InnoDB 数据可见性的核心思想是:每一行数据都记录了更新它的 trx_id,当一个事务读到一行数据的时候,判断这个数据是否可见的方法,就是通过事务的一致性视图与这行数据的 trx_id 做对比。

只读事务不分配 trx_id,有什么好处呢?
  1. 这样做可以减小事务视图里面活跃事务数组的大小。因为当前正在运行的只读事务,是不影响数据的可见性判断的。所以,在创建事务的一致性视图时,InnoDB 就只需要拷贝读写事务的 trx_id。
  2. 可以减少 trx_id 的申请次数。在 InnoDB 里,即使你只是执行一个普通的 select 语句,在执行过程中,也是要对应一个只读事务的。所以只读事务优化后,普通的查询语句不需要申请 trx_id,就大大减少了并发事务申请 trx_id 的锁冲突。

由于只读事务不分配 trx_id,一个自然而然的结果就是 trx_id 的增加速度变慢了。但是,max_trx_id 会持久化存储,重启也不会重置为 0,那么从理论上讲,只要一个 MySQL 服务跑得足够久,就可能出现 max_trx_id 达到 2的48次方-1 的上限,然后从 0 开始的情况。

thread_id

thread_id 的逻辑很好理解:系统保存了一个全局变量 thread_id_counter,每新建一个连接,就将 thread_id_counter 赋值给这个新连接的线程变量。

thread_id_counter 定义的大小是 4 个字节,因此达到 2的32次方-1 后,它就会重置为 0,然后继续增加。但是,你不会在 show processlist 里看到两个相同的 thread_id。这,是因为 MySQL 设计了一个唯一数组的逻辑。

小结
  1. 表的自增 id 达到上限后,再申请时它的值就不会改变,进而导致继续插入数据时报主键冲突的错误。
  2. row_id 达到上限后,则会归 0 再重新递增,如果出现相同的 row_id,后写的数据会覆盖之前的数据。
  3. Xid 只需要不在同一个 binlog 文件中出现重复值即可。虽然理论上会出现重复值,但是概率极小,可以忽略不计。
  4. InnoDB 的 max_trx_id 递增值每次 MySQL 重启都会被保存起来,所以我们文章中提到的脏读的例子就是一个必现的 bug,好在留给我们的时间还很充裕。
  5. thread_id 是我们使用中最常见的,而且也是处理得最好的一个自增 id 逻辑了。

学习地址:MySQL实战45讲

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值