Open annidy opened 1 week ago
redo log就是常用的WAL(Write-Ahead log),它是InnoDB才支持的特性。 binlog(归档)是Server层的日志,它是逻辑日志,记录的是操作日志。
典型的日志写入顺序为 redo log(prepare) -> binlog -> redo log(commit)。redo log的二次提交主要是为了crash恢复:在binlog写入之前crash的下次启动回滚,否则commit;binlog 则是为了数据库回滚,比如误删数据库,可以利用上一次的备份+binlog前一次操作恢复到删除之间的状态。
binlog日志支持3种记录模式1. Statement 记录SQL语句;2. ROW 记录更新前后的行;3. Mix 混合模式。第二种方式有利于恢复,但是空间占用比较大。
查看记录模式使用SHOW VARIABLES LIKE 'binlog_format';
简单的mysqldump备份
mysqldump -u $MYSQL_USER -p$MYSQL_PASS $DATABASE | gzip > "$BAK_DIR/$DATABASE-$BAK_DATE.sql.gz"
mysql -u 用户名 -p 密码 数据库名 < /path/to/your/backup_yesterday.sql
SHOW MASTER LOGS;
或SHOW BINARY LOGS;
查看昨天binlog的结束时间mysqlbinlog --start-position=开始位置 --stop-datetime="YYYY-MM-DD HH:MM:SS" /path/to/binlogs/binlog_file_1 | mysql -u 用户名 -p 密码
以上是一个比较粗劣的过程。注意恢复期间需要停止mysql服务。
SQL 标准的事务隔离级别包括:读未提交(read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(serializable )
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。在“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图。在“读提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的。这里需要注意的是,“读未提交”隔离级别下直接返回记录上的最新值,没有视图概念;而“串行化”隔离级别下直接用加锁的方式来避免并行访问。
查看隔离级别
show variables like 'transaction_isolation';
每个对数据的修改都有回滚日志,通过read view的索引就可以知道事务视图创建时的值时什么。 当read view没有任何事务引用时,视图会自动清理。所以建议不要使用长事务,会妨碍清理。
另外事务还会影响ALTER表。
可以去information_schema.innodb_trx表中查看正在运行的事务
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>60
隐性事务是指没有用begin命令启动的事务。当set autocommit=0
时,执行SQL语句(比如SELECT)默认开启了一个事务,而且这个事务不会提交,直到主动commit/rollback或线程or连接结束。
默认autocommit=1,即执行一条SQL语句后自动提交(原子操作)。某些ORM为了方便开启了隐性事务,对并发性能影响很大。
最常用的三种索引数据结构:哈希表、有序数组和搜索树。哈希表不适用于范围查找,只在KV数据库中有用。有序数组插入删除太慢,一般只用于固定数据集。因此,数据库的选择只剩搜索树。
二叉树最接近理论上限,但是它的需要多次跳转,对磁盘访问不友好,所以数据都使用的N叉树。(跳表、LSM搜索树在有些场景中也在使用)
每一个索引都对应一个B+树。每个表都有一个主键,每一行按照主键的顺序保存在B+树中。 示例:
create table T(id int primary key, k int not null, index(k)) engine=InnoDB;
R1~R5 的 (ID,k) 值分别为 (100,1)、(200,2)、(300,3)、(500,5) 和 (600,6),两棵树的示例示意图如下。
主键索引下存的是整行数据,非主键索引下存的是主键的值。 所以非主键查询有一个回表过程,相对主键会慢一点。
索引在磁盘中需要保持顺序,因此主键最好是自增的(AUTO_INCREMENT),这样写入新数据直接在后面,性能会比较好(为什么UUID不适合做主键,原因在此)。使用业务字段做主键,需要考虑二级索引保存主键的成本(列举了一个身份证号的例子)。
alter table T drop index k;
alter table T add index(k);
重建索引能清理B树中因为删除而导致的空洞,所以有一定的现实意义。但是如果要重建主键,正确的写法是
alter table T engine=InnoDB;
虽然把上面的index改为primary key语法没有错,但是drop主键后,表会重新创建一个默认的rowid作为主键,重新创建表,此时再add主键又重复建表的操作,等于第一次drop完全没有意义。
索引覆盖是指索引的值里有我们想要的数据,不需要回表。由于索引中保存的是主键,如果我们把主键扩展,加入一些业务字段,称之为联合索引。
CREATE TABLE `tuser` (
`id` int(11) NOT NULL,
`id_card` varchar(32) DEFAULT NULL,
`name` varchar(32) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`ismale` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `id_card` (`id_card`),
KEY `name_age` (`name`,`age`)
) ENGINE=InnoDB
这里name_age就是联合索引。表创建后,可以通过id_card很快的查找到name和age,同时基于最左前缀原则,name的搜索也很快。
INDEX和KEY关键字是同义词,也可以用下面的方式创建 CREATE TABLE tuser( id int PRIMARY KEY, id_card varchar(32) DEFAULT NULL, name varchar(32) DEFAULT NULL, age int(11) DEFAULT NULL, ismale tinyint(1) DEFAULT NULL, INDEX(id_card), -- 单独为id_card创建索引 INDEX(name_age (name, age)) -- 为name和age创建联合索引,并命名该索引为name_age ) ENGINE=InnoDB;
索引下推是另一种数据库优化索引使用的技术,比如select * from tuser where name like '张%' and age=10 and ismale=1;
,age的条件也可以使用name_age索引。
全局锁可以阻止数据库上所有写操作,常用于备份、校验操作
FLUSH TABLES WITH READ LOCK; -- 上锁
UNLOCK TABLES; -- 解锁
这个操作比较少用,一般的备份工具比如mysqldump,都是通过开启一个事务方式来确保拿到一致性视图
事务不局限于单个表,开启事务整个数据库都有一个快照
表锁加锁的粒度更低、更细,可以阻止读。列表全局锁是读写锁,表锁是互斥
LOCK TABLES t1 READ, t2 WRITE; -- 上锁。表 t1 禁止别人读
UNLOCK TABLES; -- 解锁
现实中,遇到的表锁同时是MDL(metadata lock),当我们alter table(DDL)时,引擎默认加一个MDL写锁,其他人在使用表时,默认加MDL读锁。 如果数据库一直有人在SELECT和INSERT表,是不是ALTER就没机会执行了?在操作系统中这是可能的(饥饿),数据库会用排队方式解决这个问题,但是会导致ALTER后面的查询都阻塞了。同时ALTER也会提示
Waiting for table metadata lock
前面提到开发者可能不小心引入了长事务,长事务也会加MDL读锁!MDL的锁也有超时,超时后会提示
Lock wait timeout exceeded; try restarting transaction
这个值是通过innodb_lock_wait_timeout控制,默认是50s。但是客户端的超时一般更短,超时重试又会新建一个session,如果没有规避策略,业务服务器有可能很快被打爆。
ALTER导致的线上事故有很多,规避方式总结下来主要有:
SHOW PROCESSLIST;
找到锁等待的ID,KILL掉前面提过,MySQL通过read view实现多事务读,但是更新操作需要加行锁,且行锁的释放需要等到事务结束。所以当两个事务更新同一行数据时,另一个必须等前一个事务提交后才能继续。
由于行锁太多,现实中必然会出现死锁。MySQL的策略是发起死锁检测,如果发现两个事务死锁了,就主动回滚其中的某个事务。 死锁检测的复杂度是O(n)。注意这个检测是不管有没有死锁,只要事务更新时遇到了行锁,就会发起死锁检测。
死锁检测可以关闭,但不推荐。
数据库CPU飙高,但TPS不高,有可能是死锁检测的锅
查看事务统计的命令 SHOW GLOBAL STATUS LIKE 'Com_commit'; -- 数据库上次启动以来已经提交的事务数量 SHOW GLOBAL STATUS LIKE 'Com_rollback'; -- 数据库上次启动以来已经回滚的事务数量
死锁不会损坏数据库,只会让事务回滚,客户端需要做重试。但是生产中还是需要尽量避免死锁
事务启动时刻一般不是
begin
的时间,而是第一次访问数据的时刻
InnoDB在实现时,为每个事务构造了一个数组,数组中保存事务启动瞬间,还活跃的事务ID集合。上面第3步实际上是”比事务id小且不在活跃事务ID集合中“,否则可能看到前一个事务未提交的数据。
上图中,事务B的set k=k+1是在102版本上累加;如果是先k'=get k,再set k=k‘+1,结果会不一样。
set k=k+1称之为当前读,它看起来有悖快照隔离(不是可重复读,像是读已提交那种隔离级别),但大部分SQL都是这样实现的(为了效率)。
如果希望k'=get k和set k=k+1的效果一致,需要给它加锁
select k from t where id=1 for update; -- 排他锁,可阻止其事务写
但是,加了锁就不支持多事务并发。魔鬼藏在细节中。
update使用的是当前读,它看到的数据和select返回的不一样。比如
begin;
select * from t;
update t set k=0 where id=k;
select * from t;
commit;
由于事务开始后做了一次select,建立了快照,后面的select是用的前面的快照,而条件where语句中的id和k,不是快照的值,是redo log中最新的数据。
两种索引都是B树,区别是重复索引在查找的时候,需要判断下一个是不是重复值。性能差距不大。 拉开差距的是InnoDB实现是引入的特性。change buffer主要用于提高对非聚集索引(非唯一普通索引)更新操作的性能。当对索引进行插入、删除或更新操作时,如果该索引页尚未加载到Buffer Pool中,InnoDB会将这些变更操作暂时存储在Change Buffer中。之后,当相关索引页由于其他查询而被读入Buffer Pool时,Change Buffer中累积的变更会与索引页合并(Merge),这个过程称为“Change Buffer Merge”。同时,后台线程也会定期同步change buffer到磁盘。
SHOW VARIABLES LIKE 'innodb_change_buffering';
有all、none、inserts几种可选,默认是all,表示插入、删除、更新操作都开启缓存。
一般只有插入后需要立马消费才有必要关闭成none
唯一索引只会增加数据库的压力。现实中往往是业务不想保证插入的唯一性,而是希望数据库来做约束,才会使用唯一索引。
唯一索引引入的问题:插入数据慢,数据量大时每次写盘会很明显变慢。所以,如果业务能保证数据插入唯一,最好不要在数据库侧做约束。
解释执行计划功能打印的内容包括:可能用到的索引、扫描行数,过滤百分比。扫描的行数越少说明效率越高。
mysql> explain select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+------------------------------------+
| 1 | SIMPLE | t | NULL | range | a,b | b | 5 | NULL | 48973 | 1.02 | Using index condition; Using where |
+----+-------------+-------+------------+-------+---------------+------+---------+------+-------+----------+------------------------------------+
1 row in set, 1 warning (0.00 sec)
另一种就是查看慢日志,这个会更精确一些
# Time: 2024-06-27T07:49:17.128053Z
# User@Host: root[root] @ localhost [127.0.0.1] Id: 14
# Query_time: 0.086870 Lock_time: 0.000002 Rows_sent: 0 Rows_examined: 50001
SET timestamp=1719474557;
select * from t where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;
MySQL在真正开始执行语句之前,并不能精确地知道满足这个条件的记录有多少条,而只能根 据统计信息来估算记录数。 这个统计信息就是索引的“区分度”。显然,一个索引上不同的值越多,这个索引的区分度就越 好。而一个索引上不同的值的个数,我们称之为“基数”(cardinality)。也就是说,这个基数越 大,索引的区分度越好。
mysql> show index from t;
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| t | 0 | PRIMARY | 1 | id | A | 0 | NULL | NULL | | BTREE | | | YES | NULL |
| t | 1 | a | 1 | a | A | 0 | NULL | NULL | YES | BTREE | | | YES | NULL |
| t | 1 | b | 1 | b | A | 0 | NULL | NULL | YES | BTREE | | | YES | NULL |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
3 rows in set (0.00 sec)
上面显示统计值是0,是有问题的。补救方法是重新分析并修正索引统计信息
analyze table t;
MySQL优化器选错索引无法避免,我们可以用force index强制MySQL使用某个索引,但这个有一定风险且不通用。 修改查找的后置条件(order by)也许可以提示优化器,会有一点玄学。 最后作者推荐是删除不必要的索引,最直接。
字符串的特点是可以利用前缀比较解决查询问题,索引针对字符串的索引可以设置前缀长度
alter table SUser add index idx6(email(6)); -- 修改前缀长度为6
使用前缀的优势是索引的B树需要的存储空间更小,缺点是查找可能需要更多次比较。
下面的SQL可以计算不同前缀长度的区分度,可以辅助决策前缀长度
select
count(distinct left(email,4))as L4,
count(distinct left(email,5))as L5,
count(distinct left(email,6))as L6,
count(distinct left(email,7))as L7,
from SUser;
文中有两种方案:倒序存储和hash字段。都是通过优化存储的方式来实现的。
MySQL的索引除了BTree,还有其它几种不太常见的算法,列举如下:
引发数据库flush的几种场景
作者提出的优化方案是控制刷脏页的速度,innodb_io_capacity 这个值在 MySQL 的 InnoDB 存储引擎中代表系统每秒可以进行的 I/O 操作数量。 SSD硬盘可以配置为0,代表可以全力刷。在我老旧的mysql5.6,这个值是200.
delete命令,删除数据只是把记录or页标记为删除,他们下次有需要时还会复用。
drop table倒是可以清理文件,但这不是我们想要的。
如果要消除『空洞』,需要进行重建表。原理基本上是新建一个临时表,然后按照顺序把数据复制过去。由于是按主键顺序,所以不会出现插入空洞。等效的命令是
alter table T engine=InnoDB;
alter操作会导致MDL加锁的,不过MySQL对这种情况有优化,会尝试以ALGORITHM=inplace的方式执行。表锁的时间非常短,后面就变成了行锁,不会阻塞DML(数据操作语言,如INSERT、UPDATE、DELETE)操作。
统计行数是InnoDB需要把每行都读出来再计算总和。由于索引的count和表的count是相同的,mysql会优化读索引最小的那颗B树。
优化获取总行的方案主要有两个:1. 使用外存保存总和,比如redis;2. 用单独一张表。用表的方案会比较精确一些,可以在事务中添加行。
count(字段)<count(主键id<count(1)≈count() 。count()是特殊优化过的,建议取长度都用这种方式。
当使用where+order by时,具体的实现原理是先按照city取出所有的数据,然后再进行排序
select city,name,age from t where city='杭州' order by name limit 1000 ;
排序的过程可能发生在文件上(归并排序),取决于设置的sort_buffer_size大小。
创建一个city和name的索引,可以完美解决二次排序问题
alter table t add index city_user(city, name);
必须是联合索引,单独的name索引解决不了上面的问题。
本章的重点是分析下面的语句为什么性能第
select word from words order by rand() limit 3;
其中,order by rand()
被认为不是一个高效的操作。
对于上面的操作,它需要先把所有的words取出来并建立一个新的临时表,(以下是我猜测)临时表有一个随机值的列,同时记录前3个随机值,将临时表的随机列重新排序,使用类似where的语句查找的记录的3个随机值。
mysql> explain select word from words order by rand() limit 3;
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+---------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+---------------------------------+
| 1 | SIMPLE | words | NULL | ALL | NULL | NULL | NULL | NULL | 10000 | 100.00 | Using temporary; Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+---------------------------------+
MySQL随机值需要排序这个问题可能后面会被优化,这也是声明式语言的优势
在业务层模拟随机的方案很多。最通用的解法是计算中的行数,然后随机挑选一行。用SQL实现是
select count(*) into @C from words;
set @Y = floor(@C * rand());
select word from words limit @Y, 1;
limit X, Y表示跳过X行,返回Y行。由于没有用到索引,时间复杂度O(n)
有些数据库用offset
列举了一些常见的错误使用的场景
条件字段函数操作 在条件语句中使用函数时,无法利用索引快速查找。比如
select count(*) from t where month(t_modified)=7;
select count(*) from t where t_modified >= '2024-7-1' and t_modified < '2024-8-1';
month函数破坏了索引查找,最终只能是遍历所有索引(全索引扫描)。
隐式类型转换 字符串和数字比较时,引擎先将字符串转换为数字再比较。这点跟JavaScript很类似。
隐式字符编码转换 作者这里故意构造一个联级查询,两个表是字符编码不同,然后查询条件判断是否相等。
上面三个例子,都是一个原因:对索引字段做函数操作,可能会破坏索引值的有序性,因此优化器就决定放弃走树搜索功能。
where id+1=1000 这个条件语句也破坏了索引,要改写成 where id=1000-1。这纯粹是优化器偷懒了。
锁问题第一时间用 show processlist;
命令是分析是那个过程加锁了。
没有命中索引是最常见的一种慢查询。
还列举了一个极限场景,read view非常长,如果select没有加lock in share mode
,就要回溯read view,也可能花很多时间。
幻读的定义是:在一个事务中,同一个范围的查询,后一次查询看到了前一次查询没有看到的行。 这个是很常见的,在可重复读的隔离下,也可以看到其他事务插入的数据(注意是插入,MVCC是能屏蔽更新的)。
幻读的根因是普通的行锁无法阻止新的插入,但是间隙锁可以。
在InnoDB中,当你使用SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE语句时,如果查询条件使用了范围条件(如BETWEEN、<、>;等),并且查询的索引是唯一的(如主键索引或唯一索引),InnoDB可能会自动加上间隙锁。
另外一种间隙锁没有范围条件,当where条件的key不存在是,也会在B树的前后加上间隙锁。
begin;
select * from t where d=9 for update;
insert into t(id, c) values (8, 8); -- ok,但是其它事务执行这条语句会阻塞
如果d=9不存在,但是存在d=5和d=10,那么在它两个中间是有间隙锁的。
作者总结的加锁规则里面,包含了两个“原则”、两个“优化”和一个“bug”。
汇总下来,就是关于next-key lock的行为理解。这是MySQL最细粒度的锁,它不同于普通的锁,是包含范围的。因为B树中可能有空洞,锁必须保证空洞的安全。插入、删除某个索引时,理论上也要基于前开后闭的方式获取前后行的锁。 当然,如果有可能优化(比如唯一索引),退化为行锁影响就小很多了。
01 | 基础架构:一条SQL查询语句是如何执行的?
分析器是分析这条SQL是的语法是否正确,是否是查询语句,执行的是个什么操作。 优化器分析这条查询语句怎么执行,使用那个索引。由于SQL是声明式语言,如果里面有多个查询过程,执行顺序没有定义。优化器需要计算不同执行的效率,这里的优化空间不小。 慢查询通过
long_query_time
配置(查询同上),默认10s。慢查询日志slow_query_log_file
开启命令:执行器翻译优化器的指令,调用引擎的接口,(引擎提供的接口类似I/O),把结果存发给客户端。执行器不缓存数据? 引擎则是真实的读取每一行或查询索引