图片来自极客时间,如有版权问题,请联系我删除。
扫码加入学习!
千万不能误删
binlog_format=row 和 binlog_row_image=FULL 可以使用Flashback回放。
不建议直接在主库使用,应该在备库执行,然后再将确认过的临时库的数据,恢复回主库。
取全量备份,和全量备份时间点之后的binlog恢复。但mysqlbinlog不够快。
一个加速的方法,将全量备份恢复的临时实例,设置为线上备库的从库。
MySQL 5.6 版本引入,通过 CHANGE MASTER TO MASTER_DELAY = N 命令,可以指定这个备库持续保持跟主库有N 秒的延迟。
kill query + 线程 id:表示终止这个线程中正在执行的语句;
kill connection + 线程 id,这里 connection 可缺省,表示断开这个线程的连接,如果这个线程有语句正在执行,也是要先停止正在执行的语句的。
mysql kill命令不是直接终止线程。
mysql处理过程中有许多埋点,这些“埋点”的地方判断线程状态,如果发现线程状态是 THD::KILL_QUERY,才开始进入语句终止逻辑。
如果碰到一个被 killed 的事务一直处于回滚状态,尽量不要重启,因为重启之后该做的回滚动作还是不能少的,所以从恢复速度的角度来说,应该让它自己结束。如果这个语句可能会占用别的锁,或者由于占用 IO 资源过多,从而影响到了别的语句执行的话,就需要先做主备切换,切到新主库提供服务。避免大事务
net_buffer由参数 net_buffer_length 定义的,默认是 16k。
mysql是遍读遍发的,所以当net_buffer写满的时候就需要等待。使用show processlist可以看到state=“Sending to client”。
mysql还要一个state=“Sending data”,它的意思只是“正在执行”。
介绍 WAL 机制时,分析了Buffer Pool 加速更新的作用。Buffer Pool 还有一个更重要的作用,就是加速查询。
执行 show engine innodb status可以查看一个系统当前的 BP 命中率。
InnoDB Buffer Pool 的大小是由参数 innodb_buffer_pool_size 确定的,一般建议设置成可用物理内存的 60%~80%。
InnoDB 内存管理用的是最近最少使用 (LRU) 算法,这个算法的核心就是淘汰最久未使用的数据。
如果在查询历史数据使用这个算法,会导致很多请求会从磁盘读取数据。所以mysql对LRU算法进行了改进。
在 InnoDB 实现上,按照 5:3 的比例把整个 LRU 链表分成了 young 区域和 old 区域。
1s由参数 innodb_old_blocks_time 控制的。其默认值是 1000,单位毫秒。
表结构
CREATE TABLE `t2` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `a` (`a`)
) ENGINE=InnoDB;
drop procedure idata;
delimiter ;;
create procedure idata()
begin
declare i int;
set i=1;
while(i<=1000)do
insert into t2 values(i, i, i);
set i=i+1;
end while;
end;;
delimiter ;
call idata();
create table t1 like t2;
insert into t1 (select * from t2 where id<=100)
select * from t1 straight_join t2 on (t1.a=t2.a);
t1只有100行,所有一共扫描200行。
如果执行select * from t1,再执行select * from t2 where a=$R.a。虽然都可以走索引,也只扫描200行。但需要执行101行sql。
如果可以走索引:
如果驱动表用不上索引。
select * from t1 straight_join t2 on (t1.a=t2.b);
因为t2.b没有索引,所以需要全表扫描。总共需扫描100*1000行。
MySQL 没有使用 Simple Nested-Loop Join 算法,而是使用了“Block Nested-Loop Join”算法,简称BNL。
虽然都会扫描100*1000行,但BNL是内存判断,所以会快一点。
如果被驱动表是个大表,会把冷数据的page加入到buffer pool,并且BNL要扫描多次,两次扫描的时间可能会超过1秒,使上节提到的分代LRU优化失效,把热点数据从buffer pool中淘汰掉,影响正常业务的查询效率。
表结构
create table t1(id int primary key, a int, b int, index(a));
create table t2 like t1;
drop procedure idata;
delimiter ;;
create procedure idata()
begin
declare i int;
set i=1;
while(i<=1000)do
insert into t1 values(i, 1001-i, i);
set i=i+1;
end while;
set i=1;
while(i<=1000000)do
insert into t2 values(i, i, i);
set i=i+1;
end while;
end;;
delimiter ;
call idata();
回忆一下回表。回表是指,InnoDB 在普通索引 a 上查到主键 id 的值后,再根据一个个主键 id 的值到主键 id 的值到主键索引上去查整行数据的过程。
主键索引是一棵 B+ 树,在这棵树上,每次只能根据一个主键 id 查到一行数据。因此,回表肯定是一行行搜索主键索引的。
如果随着 a 的值递增顺序查询的话,id 的值就变成随机的,那么就会出现随机访问,性能相对较差。虽然“按行查”这个机制不能改,但是调整查询的顺序,还是能够加速的。
因为大多数的数据都是按照主键递增顺序插入得到的,所以我们可以认为,如果按照主键的递增顺序查询的话,对磁盘的读比较接近顺序读,能够提升读性能。
MRR 优化的设计思路:
read_rnd_buffer 的大小是由 read_rnd_buffer_size 参数控制。如果想要稳定地使用 MRR 优化的话,需要设置set optimizer_switch=“mrr_cost_based=off”,如果不设置,优化器会判断消耗,倾向于不使用MRR。
MySQL 在 5.6 版本后开始引入的 Batched Key Acess(BKA) 算法了。其实就是对 NLJ 算法的优化。
NLJ 算法执行的逻辑是:从驱动表 t1,一行行地取出 a 的值,再到被驱动表 t2 去做 join。也就是说,对于表 t2 来说,每次都是匹配一个值。这时,MRR 的优势就用不上了。
BKA 算法就是缓存多行传给其他表,流程如下:
启动BKA:
set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';
上篇文章末尾说了,如果一个使用 BNL 算法的 join 语句,多次扫描一个冷表,而且这个语句执行时间超过 1 秒,就会在再次扫描冷表的时候,把冷表的数据页移到LRU 链表头部。
为了减少这种影响,可以考虑增大join_buffer_size 的值,减少对被驱动表的扫描次数。
优化的常见做法是,给被驱动表的 join 字段加上索引,把 BNL 算法转成 BKA 算法。
还可以考虑使用临时表。使用临时表的大致思路是:
sql如下:
create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb;
insert into temp_t select * from t2 where b>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);
mysql目前还没有hash索引,MariaDB支持。
所以可以自己实现在业务端。实现流程大致如下:
这个过程会比临时表方案的执行速度还要快一些。
上节提到了临时表。
如果是使用 InnoDB 引擎或者 MyISAM 引擎的临时表,写数据的时候是写到磁盘上的。当然,临时表也可以使用 Memory 引擎。
临时表的特点:
分表分库跨库查询
分库分表系统都有一个中间层 proxy,如果 sql 能够直接确定某个分表,这种情况是最理想的。
但如果涉及到跨库,一般有两种方式
MySQL 要给临时 InnoDB 表创建一个 frm 文件保存表结构定义,还要有地方保存表数据。
这个 frm 文件放在临时文件目录下,文件名的后缀是.frm,前缀是“#sql{进程 id}_{线程 id}_ 序列号”。可以使用 select @@tmpdir 命令,来显示实例的临时文件目录。
表中数据存放:
MySQL 维护数据表,除了物理上要有文件外,内存里面也有一套机制区别不同的表,每个表都对应一个table_def_key。
如果当前的 binlog_format=row,那么跟临时表有关的语句,就不会记录到 binlog 里。
binlog_format=statment/mixed 的时候,binlog 中才会记录临时表的操作。
这种情况下,创建临时表的语句会传到备库执行,因此备库的同步线程就会创建这个临时表。主库在线程退出的时候,会自动删除临时表,但是备库同步线程是持续在运行的。所以,这时候我们就需要在主库上再写一个 DROP TEMPORARY TABLE 传给执行。
create table t1(id int primary key, a int, b int, index(a));
delimiter ;;
create procedure idata()
begin
declare i int;
set i=1;
while(i<=1000)do
insert into t1 values(i, i, i);
set i=i+1;
end while;
end;;
delimiter ;
call idata();
执行这条语句
(select 1000 as f) union (select id from t1 order by id desc limit 2);
执行流程:
如果把上面这个语句中的 union 改成 union all的话,就不需要“去重”。这样执行的时候,就依次执行子查询,得到的结果直接作为结果集的一部分,发给客户端。因此也就不需要临时表了。
select id%10 as m, count(*) as c from t1 group by m;
执行流程:
如果不需要排序则直接取内存临时表的数据。
但内存临时表的大小是有限制的,参数 tmp_table_size 就是控制这个内存大小的,默认是 16M。如果内存不够则使用磁盘临时表。
索引
假设有个这样的数据结构:
如果可以确保输入的数据是有序的,那么计算 group by 的时候,就只需要从左到右,顺序扫描,依次累加。
InnoDB 的索引,就可以满足这个输入有序的条件。
直接排序
如果临时表数据量特别大,可让 MySQL 直接走磁盘临时表,在 group by 语句中加入 SQL_BIG_RESULT 这个提示(hint)。
MySQL 的优化器会直接用数组来存,而不是B+ 树存储。这样
select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m;
执行流程:
MySQL 什么时候会使用内部临时表?
group by使用的指导原则:
表 t1 使用 Memory 引擎, 表 t2 使用InnoDB 引擎。
create table t1(id int primary key, c int) engine=Memory;
create table t2(id int primary key, c int) engine=innodb;
insert into t1 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
insert into t2 values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6),(7,7),(8,8),(9,9),(0,0);
可以看到两个引擎顺序不一致。
InnoDB 表的数据就放在主键索引树上,主键索引是 B+ 树。
与 InnoDB 引擎不同,Memory 引擎的数据和索引是分开的。
内存表的数据部分以数组的方式单独存放,而主键 id 索引里,存的是每个数据的位置。主键 id 是 hash 索引,可以看到索引上的 key 并不是有序的。
在内存表 t1 中,执行 select * 按数组顺序全表扫描。因此,0 就是最后一个被读到。
所以InnoDB 和 Memory 引擎的数据组织方式是不同的:
两个引擎的一些典型不同:
由于内存表的这些特性,每个数据行被删除以后,空出的这个位置都可以被接下来要插入的数据复用。
内存表 t1 的这个主键索引是哈希索引,因此如果执行范围查询是用不上主键索引的,需要走全表扫描。
内存表也是支 B-Tree 索引的
alter table t1 add index a_btree_index using btree (id);
这里的原因主要包括两个方面:
内存表的锁
内存表不支持行锁,只支持表锁。
数据持久性问题
数据库重启的时候,所有的内存表都会被清空。
主从模式,从库掉电重启收到主库请求会找不到行。双主模式下,一台掉电重启会发送delete到另一台清空数据。
第 35 和 36 篇说到的用户临时表。在数据量可控,不会耗费过多内存的情况下,你可以考虑使用内存表。
create temporary table temp_t(id int primary key, a int, b int, index (b))engine=memory;
insert into temp_t select * from t2 where b>=1 and b<=2000;
select * from t1 join temp_t on (t1.b=temp_t.b);
不同的引擎对于自增值的保存策略不同。
如果字段 id 被定义为 AUTO_INCREMENT
假设,某次要插入的值是 X,当前的自增值是 Y。
新的自增值生成算法是:从 auto_increment_offset 开始,以 auto_increment_increment 为步长,持续叠加,直到找到第一个大于 X 的值,作为新的自增值。(双主架构可以设置一个库的自增id都是奇数,另一个都是偶数)。
自增值会在插入数据之前自增。
所以唯一键冲突是导致自增主键 id 不连续的第一种原因。类似,事务回滚也会产生类似的现象。
MySQL 5.1.22 版本引入了一个新策略,新增参数 innodb_autoinc_lock_mode,默认值是 1。
生产上,如果有insert … select、replace … select 和 load data 语句,这种批量插入数据的场景时,建议设置:innodb_autoinc_lock_mode=2 ,并且 binlog_format=row。
对于批量插入数据的语句,MySQL 有一个批量申请自增 id 的策略:同一个语句去申请自增 id,每次申请到的自增 id 个数都是上一次的两倍。所以如果多申请了id也会导致自增主键 id 不连续。
普通insert语句,即使 innodb_autoinc_lock_mode 设置为 1,也不会等语句执行完成才释放锁。因为在申请自增 id 的时候,是可以精确计算出需要多少个 id 的,然后一次性申请,申请完成后锁就可以释放了。
可重复读隔离级别下,binlog_format=statement。
表结构
CREATE TABLE `t` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(null, 1,1);
insert into t values(null, 2,2);
insert into t values(null, 3,3);
insert into t values(null, 4,4);
create table t2 like t
session B执行时需要对表 t 的所有行和间隙加锁。如果没有锁,就可能出现 session B 的 insert 语句先执行,但是后写入 binlog 的情况。所以会引起主备不一致。
执行 insert … select 的时候,对目标表也不是锁全表,而是只锁住需要访问的资源。
现在有这么一个需求:要往表 t2 中插入一行数据,这一行的 c 值是表 t 中 c 值的最大值加 1。
insert into t2(c,d) (select c+1, d from t force index(c) order by c desc limit 1);
这个语句的加锁范围,就是表 t 索引 c 上的 (3,4]和 (4,supremum] 这两个 next-key lock,以及主键索引上 id=4 这一行。
它的执行流程也比较简单,从表 t 中按照索引 c 倒序,扫描第一行,拿到结果写入到表 t2 中。
insert into t(c,d) (select c+1, d from t force index(c) order by c desc limit 1);
如果执行这句sql,可以看到,这时候的 Rows_examined 的值是 5。并且使用了临时表。
Explain 结果 rows=1 是因为受到了 limit 1 的影响。可能不准确。
使用执行Innodb_rows_read 语句查看查看sql执行前后扫描行数。
可以看到,这个语句执行前后,Innodb_rows_read 的值增加了 4。因为默认临时表是使用 Memory 引擎的,所以这 4 行查的都是表 t,也就是说对表 t 做了全表扫描。
所以整个执行流程:
这个语句会导致在表 t 上做全表扫描,并且会给索引 c 上的所有间隙都加上共享的 next-key lock。
这个语句的执行为什么需要临时表,原因是这类一边遍历数据,一边更新数据的情况,如果读出来的数据直接写回原表,就可能在遍历过程中,读到刚刚插入的记录,新插入的记录如果参与计算逻辑,就跟语义不符。
由于实现上这个语句没有在子查询中就直接使用 limit 1,从而导致了这个语句的执行需要遍历整个表 t。
优化方案:
create temporary table temp_t(c int,d int) engine=memory;
insert into temp_t (select c+1, d from t force index(c) order by c desc limit 1);
insert into t select * from temp_t;
drop table temp_t;
session A 执行的 insert 语句,发生唯一键冲突的时候,并不只是简单地报错返回,还在冲突的索引上加了锁。session A 持有索引 c 上的 (5,10] 共享 next-key lock(读锁)。
这个读锁作用上来看,这样做可以避免这一行被别的事务删掉。
执行相同的 insert 语句,发现了唯一键冲突,加上读锁(Next-key lock)。session A 回滚,session B 和 session C 都试图继续执行插入操作,都要加上插入意向锁(LOCK_INSERT_INTENTION)。
语义的逻辑是,插入一行数据,如果碰到唯一键约束,就执行后面的更新语句。
insert into t values(11,10,10) on duplicate key update d=100;
如果有多个列违反了唯一性约束,就会按照索引的顺序,修改跟第一个索引冲突的行。
如果可以控制对源表的扫描行数和加锁范围很小的话,我们简单地使用 insert … select 语句即可实现。
表结构:
create database db1;
use db1;
create table t(id int primary key, a int, b int, index(a))engine=innodb;
delimiter ;;
create procedure idata()
begin
declare i int;
set i=1;
while(i<=1000)do
insert into t values(i,i,i);
set i=i+1;
end while;
end;;
delimiter ;
call idata();
create database db2;
create table db2.t like db1.t
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
//导出
select * from db1.t where a>900 into outfile '/server_tmp/t.csv';
//导入
load data infile '/server_tmp/t.csv' into table db2.t;
在 MySQL 5.6 版本引入了可传输表空间(transportable tablespace) 的方法,可以通过导出 + 导入表空间的方式,实现物理拷贝表的功能。
假设我们现在的目标是在 db1 库下,复制一个跟表 t 相同的表 r:
先创建一个用户:
create user 'ua'@'%' identified by 'pa';
这条命令做了两个动作:
// 增加权限
grant all privileges on *.* to 'ua'@'%' with grant option;
// 取消权限
revoke all privileges on *.* from 'ua'@'%';
将上述第1步权限字段的值 N 全改为 Y;把上述第2步内存数组 acl_users 全改为1。
grant all privileges on db1.* to 'ua'@'%' with grant option;
grant 操作对于已经存在的连接的影响,在全局权限和基于 db 的权限效果是不同的。如果当前会话已经处于某一个 db 里面, use 这个库的时候拿到的库权限会保存在会话变量中,所以 revoke 会不生效。
表权限定义存放在表 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;
正常情况下,grant 命令之后,没有必要跟着执行 flush privileges 命令,因为会同时刷新内存数据。
但当数据表中的权限数据跟内存中的权限数据不一致的时候,flush privileges 语句可以用来重建内存数据,达到一致状态。这种不一致往往是由不规范的操作导致的,比如直接用 DML 语句操作系统权限表。
CREATE TABLE `t` (
`ftime` datetime NOT NULL,
`c` int(11) DEFAULT NULL,
KEY (`ftime`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1
PARTITION BY RANGE (YEAR(ftime))
(PARTITION p_2017 VALUES LESS THAN (2017) ENGINE = InnoDB,
PARTITION p_2018 VALUES LESS THAN (2018) ENGINE = InnoDB,
PARTITION p_2019 VALUES LESS THAN (2019) ENGINE = InnoDB,
PARTITION p_others VALUES LESS THAN MAXVALUE ENGINE = InnoDB);
insert into t values('2017-4-1',1),('2018-4-1',1);
磁盘文件
由于分区表的规则,session A 的 select 语句其实只操作了分区 p_2018。
如果是MyISAM则锁表p_2018 。
分区表和手工分表,一个是由 server 层来决定使用哪个分区,一个是由应用层代码来决定使用哪个分表。因此,从引擎层看,这两种方式也是没有差别的。
主要区别在server 层上,分区表一个被广为诟病的问题:打开表的行为。
MyISAM 引擎每当第一次访问一个分区表的时候,MySQL 需要把所有的分区都访问一遍。MySQL 启动的时候,open_files_limit 参数使用的是默认值 1024,如果超过上限将报错。InnoDB 引擎的话,并不会出现这个问题。
如果从 server 层看的话,一个分区表就只是一个表。
虽然 session B 只需要操作 p_2107 这个分区,但是由于 session A 持有整个表 t 的 MDL 锁,就导致了 session B 的 alter 语句被堵住。
分区表的一个显而易见的优势是对业务透明,相对于用户分表来说,使用分区表的业务代码更简洁。还有,分区表可以很方便的清理历史数据。
按照时间分区的分区表,就可以直接通过 alter tablet drop partition …这个语法删掉分区,从而删掉过期的历史数据。
create table a(f1 int, f2 int, index(f1))engine=innodb;
create table b(f1 int, f2 int)engine=innodb;
insert into a values(1,1),(2,2),(3,3),(4,4),(5,5),(6,6);
insert into b values(3,3),(4,4),(5,5),(6,6),(7,7),(8,8);
其实就是下面这两种写法的区别:
select * from a left join b on(a.f1=b.f1) and (a.f2=b.f2); /*Q1*/
select * from a left join b on(a.f1=b.f1) where (a.f2=b.f2);/*Q2*/
Q1的explain:
Q1使用BNL算法,第 35 篇文章《join 语句怎么优化?》中讲过。
Q2的explain:
Q1使用NLJ算法,执行流程是这样的:顺序扫描表 b,每一行用 b.f1 到表 a 中去查,匹配到记录后判断 a.f2=b.f2 是否满足,满足条件的话就作为结果集的一部分返回。
差别
在 MySQL 里,NULL 跟任何值执行等值判断和不等值判断的结果,都是 NULL。所以 Q2 没有1和2。
Q2这条语句虽然用的是 left join,但是语义跟 join 是一致的。优化器会把Q2优化成join。因为表 a 的 f1 上有索引,就把表 b 作为驱动表,这样就可以用上 NLJ 算法。使用show warning;可以看到优化后的语句。
所以使用 left join 时,左边的表不一定是驱动表。
如果需要 left join 的语义,就不能把被驱动表的字段放在 where 条件里面做等值判断或不等值判断,必须都写在 on 里面。
再来看两条sql:
select * from a join b on(a.f1=b.f1) and (a.f2=b.f2); /*Q3*/
select * from a join b on(a.f1=b.f1) where (a.f2=b.f2);/*Q4*/
在这种情况下,join 将判断条件是否全部放在 on 部分就没有区别了。
Simple Nested Loop Join 算法,其实也是把数据读到内存里,然后按照匹配条件进行判断,为什么性能差距会这么大呢?
解释这个问题,需要用到 MySQL 中索引结构和 Buffer Pool 的相关知识点:
select a from t group by a order by null;
select distinct a from t;
group by 没有聚合函数,这两句sql的效率相同。
第 39 篇文章《自增主键为什么不是连续的?》评论区,@帽子掉了 同学问到:在 binlog_format=statement 时,语句 A 先获取 id=1,然后语句 B 获取 id=2;接着语句 B 提交,写 binlog,然后语句 A 再写 binlog。这时候,如果 binlog 重放,是不是会发生语句 B 的 id 为 1,而语句 A 的 id 为 2 的不一致情况呢?
不会,虽然 statement 格式下“自增 id 的生成顺序,和 binlog 的写入顺序可能是不同的”。
create table t(id int auto_increment primary key);
insert into t values(null);
主库上语句 A 的 id 是 1,语句 B 的 id 是 2,但是写入 binlog 的顺序先 B 后 A,那么binlog 就变成:
SET INSERT_ID=2;
语句 B;
SET INSERT_ID=1;
语句 A;
表定义的自增值达到上限后的逻辑是:再申请下一个 id 时,得到的值保持不变。
create table t(id int unsigned auto_increment primary key) auto_increment=4294967295;
insert into t values(null);
// 成功插入一行 4294967295
show create table t;
/* CREATE TABLE `t` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4294967295;
*/
insert into t values(null);
//Duplicate entry '4294967295' for key 'PRIMARY'
主键冲突,如果 4 个字节无符号整型 (unsigned int) 不够用的情况下,可以使用 8 个字节的 bigint unsigned。
如果你创建的 InnoDB 表没有指定主键,那么 InnoDB 会给你创建一个不可见的,长度为 6 个字节的 row_id。InnoDB 维护了一个全局的 dict_sys.row_id 值,所有无主键的 InnoDB 表,每插入一行数据,都将当前的 dict_sys.row_id 值作为要插入数据的 row_id,然后把 dict_sys.row_id 的值加 1。
如果到达上限后,再有插入数据的行为要来申请 row_id,拿到以后再取最后 6 个字节的话就是 0,然后继续循环。所以会导致覆盖数据。
redo log 和 binlog 相配合的时候,它们有一个共同的字段叫作 Xid。它在 MySQL 中是用来对应事务的。
MySQL 内部维护了一个全局变量 global_query_id,每次执行语句的时候将它赋值给 Query_id,然后给这个变量加 1。如果当前语句是这个事务执行的第一条语句,那么 MySQL 还会同时把 Query_id 赋值给这个事务的 Xid。
而 global_query_id 是一个纯内存变量,重启之后就清零了。所以你就知道了,在同一个数据库实例中,不同事务的 Xid 也是有可能相同的。但是 MySQL 重启之后会重新生成新的 binlog 文件,这就保证了,同一个 binlog 文件里,Xid 一定是唯一的。
不过 global_query_id 达到上限后,会继续从 0 开始计数,由于 global_query_id 为8个字节,所以一般不会出现到达上限的情况。
Xid 是由 server 层维护的。InnoDB 内部使用 Xid ,就是为了能够在 InnoDB 事务和 server 之间做关联。但是,InnoDB 自己的 trx_id,是另外维护的。
InnoDB 内部维护了一个 max_trx_id 全局变量,每次需要申请一个新的 trx_id 时,就获得 max_trx_id 的当前值,然后并将 max_trx_id 加 1。
InnoDB 数据可见性的核心思想是:每一行数据都记录了更新它的 trx_id,当一个事务读到一行数据的时候,判断这个数据是否可见的方法,就是通过事务的一致性视图与这行数据的 trx_id 做对比。
对于正在执行的事务,你可以从 information_schema.innodb_trx 表中看到事务的 trx_id。
但是对于只读事务,InnoDB 并不会分配 trx_id。
max_trx_id 会持久化存储,重启也不会重置为 0,那么从理论上讲,只要一个 MySQL 服务跑得足够久,就可能到达上限,然后从 0 开始的情况。然后就会导致脏读。但只存在理论上,如果一个 MySQL 实例的 TPS 是每秒 50 万,持续这个压力的话,在 17.8 年后,就会出现这个情况。
show processlist 里面的第一列,就是 thread_id。
系统保存了一个全局变量 thread_id_counter,每新建一个连接,就将 thread_id_counter 赋值给这个新连接的线程变量。
thread_id_counter 定义的大小是 4 个字节,到达上限则从0开始。
45讲结束啦,前面的差不多都忘了,哈哈哈!~