精心整理46道MySQL高频面试题及答案,轻松秒杀面试官

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情

最近陆陆续续分享了十几篇MySQL高频面试问题,算是告一段落,今日做个汇总。

本文精心整理上万字,几十张图,46道高频面试问题。

通俗易懂,内容详实,干货满满,信息量巨大,值得多刷。

别问我为什么总结的这么详细,这么多干货,还无私分享出来?

问就是雷锋精神。

欢迎点赞评论收藏关注,我会持续分享更多技术干货。

MySQL基础架构

1. MySQL底层架构体系及每层作用?

image-20220717185949293.png

由图中可以看到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='用户表';

image-20220504221759177.png

image-20220504221847348.png

4. 什么是二叉查找树?

  1. 若左子树不空,则左子树上所有结点的值均小于它的根结点的值;
  2. 若右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  3. 左、右子树也分别为二叉查找树;

二叉搜索树查找数据的时间复杂度是O(logN),如图所示,最多查找3次就可以查到所需数据。

image-20220503001843598.png

极端情况下,二叉查找树可能退化成线性链表。

image-20220503002832808.png

5. 什么是红黑树?

  1. 结点是红色或黑色
  2. 根结点是黑色
  3. 所有叶子都是黑色(叶子是NIL结点)
  4. 每个红色结点的两个子结点都是黑色(从每个叶子到根的所有路径上不能有两个连续的红色结点)
  5. 从任一结点到其每个叶子的所有路径都包含相同数目的黑色结点

image-20220503101640833.png

红黑树的优点: 限制了左右子树的树高,不会相差过大。

缺点: 规则复杂,一般人想要弄懂这玩意儿,就已经很费劲了,更别说使用了。

6. 什么是B树?

对于一个m阶的B树:

  1. 根节点至少有2个子节点
  2. 每个中间节点都包含k-1个元素和k个子节点,其中 m/2 <= k <= m
  3. 每个叶子节点都包含k-1个元素,其中 m/2 <= k <= m
  4. 中间节点的元素按照升序排列
  5. 所有的叶子结点都位于同一层

image-20220503105555414.png

B树这样的设计有哪些优点呢?

高度更低,每个节点含有多个元素,查找的时候一次可以把一个节点中的所有元素加载到内存中作比较,两种改进都大大减少了磁盘IO次数。

7. 什么是B+树?

相比较B树,B+树又做了如下约定:

  1. 有k个子节点的中间节点就有k个元素(B树中是k-1个元素),也就是子节点数量 = 元素数量。 每个元素不保存数据,只用来索引,所有数据都保存在叶子节点。

  2. 所有的中间节点元素都同时存在于子节点,在子节点元素中是最大(或最小)元素。

  3. 非叶子节点只保存索引,不保存数据。(B树中两者都保存)

  4. 叶子结点包含了全部元素的信息,并且叶子结点按照元素大小组成有序列表。

image-20220503163817637.png

B+树这样设计有什么优点呢?

  1. 每个节点存储的元素更多,看起来比B树更矮胖,导致磁盘IO次数更少。
  2. 非叶子节点不存储数据,只存储索引,叶子节点存储全部数据。 这样设计导致每次查找都会查到叶子节点,效率更稳定,便于做性能优化。
  3. 叶子节点之间使用有序链表连接。 这样设计方便范围查找,只需要遍历链表中相邻元素即可,不再需要二次遍历二叉树。

8. MySQL索引底层实现为什么要用B+树结构?

看完了二叉树查找树、红黑树、B树、B+树的底层实现原理,就明白了B树和B+树就是为了文件检索系统设计的,更适合做索引结构。

B+树的优点是:

  1. 每个节点存储的元素更多,看起来比B树更矮胖,导致磁盘IO次数更少。
  2. 非叶子节点不存储数据,只存储索引,叶子节点存储全部数据。 这样设计导致每次查找都会查到叶子节点,效率更稳定,便于做性能优化。
  3. 叶子节点之间使用有序链表连接。 这样设计方便范围查找,只需要遍历链表中相邻元素即可,不再需要二次遍历二叉树。

MySQL索引

9. 联合索引的底层存储结构是什么样的?

在age和name字段建一个联合索引(age,name),它的存储结构就是这样:

image-20220504220728526.png

联合索引的优点:可以快速匹配到所需数据,大大减少扫描行数。

10. 什么是最左匹配原则?

最左匹配原则是指在建立联合索引的时候,遵循最左优先,以最左边的为起点任何连续的索引都能匹配上。

当我们在(age,name)上建立联合索引的时候,where条件中只有age可以用到索引,同时有age和name也可以用到索引。但是只有name的时候是无法用到索引的。

看上面的图,就理解了,(age,name)的联合索引,是先按照age排序,age相等的行再按照name排序。如果where条件只有一个name,当然无法用到索引。

11. 什么是覆盖索引和回表查询?

在使用非聚簇索引查询的时候,叶子节点中已经有了所需结果,无需再查询主键索引,这时候就是用到了覆盖索引。

二次查询主键索引的行为就是回表查询。

image-20220504221929024.png

当我们在age上建索引的时候,查询SQL是这样的时候:

select id from user where age = 18;

就会用到覆盖索引,因为ID字段我们使用age索引的时候已经查出来,不需要再二次回表查询了。

12. 怎么创建联合索引,查询效率最高?

创建联合索引时,区分度高的字段放前面。

这样可以减少查询次数,更快地匹配到所需数据。

比如对于一张用户表来说,生日比性别的区分度更高,更适合创建索引。

可以使用下面的方式手动统计一下,每个字段的区分度,值越大,区分度越高:

select 
    count(distinct birthday)/count(*), 
    count(distinct gender)/count(*) 
from user;

image-20220730230017044.png

对于已经创建好的索引,我们还可以使用MySQL命令查看每个索引的区分度排名:

image-20220730230358758.png

图中Cardinality列表示索引的区分度排名,也被称为基数。

13. 什么是索引下推?优点是什么?

在(age,name)上面建联合索引,并且查询SQL是这样的时候:

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

image-20220504224330386.png

如果没有索引下推,会先匹配出 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 的过程:

image-20220615233558509.png

  1. 从磁盘加载数据到内存
  2. 在内存中修改数据
  3. 把新数据写到Redo Log Buffer
  4. Redo Log Buffer中数据持久化到Redo Log文件中
  5. Redo Log文件中数据持久化到数据库磁盘中

17. MySQL Undo Log日志作用及实现原理?

Undo Log记录的是逻辑日志,也就是SQL语句。

比如:当我们执行一条insert语句时,Undo Log就记录一条相反的delete语句。

作用:

  1. 回滚事务时,恢复到修改前的数据。
  2. 实现 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之后的事务流程:

image-20220615233733690.png

这就是二阶段提交的概念,先写处于prepare状态的Redo Log,事务提交后,再写处于commit状态的Redo Log。

19.MySQL MVCC(多版本并发控制)作用及实现原理?

MVCC(MultiVersion Concurrency Control)就是通过一定机制生成一个数据请求时间点的一致性数据快照,并用这个快照来提供一定级别(语句级或事务级)的一致性读取。

记录的是某个时间点上的数据快照,用来实现不同事务之间数据的隔离性。

MVCC的实现方式通过两个隐藏列trx_id(最近一次提交事务的ID)和roll_pointer(上个版本的地址),建立一个版本链。并在事务中读取的时候生成一个ReadView(读视图),在Read Committed隔离级别下,每次读取都会生成一个读视图,而在Repeatable Read隔离级别下,只会在第一次读取时生成一个读视图。

image-20220515235126685.png

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;

什么情况下需要用到表锁?

  1. 当需要更新表中的大部分数据
  2. 事务涉及到多张表,业务逻辑复杂,加表锁可以避免死锁。

页面锁:

优点:开销和加锁速度介于表锁和行锁之间。

缺点:会出现死锁,锁定粒度介于表锁和行锁之间,并发度一般。

目前只有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)实现的。

适用于读多写少的场景。

加锁方式:

  1. 读取version

    select id,name,age,version from user id=1;
    
  2. 更新数据,判断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. 不同索引之间的加锁范围解析?

  1. MySQL锁是加在索引记录上面的。

  2. 如果是非唯一性索引,不论表中是否存在该记录,除了会对该记录所在范围加锁,还会向右遍历到不满足条件的范围进行加锁。

  3. 如果是唯一索引,如果表中存在该记录,只对该行记录加锁。如果表中不存在该记录,除了会对该记录所在范围加锁,还会向右遍历到不满足条件的范围进行加锁。

详情请查看:一条update语句加了多少锁?

30. 如何查看死锁日志,并手把手解决线上MySQL死锁问题?

通过MySQL查看最近一次的死锁日志:

show engine innodb status;

死锁日志.png

在死锁日志中,可以清楚地看到这两条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主动同步的作用及实现原理?

作用是:

  1. 读写分离,提升数据库性能
  2. 容灾恢复,主服务器不可用时,从服务器提供服务,提高可用性
  3. 冗余备份,主服务器数据损坏丢失,从服务器保留备份

MySQL主从同步是基于Bin Log实现的,而Bin Log记录的是原始SQL语句。

主从同步的过程:

image-20220623221744177.png

  1. 当主库数据发生变更时,写入本地Bin Log文件

  2. 从库IO线程发起dump主库Bin Log文件的请求

  3. 主库IO线程推送Bin Log文件到从库中

  4. 从库IO线程把Bin Log内容写入本地的Relay Log文件中

  5. 从库SQL线程读取Relay Log文件内容

  6. 从库SQL线程重新执行一遍SQL语句

32. MySQL主动同步延迟问题产生原因?

  1. 从库机器性能较差

    主库负责所有读写请求,从库只用来备份,会用性能较差的机器,执行时间自然较慢。

  2. 从库压力更大

    读写分离后,主库负责写请求,从库负责读请求。

    互联网应用一般读请求更多,所以从库读压力更大,占用更多CPU资源。

  3. 网络延迟

    当主库的Bin Log文件往从库上发送时,可能产生网络延迟,也会导致从库数据跟不上。

  4. 主库有大事务

    当主库上有个大事务需要执行5分钟,把Bin Log文件发送到从库,从库至少也需要执行5分钟,所以这时候从库就出现了5分钟的延迟。

33. MySQL主动同步延迟的解决方案?

  1. 从库机器性能较差

    把从库换成跟主库同等规格的机器。

  2. 从库压力更大

    多搞几台从库,分担读请求压力。

  3. 网络延迟

    联系运维或者云服务提供商解决。

  4. 主库有大事务

    把大事务分割成小事务执行,大事务不但会产生从库延迟,还可能产生死锁,降低数据库并发性能,所以尽量少用大事务。

34. 如何提升MySQL主从同步的性能?

  1. 从库开启多线程复制

就是在主从同步的最后两步使用多线程,修改配置 slave_parallel_workers=4,代表开启4个复制线程。

image-20220623222719256.png

  1. 修改同步模式,改为异步

主从同步共有三种复制方式:

  1. 全同步复制

    当主库执行完一个事务,并且所有从库都执行完该事务后,才给客户端返回成功。

  2. 半同步复制

    至少有一个从库执行完成后,就给客户端返回成功。

  3. 异步复制

    主库执行完后,立即返回成功,不关心从库是否执行完成。

如果对数据安全性要求没那么高,可以把同步模式改成半同步复制或者异步复制。

  1. 修改从库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索引失效场景?

  1. 数据类型隐式转换
  2. 模糊查询 like 以%开头
  3. or前后没有同时使用索引
  4. 联合索引,没有使用第一列索引
  5. 在索引字段进行计算操作
  6. 在索引字段字段上使用函数
  7. 优化器选错索引

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;
  1. 使用子查询

    先用子查询查出符合条件的主键,再用主键ID做条件查出所有字段。

    select * from user 
    where id in (
      select id from user 
      where create_time>'2022-07-03' 
      limit 100000,10
    );
    
  2. 使用分页游标

实现方式就是:当我们查询第二页的时候,把第一页的查询结果放到第二页的查询条件中。

例如:首先查询第一页

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)使用详解?

image-20220727213040374.png

详情请查看:学会使用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. 哪些字段适合创建索引?

  1. 频繁查询的字段
  2. 在where和on条件出现的字段
  3. 区分度高的字段
  4. 有序的数值类型字段

39. 哪些字段不适合创建索引?

  1. 区分度低的字段
  2. 频繁更新的字段
  3. 过长的字段
  4. 无序的字段

40. 创建索引的注意事项?

  1. 优先使用联合索引
  2. 使用联合索引时,区分度的字段放前面
  3. 过长字符串可以使用前缀索引
  4. 值唯一的字段,使用唯一索引
  5. 避免创建过多索引

41. 精心总结MySQL开发16条规范?

  1. 禁止使用select *
  2. 用小表驱动大表
  3. join关联表不宜过多
  4. 禁止使用左模糊或者全模糊查询
  5. 索引访问类型至少达到range级别
  6. 更优雅的使用联合索引
  7. 注意避免深分页
  8. 单表字段不要超过30个
  9. 枚举字段不要使用字符类型
  10. 小数类型禁止使用float和double
  11. 所有字段必须设置默认值且不允许为null
  12. 必须创建主键,最好是有序数值类型
  13. 快速判断是否存在某条记录
  14. in条件中数量不宜过多
  15. 禁止创建预留字段
  16. 单表索引数不要超过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';

实现比较简单,不过还不能用于实际生产中,有几个问题没有解决:

  1. 这把锁不支持阻塞,insert失败立即就返回了。当然可以用while循环直到插入成功,不过自旋也会占用CPU。
  2. 这把锁不是可重入的,已经获取到锁的线程再次插入也会失败,我们可以增加两列,一列记录获取到锁的节点和线程,另一列记录加锁次数。获取锁,次数加一,释放锁,次数减一,次数为零就删除这把锁。
  3. 这把锁没有过期时间,如果业务处理失败或者机器宕机,导致没有释放锁,锁就会一直存在,其他线程也无法获取到锁。我们可以增加一列锁过期时间,再启动一个异步任务扫描过期时间大于当前时间的锁就删除。

就是这么麻烦,我们看一下优化之后的锁变成什么样了:

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. 什么情况下需要分库?什么情况下需要分表?

  1. 当数据库的QPS过高,数据库连接数不足的时候,就需要分库。
  2. 当单表数据量过大,读写性能较差,就需要分表。
  3. 当两者都有的时候,就需要分库分表。

至于先分库还是先分表?建议先分表,如果分表能解决问题,就不需要分库了,毕竟需要单独服务器资源,成本更高。

44. 如何实现分库分表?有哪些拆分方式?

分库分表有垂直拆分和水平拆分。垂直拆分又有垂直分库、垂直分表。

垂直分库,不同的业务拆分到不同的数据库。

image-20220106200122551.png

垂直分表,把长度较大或者访问频次较低的字段,拆分到扩展表中。

水平分表,单表数据量过大时,按照订单ID拆分到多张表中。

image-20220106201936851.png

45. 分库分表引入哪些问题?

垂直分库: 不同库多表之间无法join关联查询,只能通过接口聚合,复杂度直线上升。 横跨多个数据库导致无法使用本地事务,数据强一致性就别想了,只能引入更为复杂的分布式事务,勉强实现数据的最终一致性,可用性直线下降。

垂直分表: 本来一张表能查出来的数据,现在需要多张表join关联查询,这不瞎耽误事。

水平分表: 多张表关联查询时,无法实现分页、排序功能。

46. 分库分表引入问题的解决方案?

跨库查询问题: 采用字段冗余方案,比如订单表存储店铺ID、店铺名称,就不需要再查询商户数据库了。 不过这种方案要求冗余字段要很少变动,就算变动后,也能容忍返回旧数据。

多表分页查询问题: 这个处理起来就很需要技术含量了。 比如:订单表按照订单ID分片,(order_id % 128),分成了128张表。 Leader看了说:每张表的数据量差不多,分的很均匀,以后不要再分了。

同一个用户的订单散落在不同的表,用户想查询自己的订单,根本无法做到分页查询。难道一次全部查询该用户的所有订单,然后做内存分页,多大的机器内存都让你搞挂。 想要实现用户订单分页查询,可以采用按照用户ID分片,(user_id % 128),这样同一个用户的订单只会存储在一张表中,咋分页展示都行。

没有完美的分片方案,如果商户想要分页查看自己店铺的订单怎么办? 那就把订单再冗余存储一份,按照店铺ID分片,(shop_id % 128)。不过由于商户数量较少,可以搞个异步线程往商户订单分片表同步。

订单按照用户ID分片后,发生数据倾斜怎么办? 因为不同用户的订单量是不同的,一个爱好购物的小姐姐的订单量抵得上几十个老爷们。导致一张表数据几百条,另一张表数据量千万级,这该咋整? 做冷热数据分离,基础库只存储3个月内的订单,其他的移动到历史订单库。这个要跟产品商量好,3个月前的订单需要单独的查询页面。

跨库事务问题: 这个问题就更复杂了。

image-20220107205351657.png

下一个订单需要调用多个服务,只能使用分布式事务。 分布式事务的实现非常复杂,常用的有以下几种解决方案:

二阶段提交 TCC 本地消息表 MQ事务消息 分布式事务中间件

猜你喜欢

转载自juejin.im/post/7126918029060866079