作为SQLBoy,基础部分不会有人不会吧?面试也不怎么问,基础掌握不错的小伙伴可以跳过这一部分。当然,可能会现场写一些SQL语句,SQ语句可以通过牛客、LeetCode、LintCode之类的网站来练习。
DDL全称是Data Definition Language,即数据定义语言;
DML全称是Data Manipulation Language,即数据操纵语言;
DQL全称是Data Query Language,即数据查询语言;
DCL全称是Data Control Language,即数据控制语言。
AXB={, , , , , }
。MySQL 的连接主要分为内连接和外连接,外连接常用的有左连接、右连接。
MySQL-joins-来源菜鸟教程
三大范式的作用是为了控制数据库的冗余,是对空间的节省,实际上,一般互联网公司的设计都是反范式的,通过冗余一些数据,避免跨表跨库,利用空间换时间,提高性能。
char:
varchar:
日常的设计,对于长度相对固定的字符串,可以使用char,对于长度不确定的,使用varchar更合适一些。
相同点:
区别:
1000-01-01 00:00:00.000000
到 9999-12-31 23:59:59.999999
;TIMESTAMP 的时间范围是 1970-01-01 00:00:01.000000
UTC 到2038-01-09 03:14:07.999999
UTCMySQL中的 in语句是把外表和内表作hash连接,而exists语句是对外表作loop循环,每次loop循环再对内表进行查询。我们可能认为 exists 比 in 语句的效率要高,这种说法其实是不准确的,要区分情景:
货币在数据库中MySQL常用Decimal
和Numric
类型表示,这两种类型被MySQL实现为同样的类型。他们被用于保存与货币有关的数据。
例如salary DECIMAL(9, 2),9(precision)代表将被用于存储值的总的小数位数,而2(scale)代表将被用于存储小数点后的位数。存储在salary列中的值的范围是从-9999999.99到9999999.99
DECIMAL和NUMERIC值作为字符串存储,而不是作为二进制浮点数,以便保存那些值的小数精度。
之所以不使用 float 或者 double 的原因:因为 float 和 double 是以二进制存储的,所以有一定的误差。
MySQL 可以直接使用字符串存储 emoji.
但是需要注意的,utf8编码是不行的,MySQL中的utf8是阉割版的utf8,它最多只用3个字节存储字符,所以存储不了表情。那该怎么办?需要使用utf8mb4编码。
alter table blogs modify content text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci not null;
三者都表示删除,但是三者有一些差别:
因此,在不再需要一张表的时候,用drop;在想删除部分数据行时候,用delete;在保留表而删除所有数据的时候用 truncate。
执行效果:
执行速度:
MySQL 逻辑架构图主要分三层:
语法分析
,提取 sql 语句中 select 等关键元素,然后判断 sql 语句是否有语法错误,比如关键词是否正确等等。调用数据库引擎接口
,返回执行结果。一条SQL更新语句在MySQL中的执行过程?
InnoDB、MyISAM、MEMORY
主要存储引擎以及功能如下:
MySQL5.5之前,默认存储引擎是MyISAM,5.5之后变成了InnoDB。
InnoDB支持的哈希索引是自适应的,InnoDB会根据表的使用情况自动为表生成哈希索引,不能人为干预是否在一张表中生成哈希索引。
MySQL 5.6 开始 InnoDB 支持全文索引。
大致上可以这么选择:
使用哪一种引擎可以根据需要灵活选择,因为存储引擎是基于表的,所以一个数据库中多个表可以使用不同的引擎以满足各种性能和实际需求。使用合适的存储引擎将会提高整个数据库的性能。
PS:MySQL8.0 都开始慢慢流行了,如果不是面试,MyISAM 其实可以不用怎么了解。
MySQL 日志文件有很多,包括:
还有两个 InnoDB 存储引擎特有的日志文件:
更新语句的执行是 Server 层和引擎层配合完成,数据除了要写入表中,还要记录相应的日志。
从上图可以看出,MySQL 在执行更新语句的时候,在服务层进行语句的解析和执行,在引擎层进行数据的提取和存储;同时在服务层对binlog进行写入,在InnoDB内进行redo log的写入。
不仅如此,在对 redo log 写入时有两个阶段的提交,一是 binlog 写入之前 **prepare
**状态的写入,二是 binlog 写入之后 commit
状态的写入。
为什么要两阶段提交呢?直接提交不行吗?
我们可以假设不采用两阶段提交的方式,而是采用"单阶段"进行提交,即要么先写入redo log,后写入binlog;要么先写入binlog,后写入redo log。这两种方式的提交都会导致原先数据库的状态和被恢复后的数据库的状态不一致。
先写入 redo log,后写入 binlog:
在写完redo log之后,数据此时具有crash-safe能力,因此系统崩溃,数据会恢复成事务开始之前的状态。但是,若在redo log写完时候,binlog写入之前,系统发生了宕机。此时binlog没有对上面的更新语句进行保存,导致当使用binlog进行数据库的备份或者恢复时,就少了上述的更新语句。从而使得id=2这一行的数据没有被更新。
先写入 binlog,后写入 redo log:
写完 binlog 之后,所有的语句都被保存,所以通过 binlog 复制或恢复出来的数据库中 id=2 这一行的数据会被更新为a=1。但是如果在redo log写入之前,系统崩溃,那么redo log中记录的这个事务会无效,导致实际数据库中 id=2 这一行的数据并没有更新。
简单说,redo log和binlog都可以用于表示事务的提交状态,而两阶段提交就是让这两个状态保持逻辑上的一致。
redo log的写入不是直接落到磁盘,而是在内存中设置了一片称之为 redo log buffer
的连续内存空间,也就是redo日志缓冲区
。
什么时候会刷入磁盘?
在如下的一些情况中,log buffer 的数据会刷入磁盘:
log buffer的大小是有限的,如果不停的往这个有限大小的log buffer里塞入日志,很快它就会被填满。如果当前写入 log buffer 的 redo 日志量已经占满了 log buffer 总容量的大约一半左右,就需要把这些日志刷新到磁盘上。
在事务提交时,为了保证持久性,会把log buffer中的日志全部刷到磁盘。注意,这时候,除了本事务的,可能还会刷入其它事务的日志。
有一个后台线程,大约每秒都会刷新一次 log buffer
中的 redo log
到磁盘。
正常关闭服务器时
触发 checkpoint 规则
重做日志缓存、重做日志文件都是以块(block)的方式进行保存的,称之为重做日志块(redo log block),块的大小是固定的512字节。我们的redo log它是固定大小的,可以看作是一个逻辑上的log group,由一定数量的log block组成。
它的写入方式是从头到尾开始写,写到末尾又回到开头循环写。
其中有两个标记位置:
write pos
是当前记录的位置,一边写一边后移,写到第3号文件末尾后就回到0号文件开头。checkpoint
是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到磁盘。
当write_pos
追上 checkpoint
时,表示 redo log 日志已经写满。这时候就不能接着往里写数据了,需要执行checkpoint
规则腾出可写空间。
所谓的checkpoint 规则,就是 checkpoint 触发后,将 buffer 中日志页都刷到磁盘。
慢 SQL 的监控主要通过两个途径:
慢 SQL 的优化,主要从两个方面考虑,SQL 语句本身的优化,以及数据库设计的优化。
避免不必要的列
这个是老生常谈,但还是经常会出的情况,SQL 查询的时候,应该只查询需要的列,而不要包含额外的列,像slect *
这种写法应该尽量避免。
分页优化
在数据量比较大,分页比较深的情况下,需要考虑分页的优化。
例如:
select * from table where type = 2 and level = 9 order by id asc limit 190289,10;
优化方案:
select a.* from table a,
(select id from table where type = 2 and level = 9 order by id asc limit 190289,10 ) b
where a.id = b.id
select * from table where id >
(select * from table where type = 2 and level = 9 order by id asc limit 190190289,10 )
索引优化
合理地设计和使用索引,是优化慢 SQL 的利器。
select name from test where city='上海'
我们将被查询的字段建立到联合索引中,这样查询结果就可以直接从索引中获取
alter table test add index idx_city_name (city, name);
例如,把 column<>'aaa',改成column>'aaa' or column<'aaa'
,就可以使用索引了
适当使用前缀索引
适当地使用前缀索引,可以降低索引的空间占用,提高索引的查询效率。
比如,邮箱的后缀都是固定的"@xxx.com
",那么类似这种后面几位为固定值的字段就非常适合定义为前缀索引
alter table test add index index2(email(6));
PS:需要注意的是,前缀索引也存在缺点,MySQL 无法利用前缀索引做 order by 和 group by 操作,也无法作为覆盖索引
select * from test where id + 1 = 50;
select * from test where month(updateTime) = 7;
JOIN 优化
select name from A left join B ;
排序优化
利用索引扫描做排序
MySQL有两种方式生成有序结果:其一是对结果集进行排序的操作,其二是按照索引顺序扫描得出的结果自然是有序的
但是如果索引不能覆盖查询所需列,就不得不每扫描一条记录回表查询一次,这个读操作是随机IO,通常会比顺序全表扫描还慢
因此,在设计索引时,尽可能使用同一个索引既满足排序又用于查找行
例如:
--建立索引(date,staff_id,customer_id)
select staff_id, customer_id from test where date = '2010-01-01' order by staff_id,customer_id;
只有当索引的列顺序和ORDERBY子句的顺序完全一致,并且所有列的排序方向都一样时,才能够使用索引来对结果做排序
UNION优化
条件下推
MySQL处理union的策略是先创建临时表,然后将各个查询结果填充到临时表中最后再来做查询,很多优化策略在union 查询中都会失效,因为它无法利用索引
最好手工将where、limit等子句下推到union的各个子查询中,以便优化器可以充分利用这些条件进行优化
此外,除非确实需要服务器去重,一定要使用union all,如果不加all 关键字,MySQL 会给临时表加上distinct选项,这会导致对整个临时表做唯一性检查,代价很高。
explain是sql优化的利器,除了优化慢sql,平时的sql编写,也应该先explain,查看一下执行计划,看看是否还有优化的空间。
直接在select 语句之前增加explain关键字,就会返回执行计划的信息。
索引可以说是MySQL面试中的重中之重,一定要彻底拿下。
从三个不同维度对索引分类:
例如从基本使用使用的角度来讲:
传统的查询方法,是按照表的顺序遍历的,不论查询几条数据,MySQL 需要将表的数据从头到尾遍历一遍。
在我们添加完索引之后,MySQL一般通过BTREE 算法生成一个索引文件,在查询数据库时,找到索引文件进行遍历,在比较小的索引数据里查找,然后映射到对应的数据,能大幅提升查找的效率。
和我们通过书的目录,去查找对应的内容,一样的道理。
索引虽然是sql性能优化的利器,但是索引的维护也是需要成本的,所以创建索引,也要注意:
索引应该建在查询应用频繁的字段。在用于where判断、order排序和join的(on)字段上创建索引。
索引的个数应该适量。索引需要占用空间;更新时候也需要维护。
区分度低的字段,例如性别,不要建索引。离散度太低的字段,扫描的行数降低的有限。
频繁更新的值,不要作为主键或者索引维护索引文件需要成本;还会导致页分裂,I0次数增多。
组合索引把散列性高(区分度高)的值放在前面为了满足最左前缀匹配原则
创建组合索引,而不是修改单列索引。组合索引代替多个单列索引(对于单列索引,MySQL 基本只能使用一个索引,所以经常使用多个条件查询时更适合使用组合索引)
过长的字段,使用前缀索引。当字段值比较长的时候,建立索引会消耗很多的空间,搜索起来也会很慢。我们可以通过截取字段的前面一部分内容建立索引,这个就叫前缀索引。
不建议用无序的值(例如身份证、UUID)作为索引。
当主键具有不确定性,会造成叶子节点频繁分裂,出现磁盘存储的碎片化
数据量比较少的表不适合加索引
更新比较频繁的字段也不适合加索引
离散低的字段不适合加索引(如性别)
当然不是。
MySQL的默认存储引擎是InnoDB,它采用的是B+树结构的索引。
在这张图里,有几个重点:
可以从几个维度去看这个问题,查询是否够快,效率是否稳定,存储数据多少,以及查找磁盘次数。
为什么不用普通二叉树?
普通二叉树存在退化的情况,如果它退化成链表,相当于全表扫描。平衡二叉树相比于二叉查找树来说,查找效率更稳定,总体的查找速度也更快。
为什么不用平衡二叉树呢?
读取数据的时候,是从磁盘读到内存。如果树这种数据结构作为索引,那每查找一次数据就需要从磁盘中读取一个节点,也就是一个磁盘块,但是平衡二叉树可是每个节点只存储一个键值和数据的,如果是B+树,可以存储更多的节点数据,树的高度也会降低,因此读取磁盘的次数就降下来啦,查询效率就快。
B+相比较 B 树,有这些优势:
B Tree 解决的两大问题:每个节点存储更多关键字;路数更多
如果我们要对表进行全表扫描,只需要遍历叶子节点就可以 了,不需要遍历整棵 B+Tree 拿到所有的数据。
根节点和枝节点不保存数据区,所以一个节点可以保存更多的关键字,一次磁盘加载的关键字更多,IO次数更少。
因为叶子节点上有下一个数据区的指针,数据形成了链表。
首先理解聚簇索引不是一种新的索引,而是而是一种数据存储方式。
聚簇表示数据行和相邻的键值紧凑地存储在一起。我们熟悉的两种存储引擎–MyISAM 采用的是非聚簇索引,InnoDB采用的是聚簇索引。
可以这么说:
索引的数据结构是树,聚簇索引的索引和数据存储在一棵树上,树的叶子节点就是数据,非聚簇索引索和数据不在一棵树上。
在InnoDB存储引擎里,利用辅助索引查询,先通过辅助索引找到主键索引的键值,再通过主键值查出主键索引里面没有符合要求的数据,它比基于主键索引的查询多扫描了一棵索引树,这个过程就叫回表。
例如:select \* from user where name = '张三';
在辅助索引里面,不管是单列索引还是联合索引,如果 select 的数据列只用辅助索引中就能够取得,不用去查主键索引,这时候使用的索引就叫做覆盖索引,避免了回表。
如,select name from user where name ='张三';
注意:最左前缀原则、最左匹配原则、最左前缀匹配原则这三个都是一个概念。
最左匹配原则:在InnoDB的联合索引中,查询的时候只有匹配了前一个/左边的值之后,才能匹配下一个。
根据最左匹配原则,我们创建了一个组合索引,如(a1,a2,a3),相当于创建了(a1)、(a1,a2)和(a1,a2,a3)三个索引。
为什么不从最左开始查,就无法匹配呢?
比如有一个 user 表,我们给 name 和 age 建立了一个组合索引。
ALTER TABLE user add INDEX comidx_name_phone (name,age);
组合索引在 B+Tree 中是复合的数据结构,它是按照从左到右的顺序来建立搜索树的(name 在左边,age 在右边)。
从这张图可以看出来,name是有序的,age是无序的。当name相等的时候,age才是有序的。
这个时候我们使用where name = '张三' and age ='20'
去查询数据的时候,B+Tree会优先比较name来确定下一步应该搜索的方向,往左还是往右。如果name相同的时候再比较age。但是如果查询条件没有name,就不知道下一步应该查哪个节点,因为建立搜索树的时候 name是第一个比较因子,所以就没用上索引。
索引条件下推优化**(Index Condition Pushdown(ICP))**是MySQL5.6 添加的,用于优化数据查询。
例如一张表,建了一个联合索引(name,age),查询语句:select * from t_user where name like '张%' and age=10;
,由于 name
使用了范围查询,根据最左匹配原则:
不使用ICP,引擎层查找到name like '张%'
的数据,再由Server层去过滤age=10
这个条件,这样一来,就回表了两次,浪费了联合索引的另外一个字段 age
。
但是,使用了索引下推优化,把where的条件放到了引擎层执行,直接根据name like '张%' and age=10
的条件进行过滤,减少了回表的次数。
索引条件下推优化可以减少存储引擎查询基础表的次数,也可以减少 MySQL 服务器从存储引擎接收数据的次数。
如果按锁粒度划分,有以下 3 种
如果按照兼容性,有两种:
我们拿这么一个用户表来表示行级锁,其中插入了4行数据,主键值分别是1,6,8,12,现在简化它的聚簇索引结构,只保留数据记录。
InnoDB的行锁的主要实现如下:
select * from t where id = 6 for update
;就会将id=6
的记录锁定。间隙锁就是锁定某些间隙区间的。当我们使用用等值查询或者范围查询,并且没有命中任何一个record,此时就会将对应的间隙区间锁定。例如select * from t where id =3 for update;
或者select * from t where id > 1 and id < 6 for update;
就会将(1,6)区间锁定。
临键锁就是记录锁(Record Locks)和间隙锁(Gap Locks)的结合,即除了锁住记录本身,还要再锁住索引之间的间隙。当我们使用范围查询,并且命中了部分record记录,此时锁住的就是临键区间。注意,临键锁锁住的区间会包含最后一个record 的右边的临键区间。例如 select * from t where id > 5 and id <= 7 for update;
会锁住(1,6]、(6,8]。mysql 默认行锁类型就是 临键锁(Next-KeyLocks)。当使用唯一性索引,等值查询匹配到一条记录的时候,临键锁(Next-Key Locks)会退化成记录锁;没有匹配到任何记录的时候,退化成间隙锁。
间隙锁(Gap Locks)和临键锁(Next-Key Locks)都是用来解决幻读问题的,
在已提交读(READ COMMITTED)隔离级别下,间隙锁(Gap Locks)和 临键锁(Next-Key Locks)都会失效!
上面是行锁的三种实现算法,除此之外,在行上还存在插入意向锁。
一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了意向锁,如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交。但是事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在在等待。这种类型的锁命名为 Insert Intention Locks,也就是插入意向锁。
假如我们有个T1事务,给(1,6)区间加上了意向锁,现在有个T2事务,要插入一个数据,id为4,它会获取一个(1,6)区间的插入意向锁,又有有个T3 事务,想要插入一个数据,id为 3,它也会获取一个(1,6)区间的插入意向锁,但是,这两个插入意向锁锁不会互斥。
悲观锁认为被它保护的数据是极其不安全的,每时每刻都有可能被改动,一个事务拿到悲观锁后,其他任何事务都不能对该数据进行修改,只能等待锁被释放才可以执行。
数据库中的行锁,表锁,读锁,写锁均为悲观锁。
乐观锁认为数据的变动不会太频繁。
乐观锁通常是通过在表中增加一个版本(version)或时间戳(timestamp)来实现,其中,版本最为常用。
事务在从数据库中取数据时,会将该数据的版本也取出来(v1),当事务对数据变动完毕想要将其更新到表中时,会将之前取出的版本v1与数据中最新的版本v2相对比,如果v1=v2,那么说明在数据变动期间,没有其他事务对数据进行修改,此时,就允许事务对表中的数据进行修改,并且修改时version会加1,以此来表明数据已被变动。
如果,v1不等于v2,那么说明数据变动期间,数据被其他事务改动了,此时不允许数据更新到表中,一般的处理办法是通知用户让其重新操作。不同于悲观锁,乐观锁通常是由开发者实现的。
排查死锁的一般步骤是这样的:
(1)查看死锁日志 show engine innodb status;
(2)找出死锁 sql
(3)分析 sql 加锁情况
(4)模拟死锁案发
(5)分析死锁日志
(6)分析死锁结果
当然,这只是一个简单的流程说明,实际上生产中的死锁千奇百怪,排查和解决起来没那么简单。
ACID(Atomicity、Consistency、Isolation、Durability)
事务的四个隔离级别
MySQL 默认的事务隔离级别是可重复读(Repeatable Read)。
不同的隔离级别,在并发事务下可能会发生的问题:
详细内容讲解可参考:一文详解幻读、脏读和不可重复读_weixin_45483322的博客-CSDN博客
读未提交
读未提交,就不用多说了,采取的是读不加锁原理。
读取已提交&可重复读
读取已提交和可重复读级别利用了ReadView
和MVCC
,也就是每个事务只能读取它能看到的版本(ReadView)。
串行化
串行化的实现采用的是读写都加锁的原理。
串行化的情况下,对于同一行事务,写会加写锁
,读会加读锁
。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
MVCC(Multi Version Concurrency Control),中文名是多版本并发控制,简单来说就是通过维护数据历史版本,从而解决并发访问情况下的读一致性问题。
关于它的实现,要抓住几个关键点,隐式字段、undo日志、版本链、快照读&当前读、Read View。
版本链
对于 InnoDB 存储引擎,每一行记录都有两个隐藏列DB_TRX_ID、DB_ROLL_PTR
假如有一张 user 表,表中只有一行记录,当时插入的事务id为80。此时,该条记录的示例图如下:
接下来有两个 DB_TRX_ID 分别为 100、200 的事务对这条记录进行 update 操作,整个过程如下:
由于每次变动都会先把undo日志记录下来,并用DB_ROLL_PTR指向undo日志地址。因此可以认为,对该条记录的修改日志串联起来就形成了一个版本链,版本链的头节点就是当前记录最新的值。如下:
ReadView
对于
Read Committed
和Repeatable Read
隔离级别来说,都需要读取已经提交的事务所修改的记录,也就是说如果版本链中某个版本的修改没有提交,那么该版本的记录时不能被读取的。所以需要确定在Read Committed和Repeatable Read隔离级别下,版本链中哪个版本是能被当前事务读取的。于是就引入了ReadView 这个概念来解决这个问题。
Read View就是事务执行快照读时,产生的读视图,相当于某时刻表记录的一个快照,通过这个快照,我们可以获取:
有了这个 ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见:
如果某个版本的数据对当前事务不可见的话,那就顺着版本链找到下一个版本的数据,继续按照上边的步骤判断可见性,依此类推,直到版本链中的最后一个版本。如果最后一个版本也不可见的话,那么就意味着该条记录对该事务完全不可见,查询结果就不包含该记录。
在MySQL中,READ COMMITTED和REPEATABLE READ隔离级别的的一个非常大的区别就是它们生成ReadView 的时机不同。
READ COMMITTED是每次读取数据前都生成一个ReadView,这样就能保证自己每次都能读到其它事务提交的数据;
REPEATABLE READ是在第一次读取数据时生成一个ReadView,这样就能保证后续读取的结果完全一致。
读写分离的基本原理是将数据库读写操作分散到不同的节点上,下面是基本架构图:
读写分离的基本实现是:
将读写操作区分开来,然后访问不同的数据库服务器,一般有两种方式:程序代码封装和中间件封装。
程序代码封装指在代码中抽象一个数据访问层(有的文章也称这种方式为"中间层封装"),实现读写操作分离和数据库服务器连接的管理。例如,基于 Hibernate 进行简单封装,就可以实现读写分离:
目前开源的实现方案中,淘宝的TDDL(Taobao Distributed Data Layer,外号:头都大了)是比较有名的。
中间件封装指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供SQL 兼容的协议,业务服务器无须自己进行读写分离。
对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。
其基本架构是:
主从同步延迟的原因:
一个服务器开放N个链接给客户端来连接的,这样有会有大并发的更新操作,但是从服务器的里面读取binlog的线程仅有一个,当某个SQL在从服务器上执行的时间稍长或者由于某个SQL要进行锁表就会导致,主服务器的SQL大量积压,未被同步到从服务器里。这就导致了主从不一致,也就是主从延迟。
主从同步延迟的解决办法:
解决主从复制延迟有几种常见的方法:
例如,注册账号完成后,登录时读取账号的读操作也发给数据库主服务器。这种方式和业务强绑定,对业务的侵入和影响较大,如果哪个新来的程序员不知道这样写代码,就会导致一个bug。
这就是通常所说的"二次读取",二次读取和业务无绑定,只需要对底层数据库访问的API进行封装即可,实现代价较小,不足之处在于如果有很多二次读取,将大大增加主机的读操作压力。例如,黑客暴力破解账号,会导致大量的二次读取操作,主机可能顶不住读操作的压力从而崩溃。
例如,对于一个用户管理系统来说,注册+登录的业务读写操作全部访问主机,用户的介绍、爰好、等级等业务,可以采用读写分离,因为即使用户改了自己的自我介绍,在查询时却看到了自我介绍还是旧的,业务影响与不能登录相比就小很多,还可以忍受。
什么是路由呢?就是数据应该分到哪一张表。
水平分表主要有三种路由方式:
我们可以观察一些支付系统,发现只能查一年范围内的支付记录,这个可能就是支付公司按照时间进行了分表。
范围路由设计的复杂点主要体现在分段大小的选取上,分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在100万至2000万之间,具体需要根据业务选取合适的分段大小。
范围路由的优点是可以随着数据的增加平滑地扩充新的表。例如,现在的用户是100万,如果增加到1000万,只需要增加新的表就可以了,原有的数据不需要动。范围路由的一个比较隐含的缺点是分布不均匀,假如按照1000万来进行分表,有可能某个分段实际存储的数据量只有1000条,而另外一个分段实际存储的数据量有900万条。
同样以订单id为例,假如我们一开始就规划了4个数据库表,路由算法可以简单地用id%4的值来表示数据所属的数据库表编号,id为12的订单放到编号为50的子表中,id为13的订单放到编号为61的字表中。
Hash路由设计的复杂点主要体现在初始表数量的选取上,表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。而用了Hash路由后,增加子表数量是非常麻烦的,所有数据都要重分布。Hash路由的优缺点和范围路由基本相反,Hash 路由的优点是表分布比较均匀,缺点是扩充新的表很麻烦,所有数据都要重分布。
配置路由设计简单,使用起来非常灵活,尤其是在扩充表的时候,只需要迁移指定的数据,然后修改路由表就可以了。
配置路由的缺点就是必须多查询一次,会影响整体性能;而且路由表本身如果太大(例如,几亿条数据),性能同样可能成为瓶颈,如果我们再次将路由表分库分表,则又面临一个死循环式的路由算法选择问题。
实际上,不停机扩容,实操起来是个非常麻烦而且很有风险的操作,当然,面试回答起来就简单很多。
第一阶段:在线双写,查询走老库
第二阶段:在线双写,查询走新库
从分库的角度来讲:
事务的问题
跨库 JOIN 问题
从分表的角度来看:
跨节点的 count,order by,group by 以及聚合函数问题
数据迁移,容量规划,扩容等问题
ID 问题
关于索引:由于索引需要额外的维护成本,因为索引文件是单独存在的文件,所以当我们对数据的增加,修改,删除,都会产生额外的对索引文件的操作,这些操作需要消耗额外的IO,会降低增/改/删的执行效率。
所以,在我们删除数据库百万级别数据的时候,查询MySQL官方手册得知删除数据的速度和创建的索引数量是成正比的。
当线上的数据库数据量到达几百万、上千万的时候,加一个字段就没那么简单,因为可能会长时间锁表。
大表添加字段,通常有这些做法:
通过中间表转换过去:
用pt-online-schema-change :
先在从库添加 再进行主从切换:
排查过程:
(1)使用 top 命令观察,确定是 mysqld 导致还是其他原因。
(2)如果是 mysqld 导致的,show processlist,查看 session 情况,确定是不是有消耗资源的 sql 在运行。
(3)找出消耗高的 sql,看看执行计划是否准确,索引是否缺失,数据量是否太大。
处理:
(1)kill 掉这些线程(同时观察 cpu 使用率是否下降),
(2)进行相应的调整(比如说加索引、改 sql、改内存参数)
(3)重新跑这些 SQL。
其他情况:
也有可能是每个sql消耗资源并不多,但是突然之间,有大量的session连进来导致cpu飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等。
资料来源:面渣逆袭:MySQL六十六问,两万字+五十图详解!有点六!