Py学习  »  DATABASE

MySQL灵魂十连问

ImportNew • 2 年前 • 291 次点击  

1. SQL 语句执行流程


MySQL 大体上可分为 Server 层和存储引擎层两部分。


Server 层


  • 连接器:TCP 握手后服务器来验证登陆用户身份,A用户创建连接后,管理员对A用户权限修改了也不会影响到已经创建的链接权限,必须重新登陆;
  • 查询缓存:查询后的结果存储位置,MySQL 8.0 版本以后已经取消,因为查询缓存失效太频繁,得不偿失;
  • 分析器:根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法;
  • 优化器:多种执行策略可实现目标,系统自动选择最优进行执行;
  • 执行器:判断是否有权限,将最终任务提交到存储引擎。

存储引擎层


负责数据的存储和提取。其架构模式是插件式的,支持InnoDB、MyISAM、Memory等多个存储引擎。


现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎(经常用的也是这个)。



SQL执行顺序:



2. BinLog、RedoLog、UndoLog


2.1 BinLog


BinLog 是记录所有数据库表结构变更(例如 create、alter table)以及表数据修改(insert、update、delete)的二进制日志,主从数据库同步用到的都是 BinLog 文件。


BinLog 日志文件有三种模式:


STATEMENT 模式


  • 内容:binlog 只会记录可能引起数据变更的 SQL 语句;

  • 优势:该模式下因为没有记录实际的数据,所以日志量和 IO 都消耗很低,性能是最优的;

  • 劣势:但有些操作并不是确定的,比如 uuid() 函数会随机产生唯一标识,当依赖 binlog 回放时,该操作生成的数据与原数据必然是不同的,此时可能造成无法预料的后果。


ROW 模式


  • 内容:在该模式下,binlog 会记录每次操作的源数据与修改后的目标数据,StreamSets 就要求该模式;

  • 优势:可以绝对精准的还原,从而保证了数据的安全与可靠,并且复制和数据恢复过程可以是并发进行的;

  • 劣势:缺点在于 binlog 体积会非常大。同时,对于修改记录多、字段长度大的操作来说,记录时性能消耗会很严重。阅读的时候也需要特殊指令来进行读取数据。


MIXED 模式


  • 内容:是对上述 STATEMENT 跟 ROW  两种模式的混合使用;

  • 细节:对于绝大部分操作,都使用 STATEMENT 来进行 binlog 的记录,只有以下操作使用 ROW 来实现:表的存储引擎为 NDB,使用了uuid() 等不确定函数,使用了 insert delay 语句,使用了临时表。




主从同步流程:


  1. 主节点必须启用二进制日志,记录任何修改了数据库数据的事件;

  2. 从节点开启一个线程(I/O Thread)把自己扮演成 MySQL 的客户端,通过 MySQL 协议,请求主节点的二进制日志文件中的事件 ;

  3. 主节点启动一个线程(dump Thread),检查自己二进制日志中的事件,跟对方请求的位置对比。如果不带请求位置参数,则主节点就会从第一个日志文件中的第一个事件一个一个发送给从节点;

  4. 从节点接收到主节点发送过来的数据把它放置到中继日志(Relay log)文件中。并记录该次请求到主节点的具体哪一个二进制日志文件内部的哪一个位置(主节点中的二进制文件会有多个);

  5. 从节点启动另外一个线程(SQL Thread ),把 Relay log 中的事件读取出来,并在本地再执行一次。


MySQL 默认的复制方式是异步的,并且复制的时候是有并行复制能力的。主库把日志发送给从库后不管了。


这样会产生一个问题:假设主库挂了,从库处理失败了,这时候从库升为主库后,日志就丢失了。


由此产生两个概念:


全同步复制


主库写入 binlog 后强制同步日志到从库。所有的从库都执行完成后才返回给客户端,但是很显然这个方式的话性能会受到严重影响。


半同步复制


半同步复制的逻辑是这样,从库写入日志成功后返回 ACK 确认给主库,主库收到至少一个从库的确认就认为写操作完成。


还可以延伸到由于主从配置不一样、主库大事务、从库压力过大、网络震荡等造成主备延迟。


如何避免这个问题?


  • 主备切换的时候用可靠性优先原则还是可用性优先原则?

  • 如何判断主库Crash了?

  • 互为主备情况下如何避免主备循环复制?

  • 被删库跑路了如何正确恢复?


感觉越来越扯到 DBA 的活儿上去了。



2.2 RedoLog


可以先通过下面 Demo 理解:


饭点记账可以把账单写在账本上也可以写在粉板上。有人赊账或者还账的话,一般有两种做法:


  1. 直接把账本翻出来,把这次赊的账加上去或者扣除掉;

  2. 先在粉板上记下这次的账,等打烊以后再把账本翻出来核算。


生意忙时选后者,因为前者太麻烦了。得在密密麻麻的记录中找到这个人的赊账总额信息,找到之后再拿出算盘计算,最后再将结果写回到账本上。


同样,在 MySQL 中如果每一次的更新操作都需要写进磁盘,然后磁盘也要找到对应的那条记录然后再更新。整个过程 IO 成本、查找成本都很高。


而粉板和账本配合的整个过程就是 MySQL 用到的是 Write-Ahead Logging 技术,它的关键点就是先写日志,再写磁盘。


此时账本 = BinLog,粉板 = RedoLog。


  • 记录更新时,InnoDB 引擎就会先把记录写到 RedoLog(粉板)里面,并更新内存。同时,InnoDB 引擎会在空闲时将这个操作记录更新到磁盘里面。

  •  如果更新太多 RedoLog 处理不了的时候,需先将 RedoLog 部分数据写到磁盘,然后擦除 RedoLog 部分数据。RedoLog 类似转盘。


RedoLog 有 write pos 跟 checkpoint


  • write pos :是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头;

  • check point:是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。


write pos 和 check point 之间的是粉板上还空着的部分,可以用来记录新的操作。


如果 write pos 追上checkpoint 表示粉板满了,这时候不能再执行新的更新。得停下来先擦掉一些记录,把 checkpoint 推进一下。


有了 RedoLog,InnoDB 就可以保证即使数据库发生异常重启,之前提交的记录都不会丢失,这个能力称为 crash-safe。



RedoLog 两阶段提交:为了让binlog跟redolog两份日志之间的逻辑一致。提交流程大致为:1 prepare阶段 ->  2 写binlog  -> 3 commit


  • 当在 2 之前崩溃时,重启恢复后发现没有 commit,回滚。备份恢复:没有 binlog,一致;
  • 当在 3 之前崩溃时,重启恢复发现虽没有 commit,但满足 prepare 和 binlog 完整,所以重启后会自动 commit。备份:有 binlog 一致。


BinLog RedoLog 区别:


  • RedoLog  是 InnoDB 引擎特有的。BinLog 是 MySQL 的 Server 层实现的,所有引擎都可以使用;
  • RedoLog  是物理日志,记录的是在某个数据页上做了什么修改。BinLog 是逻辑日志,记录的是这个语句的原始逻辑。比如给 ID=2 这一行的c字段加 1;
  • RedoLog  是循环写的,空间固定会用完。BinLog 是可以追加写入的。追加写是指 BinLog 文件写到一定大小后会切换到下一个,并不会覆盖以前的日志。

2.3 UndoLog


UndoLog 一般是逻辑日志,主要分为两种:


insert undo log


代表事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃。


update undo log

事务在进行 update 或 delete 时产生的 undo log。不仅在事务回滚时需要,在快照读时也需要。


所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除。


3. MySQL中的索引


索引的常见模型有哈希表、有序数组和搜索树。


  • 哈希表:一种以 KV 存储数据的结构,只适合等值查询,不适合范围查询;

  • 有序数组:只适用于静态存储引擎,涉及到插入的时候比较麻烦。可以参考 Java 中的 ArrayList;

  • 搜索树:按照数据结构中的二叉树来存储数据,不过此时是 N 叉树(B+ 树)。广泛应用在存储引擎层中。




B+ 树比 B 树优势在于:


  • B+ 树非叶子节点存储的只是索引,可以存储的更多。B+ 树比B树更加矮胖,IO 次数更少;
  • B+ 树叶子节点前后管理,更加方便范围查询。同时结果都在叶子节点,查询效率稳定;
  • B+ 树中更有利于对数据扫描,可以避免 B 树的回溯扫描。


索引的优点:


  • 唯一索引可以保证每一行数据的唯一性;

  • 提高查询速度;

  • 加速表与表的连接;

  • 显著的减少查询中分组和排序的时间;

  • 通过使用索引,可以在查询的过程中,使用优化隐藏器,提高系统的性能。


索引的缺点:


  • 创建跟维护都需要耗时;

  • 创建索引时,需要对表加锁,在锁表的同时,可能会影响到其他的数据操作; 

  • 索引需要磁盘的空间进行存储,磁盘占用也很快;

  • 当对表中的数据进行 CRUD 的时,也会触发索引的维护,而维护索引需要时间,可能会降低数据操作性能。


索引设计的原则不应该


  • 索引不是越多越好。索引太多,维护索引需要时间跟空间;

  • 频繁更新的数据,不宜建索引;

  • 数据量小的表没必要建立索引。


应该:


  1. 重复率小的列建议生成索引。因为重复数据少,索引树查询更有效率,等价基数越大越好;

  2. 数据具有唯一性,建议生成唯一性索引。在数据库的层面,保证数据正确性;

  3. 频繁group by、order by的列建议生成索引。可以大幅提高分组和排序效率;

  4. 经常用于查询条件的字段建议生成索引。通过索引查询,速度更快。


索引失效的场景


  • 模糊搜索:左模糊或全模糊都会导致索引失效,比如 '%a' 和 '%a%'。但是右模糊是可以利用索引的,比如 'a%' ;

  • 隐式类型转换:比如 select * from t where name = xxx , name 是字符串类型,但是没有加引号,所以是由 MySQL 隐式转换的,所以会让索引失效 3。当语句中带有 or 的时候:比如 select * from t where name=‘sw’ or age=14;

  • 不符合联合索引的最左前缀匹配:(A,B,C) 的联合索引,where 条件只处理了 C 或 B 或只有 B,C。


关于索引的知识点


  • 主键索引:主键索引的叶子节点存的是整行数据信息。在 InnoDB 里,主键索引也被称为聚簇索引(clustered index)。主键自增是无法保证完全自增的哦,遇到唯一键冲突、事务回滚等都可能导致不连续;

  • 唯一索引:以唯一列生成的索引,该列不允许有重复值,但允许有空值(NULL);

  • 普通索引跟唯一索引查询性能:InnoDB 的数据是按数据页为单位来读写的,默认每页 16KB,因此这两种索引查询数据性能差别微乎其微;

  • change buffer:普通索引用在更新过程的加速,更新的字段如果在缓存中,如果是普通索引则直接更新即可。如果是唯一索引需要将所有数据读入内存来确保不违背唯一性,所以尽量用普通索引;

  • 非主键索引:非主键索引的叶子节点内容是主键的值。在 InnoDB 里,非主键索引也被称为二级索引(secondary index);

  • 回表:先通过数据库索引扫描出数据所在的行,再通过行主键id取出索引中未提供的数据,即基于非主键索引的查询需要多扫描一棵索引树;

  • 覆盖索引:如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为覆盖索引;

  • 联合索引:相对单列索引,组合索引是用多个列组合构建的索引,一次性最多联合 16 个;

  • 最左前缀原则:对多个字段同时建立的组合索引(有顺序,ABC、ACB 是完全不同的两种联合索引)以联合索引(a,b,c)为例,建立这样的索引相当于建立了索引 a、ab、abc 三个索引。另外组合索引实际还是一个索引,并非真的创建了多个索引,只是产生的效果等价于产生多个索引;

  • 索引下推:MySQL 5.6 引入了索引下推优化。可以在索引遍历过程中,对索引中包含的字段先做判断,过滤掉不符合条件的记录,减少回表次数;

  • 索引维护:B+ 树为了维护索引有序性涉及到页分裂跟页合并。增删数据时需考虑页空间利用率。

  • 自增主键:一般会建立与业务无关的自增主键,不会触发叶子节点分裂;

  • 延迟关联:通过使用覆盖索引查询返回需要的主键,再根据主键关联原表获得需要的数据;

  • InnoDB 存储:* .frm 文件是一份定义文件,也就是定义数据库表是一张怎么样的表。*.ibd 文件则是该表的索引,数据存储文件,既该表的所有索引树,所有行记录数据都存储在该文件中;

  • MyISAM 存储 :* .frm文件是一份定义文件,也就是定义数据库表是一张怎么样的表。* .MYD 文件是 MyISAM 存储引擎表的所有行数据的文件。* .MYI 文件存放的是 MyISAM 存储引擎表的索引相关数据的文件。MyISAM 引擎下,表数据和表索引数据是分开存储的;

  • MyISAM 查询:在 MyISAM 下,主键索引和辅助键索引都属于非聚簇索引。查询不管是走主键索引,还是非主键索引,在叶子结点得到的都是目的数据的地址,还需要通过该地址,才能在数据文件中找到目的数据。PS:InnoDB 支持聚簇索引,MyISAM 不支持聚簇索引。


4. SQL 事务隔离级别


ACID 四个特性


  • 原子性(Atomicity):把多个操作放到一个事务中,保证这些操作要么都成功,要么都不成功;
  • 一致性(Consistency):理解成一串对数据进行操作的程序执行下来,不会对数据产生不好的影响,比如凭空产生或消失;
  • 隔离性(Isolation,又称独立性):隔离性的意思就是多个事务之间互相不干扰,即使是并发事务的情况下,他们只是两个并发执行没有交集,互不影响的东西;当然实现中,也不一定需要这么完整隔离性,即不一定需要这么的互不干扰,有时候还是允许有部分干扰的。所以 MySQL 可以支持 4 种事务隔离性;
  • 持久性(Durability):当某个操作操作完毕了,那么结果就是这样了,并且这个操作会持久化到日志记录中。


ACID 中 C 与 CAP 定理中 C 的区别


ACID 的 C 着重强调单数据库事务操作时,要保证数据的完整和正确性,数据不会凭空消失跟增加。CAP 理论中的 C 指的是对一个数据多个备份的读写一致性。


事务操作可能会出现的数据问题


  • 脏读(dirty read):B 事务更改数据还未提交,A 事务已经看到并且用了。B 事务如果回滚,则 A 事务做错了 ;

  • 不可重复读(non-repeatable read):不可重复读的重点是修改。同样的条件, 你读取过的数据,再次读取出来发现值不一样了。只需要锁住满足条件的记录 ;

  • 幻读(phantom read):事务 A 先修改了某个表的所有纪录的状态字段为已处理,未提交;事务 B 也在此时新增了一条未处理的记录,并提交了;事务 A 随后查询记录,却发现有一条记录是未处理的造成幻读现象。幻读仅专指新插入的行。幻读会造成语义上的问题跟数据一致性问题;


在可重复读 RR 隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的。因此,幻读在当前读下才会出现。要用间隙锁解决此问题。


在说隔离级别之前,你首先要知道,你隔离得越严实,效率就会越低。因此很多时候,我们都要在二者之间寻找一个平衡点。SQL标准的事务隔离级别由低到高如下:



上图从上到下的模式会导致系统的并行性能依次降低,安全性依次提高。


  • 读未提交:别人改数据的事务尚未提交,我在自己的事务中也能读到;

  • 读已提交(Oracle 默认):别人改数据的事务已经提交,我在我的事务中才能读到;

  • 可重复读(MySQL 默认):别人改数据的事务已经提交,我在我的事务中也不去读,以此保证重复读一致性;

  • 串行:我的事务尚未提交,别人就别想改数据。


标准跟实现


上面都是关于事务的标准,但是每一种数据库都有不同的实现,比如 MySQL InnDB 默认为 RR 级别,但是不会出现幻读。


因为当事务A更新了所有记录的某个字段,此时事务A会获得对这个表的表锁,因为事务 A 还没有提交,所以事务 A 获得的锁没有释放。此时事务 B 在该表插入新记录,会因为无法获得该表的锁,则导致插入操作被阻塞。只有事务 A 提交了事务后,释放了锁,事务 B 才能进行接下去的操作。


所以,可以说   MySQL 的 RR 级别的隔离是已经实现解决了脏读、不可重复读和幻读的。


5. MySQL 中的锁


无论是 Java 的并发编程还是数据库的并发操作都会涉及到锁,研发人员引入了悲观锁跟乐观锁这样一种锁的设计思想。


悲观锁


  • 优点:适合在写多读少的并发环境中使用。虽然无法维持非常高的性能,但是在乐观锁无法提更好的性能前提下,可以做到数据的安全性;

  • 缺点:加锁会增加系统开销,虽然能保证数据的安全,但数据处理吞吐量低,不适合在读书写少的场合下使用。


乐观锁


  • 优点:在读多写少的并发场景下,可以避免数据库加锁的开销,提高 DAO 层的响应性能,很多情况下 ORM 工具都有带有乐观锁的实现,所以这些方法不一定需要我们人为的去实现;

  • 缺点:在写多读少的并发场景下,即在写操作竞争激烈的情况下,会导致 CAS 多次重试,冲突频率过高,导致开销比悲观锁更高;

  • 实现:数据库层面的乐观锁其实跟 CAS 思想类似, 通数据版本号或者时间戳也可以实现。


数据库并发场景主要有三种


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

  • 读-写:有隔离性问题,可能遇到脏读、幻读、不可重复读;

  • 写-写:可能存更新丢失问题,比如第一类更新丢失,第二类更新丢失。


两类更新丢失问题


  • 第一类更新丢失:事务A的事务回滚覆盖了事务 B 已提交的结果;

  • 第二类更新丢失:事务A的提交覆盖了事务 B 已提交的结果。


为了合理贯彻落实锁的思想,MySQL 中引入了杂七杂八的各种锁:


锁分类


MySQL支持三种层级的锁定,分别为:


  • 表级锁定MySQL 中锁定粒度最大的一种锁,最常使用的 MYISAM 与 INNODB 都支持表级锁定;

  • 页级锁定:是 MySQ L中锁定粒度介于行级锁和表级锁中间的一种锁,表级锁速度快但冲突多,行级冲突少但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录;

  • 行级锁定MySQL 中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大行级锁不一定比表级锁要好:锁的粒度越细,代价越高,相比表级锁在表的头部直接加锁,行级锁还要扫描找到对应的行对其上锁,这样的代价其实是比较高的,所以表锁和行锁各有所长。

MyISAM 中的锁


虽然 MySQL 支持表、页、行三级锁定,但 MyISAM 存储引擎只支持表锁。所以MyISAM 的加锁相对比较开销低,但数据操作的并发性能相对就不高。但如果写操作都是尾插入,那还是可以支持一定程度的读写并发。


从 MyISAM 所支持的锁中也可以看出:MyISAM 是一个支持读读并发,但不支持通用读写并发,写写并发的数据库引擎,所以它更适合用于读多写少的应用场合,一般工程中也用的较少。


InnoDB 中的锁


该模式下支持的锁实在是太多了,具体如下:


  • 共享锁和排他锁 (Shared and Exclusive Locks)

  • 意向锁(Intention Locks) 

  • 记录锁(Record Locks) 

  • 间隙锁(Gap Locks) 

  • 临键锁 (Next-Key Locks) 

  • 插入意向锁(Insert Intention Locks) 

  • 主键自增锁 (AUTO-INC Locks) 

  • 空间索引断言锁(Predicate Locks for Spatial Indexes)


举个栗子,比如行锁里的共享锁跟排它锁 lock in share modle 共享读锁:


为了确保自己查到的数据没有被其他的事务正在修改,也就是说确保查到的数据是最新的数据,并且不允许其他人来修改数据。


但是自己不一定能够修改数据,因为有可能其他的事务也对这些数据使用了 in share mode 的方式上了 S 锁。如果不及时的 commit 或者 rollback 也可能会造成大量的事务等待。


for update 排它写锁


为了让自己查到的数据确保是最新数据,并且查到后的数据只允许自己来修改的时候,需要用到 for update。相当于一个 update 语句。


在业务繁忙的情况下,如果事务没有及时的 commit 或者 rollback 可能会造成其他事务长时间的等待,从而影响数据库的并发使用效率。


Gap Lock 间隙锁


行锁只能锁住行,如果在记录之间的间隙插入数据就无法解决了。因此 MySQL 引入了间隙锁(Gap Lock)。间隙锁是左右开区间。间隙锁之间不会冲突。


间隙锁和行锁合称 NextKeyLock,每个 NextKeyLock 是前开后闭区间。


间隙锁加锁原则


  1. 加锁的基本单位是 NextKeyLock,是前开后闭区间;

  2. 查找过程中访问到的对象才会加锁;

  3. 索引上的等值查询,给唯一索引加锁的时候,NextKeyLock 退化为行锁;

  4. 索引上的等值查询,向右遍历时且最后一个值不满足等值条件的时候,NextKeyLock 退化为间隙锁;

  5. 唯一索引上的范围查询会访问到不满足条件的第一个值为止。


6. MVCC


MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制。MVCC是一种并发控制的理念,维持一个数据的多个版本,使得读写操作没有冲突。


MVCC 在 MySQL InnoDB 中实现目的主要是为了提高数据库并发性能,用更好的方式去处理读写冲突。做到即使有读写冲突时,也能做到不加锁,非阻塞并发读。


MySQL InnoDB 下的当前读和快照读


当前读

像 select lock in share mode(共享锁)、select for update 、update、insert、delete(排他锁)这些操作都是一种当前读。


就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。


当前读可以认为是悲观锁的具体功能实现。


快照读

不加锁的 select 就是快照读,即不加锁的非阻塞读。


快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读。


之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即 MVCC。可以认为 MVCC 是行锁的一个变种但它在很多情况下,避免了加锁操作,降低了开销


既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本。


快照读就是 MVCC 思想在 MySQL 的具体非阻塞读功能实现。MVCC 的目的就是为了实现读写冲突不加锁,提高并发读写性能。而这个读指的就是快照读。


快照读就是 MySQL 为我们实现 MVCC 理想模型的其中一个具体非阻塞读功能。


因为大佬不满意只让数据库采用悲观锁这样性能不佳的形式去解决读写冲突问题,因此提出了 MVCC,所以我们可以形成两个组合:


  • MVCC + 悲观锁:MVCC解决读写冲突,悲观锁解决写写冲突;

  • MVCC + 乐观锁:MVCC解决读写冲突,乐观锁解决写写冲突。


MVCC 的实现原理


MVCC 实现原理主要是依赖记录中的 四个隐式字段、undo日志 、Consistent Read View 来实现的。


四个隐式字段


  1. DB_TRX_ID6 byte,最近修改(修改/插入)事务 ID。记录创建这条记录/最后一次修改该记录的事务ID;
  2. DB_ROLL_PTR7 byte,回滚指针。指向这条记录的上一个版本(存储于 rollback segment 里);
  3. DB_ROW_ID6 byte,隐含的自增ID(隐藏主键)。如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引;
  4. FLAG一个删除 flag 隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除 flag 变了。


事务对一条记录的修改,会导致该记录的 undo log 成为一条记录版本线性表(链表), undo log 的链首就是最新的旧记录,链尾就是最早的旧记录。


undo 日志


此知识点上文已经说过了,对 MVCC 有帮助的实质是 update undo log。undo log 实际上就是存在 rollback segment 中旧记录链。


一致读视图 Consistent Read View:Read View 是事务进行快照读操作的时候生产的读视图(Read View)。


在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID。


InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向InnoDB的事务系统申请的,是按申请顺序严格递增的。


拿着这个 ID 跟记录中 ID 对比进行选择性展示,这里说下大致的思路。


你可以简单的理解为 MVCC 为每一行增加了两个隐藏字段,两个字段分别保存了这个行的当前事务 ID 跟行的删除事务 ID。


insert
InnoDB 为新插入的每一行保存当前系统版本号作为版本号。

select


InnoDB 只会查找版本早于当前事务版本的数据行,也就是行的系统版本号 <= 事务的系统版本号。这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。


行的删除版本要么未定义,要么大于当前事务版本号,这可以确保事务读取到的行在事务开始之前未被删除。


只有同时满足上述条件的记录,才能返回作为查询结果。


delete

InnoDB 会为删除的每一行保存当前系统的版本号(事务的 ID)作为删除标识。


update

InnoDB 执行 update,实际上是新插入了一行记录,并保存其创建时间为当前事务的ID。同时保存当前事务 ID 到要 update 的行的删除时间。


上面只是一个浅显的讲解 MVCC 选择标准流程,源码层面应该是根据低水位跟高水位来截取的。具体实现可自行搜索。


重点


事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读的地方非常关键,它有决定该事务后续快照读结果的能力。


在 RC 隔离级别下,是每个快照读都会生成并获取最新的 Read View;而在 RR 隔离级别下,则是同一个事务中的第一个快照读才会创建 Read View,之后的快照读获取的都是同一个 Read View。


7. 缓冲池(buffer pool)



应用系统分层架构,为了加速数据访问会把最常访问的数据,放在缓存(cache)里,避免每次都去访问数据库。


操作系统会有缓冲池(buffer pool)机制,避免每次访问磁盘,以加速数据的访问。


MySQL 作为一个存储系统,同样具有缓冲池(buffer pool)机制,以避免每次查询数据都进行磁盘 IO。


主要作用


  • 存在的意义是加速查询;

  • 缓冲池(buffer pool)是一种常见的降低磁盘访问的机制;

  • 缓冲池通常以页(page 16K)为单位缓存数据;

  • 缓冲池的常见管理算法是 LRU,memcache、OS、InnoDB 都使用了这种算法;

  • InnoDB 对普通 LRU 进行了优化:将缓冲池分为老生代和新生代。入缓冲池的页优先进入老生代,该页被访问才进入新生代,以解决预读失效的问题页被访问。且在老生代停留时间超过配置阈值的,才进入新生代。以解决批量数据访问,大量热数据淘汰的问题。


预读失效


由于预读(Read-Ahead)提前把页放入了缓冲池,但最终 MySQL 并没有从页中读取数据,称为预读失效。



缓冲池污染


当某一个 SQL 语句,要批量扫描大量数据时可能导致把缓冲池的所有页都替换出去,进而导致大量热数据被换出,MySQL性能急剧下降,这种情况叫缓冲池污染。


解决办法:加入老生代停留时间窗口策略后,短时间内被大量加载的页,并不会立刻插入新生代头部,而是优先淘汰那些,短期内仅仅访问了一次的页。


8. table 瘦身


空洞


MySQL 执行 delete 命令其实只是把记录的位置或者数据页标记为了可复用,但磁盘文件的大小是不会变的。


通过 delete 命令是不能回收表空间的。这些可以复用而没有被使用的空间,看起来就像是空洞。


插入时候引发分裂同样会产生空洞。


重建表思路


  1. 新建一个跟 A 表结构相同的表B;

  2. 按照主键 ID 将 A 数据一行行读取同步到表 B;

  3. 用表 B 替换表 A 实现效果上的瘦身。


重建表指令


  • 慎用 alter table A engine=InnoDB ,牛逼的 DBA 都用下面的开源工具。

  • 推荐 GitHub:gh-ost。


9. SQL Joins、统计、 随机查询


7 种 join



统计


  1. MyISAM 模式下把一个表的总行数存在了磁盘上,直接拿来用即可;

  2. InnoDB 引擎由于 MVCC 的原因,需要把数据读出来然后累计求和 ;

  3. 性能由坏到好:count(字段) < count(主键id) < count(1) ≈ count(*)。尽量用count(*)即可。


随机查询


mysql> select word from words order by rand() limit 3;

直接使用 order by rand()。explain 这个语句发现需要 Using temporary 和 Using filesort,查询的执行代价往往是比较大的。


在设计的时要避开这种写法。


mysql> 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; select * from t limit @Y2,1;select * from t limit @Y3,1;

这样可以避免临时表跟排序的产生,最终查询行数 = C + (Y1+1) + (Y2+1) + (Y3+1)

exist 和 in 对比:


  • in 查询时首先查询子查询的表,然后将内表和外表做一个笛卡尔积,然后按照条件进行筛选;

  • 子查询使用 exists 会先进行主查询。将查询到的每行数据循环带入子查询校验是否存在,过滤出整体的返回数据;

  • 两表大小相当,in 和 exists 差别不大。内表大的时候,用 exists 效率较高;内表小的时候,用 in 效率较高;

  • 查询用 not in 那么内外表都进行全表扫描,没有用到索引;而 not exists 的子查询依然能用到表上的索引。not exists 比 not in 要快。


10. MySQL 优化


SQL 优化主要分 4 个方向:SQL 语句跟索引、表结构、系统配置、硬件。


总优化思路就是最大化利用索引、尽可能避免全表扫描、减少无效数据的查询


  • 减少数据访问:设置合理的字段类型,启用压缩,通过索引访问等减少磁盘 IO;

  • 返回更少的数据:只返回需要的字段和数据分页处理,减少磁盘 IO 及网络 IO;

  • 减少交互次数:批量 DML 操作,函数存储等减少数据连接次数;

  • 减少服务器 CPU 开销:尽量减少数据库排序操作以及全表查询,减少 CPU 内存占用 ;

  • 分表分区:使用表分区,可以增加并行操作,更大限度利用 CPU 资源。


SQL 语句优化举例:


  • 合理建立覆盖索引:可以有效减少回表;

  • union、or、in都能命中索引,建议使用 in;

  • 负向条件 !=、<>、not in、not exists、not like 等不会使用索引,建议用 in;

  • 在列上进行运算或使用函数会使索引失效,从导致行全表扫描 ;

  • 小心隐式类型转换,原字符串用整型会触发CAST函数导致索引失效。原int用字符串则会走索引;

  • 不建议使用 % 前缀模糊查询;

  • 多表关联查询时,小表在前大表在后。在 MySQL 中,执行 from 后的表关联查询是从左往右执行的(Oracle 相反)。第一张表会涉及到全表扫描;

  • 调整 where 字句中的连接顺序。MySQL 采用从左往右,自上而下的顺序解析where 子句。根据这个原理,应将过滤数据多的条件往前放,最快速度缩小结果集。


SQL 调优大致思路


  1. 先用慢查询日志定位具体需要优化的 SQL; 

  2. 使用 explain 执行计划查看索引使用情况;

  3. 重点关注:一般情况下根据这 4 列就能找到索引问题。key 查看有没有使用索引,key_len 查看索引使用是否充分,type 查看索引类型,Extra 查看附加信息。包括排序、临时表、where 条件为 false 等;

  4. 根据上一步找出的索引问题优化 SQL;

  5. 再回到第 2 步。



表结构优化


  1. 尽量使用 TINYINT、SMALLINT、MEDIUM_INT 作为整数类型而非 INT,如果非负则加上 UNSIGNED;

  2. VARCHAR 的长度只分配真正需要的空间;

  3. 尽量使用 TIMESTAMP 而非 DATETIME;

  4. 单表不要有太多字段,建议在 20 以内;

  5. 避免使用 NULL 字段,很难查询优化且占用额外索引空间。字符串默认为''。


读写分离


只在主服务器上写,只在从服务器上读。


对应到数据库集群一般都是一主一从、一主多从。业务服务器把需要写的操作都写到主数据库中,读的操作都去从库查询。主库会同步数据到从库保证数据的一致性。


一般读写分离的实现方式有两种,代码封装与数据库中间件。


分库分表


分库分表分为垂直和水平两个方式,一般是先垂直后水平。


  1. 垂直分库:将应用分为若干模块,比如订单模块、用户模块、商品模块、支付模块等等。其实就是微服务的理念;

  2. 垂直分表:一般将不常用字段跟数据较大的字段做拆分;

  3. 水平分表:根据场景选择什么字段作分表字段,比如淘宝日订单 1000 万,用 userId 作分表字段。数据查询支持到最近 6 个月的订单,超过 6 个月的做归档处理,那么 6 个月的数据量就是 18 亿,分 1024 张表,每个表存 200W 数据,hash(userId )% 100 找到对应表格;

  4. ID 生成器:分布式 ID 需要跨库全局唯一方便查询存储-检索数据,确保唯一性跟数字递增性。


目前主要流行的分库分表工具 就是 MyCat 和 sharding-sphere。


TiDB


开源分布式数据库,结合了传统的 RDBMS 和 NoSQL 的最佳特性。TiDB 兼容 MySQL,支持无限的水平扩展,具备强一致性和高可用性。


TiDB 的目标是为 OLTP(Online Transactional Processing) 和 OLAP(Online Analytical Processing)场景提供一站式的解决方案。


TiDB 具备如下核心特点


  1. 支持 MySQL 协议(开发接入成本低);

  2. 100% 支持事务(数据一致性实现简单、可靠);

  3. 无限水平拓展(不必考虑分库分表)不停服务;

  4. TiDB 支持和 MySQL 的互备;

  5. 遵循 JDBC 原则,学习成本低、强关系型、强一致性,不用担心主从配置,不用考虑分库分表,还可以无缝动态扩展。


适合


  1. 原业务的 MySQL 的业务遇到单机容量或者性能瓶颈时,可以考虑使用 TiDB 无缝替换 MySQL;

  2. 大数据量下,MySQL 复杂查询很慢;

  3. 大数据量下,数据增长很快,接近单机处理的极限,不想分库分表或者使用数据库中间件等对业务侵入性较大、对业务有约束的 Sharding 方案;

  4. 大数据量下,有高并发实时写入、实时查询、实时统计分析的需求。

  5. 有分布式事务、多数据中心的数据 100% 强一致性、auto-failover 的高可用的需求。


不适合


  1. 单机 MySQL 能满足的场景也用不到 TiDB;

  2. 数据条数少于 5000w 的场景下通常用不到 TiDB,TiDB 是为大规模的数据场景设计的;

  3. 如果你的应用数据量小(所有数据千万级别行以下),且没有高可用、强一致性或者多数据中心复制等要求,那么就不适合使用 TiDB。


- EOF -

推荐阅读  点击标题可跳转

1、35 张图带你 MySQL 调优

2、MySQL 中的反斜杠 \\,真是太坑了!!

3、MySQL性能篇:索引、锁、事务、分库分表如何撑起亿级数据


看完本文有收获?请转发分享给更多人

关注「ImportNew」,提升Java技能

点赞和在看就是最大的支持❤️



Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/117546
 
291 次点击