携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情
最近陆陆续续分享了十几篇MySQL高频面试问题,算是告一段落,今日做个汇总。
本文精心整理上万字,几十张图,46道高频面试问题。
通俗易懂,内容详实,干货满满,信息量巨大,值得多刷。
别问我为什么总结的这么详细,这么多干货,还无私分享出来?
问就是雷锋精神。
欢迎点赞评论收藏关注,我会持续分享更多技术干货。
MySQL基础架构
1. MySQL底层架构体系及每层作用?
由图中可以看到MySQL架构主要分为Server层和存储引擎层。
Server层又分为连接器、缓存、分析器、优化器、执行器。所有跨存储引擎的功能都在这层实现,比如:函数、存储过程、触发器、视图等。
存储引擎是可插拔式的,常见的存储引擎有MyISAM、InnoDB、Memory等,MySQL5.5之前默认的是MyISAM,之后默认的是InnoDB。
2. InnoDB和MylSAM存储引擎的区别?
事务支持: InnoDB 支持事务,MyISAM 不支持事务。
存储结构: InnoDB存储一个文件,MyISAM存储三个文件。
锁: InnoDB支持表锁、行锁,MyISAM只支持表锁。
主键: InnoDB必须有主键,MyISAM允许没有主键。
外键: InnoDB支持外键,MyISAM不支持外键。
表总行数: InnoDB没有存储表总行数,只能实时遍历,MyISAM缓存了表总行数。
应用场景: InnoDB支持事务、行锁、外键,适合高并发和数据安全性比较高的场景。MyISAM提供高效存储和查询,适合读多和数据安全性较低的场景。
3. 聚簇索引和非聚簇索引的区别?
聚簇索引的叶子节点保存整行记录,非聚簇索引的叶子节点索引数据和主键ID。
针对下面这张用户表,聚簇索引和非聚簇索引的存储结构是这样的:
CREATE TABLE `user` (
`id` int COMMENT '主键ID',
`name` varchar(10) COMMENT '姓名',
`age` int COMMENT '年龄',
PRIMARY KEY (`id`)
) ENGINE=InnoDB CHARSET=utf8 COMMENT='用户表';
4. 什么是二叉查找树?
- 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 左、右子树也分别为二叉查找树;
二叉搜索树查找数据的时间复杂度是O(logN),如图所示,最多查找3次就可以查到所需数据。
极端情况下,二叉查找树可能退化成线性链表。
5. 什么是红黑树?
- 结点是红色或黑色
- 根结点是黑色
- 所有叶子都是黑色(叶子是NIL结点)
- 每个红色结点的两个子结点都是黑色(从每个叶子到根的所有路径上不能有两个连续的红色结点)
- 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点
红黑树的优点: 限制了左右子树的树高,不会相差过大。
缺点: 规则复杂,一般人想要弄懂这玩意儿,就已经很费劲了,更别说使用了。
6. 什么是B树?
对于一个m阶的B树:
- 根节点至少有2个子节点
- 每个中间节点都包含k-1个元素和k个子节点,其中 m/2 <= k <= m
- 每个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
- 中间节点的元素按照升序排列
- 所有的叶子结点都位于同一层
B树这样的设计有哪些优点呢?
高度更低,每个节点含有多个元素,查找的时候一次可以把一个节点中的所有元素加载到内存中作比较,两种改进都大大减少了磁盘IO次数。
7. 什么是B+树?
相比较B树,B+树又做了如下约定:
-
有k个子节点的中间节点就有k个元素(B树中是k-1个元素),也就是子节点数量 = 元素数量。 每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。
-
所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。
-
非叶子节点只保存索引,不保存数据。(B树中两者都保存)
-
叶子结点包含了全部元素的信息,并且叶子结点按照元素大小组成有序列表。
B+树这样设计有什么优点呢?
- 每个节点存储的元素更多,看起来比B树更矮胖,导致磁盘IO次数更少。
- 非叶子节点不存储数据,只存储索引,叶子节点存储全部数据。 这样设计导致每次查找都会查到叶子节点,效率更稳定,便于做性能优化。
- 叶子节点之间使用有序链表连接。 这样设计方便范围查找,只需要遍历链表中相邻元素即可,不再需要二次遍历二叉树。
8. MySQL索引底层实现为什么要用B+树结构?
看完了二叉树查找树、红黑树、B树、B+树的底层实现原理,就明白了B树和B+树就是为了文件检索系统设计的,更适合做索引结构。
B+树的优点是:
- 每个节点存储的元素更多,看起来比B树更矮胖,导致磁盘IO次数更少。
- 非叶子节点不存储数据,只存储索引,叶子节点存储全部数据。 这样设计导致每次查找都会查到叶子节点,效率更稳定,便于做性能优化。
- 叶子节点之间使用有序链表连接。 这样设计方便范围查找,只需要遍历链表中相邻元素即可,不再需要二次遍历二叉树。
MySQL索引
9. 联合索引的底层存储结构是什么样的?
在age和name字段建一个联合索引(age,name),它的存储结构就是这样:
联合索引的优点:可以快速匹配到所需数据,大大减少扫描行数。
10. 什么是最左匹配原则?
最左匹配原则是指在建立联合索引的时候,遵循最左优先,以最左边的为起点任何连续的索引都能匹配上。
当我们在(age,name)上建立联合索引的时候,where条件中只有age可以用到索引,同时有age和name也可以用到索引。但是只有name的时候是无法用到索引的。
看上面的图,就理解了,(age,name)的联合索引,是先按照age排序,age相等的行再按照name排序。如果where条件只有一个name,当然无法用到索引。
11. 什么是覆盖索引和回表查询?
在使用非聚簇索引查询的时候,叶子节点中已经有了所需结果,无需再查询主键索引,这时候就是用到了覆盖索引。
二次查询主键索引的行为就是回表查询。
当我们在age上建索引的时候,查询SQL是这样的时候:
select id from user where age = 18;
就会用到覆盖索引,因为ID字段我们使用age索引的时候已经查出来,不需要再二次回表查询了。
12. 怎么创建联合索引,查询效率最高?
创建联合索引时,区分度高的字段放前面。
这样可以减少查询次数,更快地匹配到所需数据。
比如对于一张用户表来说,生日比性别的区分度更高,更适合创建索引。
可以使用下面的方式手动统计一下,每个字段的区分度,值越大,区分度越高:
select
count(distinct birthday)/count(*),
count(distinct gender)/count(*)
from user;
对于已经创建好的索引,我们还可以使用MySQL命令查看每个索引的区分度排名:
图中Cardinality列表示索引的区分度排名,也被称为基数。
13. 什么是索引下推?优点是什么?
在(age,name)上面建联合索引,并且查询SQL是这样的时候:
select * from user where age = 18 and name = '张三';
如果没有索引下推,会先匹配出 age = 18 的三条记录,再用ID回表查询,筛选出 name = '张三' 的记录。
如果使用索引下推,会先匹配出 age = 18 的三条记录,再筛选出 name = '张三' 的一条记录,最后再用ID回表查询。
索引下推优点: 可以减少回表查询次数,提高查询效率。
MySQL事务
14. MySQL事务的四大特性?
事务有四大特性,分别是原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),简称ACID。
原子性 是指事务中所有操作要么全部成功,要么全部失败。
一致性 是指事务执行前后,数据始终处于一致性状态,不会出现数据丢失。
隔离性 是指事务提交前的中间状态对其他事务不可见,即相互隔离。
持久性 是指事务提交后,数据的修改永久保存在数据库中。
15. MySQL事务四大特性的实现原理?
原子性是undo log实现的,一致性是由代码逻辑层面保证的,隔离性是由mvcc实现的,持久性是基于redo log实现的。
16. MySQL Redo Log日志作用及实现原理?
Redo Log 记录的是物理日志,也就是磁盘数据页的修改。
作用: 用来保证服务崩溃后,仍能把事务中变更的数据持久化到磁盘上。
写入Redo Log 的过程:
- 从磁盘加载数据到内存
- 在内存中修改数据
- 把新数据写到Redo Log Buffer中
- 把Redo Log Buffer中数据持久化到Redo Log文件中
- 把Redo Log文件中数据持久化到数据库磁盘中
17. MySQL Undo Log日志作用及实现原理?
Undo Log记录的是逻辑日志,也就是SQL语句。
比如:当我们执行一条insert语句时,Undo Log就记录一条相反的delete语句。
作用:
- 回滚事务时,恢复到修改前的数据。
- 实现 MVCC(多版本并发控制,Multi-Version Concurrency Control) 。
18. MySQL Bin Log 日志作用及实现原理?
Bin Log记录的是逻辑日志,即原始的SQL语句,是MySQL自带的。
作用: 数据备份和主从同步。
Bin Log共有三种日志格式,可以binlog_format配置参数指定。
参数值 | 含义 |
---|---|
Statement | 记录原始SQL语句,会导致更新时间与原库不一致。 比如 update_time=now() |
Row | 记录每行数据的变化,保证了数据与原库一致,缺点是数据量较大。 |
Mixed | Statement和Row的混合模式,默认采用Statement模式,涉及日期、函数相关的时候采用Row模式,既减少了数据量,又保证了数据一致性。 |
加入写Bin Log之后的事务流程:
这就是二阶段提交的概念,先写处于prepare状态的Redo Log,事务提交后,再写处于commit状态的Redo Log。
19.MySQL MVCC(多版本并发控制)作用及实现原理?
MVCC(MultiVersion Concurrency Control)就是通过一定机制生成一个数据请求时间点的一致性数据快照,并用这个快照来提供一定级别(语句级或事务级)的一致性读取。
记录的是某个时间点上的数据快照,用来实现不同事务之间数据的隔离性。
MVCC的实现方式通过两个隐藏列trx_id(最近一次提交事务的ID)和roll_pointer(上个版本的地址),建立一个版本链。并在事务中读取的时候生成一个ReadView(读视图),在Read Committed隔离级别下,每次读取都会生成一个读视图,而在Repeatable Read隔离级别下,只会在第一次读取时生成一个读视图。
20. MySQL并发事务产生的问题?
脏读: 一个事务读到其他事务未提交的数据。
不可重复读: 多次读取相同的数据,得到的结果集不一致,即读到其他事务提交后的数据。
幻读: 相同的查询条件,多次读取的结果不一致,即读到其他事务提交后的数据。
不可重复读与幻读的区别是: 不可重复读是读到了其他事务执行update、delete后的数据,而幻读是读到其他事务执行insert后的数据。
21. MySQL 四大隔离级别作用和问题?
Read UnCommitted(读未提交): 读到其他事务未提交的数据,会出现脏读、不可重复读、幻读。
Read Committed(读已提交): 读到其他事务已提交的数据,解决了脏读,会出现不可重复读、幻读。
Repeatable Read(可重复读): 相同的数据,多次读取到的结果集一致。解决了不可重复读,还是会出现幻读。
Serializable(串行化): 所有事务串行执行,解决了幻读。
22. 什么是快照读和当前读?
当前读: 读取数据的最新版本,并对数据进行加锁。
例如:insert、update、delete、select for update
快照读: 读取数据的历史版本,不对数据加锁。
例如:select
23. MySQL是如何解决幻读的?
在当前读的情况下,是通过加锁来解决幻读。
在快照读的情况下,是通过MVCC来解决幻读。
MySQL锁
24. MySQL锁的分类?
按锁的粒度可分为:表锁、页面锁、行锁、记录锁、间隙锁、临键锁
按锁的属性可分为:共享锁、排它锁
按加锁机制可分为:乐观锁、悲观锁
25. MySQL表锁、页面锁、行锁的作用及优缺点?
表锁:
MyISAM和InnoDB引擎均支持表锁。
优点: 开销小,加锁快,不会出现死锁。
缺点: 锁定力度大,发生锁冲突概率高,并发度最低。
加锁方式:
# 对user表加读锁
lock table user read;
# 同时对user表加读锁,对order表加写锁
lock tables user read, order write;
什么情况下需要用到表锁?
- 当需要更新表中的大部分数据
- 事务涉及到多张表,业务逻辑复杂,加表锁可以避免死锁。
页面锁:
优点:开销和加锁速度介于表锁和行锁之间。
缺点:会出现死锁,锁定粒度介于表锁和行锁之间,并发度一般。
目前只有BDB引擎支持页面锁,应用场景较少。
行锁:
只有InnoDB引擎支持行锁,另外锁是加在索引上面的。
优点: 开销大,加锁慢;会出现死锁。
缺点:锁定粒度小,发生锁冲突的概率低,并发度高。
另外记录锁、间隙锁、临键锁均属于行锁。
26. 什么是记录锁(Record Locks)、间隙锁(Gap Locks)、临键锁(Next-Key Locks)?
记录锁(Record Locks):
即对某条记录加锁。
# 对id=1的用户加锁
update user set age=age+1 where id=1;
间隙锁(Gap Locks):
即对某个范围加锁,但是不包含范围的临界数据。
# 对id大于1并且小于10的用户加锁
update user set age=age+1 where id>1 and id<10;
上面SQL的加锁范围是(1,10)。
临键锁(Next-Key Locks):
由记录锁和间隙锁组成,既包含记录本身又包含范围,左开右闭区间。
# 对id大于1并且小于等于10的用户加锁
update user set age=age+1 where id>1 and id<=10;
27. 什么是共享锁(又称读锁、S锁)和排他锁(又称写锁、X锁)?
共享锁(又称读锁、S锁):
作用:防止其他事务修改当前数据。
加锁方式:
在select语句末尾加上lock in share mode关键字。
# 对id=1的用户加读锁
select * from user where id=1 lock in share mode;
排他锁(又称写锁、X锁):
作用:防止其他事务读取或者更新当前数据。
加锁方式:
在select语句末尾加上for update关键字。
# 对id=1的用户加写锁
select * from user where id=1 for update;
28. 什么是乐观锁和悲观锁?作用及实现方式?
乐观锁:
总是假设别人不会修改当前数据,所以每次读取数据的时候都不会加锁,只是在更新数据的时候通过version判断别人是否修改过数据,Java的atomic包下的类就是使用乐观锁(CAS)实现的。
适用于读多写少的场景。
加锁方式:
-
读取version
select id,name,age,version from user id=1;
-
更新数据,判断version是否修改过。
update user set age=age+1 where id=1 and version=1;
悲观锁:
总是假设别人会修改当前数据,所以每次读取的时候,总是加锁。
适用于写多读少的场景。
加锁方式:
# 加读锁
select * from user where id=1 lock in share mode;
# 加写锁
select * from user where id=1 for update;
29. 不同索引之间的加锁范围解析?
-
MySQL锁是加在索引记录上面的。
-
如果是非唯一性索引,不论表中是否存在该记录,除了会对该记录所在范围加锁,还会向右遍历到不满足条件的范围进行加锁。
-
如果是唯一索引,如果表中存在该记录,只对该行记录加锁。如果表中不存在该记录,除了会对该记录所在范围加锁,还会向右遍历到不满足条件的范围进行加锁。
详情请查看:一条update语句加了多少锁?
30. 如何查看死锁日志,并手把手解决线上MySQL死锁问题?
通过MySQL查看最近一次的死锁日志:
show engine innodb status;
在死锁日志中,可以清楚地看到这两条insert语句产生了死锁,最终事务2被会回滚,事务1执行成功。
# 事务1
insert into user (id,name,age) values (5,'张三',5);
# 事务2
insert into user (id,name,age) values (6,'李四',6);
解决过程请查看:记一次排查线上MySQL死锁过程,不能只会crud,还要知道加锁原理
MySQL主从同步
31. MySQL主动同步的作用及实现原理?
作用是:
- 读写分离,提升数据库性能
- 容灾恢复,主服务器不可用时,从服务器提供服务,提高可用性
- 冗余备份,主服务器数据损坏丢失,从服务器保留备份
MySQL主从同步是基于Bin Log实现的,而Bin Log记录的是原始SQL语句。
主从同步的过程:
-
当主库数据发生变更时,写入本地Bin Log文件
-
从库IO线程发起dump主库Bin Log文件的请求
-
主库IO线程推送Bin Log文件到从库中
-
从库IO线程把Bin Log内容写入本地的Relay Log文件中
-
从库SQL线程读取Relay Log文件内容
-
从库SQL线程重新执行一遍SQL语句
32. MySQL主动同步延迟问题产生原因?
-
从库机器性能较差
主库负责所有读写请求,从库只用来备份,会用性能较差的机器,执行时间自然较慢。
-
从库压力更大
读写分离后,主库负责写请求,从库负责读请求。
互联网应用一般读请求更多,所以从库读压力更大,占用更多CPU资源。
-
网络延迟
当主库的Bin Log文件往从库上发送时,可能产生网络延迟,也会导致从库数据跟不上。
-
主库有大事务
当主库上有个大事务需要执行5分钟,把Bin Log文件发送到从库,从库至少也需要执行5分钟,所以这时候从库就出现了5分钟的延迟。
33. MySQL主动同步延迟的解决方案?
-
从库机器性能较差
把从库换成跟主库同等规格的机器。
-
从库压力更大
多搞几台从库,分担读请求压力。
-
网络延迟
联系运维或者云服务提供商解决。
-
主库有大事务
把大事务分割成小事务执行,大事务不但会产生从库延迟,还可能产生死锁,降低数据库并发性能,所以尽量少用大事务。
34. 如何提升MySQL主从同步的性能?
- 从库开启多线程复制
就是在主从同步的最后两步使用多线程,修改配置 slave_parallel_workers=4,代表开启4个复制线程。
- 修改同步模式,改为异步
主从同步共有三种复制方式:
-
全同步复制
当主库执行完一个事务,并且所有从库都执行完该事务后,才给客户端返回成功。
-
半同步复制
至少有一个从库执行完成后,就给客户端返回成功。
-
异步复制
主库执行完后,立即返回成功,不关心从库是否执行完成。
如果对数据安全性要求没那么高,可以把同步模式改成半同步复制或者异步复制。
- 修改从库Bin Log配置
修改sync_binlog配置:
sync_binlog=0 ,表示写binlog不立即刷新磁盘,由系统决定什么时候刷新磁盘。
sync_binlog=1,每次写binlog都刷新磁盘,安全性高,性能差。
sync_binlog=N,写N次binlog才刷新磁盘。
从库对数据安全性要求没那么高,可以设置sync_binlog=0。
修改innodb_flush_log_at_trx_commit配置:
innodb_flush_log_at_trx_commit=0,每隔一秒钟,把事务日志刷新到磁盘。
innodb_flush_log_at_trx_commit=1,每次事务都刷新到磁盘。
innodb_flush_log_at_trx_commit=2,每次事务都不主动刷新磁盘,由系统决定什么时候刷新磁盘。
从库对数据安全性要求没那么高,可以设置innodb_flush_log_at_trx_commit=2。
MySQL查询性能优化
35. MySQL索引失效场景?
- 数据类型隐式转换
- 模糊查询 like 以%开头
- or前后没有同时使用索引
- 联合索引,没有使用第一列索引
- 在索引字段进行计算操作
- 在索引字段字段上使用函数
- 优化器选错索引
36. MySQL深分页问题及解决方案?
每页10条,当我们查询第一页的时候,速度很快:
select * from user
where create_time>'2022-07-03'
limit 0,10;
在不到0.01秒内直接返回了。
当我们翻到第10000页的时候,查询效率急剧下降:
select * from user
where create_time>'2022-07-03'
limit 100000,10;
-
使用子查询
先用子查询查出符合条件的主键,再用主键ID做条件查出所有字段。
select * from user where id in ( select id from user where create_time>'2022-07-03' limit 100000,10 );
-
使用分页游标
实现方式就是:当我们查询第二页的时候,把第一页的查询结果放到第二页的查询条件中。
例如:首先查询第一页
select * from user
where create_time>'2022-07-03'
limit 10;
然后查询第二页,把第一页的查询结果放到第二页查询条件中:
select * from user
where create_time>'2022-07-03' and id>10
limit 10;
37. MySQL执行计划(Explain)使用详解?
详情请查看:学会使用MySQL的Explain执行计划,SQL性能调优从此不再困难
MySQL索引的分类?
常见的索引有,普通索引、唯一索引、主键索引、联合索引、全文索引等。
普通索引
普通索引就是最基本的索引,没有任何限制。
可以使用命令创建普通索引:
ALTER TABLE `table_name` ADD INDEX index_name (`column`);
唯一索引
与普通索引不同,唯一索引的列值必须唯一,允许为null。
创建方式是这样的:
ALTER TABLE `table_name` ADD UNIQUE index_name (`column`);
主键索引
主键索引是一种特殊的唯一索引,并且一张表只有一个主键,不允许为null。
创建方式是这样的:
ALTER TABLE `table_name` ADD PRIMARY KEY (`column`);
联合索引
联合索引是同时在多个字段上创建索引,查询效率更高。
创建方式是这样的:
ALTER TABLE `table_name` ADD INDEX index_name (`column1`, `column2`, `column3`);
全文索引
全文索引主要用来匹配字符串文本中关键字。
当需要字符串中是否包含关键字的时候,我们一般用like,如果是以%开头的时候,则无法用到索引,这时候就可以使用全文索引了。
创建方式是这样的:
ALTER TABLE `table_name` ADD FULLTEXT (`column`);
38. 哪些字段适合创建索引?
- 频繁查询的字段
- 在where和on条件出现的字段
- 区分度高的字段
- 有序的数值类型字段
39. 哪些字段不适合创建索引?
- 区分度低的字段
- 频繁更新的字段
- 过长的字段
- 无序的字段
40. 创建索引的注意事项?
- 优先使用联合索引
- 使用联合索引时,区分度的字段放前面
- 过长字符串可以使用前缀索引
- 值唯一的字段,使用唯一索引
- 避免创建过多索引
41. 精心总结MySQL开发16条规范?
- 禁止使用select *
- 用小表驱动大表
- join关联表不宜过多
- 禁止使用左模糊或者全模糊查询
- 索引访问类型至少达到range级别
- 更优雅的使用联合索引
- 注意避免深分页
- 单表字段不要超过30个
- 枚举字段不要使用字符类型
- 小数类型禁止使用float和double
- 所有字段必须设置默认值且不允许为null
- 必须创建主键,最好是有序数值类型
- 快速判断是否存在某条记录
- in条件中数量不宜过多
- 禁止创建预留字段
- 单表索引数不要超过5个
MySQL分布式锁
42. 怎样使用MySQL实现分布式锁?
使用MySQL实现分布式锁比较简单,建一张表:
CREATE TABLE `distributed_lock` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`resource_name` varchar(200) NOT NULL DEFAULT '' COMMENT '资源名称(唯一索引)',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_resource_name` (`resource_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='分布式锁';
获取锁的时候,就插入一条记录。插入成功就代表获取到锁,插入失败就代表获取锁失败。
INSERT INTO distributed_lock (`resource_name`) VALUES ('资源1');
释放锁的时候,就删除这条记录。
DELETE FROM distributed_lock WHERE resource_name = '资源1';
实现比较简单,不过还不能用于实际生产中,有几个问题没有解决:
- 这把锁不支持阻塞,insert失败立即就返回了。当然可以用while循环直到插入成功,不过自旋也会占用CPU。
- 这把锁不是可重入的,已经获取到锁的线程再次插入也会失败,我们可以增加两列,一列记录获取到锁的节点和线程,另一列记录加锁次数。获取锁,次数加一,释放锁,次数减一,次数为零就删除这把锁。
- 这把锁没有过期时间,如果业务处理失败或者机器宕机,导致没有释放锁,锁就会一直存在,其他线程也无法获取到锁。我们可以增加一列锁过期时间,再启动一个异步任务扫描过期时间大于当前时间的锁就删除。
就是这么麻烦,我们看一下优化之后的锁变成什么样了:
CREATE TABLE `distributed_lock` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`resource_name` varchar(200) NOT NULL DEFAULT '' COMMENT '资源名称(唯一索引)',
`owner` varchar(200) NOT NULL DEFAULT '' COMMENT '锁持有者(机器码+线程名称)',
`lock_count` int NOT NULL DEFAULT '0' COMMENT '加锁次数',
`expire_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '锁过期时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_resource_name` (`resource_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='分布式锁';
这下应该完美了吧?不行,还有个问题:
业务逻辑没处理完,锁过期了怎么办?
假如我们设置锁过期时间是6秒,正常情况下业务逻辑可以在6秒内处理完成,但是当JVM发生FullGC或者调用第三方服务出现网络延迟,业务逻辑还没处理完,锁已经过期,被删掉,然后被其他线程获取到锁,岂不是要出问题?
这就引入了另一个知识点锁续期:
获取锁的同时,启动一个异步任务,每当业务执行到三分之一时间,也就是6秒中的第2秒的时候,就自动延长锁过期时间,继续延长到6秒,这样就能保证业务逻辑处理完成之前锁不会过期。
MySQL分库分表
43. 什么情况下需要分库?什么情况下需要分表?
- 当数据库的QPS过高,数据库连接数不足的时候,就需要分库。
- 当单表数据量过大,读写性能较差,就需要分表。
- 当两者都有的时候,就需要分库分表。
至于先分库还是先分表?建议先分表,如果分表能解决问题,就不需要分库了,毕竟需要单独服务器资源,成本更高。
44. 如何实现分库分表?有哪些拆分方式?
分库分表有垂直拆分和水平拆分。垂直拆分又有垂直分库、垂直分表。
垂直分库,不同的业务拆分到不同的数据库。
垂直分表,把长度较大或者访问频次较低的字段,拆分到扩展表中。
水平分表,单表数据量过大时,按照订单ID拆分到多张表中。
45. 分库分表引入哪些问题?
垂直分库: 不同库多表之间无法join关联查询,只能通过接口聚合,复杂度直线上升。 横跨多个数据库导致无法使用本地事务,数据强一致性就别想了,只能引入更为复杂的分布式事务,勉强实现数据的最终一致性,可用性直线下降。
垂直分表: 本来一张表能查出来的数据,现在需要多张表join关联查询,这不瞎耽误事。
水平分表: 多张表关联查询时,无法实现分页、排序功能。
46. 分库分表引入问题的解决方案?
跨库查询问题: 采用字段冗余方案,比如订单表存储店铺ID、店铺名称,就不需要再查询商户数据库了。 不过这种方案要求冗余字段要很少变动,就算变动后,也能容忍返回旧数据。
多表分页查询问题: 这个处理起来就很需要技术含量了。 比如:订单表按照订单ID分片,(order_id % 128),分成了128张表。 Leader看了说:每张表的数据量差不多,分的很均匀,以后不要再分了。
同一个用户的订单散落在不同的表,用户想查询自己的订单,根本无法做到分页查询。难道一次全部查询该用户的所有订单,然后做内存分页,多大的机器内存都让你搞挂。 想要实现用户订单分页查询,可以采用按照用户ID分片,(user_id % 128),这样同一个用户的订单只会存储在一张表中,咋分页展示都行。
没有完美的分片方案,如果商户想要分页查看自己店铺的订单怎么办? 那就把订单再冗余存储一份,按照店铺ID分片,(shop_id % 128)。不过由于商户数量较少,可以搞个异步线程往商户订单分片表同步。
订单按照用户ID分片后,发生数据倾斜怎么办? 因为不同用户的订单量是不同的,一个爱好购物的小姐姐的订单量抵得上几十个老爷们。导致一张表数据几百条,另一张表数据量千万级,这该咋整? 做冷热数据分离,基础库只存储3个月内的订单,其他的移动到历史订单库。这个要跟产品商量好,3个月前的订单需要单独的查询页面。
跨库事务问题: 这个问题就更复杂了。
下一个订单需要调用多个服务,只能使用分布式事务。 分布式事务的实现非常复杂,常用的有以下几种解决方案:
二阶段提交 TCC 本地消息表 MQ事务消息 分布式事务中间件