关系型数据库设计理论及部署实现

news2024/12/23 16:53:29

ACID

索引实现方式

事务隔离级别

并发场景

写-写冲突

MVCC

数据库隐式字段

读视图

删表语句

insert与replace区别

Mysql相关参数

索引扫描方式

索引下推

复制日志

基于操作语句复制

基于预写日志(WAL)复制

基于行的逻辑日志复制

基于触发器的复制

主从同步

多主复制

Mysql备份

数据恢复方式

分布式数据库要注意的一些点

缓冲

buffer pool读缓冲

change buffer写缓冲

Doublewrite Buffer双写缓冲

Redo log

Wal预写日志

redo log刷磁盘规则

Undo log

undo与MVCC的关系

redo与undo关系

purge线程

Binlog

两阶段提交

Group commit

ARIES理论

LSN

ARIES理论示例

Checkpoint

自增锁(Auto-inc Locks)

共享/排他锁(Shared and Exclusive Locks)

意向锁(Intention Locks)

插入意向锁(Insert Intention Locks)

记录锁(Record Locks)

间隙锁(Gap Locks)

临键锁(Next-key Locks)

元数据锁

全局锁

快照读与当前读

select加锁逻辑

insert加锁逻辑

回滚

死锁

CAS

表设计模式

表空间

表结构

存储形式

表设计相关原则

虚拟表

物化视图

分库分表

排序实现方式

Union

DECLARE EXIT HANDLER FOR SQLEXCEPTION

性能调优

子查询优化

深分页处理

大范围查找不走索引的情况

Join

optimize table

Explain执行示意

ACID

ACID

实现方式

原子性(Atomicity)

MVCC、undo log

一致性(Consistency)

约束(主键、外键)

隔离性

MVCC、锁

持久性

WAL日志(redo log、dwb log)

索引实现方式

二叉树:极端情况可能退化为链表,性能不稳定;

红黑树:插入和删除运算复杂,性能不佳;对大数据量的支撑不好,当数据量很大时,树的高度太高,操作耗时变长。

B树:包含有键值、存储子节点的指针信息、及除主键外的数据,相对于B+树而言B-树非叶子节点存储信息太大,树层数较高;

不选哈希表的原因:需要支持范围查询、模糊查询、最左匹配、排序操作。

事务隔离级别

可重复读(REPEATABLE READ):当前正在执行事务产生的数据变化不能被外部看到,也就是说,如果用户在另外一个事务中执行同条SELECT语句数次,结果总是相同的。

标准上RR级别会有幻读,innodb通过临键锁nextkey lock在RR级别下不会有幻读问题,即达到了serialize的隔离级别。

读提交(READ COMMITTED):处于READ COMMITTED 级别的事务可以看到其他事务已提交的对数据的修改。也就是说,在事务处理期间,如果其他事务修改了相应的表,那么同一个事务的多个 SELECT 语句可能返回不同的结果。

读未提交(READ UNCOMMITTED):处于这个隔离级的事务可以读到其他事务还没有提交的数据。一般不使用。

设置不同的隔离级别主要是为了提高数据库的并发性能,用更好的方式去处理读-写冲突,做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。

并发场景

所有系统的并发场景都是三种,对于数据库而言为:

读-读:不存在任何问题,不需要并发控制。

读-写:有线程安全问题,可能会造成事务隔离性问题,也就是脏读,不可重复读,幻读。

写-写:有线程安全问题,会存在更新丢失问题:第一类更新丢失(别的事务回滚将本事务的数据覆盖),第二类更新丢失(覆盖丢失)。

-写冲突

实际数据库的实现不会出现第一类更新丢失。快照读是第二类更新丢失的一个主要原因,因为快照读无法获取最新的数据即别的事务刚修改完已经提交的数据。需要将一致性非锁定读替换为一致性锁定读,使用悲观锁/乐观锁解决。

第二类更新丢失场景:事务中,先select取到对应行,然后对该行进行更新,然后执行update,因此将这一时间段内其他事务对该条数据(已提交)的更新覆盖了。

因此如果业务逻辑复杂,需要悲观锁:

Select...For update  //锁指定的行,一直到事务提交或回滚。

悲观锁:可能死锁,且阻塞其他事务。

乐观锁:不加锁,基于update的原子性实现CAS。

总结:

MVCC可以解决多版本读写冲突加锁,而写写冲突还是需要锁来保证。

原子操作通常采用对读取对象加独占锁的方式来实现,这样在更新被提交之前不会其他事务可以读它。如果数据库不支持内置原子操作,另一种防止更新丢失的方法是由应用程序显式锁定待更新的对象(for update)。

MVCC

MVCC用于解决读写冲突不加锁,提升并发性能。其实现基于几个概念:隐藏字段、undo log、读视图。

数据库隐式字段

数据库表每行记录除了业务字段外,还有数据库隐式定义的几个字段:

DB_ROW_ID:隐藏自增ID,如果表没有定义主键,会使用该ID。

DB_TRX_ID:最近修改的事务ID,记录创建这条记录以及最后一次修改该记录的事务的ID。

DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本(undo log日志)。

DELETED_BIT:delete操作跟truncate/drop不一样,执行delete时仅使用该标志位标记逻辑删除,在purge线程中完成真正回收。

读视图

不同隔离级别在select读时表现不同,主要是生成读视图(Read View)的策略不同。读视图指当前事务执行快照读的那一刻,生成数据库系统当前的一个快照。实现方式为记录当前活跃事务的ID列表,即trx_list。事务ID默认单调递增。查询不会新增事务ID,只有DML才会导致事务ID增加。

读视图有三个全局参数(每个存储引擎实例各自单独维护):

trx_list:未提交事务ID列表,用来维护Read View生成时刻系统正处于活跃状态的事务ID。

up_limit_id:记录trx_list事务ID列表中最小的ID,也就是最初修改该记录的事务。

low_limit_id:Read View生成时刻数据库尚未分配的下一个事务ID,也就是当前最大事务ID + 1。

以RR级别和RC级别来举例,假定当前事务ID = 10,trx_list为(4,8, 10),因为当前事务10正在执行,所以自己也活跃,所以此时 up_limit_id=4,low_limit_id=11。

如果读到一行数据的事务ID=3,小于活跃列表的最小值(up_limit_id=4),可见;如果读到一个数据的事务ID = 12,大于low_limit_id,不可见,需要根据回滚指针db_roll_ptr从undo log中找到小于up_limit_id的版本,或者不在活跃事务列表trx_list中的版本。

RR级别和RC级别的区别是:

RR级别事务第一次执行select的时候生成一个读视图,并且整个事务过程中一直使用这个视图;RC级别在每次读取时都获取最新的视图,即trx_list会更新,因此会读到已提交事务的修改信息。

trx_list等数据是InnoDB内部数据结构,如果需要对存储引擎内部的事务进行监控和分析,可以使用performance_schema和information_schema系统表。

删表语句

删表的有三条命令:Delete、truncate、drop,区别为:

Truncate、drop属于DDL,自动提交事务,delete需要手动提交。

DELETE语句执行删除的过程是每次从表中删除一行,并且同时将该行的删除操作作为事务记录在日志中保存以便进行进行回滚操作。

truncate重新创建一个表,并不影响与被删除的表相关联的任何结构、约束、触发器或者授权。

drop是整表删除,当删除和重新创建表时,所有与之相关联的索引、完整性约束和触发器也被删除。同样,所有针对被删除表的授权也会被删除。

truncate会使得自增列计数复原;

delete所有数据后,自增列计数并不会从头开始。

insert与replace区别

insert into ON DUPLICATE KEY UPDATE语句与replace into流程相同,先执行delete,后执行insert into。区别为,如果replace into填充的字段不全,则未被更新的字段都会被修改为默认值。insert into ON DUPLICATE KEY UPDATE语句相当于执行的是if exist do update else do insert,只是更新部分字段,对于未被更新的字段不会改动。 所以:在相关应用场景中,如数据同步服务,尽量使用insert into ON DUPLICATE KEY UPDATE语句,保证不会造成数据字段丢失。举例:

insert into person values (5,'s','1','内蒙古') ON DUPLICATE KEY UPDATE address = '黑龙江';//如果id5存在,仅修改address,name和phone字段不会被修改;

replace into person(id,name,phone) values (1,'张','1') ;//如果id1存在,会导致address被重置为默认值。

Mysql相关参数

发送消息包大小限制参数max_allowed_packet参数,默认是4M;

expire_log_days:指定天数后清理二进制日志;

Innodb_buffer_pool_instances 把缓冲区分为多个段,多核扩展。

索引扫描方式

索引跳跃扫描

示例:select * from t_table where a >= 1 and b = 2;

使用a-b联合索引,从符合a = 1 and b = 2条件的第一条记录开始扫描,而不需要从a字段值为1的第一条记录开始扫描。

mysql8.0有索引跳跃扫描(Skip scan range),如果命令不写成"select * ",写成" select a,b",即满足select列和where列包含在一个索引中(即不回表),就会使用Skip Scan Range来优化。使用explain可以看到Extra列显示Using index for skip scan,key_len就是使用索引的长度。

在mysql8.0之前的版本中,如果要使用到索引进行扫描,条件必须满足索引前缀列,比如索引idx(col1,col2), 如果where条件只包含col2的话,是无法有效使用idx的, 需要扫描索引上所有的行即扫表,然后再根据col2上的条件过滤。

Skip scan range可以避免全量索引扫描,而是根据每个col1上的值+col2上的条件,启动多次range scan。每次range scan根据构建的key值直接在索引上定位,直接忽略了那些不满足条件的记录。

跳跃扫描机制只有在唯一性较差的情况下,才能发挥优化效果。如果联合索引的第一个字段是一个值具备唯一性的字段,那去重一次再拼接,几乎等价于扫全表。且使用有一些限制,如多表联查时无法触发、SQL条件中有分组操作无法触发、SQL中用了DISTINCT去重也无法触发。

松散索引扫描

松散索引扫描相当于Oracle中的跳跃索引扫描(skip index scan)。

先讲一下GROUP BY子句的实现方式:1、扫描整个表并创建一个新的临时表;紧凑索引扫描;松散索引扫描。使用临时表时,表中每个组的所有行为连续的,然后使用该临时表来找到组并应用累积函数。基于松散索引扫描,可以通过索引访问而不用创建临时表。

select * from xxx where B = xxxgroup by A;

先根据A索引分组后,会在每个A的范围内使用索引进行快速查询定位所需要的B列,这就叫做松散索引扫描。使用explain命令,Extra列会显示using index for group-by。

999cbb998cec4143e2038be0a34f608b.png

当 GROUP BY 条件字段并不连续或者不是索引前缀部分的时候,检查where 中的条件字段是否有索引的前缀部分,如果有前缀部分,且该部分是一个常量,且与group by 后的字段可以组合成为一个连续的索引,这时按紧凑索引扫描。

松散索引扫描同样有限制:单表、group by条件字段为联合索引,且连续;不能有聚合运算函数。

对比

索引跳跃扫描通过索引的层级关系,跳过不符合查询条件的索引行,直接访问符合条件的索引行,以加快查询速度。松散索引扫描通过扫描多个相邻的索引页,获得所有索引行,并且可以充分利用索引页(Index Page)的缓存,减少 I/O 操作。

索引跳跃扫描适用于高选择性的查询(即查询结果占数据总量比较少的情况),而松散索引扫描适用于需要计算或排序的查询。

索引下推

如下例,在(age,name)字段建联合索引,并且查询SQL是这样的时候:

select* from user where age = 18 and name = '张三';

如果没有索引下推,会先匹配出age = 18的多条记录拿到id,再用ID回表查询,筛选出 name = '张三' 的记录。

如果使用索引下推,会先匹配出age = 18的多条记录,再筛选出 name = '张三' 的一条记录,拿到id,最后再用ID回表查询。

由此得出,索引下推的优点:减少了回表的扫描行数。

索引下推有效的前提是联合索引可以覆盖到要搜索的范围即where条件。

工程实现中,基于性能考虑,对二级索引的更新都是异步的。

复制日志

基于操作语句复制

缺点:任何调用非确定性函数的语句,如NOW()获取当前时间,或RAND()获取随机数等,可能会在不同的副本上产生不同的值。

如果语句中使用了自增列,或者依赖于数据库的现有数据(例如,UPDATE WHERE <条件>),则所有副本必须按照完全相同的顺序执行,否则可能会带来不同的结果。

有副作用的语句(例如,触发器、存储过程、用户定义的函数等),可能会在每个副本上产生不同的副作用。

基于预写日志(WAL)复制

缺点:日志描述的数据结果包含了哪些磁盘块的哪些字节发生改变,使得复制方案和存储引擎紧密耦合。如果数据库的存储格式从一个版本改为另一个版本,那么系统通常无法支持主从节点上运行不同版本造成兼容性问题。

如果复制协议严格要求多节点版本一致,主节点切换时需要停机才能切换,否则可能从节点版本比主节点更新。

基于行的逻辑日志复制

复制和存储引擎采用不同的日志格式,这样复制与存储逻辑剥离。这种复制日志称为逻辑日志,以区分物理存储引擎的数据表示。

通常的实现方式:

对于行插入,日志包含所有相关列的新值。

对于行删除,日志里有足够的信息来唯一标识已删除的行,通常是靠主键,但如果表上没有定义主键,就需要记录所有列的旧值。

对于行更新,日志包含足够的信息来唯一标识更新的行,以及所有列的新值(或至少包含所有已更新列的新值)。

基于触发器的复制

将复制操作交给应用层,灵活性更高。通过触发器,可以将数据更改记录到一个单独的表中, 然后外部处理逻辑访问该表。

主从同步

mysql高可用实现方式:主从复制、cluster系统、DRBD。

磁盘复制drbd

与RAID1的区别:RAID1也是实现不同存储设备间的数据镜像备份的,不同的是RAID1各存储设备是连接一个RAID控制器接入到一台主机上的,而DRBD是通过网络实现不同节点主机存储设备数据的镜像备份。

多主复制

多数据中心修改数据,需要保证收敛一致。

给每个写入分配唯一的ID,冲突时选最大的ID,会有已收到写入成功会的的消息丢失的情况。

为每个副本/数据中心分配唯一ID,冲突时选最大ID的结果。

以某种方式合并冲突,如git合并冲突的方式。

用预定义好的格式记录冲突,并通知用户。可以预定义一段逻辑,在写入或者读取时执行。

Mysql备份

1、在线逻辑备份,mysqldump;

导出包含了建表,插入数据的SQL语句集

可以在线进行,不影响数据库对线上持续提供服务。

相比物理备份拷贝库文件,备份和恢复都要慢非常多。

2、离线物理备份(冷备),拷贝从库库文件;

(a)第一步,将一个从库从集群里摘下并下线,此时离线库文件不会再发生变化;

(b)第二步,scp拷贝库文件,即完成了库的物理备份;

(c)文件拷贝完成后,将从库挂回集群;

备份和恢复都非常快。
缺点:备份过程中从库无法对线上持续提供服务。

3、PXB

热备非阻塞备份工具。

PXB启动一个线程,并不断监听并复制redo log的增量到另外的文件。redo log是循环使用的,PXB则必须记录下checkpoint LSN之后的所有redo log。

然后,PXB启动另一个线程,然后开始复制数据文件,复制数据文件过程可能会比较长,整个过程中数据文件可能在不停的修改,导致数据不一致。但没有关系,所有的修改都已经记录在了第一步中,额外记录的redo log里。

最后,通过备份的数据文件,重放redo log,执行类似于MySQL崩溃恢复过程中的动作,就能够使得数据文件恢复到能保证一致性的checkpoint检查点。

数据恢复方式

1、基于gtid

2、基于position

分布式数据库要注意的一些点

读自己的写

某个业务,数据写入A节点,读的却是B节点的数据,因此未获取到最新的写更新。这里需要基于业务ID进行哈希,此外还需要考虑多终端登录时网络时延不一致的情况。

两次读,返回不同节点的结果,导致不一致。单调读一致性要求用户总从某个从节点读取数据。

前缀一致读:对于一系列按照某个顺序发生的写请求,那么读取这些内容时也会按照当时写入的顺序。

缓冲

05fca320a55fafebd59762c1f8c4678f.png

buffer pool读缓冲

mysql数据存放在磁盘里面,如果每次查询都直接从磁盘里面查询,会影响性能,因此需要内存态缓存池。

free链表挂载空闲缓存页,flush链表挂载缓存脏页,脏页定时刷回磁盘。通过“表空间号+数据页号”作为key去数据页缓存哈希表里查一下,如果没有就读取数据页,如果已经有了,就说明数据页已经被缓存了。

提升缓存命中率:

innodb的缓存淘汰机制是改进版LRU,防止大量临时缓存挤出热点数据。buffer pool读缓存分为了老年代和新生代,当有新页面加入buffer pool时,插入的位置是老年代(冷数据)的头部,并且该页面1s后再次被访问的话,再被移动到新生代(热数据),防止短时间频繁访问的临时数据污染缓冲区。

change buffer写缓冲

如果是写请求,且需要修改的数据不在缓冲池,一般不会将磁盘页先加载到缓冲池,而是把写操作缓存到change buffer中,当下次访问到这条数据后,会把数据页加载到buffer pool中,并且合并change buffer里面的变更。这样保证了数据的一致性,且批量写对性能影响不大。

写缓冲过程既有内存缓存,也有对应的磁盘页持久化。一个写请求加入写缓冲(内存),然后使用WAL机制写入redo log(磁盘)。

如果索引设置了唯一(unique)属性,在进行修改操作时,InnoDB需要做唯一性检查也就是将相应的页从磁盘读入到缓存。因此不适合写缓冲。所以写缓冲适合写多读少,且非唯一索引的场景。将原本每次写入都需要进行磁盘IO(随机写)的SQL,优化为定期批量写磁盘。

Doublewrite Buffer双写缓冲

Innodb的数据页一般是16K,磁盘的页一般是4K,所以写一次磁盘数据,会有4次写磁盘的原子操作。因此如果刷盘时写完前面4K后断电,此时4K是新数据,后面的12K是老数据,出现了数据被破坏的情况,也就是页分裂。此时虽然通过page的checksum能判断page损坏了,但是因为redo log对于insert/update操作不是记录原页完整数据,而是仅记录diff数据,因此无法基于redo log修复这类页数据损坏异常。需要使用double write buffer来保证磁盘数据写入的成功。

68dcae4352157c33d7d67bac27ecbf9b.png

如上图,脏页刷盘时写两次磁盘,一次顺序追加写入dwb磁盘页,然后才是更新数据库真实磁盘页。dwb磁盘页类似rodo log,也是两个日志文件来回切,会覆写。

当然也有其他解决办法,比如将磁盘页原值完整写入redo log,或者使用16k页大小的文件系统,由硬件保证16kb写入的原子性。

Dwb log和redo log都是基于数据库的持久性。数据库异常崩溃时,如果没出现磁盘页数据损坏,能够通过redo恢复数据;如果出现磁盘页数据损坏,能够通过double write buffer再通过redo恢复页数据。

Redo log

redo日志用于保障已提交事务的持久性。且将随机写优化为顺序写。Redo log是循环使用的。

Redo log记录数据库变化的日志,记录的数据块包括:表所在数据块(表数据块),索引所在数据块(索引数据块),以及undo段所在数据块(undo数据块)。

redo log内容需要记录:

1、某个数据页中(page num);

2、某个某个偏移位置(offset);

3、某个类型的数据(type);

4、改后的新值(value);

当数据库崩溃的时候,如果缓冲池中的新数据没有来得及刷盘,数据库恢复后就可以通过redo log,如把第1234页,偏移量为5678处的1个字节改为1,以此来恢复数据。

redo记录流程大致如下:

1、InnoDB将记录从硬盘读入内存,或直接写change buffer;

2、生成一条undo log并写入redo log buffer,记录的是数据被修改后的值;

3、当事务commit时,将redo log buffer中的内容刷新到redo log file磁盘,对 redo log file采用追加写的方式;

4、定期将内存中修改的数据刷新到磁盘中。这里不是指从redo log file刷入磁盘,而是业务数据从内存缓存刷入磁盘,redo log file只在崩溃恢复数据时才用。如果数据库崩溃,则依据redo log buffer、redo log file进行重做,否则是数据页正常刷盘流程。

Wal预写日志

数据页有LSN,redo log中有LSN,Checkpoint也有LSN。

redo log刷盘先于数据页中数据修改,即redo_log_on_disk_lsn高于data_in_buffer_lsn。当事务commit提交时,innodb先将redo log buffer 顺序写入到redo log file进行持久化,事务的commit才算操作完成。在持久化一个数据页之前,先将内存中相应的redo日志页持久化。

默认事务提交之前,对数据更改产生的日志(redo log和undo log)都存储在log buffer 中,事务提交时,才持久化到磁盘文件中。log buffer的持久化策略可以通过参数innodb_flush_log_at_trx_commit 进行调节。

innodb_flush_log_at_trx_commit设置为2,为每次写到系统缓存,每隔1s刷到磁盘,此时redo log不能完全保证数据不丢失;设置为1,即每次提交事务都会刷磁盘。

Redo log采用物理与逻辑混合记法,以page为单位记录日志,page里面采用逻辑记法(记录page里面哪一行哪个offset改前改后的值)。

redo log刷磁盘规则

1、commit事务提交时(由innodb_flush_log_at_trx_commit设置控制);

2、innodb_flush_log_at_timeout设置刷磁盘间隔,默认每秒刷一次;

3、log buffer中已经使用的内存超过log buffer总容量一半时;

4、正常关闭服务器。MySQL停止时是否将脏数据和脏日志刷入磁盘,由innodb_fast_shutdown决定,可以实现快速关闭;

5、有checkpoint时,checkpoint触发后,会将buffer中脏数据页和脏日志页都刷到磁盘。

数据刷盘的规则只有一个:checkpoint

触发checkpoint的情况:1、每隔10s;2、脏页达到阈值强制触发。

innodb_fast_shutdown:

取0,表示当MYSQL关闭时,Innodb需要完成所有full purge和merge insert buffer操作;

取1,不需要完成full purge和merge insert buffer操作,但是在缓冲池的数据脏页会刷新到磁盘。

取2,不需要完成full purge和merge insert buffer操作,也不将缓冲池中的数据脏页写回磁盘,而是将日志都写入日志文件。如果在上次关闭innodb的时候是在innodb_fast_shutdown=2或是mysql crash这种情况,那么启动时会利用redo log重做那些已经提交了的事务。

Oracle日志刷磁盘规则

1、日志缓冲区中的记录达到1M;

2、间隔3秒;

3、日志缓冲区已经用了三分之一。

Undo log

undo日志用于保障未提交事务不会对数据库的ACID特性产生影响,即一致性。

undo段记录内容分为:

insert undo log;

update undo log;

因为对其他事务的可见性不同,这两种log分别存放在不同的buffer里。

insert undo log是指在insert 操作中产生的undo log,因为insert操作的记录,只对事务本身可见,对其他事务不可见。因此undo log可以在事务提交后直接删除,不需要进行purge操作。

而update undo log记录的是对delete 和update操作产生的undo log,该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行真正的删除。

索引

Undo log属于单独表空间数据,也有一个基于B+树的索引结构。该索引结构将每个事务的日志记录按顺序组织,并采用预读(prefetching)技术来加速索引的访问。当一个事务发起读取或修改操作时,undo log会使用该事务的ID来定位其对应的日志记录,并根据索引结构将其读入内存。为了减少索引访问的开销,undo log还使用了分区和缓存等技术来优化索引性能,都是分布式系统通用的性能优化方式。

分区副本

为了提高可靠性和容错性,undo log支持将日志副本(LogReplica)存储在多个节点上。每个节点都可以担任一个或多个副本的存储节点,当一个节点发生故障时,其他节点可以用其副本来恢复数据。

存储布局

不同的数据存在不同的页面中,B+树索引存在 fil_page_index 类型的页面中,表空间头部信息存储在fil_page_type_fsp_hdr类型的页面中,undo日志存储在类型为fil_page_undo_log的页面中。所以undo log的多少不会引起数据页分裂。

隔离级别为RR时,事务读取的都是开启事务时的提交行版本,只要该事务不结束,该行版本就不能删除,即undo log不能删除。

回滚

对于insert操作,undo日志仅记录新数据的主键PK(ROW_ID),回滚时直接删除;

对于delete/update操作,undo日志需要记录旧数据整行内容,回滚时直接恢复。

在insert一条数据时,会向聚簇索引、二级索引都插入一条记录,但是undo日志只会记录一条针对聚簇索引的日志。如果回滚,使用聚簇索引进行回滚删除会将其他索引也一起删除,delete、update操作也是相同方式。

undo与MVCC的关系

MVCC多版本并发控制就是基于undo log实现,undo log简化理解为多个事务对某行数据写操作的记录链,类似写时复制。示例:

1、事务0往person表中插入了一条新纪录,记录为:name = a1,age = 11;

隐式主键 = 1,事务ID和回滚指针此时都为 NULL;

2、事务1对该行的name作出修改,改为a2;

2.1在事务1修改该行记录数据的同时,数据库会先对该行加排他行锁。

2.2上锁完毕后,将该行数据拷贝到undo log中,作为旧记录,即在 undo log 中有当前行的拷贝副本。

2.3拷贝完毕后,修改该行的 name 为 tom,并且修改隐藏字段的事务ID为当前事务1的ID,这里我们默认是从1开始递增,回滚指针指向拷贝到 undo log 的副本记录,即通过该指针可以找到上一个版本。

2.4事务提交后,释放锁。

3、又来一个事务2修改person表的同一行记录,将age项修改为30;

在事务2修改该行数据之前,数据库继续给该行上排他锁。

上锁完毕之后,把该行数据拷贝到undo log 中,作为旧记录,发现操作的这行记录已经有undo log的记录了,那么最新的旧数据作为链表的表头,插在这行记录的 undo log 日志的最前面。

修改该行age为22,并且修改隐藏字段的事务ID为当前事务2的ID,回滚指针指向刚刚拷贝到undo log的副本记录。

事务提交,释放锁。

undo log的链表首部是最新的旧记录,尾部是最旧的记录。purge线程会定期清理掉up_limit_id之前的事务记录。

redoundo关系

前滚:当实例崩溃时,可以使用redo从以前正常的点前滚到崩溃点。(前滚从一致性检查点,“即当时检查过所有的SCN/LSN是全部一致的时间点”,一直往前滚到崩溃的时间点)。当数据库回到一致性检查点时,相当于之后什么都没有发生过,数据全被清空了。数据库只好根据redo模拟还原之前的数据变更操作,使用redo里的信息重做(use redo log to redo),构造undo块,表块,索引块等。然后再根据undo log恢复磁盘数据。

回滚:构造的表数据块中,有已修改的脏数据但未提交,就需要利用前滚中构造的undo数据块里的信息来undo撤销还原,覆盖回滚rollback(保持一致性,每种块里的scn/lsn号都一样,那么数据库就可以打开了)。

purge线程

purge线程两个主要作用是:清理回收undo页和清除page里面带有Delete_Bit标识的数据行。在InnoDB中,事务中的Delete操作实际上并不是真正的删除掉数据行,而是一种Delete Mark操作,在记录上标识Delete_Bit,而不删除记录。只是做了个标记,真正的删除工作需要后台purge线程去完成。

innodb_purge_threads控制后台线程数量。

Binlog

不管什么存储引擎,对数据库进行了修改都会产生二进制日志,类似于redis 的AOF备份。记录表结构变更及表数据修改。二阶段提交中为保证主从一致,binlog先于redo log被记录。

binlog是通过追加的方式进行写入的,可以通过max_binlog_size 参数设置每个 binlog文件的大小,当文件大小达到给定值之后,会生成新的文件来保存日志。binlog使用.index文件来做索引,一个事务的binlog有完整的格式校验。

binlog为什么不能单独作为数据恢复:

bin log 是追加日志,保存的是全量的日志。因此没有标志能让InnoDB从bin log中判断哪些数据已经刷入磁盘了,哪些数据还没有。

两阶段提交

innodb将更改记录写入redo log,将状态设置为prepare状态;

生成binlog并写入磁盘;

将redo log更新为commit状态。

两阶段提交中崩溃恢复的判断规则是这样的:

1、如果redo log里面的事务是完整的,也就是已经有了commit标识,说明binlog肯定完整,则直接提交;

2、如果 redo log 里面的事务处于prepare状态,则判断对应的事务binlog是否存在并完整:

2.1. 如果binlog存在并完整,则提交事务。因为binlog写入成功之后就会被备库同步过去,所以为了主备一致,主库需要提交事务。

2.2. 否则,回滚事务。因为binlog还没有写入,所以就不会传给备库,之后从库进行同步的时候,无法执行这个操作,但是实际上主库已经完成了这个操作,所以为了主备一致,在主库上需要回滚这个事务。

binlog使用场景

在实际应用中,binlog 的主要使用场景有两个,分别是 主从复制 和 数据恢复 。

主从复制:在Master 端开启 binlog ,然后将 binlog发送到各个 Slave 端, Slave 端重放 binlog 从而达到主从数据一致。

数据恢复:通过使用 mysqlbinlog 工具来恢复数据。

binlog刷盘时机

开启事务后,binlog会在写缓冲里面,同步到磁盘的时机由sync_binlog=N决定,N次提交后刷盘。

binlog格式

binlog日志有三种格式:STATMENT、ROW和MIXED。日志格式通过 binlog-format 指定:STATMENT:基于SQL 语句的复制,每一条会修改数据的sql语句会记录到binlog 中。

优点:不需要记录每一行的变化,减少binlog日志量,节约了IO。

缺点:在某些情况下会导致主从数据不一致,比如执行sysdate()等 。

ROW:基于行的复制,将被修改的相关行的每一列的值都在日志中保存下来,是默认格式。

优点:不会出现某些特定情况下的存储过程、function、trigger的调用和触发无法被正确复制的问题;

缺点:会产生大量的日志,尤其是` alter table ` 的时候会让日志暴涨;

MIXED:基于STATMENT和 ROW 两种模式的混合复制。一般的复制使用STATEMENT 模式保存 binlog ,对于STATEMENT 模式无法复制的操作使用 ROW 模式保存 binlog。

binlog日志删除

binlog日志文件按照指定大小,当日志文件达到指定的最大的大小之后,进行滚动更新,生成新的日志文件。

binlog的默认是保持时间由参数expire_logs_days配置,也就是说对于非活动的日志文件,在生成时间超过expire_logs_days配置的天数之后,会被purge线程删除。也可以使用purge命令手动删除,但是不能直接删除文件,直接删文件index不会更新。

Group commit

多个binlog一起调用fsync刷盘。

ARIES理论

LSN

Redo log、数据页都有LSN参数(等同于oracle的SCN),用于实现innodb的事务性中的原子性与持久性。

应用层的一个事务,可能修改多个不同磁盘页。比如下面这个事务:

start transaction
update 表1某行记录
delete 表1某行记录
insert 表2某行记录
Commit

应用层面,事务是三条SQL语句,涉及两张表;而物理层面,可能是修改了四个或多个Page。所以,一个逻辑事务对应的物理日志不是连续的,但一个物理事务(Mtr)对应的日志一定是连续的,即使横跨多个redo log Block。

因为所有应用事务共享redo log buffer,因此一个应用事务对应的物理事务在redo log的block里可能是不连续的,中间穿插其他事务的日志,两个逻辑事务的Redo Log在磁盘上排列可能如下所示,存在交叉:

ecd71d3484e3382a67737664404bf6b9.png

物理事务:innodb引擎保证物理页面写入操作完整性及持久性的机制。

同一个事务的多条LSN日志通过链表串联,最终形成的redo block块如下表所示。TxID是InnoDB为每个事务分配的隐式字段,是一个单调自增id。

06cd8ac5b2fec97fc5569c92e70819ef.png

因为不同事务的redo log日志是交叉存在的,因此redo日志刷盘时不管buffer里面的事务有没有提交,都会刷入磁盘。那么数据库崩溃恢复后,redo log会全部重放,那么如何将未提交的事务找到并使用undo log回滚呢?ARIES理论提供了解决方案。

下图可以看出,checkpoint时刻会更新data buffer、data disk、redo buffer、redo disk、checkpoint的LSN,事务提交会更新data buffer、redo buffer的LSN。

f2495cc4a10b373608a8d9fc8836cb03.png

ARIES理论示例

aries主要描述了“No Force, Steal”的WAL日志实现,描述数据库崩溃时事务的恢复逻辑。

假定有T0~T5共6个事务,每个事务所在的线段区间表示在Redo Log中的起始和终止位置。数据库宕机时,T0、T1、T2已经完成,T3、T4、T5还在进行中,所以回滚的时候,要回滚T3、T4、T5三个事务。

9a254a67f4e3515186e7371ea2baa56e.png

崩溃恢复流程如下:

(1)分析阶段

分析阶段解决两个问题:

第一,确定哪些数据页是脏页,为阶段2的redo做准备。发生宕机时,虽然T0、T1、T2事务已经提交了,但只能保证对应的Redo Log已经刷盘,其对应的数据页不一定从buffer刷到磁盘上了。所以需要找到从上一次checkpoint到宕机这段时间内未刷盘的数据脏页。

第二,确定哪些事务未提交,为阶段3的undo做准备。未提交事务的日志也写入了Redo Log。对应到此图,就是T3、T4、T5的部分日志也在Redo Log中。需要基于checkpoint判断出T3、T4、T5事务未提交,然后对其进行回滚。

Checkpoint

Checkpoint是每隔一段时间对内存中的数据进行刷盘,有如下两种方式:

Fuzzy checkpoint:进行部分脏页的刷新,有效循环利用Redo日志。

Sharp checkpoint:发生在关闭数据库时,将所有脏页刷回磁盘。

因为内存缓存数据量比较大,实际使用中使用Fuzzy checkpoint机制,要使用Sharp机制需设置快速关闭参数innodb_fast_shutdown=1。

Fuzzy checkpoint机制用到了两张表:活跃事务表和数据脏页表。

活跃事务表是当前所有未提交事务的集合,每个事务维护了一个关键变量lastLSN,是该事务产生的日志中最后一条日志的LSN。

78bc054754c6cdc5138144f54265e284.png

脏页表是当前所有未刷到磁盘上的Page的集合(包括已提交的事务和未提交的事务),其中recoveryLSN是导致该Page为脏页的最早的LSN,也就是最后一次刷盘后最早开始的事务产生的日志的LSN。

f4478da5a25295c8ac53c597ba6a2a3c.png

Fuzzy Checkpoint机制会把这两个表的数据生成一个快照,形成一条checkponit日志,记入Redo Log中。

fuzzy checkpoints策略下,在做checkpoint的时候,所有的事务都可以正常工作,并且不会强制把所有的脏页都落盘.

fuzzy checkpoints策略把原先的日志中的checkpoint的一个时间点变成一个时间段:checkpoint阶段会写两条日志:checkpoint-begin与checkpoint-end,对应checkpoint阶段的开始与结束.

基于上面两张表可以解决下面的问题:

1、得到宕机时所有未提交的事务。

如上图,在最近的一次Checkpoint 2时候,未提交事务集合是{T2,T3},此时还没有T4、T5。从此处开始,遍历Redo Log到末尾。

在遍历的过程中,首先遇到了T2的结束标识,把T2从集合中移除,剩下{T3};

之后遇到了事务T4的开始标识,把T4加入集合,集合变为{T3,T4};

之后遇到了事务T5的开始标识,把T5加入集合,集合变为{T3,T4,T5}。

最终直到末尾,没有遇到{T3,T4,T5}的结束标识,所以未提交事务是{T3,T4,T5}。

57b9cd4ade06a4bd4cee22a41d8db76f.png

事务的开始标识、结束标识以及Checkpoint在Redo Log中的位置如上图。其中的S表示Start transaction,事务开始的日志记录;C表示Commit,事务结束的日志记录。每隔一段时间,做一次Checkpoint,会插入一条Checkpoint日志。Checkpoint日志记录了Checkpoint时所对应的活跃事务的列表和脏页列表。

2、得到宕机时所有未刷盘的脏页。

假设在Checkpoint 2的时候,脏页的集合是{P1,P2}。从Checkpoint开始,一直遍历到Redo Log末尾,一旦遇到Redo Log操作的是新的磁盘页,就把它加入脏页集合,最终结果可能是{P1,P2,P3,P4}。因为redo log在磁盘页上的操作是幂等的,因此对已经刷了变更数据的磁盘页重复执行redo操作是没有影响的。

阶段2:进行Redo操作

假定得到的脏页集合是{P1,P2,P3,P4,P5}。取集合中所有脏页的recoveryLSN的最小值,也就是最后一次刷盘后最早活跃的事务id,假定为firstLSN。从firstLSN遍历Redo Log到末尾,把每条Redo Log对应的数据页全部重刷一次磁盘。重放redo日志时,如果redo日志的LSN <= pageLSN,则不修改redo日志对应的数据页,丢弃该条日志。

阶段3:进行Undo操作

在阶段1,已经找出了所有的未提交事务{T3,T4,T5}。因为undo日志是数据链,所以可以沿着T3、T4、T5各自的日志链一直回溯,直到回溯到第一条日志。

基于redo log的持久性,在undo回滚时遇到宕机,我们可以在崩溃恢复后继续回滚。

整体而言,如何加锁与隔离级别及字段索引有关。

模式

锁表

锁行

锁范围

共享S

表共享锁

行共享锁

Gap、nextkey、插入意向锁

排他X

表排他锁

行排他锁

意向共享IS

表意向共享锁

/

意向排他IX

表意向排他锁

/

自增锁


/

自增锁(Auto-inc Locks)

自增锁是锁,专门针对事务插入AUTO_INCREMENT类型的列。
串行时,如果一个事务正在往表中插入记录,所有其他事务的插入必须等待,以便第一个事务插入的行,是连续的主键值。

InnoDB提供了innodb_autoinc_lock_mode配置,可以配置insert时是否加锁。

insert大致上可以分成三类:
1、simple insert,如insert into t(name) values('test');
2、bulk insert,如load data | insert into ... select .... from ....;
3、mixed insert,如insert into t(id,name) values(1,'a'),(null,'b'),(5,'c');

innodb_autoinc_lock_mode配置为1时,对simple insert 做了优化。由于simple insert一次性插入值的个数提前确定,所以mysql可以一次生成几个连续的值,使用内存锁自增数即可。

innodb_autoinc_lock_mode配置为2时,此时不用自增锁,性能最好,但是可能id不连续。因此范围查询id时可能出现幻读,即同一事务连续执行的两条insert记录之间有id空洞。

binglog格式为mixed/row时,innodb_autoinc_lock_mode配置为任何值都是安全的。此时mode可以设置为2获取最佳性能;

binglog格式为statement 时,可能出现binlog写入不一致的问题。如事务A的insert into t1 values(xx);语句后执行但先写入binlog,事务B的insert into t2 select * from t1;先执行但后写binlog,备库就会将xx这条记录也写入表t1,出现主备不一致。

如果不是自增列,就不会使用自增锁,而是插入意向锁。

注意:InnoDB 表只是把自增主键的最大ID(LAST_INSERT_ID)记录到内存中,所以重启数据库或者是对表进行OPTIMIZE 操作,都会导致最大ID丢失。

共享/排锁(Shared and Exclusive Locks)

粒度为行或表,共享/排他锁:

(1)事务拿到某一行记录的共享S锁,才可以读取这一行;

(2)事务拿到某一行记录的排它X锁,才可以修改或者删除这一行;

共享/排它锁的潜在问题是,读写不能充分的并行,解决方案是数据多版本MVCC。

意向锁(Intention Locks)

意向锁解决排他锁效率问题,如下场景:事务A在某行记录加了排他锁,事务B需要给表加排他锁,此时如果逐行判断,效率太低。innodb使用意向锁解决这个问题,事务A先申请表的意向共享锁IS,成功后再申请某行记录的记录锁S。事务B发现表上有IS,则申请X锁被阻塞。

IS、IX锁跟表S、表X锁互斥。意向锁之间,即IS、IX都相互兼容。


IS

IX

S

X

AI

IS

v

v

v

/

v

IX

v

v

/

/

/

S

v

/

v

/

/

X

/

/

/

/

/

AI

v

v

/

/

/

意向锁特点:

a、级别锁;

b、意向锁分为:

意向共享锁(intention shared lock, IS),指事务有意向对表中的某些行加共享S锁;

意向排它锁(intention exclusive lock, IX),指事务有意向对表中的某些行加排它X锁;

对于select当前读,select ... lock in share mode,要设置IS锁;select ... for update,要设置IX锁;

c、意向锁规则:

事务要获得某些行的S锁,必须先获得表的IS锁;

事务要获得某些行的X锁,必须先获得表的IX锁;

d、意向锁仅仅表明意向,意向锁之间并不相互互斥,而是可以并行。

插入意向锁(Insert Intention Locks)

对已有数据行的修改与删除,必须加强互斥锁X锁;而对于数据的插入insert,不需要这么强的互斥,使用插入意向锁。

插入意向锁,是间隙锁的一种,也是实现在索引上的,用于insert操作执行前。多个事务,在同一个索引,同一个范围区间执行insert插入数据时,如果插入的位置不冲突,不会阻塞彼此。插入意向锁只会和间隙锁或者临键锁冲突,间隙锁作用就是防止其他事务插入记录造成幻读。正是由于在执行INSERT语句时需要加插入意向锁,而插入意向锁和间隙锁冲突,从而阻止了插入操作的执行。

假设不是插入并发,而是读写并发,事务A先执行,select查询一些记录,还未提交;事务B后执行,在两条记录中insert插入了一行,除非显式加锁,普通的select语句都是快照读,不加锁。

记录锁(Record Locks)

记录锁锁索引记录,例如:

select * from t where id=1for update;

会在id=1的索引记录上加锁,以阻止其他事务插入,更新,删除id=1的这一行。“ FOR UPDATE ”语句会对返回的所有结果行加锁,直到事务commit或回滚。

间隙锁(Gap Locks)

间隙锁的主要目的是为了防止其他事务在间隔中插入数据,以导致“不可重复读”。间隙锁只阻止其他事务插入到间隙中,不阻止其他事务在同一个间隙上获得间隙锁,所以 gap x lock 和 gap s lock有相同的作用。

如果把事务的隔离级别降级为读提交RC,间隙锁则会自动失效。即RC级别只有行锁,没有间隙锁。

Gap锁用在非唯一索引或者不走索引的当前读中,对于普通非唯一索引来讲,并不是所有的gap都会去上锁,只会对要修改的地方的周边上gap锁。

1、索引的范围搜索,如:

SELECT * FROM test WHERE id BETWEEN 5 AND 7 FOR UPDATE;

2、索引的不存在查找,如:

SELECT * FROM test WHERE id=3 FOR UPDATE;

当前读不走索引的时候,它会对所有的gap都上锁,这也就类似锁表了,这样也同样能达到防止幻读的效果。

临键锁(Next-key Locks)

临键锁,是记录锁间隙锁的组合,临键锁会封锁索引记录本身,以及索引记录之前的区间

临键锁的主要目的,也是为了避免幻读。

元数据锁

元数据锁即MDL锁,表查询时MySQL会在表上加该锁,防止被别的session修改了结构定义,如多加了一个字段。不是用于锁数据。

每执行一条DML、DDL语句时都会申请metadata锁。DML操作需要metadata读锁,DDL操作需要metadata写锁。访问表时自动加上,无需业务层控制。

查询不加锁,是指不在表上加innodb行锁;如果打开了元数据锁,是会加SHARED_READ锁的。

MDL锁是排队的,所以如果前面有修改表字段的事务在等锁,会阻塞后面所有的读事务。

如果设置auto commit=true , 对于select没有显式开启事务(begin)的语句,元数据锁和行锁都不加,是真的“读不加锁”。

全局锁

对整个数据库实例加锁,此时数据库处于只读状态,一般用于逻辑备份。与设置数据库只读命令 set global readonly=true 相比,全局锁在发生异常时会自动释放,而且global readonly可能有其他的作用,比如用来判断是否为备库,手动修改可能有其他影响。

快照读与当前读

当前读

又称一致性读,select lock in share mode(共享锁),select for update;update,insert,delete(排他锁);这些操作都是一种当前读,当前读读取的记录都是目前数据库中最新的版本。读取时还要保证其它并发事务不能修改当前记录,所以会对读取数据加锁

快照读

像不加锁的select命令及DML命令就是使用的快照读,即不加锁的非阻塞读。串行级别下的快照读会加锁,退化成当前读。

不加锁的select操作(即不包括加锁命令select ... lock in share mode, select ... for update)。快照读读到的版本数据取决于快照读先出现的地方的时机:

  RC隔离级别:每次select都生成一个快照读,RC级别下快照读每次都能看到其他事务提交的最新的修改。

RR隔离级别:开启事务后第一个select语句才是生成快照读的地方,此时将当前系统中活跃的其他事务记录起来,而不是一开启事务就生成快照读。如果首次生成快照读是在别的事务进行修改之前调用的,此后即便别的事务做了修改,本事务还是看不到改动后的最新数据,因为会从undo log里找到生成快照读事的事务id。

这里就引出RR级别快照读比当前读查询慢的问题:

select * from t where id=1;

可能比

select *fromt where id=1 lock in share mode;

执行慢,也就是加锁操作反而执行快一些。

session A

session B

start transaction with consistent snapshot;



update t set c=c+1 where id=1;//执行100万次

select* from t where id=1;


select*from t where id=1 lock in share mode;


session B执行完100万次update语句后,id=1这一行生成了100万个回滚日志(undo log),用链表指向。

带lock in share mode的SQL命令,是当前读,因此会直接读到1000001这个结果,执行速度很快;而select *from t where id=1命令,是一致性读,RR级别下需要从事务1000001开始,依次执行undo log,回滚100万次以后,才将1这个结果返回。

select加锁逻辑

1、普通的select使用快照读(RR/RC隔离级别相同):

select * from t where id=1;

快照读是不加锁的一致性读,基于MVCC来实现。

2、加锁的select(select ... in share mode / select ... for update),DML命令,加锁条件取决于索引是否唯一,where查找条件是唯一还是范围查找:

2.1在唯一索引上使用唯一的查询条件,也就是where条件全部命中,会使用记录锁(record lock),锁住对应的那条记录,而不会封锁记录之间的间隔,即不会使用间隙锁(gap lock)与临键锁(next-key lock) ;如果回表,聚簇索引对应的主键也会加锁;

2.2其他情况,如范围查询条件,或者where未全部命中等,会使用间隙锁与临键锁,锁住索引记录之间的范围,避免范围间被其他事务插入记录,以避免产生幻影行记录,避免不可重复读。对于update,如果update的是聚集索引记录,则对应的普通索引记录也会被加锁,因为普通索引存储主键的值,再回表扫描聚集索引。

RC级别除了在外键约束检查以及重复键检查时会锁区间,其他场景都只使用记录锁,锁住该条记录,而不是锁区间,因此会有幻读,即两次select结果不一致。

如果是RC级别,普通读是快照读,不加锁;加锁的select, DML命令,使用记录锁,因此可能出现不可重复读的现象。

RR级别下,删除不存在的记录,需要获取共享间隙锁;插入需要获取排他间隙锁。

insert加锁逻辑

1、对插入的间隙加插入意向锁(Insert Intension Locks):

1.1如果该间隙已被加上了间隙锁与临键锁,则加锁失败进入等待;这两种锁与插入意向锁互斥;

1.2如果没有,则加锁成功,表示可以插入;

2、然后判断插入记录是否有唯一键,如果有,则进行唯一性约束检查:

2.1如果不存在相同键值,则完成插入;

2.2如果存在相同键值,则判断该键值是否加锁:

2.2.1如果没有锁,判断该记录是否被标记为删除:

如果标记为删除,说明事务已经提交,还没来得及被purge,这时加S锁等待;

如果没有标记删除,则返回duplicate key错误;

2.2.2如果有锁,说明该记录正在被其他事务处理(新增、删除或更新),且事务还未提交,加S锁等待;

3、插入记录;//并对记录加X记录锁;

回滚

MySQL给SQL加锁时,可以通过innodb_lock_wait_timeout参数设置超时时间,如果加锁等待超过这个时间,就会回滚,回滚的方式有两种:

innodb_rollback_on_timeout等于OFF,commit时,只会回滚当前加锁的这条语句,此时事务并不会结束,业务层执行rollback时整个事务会回滚。InnoDB在执行每条SQL语句之前,都会创建一个保存点(即undo log),为off时事务会回滚到上一个保存点。系统默认为OFF。

innodb_rollback_on_timeout等于ON,不管提交还是回滚都是整体事物回滚。

回滚基于undo log实现,redo实现前滚,undo实现回滚。

死锁

InnoDB有死锁检测机制,基于检查有向图是否成环实现。如果两个事务死锁,一个会自动放弃从而解锁,另一个会执行成功。

通过show engine innodb status;可以查看innodb锁的情况。

CAS

如下例:

UPDATE wiki_pages SET content = ’ new content ’ WHERE id = 1234 AND content = ’ old content ’;

如果WHERE语句是运行在数据库的某个旧的快照上,即使另一个并发写入正在运行,条件可能仍然为真,最终可能无法防止更新丢失问题。

对于支持多副本的数据库,如果多节点并发改(多主的场景),会保留多个冲突版本,之后由应用层逻辑或依靠特定的数据结构来解决、合并多版本。

表设计模式

表空间

系统表空间

存放change buffer的区域,默认文件名为ibdata1.

独立表空间

当innodb_file_per_table选项开启(默认)时,表将被创建于表空间中,否则将被创建于系统表空间中。每个表文件表空间由一个.ibd数据文件代表,该文件默认被创建于数据库目录中。表空间的表文件支持动态(dynamic)和压缩 (commpressed)行格式。

通用表空间

使用CREATE TABLESPACE语法创建的共享InnoDB表空间,和系统表空间类似,也是共享的表空间,一个文件能够存储多个表数据。

Undo log

撤销日志又叫回滚表空间。

临时表空间

分为两种,session temporary tablespaces 和global temporary tablespace。

session temporary tablespaces存储的是用户创建的临时表和内部的临时表,一个session最多有两个表空间(用户临时表和内部临时表)。

global temporary tablespace储存用户临时表的回滚段(rollback segments )。

数据字典

InnoDB数据字典由内部系统表组成,这些表包含用于查找表、索引和表字段等对象的元数据。元数据物理上位于InnoDB系统表空间中。

使用hexdump可以查看frm文件结构,如表里有几个索引,全部索引包含哪些字段等。frm文件可用来恢复表结构。

表结构

表空间由段区页组成,对应ibd文件。

表空间由各个段组成:

数据段 b+树子节点

索引段 b+树非子节点

回滚段 undo log等。

一个段包含256个区(256M大小)。

一个段最多可以申请4个区,一个区由64个连续的页组成,每个页大小为16KB,即每个区的大小为64 * 16 = 1M。

页是InnoDB磁盘管理的最小单位,也就是说数据是按行进行存放的。每个页最多存放16KB / 2 ~ 200  = 7992行的记录。

对于表空间而言,第一组数据区的第一个数据区的前3个数据页,都是固定的,里面存放了一些描述性的数据。如FSP_HDR这个数据页存放了表空间和这一组数据区的一些属性。IBUF_BITMAP数据页存放的就是insert buffer的信息,INODE数据页存放的也是特殊信息。

存储形式

Compact、redundant;compressed、dynamic

行格式(Compact格式)

105b66118e21a56f1114472163ae7fdb.png

第一部分记录变长字段的长度,且为逆序排列;

第二部分为null标识位,如果某列有null值,在该列对应的bit位置1。二进制位同样按照列的顺序逆序排列。

第三部分是记录头信息,固定占用5个字节,主要就是包含比如该行是否已被删除、页中下一条记录的相对位置等。

MySQL的单行最大能存储65535byte的数据,但是当varchar的长度超过65532byte时,会发生错误,因为数据页中每一行中都有几个隐藏列,所以将varchar的长度降低到65532byte即可成功创建表。Compact格式的实现思路是,当列的类型为VARCHAR、VARBINARY、BLOB、TEXT时,该列超过768byte的数据放到其他数据页(Uncompress BLOB页)中去,然后通过一个偏移量将两者关联起来,这就是行溢出机制。所以innodb在页内部通过链表结构串连各个行记录。

CHAR/VARCHAR类型,在compact格式下null值不占用任何空间。

聚集索引的存储并不一定是物理上连续的,而是逻辑上连续的。一、页通过双向链表链接,页按照主键的顺序排序;二、每个页中的记录也是通过双向链表进行维护的,物理存储上可以同样不按照主键存储。

表设计相关原则

三大范式

范式优点:更新操作比反范式化要快;没有重复数据;表通常更小;

反范式优点:通过适量冗余数据避免关联;

建表规范:库名、表名、字段名,使用小写和下划线_分割;

索引规范:要求有自增ID作为主键,不要使用UUID、order_id等作为主键;

虚拟表

MySQL中有三种虚拟表:临时表、内存表、视图。

每个连接都会有临时表,临时表仅对当前连接可见,在连接关闭时,临时表会被删除幵释放所有表空间;如果超出了临时表的容量(MAX_HEAP_TABLE_SIZE),内存临时表会转换成磁盘临时表;

内存表

是指使用Memory引擎的表,这种表的结构在磁盘里,数据都保存在内存里,系统重启时候数据会被清空,但表结构还在,用于存放中小型数据。内存表也可以看作是临时表的一种。

语法:需要将存储引擎设置为:ENGINE =MEMORY。

物化视图

预先计算并存储在磁盘上的表,提高查询性能。预计算和缓存是提高性能的两种方法。

flexviews可以提取原表的修改,增量重新计算物化视图内容。

分库分表

一般业务数据量大时利用中间件如sharding-jdbc写入数据,做负载均衡。

在写入数据的时候,需要做两次路由,先对订单id(需要唯一)hash后对数据库的数量取模,可以路由到一台数据库上,然后再对那台数据库上的表数量取模,就可以路由到数据库上的一个表里了。

d63f29053e4645ad64160c3eef0dfe0b.png

读多写少使用读写分离:

28f7b552cf746eb5c32dc1eba03fb6b6.png

对于访问频率低的大字段,可以从原表拆分出来,减少IO。

排序实现方式

Order by、group by、distinct均有排序,即默认有Using filesort,显式指定order by null可以消除排序;利用sort_buffer排序可以不使用临时表,如果sort_buffer不够大,可以调大sort_buffer_size参数;使用SQL_SMALL_RESULT和SQL_BIG_RESULT标签可以强制告诉优化器如何使用临时表及排序。

如果Order by/group by/distinct是按索引归类,且select字段不需要回表,那么直接走索引扫描。

临时表使用的典型场景是union和group by。为了消除临时表,我们需要对group by列添加索引,或者对于大结果集,使用SQL_BIG_RESULT等。

常规排序
(1).从表t1中获取满足WHERE条件的记录;
(2).对于每条记录,将记录的主键+排序键(id,col2)取出放入sort buffer;
(3).如果sort buffer可以存放所有满足条件的(id,col2)对,则进行排序;否则sort buffer满后,进行排序(采用快排)并固化到临时文件中。
(4).若排序中产生了临时文件,需要利用归并排序算法,保证临时文件中记录是有序的;
(5).循环执行上述过程,直到所有满足条件的记录全部参与排序;
(6).扫描排好序的(id,col2)对,并利用id去捞取SELECT需要返回的字段(col1,col2,col3);
(7).将获取的结果集返回给用户。
      从上述流程来看,是否使用文件排序主要看sort_buffer是否能容下需要排序的(id,col2)对,sort_buffer的大小由sort_buffer_size参数控制。此外一次排序需要两次IO,一次是捞(id,col2),第二次是捞(col1,col2,col3)。由于返回的结果集是按col2排序,因此id是乱序的,通过乱序的id去捞(col1,col2,col3)时会产生大量的随机IO。对于第二次IO,MySQL有一个优化,即在捞之前首先将id排序,并放入缓冲区,这个缓存区大小由参数read_rnd_buffer_size控制,然后有序去捞记录,将随机IO转为顺序IO,即下面的排序优化。

优化排序
常规排序方式除了排序本身,还需要额外两次IO。优化的排序方式相对于常规排序,减少了第二次IO耗时。主要区别在于,放入sort buffer不是(id,col2),而是(col1,col2,col3)。MySQL提供了参数max_length_for_sort_data,只有当返回字段的最大长度小于max_length_for_sort_data时,才能利用优化排序方式,否则只能用常规排序方式。

优先队列排序
     为了得到最终的排序结果,无论怎样,都需要将所有满足条件的记录进行排序才能返回。那么相对于优化排序方式,是否还有优化空间呢?5.6版本针对Order by limit M,N语句,在空间层面做了优化,加入了一种新的排序方式--优先队列,这种方式采用堆排序实现。堆排序算法特征正好可以解limit M,N 这类排序的问题,虽然仍然需要所有元素参与排序,但是只需要M+N个元组的sort buffer空间即可,对于M,N很小的场景,基本不会因为sort buffer不够而导致需要临时文件进行归并排序的问题。对于升序,采用大顶堆,最终堆中的元素组成了最小的N个元素,对于降序,采用小顶堆,最终堆中的元素组成了最大的N的元素。

堆排序是非稳定的(对于相同的key值,无法保证排序后与排序前的位置一致)所以导致分页重复的现象。为了避免这个问题,我们可以在排序中加上唯一值,比如主键id,这样由于id是唯一的,确保参与排序的key值不相同。如可将SQL写成如下:

select * from t order by c,id asc limit 0,3;

Union

1、union: 对两个结果集进行并集操作, 不包括重复行,相当于distinct, 同时进行默认规则的排序;

2、union all: 对两个结果集进行并集操作, 包括重复行, 即所有的结果全部显示, 不管是不是重复;

union会自动将完全重复的数据去除掉,也就是两行完全相同会去掉;union all会保留那些完全重复的数据。union all只是合并查询结果,并不会进行去重和排序操作,在没有去重的前提下,使用union all的执行效率要比union高。

通过union连接的SQL它们分别单独取出的列数必须相同;

被union 连接的sql 子句,单个子句中不用写order by,因为不会有排序的效果。但可以对最终的结果集进行排序,即:

(select id,name from A order by id) union all (select id,name from B order by id); //没有排序效果;

(select id,name from A ) union all (select id,name from B ) order by id; //有排序效果;

DECLARE EXIT HANDLER FOR SQLEXCEPTION

使用该语句声明存储过程。可以简单使用rollback,也可以自定义动作,如TRUNCATE TABLE等,用于异常时清理。

自动回滚不会抛出异常,在分析问题时比较麻烦,比较好的实现方式是存储过程仅仅执行dml动作,在应用层做事务控制及捕获异常,从而知道具体的错误。

性能调优

如果偶尔慢,可能是出现了下面两种情况:

频繁刷redo log脏页;

等锁,可以用 show processlist命令来查看当前的状态;

如果一直慢,可能是下面情况:

没有走索引,字段没有索引;字段有索引,但却没有用索引;

数据库可能自己选错索引,查看执行计划;

索引优化

索引列不能是表达式的一部分,不能有类型强转;选择合适的索引列顺序;

利用多核特性并行查询。

子查询优化

子查询可以分为关联子查询和非关联子查询;

通过物化表,子查询转换成关联查询;

临时表不会过大的话,会存放在内存中,即Memory表;如果临时表过大将被放到磁盘中(超过 tmp_table_size 或 max_heap_table_size),索引结构也转换为B+树,通过B+树也可快速找到记录。

使用join代替in+自查询,因为in可能会导致优化器预算不准,走全表查询。

Group by与distinct区别:

在语义相同,有索引的情况下:group by和distinct都能使用索引,效率相同,DISTINCT 是在 GROUP BY 之后的每组中只取出一条记录。

在语义相同,无索引的情况下:效率相同,旧版本的mysql中group by会进行排序,可能触发filesort,导致sql执行效率低下。

group by的原理是先对结果进行分组排序,然后返回每组中的第一条数据。如果要显示排序,还是使用order by。

1、利用索引优化数据库查询的时候,范围查询应该使用闭包条件,或者关键字BETWEEN。

2、使用like 模糊查询不影响后面字段用到联合索引。

order by后面的条件,也要遵循联合索引的最左匹配原则,且要加limit/where限制,才会走索引:

select * from user order by code,age,name limit 100;

关联查询优化

确保on和union子句中的列上有索引;

确保group by和order by的表达式只涉及一个表的列;

磁盘优化

固态存储;RAID优化,多卷磁盘;

使用SAN和NAS。

深分页处理

场景:翻页过多时,查询性能下降。

解决方案:

1、使用子查询;

子查询利用二级索引先把符合条件的id查找出来,然后回表查询数据,深分页场景下极大减少了回表次数。

2、使用内关联查询;

3、使用游标:

游标查询条件值需要选取唯一id,不唯一的话会有重复数据。

大范围查找不走索引的情况

某个sql查询可以使用PRIMARY、OrderID、OrdersOrder_ Details三个索引,但是在实际的索引使用中,优化器选择了 PRIMARY聚集索引,也就是表扫描(tablescan),而非OrderID辅助索引扫描(indexscan)。

原因在于用户要选取的数据是整行信息,而OrderID索引不能覆盖到业务要查询的信息,因此在对OrderID索引查询到指定数据后,还需要一次回表访问来查找整行数据的信息。虽然OrderID索引中数据是顺序存放的,但是再一次进行回表查找的数据则是无序的,因此变为了磁盘上的离散读操作。如果要求访问的数据量很小,则优化器还是会选择辅助索引,但是当访问的数据占整个表中数据的相当一部分时(一般是20%左右),优化器会选择通过聚集索引来查找数据,因为顺序读要远远快于离散读。

因此对于不能进行索引覆盖的情况,优化器选择辅助索引的情况是,通过辅助索引查找的数据是少量的,即利用顺序读来替换随机读的查找。若用户使用的磁盘是固态硬盘,随机读操作非常快,同时有足够的自信来确认使用辅助索引可以带来更好的性能,那么可以使用关键字FORCE INDEX来强制使用某个索引,如:

SELECT * FROM orderdetails FORCE INDEX(OrderID) WHERE orderid>10000 and orderid<102000;

Join

对被驱动表的关联字段建立索引,所以每次搜索只需要在辅助索引树上扫描一行就行了,性能比较高。

原则是小表驱动大表,准确的说是小结果集驱动大结果集。

join为非ref类型时,会用到join buffer。可以用join_buffer_size调节buffer大小。

optimize table

有时尽管一张表删除了许多数据,但是这张表表的数据文件和索引文件却没有变小。这是因为mysql在删除数据(特别是有Text和BLOB)的时候,会留下许多数据空洞,这些空洞会占据原来数据的空间,所以文件的大小没有改变。可以使用OPTIMIZE TABLE来重新利用未使用的空间,并整理数据文件的碎片。

对于InnoDB表,OPTIMIZE TABLE被映射到ALTER TABLE上,重建表,过程如下:

扫描原表主键索引的所有记录;

生成新的b+树记录到临时文件;

生成临时文件的过程中,新的变更记录到一个中转日志row log中;

在临时文件生成后,将期间row log的变更应用到新的临时文件中;

然后替换临时文件为当前文件。

Explain执行示意

explain的执行结果:

3c9a7434a1d5159c3204ade09e61e3af.png

type列表示连接类型:

system >const > eq_ref > ref > range > index > ALL

key列和key_len列表示使用到的索引。

rows列表示估算的需要读取的行数。

extra表示附加信息,如Using temporary、Using filesort、Using index等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1059707.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

關聯式資料庫模型The relational data model

RELATIONAL MODEL關係模型 結構化查詢語言&#xff08;SQL&#xff09;基礎 foundation of structured query language (SQL) 許多資料庫設計方法的基礎foundation of many database design methodologies 資料庫研究基礎 foundation of database research 關係(Relation) …

Curve 文件存储的缓存策略

Curve 文件存储简介 Curve 文件存储的架构如下&#xff1a; 客户端 Posix 兼容&#xff1a;像本地文件系统一样使用&#xff0c;业务无缝接入&#xff0c;无侵入性&#xff1b; 独立的元数据集群&#xff1a;元数据分布式设计&#xff0c;可以无限扩展。同一文件系统可以在数…

Elasticsearch:什么时候应该考虑在 Elasticsearch 中添加协调节点?

仅协调节点&#xff08;coordinating only nodes&#xff09;充当智能负载均衡器。 仅协调节点的这种特殊角色通过减轻数据和主节点的协调责任&#xff0c;为广泛的集群提供了优势。 加入集群后&#xff0c;这些节点与任何其他节点类似&#xff0c;都会获取完整的集群状态&…

基于Java的医院挂号就诊系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

yolov5检测cs2中的目标

环境介绍 系统&#xff1a;Windows11 显卡&#xff1a;4070ti cuda:11.8 配置环境 python环境 安装python的虚拟环境anaconda。Free Download | Anaconda 成功安装后可以按Win键搜索anaconda&#xff0c;可以看到桌面版和命令行版本&#xff0c;我们这里直接用命令行版本…

VUE3照本宣科——响应式与生命周期钩子

VUE3照本宣科——响应式与生命周期钩子 前言一、响应式1.ref()2.reactive()3.computed()4.watch()5.代码演示 二、defineProps() 和 defineEmits()三、生命周期钩子1.onMounted()2.onUpdated()3.onUnmounted()4.onBeforeMount()5.onBeforeUpdate()6.onBeforeUnmount()7.onError…

公众号突破2个限制技巧

许多用户在注册公众号时可能会遇到“公众号显示主体已达上限”的问题。这是因为在2018年11月16日对公众号注册数量进行了调整&#xff0c;具体调整如下&#xff1a;1、个人主体注册公众号数量上限从2个调整为1个。2、企业主体注册公众号数量上限从5个调整为2个。这意味着&#…

vs2015 执行后出来空白界面的解决

为什么在visual studio上写的代码点击开始执行&#xff0c;出来的是空白界面&#xff1f;(代码没问题)? - 知乎 Visual Studio 2015 - 新建 C/C 项目 (Project)_vs2015创建一个c项目-CSDN博客

微信小程序点单左右联动的效果实现

微信小程序点单左右联动的效果实现 原理解析&#xff1a;   点击左边标签会跳到右边相应位置&#xff1a;点击改变rightCur值&#xff0c;转跳相应位置滑动右边&#xff0c;左边标签会跳到相应的位置&#xff1a;监听并且设置每个右边元素的top和bottom&#xff0c;再判断当…

Linux内存管理 | 一、内存管理的由来及思想

我的圈子&#xff1a; 高级工程师聚集地 我是董哥&#xff0c;高级嵌入式软件开发工程师&#xff0c;从事嵌入式Linux驱动开发和系统开发&#xff0c;曾就职于世界500强企业&#xff01; 创作理念&#xff1a;专注分享高质量嵌入式文章&#xff0c;让大家读有所得&#xff01; …

Linux系统常用指令篇---(一)

Linux系统常用指令篇—(一) 1.cd指令 Linux系统中&#xff0c;磁盘上的文件和目录被组成一棵目录树&#xff0c;每个节点都是目录或文件。 语法:cd 目录名 功能&#xff1a;改变工作目录。将当前工作目录改变到指定的目录下。 (简单理解为进入指定目录下) 举例: cd .. : 返…

maven 初学

1. maven 安装 配置安装 路径 maven 下载位置: D:\software\apache-maven-3.8.6 默认仓库位置: C:\Users\star-dream\.m2\repository 【已更改】 本地仓库设置为&#xff1a;D:\software\apache-maven-3.8.6\.m2\repository 镜像已更改为阿里云中央镜像仓库 <mirrors>…

文件编码格式

一、问题场景 笔者在写controller层出现了一些小问题&#xff1a;测试controller层的一些请求的时候&#xff0c;后端控制台打印的是乱码&#xff0c;网上找了很多说改UTF-8的&#xff0c;但是我去设置里面全部都改为UTF-8了&#xff0c;结果仍然无济于事&#xff0c;甚至还把…

flink自定义窗口分配器

背景 我们知道处理常用的滑动窗口分配器&#xff0c;滚动窗口分配器&#xff0c;全局窗口分配器&#xff0c;会话窗口分配器外&#xff0c;我们可以实现自己的自定义窗口分配器&#xff0c;以实现我们的自己的窗口逻辑 自定义窗口分配器的实现 package wikiedits.assigner;i…

camtasia 2023怎么导出mp4

MP4是常见的视频格式之一&#xff0c;那么使用电脑录屏软件Camtasia完成对视频的剪辑后&#xff0c;如何将其导出为MP4格式保存在我们的电脑中呢&#xff1f; 1.剪辑好视频后&#xff0c;我们找到软件界面右上角的“导出”按钮。 Camtasia Studio- 2023 win-安装包&#xff1a…

【数据结构】布隆过滤器

布隆过滤器的提出 在注册账号设置昵称的时候&#xff0c;为了保证每个用户昵称的唯一性&#xff0c;系统必须检测你输入的昵称是否被使用过&#xff0c;这本质就是一个key的模型&#xff0c;我们只需要判断这个昵称被用过&#xff0c;还是没被用过。 方法一&#xff1a;用红黑…

C/C++学习 -- 分组加密算法(DES算法)

数据加密标准&#xff08;Data Encryption Standard&#xff0c;DES&#xff09;是一种对称密钥加密算法&#xff0c;是信息安全领域的经典之作。本文将深入探讨DES算法的概述、特点、原理&#xff0c;以及提供C语言和C语言实现DES算法的代码案例。 一、DES算法概述 DES算法是…

【网络安全---XSS漏洞(1)】XSS漏洞原理,产生原因,以及XSS漏洞的分类。附带案例和payload让你快速学习XSS漏洞

一&#xff0c;什么是XSS漏洞&#xff1f; XSS全称&#xff08;Cross Site Scripting&#xff09;跨站脚本攻击&#xff0c;为了避免和CSS层叠样式表名称冲突&#xff0c;所以改为了XSS&#xff0c;是最常见的Web应用程序安全漏洞之一&#xff0c;位于OWASP top 10 2013/2017年…

idea配置文件属性提示消息解决方案

在项目文件路径下找到你没有属性提示消息的文件 选中&#xff0c;ok即可 如果遇到ok无法确认的情况&#xff1a; 在下图所示位置填写配置文件名称即可

lv7 嵌入式开发-网络编程开发 06 socket套接字及TCP的实现框架

目录 1 socket套接字 1.1 体系结构的两种形式 1.2 几种常见的网络编程接口 1.3 socket套接字 2 socket常用API介绍 2.1 API 2.2 地址族结构体 2.3 套接字类型 2.4 socket套接字 3 TCP通信的实现过程 4 练习 1 socket套接字 1.1 体系结构的两种形式 网络的体系结构 …