MySQL45讲总结
参数和命令
事务
-
set autocommit=1
- 可以预防不知情的情况下产生长事务
- 没有手动start transaction开启事务,mysql默认也会将用户的操作当做事务即时提交
-
set max_execution_time
- 设置语句执行最长时间
-
innodb_undo_tablespaces
-
select * from information_schema.innodb_trx;
- 查询当前执行中的事务
索引
-
change buffer
-
前身是insert buffer
-
merge
- 把change buffer应用到旧的数据页,得到新的数据页的过程称为merge
-
3种触发时机
- 访问这个数据页
- 后台线程,定期触发merge
- 数据库正常关闭,执行merge
-
好处
- 减少读磁盘,避免占用内存,提高内存利用率
-
局限性
- 唯一索引的更新不能使用
-
大小
- innodb_change_buffer_max_size(该值为buffer pool的百分比)
-
-
优化器选错索引几种方式
- force index
- 删掉一些索引
- 新建更合适的索引
- 子主题 4
-
基数:show index from table_name里面有个cardinality ,基数越大,索引区分度越好
- 索引基数统计是采样统计,Innodb默认选择N个数据页,统计这些页面上的不同值,得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数。
- 而数据表是会持续更新的,索引统计信息也不会固定不变。所以,当变更的数据行数超过1/M的时候,会自动触发重新做一次索引统计
-
innodb_stats_persistent 索引统计的方式
- on:表示统计信息会持久化存储,默认N是20,M是10
- off:表示统计信息只会存储在内容中,N是8,M是16
慢查询
- long_query_time
analyze table t 重新统计索引信息
线程
-
innodb_thread_concurrency
- 控制InnoDB的并发线程上限。
- 就是说,一旦并发线程数达到这个值,InnoDB在接收到新请求的时候,就会进入等待状态,直到有线程退出。
kill
-
kill query + 线程id
- 终止这个线程中正在执行的语句
- 场景:锁等待、死锁
-
实现流程:
- 把session B的运行状态改成THD::KILL_QUERY(将变量killed赋值为THD::KILL_QUERY);
-
给session B的执行线程发一个信号。
- 发一个信号的目的,就是让session B退出等待,来处理THD::KILL_QUERY状态。
-
kill connection + 线程id
- 这里connection可缺省,表示断开这个线程的连接,当然如果这个线程有语句正在执行,也是要先停止正在执行的语句的
-
kill 失效
-
线程没有执行到判断线程状态的逻辑
-
终止逻辑耗时较长
-
超大事务执行期间被kill。这时候,回滚操作需要对事务执行期间生成的所有新数据版本做回收操作,耗时很长。
-
大查询回滚。如果查询过程中生成了比较大的临时文件,加上此时文件系统压力大,删除临时文件可能需要等待IO资源,导致耗时较长
-
DDL命令执行到最后阶段,如果被kill,需要删除中间过程的临时文件,也可能受IO资源影响耗时
3 问题1 Ctrl+c,是不是可以直接终止线程呢?
答案是,不可以;这里有一个误解,其实在客户端的操作只能操作到客户端的线程,客户端和服务端只能通过网络交互,是不可能直接操作服务端线程的
-
-
而由于MySQL是停等协议,所以这个线程执行的语句还没有返回的时候,再往这个连接里面继续发命令也是没有用的。实际上,执行Ctrl+C的时候,是MySQL客户端另外启动一个连接,然后发送一个kill query 命令。
-
net_buffer
- 由参数net_buffer_length控制默认16kb
join_buffer
- 由参数join_buffer_size设定的,默认值是256kb
技巧部分
回表
页分裂
- 子主题 1
自增主键好处
- 主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小
重建索引
-
analyze table t
- 发现explain的结果预估的rows值跟实际情况差距比较大,可以采用这个方法来处理
身份证号建立索引
- reverse 倒序存储身份证:缺点多一次cpu计算,依然是前缀索引,会增加扫描行数
- hash: crc32,缺点冲突
隐式类型转换
- 规则:mysql在字符串与数字比较的时候,把字符串转为数字
- 隐式转换相当于把字段上加上函数,所以使用不了索引
-
常见的有类型转换、字符集转换
- 解决方案:把值转为相同类型,不要转字段 如 a=cast(b as unsigned int)
随机显示3条信息
-
内存临时表
- order by rand() 不可取,读取行数太多
- 使用rowid方法
- tmp_table_size这个配置限制了内存临时表的大小,默认值是16M,超过了就会变成磁盘临时表
-
磁盘临时表
- internal_tmp_disk_storage_engine默认引擎是innodb
-
解决方案
- select count(*) into @C from t;
set @Y1 = floor(@C * rand());
set @Y2 = floor(@C * rand());
set @Y3 = floor(@C * rand());
select * from t limit @Y1,1; //在应用代码里面取Y1、Y2、Y3值,拼出SQL后执行
select * from t limit @Y2,1;
select * from t limit @Y3,1;
- select count(*) into @C from t;
QPS 突增的问题
- 确定,业务方很快会回滚,如果有白名单,去掉白名单里面值
- 新功能 单独数据库、把用户删掉
- 与主功能耦合,只能修改压力大的语句为 select 1
MySQL IO瓶颈
- 设置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count参数,减少binlog的写盘次数。这个方法是基于“额外的故意等待”来实现的,因此可能会增加语句的响应时间,但没有丢失数据的风险。
- 将sync_binlog 设置为大于1的值(比较常见是100\~1000)。这样做的风险是,主机掉电时会丢binlog日志
- 将innodb_flush_log_at_trx_commit设置为2。这样做的风险是,主机掉电的时候会丢数据。
MySQL 高可用
-
主备延迟
-
show slave status
- seconds_behind_master 显示落后时间,单位秒
-
即便主备设置的时间不一致,也不会造成影响,会自动扣掉时间,因为备库会获取主库的时间,不一致的时候,会自动减掉差值
- 如备库时间为38而 主库时间为35那么差值就是3,执行时间A-主库写入时间B-3 就是落后的时间
-
延迟原因
-
主备物理机性能差
-
备库压力大,不加限制的读,cpu飙升,影响同步速度
- 解决方案:binlog 输送到外部系统
-
大事务:因为主库上必须等事务执行完成才会写入binlog,再传给备库
-
产生大事务的原因
- delete 删除数据太多
- 大表的DDL
-
-
备库的并行复制
- 把原来的单线程换成多个工作线程
- slave_parallel_workers 决定工作线程的个数
-
-
对应策略
-
可靠性优先
- 始终判断seconds_behind_master 小于x,直到满足条件,否则一直判断,然后主库只读,再判断sbm(seconds_behind_master)=0,把备库 改为可写
- 都改为只读的时候,数据库有段时间不可写
-
可用性优先
- 直接切备库,没有不可用时间,会出现数据不一致
-
-
-
主备切换
-
基于位点的主备切换
- 从主库的master_log_name文件的master_log_pos这个位置的日志继续同步。而这个位置就是我们所说的同步位点,也就是主库对应的文件名和日志偏移量
-
问题:跳过一个事务、跳过指定错误
- 1062错误是插入数据时唯一键冲突;
1032错误是删除数据时找不到行。 - slave_skip_errors
- set global sql_slave_skip_counter = 1; 跳过事务
- 1062错误是插入数据时唯一键冲突;
-
基于GTID的主备切换
-
Global Transaction Identifier,也就是全局事务ID
*
-
组成:GTID=source_id\:transaction_id
-
启动:gtid_mode=on和enforce_gtid_consistency=on就可以了。
-
生成方式
- gtid_next=automatic,代表使用默认值
-
-
读写分离
- 目的:分摊主库的压力
- 实现方式:客户端主动做负载均衡,把数据库连接信息放在客户端的链接查询,在mysql与客户端之间有个中间代理层
-
主从延迟
-
强制走主库方案
- 特殊场景下直接走主库
-
sleep方案
-
判断主备无延迟方案
-
show slave status的结果里 seconds_behind_master参数的值,先判断是否等于0 等于0再执行查询请求
-
对比点位确保主备无延迟
- Master_Log_File和Read_Master_Log_Pos,表示的是读到的主库的最新位点
- Relay_Master_Log_File和Exec_Master_Log_Pos,表示的是备库执行的最新位点。如果Master_Log_File和Relay_Master_Log_File、Read_Master_Log_Pos和Exec_Master_Log_Pos这两组值完全相同,就表示接收到的日志已经同步完成
-
对比GTID集合确保主备无延迟
- Auto_Position=1 ,表示这对主备关系使用了GTID协议
- Retrieved_Gtid_Set,是备库收到的所有日志的GTID集合
- Executed_Gtid_Set,是备库所有已经执行完成的GTID集合。
如果这两个集合相同,也表示备库接收到的日志都已经同步完成。 - 特点:对比位点和对比GTID这两种方法,都要比判断seconds_behind_master是否为0更准确
-
-
配合semi-sync的方案
-
半同步复制,semi-sync replication
- 事务提交的时候,主库把binlog发给从库;
-
-
从库收到binlog以后,发回给主库一个ack,表示收到了;
主库收到这个ack以后,才能给客户端返回“事务完成”的确认。
也就是说,如果启用了semi-sync,就表示所有给客户端发送过确认的事务,都确保了备库已经收到了这个日志。
- 等主库位点方案
- select master_pos_wait(file, pos[, timeout]);
- 这条命令的逻辑如下:
它是在从库执行的;
参数file和pos指的是主库上的文件名和位置;
timeout可选,设置为正整数N表示这个函数最多等待N秒。
- 等GTID方案
- select wait_for_executed_gtid_set(gtid_set, 1);
- 等待,直到这个库执行的事务中包含传入的gtid_set,返回0,
超时返回1
判断数据库是不是出问题
-
select 1?
- 只能说明这个库的进程还在,并不能说明主库没问题
-
查表判断
- 建立一张表,只有一行数据,定期执行
-
更新判断
- 常见做法是放一个timestamp字段,用来表示最后一次执行检测的时间。
- 缺点:主备之间,都用相同的更新命令就可能出现行冲突,也就是可能会导致主备同步停止。所以,现在看来mysql.health_check 这个表就不能只有一行数据了。
为了让主备之间的更新不产生冲突,我们可以在mysql.health_check表上存入多行数据,并用A、B的server_id做主键。
-
内部统计
-
5.6版本之后
- performance_schema库,表 file_summary_by_event_name表里统计了每次IO请求的时间
- select * from performance_scheme.file_summary_by_event_name='wait/io/file/innodb/innodb_log_file
- 打开会有10%左右的性能损耗
-
打开redo log的时间监控
- update setup_instruments set ENABLED='YES', Timed='YES' where name like '%wait/io/file/innodb/innodb_log_file%';
-
检测:select event_name,MAX_TIMER_WAIT FROM performance_schema.file_summary_by_event_name where event_name in ('wait/io/file/innodb/innodb_log_file','wait/io/file/sql/binlog') and MAX_TIMER_WAIT>200*1000000000;
- 单次IO请求时间超过200毫秒属于异常,
-
误删数据-提桶跑路
-
场景
-
1 使用delete语句误删除数据行
- 方案:用flashback工具通过闪回把数据恢复,Flashback恢复数据的原理,是修改binlog的内容,拿回原库重放。而能够使用这个方案的前提是,需要确保binlog_format=row 和 binlog_row_image=FULL。
-
2 使用drop table 或者 truncate table 语句误删数据表
- 没法用flashback 恢复,因为binlog语句只有一个truncate/drop ,即便配置了binlog_format=row
- 解决方案:使用全量备份,加增量日志的方式
-
3 使用drop database 语句误删除库
-
4 使用rm命令误删整个MySQL实例
- 分布式集群不怕删除单节点的数据
- HA会选出新主库,保证运行正常
- 跨机房备份,跨城市保存
-
-
延迟复制备库
- 通过 CHANGE MASTER TO MASTER_DELAY = N命令,可以指定这个备库持续保持跟主库有N秒的延迟。
-
预防措施
- 上线SQL审计
- sql_safe_updates参数设置为on,忘记在delete或者update语句中写where条件,或者where条件里面没有包含索引字段的话,这条语句的执行就会报错。
- 账号分离,避免写错命令,只给业务DML的权限,而不给truncate、drop 权限
- 制定规范:删除表之前,先对表改名,观察一段时间,确保对业务没有影响,然后再删除
连表查询JOIN
- straight join 让mysql 按照指定方式join 前面是驱动表,后面是被驱动表
- 如果使用join语句,在可以使用被驱动表的索引的情况下,需要让小表做驱动表
-
算法
-
Index Nested-Loop Join
- 可以用上被驱动表上的索引
-
Simple Nested-Loop Join
- 类似于笛卡尔积
- mysql 并未使用这种算法
-
Block Netsted-Loop Join
- 时间复杂度跟SNLJ 一样
- 如果被驱动表上没有可用索引,最差情况下也是两个表的笛卡尔积,但是这个笛卡尔积的操作是在内存中完成的
-
怎么选择驱动表
-
join buffer 里面如果放不下,就会分段来放,先读取表的一部分,假设100行数据,前50行就放满了,那先读这50行,然后去链表,接着再剩下的50行,再去连表,这就是block的由来
-
假设,驱动表的数据行数是N,需要分K段才能完成算法流程,被驱动表的数据行数是M。
-
注意,这里的K不是常数,N越大K就会越大,因此把K表示为λ*N,显然λ的取值范围是(0,1)。
-
扩展:在N+λNM这个式子里,λ才是影响扫描行数的关键因素,这个值越小越好。
join_buffer_size越大,一次可以放入的行越多,分成的段数也就越少,对被驱动表的全表扫描次数就越少。
-
-
这就是为什么,你可能会看到一些建议告诉你,如果你的join语句很慢,就把join_buffer_size改大。
- 影响
- 可能会多次扫描被驱动表,占用磁盘IO资源
- 判断join条件需要执行M*N次对比(M、N分别是两张表的行数),如果是大表就会占用非常多的CPU资源
- 可能会导致Buffer Pool的热数据被淘汰,影响内存命中率。
- Batched Key Access
- 版本限制5.6之后
- BKA的算法是对NLJ的优化
- 启用方式:set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on'; 因为BKA以来MRR算法 所以要同时启用
- 思路都是让join语句能够用上被驱动表上的索引来触发BKA算法,提升查询西能
-
Multi-Range Read 优化
-
原理:顺序读盘,MRR能够提升性能的核心在于,这条查询语句在索引a上做的是一个范围查询(也就是说,这是一个多值查询),可以得到足够多的主键id。这样通过排序以后,再去主键索引查数据,才能体现出“顺序性”的优势
-
回表
- InnoDB在普通索引a上查到主键id的值后,再根据一个个主键id的值到主键索引上去查整行数据的过程
- 回表过程是一行行的查数据,主键索引是一个颗b+树,每次只能根据一个主键查到一行数据。
-
设计思路
- 根据索引a,定位到满足条件的记录,将id 值放入 read_rnd_buffer
- 将read_rnd_buffer中的id进行递增排序
- 排序后的id数组,依次到主键id索引中查记录,并作为结果返回
- read_rnd_buffer_size 参数控制 read_rnd_buffer
-
使用方法:set opimizer_switch='mrr_cost_based=off'
-
查询比较慢,即使只差一条
-
场景
-
等MDL锁
- 排查手段:show processlist ;select blocking_pid from sys.shcema_table_lock_waits
- 现象:Waiting for table metadata lock
- 解决:kill 掉 连接
-
等flush
- 排查手段:show processlist ;
- 现象:state 列 显示 Waiting for table flush 在Info 列有 哪个正在flush table t kill掉这个连接
- flush tables t with read lock;
-
flush tables with read lock; 一般这两种sql 会引起这个情况
- 等行锁
- 排查手段:select * from t sys.innodb_lock_waits where locked_table=`'test'.'t'
- 解决:kill pid(这个pid 就是查出来的),连接断开会自动回滚正在执行的线程
- 查询慢
- 没有使用到索引,全表扫描
- 一致性读,需要从事务里慢慢查找
- 解决:使用当前读,例如 lock in share model ,for update
事务
start transaction with consistent snapshot
- 立马启动一个事务
begin/start transaction
- 命令并不是一个事务的起点,在执行到它们之后的第一个操作InnoDB表的语句(第一个快照读语句),事务才真正启动。
transaction_id
- 唯一事务id,事务开始时候申请,严格递增
概念
- 当前读:更新数据都是先读后写的,而这个读,只能读当前的值,称为“当前读”(current read)。除了update语句外,select语句如果加锁,也是当前读
- 一致性读:同一个事务,不论在什么时候读取同一条信息结果都是一致的
binlog
-
事务执行过程中,先把日志写到binlog cache
事务提交的时候,再把binlog cache写到binlog文件中 -
binlog_cache_size
- 单个线程内binlog cache所占内存的大小。如果超过了这个参数规定的大小,就要暂存到磁盘。
-
write 把cache 写进文件系统的page_cache,并没有把数据持久化到磁盘,fsync 将数据持久化到磁盘
- sync_binlog=0的时候,表示每次提交事务都只write,不fsync
- sync_binlog=1的时候,表示每次提交事务都会执行fsync;
- sync_binlog=N(N>1)的时候,表示每次提交事务都write,但累积N个事务后才fsync
- 建议:设置为100\~1000中的某个数值
- 风险是:如果主机发生异常重启,会丢失最近N个事务的binlog日志
-
三种格式
- row:比较占用空间
- statement:可能造成主备不一致,写binlog耗费IO资源,影响执行速度;可以用来恢复数据
- mixed:MySQL 自己判断
redo log
-
先写入到redo log buffer
-
innodb_flush_log_at_trx_commit
-
设置为0的时候,表示每次事务提交时都只是把redo log留在redo log buffer中;
-
设置为1的时候,表示每次事务提交时都将redo log直接持久化到磁盘;
-
设置为2的时候,表示每次事务提交时都只是把redo log写到page cache。InnoDB有一个后台线程,每隔1秒,就会把redo log buffer中的日志,调用write写到文件系统的page cache,然后调用fsync持久化到磁盘。
-
-
双1配置
- 指的就是sync_binlog和innodb_flush_log_at_trx_commit都设置成 1。也就是说,一个事务完整提交前,需要等待两次刷盘,一次是redo log(prepare 阶段),一次是binlog
-
组提交
- LSN 日志逻辑序列号log sequence number
- 第一个加入组的是leader
- 组员越多 节约IOPS的效果就好
-
提升效果
- binlog_group_commit_sync_delay参数,表示延迟多少微秒后才调用fsync;
- binlog_group_commit_sync_no_delay_count参数,表示累积多少次以后才调用fsync。
- 二者是或的关系
SQL优化
order by
-
sort buffer
-
explain分析的时候 Extra这一列 Using filesort,表示的就是需要排序,MySQL会给每个线程分配一块内存用于排序称为sot_buffer
-
sort_buffer_size
- 排序所需内存大于此值,利用磁盘排序
- 小于此值在内存中排序
-
如何确定用了哪种排序?
- 查看OPTIMIZER_TRACE结果,SET optimizer_trace='enabled=on';SELECT * FROM information_schema.OPTIMIZER_TRACE\G
- 结果中number_of_tmp_files,排序过程中使用的临时文件数,为0代表在内存中排序,否则就是在临时文件中排序
-
max_length_for_sort_data
- 专门控制用于排序的行数据的长度的一个参数,超过此值就会更换算法
-
-
排序算法
- packed_additional_fields, 全字段排序,排序过程对字符串做了“紧凑”处理,按照实际长度来分配空间的
缺点:数据字段多,内存里能够同时放下的行数很少,要分成很多个临时文件,排序性能差
优点:减少磁盘访问量 - rowid,当单行的长度超过max_length_for_sort_data值就用rowid算法
缺点:排序之后,再次根据id读表, - 5.6开始 引入优先队列算法
- packed_additional_fields, 全字段排序,排序过程对字符串做了“紧凑”处理,按照实际长度来分配空间的
索引设计差
-
工具:gh-ost
-
古老魔法:alter +不写日志 set sql_log_bin=off
-
query_rewrite :可以把输入的一种语句改写成另外一种模式。
- insert into query_rewrite.rewrite_rules(pattern, replacement, pattern_database) values ("select from t where id + 1 = ?", "select from t where id = ? - 1", "db1");
这里为什么是flush 开头的
call query_rewrite.flush_rewrite_rules();
- insert into query_rewrite.rewrite_rules(pattern, replacement, pattern_database) values ("select from t where id + 1 = ?", "select from t where id = ? - 1", "db1");
-
force_index
索引
覆盖索引
最左前缀原则
- 主键长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小
联合索引
- 考虑空间大小,如果需要同时维护(name,age),(age),name字段比age大,就维护一个age索引
索引下推
- 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。没有索引下推出现之前,只能一个个回表,到主键索引上找出数据行,再对比字段值。
- 5.6版本开始出现
普通索引
唯一索引
前缀索引
-
定义字符串的一部分作为索引
-
如果你创建索引的语句不指定前缀长度,那么索引就会包含整个字符串。
-
优缺点
- 优点:占用空间小
- 缺点:增加额外的扫描记录数,不能利用覆盖索引了
-
区分度
- 区分度越高越好。因为区分度越高,意味着重复的键值越少
- 计算区分度:select count(distinct email) as L from SUser;
- 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;
概念
flush
- 把内存里的数据写入磁盘的过程叫flush
-
触发时机
-
Innodb redo log 快满了,系统推进checkpoint,就会对应范围的脏页flush进磁盘
- 这种情况尽量避免
-
内存不足时可能会淘汰脏页,也会flush
-
MySQL 认为系统很闲的时候
- 系统没什么压力
-
MySQL 正常关闭的时候
-
脏页
-
当内存数据页跟磁盘数据页内容不一致的时候,我们称这个内存页为“脏页”
-
脏页控制策略:innodb_io_capacity ,它能告诉Innodb 磁盘的刷盘能力,默认是200;所以建议设置为IOPS
-
刷盘速度两个因素:脏页比例,redo log写盘速度
- 脏页比例上限:innodb_max_dirty_pages_pct 默认值 75%
-
脏页比例计算方法:Innodb_buffer_pool_pages_dirty/Innodb_buffer_pool_pages_total
- select VARIABLE_VALUE into @a from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty';
select VARIABLE_VALUE into @b from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_total';
select @a/@b;
- select VARIABLE_VALUE into @a from global_status where VARIABLE_NAME = 'Innodb_buffer_pool_pages_dirty';
-
刷盘的“连坐”机制
- 在准备刷一个脏页的时候,如果这个数据页旁边的数据页刚好是脏页,就会把这个“邻居”也带着一起刷掉;而且这个把“邻居”拖下水的逻辑还可以继续蔓延,也就是对于每个邻居数据页,如果跟它相邻的数据页也还是脏页的话,也会被放到一起刷。
- 参数innodb_flush_neighbors=1 就连坐,等于0就只管自己的。机械硬盘建议1,减少随机IO,固态硬盘建议0,更快完成刷盘,减少语句响应时间
-
并法连接数和并发查询数
-
show processlist 显示的是并发连接数
- 影响:占据内存
-
当前正在执行的语句,才是我们所说的并发查询
- 进入锁等待,并发线程计数会减一
客户端的误解
-
误解1:如果库里的表特别多,连接就会很慢
- 每个客户端在和服务端建立连接的时候,需要做的事情就是TCP握手、用户校验、获取权限。但这几个操作,显然跟库里面表的个数无关
-
为什么会感觉很慢?
-
原因
- 当使用默认参数连接的时候,MySQL客户端会提供一个本地库名和表名补全的功能。为了实现这个功能,客户端在连接成功后,需要多做一些操作:
-
执行show databases;
切到db1库,执行show tables;
把这两个命令的结果用于构建一个本地的哈希表。
- 最花时间的就是第三步在本地构建哈希表的操作。所以,当一个库中的表个数非常多的时候,这一步就会花比较长的时间。
也就是说,我们感知到的连接过程慢,其实并不是连接慢,也不是服务端慢,而是客户端慢
- 解决方案
- 跳过补全功能
- 增加-quick 或者-q 参数
- 作用
- 跳过补全
- 不用申请本地缓存
- 不记录执行命令记录到本地的命令历史文件
- 增加-A参数
- 自动补全的效果就是,你在输入库名或者表名的时候,输入前缀,可以使用Tab键自动补全表名或者显示提示。
- 实现原理
- MySQL客户端发送请求后,接收服务端返回结果的方式有两种
- 一种是本地缓存,也就是在本地开一片内存,先把结果存起来。如果你用API开发,对应的就是mysql_store_result 方法。
- 一种是不缓存,读一个处理一个。如果你用API开发,对应的就是mysql_use_result方法。
- MySQL客户端默认采用第一种方式,而如果加上–quick参数,就会使用第二种不缓存的方式。
- 采用不缓存的方式时,如果本地处理得慢,就会导致服务端发送结果被阻塞,因此会让服务端变慢。
结果集存储到哪?
- net_buffer
-
假设表200G大小,select * from t 全表扫描,实际上并不需要保存一个完整的结果集
-
流程
-
获取一行,写到net_buffer中,重复获取行,直到net_buffer写满,调用网络接口发出去。
-
如果发送成功,就清空net_buffer,然后继续取下一行,并写入net_buffer。
-
如果发送函数返回EAGAIN或WSAEWOULDBLOCK,就表示本地网络栈(socket send buffer)写满了,进入等待。直到网络栈重新可写,再继续发送
-
-
关键点
- 默认定义/proc/sys/net/core/wmem_default的socket send buffer 也不可能达到200GB,如果它被写满了,就会暂停读取数据的流程
-
问题:引起长事务
- MySQL是“边读边发的”,这个概念很重要。这就意味着,如果客户端接收得慢,会导致MySQL服务端由于结果发不出去,这个事务的执行时间变长
-
现象
- 用show processlist 查看会看到 sending client data
-
注意sending data和sending client data 是两个状态,
-
区别
- sending data 仅代表mysql 进入sql 执行阶段
- sending client data 当一个线程处于“等待客户端接收结果”的状态,才会显示"Sending to client"
-
-
buffer pool 简称BP
-
作用
- 加速查询
-
指标
- 内存命中率:show engine innodb status Buffer pool hit rate 990/1000
-
参数
-
innodb_buffer_pool_size
- 一般建议设置成可用物理内存的60%\~80%。
-
-
内存管理方法
-
LRU(最近最少使用)
- 底层数据结构是链表
- 有young 区和old 区,占比为5:3
- 对于old 区的数据页,每次访问都会做个判断,如果数据页在链表中的时间超过指定时间,就把它移动到链表头部,
- innodb_old_blocks_time 来控制 指定时间,默认是1000毫秒即1秒
-
临时表
-
误区
- 临时表就是内存表
-
内存表
- 指的是使用Memory引擎的表,建表语法是create table … engine=memory。这种表的数据都保存在内存里,系统重启的时候会被清空,但是表结构还在
-
临时表
-
可以使用各种引擎类型 。如果是使用InnoDB引擎或者MyISAM引擎的临时表,写数据的时候是写到磁盘上的。当然,临时表也可以使用Memory引擎
-
特性
-
建表语法是create temporary table
-
一个临时表只能被创建它的session访问,对其他线程不可见
-
临时表可以与普通表同名
- 普通表table_def_key的值由库名+表名
- 临时表table_def_key由table_def_key=库名+表名+server_id+thread_id
-
session A内有同名的临时表和普通表的时候,show create语句,以及增删改查语句访问的是临时表
-
show tables命令不显示临时表
-
每个线程都维护了自己的临时表链表,每次session 内操作表,先遍历链表,检查是否有这个名字的临时表,有就先操作临时表,如果没有操作普通表,对链表的每个临时表 执行 drop temporary table+表名操作
-
binlog_format=statment或者mixed的时候,binlog中才会记录临时表的操作
-
如果数据库掉电,重启以后MySQL会扫描临时目录,把表都删掉;
-
-
应用
- 分布式系统的跨库查询
-
tmp_table_size 控制内存临时表的大小 默认是16MB
-
内存临时表的放不下的时候,就把临时表转成磁盘临时表,默认引擎是InnoDB
-
场景:union 语句,group by 无序排序且没有加order by null,group by 没有使用到索引
-
groub by
-
执行流程
- 创建内存临时表,依次取出数据,记录临时表,如果有x,就将x的值加1,再根据排序字段排序,如果没有排序,加上order by null
-
优化方法
-
索引 5.7版本 generated column 机制
- alter table t1 add column z int generated always as(id % 100), add index(z);
-
group by语句中加入SQL_BIG_RESULT
- 就可以告诉优化器:这个语句涉及的数据量很大,请直接用磁盘临时表。
- join_buffer 是无序数组,sort_buffer 是有序数组,临时表是二维表的结构
-
Memory 引擎
-
InnoDB表的数据就放在主键索引树上(B+树)
- 索引组织表(index organizied table)
-
Memory引擎的数据和索引是分开的,数据部分以数组的方式单独存放,而主键id索引里,存的是每个数据的位置,主键的id是hash索引,可以索引上的key并不是有序的
- 堆组织表(heap organizied table)
-
跟InnoDB的区别
- InnoDB表的数据总是有序存放的,而内存表的数据就是按照写入顺序存放的
- 当数据文件有空洞的时候,InnoDB表在插入新数据的时候,为了保证数据有序性,只能在固定的位置写入新值,而内存表找到空位就可以插入新值
- 数据位置发生变化的时候,InnoDB表只需要修改主键索引,而内存表需要修改所有索引
- InnoDB表用主键索引查询时需要走一次索引查找,用普通索引查询的时候,需要走两次索引查找。而内存表没有这个区别,所有索引的“地位”都是相同的
- InnoDB支持变长数据类型,不同记录的长度可能不同;内存表不支持Blob 和 Text字段,并且即使定义了varchar(N),实际也当作char(N),也就是固定长度字符串来存储,因此内存表的每行数据长度相同
-
索引
- 它支持hash索引和b-tree索引
- 内存表快 Memory 支持hash索引,且数据都在内存
-
缺点
- 不支持行锁,只支持表锁
- 数据库重启的时候,所有内存表的都会被清空
- 临时表数据只有2000行
自增主键
-
保存的位置
- 表的结构定义存放在后缀名为.frm 文件中,但是并不会保存自动值
-
不同引擎保存策略不同
-
MyISAM 引擎的自增值保存在数据文件中
-
InnoDB 引擎的自增值,其实是保存在了内存里
- 8.0 自增持久化,将自增值的变更记录放在了redo log
- 5.7及之前的版本,自增只保存在内存里,并没有持久化,每次重启之后,第一次打开表的时候获取最大值+1,作为这个表当前的自增值
-
自增值修改机制
- 如果插入的id为0 null,或未指定,就把表的当前的auto_increment值插入
- 指定了就使用指定的值,如果自增值是Y,插入是X,X < Y 那么这个表的自增值不变,如果x>= Y就需要把当前的自增值修改为新的自增值
-
修改时机
- 没有指定id
- 子主题 2
-
新的自增生成算法
- 从auto_increment_offset开始,以auto_increment_increment为步长,持续叠加,直到找到第一个大于X的值,作为新的自增值。
- 其中,auto_increment_offset 和 auto_increment_increment是两个系统参数,分别用来表示自增的初始值和步长,默认值都是1
-
-
自增锁
- 5.0版本是语句级别的
- 5.1.22版本引入了一个新的策略,新增参数innodb_autoinc_lock_mode=1(默认是1),为0表示用5.0的版本,这个参数的值被设置为2时,所有的申请自增主键的动作都是申请后就释放锁
-
不连续的原因
- 唯一键冲突
- 事务回滚
-
申请的策略
- 批量插入的语句,第一次申请自增id,会分配1个;
1个用完以后,这个语句第二次申请自增id,会分配2个;
2个用完以后,还是这个语句,第三次申请自增id,会分配4个;依此类推,同一个语句去申请自增id,每次申请到的自增id个数都是上一次的两倍。
表分区
-
对于引擎层来说是多个表,对于server层来说是1个表
-
分区表间隙锁只在一个表上
-
优缺点:
- server层认为都是一张,因此所有分区共用同一个MDL锁
- 打开表行为,每当第一次访问一个分区表的时候,MySQL需要把所有的分区都访问一遍。如果系统参数 open_files_limit 使用默认值1024,超过这个数值会报错的
-
优点
- 分区表可以很方便的清理历史数据 alter table t drop partition
- 业务代码更简洁
-
分区策略
-
myisam
-
通用分区策略(generic partition)
- 每次访问分区都由server层控制
-
-
mysql 5.7.9
-
Innodb 本地分区策略(native partitioning)
- 在InnoDB内部自己管理打开分区的行为。
-
-
mysql 5.7.17
- 将myISAM分区表标记为即将弃用deprecated
-
mysql 8.0 不允许创建myisam分区表
-
-
应用场景
幻读
- 定义:幻读指的是一个事务在前后两次查询同一个范围的时候,后一次查询看到了前一次查询没有看到的行。
- 现象:数据不一致
- 解决方案:间隙锁
- 出现的前提:事务隔离级别为RR
连接
断开连接
- 规则:优先断开事务外空闲太久的连接;如果这样还不够,再考虑断开事务内空闲太久的连接
- 从服务端断开连接使用的是kill connection + id的命令
短连接:短连接则是指每次执行完很少的几次查询就断开连接,下次查询再重新建立一个
-
短连接风暴
-
max_connections
- 只要连接着就占用一个计数
-
wait_timeout
- 一个线程空闲wait_timeout这么多秒之后,就会被MySQL直接断开连接
-
长连接:数据库里面,长连接是指连接成功后,如果客户端持续有请求,则一直使用同一个连接
锁
自增锁(AUto-inc locks)
- auto-inc锁是当使用含有auto_increment列的表中插入数据时需要获取的一种特殊的表级锁,在执行插入语句时在表级别加一个auto-inc锁,然后为每条待插入记录的auto_increment修饰的列分配递增的值,在语句执行结束后,再把auto-inc锁释放掉
- innodb_autoinc_lock_mode
- 作用:保证一个语句中分配的递增值是连续的
- 缺点:每条语句都要对这个表锁进行竞争,这样的并发性低下
-
模式
-
innodb_autoinc_lock_mode = 0(“传统”锁定模式)
- 所有insert 语句 开始时候都会获取一个表锁autoinc_lock,获取到
-
innodb_autoinc_lock_mode = 1(“连续”锁定模式)
- 针对bulk insert,自增锁会被一直持有直到语句执行结束才会被释放。
-
innodb_autoinc_lock_mode = 2(“交错”锁定模式)
- simple insert语句能保证ID是连续的,但是bulk insert的ID则可能有空洞。
-
主从复制的同一张表下的同一行id有可能不一样。
- Insert 语句有三种类型,simple insert ,bulk insert ,mixed insert
临键锁(Next-keys locks)
- 前开后闭的区间(0,5] 不包括5
- 实际上它是行锁+间隙锁实现的
间隙锁(Gap locks)
死锁(deadlock)
- 当并发系统中不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程都进入无限等待的状态,称为死锁
-
解决策略:
- 等,直到超时innodb_lock_wait_timeout=50s 默认
- 起死锁检测,发现死锁后,主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。将参数innodb_deadlock_detect设置为on,表示开启这个逻辑。
- 直接kill掉 connection
记录锁(Record locks)
乐观锁
- 其实主要就是一种思想,因为乐观锁的操作过程中其实没有没有任何锁的参与,乐观锁只是和悲观锁相对,严格的说乐观锁不能称之为锁
- 实现方式 数据库表的version,增加时间戳类型的字段
悲观锁
- 行锁、表锁、读锁、写锁
根据不同引擎
-
innodb
-
自增锁(Auto-inc locks)
- 门针对事务插入 AUTO_INCREMENT类型的列
-
共享锁 允许持有该锁的事务读取行
-
排他锁 允许持有该锁的事务更新或删除行。
-
意向锁(Intention locks)InnoDB用意向锁来实现多粒度级别的锁
-
意向共享锁 (intention shared lock, IS)表示事务打算在表中的各个行上设置共享锁
- select column from table ...lock in share model
-
意向排他锁 (intention exclusive lock, IX)表示事务打算在表中的各个行上设置排他锁
- select column from table ... for update
-
意向锁是由存储引擎 自己维护的 ,用户无法手动操作意向锁,
-
意向锁是一种不与行级别锁冲突的表级锁。
-
意向锁不会与行级别的锁互斥,会与表级别的S和X互斥
-
-
记录锁(Record Locks)记录锁是对索引记录的锁定
- 没有索引也能锁定索引记录,innodb 有个隐藏的聚簇索引可用来做记录锁
-
间隙锁(Gap locks)
- 它封锁索引记录中的间隔
- RR情况下才有效
- 关闭可能遇到幻读
-
插入意向锁 insert intention locks
- 插入意向锁是一种Gap锁,不是意向锁,在insert操作时产生
- 跟普通的区别,普通的Gap Lock 不允许 在 (上一条记录,本记录) 范围内插入数据
-
插入意向锁Gap Lock 允许 在 (上一条记录,本记录) 范围内插入数据
- 插入意向锁的作用是为了提高并发插入的性能
- 临键锁 (Next-key locks)
- Next-Key锁是索引记录上的记录锁和索引记录之前的区间上的区间锁的组合。
- (1,5] 类似这种前开后闭的区间(数学上称之为 半开区间)
锁粒度
-
表锁
-
表锁
-
lock tables t read 加S锁,lock tables t write
-
自增锁
-
意向锁(Intention lock)
- 允许行锁和表锁共同存在
-
元数据锁(MDL)
-
-
元数据锁 meta data lock MDL
- MDL不需要显式使用,在访问一个表的时候会被自动加上。MDL的作用是,保证读写的正确性,你可以想象一下,如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个表结构做变更,删了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。
- 等事务提交后再释放
-
-
行锁
- myisam 不支持行锁
- 行锁就是针对数据表中行记录的锁
- 可以是一行也可以是多行
-
范围
- 记录锁(单行的),间隙锁(多行的),临键锁,插入意向锁
-
全局锁
-
全局锁就是对整个数据库实例加锁
- Flush tables with read lock (FTWRL)。当你需要让整个库处于只读状态的时候,可以使用这个命令
-
加锁规则
- 原则1:加锁的基本单位是next-key lock。希望你还记得,next-key lock是前开后闭区间。
原则2:查找过程中访问到的对象才会加锁。
优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock退化为行锁。
优化2:索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,next-key lock退化为间隙锁。
一个bug:唯一索引上的范围查询会访问到不满足条件的第一个值为止
DCL\&DDL
database controll language
grant:用来给用户赋权
-
创建用户:create user 'ua'@'%' identified by 'pa'
- 在磁盘上往mysql.user表里插入一行
- 内存里,往数组acl_users里插入一个acl_user对象,这个对象的access字段的值为0
-
全局权限:grant all privileges on . to 'ua'@'%' with grant option;
- 磁盘上,将mysql.user表里,用户’ua’@’%'这一行的所有表示权限的字段的值都修改为‘Y’;
- 内存里,从数组acl_users中找到这个用户对应的对象,将access值(权限位)修改为二进制的“全1“
- grant执行之后,新的客户端使用用户名ua登录,mysql会为新🔗维护一个线程对象,然后从acl_users数组查询到用户的权限,并将权限值拷贝到这个线程对象中,之后在这个连接中执行的全局权限判断,都使用线程对象内部保存的前线位
- 注意:已经存在的连接全局判断不受grant影响
-
回收权限 revoke all privileges on . from 'ua'@'%'
- 磁盘上,将mysql.user表里,用户'ua'@'%' 这一行的所有标识权限的字段的值都修改为’N‘
- 内存里,从数组acl_users中找到这个用户对应的对象,将access的值修改为0
DB权限
-
rant all privileges on db1.* to 'ua'@'%' with grant option;
- 磁盘上,往mysql.db表中插入了一行记录,所有权限位字段设置为“Y”;
- 内存里,增加一个对象到数组acl_dbs中,这个对象的权限位为“全1”。
-
每次判断一个用户对一个数据库读写权限的时候,都需要遍历一次acl_dbs数组,根据user,host和db找到匹配对象,然后根据对象权限位来判断,也就是说grant 修改db权限的时候,是同时对磁盘和内存生效的
- 特例:如果当前会话已经处于一个db里面,之前use这个裤的时候拿到的库权限会保存在会话变量重
表权限和列权限
- 表权限定义存放在表mysql.tables_priv中
- 列权限定义存放在表mysql.columns_priv中。这两类权限
- 这两类权限,组合起来存放在内存的hash结构column_priv_hash中。
-
赋权指令
- 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;
- 同时修改数据表和内存中的hash,会立马影响到已经存在的连接
结论
-
flush privileges
- flush privileges命令会清空acl_users数组,然后从mysql.user表中读取数据重新加载,重新构造一个acl_users数组。也就是说,以数据表中的数据为准,会将全局权限内存数组重新加载一遍
-
正常情况下,grant命令之后,没有必要跟着执行flush privileges命令
-
当数据表中的权限数据跟内存中的权限数据不一致的时候,flush privileges语句可以用来重建内存数据,达到一致状态。
- 这种一般是去mysql.user表执行了dml操作,这种是不规范的写法
数据库架构
客户端
服务层
-
连接器:连接器负责跟客户端建立连接、获取权限、维持和管理连接
-
查询缓存:只要表上有更新,这个表所有的查询都会被清空
- query_cache_type 设置为demand
- 8.0 版本已经移除了查询缓存
- 显示指定:select SQL_CACHE * from t
-
分析器:分析每个字符串分别是什么
-
优化器:决定索引的使用,关联表的顺序
- 执行器:检查执行权限,去调用不同的引擎接口,慢日志里面有rows_examined,表示语句执行过程中扫描了多少行
-
-