自己在准备面试/复习的时候,整理了一些高频面试题,如有错误欢迎指正哦。
1. B+树、B树、红黑树的特点和区别
B树(B-Tree)
一种平衡多路查找树,常用于数据库和文件系统的索引。
特点:
- 每个节点可以有多个子节点,称为多路平衡树。
- 每个节点最多可以有 m-1 个键,至少有 ceil(m / 2) - 1 个键。
- 所有叶子节点在同一层,保证了树的高度平衡。
- 中序遍历节点时,键值是有序的。
B+树(B+ Tree)
B树的变种,叶子节点采用双向链表相连,更适合范围查询,广泛的用于数据库索引。
特点:
- 所有的数据都存储在叶子节点。
- 非叶子节点只存储键,不存储数据,可以存储更多的子节点,树的高度比B树更低。
- 叶子节点通过双向链表相连,提高了范围查询效率。
红黑树(Red-Black Tree)
一种自平衡的二叉搜索树,常用于内存中的动态集合操作,如 Java 的 HashMap 。
特点:
- 它是一种自平衡二叉搜索树,节点只有红色和黑色。
- 根节点是黑色,每个红色节点的子节点必须是黑色。
- 从根节点到叶子的每条路径上必须有相同数量的黑色节点。
- 通过左旋、右旋操作保持平衡,插入、删除、查找操作平均时间复杂度为 O(log n) 。
三者区别
- 结构:1️⃣ B树和B+树是多路平衡树,而红黑树是二叉搜索树。2️⃣ B树和B+树的高度较低,红黑树高度平衡接近 log_2(n) 。
- 节点存储:1️⃣ B树所有节点存储数据。2️⃣ B+树的非叶子节点存储键,叶子节点存储数据,并通过双向链表相连。3️⃣ 红黑树的每个节点存储键和颜色标记。
- 查找效率:1️⃣ B树和B+树适合大规模数据的索引和范围查询。2️⃣ 红黑树适合内存中的动态集合操作。
应用场景
- B树和B+树由于其高度较低和适合磁盘存储的特性,常用于数据库和文件系统的索引。例如, MySQL 的 InnoDB 存储引擎使用B+树来实现其索引。
- 红黑树因其平衡性和动态操作效率,广泛应用于内存中的数据结构实现,如 Java 的 TreeMap 和TreeSet 、C++ 的 map 和 set 。
2. MySQL索引的原理
文章推荐:索引常见面试题
3. MySQL事务的隔离级别
-
读未提交(Read Uncommitted):
- 事务可以读取其他事务未提交的数据。
- 存在的问题:脏读(Dirty Read),即读取到其他事务尚未提交的更改。
- 最低的隔离级别,可能导致数据不一致。
-
读已提交(Read Committed):
- 事务只能读取其他事务已提交的数据。
- 解决了脏读问题,但存在不可重复读(Non-repeatable Read)问题,即同一个事务中两次读取同一数据,可能会得到不同的结果。
-
可重复读(Repeatable Read):
- 事务在开始时确定的数据读取范围在事务内保持一致。
- 解决了脏读和不可重复读的问题,但仍然存在幻读(Phantom Read)问题,即在同一个事务中,如果其他事务插入了新的数据,可能导致查询结果集的变化。
- MySQL InnoDB 存储引擎的默认隔离级别,通过 MVCC 减少了幻读现象。
-
可串行化(Serializable):
- 最高的隔离级别,通过强制事务顺序执行,完全避免脏读、不可重复读和幻读。
- 事务之间完全隔离,像是一个一个顺序执行的。
- 这种级别会带来显著的性能开销,因为它需要对涉及的表进行锁定。
拓展知识:MVCC 原理是什么?
4. MySQL索引失效的场景
-
使用不等于(!= 或 <>)操作符:
SELECT * FROM table_name WHERE column_name != 'value';
-
使用 IS NULL 或 IS NOT NULL:
- 索引对 IS NULL 和 IS NOT NULL 操作符的支持有限。
SELECT * FROM table_name WHERE column_name IS NULL;
-
使用函数或表达式:
- 如果在索引列上使用函数或表达式,索引将失效。
SELECT * FROM table_name WHERE UPPER(column_name) = 'VALUE';
-
数据类型不匹配:
- 查询条件的字段类型与索引列的类型不一致时,索引可能失效。
SELECT * FROM table_name WHERE column_name = 123; -- column_name 为字符串类型
-
使用前导通配符的 LIKE 查询:
- LIKE 查询中以通配符开头的模式会导致索引失效。
SELECT * FROM table_name WHERE column_name LIKE '%value';
-
使用 OR 连接的条件:
- 如果 OR 连接的条件中有一个未使用索引,整个查询将不使用索引。
SELECT * FROM table_name WHERE column1 = 'value1' OR column2 = 'value2';
-
范围查询后再进行排序:
- 在范围查询后对结果进行排序,索引可能失效。
SELECT * FROM table_name WHERE column_name > 'value' ORDER BY column_name;
-
复合索引的列顺序不匹配(最左匹配原则):
- 使用复合索引时,如果查询条件的列顺序与索引定义的顺序不一致,索引可能失效。
-- 复合索引 (column1, column2) SELECT * FROM table_name WHERE column2 = 'value'; -- 索引失效
-
更新操作导致的索引失效:
- 在频繁更新索引列的数据时,索引可能暂时失效。
UPDATE table_name SET column_name = 'new_value' WHERE column_name = 'old_value';
-
隐式类型转换:
- 当索引列进行隐式类型转换时,索引会失效。
SELECT * FROM table_name WHERE column_name = '123'; -- column_name 为整数类型
如何避免索引失效
-
避免使用函数或表达式:
- 在索引列上避免使用函数或表达式,可以通过在应用层进行计算来解决。
-
尽量避免前导通配符:
- 使用 LIKE 查询时,尽量避免前导通配符 % 。
-
确保数据类型匹配:
- 确保查询条件中的数据类型与索引列的数据类型一致。
-
合理使用复合索引:
- 创建符合查询条件的复合索引,并确保查询时的列顺序与索引定义的顺序一致(最左匹配原则)。
-
使用 UNION 代替 OR:
- 如果有多个 OR 条件,可以考虑使用 UNION 来重写查询。
SELECT * FROM users WHERE first_name = 'John' UNION SELECT * FROM users WHERE last_name = 'Doe';
推荐文章:小林Coding 防止索引失效
5. MySQL左连接和右连接的区别
- 左连接:左连接返回左表中的所有记录,以及右表中满足连接条件的记录。如果右表中没有匹配的记录,结果集中右表的字段将包含NULL。
- 右连接:右连接返回右表中的所有记录,以及左表中满足连接条件的记录。如果左表中没有匹配的记录,结果集中左表的字段将包含NULL。
主要区别
方向:左连接优先返回左表的所有记录,而右连接优先返回右表的所有记录。
NULL值的位置:在左连接中,如果没有匹配的右表记录,右表字段会是NULL;而在右连接中,如果没有匹配的左表记录,左表字段会是NULL。
示例
假设我们有两个表:students
和 courses
。
students
表
student_id | name | course_id |
---|---|---|
1 | Alice | 101 |
2 | Bob | 102 |
3 | Charlie | NULL |
4 | David | 103 |
courses
表
course_id | course_name |
---|---|
101 | Math |
102 | Science |
103 | History |
104 | Art |
左连接(LEFT JOIN)
SELECT students.name, courses.course_name
FROM students
LEFT JOIN courses
ON students.course_id = courses.course_id;
结果:
name | course_name |
---|---|
Alice | Math |
Bob | Science |
Charlie | NULL |
David | History |
解释:
- Alice、Bob 和 David 都有对应的课程,所以显示他们的课程名称。
- Charlie 没有课程(
course_id
为 NULL),因此course_name
为 NULL。 - 所有
students
表中的记录都出现在结果中,即使他们没有匹配的课程。
右连接(RIGHT JOIN)
SELECT students.name, courses.course_name
FROM students
RIGHT JOIN courses
ON students.course_id = courses.course_id;
结果:
name | course_name |
---|---|
Alice | Math |
Bob | Science |
David | History |
NULL | Art |
解释:
- Alice、Bob 和 David 都有对应的课程,所以显示他们的课程名称。
- 课程 Art 没有学生选修,所以
name
为 NULL。 - 所有
courses
表中的记录都出现在结果中,即使没有学生选修该课程。
6. MySQL慢查询优化
优化 MySQL 慢查询,常见的优化过程:
1. 确定慢查询
首先,需要找出哪些查询运行缓慢,启用 MySQL 慢查询日志:
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1; -- 将 long_query_time 设置为合适的阈值,以秒为单位
2. 分析慢查询日志
慢查询日志中包含了运行时间超过 long_query_time
阈值的所有查询。使用 mysqldumpslow
工具分析日志:
mysqldumpslow -s t /path/to/slow-query.log # -s t 表示按时间排序
3. 使用 EXPLAIN 分析查询
使用 EXPLAIN
命令来查看查询的执行计划:
EXPLAIN SELECT * FROM your_table WHERE condition;
会显示查询的执行计划,包括表扫描、索引使用情况等信息。
4. 优化查询语句
- 减少数据量:使用
LIMIT
限制返回行数。 - 使用合适的索引:确保查询使用了索引。例如,在查询条件的列上添加索引。
CREATE INDEX idx_column ON your_table(column);
- 避免全表扫描:通过优化
WHERE
子句来减少扫描行数。 - 优化 JOIN 操作:确保连接列有索引,并尽量减少连接的表数量。
5. 重构数据库结构
- 标准化和反规范化:根据需求选择适当的表设计。
- 分区:对于大表,可以考虑分区以提高查询性能。
CREATE TABLE your_table ( ... ) PARTITION BY RANGE (column) ( PARTITION p0 VALUES LESS THAN (1991), PARTITION p1 VALUES LESS THAN (1992), ... );
6. 监控和重复优化
持续监控数据库性能,定期分析慢查询日志和性能指标,进行持续的优化。
7. EXPLAIN分析查询后的关键列
1. select_type
- 解释:查询的类型,通常为
SIMPLE
(简单查询,没有子查询或联合)、PRIMARY
(最外层的查询)、SUBQUERY
(子查询)、DERIVED
(派生表,如子查询中的 FROM 子句)。 - 注意:了解查询的结构和复杂度。
2. type
- 解释:连接类型,表示 MySQL 查找表中行的方式。常见值包括:
ALL
:全表扫描,性能最差。index
:全索引扫描,稍好于 ALL。range
:索引范围扫描。ref
:使用非唯一索引扫描。eq_ref
:使用唯一索引扫描。const
/system
:常量表或系统表,性能最好。
- 注意:尽量避免
ALL
和index
类型,优选range
、ref
和eq_ref
。
3. key
- 解释:实际使用的索引。
- 注意:确认查询是否使用了合适的索引。
5. rows
- 解释:估计需要读取的行数。
- 注意:行数越少越好,查询的效率更高。
6. Extra
- 解释:提供了有关查询的额外信息。常见值包括:
Using where
:表示使用了 WHERE 子句来过滤行。Using index
:表示查询只使用了索引,通常性能较好。Using temporary
:表示查询需要使用临时表,可能影响性能。Using filesort
:表示需要排序操作,可能影响性能。
- 注意:尽量避免
Using temporary
和Using filesort
。
示例
EXPLAIN SELECT * FROM orders WHERE customer_id = 12345;
输出结果:
id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
---|---|---|---|---|---|---|---|---|---|---|
1 | SIMPLE | orders | ref | idx_customer | idx_customer | 4 | const | 10 | 100.00 | Using where |
这个结果中:
- type 是
ref
,表示使用了非唯一索引扫描,性能较好。 - key 是
idx_customer
,表示实际使用了customer_id
列上的索引。 - rows 是 10,表示估计需要读取 10 行,较少的行数表示查询较高效。
- Extra 显示
Using where
,表示使用了 WHERE 子句来过滤行。
8. Redis的五种数据结构
推荐文章:Redis 数据结构
9. 缓存穿透、缓存击穿、缓存雪崩
推荐文章:Redis 缓存设计
10. Redis实现分布式锁的原理
Redis实现分布式锁的原理主要依赖于其原子性操作和键过期机制。
1. 设置锁
加锁操作:使用Redis的SET
命令加锁。通过SET key value NX PX expire
命令来设置一个键值对,其中NX
表示只有当键不存在时才设置,PX expire
设置键的过期时间。例如:
SET lock_key unique_lock_value NX PX 30000
lock_key
:锁的名称。unique_lock_value
:锁的唯一标识(例如,客户端ID)。NX
:只有当键不存在时才会设置成功。PX 30000
:锁的过期时间为30秒。
2. 检查锁
获取锁:客户端尝试使用SET
命令获取锁,如果成功,则表示获得锁;如果失败,则表示锁已被其他客户端持有。
3. 释放锁
释放锁操作:释放锁时需要确保锁是由当前持有者释放的。通常使用DEL
命令删除键,但为了防止误删除其他客户端持有的锁,通常结合Lua
脚本进行验证:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
KEYS[1]
:锁的键(lock_key
)。
ARGV[1]
:锁的唯一标识(unique_lock_value
)。
脚本确保只有当锁的唯一标识与当前持有者匹配时才会删除锁。
4. 处理锁过期
锁超时:设置锁时的过期时间可以防止因为客户端故障或其他异常情况导致锁永远不会释放。如果锁的持有者在过期时间内没有释放锁,Redis 会自动删除该锁。
11. 如何保证Redis与MySQL的数据一致性
-
延时双删策略:
- 先删除缓存,再更新数据库,接着设置一个短暂的延时(如500ms),再次删除缓存。这种方法可以减少并发请求带来的数据不一致问题。
-
先写数据库,再写缓存:
- 先将数据写入MySQL数据库,然后再更新Redis缓存。这种方法可以确保数据库中的数据是最新的,但在高并发情况下可能会导致缓存失效。
-
使用消息队列:
- 通过消息队列(如Kafka、RabbitMQ)来异步更新Redis。应用程序先将数据写入MySQL,然后将更新操作发送到消息队列,消费者从队列中读取消息并更新Redis。这种方法可以提高系统的可靠性和一致性。
-
缓存失效策略:
- 在更新MySQL数据后,立即使相关的Redis缓存失效。这样可以确保下次读取时从MySQL获取最新数据并更新缓存。
-
事务机制:
- 使用分布式事务(如两阶段提交协议)来保证Redis和MySQL的操作要么全部成功,要么全部失败。这种方法实现复杂且性能开销较大,但可以提供强一致性。
-
数据对比与修复:
- 定期对比Redis和MySQL中的数据,发现不一致时进行修复。这种方法适用于对一致性要求不高的场景。
12. Redis的持久化、Redis的分片集群、Redis的大key和热key
推荐文章:
○为什么有Redis后还需要设置本地缓存
- 降低延迟:本地缓存(如 Guava)存储在应用服务器的内存中,访问速度比通过网络访问 Redis 更快。对于频繁访问的数据,本地缓存可以显著降低延迟。
- 减少网络开销:每次访问 Redis 都需要通过网络,这会增加网络流量和延迟。使用本地缓存可以减少对 Redis 的请求次数,降低了网络开销。
- 减轻 Redis 负载:如果所有请求都直接访问 Redis,Redis 可能会成为瓶颈。通过使用本地缓存,可以减少对 Redis 的访问频率,减轻了 Redis 的负载。
- 提高容错能力:在 Redis 不可用或网络故障的情况下,本地缓存可以作为数据的临时存储,保证应用在短时间内仍然能够正常运行,增强系统的容错能力。
- 缓存层次结构:使用多级缓存(如本地缓存+Redis)可以形成缓存层次结构。应用首先查询本地缓存,如果未命中,再查询 Redis。如果 Redis 也未命中,再查询数据库。这种策略可以优化缓存命中率。
- 数据隔离:在某些情况下,不同的应用实例可能需要隔离的数据副本。使用本地缓存可以确保每个应用实例都有自己的缓存数据副本,避免相互干扰。
具体使用场景
- 高频访问的数据:对于访问频率极高且数据更新较少的场景,本地缓存非常适用。
- 低延迟需求:对响应时间要求极高的场景,本地缓存可以提供更快的访问速度。
- 分布式系统:在大型分布式系统中,结合本地缓存和 Redis 可以优化整体性能和资源使用。
13. MQ如何保证消息不丢失
消息队列(MQ)在分布式系统中,保证消息不丢失常见的方法有:
-
消息持久化:
- 将消息持久化到磁盘上,即使系统崩溃或重启,消息也不会丢失。例如,RabbitMQ可以通过设置消息的持久化属性来实现这一点。
-
确认机制(Acknowledgment):
- 消息生产者和消费者都可以使用确认机制来确保消息被成功处理。生产者在发送消息后等待确认,消费者在处理完消息后发送确认。例如,Kafka使用ACK机制来确保消息被所有副本成功写入。
-
事务机制:
- 使用事务来保证消息的原子性操作,即消息的发送和处理要么全部成功,要么全部失败。例如,RabbitMQ支持事务模式,可以在一个事务中发送多条消息,确保它们要么全部成功,要么全部失败。
-
重试机制:
- 在消息发送失败时,系统可以自动重试发送,直到成功为止。这种机制可以有效减少由于网络或临时故障导致的消息丢失。
-
死信队列(Dead Letter Queue, DLQ):
- 当消息处理失败达到一定次数后,可以将消息发送到死信队列进行特殊处理,避免消息丢失。
14. MQ如何保证消息有序
-
单队列:
- 在单队列模式下,所有消息都被放入同一个队列中,消费者按照先进先出的顺序(FIFO)处理消息。这样可以确保消息的顺序性,但它的吞吐量可能会受到限制,因为单个队列可能成为瓶颈。
-
分区和键:
- 一些MQ系统(例如Apache Kafka)通过分区(partition)和键(key)来保证消息的顺序性。消息被分配到不同的分区中,并且在同一个分区内,消息是有序的。通过使用键,可以确保具有相同键的消息总是被发送到同一个分区,从而保证这些消息的顺序性。
-
消息分组:
- 某些MQ系统允许将消息分组(grouping),并通过分组来保证顺序性。例如,RabbitMQ可以通过使用消息属性(如消息ID)将消息分组,消费者可以按照分组顺序处理消息。
-
消息ID和去重:
- 在某些场景下,可以通过为每个消息分配唯一的消息ID,并在消费者侧进行去重和排序来保证顺序性。这种方法需要额外的逻辑来处理重复消息和排序,但可以在多消费者的情况下保持消息顺序。
-
事务性消息:
- 一些MQ系统支持事务性消息(transactional messages),允许生产者在事务范围内发送一组消息,确保这些消息被消费者按顺序处理。例如,Kafka的事务性消息可以保证在同一个事务内的消息被按顺序处理。
-
顺序消费:
- 某些MQ系统(如RocketMQ)支持顺序消费模式,消费者按照消息的生产顺序进行消费。这种模式通常需要在消费者端进行相应的配置和处理,以确保消息的顺序性。
常见MQ系统中的实现
Apache Kafka
Kafka通过分区来实现消息的顺序性。生产者可以指定一个键,使得具有相同键的消息被发送到同一个分区。消费者在读取分区中的消息时,会按照消息的生产顺序进行处理。
RabbitMQ
RabbitMQ可以通过使用队列和绑定键来实现消息的顺序性。消费者可以订阅特定的队列,并按照队列中的消息顺序进行消费。通过合理的队列设计,可以保证消息的顺序性。
RocketMQ
RocketMQ支持顺序消息。在生产者端,消息可以按照顺序发送到特定的队列。在消费者端,可以通过顺序消费模式确保消息的顺序性。
15. MQ如何保证高性能
-
分区和分片:
- 通过分区(partitioning)或分片(sharding),可以将消息分散到多个物理节点或逻辑分区上。这种方法不仅可以提高并行处理能力,还可以避免单点瓶颈。例如,Apache Kafka使用分区来实现高吞吐量,多个消费者可以并行处理不同分区的消息。
-
异步处理:
- 异步处理可以显著提高消息处理的效率。生产者和消费者都可以采用异步发送和接收消息的方式,避免同步等待。这样可以充分利用系统资源,提高并发处理能力。
-
批量处理:
- 批量处理可以有效减少网络开销和I/O操作的频率。生产者可以将多条消息批量发送到MQ系统,消费者也可以批量消费消息。Kafka和RabbitMQ等系统都支持批量操作。
-
持久化优化:
- 消息的持久化操作会影响系统性能。采用高效的存储引擎和优化I/O操作可以提高持久化性能。Kafka使用顺序写入日志文件和零拷贝技术,显著提升了持久化性能。
-
内存缓存:
- 通过使用内存缓存,可以减少对磁盘I/O的依赖,提高消息处理速度。消息可以先写入内存缓存,之后再异步写入磁盘。Redis等基于内存的消息队列可以提供高吞吐量和低延迟。
-
水平扩展:
- 通过水平扩展,可以增加系统的处理能力。增加更多的节点来分担负载,确保系统在高并发场景下依然能够保持高性能。Kafka、RabbitMQ和RocketMQ等系统都支持水平扩展。
-
流控和限流:
- 流控和限流可以防止系统过载,确保系统在高负载下依然稳定运行。通过设置生产者和消费者的速率限制,可以平衡系统负载,避免瓶颈。
-
高效的网络协议:
- 采用高效的网络协议可以减少网络开销,提高数据传输效率。Kafka使用高效的二进制协议,RabbitMQ支持AMQP协议,这些协议都经过优化,以减少网络延迟和开销。
-
分布式架构:
- 分布式架构可以提高系统的可扩展性和容错能力。通过将消息队列分布在多个节点上,可以提高系统的整体性能和可靠性。
常见MQ系统的高性能特性
Apache Kafka
- 分区机制:将消息分布到多个分区,支持高并发消费。
- 顺序写入和零拷贝:优化磁盘I/O操作,提高写入性能。
- 批量处理:支持批量发送和消费消息,减少网络和I/O开销。
RabbitMQ
- 内存缓存:使用内存缓存提高消息处理速度。
- 流控机制:支持流控和限流,避免系统过载。
- 插件扩展:通过插件机制扩展功能,提高系统性能。
RocketMQ
- 分区和分片:支持分区和分片,提高并行处理能力。
- 批量处理:支持批量发送和消费消息。
- 高效存储:优化存储引擎,提高持久化性能。
16. MQ消息堆积的解决方法
在消息队列中,当遇到消息堆积问题时,可以从两个主要方面解决:生产者生产速度过快或消费者消费速度过慢。
-
生产者生产速度过快:
- 限流降级:对生产者进行限流,控制生产速率。
- 水平扩展:增加多个消费者实例,提高整体消费能力,匹配生产的增加。
-
消费者消费速度过慢:
- 错误检查:检查消费者是否出现了大量消费错误或线程卡死,查看日志以识别资源锁问题。
- 增加消费者实例:增加消费者实例可以提高消费能力,但需要注意每个主题的队列数量也需增加。例如在 RocketMQ 中,一个队列只能被一个消费者实例消费。
总之,最快速的解决方案是增加消费者实例,同时增加每个主题的队列数量,以避免出现单个队列被过多消费者实例竞争的情况。
推荐文章:消息堆积的解决方法