本文是阅读《MySQL技术内幕-InnoDB存储引擎》第二版,《MySQL DBA修炼之道》,以及《MySQL王者晋级之路》所记录的笔记.
InnoDB体系架构
线程
- 主线程(Master Thread):将缓冲池中的数据异步刷新到磁盘.
- IO线程(IO Thread):处理异步IO的请求以及回调.
- 清理线程(Purge Thread):回收已经使用但并未分配的Undo页.
- 页清理线程(Page Cleaner Thread):负责脏页的刷新操作
内存
LRU列表用来管理已经读取的页,如果LRU列表里读取的页被修改,那么这个时候内存和磁盘的数据不一致就会产生脏页.
脏页由FlushList进行管理.FreeList管理空闲页的列表,如果后续FreeList列表空间不足,从LRU列表进行淘汰再进行分配空间.
脏页通过CheckPoint机制刷新到磁盘.
- 缓冲池:缓冲池可以将索引页,数据页,undo页,插入缓存,自适应哈希索引,InnoDB存储的锁信息,数据字典等信息放入到内存,加速查询以及修改.缓冲池不可能完全承载文件数据,因此缓冲池当中的数据需要根据改进的LRU算法进行淘汰.
- 重做日志缓冲:存储重做日志
- 额外的内存池:存储内部数据结构
Check Point 机制
CheckPoint由主线程按照1s或者10s刷新一定比例的脏页到磁盘.
CheckPoint目的是为了:缩短数据库恢复时间,释放缓冲池空间,重做日志不可用时刷新磁盘.
InnoDB核心设计
以下为InnoDB特性,为数据库可靠性,高性能提供了保证.
InsertBuffer
为了解决非聚集索引的离散插入,删除或更新的问题的问题,InsertBuffer诞生.
注意官方后续将InsertBuffer改名为ChangeBuffer,显然ChangeBuffer更合适一些.
众所周知,非聚集索引插入会引起随机IO,因为非聚集索引和聚集索引的顺序可能是不一致的,当一条数据插入时是数据页是顺序的但是对于非聚集索引就可能不是了,因此需要将插入操作合并,以减少随机IO.
前提条件:
索引是辅助索引.
索引是非唯一索引(因为InnoDB不能保证插入新值的唯一性).
InsertBuffer是存在于共享表空间中(ibdata1),本质是一个B+树,当一辅助索引插入到非聚集索引页当中,先判断这个页在缓冲池如果在直接插入,若不存在构造一个search key插入到B+树页子节点,search key包含space id(表空间),offset(页中偏移量).
如下情况会进行Merge Insert Buffer :
- 辅助索引被读取到缓冲池,InsertBuffer中的该辅助索引页的相关记录会插入辅助索引页
- MasterThread
- 辅助索引页无可用空间
这时候随机插入变为顺序写入,性能极大提升.
DoubleWrite
当一种情况写入16KB的页之写入一半发生故障后,该页被损坏不能直接从重做日志恢复.
因此两次写保证InnoDB写入文件的可靠性.
思路:将页复制一个副本,防止写入失效时页损坏.进行恢复时,先通过的页的副本还原该页,然后进行重做日志恢复.
DoubleWriteBuffer提供2M的共享表空间,存储写入页的信息.
先将脏页写入到DoubleWriteBuffer内存中(memcpy),然后写入共享表空间中(此时为顺序写,需要进行fsync),然后在将数据页写入相应的表空间文件.
AdaptiveHash
InnoDB会根据数据的访问频率和模式(联合索引 (a, b),where a=X 与 where a=X and b=X是不同模式)来自动建立Hash索引.
但是有一些局限性就是只适合 =
条件.
AIO与刷新邻近页
通过内核异步IO可以提升磁盘效率.
写入脏页时会判断临近的脏页如果存在会一起同过AIO执行.
日志
查询日志
查询日志记录对应主机查询的所有请求记录日志,这个一般不被开启.
set global general_log = off
慢查询日志
通过配置long_query_time
慢查询时间,log_slow_queries
开启慢查询.
# Time: 180927 0:17:41
# User@Host: schema_sync2[schema_sync2] @ [10.12.26.10] Id: 24454
# Query_time: 1.472336 Lock_time: 0.000084 Rows_sent: 679 Rows_examined: 679 Logical_reads:1000 Phiscal_reads:20
select * from orders;
二进制日志
二进制存储格式有三种:
1.statement:按照语句存储,可能会导致主从数据不一致
2.row:按记录存储,保证数据一致性,二进制日志比较安全主要是由于包含了变化之前的数据信息
3.mixed:混合模式兼容效率与存储一致性的一种方式,当SQL中包含有SQL_FOUND_ROWS(),UUID()等函数会转为row模式
### UPDATE `tjy`.`idx_tjy_orders_apply_order_id`
### WHERE
### @1=1527782400004279 /* LONGINT meta=0 nullable=0 is_null=0 */
### @2=480939048 /* LONGINT meta=0 nullable=0 is_null=0 */
### @3=0 /* LONGINT meta=0 nullable=0 is_null=0 */
### SET
### @1=1527782400004279 /* LONGINT meta=0 nullable=0 is_null=0 */
### @2=480939048 /* LONGINT meta=0 nullable=0 is_null=0 */
### @3=12701324 /* LONGINT meta=0 nullable=0 is_null=0 */
重做日志
重做日志文件存储InnoDB存储引擎的事务重做日志.
配合redo log buffer进行写入,先写入缓存然后写入日志文件.
为了高效恢复数据库,节省磁盘空间,重做日志采用了循环写文件方式.
保证事务的ACID中持久性,innodb_flush_log_at_trx_commit
需要设置为1,redo log buffer实时同步到磁盘.当然也可以设置为0,2,代表着不同的意思,根据实际生产环境需要设置.
undo日志
undo日志存储的是事务回滚的逻辑日志.
与redo日志不同的是undo日志存放在数据库中一个特殊段内,存放于共享表空间内.
表
索引组织表:表都是按照主键顺序进行存放.如果表没有定义主键,判断是否有非空的唯一索引,如果没有自动创建一个6字节大小的指针.
设置inno_db_file_per_table
后存储空间每个表设置独立的表空间.
表名.idb:存储表数据,索引和插入缓冲位图等信息.
表名.frm:表结构关系.
段
表空间由各个段组成,常见的段有数据段,索引段,回滚段等.数据段为B+树的叶子节点,索引段为B+树非叶子节点.
区
区是由连续的页组成,每个区的大小固定为1MB.
页
默认大小为16KB,页类型分为数据页,undo页,事务数据页等.
行
MySQL是面向行存储的,每个页最大存储16KB/2-200行 .
行记录格式
- Compact格式:页中存放的数据越多性能越高,列中NULL值不占用存储空间.
- Redundant格式:老版本格式,CHAR类型的NULL值需要占用存储空间,而VARCHAR类型不需要占用存储空间.
以上格式中行溢出:针对行溢出数据,如果一个页无法存储两个行记录,溢出的数据会放置到Uncompressed BLOB页,数据页只会存储前768字节,并存储一个指针指向BLOB页中的数据.
之后InnoDB进行优化,之前的Compact和Redundant格式称为Antelope.Compressed和Dynamic称为Barracuda.
Barracuda格式采取完全行溢出格式,行记录只保存20字节的指针,真正的数据存放在OffPage.
其中Compressed格式:会对行数据进行zlib算法的压缩.
数据页
除此之外数据页最外层包含了页头信息(FileHeader),校验页完整性写入磁盘(FileTrailer)
每个数据页包括Infimum 和Supremum ,这两个用于标记该数据页中行记录边界.
实际存储行记录的内容(UserRecord)与记录删除后空间会被加入到空闲链表(FreeSpace)内.
PageDirectory用于存放记录相对位置,将页加载到内存然后,通过PageDirectory二分查找对应记录.
分区表
支持四种方式进行分区:
1.HASH:对分区列进行hash,通过对指定的分区数求余数放入到对应的分区
2.Range:根据建表语句对应的范围建立分区
3.List:根据指定列的值,放入到不同分区
4.Key:和hash分区类似,不同之处是使用用户定义函数进行分区
5.Column:形式是以Range,List类似但是以上四种不支持非整型,但是Column支持字符串分区,DATE,DATETIME.
子分区
在Range,List分区的基础上再进行Hash,key分区.
每个子分区数量必须相同.
分区表的局限性
在OLTP中的使用限制比较多,例如使用非分区字段查询,会引起IO次数增加,从而影响性能.
分区功能不够强大,这种方案比较适合OLAP业务比如:销售记录的表,涉及到日期或
还有一点就是没有索引表的概念,当涉及到非分区列的查询可以避免全表扫描.
索引
聚集索引:按照主键构造B+树,叶子节点存放整张表的行记录.
辅助索引:叶子节点包含主键信息(聚集索引建),以及索引建值信息.
联合索引:多了进行索引,按照索引定义顺序进行存放.因此就会有一些问题:a,b,c创建的索引,当条件包含a=X
,a=X and b=X
, a=X and b=X c=X
时会使用索引.因为索引的排序是按照先排a再排b再排c.因此单独使用b,或者b,c都不是有序的.
覆盖索引:覆盖索引从辅助索引就可以查到记录.
MRR优化:索引都是按照索引的大小顺序进行排序.所以当通过辅助索引查到主键的值可能是离散的,离散的值意为着性能很差.MRR(Multi-Range Read)会按照主键顺序进行排序后再查询,减少数据随机访问.
ICP优化:除了MRR优化外还有ICP(INDEX CONDITION PUSHDOWN),取索引的同时会对where条件的进行过滤.
自适应哈希索引经哈希函数映射到一个哈希表中.对于=
的条件性能提升很大.关于自适应哈希索引的使用情况可以通过SHOW ENGINE INNODB STATUS
进行查看.
InnoDB支持全文索引,但是局限性很严重,只支持Latin文字.
使用建议:
1.where条件不要对索引使用表达式,如left(A, 1) = "c"
2.主键最好自增整型
3.索引基数尽量趋近于1
4.使用更短索引或者前缀索引
5.索引列适当,如果太多降低更新能
6.唯一索引性能更好
7.覆盖索引可以减少查询IO
8.利用索引排序
复制
MySQL复制实现:
1.主库将数据变更信息写入binlog
2.从库IO线程请求主库将变更信息从binlog写入relay log中继日志
3.从库SQL线程把中继日志应用到当前数据库
MySQL支持级联复制.
半同步复制
半同步主库在发送一个事务提交会阻塞到至少一个半同步从库已经”接收到事务事件”为止,否则会发生超时.
半同步从库在写入事件到中继日志,刷新到磁盘后才确认接收到事务事件.
半同步复制区别与”全同步复制”的是,当至少一个从库收到事务事件时半同步复制并不等待从库的提交.
如果没有从库接收到事务事件会发生事务超时,这时候会转换到异步复制模式,如果一个半同步从库追赶上从库,将会切到半同步复制.
表设计
三范式原则:
第一:每一列都是不可分割的数据项
第二:所有数据都要和该数据表的主键有完全依赖关系
第三:非建属性之间无关
三范式原则使用有些局限性,实际可能更多使用的是反范式设计原则.
在设计时尽量用ER模型图:
矩形框:表示实体,在框中记入实体名
菱形框:表示联系,在框中记入联系名
椭圆形框:表示实体或联系的属性,将属性名记入框中。对于主属性名,则在其名称下划一下划线
连线:实体与属性之间;实体与联系之间;联系与属性之间用直线相连,并在直线上标注联系的类型。(对于一对一联系,要在两个实体连线方向各写1; 对于一对多联系,要在一的一方写1,多的一方写N;对于多对多关系,则要在两个实体连线方向各写N,M。)
使用explian
explain在实际的开发当中有着重要的作用,可以分析当前SQL的性能.
字段 | 取值 | 解释 |
---|---|---|
id | – | 表名SQL语句执行的顺序,如果id值相同那么执行顺序从上到下,如果不同那么值越大越先被执行 |
select type | SIMPLE | 不包含UNION以及子查询 |
PRIMARY | 包含复杂查询,当前最外层查询 | |
DERIVED | 当前查询为衍生查询(from语句之后的子查询) | |
SUBQUERY | 当前SQL中SELECT或WHERE包含子查询 | |
UNION | SELECT出现在UNION之后 | |
UNION RESULT | 从UNION的表中取结果的SELECT | |
table | SQL语句查询的表 | |
type | ALL | 指明当前的SQL全表扫描 |
index | 使用索引进行全表扫描 | |
range | 索引范围扫描 | |
ref | 使用索引等值查询 | |
eq_ref | 使用唯一索引查询 | |
system | 系统优化后的查询,转化为常量 | |
const | system特例,查询中只有一行 | |
null | 性能最高的查询 | |
possible_keys | – | 可能使用的索引 |
keys | – | 真正使用的索引 |
key_len | – | 索引的长度 |
ref | – | 连接查询的字段 |
rows | – | 扫描的行数 |
extra | Using where; | 使用条件过滤 |
Using filesort | SQL中没有使用索引字段而导致使用文件排序,当SQL结果集较小会使用内存进行文件排序,如果结果集太大那么,会使用临时文件进行排序. | |
Using Index | 使用索引 | |
Using Index Condition | 使用Index Condition Pushdown,将数据过滤放置到存储引擎层,提升查询性能 | |
Using Temporary | Union,使用衍生查询或者是order by与group by字段不一致等导致使用临时表的情况,还有一些其他可能出现情况请点击,实际要避免这类情况的发生. |
配置参数
我们完全可以透过InnoDB的配置参数,来理解InnoDB设计的精髓.
这些配置其实是InnoDB设计的体现.
参数 | 说明 |
---|---|
innodb_buffer_pool_size | buffer_pool实际大小 |
innodb_buffer_pool_instances | buffer_pool个数 |
query_cache_size | 尽可能的小些,推荐设置为0 |
max_connections | 最大的连接数 |
innodb_log_file_size | redo日志文件大小, 不能设置太小会导致不停切换文件async checkpoint,设置太大会导致数据库恢复时间太长 |
innodb_adaptive_hash_index | 是否开启自适应hash索引,默认开启加速等值查询速度 |
innodb_change_buffering | 默认值是all,一般情况不需要改变,change buffer 请见InsertBuffer |
innodb_read_ahead_threshold | 是否开启缓冲池线性预读,默认开启ON,在缓冲池当中的连续访问会异步从磁盘预读多个页到缓冲池 |
innodb_thread_concurrency | 最大并发线程数量,一般不推荐修改,除非上下文切换是瓶颈 |
innodb_read_io_threads,innodb_write_io_threads | 数据页read/write IO线程数,根据部署环境可适当增大 |
innodb_io_capacity | 设置InnoDB整个IO吞吐量,默认值200,涉及到IO相关的任务(例如:刷新脏页,写入插入缓存),SSD磁盘或者RAID磁盘都可以讲该值提高 |
innodb_max_dirty_pages_pct | 超出脏页刷新的百分比,如果超出就刷新脏页 |
innodb_adaptive_flushing | 根据redo log 产生的速度以及脏页的数量来控制刷新脏页的数量,默认开启 |
innodb_rollback_segments | 回滚段个数的配置,InnoDB支持128回滚段,其中32保留用于临时表的事务.默认最大支持96K左右并发,如果设计临时表事务最大支持32K并发. |
innodb_purge_threads | Purge thread的个数,如果DML操作只执行在少数表或者单个表,那么可以设置的低一些防止与其它线程形成竞争.如果是很多个表的DML操作比较多可以适当增大 |
innodb_old_blocks_pct | 0-95, 默认将读取的新页插入到LRU列表末端的37%的位置,防止一些扫描表或索引的操作将LRU中的热数据冲刷掉. |
innodb_old_blocks_time | 毫秒数,默认插入innodb_old_blocks_pct的隔1000毫秒后再插入LRU列表热端. |
补充
update是原子操作吗
实际上来说Update是原子操作.
1.如果开启了autocomit模式,那么所有的语句都在事务当中,每个语句组成一个单一的事务,当语句执行成功后事务自动进行提交.官方文档
2.UPDATE,DELETE,锁定读 (select for update)语句一般会根据具体条件使用的索引加锁,对于非唯一索引会锁住扫描的范围(next-key),而对于唯一索引搜索伟一行会使用record lock.如果在事务当中执行,事务的隔离级别也会影响锁的范围官方文档
所以类似:UPDATE sku SET stock = stock - 1 where id = 10
这样的语句是安全可靠的.
CHAR类型和VARCHAR类型
在可变长度字符集如UTF8下,CHAR类型和VARCHAR类型实际底层存储行没有区别.
MySQL不支持物化视图
MySQL支持视图,但是不存在实际存储.
物化视图可以通过触发器简单实现.
RAID
Redundant Array of Independent Array:独立磁盘冗余数组:将几个磁盘组合成一个逻辑的扇区.
增加数据容错性,增加磁盘容量.
可以极大提升性能,提供并行IO能力.
- RAID0:将多个磁盘组成一个逻辑磁盘,并行IO,查询写入性能高.
- RAID1:将N个磁盘组成互为镜像,容错性高.
- RAID10和RAID01:结合了RAID0和RAID1的特性,性能和容错性高.
排序和索引
当我们使用主键查询并用索引列排序肯能会导致一个问题,就是使用文件排序:
文件排序是一个非常高昂的操作,需要尽量避免.
mysql> explain select id,status from orders where id > 1000 order by create_time;
+----+-------------+--------+-------+---------------+---------+---------+------+----------+-----------------------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------+-------+---------------+---------+---------+------+----------+-----------------------------+
| 1 | SIMPLE | orders | range | PRIMARY | PRIMARY | 8 | NULL | 49403851 | Using where; Using filesort |
+----+-------------+--------+-------+---------------+---------+---------+------+----------+-----------------------------+
上述例子中create_time,与id都有是索引列,但是MySQL确采用了文件排序.这是新手通常会犯的一个错误.
尽量使用以下两点避免该问题:
- 索引列和order by列尽量一致
- 对连接查询,order by 列要用首张表的列
索引的Cardinality
Cardinality用于指示索引的唯一特性,与索引字段性能有直接关系.
由于show index from
table 中Cardinality不能准确更新当前索引的基数,其更新策略 :
1.表中1/16数据变化
2.20亿数据被修改.
可以使用count(DISTINC index_col)/count(*)
来实时检测当前唯一index列的个数.