MySQL索引基础补充以及优化笔记-上
Posted 会编程的老六
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了MySQL索引基础补充以及优化笔记-上相关的知识,希望对你有一定的参考价值。
MySQL索引、基础补充以及优化笔记-上
MyISAM存储引擎索引实现
MyISAM中为非聚集索引,也就是:索引,数据分开存储。索引存储在MYI文件中,数据存储在MYD文件中。在搜索数据时,先判断查找字段是否有索引,如果有则开始从MYI文件中的根节点开始,定位索引元素。一个一个节点查找,内部折半查找,最终找到叶子节点。叶子节点中存的就是索引所在行的磁盘文件地址。在根据这个地址,在MYD文件中快速定位到对应行的位置,将数据进行取出。
Innodb存储引擎索引实现
Innodb中为聚集索引,也就是:索引和数据合并存储,表数据文件本身就是按照B+树组织的一个索引结构文件,叶节点包含了完整的数据记录。一张数据表对应两个文件,一个是表结构文件FRM文件,另一个是数据以及索引的合并存储文件IBD文件。Innodb的每张表自带主键索引,其IBD文件中的存储结构为:
叶子节点中存放的是索引所在行的其他列数据。
非主键索引结构为:
同样也是使用B+树进行构建,叶子节点存放的是索引所在行的主键。
标准B+树,相邻叶子节点之间是一个单向连接,而mysql中,是B+树一个变种,相邻叶子节点之间存放的是一个双向连接。并且每个叶子节点中还会有很小的一部分区域用来存储相邻叶子节点的地址。
Hash索引存储结构
很多时候Hash索引要比B+树索引更高效,仅满足“=”,“IN”,不支持范围查询。但是工作中基本不用Hash索引,因为它不支持范围查询,不支持模糊查询,并且可能存在hash冲突问题。
面试题:为什么建议InnoDB表必须建立主键,并且推荐使用整型的自增主键?
答:如果没有建立主键索引,则会由Innodb帮你寻找一个主键,逐列寻找一个所有元素都不相同的列。也可以说对这个列添加了一个唯一索引。然后会使用这个列来组织整张表的所有数据。如果所有列都不满足元素不同这个要求,mysql会帮这张表维护一个整型自增的隐藏列,类似于Rowid,并使用这个列维护整张表的结构。所以,如果没有主键MySQL会多做很多事情,浪费数据库资源。
查询数据的时候,免不了数据大小比对。使用整型比大小会更快,效率更高。其次,正式开发环境下使用的SSD高速存储硬盘会十分昂贵,所以应该尽量减少数据存储的空间量。如果使用UUID作为主键,则会多出很多不必要的存储量。那为何需要自增呢?方便数据的插入。当我们没有使用主键递增ID的时候,会多一个平衡索引树的操作。当我们使用主键递增ID的时候,所有新增的主键都为当前主键的最大值,所以只需要在索引树的最右边加上一个记录即可。
联合索引的底层存储结构
底层也为B+树。根据条件依次对比,先后分别比对name,age,position。如果结果唯一则停止后续条件比对。
相关优化操作:如果将条件拆开分别使用三条查询语句:
SELECT * FROM employees WHERE name = \'Bill\' and age = 31;
SELECT * FROM employees WHERE age = 31 and position = \'dev\';
SELECT * FROM employees WHERE position = \'manager\';
则只有第一条数据会使用索引,剩余两条都不会使用索引,可以通过EXPLAIN命令进行查询。所以为了在查询时使用索引,则必须按照建立索引的顺序进行查询。本例中必须按照name,age,position的顺序进行查询,才会使用索引。
SELECT * FROM employees WHERE name = \'Bill\' and age = 31 and position = \'dev\';
在最左前缀索引中,在相同的前置条件中,后续条件会依次排序。在本例子中,只有相同name条件下的age才是有序排列的。
MySQL读写分离
主从同步
主节点先将数据存储到data库中,再将sql语句存入到binlog文件中。从节点中存在两个线程,第一个线程为I/O线程,用来进行io读写,将主节点中的数据进行拷贝,放入从节点中的relay binlog文件中(中继日志)。第二个线程为SQL线程,用来从relay binlog中一条一条读出sql语句,并对从节点中的data库进行数据操作。
show binary logs; // 列出服务器上的二进制日志文件
show master status; //显示主节点正在使用的二进制日志文件以及状态
show binlog events in \'binlog name\'; //显示binlog文件的日志内容
binlog文件中只会存储对数据文件的修改操作。binlog文件还可以用来恢复数据库文件。
弊端:牺牲了数据库之间的一致性可能导致脏读,并且存在一定的延迟。可能存在数据库之间同步中断的问题(因为网络问题导致数据并没有同步)。
对于数据库不满足一致性而导致脏读情况的问题,可以使用强制查询主库的方式来解决。
对于同步中断问题的解决方法是:采用半同步方法,使用了第三发的插件来实现。具体过程为,web服务端将语句发送到主节点进行运行,要等到结果存入主节点的data库并且将sql语句存入到从节点的relay binlog文件后返回给主节点一个通知,此时主节点才会返回语句成功运行的结果通知。
企业中常用的架构方案
一主一从:用来做数据的备份,提高数据可用性。不需要考虑数据一致性。但是不能代替数据的备份,因为如果在主节点清空了表,那么从节点也会清空表。所以只能用来做容灾,数据该备份还是要备份。如果数据丢失了,则可以使用快照恢复。binlog文件虽然可以备份,但不是恢复数据的主要手段。
一主多从:从节点不适应过多,一般为2-4个从节点。否则会在主从同步上耗费大量的事件。如果为一主四从,则将三个从节点作为正常的读写,另外一个从节点做另外的特殊操作,比如十分耗时的整表操作等等。三个从节点中还需要选举一个节点来作为主节点的备份节点,当主节点宕机以后来充当主节点。
双主:业务场景中拥有大量的写操作,单个主节点承受不来,则使用双主架构。根据写操作的id进行一个取模操作,或者id是单数则在一个主节点完成,为双数则在另外一个主节点完成。如果是一个字符串,则可以先hash,再把结果取模。如果一个节点宕机,整个数据都会很混乱。
级联同步:解决master节点压力过大,主节点只将数据同步给一个从节点。这个从节点再见数据同步给另外的多个从节点。好处是:分散主节点的压力,其次如果主节点宕机以后,剩下的部分是一个天然的主从结构。弊端是:如果主节点同步的从节点宕机以后,二级从节点就变成了单独节点。
环形多主:多个主节点都可以接多个从节点,从而形成一个大型环形多主多从结构。弊端是:一个主节点宕机以后,整个结构全部不可使用。
读写分离正式结构:增加一个代理节点:
app server中所有的读与写的请求全部往代理节点中发送。代理节点去解析请求,如果语句是写入操作,则把该请求发送至主节点,如果是读操作,则把请求以负载均衡的方式发送至从节点。我们需要将数据源的连接配置到代理节点中即可。代理节点也会存在宕机和单点故障以及其他一些故障的可能性,可以通过横向扩充代理节点弥补。
基于Atlas代理实现读写分离
实施流程:
- 修改主节点配置my.cnf
- 修改从节点配置my.cnf ,配置主节点地址、端口、密码
- 安装atlas
- 配置atlas
master my.cnf:
server-id=99 //master id 不能和集群中其他mysql实列的id相同
log-bin=mysql-bin //binlog 文件前缀
binlog-do-db = xxx //对应需要同步的数据库
//以下都是不同步的数据库
binlog-ignore-db = information_schema
binlog-ignore-db = mysql
binlog-ignore-db = personalsite
binlog-ignore-db = test
配置完成后重启数据库。可以通过show master status;查看主节点正在使用的二进制日志文件。
slave my.cnf:
server-id=99 //slave id
log-bin=mysql-bin //可加可不加,如果这个节点需要向其他节点同步数据时,需要添加这项
replicate-do-db = xxx //对应需要同步的数据库
//以下都是不同步的数据库
replicate-ignore-db = information_schema
replicate-ignore-db = mysql
replicate-ignore-db = personalsite
replicate-ignore-db = test
动态配置主节点连接信息
change master to master_host=\'127.0.0.1:3306\',master_user=\'root\',master_password=\'123456789\';
配置前必须通过stop slave;
命令关闭从节点后,才可以运行此项动态配置。配置完后使用start slave;
命令再次开启从节点。使用命令show slave status;
查看从节点状态。
将配置文件放入对应的文件夹中:
编写初始化脚本 init.sql :
change master to master_host=\'127.0.0.1:3306\',master_user=\'root\',master_password=\'123456789\';
reset slave;
start slave;
使用docker-compose 编写yaml文件:
slave2节点仿照slave1节点编写,注意更换端口号。下面提前给出atlas的相关yaml配置:
atlas:下载相关的包,进入conf文件夹中修改test.conf配置文件:
注意这边的端口号使用的是容器中的端口号。这边使用了名字来代替,实际使用应该转换成相对应的地址。
pwds项中格式为 [用户名]:[密码],密码是经过加密的。
后面的配置与日志相关。
使用命令dc up -d
(docker compose 命令)启动。后续连接数据库,如果在从节点进行修改操作的话,确实可以修改从节点中的数据,但是这个操作并不会同步到主节点中。
MySQL基础补充
SQL语句执行流程
连接器:负责管理连接。流程为:建立一个新的连接,接着通过系统库中的user表加载当前用户对应的权限,然后将权限加载进连接管理对象,进行权限校验。知道当前连接可以进行哪些操作。连接建立完成以后,更改权限相对应的操作时,此连接所对应的权限操作并不会改变。必须要新连接重新建立时重新加载权限时才会被更改。
之后会去到缓存区中查询是否有这条语句的缓存结果(缓存数据形式为键值对,key为查询sql,value为sql运行结果)。如果缓存区中存在对应的键值对,就直接从缓存区中读出结果集并返回。
如果缓存中没有找到,则会进入词法分析器:进行词法分析,语法分析。分析是什么操作,校验语法等使用方法是否正确。如果不正确就返回语法错误结果。
分析完成确保语句没有问题后,进入语句优化器:执行计划生产索引选择。比如联表查询中会判断查A表和查B表那个更快,以此来判断先查哪个表来作为条件。那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。
进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎的执行结果。
接着进入执行器:调用数据库引擎接口获取查询结果。
MySQL数据库引擎都是按照插件形式,提供给了服务层调用接口。所以数据库引擎可以根据需要动态扩展。
数据库引擎会调用对应的数据库文件进行查找并记录结果记录集,结束后将结果记录集返回给执行器。执行器则会将结果集放入到缓存区中。最后将结果返回给客户端。
每次对数据库进行update等写操作,对数据库有修改的操作时,就会清除缓存区。所以缓存区适合读多写少的场景。需要注意的是mysql query cache 是对大小写敏感的,因为Query Cache 在内存中是以 HASH 结构来进行映射,HASH 算法基础就是组成 SQL 语句的字符,所以 任何sql语句的改变重新cache,这也是项目开发中要建立sql语句书写规范的原因吧
MySQL开启缓存操作为:my.cnf文件中:配置query_cache_type
参数,设置为0 的时候则是永远都不会使用缓存,设置为1的时候无论查询哪张表缓存中存在对应数据就从缓存中拿取结果集,设置为2的时候为按需使用缓存,例如我需要给test表设置缓存则需要运行SQL语句:select SQL_CACHE * from test;
show status like \'Qca%\';
可以用来查看缓存的情况。
注意:查询缓存这个功能在MySQL 8.0 版本后被移除(鸡肋)。
如果是一条更新语句,执行过程与查询类似如果有缓存,也是会用到缓存。然后拿到查询的语句,进行权限校验,接着进行相关更新操作,然后调用引擎 API 接口,写入这一行数据,InnoDB 引擎把数据保存在内存中,同时记录 redo log,此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交。执行器收到通知后记录 binlog,然后调用引擎接口,提交 redo log 为提交状态。更新完成。
更新语句执行流程如下:分析器---->权限校验---->执行器--->引擎---redo log(prepare 状态)--->binlog--->redo log(commit状态)
日志模块
Bin Log 归档日志(逻辑日志)
- 二进制日志,采用二进制编写
- Binlog在MySQL的Server层实现(引擎共用)
- Binlog为逻辑日志,记录的是一条语句的原始逻辑,类似于“给 ID=2 这一行的 c 字段加 1”
- Binlog不限大小,追加写入,不会覆盖以前的日志
使用binlog,必须要前开启这个功能。通过语句show variables like \'%log_bin%;\'
查询是否开启此项功能:
binlog
日志有三种格式,可以通过binlog_format
参数指定。
- statement,记录的内容是
SQL
语句原文但是有个问题,update_time=now()
这里会获取当前系统时间,直接执行会导致与原库的数据不一致。为了解决这种问题,我们需要指定为row
。 - row,记录的内容还包含操作的具体数据,记录内容如下。
row
格式记录的内容看不到详细信息,要通过mysqlbinlog
工具解析出来。update_time=now()
变成了具体的时间。通常情况下都是指定为row
,这样可以为数据库的恢复与同步带来更好的可靠性。但是需要大量的容量来记录,占用空间大,恢复与同步时会更消耗IO
资源,影响执行速度。 - mixed,记录的内容是前两者的混合。
MySQL
会判断这条SQL
语句是否可能引起数据不一致,如果是,就用row
格式,否则就用statement
格式。
事务执行过程中,先把日志写到binlog cache
,事务提交的时候,再把binlog cache
写到binlog
文件中。因为一个事务的binlog
不能被拆开,无论这个事务多大,也要确保一次性写入,所以系统会给每个线程分配一个块内存作为binlog cache
。我们可以通过binlog_cache_size
参数控制单个线程 binlog cache 大小,如果存储内容超过了这个参数,就要暂存到磁盘(Swap
)。
Redo Log 重做日志(物理日志)
- Innodb引擎特有,是记录InnoDB存储引擎的事务日志
- 记录的是运行的结果
- 有一个大小限定,不会无限大
- MySQL的WAL机制(Write-Ahead-Logging),先写日志再写磁盘
- 保存的文件名为:ib_logfile*
- 以循环的方式写入日志文件。不断的写与擦除,文件1写满时,切换到文件2,文件2写满时,切换到文件1
比如 MySQL
实例挂了或宕机了,重启时,InnoDB
存储引擎会使用redo log
恢复数据,保证数据的持久性与完整性。MySQL
中数据是以页为单位,你查询一条记录,会从硬盘把一页的数据加载出来,加载出来的数据叫数据页,会放入到 Buffer Pool
中。后续的查询都是先从 Buffer Pool
中找,没有命中再去硬盘加载,减少硬盘 IO
开销,提升性能。更新表数据的时候,也是如此,发现 Buffer Pool
里存在要更新的数据,就直接在 Buffer Pool
里更新。然后会把“在某个数据页上做了什么修改”记录到重做日志缓存(redo log buffer
)里,接着刷盘到 redo log
文件里。
刷盘策略:InnoDB
存储引擎为 redo log
的刷盘策略提供了 innodb_flush_log_at_trx_commit
参数,它支持三种策略:
- 0 :设置为 0 的时候,表示每次事务提交时不进行刷盘操作
- 1 :设置为 1 的时候,表示每次事务提交时都将进行刷盘操作(默认值)
- 2 :设置为 2 的时候,表示每次事务提交时都只把 redo log buffer 内容写入 page cache
另外,InnoDB
存储引擎有一个后台线程,每隔1
秒,就会把 redo log buffer
中的内容写到文件系统缓存(page cache
),然后调用 fsync
刷盘。还有一种情况,当 redo log buffer
占用的空间即将达到 innodb_log_buffer_size
一半的时候,后台线程会主动刷盘。
日志策略:redo log存储文件采用的是环形数组形式,从头开始写,写到末尾又回到头循环写,如下图所示。
在个日志文件组中还有两个重要的属性,分别是 write pos、checkpoint
- write pos 是当前记录的位置,一边写一边后移
- checkpoint 是当前要擦除的位置,也是往后推移
每次刷盘 redo log
记录到日志文件组中,write pos
位置就会后移更新。
每次 MySQL
加载日志文件组恢复数据时,会清空加载过的 redo log
记录,并把 checkpoint
后移更新。
write pos
和 checkpoint
之间的还空着的部分可以用来写入新的 redo log
记录。
如果 write pos
追上 checkpoint
,表示日志文件组满了,这时候不能再写入新的 redo log
记录,MySQL
得停下来,清空一些记录,把 checkpoint
推进一下。
MySQL索引基础补充以及优化笔记-下
MySQL索引、基础补充以及优化笔记-下
数据库优化
索引
引起索引失效的注意事项
- 全值匹配(要遵守)
- 最佳左前缀法则(要遵守)
- 不再索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
- 存储引擎不能使用索引中范围条件右边的列
- 尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),尽量不使用select *
- mysql使用不等于(!=或者 <>)的时候无法使用索引会导致全表扫描
- is null,is not null 也无法使用索引
- like以通配符开头(\'%abc...\')mysql索引失效会变成全表扫描的操作(百分号尽量写右边)
- 字符串不加单引号索引失效
- 少用or,用它来连接时索引会失效
explain命令中key_len值是索引字段的最大可能长度,为索引字段设置的大小✖当前编码下对应的字符集占用字节数
遵守上述需要遵守的规则,并避免剩余规则即索引优化操作。尽量避免索引失效问题。
查询优化
类似嵌套循环:
优化原则:小表驱动大表,即小的数据集驱动大的数据集
例子:
select * from A where id in (select id from B)
//等价于
for select id from B
for select * from A where A.id = B.id
//当B表的数据集小于A表的数据集时,用in优于exists
select * from A where exists (select 1 from B where B.id = A.id)
//等价于
for select * from A
for select * from B where B.id = A.id
//当A表的数据集小于B表的数据集时,用exists优于in
//mysql机读顺序,优先执行括号内的查询
//注意:A表与B表的ID字段应建立索引
order by 优化
- order by 子句,尽量使用Index方式排序,避免使用FileSort方式排序
MySQL支持两种方式排序,FileSort和Index,Index效率高,它指MySQL扫描索引本身完成排序,FileSort方式效率较低。
order by满足两情况,会使用index方式排序:
- order by 语句使用索引最左前列
- 使用where子句与order by 子句条件列组合满足索引最左前列
- 尽可能在索引列上完成排序操作,遵照索引建的最佳左前缀
- 如果不在索引列上,filesort有两种算法:mysql就要启动双路排序和单路排序
双路:第一遍扫描出需要排序的字段,然后进行排序后,根据排序结果,第二遍再扫描一下需要select的列数据。这回引起大量的随即IO,效率不高,但是节约内存。排序使用quick sort。但是如果内存不够则会按照block进行排序,将排序结果写入磁盘文件,然后再将结果合并。
单路:即一遍扫描数据后将select需要的列数据以及排序的列数据都取出来,这样就不需要进行第二遍扫描了,当然内存不足时也会使用磁盘临时文件进行外排。因为要把所有字段都取出,所以有可能取出的数据的总大小超出了sort_buffer的容量,导致每次只能取sort_buffer容量大小的数据,进行排序(创建tmp文件,多路合并),排完再取sort_buffer大小,从而多次I/O。
MySQL根据max_length_for_sort_data
来判断排序时使用一遍扫描还是两遍扫描。如果需要的列数据一行可以放入max_length_for_sort_data
则使用一遍扫描否则使用两遍>扫描。MySQL根据sort_buffer_size
来判断是否使用磁盘临时文件,如果需要排序的数据能放入sort_buffer_size
则无需使用磁盘临时文件,此时explain只会输出using filesort
否则需要使用磁盘临时文件explain会输出using temporary;using filesort
。
当看到MySQL的explain输出using filesort不要太过紧张,这说明排序的时候没有使用索引,如果输出using temporary;using filesort则需要引起注意了,说明使了磁盘。临时文件,效率会降低。一句话using filesort需要酌情优化。
优化策略:
- 增大sort_buffer_size 参数的设置,使得单路可以一次i/o就结束
- 增大max_length_for_sort_data参数的设置
- order by 时 select * 是一个大忌,只query需要的字段:
group by 关键字优化
几乎与order by 一致
- group by实质是先排序后进行分组,组照索引建的最佳左前缀
- 无法使用索引列时,应增大max_length_for_sort_data参数的设置+增大sort_buffer_size 参数的设置
- where高于having,能写在where限定的条件就不要去having限定了
慢SQL优化
从数据库的慢查询文件进行分析,优化的方式主要就是修改sql写法和新增索引。
优化步骤:
- 先查看慢日志,获得具体哪条sql语句是慢sql
window 下为文件my.ini , linux 下为my.cnf 文件:
[mysqld] //在此标识之后添加
slow_query_log = 1; #开启慢日志地址
slow_query_log_file=/var/lib/mysql/atguigu-slow.log #慢日志地址,缺省文件名host_name-slow.log
long_query_time=3; #运行时间超过该值的SQL会被记录,默认值>10
log_output=FILE
配置完成后,重启MySQL服务:service mysqld restart
- 再使用explain sql 语句,进行对慢sql分析
这一步也很重要,具体就是对explain的使用:explain推荐阅读 在这里就不过多叙述。
- 修改sql语句,或者增加索引。
判断是否是索引失效
优化数据库结构
- 将字段很多的表分解成多个表,将使用频率高的字段单独分离出来形成一个表,使用频率低的字段单独分离出来形成一张表
- 增加中间表,对于经常联合查询的表,通过建立中间表的方式把经常联合查询的数据插入到中间表中,然后将原来的联合查询改为对中间表查询
分解关联查询
将一个大的查询分解为多个小的查询。对关联查询进行分解,对每一个表进行一次单表查询,将结果在程序中进行关联。例如:
select * from tag join tag_post on tag_id = tag.id join post on tag_post.post_id = post.id where tag.tag = \'mysql\';
分解为:
select * from tag where tag = \'mysql\';
select * from tag_post where tag_id = 1234;
select * from post where post.id in (123,456,789);
优化limit分页
偏移量非常大的时候,尽可能的使用索引覆盖扫描,而不是查询所有的列,然后根据需要做一个关联操作在返回所需的列。
也可以给筛选字段加上索引,还可以先查询出主键id,通过id的值直接查询id后面的数据。甚至可以建立复合索引acct_id 和create_time 尽量避免引起filesort。
分库分表
一般就是垂直切分和水平切分,这是一种结果集描述的切分方式,是物理空间上的切分。
我们从面临的问题,开始解决,阐述: 首先是用户请求量太大,我们就堆机器搞定(这不是本文重点)。
然后是单个库太大,这时我们要看是因为表多而导致数据多,还是因为单张表里面的数据多。
如果是因为表多而数据多,使用垂直切分,根据业务切分成不同的库。
如果是因为单张表的数据量太大,这时要用水平切分,即把表的数据按某种规则切分成多张表,甚至多个库上的多张表。
分库分表的顺序应该是先垂直分,后水平分。 因为垂直分更简单,更符合我们处理现实世界问题的方式。
垂直拆分
-
垂直分表
也就是“大表拆小表”,基于列字段进行的。一般是表中的字段较多,将不常用的, 数据较大,长度较长(比如text类型字段)的拆分到“扩展表“。一般是针对那种几百列的大表,也避免查询时,数据量太大造成的“跨页”问题。
-
垂直分库
垂直分库针对的是一个系统中的不同业务进行拆分,比如用户User一个库,商品Producet一个库,订单Order一个库。 切分后,要放在多个服务器上,而不是一个服务器上。为什么? 我们想象一下,一个购物网站对外提供服务,会有用户,商品,订单等的CRUD。没拆分之前, 全部都是落到单一的库上的,这会让数据库的单库处理能力成为瓶颈。按垂直分库后,如果还是放在一个数据库服务器上, 随着用户量增大,这会让单个数据库的处理能力成为瓶颈,还有单个服务器的磁盘空间,内存,tps等非常吃紧。 所以我们要拆分到多个服务器上,这样上面的问题都解决了,以后也不会面对单机资源问题。
水平拆分
-
水平分表
针对数据量巨大的单张表(比如订单表),按照某种规则(RANGE,HASH取模等),切分到多张表里面去。 但是这些表还是在同一个库中,所以库级别的数据库操作还是有IO瓶颈。不建议采用。
-
水平分库分表
将单张表的数据切分到多个服务器上去,每个服务器具有相应的库与表,只是表中数据集合不同。 水平分库分表能够有效的缓解单机和单库的性能瓶颈和压力,突破IO、连接数、硬件资源等的瓶颈。
-
水平分库分表切分规则
-
-
RANGE
从0到10000一个表,10001到20000一个表;
-
HASH取模
一个商场系统,一般都是将用户,订单作为主表,然后将和它们相关的作为附表,这样不会造成跨库事务之类的问题。 取用户id,然后hash取模,分配到不同的数据库上。
-
地理区域
比如按照华东,华南,华北这样来区分业务,七牛云应该就是如此。
-
时间
按照时间切分,就是将6个月前,甚至一年前的数据切出去放到另外的一张表,因为随着时间流逝,这些表的数据 被查询的概率变小,所以没必要和“热数据”放在一起,这个也是“冷热数据分离”。
-
MVCC知识点(源于JavaGuide,总结很全面)
脏读:事务读取到其他事务没有提交的数据
不可重复读:同一次事务中前后查询不一致的问题
幻读:一次事务中前后数据量发生变化,用户产生不可预料的问题。另一个事务前后查询相同数据时的不符合预期。
innodb 引擎中隔离级别为可重复读时,也可以杜绝幻读的可能性。
更改操作涉及字段只有在索引列范围之内时,才会加上行锁。否则就加上表锁,使得程序不具备并发性。
在MySQL innodb存储引擎下读已提交和可重复读基于MVCC(多版本并发控制)进行并发事务控制,MVCC是基于“数据版本”对并发事务进行访问。
在 Repeatable Read
和 Read Committed
两个隔离级别下,如果是执行普通的 select
语句(不包括 select ... lock in share mode
,select ... for update
)则会使用 一致性非锁定读(MVCC)
。并且在 Repeatable Read
下 MVCC
实现了可重复读和防止部分幻读。
undo-log
- 当事务回滚时用于将数据恢复到修改前的样子
- 另一个作用是
MVCC
,当读取记录时,若该记录被其他事务占用或当前版本对该事务不可见,则可以通过undo log
读取之前的版本数据,以此实现非锁定读
在 InnoDB
存储引擎中 undo log
分为两种: insert undo log
和 update undo log
:
insert undo log
:指在insert
操作中产生的undo log
。因为insert
操作的记录只对事务本身可见,对其他事务不可见,故该undo log
可以在事务提交后直接删除。不需要进行purge
操作update undo log
:update
或delete
操作中产生的undo log
。该undo log
可能需要提供MVCC
机制,因此不能在事务提交时就进行删除。提交时放入undo log
链表,等待purge线程
进行最后的删除
不同事务或者相同事务的对同一记录行的修改,会使该记录行的 undo log
成为一条链表,链首就是最新的记录,链尾就是最早的旧记录。
隐藏字段
在内部,InnoDB
存储引擎为每行数据添加了三个隐藏字段:
DB_TRX_ID(6字节)
:表示最后一次插入或更新该行的事务 id。此外,delete
操作在内部被视为更新,只不过会在记录头Record header
中的deleted_flag
字段将其标记为已删除DB_ROLL_PTR(7字节)
回滚指针,指向该行的undo log
。如果该行未被更新,则为空DB_ROW_ID(6字节)
:如果没有设置主键且该表没有唯一非空索引时,InnoDB
会使用该 id 来生成聚簇索引
ReadView
class ReadView {
/* ... */
private:
trx_id_t m_low_limit_id; /* 大于等于这个 ID 的事务均不可见 */
//目前出现过的最大的事务 ID+1,即下一个将被分配的事务 ID。大于等于这个 ID 的数据版本均不可见
trx_id_t m_up_limit_id; /* 小于这个 ID 的事务均可见 */
//活跃事务列表 m_ids 中最小的事务 ID,如果 m_ids 为空,则 m_up_limit_id 为 m_low_limit_id。小于这个 ID 的数据版本均可见
trx_id_t m_creator_trx_id; /* 创建该 Read View 的事务ID */
//创建该 Read View 的事务 ID
trx_id_t m_low_limit_no; /* 事务 Number, 小于该 Number 的 Undo Logs 均可以被 Purge */
ids_t m_ids; /* 创建 Read View 时的活跃事务列表 */
//Read View 创建时其他未提交的活跃事务 ID 列表。创建 Read View时,将当前未提交事务 ID 记录下来,后续即使它们修改了记录行的值,对于当前事务也是不可见的。m_ids 不包括当前事务自己和已提交的事务(正在内存中)
m_closed; /* 标记 Read View 是否 close */
}
Read View
主要是用来做可见性判断,里面保存了 “当前对本事务不可见的其他活跃事务”。事务可见范围:
RC和RR隔离级别下MVCC的不同
- 在 RC 隔离级别下的
每次select
查询前都生成一个Read View
(m_ids 列表) - 在 RR 隔离级别下只在事务开始后
第一次select
数据前生成一个Read View
(m_ids 列表)
一致性非锁定读与锁定读
-
在
InnoDB
存储引擎中,多版本控制 (multi versioning) 就是对非锁定读的实现。如果读取的行正在执行DELETE
或UPDATE
操作,这时读取操作不会去等待行上锁的释放。相反地,InnoDB
存储引擎会去读取行的一个快照数据,对于这种读取历史数据的方式,我们叫它快照读 (snapshot read)。对于 一致性非锁定读(Consistent Nonlocking Reads) 的实现,通常做法是加一个版本号或者时间戳字段,在更新数据的同时版本号 + 1 或者更新时间戳。查询时,将当前可见的版本号与对应记录的版本号进行比对,如果记录的版本小于可见版本,则表示该记录可见。 -
如果执行的是下列语句,就是 锁定读(Locking Reads)
select ... lock in share mode
select ... for update
insert
、update
、delete
操作
在锁定读下,读取的是数据的最新版本,这种读也被称为
当前读(current read)
。锁定读会对读取到的记录加锁:select ... lock in share mode
:对记录加S
锁,其它事务也可以加S
锁,如果加x
锁则会被阻塞select ... for update
、insert
、update
、delete
:对记录加X
锁,且其它事务不能加任何锁
补充:MySQL锁
- 共享锁【S锁】
又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。
这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
- 排他锁【X锁】
又称写锁。若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁。
这保证了其他事务在T释放A上的锁之前不能再读取和修改A。
- 间隙锁【Gap Lock】
间隙锁是封锁索引记录中的间隔,或者第一条索引记录之前的范围,又或者最后一条索引记录之后的范围。间隙锁范围为左开右闭。
对于指定查询某一条记录的加锁语句,如果该记录不存在,会产生记录锁和间隙锁,如果记录存在,则只会产生记录锁,如:WHERE id = 5 FOR UPDATE;
对于查找某一范围内的查询语句,会产生间隙锁,如:WHERE id BETWEEN 5 AND 7 FOR UPDATE;
产生间隙锁的条件(RR事务隔离级别下;):
- 使用普通索引锁定;
- 使用多列唯一索引;
- 使用唯一索引锁定多行记录。
死锁问题:不同于写锁相互之间是互斥的原则,间隙锁之间不是互斥的,如果一个事务A获取到了(5,10]之间的间隙锁,另一个事务B也可以获取到(5,10]之间的间隙锁。这时就可能会发生死锁问题。
解决方案:通过修改数据库的参数innodb_locaks_unsafe_for_binlog
来取消间隙锁从而达到避免这种情况的死锁的方式尚待商量, 那就只有修改代码逻辑, 存在才删除,尽量不去删除不存在的记录。
- 临键锁【Next-key Locks】
是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。临键锁的主要目的,也是为了避免幻读(Phantom Read)。如果把事务的隔离级别降级为RC,临键锁则也会失效。
InnoDB
存储引擎在 RR 级别下通过 MVCC
和 Next-key Lock
来解决幻读问题:
1、执行普通 select
,此时会以 MVCC
快照读的方式读取数据
在快照读的情况下,RR 隔离级别只会在事务开启后的第一次查询生成 Read View
,并使用至事务提交。所以在生成 Read View
之后其它事务所做的更新、插入记录版本对当前事务并不可见,实现了可重复读和防止快照读下的 “幻读”
2、执行 select...for update/lock in share mode、insert、update、delete 等当前读
在当前读下,读取的都是最新的数据,如果其它事务有插入新的记录,并且刚好在当前事务查询范围内,就会产生幻读!InnoDB
使用 Next-key Lock 来防止这种情况。当执行当前读时,会锁定读取到的记录的同时,锁定它们的间隙,防止其它事务在查询范围内插入数据。只要我不让你插入,就不会发生幻读
以上是关于MySQL索引基础补充以及优化笔记-上的主要内容,如果未能解决你的问题,请参考以下文章