feat: add interview-prep topic group with 250 questions (6 subtopics, 5 question types)
Deploy Examination / deploy (push) Successful in 10s

Subtopics:
- distributed-microservice: 45 questions (分布式微服务架构)
- message-queue: 45 questions (消息队列)
- k8s-observability: 45 questions (K8s与可观测性)
- go-java-concurrency: 45 questions (Go/Java并发模型)
- database-advanced: 35 questions (数据库进阶)
- ai-engineering: 35 questions (AI工程实践)

Question types: single_choice, true_false, fill_blank, short_answer, code_reading
This commit is contained in:
2026-09-09 16:36:27 +08:00
parent 515acbcc7f
commit 0f68a64829
38 changed files with 6835 additions and 2 deletions
@@ -0,0 +1,309 @@
{
"topic": "database-advanced",
"type": "code_reading",
"schema_version": "1.0.0",
"generated": "2026-09-09T00:00:00+08:00",
"questions": [
{
"id": "cr-001",
"type": "code_reading",
"difficulty": 4,
"tags": [
"mysql",
"bplus-tree",
"composite-index",
"covering-index",
"index"
],
"question": "分析以下建表语句和查询,判断索引使用情况",
"code": "CREATE TABLE orders (\n id BIGINT PRIMARY KEY AUTO_INCREMENT,\n user_id INT NOT NULL,\n product_id INT NOT NULL,\n status TINYINT DEFAULT 0,\n amount DECIMAL(10,2),\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n INDEX idx_user_product_status (user_id, product_id, status)\n) ENGINE=InnoDB;\n\n-- 查询 Q1\nSELECT user_id, product_id, status FROM orders\nWHERE user_id = 100 AND product_id = 500;\n\n-- 查询 Q2\nSELECT user_id, product_id, status, amount FROM orders\nWHERE product_id = 500 AND user_id = 100;\n\n-- 查询 Q3\nSELECT user_id, product_id, status FROM orders\nWHERE user_id = 100 AND status = 1;",
"language": "sql",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "查询 Q1 中,MySQL 优化器能否将 WHERE 条件顺序自动调整以命中联合索引 (user_id, product_id, status) 的最左前缀?",
"options": {
"A": "不能,WHERE 子句顺序必须与索引列顺序完全一致",
"B": "能,优化器会根据 WHERE 条件自动匹配索引列,不受书写顺序影响",
"C": "不能,MySQL 只会按索引从左到右扫描,不处理 WHERE 顺序",
"D": "取决于 SQL 模式 setting"
},
"answer": "B",
"explanation": "MySQL 优化器会分析 WHERE 子句中的等值条件,自动将 user_id=100 和 product_id=500 与联合索引的前两列匹配,不受 SQL 中书写顺序的影响。Q1 的两个等值条件恰好连续覆盖索引前两列 (user_id, product_id),因此可以完全命中索引前缀,type 为 ref。"
},
{
"index": 2,
"type": "short_answer",
"question": "Q2 与 Q1 的查询条件相同(只是书写顺序不同),且 Q2 多查了 amount 列。请分析:(a) Q2 的索引扫描类型是什么?(b) Q1 是覆盖索引查询吗?Q2 呢?",
"answer": "(a) Q2 的扫描类型是 ref,与 Q1 相同。(b) Q1 是覆盖索引查询——其 SELECT 的列 user_id、product_id、status 全部包含在 idx_user_product_status 中,无需回表。Q2 不是覆盖索引查询——amount 不在索引中,需要通过聚簇索引回表取值。",
"keywords": [
"ref",
"覆盖索引",
"回表",
"Extra: Using index"
],
"scoring_rubric": "Q2 扫描类型正确得1分,Q1 覆盖索引判断正确得1分,Q2 不是覆盖索引并解释回表得1分",
"explanation": "虽然 WHERE 子句中 product_id 写在 user_id 前面,但 MySQL 优化器会自动调整匹配顺序。Q1 的 SELECT 列 (user_id, product_id, status) 全在联合索引中,EXPLAIN 的 Extra 会显示 'Using index'(覆盖索引)。Q2 多了 amount 列,不在索引中,必须回表到聚簇索引获取数据,Extra 显示 'Using index condition' 或无 Using index。"
},
{
"index": 3,
"type": "short_answer",
"question": "查询 Q3 的 WHERE 条件跳过了联合索引的第二列 product_id(只有 user_id=100 和 status=1),MySQL 能否同时利用索引中的 user_id 和 status 列?请分析其索引使用情况。",
"answer": "MySQL 只能利用索引的第一列 user_id 进行范围扫描(type=ref),将 user_id=100 的所有行取出来后,在 server 层对 status=1 做过滤。索引的第三列 status 无法直接用于索引查找,因为跳过了第二列 product_id,违反了最左前缀原则。",
"keywords": [
"最左前缀",
"索引截断",
"server 层过滤"
],
"scoring_rubric": "提到只利用 user_id 列得1分,提到最左前缀原则得1分,提到需要 server 层过滤得1分",
"explanation": "联合索引 (user_id, product_id, status) 要求从最左列开始连续使用。Q3 的条件 user_id=100 AND status=1 中间跳过了 product_id,MySQL 只能使用索引的 user_id 部分进行查找,EXPLAIN 中 key_len 只反映 user_id 的长度(4 字节 INT)。剩余的 status=1 条件在 server 层通过 'Using where' 过滤,而非在存储引擎层通过索引过滤。如果 status 选择性很高,可以考虑创建 idx_user_status(user_id, status) 辅助索引。"
}
],
"explanation": "本题考察 B+树联合索引的三个核心概念:(1) 最左前缀原则——索引列必须从左到右连续使用,但 WHERE 条件的书写顺序不影响优化器匹配;(2) 覆盖索引——当查询列全部包含在索引中时,可避免回表,性能最优;(3) 索引列跳跃——跳过中间列会导致后续列无法参与索引查找,只能退化为 server 层过滤。InnoDB 的 B+树二级索引叶子节点存储的是主键值,非覆盖索引查询必须通过主键回表到聚簇索引获取完整行数据。",
"source": null,
"related": []
},
{
"id": "cr-002",
"type": "code_reading",
"difficulty": 3,
"tags": [
"mysql",
"explain",
"index"
],
"question": "分析以下 EXPLAIN 执行计划输出,判断查询性能",
"code": "EXPLAIN SELECT o.id, o.amount, u.name\nFROM orders o\nJOIN users u ON o.user_id = u.id\nWHERE o.status = 1\n AND o.created_at >= '2026-01-01'\n AND u.city = 'Beijing';\n\n-- 输出结果(简化):\n+----+------+-------+------------------+---------+------+----------+-------+\n| id | type | table | possible_keys | key | rows | filtered | Extra |\n+----+------+-------+------------------+---------+------+----------+-------+\n| 1 | ALL | o | NULL | NULL | 500K | 10.00 | Using where |\n| 1 | ref | u | PRIMARY | PRIMARY | 1 | 25.00 | Using where |\n+----+------+-------+------------------+---------+------+----------+-------+",
"language": "sql",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"options": {
"A": "驱动表是 u,被驱动表是 o,因为 u 的 rows=1 更小",
"B": "驱动表是 o,被驱动表是 u,优化器选择了 o 作为驱动表",
"C": "无法确定驱动表,取决于 MySQL 版本",
"D": "两张表同时扫描,不存在驱动/被驱动关系"
},
"question": "根据 EXPLAIN 输出,哪张表是驱动表(第一张被访问的表)?",
"answer": "B",
"explanation": "EXPLAIN 输出中 id=1 相同的情况下,输出顺序即为表的访问顺序。orders 表(别名 o)排在第一行,是驱动表。虽然 users 表的 rows=1 更小,但优化器认为先全表扫描 orders 表再对每行用主键查 users 表的总体代价更低(这是没有索引支持 o 表过滤条件时的无奈选择)。"
},
{
"index": 2,
"type": "single_choice",
"options": {
"A": "orders 表缺少 status 和 created_at 的联合索引,应创建 INDEX(status, created_at)",
"B": "users 表缺少 city 索引,应创建 INDEX(city)",
"C": "应使用 STRAIGHT_JOIN 强制 u 表作为驱动表",
"D": "增大 innodb_buffer_pool_size 即可解决全表扫描问题"
},
"question": "orders 表 type=ALL 导致全表扫描 50 万行,最有效的优化措施是什么?",
"answer": "A",
"explanation": "orders 表 type=ALL 且 possible_keys=NULL,说明没有可用索引。status 和 created_at 都是 WHERE 条件列,创建联合索引 (status, created_at) 可以让优化器利用索引定位 status=1 且 created_at >= '2026-01-01' 的行,避免全表扫描。即使 users 表缺少 city 索引(确实也缺),但 orders 表扫描 50 万行是当前最大的性能瓶颈。"
},
{
"index": 3,
"type": "short_answer",
"question": "users 表的 EXPLAIN 显示 filtered=25.00,这意味着什么?优化 users 表查询的最佳方案是什么?",
"answer": "filtered=25.00 表示通过索引(PRIMARY)找到的行中,只有 25% 满足 u.city='Beijing' 的 WHERE 条件。即每次对 orders 表的一行用主键查找 users 表时,返回的行还要在 server 层过滤掉 75%。最佳方案是创建 INDEX(city) 或 INDEX(city, name),使 city='Beijing' 能直接在索引层面完成过滤,减少回表。",
"keywords": [
"filtered",
"server 层过滤",
"city 索引"
],
"scoring_rubric": "正确解释 filtered 含义得1分,给出创建 city 索引建议得1分",
"explanation": "EXPLAIN 中的 filtered 列表示存储引擎返回的行中,经过 WHERE 条件过滤后保留的百分比。users 表通过主键 ref 查找返回 1 行,其中约 25% 满足 city='Beijing'。虽然单次过滤代价不高(因为 rows=1),但如果 orders 表有大量行,累积的 server 层过滤开销依然可观。创建 (city) 或 (city, name) 索引后,type 可从 ref 优化为更精准的 ref,filtered 接近 100%。"
}
],
"explanation": "本题考察 EXPLAIN 执行计划的核心字段分析:(1) type——ALL 表示全表扫描,ref 表示索引查找;(2) rows——估算扫描行数,直接影响 I/O 开销;(3) filtered——存储引擎返回行中满足 WHERE 条件的百分比;(4) possible_keys 与 key——可用索引和实际选择的索引;(5) Extra 中的 Using where 表示需要在 server 层额外过滤。优化思路优先级:先消除 ALL 全表扫描,再关注 filtered 过低的问题。",
"source": null,
"related": []
},
{
"id": "cr-003",
"type": "code_reading",
"difficulty": 5,
"tags": [
"mysql",
"slow-query",
"explain",
"index",
"bplus-tree"
],
"question": "分析以下慢查询日志中的 SQL 及其执行计划,找出性能问题并给出优化方案",
"code": "-- 慢查询日志 (long_query_time = 1)\n# Query_time: 12.345 Lock_time: 0.001 Rows_sent: 15 Rows_examined: 3200000\nSELECT p.id, p.title, p.price, c.name AS category,\n COUNT(DISTINCT r.user_id) AS review_count\nFROM products p\nLEFT JOIN product_categories pc ON p.id = pc.product_id\nLEFT JOIN categories c ON pc.category_id = c.id\nLEFT JOIN reviews r ON r.product_id = p.id\nWHERE p.status = 'active'\n AND p.price BETWEEN 50 AND 200\n AND (c.name = 'Electronics' OR c.name = 'Books')\nGROUP BY p.id, p.title, p.price, c.name\nHAVING review_count >= 5\nORDER BY review_count DESC\nLIMIT 20;\n\n-- EXPLAIN 结果:\n+----+-------------+-------+------+---------------+------+---------+------+--------+----------------------------------------------+\n| id | select_type | table | type | possible_keys | key | key_len | rows | filtered | Extra |\n+----+-------------+-------+------+---------------+------+---------+------+--------+----------------------------------------------+\n| 1 | SIMPLE | p | ALL | NULL | NULL | NULL | 800K | 1.25 | Using where; Using temporary; Using filesort|\n| 1 | SIMPLE | pc | ALL | NULL | NULL | NULL | 1.2M | 10.00 | Using join buffer (block nested loop) |\n| 1 | SIMPLE | c | eq_ref| PRIMARY | PRIMARY| 4 | 1 | 20.00 | Using where |\n| 1 | SIMPLE | r | ALL | NULL | NULL | NULL | 200K | 1.00 | Using join buffer (block nested loop) |\n+----+-------------+-------+------+---------------+------+---------+------+--------+----------------------------------------------+",
"language": "sql",
"sub_questions": [
{
"index": 1,
"type": "short_answer",
"question": "该查询扫描了约 320 万行但只返回 15 行,列出至少 3 个导致性能低下的原因。",
"answer": "1) products 表 type=ALL 全表扫描 80 万行,缺少 status 和 price 的索引,BETWEEN 条件无法走索引。2) product_categories 表 type=ALL 全表扫描 120 万行,缺少 product_id 外键索引,导致嵌套循环使用 Block Nested Loop 而非索引关联。3) reviews 表同样全表扫描 20 万行,缺少 product_id 索引。4) GROUP BY 产生 Using temporary 和 Using filesort,对大量中间结果进行排序和临时表操作,开销巨大。",
"keywords": [
"全表扫描",
"缺少索引",
"Block Nested Loop",
"Using temporary",
"Using filesort"
],
"scoring_rubric": "每个有效原因得1分,最多3分",
"explanation": "EXPLAIN 中三张表(p、pc、r)type=ALL 且 possible_keys=NULL,说明完全缺少索引。products 表有 80 万行,对每行都要在 product_categories 中扫描 120 万行的笛卡尔积(Block Nested Loop),再对每条匹配在 reviews 中扫描 20 万行,总扫描量呈乘积关系增长,导致 Rows_examined 高达 320 万。加上 GROUP BY + COUNT(DISTINCT) 的排序和去重开销,以及 HAVING 后置过滤,实际有效工作量远超扫描行数。"
},
{
"index": 2,
"type": "short_answer",
"question": "请给出具体的索引创建方案和 SQL 改写建议,使查询执行时间从 12 秒降到 1 秒以内。",
"answer": "索引方案:\n1) CREATE INDEX idx_products_status_price ON products(status, price); -- 覆盖 status 等值 + price 范围\n2) CREATE INDEX idx_pc_product_category ON product_categories(product_id, category_id); -- 覆盖 JOIN + category 过滤\n3) CREATE INDEX idx_reviews_product ON reviews(product_id, user_id); -- 覆盖 JOIN + COUNT(DISTINCT user_id)\n\nSQL 改写建议:\n- 将 WHERE 中 c.name IN ('Electronics','Books') 提前到 JOIN products 前作为子查询过滤,先通过 categories 表筛选 category_id,再 JOIN product_categories,减少扫描量。\n- 考虑将 LEFT JOIN 改为 INNER JOIN(如果业务允许),因为 WHERE 条件中引用了 c.name,LEFT JOIN 实际已等效于 INNER JOIN。",
"keywords": [
"联合索引",
"覆盖索引",
"子查询下推",
"INNER JOIN"
],
"scoring_rubric": "创建 idx_products_status_price 得1分,创建 idx_pc_product_category 得1分,创建 idx_reviews_product 得1分,SQL 改写建议得1分,最多4分",
"explanation": "索引设计遵循最左前缀原则:idx_products_status_price 让 status='active' 等值查找后在索引层内做 price BETWEEN 50 AND 200 的范围扫描,避免全表扫描。idx_pc_product_category 是覆盖索引,JOIN 和 category_id 过滤都在索引层完成。idx_reviews_product 覆盖 reviews 表的 JOIN 条件和 COUNT(DISTINCT user_id)。SQL 改写的核心思路是先缩小驱动表的扫描范围——通过子查询先筛选出 Electronics 和 Books 的 category_id,再反查 product_categories,将 120 万行扫描大幅缩减。"
},
{
"index": 3,
"type": "single_choice",
"options": {
"A": "增大 sort_buffer_size 到 64MB,避免 filesort 写磁盘",
"B": "在 products 表上创建 INDEX(status, price, id) 使 GROUP BY p.id 可以利用索引排序",
"C": "将 COUNT(DISTINCT r.user_id) 改为 COUNT(r.user_id) 并在应用层去重",
"D": "禁用 GROUP BY 的 ONLY_FULL_GROUP_BY 模式"
},
"question": "对于该查询的 GROUP BY + ORDER BY review_count DESC + LIMIT 20,以下哪种优化思路最合理?",
"answer": "B",
"explanation": "当前查询需要对大量中间结果做 GROUP BY 并统计 review_count,再排序取 Top 20。如果先通过索引大幅减少参与 JOIN 的行数(从 80 万降到几千),Using temporary 和 Using filesort 的代价会显著降低。创建 INDEX(status, price, id) 不是为了让 GROUP BY p.id 利用索引排序(因为有 JOIN 和聚合函数,MySQL 不会这么做),而是为了让 products 表的扫描从 ALL 变为 range,极大减少参与后续 JOIN 的行数。选项 A 的 sort_buffer_size 只影响排序内存,不改变扫描量;选项 C 改变了语义;选项 D 是危险操作且不解决根本问题。"
}
],
"explanation": "本题是一个典型的慢查询优化场景。核心问题链:products 表缺少索引导致 80 万行全表扫描 → 每行在 product_categories 中做 Block Nested Loop 扫描 120 万行 → 每行在 reviews 中再扫描 20 万行 → 巨大的中间结果集做 GROUP BY + COUNT(DISTINCT) + ORDER BY。优化策略分三层:(1) 创建精准索引消除全表扫描;(2) SQL 改写缩小驱动表数据量;(3) 考虑业务层面的折中(如限制查询范围、异步统计等)。在实际生产中,还需配合慢查询监控、pt-query-digest 分析和执行计划对比来验证优化效果。",
"source": null,
"related": []
},
{
"id": "cr-004",
"type": "code_reading",
"difficulty": 5,
"tags": [
"mysql",
"lock",
"deadlock"
],
"question": "分析以下并发事务场景,判断是否会发生死锁以及死锁产生的原因",
"code": "-- 表结构\nCREATE TABLE accounts (\n id INT PRIMARY KEY,\n balance DECIMAL(10,2) NOT NULL,\n INDEX idx_balance (balance)\n) ENGINE=InnoDB;\n\n-- 初始数据: id=1, balance=1000; id=2, balance=2000\n\n-- ========== 事务时间线 ==========\n-- T1 (2026-09-09 10:00:01.000)\nBEGIN;\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\n-- T1 获取 id=1 行的 X 锁\n\n-- T2 (2026-09-09 10:00:01.050)\nBEGIN;\nUPDATE accounts SET balance = balance + 100 WHERE id = 2;\n-- T2 获取 id=2 行的 X 锁\n\n-- T1 (2026-09-09 10:00:01.100)\nUPDATE accounts SET balance = balance + 100 WHERE id = 2;\n-- T1 尝试获取 id=2 行的 X 锁 → 阻塞(被 T2 持有)\n\n-- T2 (2026-09-09 10:00:01.150)\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\n-- T2 尝试获取 id=1 行的 X 锁 → 检测到死锁!\n\n-- ===== 另一个场景 (场景 B) =====\n-- T3 (事务)\nBEGIN;\nUPDATE accounts SET balance = balance - 50 WHERE balance = 1000;\n-- 使用二级索引 idx_balance 查找 balance=1000 的行\n\n-- T4 (事务)\nBEGIN;\nUPDATE accounts SET balance = balance - 50 WHERE balance = 2000;\n-- 使用二级索引 idx_balance 查找 balance=2000 的行\n\n-- T3\nUPDATE accounts SET balance = balance - 50 WHERE id = 1;\n-- T3 尝试获取 id=1 的 X 锁\n\n-- T4\nUPDATE accounts SET balance = balance - 50 WHERE id = 2;\n-- T4 尝试获取 id=2 的 X 锁",
"language": "sql",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"options": {
"A": "场景 A 会死锁,场景 B 不会死锁",
"B": "场景 A 会死锁,场景 B 也会死锁",
"C": "场景 A 不会死锁,场景 B 会死锁",
"D": "两个场景都不会死锁"
},
"question": "场景 A 和场景 B 分别会发生死锁吗?",
"answer": "B",
"explanation": "场景 A 是经典的交叉锁死锁:T1 持有 id=1 的 X 锁、请求 id=2 的 X 锁;T2 持有 id=2 的 X 锁、请求 id=1 的 X 锁,形成循环等待。场景 B 看似不会死锁(T3 用 balance=1000 更新 id=1,T4 用 balance=2000 更新 id=2),但 InnoDB 的二级索引加锁机制使得:T3 在 idx_balance 上获取 balance=1000 的记录锁后,还需要对聚簇索引的 id=1 加 X 锁;T4 同理。由于两个事务获取二级索引锁的顺序和聚簇索引锁的顺序可能不一致(取决于索引记录的物理顺序),实际上存在死锁风险。如果 idx_balance 上两条记录的顺序与 id 的顺序不同(如 balance=2000 对应 id=2 排在 balance=1000 对应 id=1 前面),就会产生交叉等待。"
},
{
"index": 2,
"type": "short_answer",
"question": "在场景 A 中,InnoDB 的死锁检测机制是如何工作的?死锁发生后 MySQL 会如何处理?",
"answer": "InnoDB 使用等待图(wait-for graph)算法检测死锁:维护事务等待关系的有向图,当图中出现环路时即判定死锁。检测时机是每次加锁请求发生阻塞时(T2 执行 UPDATE WHERE id=1 时触发检测)。死锁发生后,InnoDB 选择回滚代价最小的事务(通常是 undo log 量最少的事务)进行自动回滚,并返回错误 ERROR 1213 (40001): Deadlock found。被回滚的事务可以由应用层捕获异常后重试。innodb_deadlock_detect 参数可控制是否开启检测(默认 ON),innodb_lock_wait_timeout 控制锁等待超时。",
"keywords": [
"等待图",
"死锁检测",
"回滚",
"innodb_deadlock_detect",
"锁等待超时"
],
"scoring_rubric": "提到等待图/环路检测得1分,提到选择回滚代价最小的事务得1分,提到应用层重试得1分",
"explanation": "InnoDB 的死锁检测基于等待图算法,每当一个事务请求锁被阻塞时,会检查当前等待关系图是否形成环路。场景 A 中:T1→T2(T1 等待 T2 释放 id=2 的锁),T2→T1(T2 等待 T1 释放 id=1 的锁),形成环路。InnoDB 立即检测到并回滚 T2(假设 T2 的 undo log 更少)。在高并发场景下,频繁的死锁检测会消耗 CPU,可以通过 innodb_deadlock_detect=OFF 配合锁等待超时来降低开销,但这需要应用层处理超时重试逻辑。"
},
{
"index": 3,
"type": "short_answer",
"question": "请给出至少 3 种避免场景 A 中死锁的设计策略。",
"answer": "1) 固定加锁顺序:所有事务按 id 从小到大的顺序访问记录(先锁 id=1 再锁 id=2),消除循环等待条件。2) 使用 SELECT ... FOR UPDATE 显式加锁并统一在事务开始时一次性锁定所有需要的行,减少锁持有时间。3) 将大事务拆分为小事务,缩短锁持有时间窗口。4) 使用乐观锁(version 字段)替代悲观锁,通过 CAS 更新避免长时间持锁。5) 在业务层面确保转账操作的原子性——可以用单一 SQL UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100 来避免两行分两次更新。",
"keywords": [
"固定加锁顺序",
"缩短事务",
"乐观锁",
"一次性锁定"
],
"scoring_rubric": "每种有效策略得1分,最多3分",
"explanation": "死锁发生的四个必要条件是:互斥、持有并等待、不可剥夺、循环等待。打破任何一个条件即可避免死锁。固定加锁顺序打破循环等待条件——所有事务按 id 排序后依次获取锁,最多只能形成单向等待链。缩短事务持有时间降低死锁概率(虽然不完全消除)。乐观锁通过应用层版本检查避免数据库层面的锁竞争。对于转账场景,最根本的方案是将两行更新合并为应用层逻辑控制的单条 SQL,避免跨行事务。"
}
],
"explanation": "本题深入考察 InnoDB 锁机制和死锁分析。场景 A 是教科书级的交叉锁死锁,涉及行级 X 锁的互斥和循环等待。场景 B 揭示了一个更隐蔽的死锁来源:InnoDB 的二级索引加锁规则——UPDATE WHERE idx_col = val 会在二级索引上加记录锁,同时需要在聚簇索引(主键)上加 X 锁,两级索引的加锁顺序不一致可能导致死锁。实际生产中,还需要通过 SHOW ENGINE INNODB STATUS 查看 LATEST DETECTED DEADLOCK 区块来确认死锁详情,并结合 general_log 或 binlog 还原事务执行顺序。预防策略的核心思想是:统一加锁顺序、减少锁持有时间、降低锁粒度。",
"source": null,
"related": []
},
{
"id": "cr-005",
"type": "code_reading",
"difficulty": 4,
"tags": [
"redis",
"cluster",
"slot",
"hybrid-persistence",
"bigkey"
],
"question": "分析以下 Redis 集群配置和运维命令,判断集群状态和恢复方案",
"code": "# ===== Redis 集群节点信息 =====\n$ redis-cli -c -h 10.0.0.1 -p 7001 cluster nodes\n3a1b2c... 10.0.0.1:7001@17001 myself,master - 0 1694236800 1 connected 0-5460\n7d4e5f... 10.0.0.2:7002@17002 master - 0 1694236800 2 connected 5461-10922\nb8c9d0... 10.0.0.3:7003@17003 master - 0 1694236800 3 connected 10923-16383\ne1f2a3... 10.0.0.4:7004@17004 slave 3a1b2c... 0 1694236800 4 connected\na5b6c7... 10.0.0.5:7005@17005 slave 7d4e5f... 0 1694236800 5 connected\n\n# ===== 节点 7002 (10.0.0.2) 宕机后的集群状态 =====\n$ redis-cli -c -h 10.0.0.1 -p 7001 cluster info\ncluster_state:fail\ncluster_slots_assigned:16384\ncluster_slots_ok:10923\ncluster_slots_pfail:0\ncluster_slots_fail:5462\ncluster_known_nodes:6\ncluster_size:3\n\n# ===== 混合持久化配置 (redis.conf) =====\nappendonly yes\nappendfilename \"appendonly.aof\"\naof-use-rdb-preamble yes\naof-loading-trickle-size 4096\nsave 900 1\nsave 300 10\nsave 60 10000\n\n# ===== 恢复操作 =====\n# 1. 尝试恢复 7002 节点\n$ cp /var/lib/redis/7002/dump.rdb /var/lib/redis/7002/appendonlydir/appendonly.aof.1.rdb\n$ redis-server /etc/redis/7002.conf\n\n# 2. 查看恢复后的日志\n$ tail -20 /var/log/redis/7002.log\n[12345] 09 Sep 2026 10:05:00.123 * Done loading RDB preamble from AOF file\n[12345] 09 Sep 2026 10:05:00.234 * Starting BGSAVE for AOF\n[12345] 09 Sep 2026 10:05:00.235 * Background AOF rewrite started by pid 12346\n[12345] 09 Sep 2026 10:05:01.456 * Background AOF rewrite finished successfully",
"language": "redis",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"options": {
"A": "slots 5461-10922 失效,且 7005 (slave) 会自动提升为 master 接管这些 slot",
"B": "slots 5461-10922 失效,但需要手动执行 CLUSTER FAILOVER 才能故障转移",
"C": "slots 5461-10922 失效,需要手动分配 slot 给其他 master 节点",
"D": "集群状态为 fail 只是告警,不影响正常的 slot 路由"
},
"question": "节点 7002 (master, slots 5461-10922) 宕机后,集群状态 cluster_slots_fail=5462。以下分析正确的是?",
"answer": "A",
"explanation": "Redis Cluster 在 master 节点宕机后,其 slave 节点(7005)会在 cluster-node-timeout(默认 15 秒)后被标记为 PFAIL,随后被集群大多数 master 节点投票确认为 FAIL,自动提升为新的 master 并接管原 master 的所有 slot。从 cluster_nodes 输出看,7005 是 7002 的 slave,在正常配置下会自动故障转移。如果 cluster_slots_fail 持续显示 5462 而非恢复为 0,可能是因为 7005 也已下线或 cluster-node-timeout 还未到期。当 cluster_state=fail 时,整个集群对外拒绝服务(所有读写请求返回 CLUSTERDOWN 错误),直到所有 slot 都被正常 master 覆盖。"
},
{
"index": 2,
"type": "short_answer",
"question": "配置 aof-use-rdb-preamble yes 意味着什么?它如何影响 Redis 的 AOF 文件格式和恢复流程?",
"answer": "aof-use-rdb-preamble yes 表示开启混合持久化(Redis 4.0+),AOF 文件的格式变为:前半部分是 RDB 格式的全量数据快照,后半部分是 AOF 格式的增量写命令。恢复时的流程为:(1) 先以 RDB 格式快速加载前半部分的全量数据(速度快,因为是二进制格式);(2) 再逐条重放后半部分的 AOF 增量命令(保证数据完整性)。日志中 'Done loading RDB preamble from AOF file' 正是第一步完成的标志。这种混合格式兼顾了 RDB 快速恢复和 AOF 数据完整的优点——纯 AOF 恢复需要逐条重放所有命令(慢),纯 RDB 会丢失最后一次快照后的数据。",
"keywords": [
"混合持久化",
"RDB preamble",
"AOF",
"恢复流程"
],
"scoring_rubric": "正确解释混合持久化格式得1分,正确描述恢复流程(先RDB后AOF)得1分,对比纯RDB/AOF的优劣得1分",
"explanation": "混合持久化是 Redis 4.0 引入的优化。传统模式下:RDB 快照恢复快但会丢失快照间隔内的数据;AOF 数据完整但恢复慢(需要逐条执行所有写命令)。混合持久化将两者结合:触发 BGSAVE 时,生成的 AOF 文件以 RDB 头部 + AOF 尾部的方式组织。从配置看,save 900 1 等规则触发 RDB 快照,而 appendonly yes 同时记录所有写命令。aof-use-rdb-preamble yes 确保 AOF 文件以 RDB 格式开头。恢复时 Redis 检测到 RDB preamble 后先快速加载(毫秒到秒级),再重放 AOF 增量(通常很短),兼顾速度和完整性。"
},
{
"index": 3,
"type": "short_answer",
"question": "从 cluster_nodes 输出看,该集群有 3 master + 3 slave,slot 分配为 0-5460、5461-10922、10923-16383。如果需要扩展到 6 master 节点(每个 master 约 2730 个 slot),请说明 rehash 操作的大致流程。",
"answer": "Redis Cluster 扩容 rehash 流程:\n1) 新增 3 个 master 节点(7006/7007/7008)加入集群:CLUSTER MEET <ip> <port>\n2) 为每个新节点分配 slot:从现有 3 个 master 各迁移约 1820 个 slot 给 3 个新节点,例如 CLUSTER ADDSLOTS 或使用 redis-cli --cluster reshard 工具\n3) 迁移过程中,源节点将对应 slot 的数据通过 MIGRATE 命令逐 key 迁移到目标节点\n4) 迁移期间:源节点收到不属于自己的 slot 的请求时,返回 ASK 重定向;目标节点收到未完成迁移的 key 时,返回 MOVED 重定向\n5) 所有 slot 迁移完成后,集群状态恢复正常,新节点开始接收对应 slot 的读写请求\n\n注意:生产环境建议使用 redis-cli --cluster reshard <host:port> 交互式完成,它会自动处理 slot 状态标记和数据迁移。",
"keywords": [
"rehash",
"slot 迁移",
"MIGRATE",
"reshard",
"ASK/MOVED"
],
"scoring_rubric": "描述 CLUSTER MEET 加入节点得1分,描述 slot 分配和迁移过程得1分,提到 ASK/MOVED 重定向机制得1分",
"explanation": "Redis Cluster 的 slot 迁移是在线扩容的核心机制。每个 slot 在迁移前需要将状态从 NODE 设为 MIGRATING(源节点)和 IMPORTING(目标节点)。迁移过程中,源节点对正在迁移的 slot 的请求:如果 key 存在则正常处理,不存在则返回 ASK 重定向到目标节点。目标节点对 IMPORTING 状态的 slot:只有收到 ASKING 命令后的请求才会处理,否则返回 MOVED。这种设计确保了迁移期间集群的可用性——已迁移的 key 在新节点处理,未迁移的 key 在原节点处理,不会丢失数据。redis-cli --cluster reshard 工具封装了上述所有步骤。"
}
],
"explanation": "本题综合考察 Redis Cluster 的运维能力和混合持久化机制。从 cluster_nodes 输出可以分析出集群拓扑:3 master 各负责约 1/3 的 slot(共 16384 个),每个 master 有一个 slave 用于高可用。故障转移流程涉及 PFAIL → FAIL 状态转换和 slave 提升。混合持久化(aof-use-rdb-preamble)是 Redis 4.0+ 的重要特性,将 RDB 的快速加载和 AOF 的数据完整性结合。扩容 rehash 是 Redis Cluster 运维中最复杂的操作之一,涉及 ASK/MOVED 重定向、MIGRATE 命令和 slot 状态管理。实际运维中还需关注 bigkey 问题——如果迁移的 slot 中包含大 key(如百万元素的 Hash/Set),MIGRATE 命令会长时间阻塞,建议先通过 redis-cli --bigkeys 识别并拆分大 key。",
"source": null,
"related": []
}
]
}
@@ -0,0 +1,216 @@
{
"topic": "database-advanced",
"type": "fill_blank",
"schema_version": "1.0.0",
"generated": "2026-09-09T00:00:00+08:00",
"questions": [
{
"id": "fb-001",
"type": "fill_blank",
"difficulty": 2,
"tags": [
"mysql",
"bplus-tree",
"index"
],
"question": "在 InnoDB 中,主键索引的叶子节点直接存储______,因此基于主键查询效率最高;而二级索引的叶子节点存储的是主键值,查到后通常还需要______才能获取完整行数据。",
"answer": [
"完整行数据",
"整行数据",
"完整的行数据",
"聚簇索引回表",
"回表"
],
"answer_rule": "any",
"explanation": "InnoDB 使用聚簇索引(Clustered Index),主键索引的 B+ 树叶子节点存储的是完整的行数据,因此通过主键查询可以直接获取数据,无需额外的 IO。而二级索引(非聚簇索引)的叶子节点只存储主键值,查到主键后需要拿着主键回到聚簇索引中查找完整行数据,这个过程称为「回表」。回表会产生额外的随机 IO,是影响查询性能的重要因素。",
"source": null,
"related": []
},
{
"id": "fb-002",
"type": "fill_blank",
"difficulty": 3,
"tags": [
"mysql",
"index",
"composite-index"
],
"question": "联合索引 (a, b, c) 遵循最左前缀原则,以下查询中能使用该索引的有:WHERE a=1 AND b=2、WHERE a=1 AND c=3、WHERE b=2 AND c=3、WHERE a=1 ORDER BY b。其中 ______ 的查询无法使用索引(填查询条件序号,如 2、3)。",
"answer": [
"2、3",
"2和3",
"b=2 AND c=3, a=1 AND c=3",
"第2个和第3个",
"WHERE a=1 AND c=3, WHERE b=2 AND c=3"
],
"answer_rule": "any",
"explanation": "最左前缀原则要求查询条件从联合索引的最左列开始连续匹配。索引 (a, b, c):① WHERE a=1 AND b=2 命中前两列,可使用索引;② WHERE a=1 AND c=3 跳过了 b,只能用到 a 这一列,c 无法使用索引过滤;③ WHERE b=2 AND c=3 完全跳过了最左列 a,无法使用索引;④ WHERE a=1 ORDER BY b 命中 a 列,排序字段 b 在索引中紧跟 a,可避免 filesort。因此第 2、3 个查询无法有效利用索引。",
"source": null,
"related": []
},
{
"id": "fb-003",
"type": "fill_blank",
"difficulty": 3,
"tags": [
"mysql",
"index",
"covering-index"
],
"question": "当查询所需的所有列都包含在索引中时,MySQL 可以直接从索引获取数据而无需回表,这种技术称为______。在 EXPLAIN 的 Extra 列中会显示为 ______。",
"answer": [
"覆盖索引",
"Using index"
],
"answer_rule": "all",
"explanation": "覆盖索引(Covering Index)是指查询需要的所有字段都被某个索引所覆盖,InnoDB 只需扫描索引即可完成查询,避免了回表操作。此时 EXPLAIN 的 Extra 列会显示「Using index」,表示使用了覆盖索引。覆盖索引是优化查询性能的重要手段,能显著减少随机 IO。例如对索引 (name, age) 执行 SELECT name, age FROM users WHERE name = 'Tom',所有需要的列都在索引中,无需回表。",
"source": null,
"related": []
},
{
"id": "fb-004",
"type": "fill_blank",
"difficulty": 4,
"tags": [
"mysql",
"index",
"bplus-tree"
],
"question": "MySQL 5.6 引入的索引下推(Index Condition Pushdown,ICP)优化,将 ______ 下推到存储引擎层在索引扫描阶段完成过滤。在 EXPLAIN 的 Extra 列中,启用 ICP 时会显示 ______。",
"answer": [
"WHERE 条件过滤",
"部分 WHERE 条件",
"索引不覆盖的过滤条件",
"Using index condition"
],
"answer_rule": "all",
"explanation": "索引下推(ICP)是 MySQL 5.6 引入的优化。在没有 ICP 之前,存储引擎根据索引找到记录后回表,Server 层再进行 WHERE 过滤。ICP 将部分 WHERE 条件下推到存储引擎层,在索引扫描时就进行过滤,减少了回表次数和 Server 层的处理开销。当 ICP 生效时,EXPLAIN Extra 列显示「Using index condition」。注意 ICP 仅适用于 InnoDB 和 MyISAM,且只对联合索引起作用——因为单列索引的叶子节点已经包含了完整信息,不需要额外过滤。",
"source": null,
"related": []
},
{
"id": "fb-005",
"type": "fill_blank",
"difficulty": 2,
"tags": [
"mysql",
"explain"
],
"question": "EXPLAIN 执行计划的 type 列中,性能从好到差的大致顺序为:system > ______ > eq_ref > ref > range > index > ALL。其中 ______ 表示全表扫描,通常需要重点优化。",
"answer": [
"const",
"ALL"
],
"answer_rule": "all",
"explanation": "EXPLAIN 的 type 列表示 MySQL 在表中找到匹配行的方式。const 表示通过主键或唯一索引查找到单行(如 WHERE id=1),速度最快;eq_ref 表示对每一行的 JOIN 都从表中用主键/唯一索引查一行;ref 表示使用普通索引查找;range 表示索引范围扫描;index 表示全索引扫描;ALL 是全表扫描,性能最差。type 列是从左到右性能递减的,日常优化目标至少是 range 级别,避免 ALL。",
"source": null,
"related": []
},
{
"id": "fb-006",
"type": "fill_blank",
"difficulty": 3,
"tags": [
"mysql",
"slow-query",
"explain"
],
"question": "慢查询日志通过设置 ______ 参数(单位秒)开启,超过该阈值的查询会被记录。在 EXPLAIN 的 Extra 列中,______ 表示 MySQL 需要额外的排序操作,______ 表示使用了临时表,二者都可能消耗较多内存和 CPU,需要重点关注优化。",
"answer": [
"slow_query_log",
"long_query_time",
"Using filesort",
"Using temporary"
],
"answer_rule": "any",
"explanation": "慢查询日志通过 slow_query_log=ON 开启,long_query_time 设置阈值(默认 10 秒)。建议生产环境设为 1 秒甚至更低。EXPLAIN Extra 中:「Using filesort」表示 MySQL 无法利用索引完成排序,需要额外的排序算法(通常因为 ORDER BY 字段不在索引中);「Using temporary」表示使用了临时表(常见于 GROUP BY、DISTINCT、子查询等场景)。这两者通常意味着较大的内存和 CPU 开销,尤其在数据量大时性能影响显著,应通过优化索引和 SQL 来避免。",
"source": null,
"related": []
},
{
"id": "fb-007",
"type": "fill_blank",
"difficulty": 4,
"tags": [
"mysql",
"lock"
],
"question": "InnoDB 中,行锁基于索引实现,锁住的是索引记录而非物理行。当查询未命中索引时,InnoDB 会退化为______锁,锁定所有行,严重影响并发。间隙锁(Gap Lock)锁定的是索引记录之间的间隙,用于防止______插入,从而解决幻读问题。",
"answer": [
"表锁",
"幻读",
"新行",
"其他事务的"
],
"answer_rule": "any",
"explanation": "InnoDB 的行锁是通过给索引上的索引项加锁来实现的。如果 WHERE 条件没有走索引(如全表扫描),InnoDB 将锁住整个表(所有行),等效于表锁,严重降低并发性能。间隙锁(Gap Lock)锁住的是索引记录之间的「间隙」,以及第一条记录之前和最后一条记录之后的间隙,其目的是防止其他事务在间隙中插入新记录,从而避免幻读(Phantom Read)。临键锁(Next-Key Lock)= 行锁 + 间隙锁,是 InnoDB 在可重复读隔离级别下的默认锁类型。",
"source": null,
"related": []
},
{
"id": "fb-008",
"type": "fill_blank",
"difficulty": 4,
"tags": [
"redis",
"cluster",
"slot"
],
"question": "Redis Cluster 使用一致性哈希将数据分布在 16384 个 slot 中,每个 key 通过 CRC16 算法对 16384 取模确定所属 slot。当需要迁移 slot 时,使用 ______ 命令将 key-value 从源节点转移到目标节点,迁移过程中需要同时保持源和目标节点对该 slot 的访问。",
"answer": [
"MIGRATE",
"migrate"
],
"answer_rule": "any",
"explanation": "Redis Cluster 采用 16384 个虚拟节点(slot)来分配数据。每个 key 通过 CRC16(key) % 16384 确定 slot 编号,每个 master 节点负责一部分 slot。MIGRATE 命令用于在节点间迁移 key,它是一个原子操作:先在目标节点还原 key,成功后在源节点删除。迁移过程中,Redis Cluster 会通过 ASK/MOVED 重定向来保证客户端能正确路由请求。Redis Cluster 没有使用虚拟节点的概念,而是用固定的 16384 个 slot 做直接映射,每个节点负责一段连续的 slot 范围。",
"source": null,
"related": []
},
{
"id": "fb-009",
"type": "fill_blank",
"difficulty": 3,
"tags": [
"redis",
"sentinel"
],
"question": "Redis 哨兵(Sentinel)模式至少需要部署 ______ 个哨兵节点,以避免脑裂问题。哨兵通过类似 Raft 的协议选举 leader,当多数哨兵确认 master 不可达时触发故障转移。Sentinel 还负责 ______,即将新的 master 地址通知给所有客户端。",
"answer": [
"3",
"客户端通知",
"通知客户端",
"配置发布",
"发布新的 master 地址"
],
"answer_rule": "any",
"explanation": "Redis Sentinel 至少需要 3 个节点才能保证高可用,因为故障判定需要 quorum(多数派)同意。如果只有 2 个哨兵,任何一个挂掉都无法达成多数派,失去了容错能力。Sentinel 使用类似 Raft 的算法进行 leader 选举:当检测到 master 下线时,哨兵之间互相通信,通过投票选出 leader 来执行故障转移。故障转移完成后,Sentinel 通过「配置发布/订阅」机制(Pub/Sub)通知所有连接的客户端新的 master 地址,客户端会自动重连。",
"source": null,
"related": []
},
{
"id": "fb-010",
"type": "fill_blank",
"difficulty": 4,
"tags": [
"redis",
"hybrid-persistence",
"active-defrag",
"lru"
],
"question": "Redis 4.0 引入的混合持久化机制在 AOF rewrite 时,先以 RDB 格式写入当前全量数据,再追加 rewrite 期间的增量 AOF 命令,兼顾了 ______ 和 ______ 的优势。Redis 4.0 还引入了 ______ 功能,通过后台线程扫描并整理内存碎片,减少因频繁删改导致的内存浪费。当内存达到 maxmemory 时,Redis 根据 ______ 策略淘汰 key,常用策略包括 allkeys-lru、volatile-lru、volatile-lfu 等。",
"answer": [
"RDB 恢复速度",
"AOF 数据完整性",
"恢复速度",
"数据安全",
"active-defrag",
"active-defragging",
"内存淘汰"
],
"answer_rule": "any",
"explanation": "混合持久化(Hybrid Persistence)结合了 RDB 和 AOF 的优点:AOF rewrite 时先写 RDB 快照(恢复速度快),再追加增量 AOF 命令(保证数据完整性),大幅缩短了重启恢复时间。active-defrag 是 Redis 4.0 引入的主动内存碎片整理功能,通过后台线程对内存进行碎片扫描和重新分配,无需重启即可释放碎片内存,可通过 activedefrag yes 开启。Redis 的内存淘汰策略在内存达到 maxmemory 时触发:allkeys-lru 对所有 key 做 LRU 淘汰;volatile-lru 只对设了过期时间的 key 做 LRU;volatile-lfu 使用 LFU(最不经常使用)算法淘汰。LFU 比 LRU 更精准,能淘汰真正不活跃的 key。",
"source": null,
"related": []
}
]
}
@@ -0,0 +1,25 @@
{
"slug": "database-advanced",
"name": "数据库进阶",
"description": "B+树索引优化、EXPLAIN执行计划、Redis集群/哨兵、混合持久化、慢查询优化",
"tags": [],
"question_files": {
"single_choice": "single_choice.json",
"true_false": "true_false.json",
"fill_blank": "fill_blank.json",
"short_answer": "short_answer.json",
"code_reading": "code_reading.json"
},
"stats": {
"total": 35,
"by_type": {
"single_choice": 10,
"true_false": 5,
"fill_blank": 10,
"short_answer": 5,
"code_reading": 5
}
},
"created": "2026-09-09",
"updated": "2026-09-09"
}
@@ -0,0 +1,170 @@
{
"topic": "database-advanced",
"type": "short_answer",
"schema_version": "1.0.0",
"generated": "2026-09-09T00:00:00+08:00",
"questions": [
{
"id": "sa-001",
"type": "short_answer",
"difficulty": 2,
"tags": [
"mysql",
"bplus-tree",
"index",
"composite-index",
"covering-index"
],
"question": "请解释 InnoDB 中聚簇索引与非聚簇索引的区别,并说明联合索引的最左前缀原则和覆盖索引的含义。针对表 orders(user_id, order_id, amount, status),若需要高效执行以下三种查询,分别应如何设计索引?\n1. SELECT * FROM orders WHERE user_id = 100 AND order_id = 'A001'\n2. SELECT user_id, order_id FROM orders WHERE user_id = 100\n3. SELECT amount FROM orders WHERE order_id = 'A001'",
"answer": "一、聚簇索引与非聚簇索引的区别:\n聚簇索引:InnoDB 中数据行按主键顺序物理存储,每个 InnoDB 表有且仅有一个聚簇索引。主键即聚簇索引;若无主键,选择第一个唯一非空索引;都没有则生成隐藏的 row_id 作为聚簇索引。叶子节点存储完整行数据。\n非聚簇索引(二级索引):叶子节点存储的是主键值而非完整行数据。通过非聚簇索引查询非索引列时,需要根据主键回表(回表查询),即通过主键再到聚簇索引中查找完整行。\n\n二、最左前缀原则:\n联合索引 (a, b, c) 按从左到右的顺序匹配。查询条件必须从最左列开始才能命中索引。可以跳过中间列吗?不能。例如 (a, b, c) 索引,WHERE a=1 AND c=3 只能用到 a 列,c 列无法使用索引。\n\n三、覆盖索引:\n当查询所需的所有列都包含在某个索引中时,无需回表,称为覆盖索引。EXPLAIN 中 Extra 列显示 Using index 表示使用了覆盖索引。\n\n针对三种查询的索引设计:\n1. WHERE user_id=100 AND order_id='A001':建联合索引 (user_id, order_id),两列均在索引中,可精确定位。\n2. SELECT user_id, order_id WHERE user_id=100:建联合索引 (user_id, order_id),user_id 用于过滤,user_id 和 order_id 均在索引中,构成覆盖索引,无需回表。\n3. SELECT amount WHERE order_id='A001':应单独建 order_id 索引,因为 amount 不在联合索引中(即使有联合索引也无法避免回表取 amount)。如果查询频繁,可考虑建覆盖索引 (order_id, amount) 来避免回表。",
"keywords": [
"聚簇索引",
"非聚簇索引",
"二级索引",
"回表",
"主键",
"叶子节点",
"最左前缀",
"覆盖索引",
"Using index",
"联合索引"
],
"scoring_rubric": "满分10分:1)正确描述聚簇索引与非聚簇索引的区别(含叶子节点存储内容差异)得3分;2)正确解释最左前缀原则得2分;3)正确解释覆盖索引得2分;4)三道查询的索引设计各1分。缺少回表概念扣1分,缺少覆盖索引与Using index关联扣1分。",
"explanation": "聚簇索引的核心特征是「数据即索引」——叶子节点直接存储完整行数据,因此一个表只能有一个聚簇索引。非聚簇索引(二级索引)的叶子节点只存主键值,查询非索引列时需要「回表」到聚簇索引再查一次。最左前缀原则是联合索引的匹配规则,本质是 B+ 树从左到右逐列排序的结构决定的。覆盖索引是一种优化手段:如果索引已经包含了查询需要的所有列,就完全不需要回表,大幅减少 I/O。这三者共同构成了 MySQL 索引优化的基础框架。",
"source": null,
"related": []
},
{
"id": "sa-002",
"type": "short_answer",
"difficulty": 3,
"tags": [
"mysql",
"explain",
"slow-query"
],
"question": "请解释 MySQL EXPLAIN 执行计划中 type 列和 Extra 列的常见值及其含义。针对以下慢查询,请分析 EXPLAIN 输出并给出优化建议:\n\nSELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id WHERE o.created_at > '2025-01-01' AND u.status = 1 GROUP BY u.name ORDER BY o.total DESC LIMIT 10;\n\n假设 EXPLAIN 输出为:type=ALL, rows=500000, Extra=Using temporary; Using filesort。",
"answer": "一、type 列常见值(从好到差):\n1. system:表只有一行,系统表,最优。\n2. const:通过主键或唯一索引单行查找,最多匹配一行,如 WHERE id=1。\n3. eq_ref:关联查询中,被驱动表通过主键或唯一索引进行等值匹配,每次关联只匹配一行。\n4. ref:通过普通索引进行等值匹配,可能返回多行。\n5. range:索引范围扫描,如 WHERE id > 10、WHERE id IN (1,2,3)。\n6. index:全索引扫描,遍历整个索引树(不回表)。\n7. ALL:全表扫描,最差,应尽量避免。\n\n二、Extra 列常见值:\n1. Using index:覆盖索引,无需回表,好。\n2. Using where:Server 层进行了额外过滤,不完全在存储引擎层完成。\n3. Using temporary:使用了临时表,常见于 GROUP BY / DISTINCT 等操作,需关注性能。\n4. Using filesort:需要额外排序操作,未利用索引排序,可能是性能瓶颈。\n5. Using join buffer:连接查询中被驱动表无可用索引,使用了 Join Buffer。\n6. Select tables optimized away:优化器直接从索引或统计信息中获取结果,无需扫描表。\n\n三、针对给定查询的优化建议:\n当前 type=ALL 表示全表扫描 orders 表(或 users 表),rows=500000 说明扫描了大量行。Extra 中 Using temporary 和 Using filesort 说明 GROUP BY 和 ORDER BY 都无法利用索引。\n\n优化措施:\n1. 为 orders 表建索引 (created_at, user_id, total)——created_at 用于 WHERE 过滤,user_id 用于 JOIN,total 用于覆盖排序和 SELECT。\n2. 为 users 表建索引 (status, id, name)——status 过滤,id 关联,name 覆盖 GROUP BY。\n3. 如分页不深,可考虑延迟关联优化:先查子查询获取 id 列表,再关联取完整数据。\n4. 使用 SHOW WARNINGS 查看优化器重写后的 SQL,确认查询是否被正确优化。",
"keywords": [
"EXPLAIN",
"type",
"ALL",
"const",
"eq_ref",
"ref",
"range",
"Extra",
"Using temporary",
"Using filesort",
"Using index",
"慢查询",
"全表扫描",
"索引设计"
],
"scoring_rubric": "满分10分:1)正确解释 type 列至少4个常见值(含全表扫描ALL)得3分;2)正确解释 Extra 列至少3个常见值(含Using temporary和Using filesort)得3分;3)针对给定查询给出至少2条合理优化建议得2分;4)能正确识别 type=ALL 和 Extra=Using temporary; Using filesort 的问题并关联到具体表得2分。",
"explanation": "EXPLAIN 是 MySQL 慢查询分析的核心工具。type 列反映了存储引擎查找数据的方式,ALL 是最差的全表扫描,const/eq_ref 是最优的单行查找。Extra 列揭示了额外执行信息:Using temporary 表示需要创建临时表来完成 GROUP BY,Using filesort 表示需要额外的排序操作,这两者在大数据量下是严重的性能隐患。在本例中,type=ALL + Using temporary + Using filesort 组合表明查询完全没有利用到索引,需要通过添加合适的联合索引来同时覆盖过滤、连接和排序需求。",
"source": null,
"related": []
},
{
"id": "sa-003",
"type": "short_answer",
"difficulty": 3,
"tags": [
"redis",
"cluster",
"slot",
"consistent-hashing"
],
"question": "Redis Cluster 使用 16384 个 slot 对数据进行分片。请回答:\n1. slot 的编号范围和分配方式是什么?\n2. 当客户端发送一个 GET key 命令时,Redis Cluster 如何确定该 key 应该由哪个节点处理?\n3. 在线扩容(增加一个新节点)的完整流程是什么?缩容时需要注意什么?\n4. 如果在扩容过程中,客户端访问了一个正在迁移中的 slot,会发生什么?",
"answer": "1. Slot 编号与分配:\n编号范围为 0-16383(共 16384 个 slot)。每个 Redis Cluster 节点负责一部分 slot。分配方式由 cluster.conf 中的 cluster-config-file 记录,默认均匀分配。例如 3 节点集群,节点 A 负责 0-5460,节点 B 负责 5461-10922,节点 C 负责 10923-16383。\n\n2. Key 到节点的路由:\n计算公式:slot = CRC16(key) % 16384。CRC16 是循环冗余校验算法。客户端(如 redis-cli)在发送命令前先计算 slot,然后根据集群元数据(slot 到节点的映射关系)将请求发送到对应的 master 节点。如果请求发到了错误节点,该节点会返回 MOVED 重定向响应,客户端更新路由缓存后重发。Redis 3.2+ 还支持 ASK 临时重定向(用于 slot 迁移中)。\n\n3. 在线扩容流程:\na) 启动新节点并加入集群:redis-cli --cluster add-node <new_node> <existing_node>\nb) 分配 slot:redis-cli --cluster reshard <node>,指定要迁移的 slot 数量和目标节点。系统会自动选择源节点。\nc) 迁移过程:对每个要迁移的 slot,源节点执行 MIGRATE 命令将 slot 中的所有 key 迁移到目标节点。迁移期间源节点对迁移中的 key 的写操作会触发 ASK 重定向。\nd) 确认完成:redis-cli --cluster check 验证集群状态。\n\n缩容注意事项:\na) 先将要下线节点的 slot 迁移到其他节点(reshard)。\nb) 确认该节点上的所有 slot 已迁移完毕(cluster nodes 中该节点不再负责任何 slot)。\nc) 删除节点:redis-cli --cluster del-node。\nd) 注意:如果要下线的是 master 节点,需要先处理其 slave 节点(先 del slave,再 del master,或者先将 slave 绑定到其他 master)。\n\n4. 访问迁移中 slot 的行为:\nRedis Cluster 在 slot 迁移过程中支持 ASK 重定向。具体流程:\n- 客户端发送命令到源节点。\n- 源节点发现该 slot 正在迁移到目标节点(IMPORTING 状态),如果 key 还在源节点则正常处理;如果 key 已迁走,返回 ASK 重定向到目标节点。\n- 客户端收到 ASK 后,向目标节点发送 ASKING + 原命令。注意 ASK 是单次重定向,不会更新客户端的 slot 路由缓存(与 MOVED 不同)。\n- 迁移完成后,源节点发送 MOVED,客户端永久更新路由。",
"keywords": [
"16384",
"CRC16",
"slot",
"MOVED",
"ASK",
"reshard",
"MIGRATE",
"cluster",
"分片",
"扩容",
"缩容",
"重定向"
],
"scoring_rubric": "满分10分:1)正确描述 slot 编号范围和 CRC16 路由机制得3分;2)正确解释 MOVED 和 ASK 重定向的区别得2分;3)正确描述扩容完整流程(add-node + reshard + MIGRATE)得3分;4)正确说明缩容注意事项(先迁移 slot 再删除节点)得2分。缺少 CRC16 公式扣1分,混淆 MOVED 和 ASK 扣1分。",
"explanation": "Redis Cluster 使用哈希槽(hash slot)实现数据分片,是典型的分布式哈希方案。16384 这个数字的选择是权衡了节点数量上限(约 1000 个)和每个节点需要维护的 slot 位图大小(16384/8=2048 字节)后的结果。CRC16%16384 保证了数据在节点间的均匀分布。MOVED 和 ASK 的设计体现了分布式系统中「强一致路由」与「临时过渡路由」的分离,ASK 仅在迁移期间使用且不更新客户端缓存,避免了全局路由信息的不一致。",
"source": null,
"related": []
},
{
"id": "sa-004",
"type": "short_answer",
"difficulty": 4,
"tags": [
"mysql",
"lock",
"deadlock"
],
"question": "请详细解释 MySQL InnoDB 中以下三种锁的区别和应用场景:\n1. 行锁(Record Lock)\n2. 间隙锁(Gap Lock)\n3. 临键锁(Next-Key Lock)\n\n并回答以下问题:\n- 在可重复读(RR)隔离级别下,以下 SQL 会产生什么类型的锁?\n SELECT * FROM accounts WHERE balance BETWEEN 1000 AND 5000 FOR UPDATE;\n (假设 balance 列有索引,值分布为 500, 2000, 8000)\n- 什么是死锁?MySQL 如何检测死锁?如何预防死锁?",
"answer": "一、三种锁的区别:\n1. 行锁(Record Lock):锁定索引记录本身。只锁住该行,其他事务可以修改同一范围的其他行。是最细粒度的锁。\n\n2. 间隙锁(Gap Lock):锁定索引记录之间的间隙(即两个索引值之间的范围),不锁定记录本身。目的是防止其他事务在间隙中插入新记录,用于解决幻读问题。例如索引值为 5 和 10,则间隙锁可能锁住 (5, 10) 这个范围。\n\n3. 临键锁(Next-Key Lock):行锁 + 间隙锁的组合,锁定一个索引记录及其前面的间隙。即锁定 [gap, record]。InnoDB 在 RR 隔离级别下的默认加锁方式。例如索引值为 5, 10, 15,临键锁可能锁住 (5, 10] 这个区间。\n\n二、针对给定 SQL 的锁分析:\nSELECT * FROM accounts WHERE balance BETWEEN 1000 AND 5000 FOR UPDATE;\n索引值分布为 500, 2000, 8000。\n\n在 RR 隔离级别下:\n- balance 在 1000-5000 范围内的索引值只有 2000,所以会对 balance=2000 这行加行锁。\n- 由于 BETWEEN 1000 AND 5000 的范围,还会在两侧间隙加间隙锁:(500, 1000) 和 (2000, 5000)——实际是左闭右开的区间。\n- 综合起来:临键锁覆盖 (500, 2000] + (2000, 5000),即防止在此范围内插入新值(如 balance=3000 的记录),同时锁住已存在的 2000 这行。\n\n三、死锁相关:\n1. 死锁定义:两个或多个事务互相持有对方需要的锁,形成循环等待,永远无法继续执行。\n\n2. MySQL 检测机制:InnoDB 使用「等待图」(wait-for graph)检测死锁。维护一个事务依赖关系图,如果图中出现环路,则存在死锁。InnoDB 有一个 innodb_deadlock_detect 参数控制是否开启检测(默认开启)。还可以通过 innodb_lock_wait_timeout 设置锁等待超时。\n\n3. 死锁处理:检测到死锁后,InnoDB 选择一个「代价最小」的事务进行回滚(通常是更新最少行的事务),并向该事务返回错误 ERROR 1213 (40001): Deadlock found。\n\n4. 死锁预防措施:\na) 保持事务尽量短小,减少持锁时间。\nb) 事务中的 SQL 按固定顺序访问表和行,避免交叉加锁。\nc) 合理设计索引,使 SQL 能精确锁定需要的行,避免大范围加锁(如使用覆盖索引减少锁范围)。\nd) 在业务允许的情况下降低隔离级别为 READ COMMITTED(RC),RC 下没有间隙锁,大大降低死锁概率。\ne) 使用 SELECT ... FOR UPDATE NOWAIT 或 SKIP LOCKED 避免锁等待。",
"keywords": [
"行锁",
"Record Lock",
"间隙锁",
"Gap Lock",
"临键锁",
"Next-Key Lock",
"死锁",
"等待图",
"innodb_deadlock_detect",
"幻读",
"RR隔离级别",
"FOR UPDATE"
],
"scoring_rubric": "满分10分:1)正确区分三种锁的定义和区别得3分;2)正确分析给定 SQL 产生的锁类型及范围得3分;3)正确解释死锁定义和检测机制得2分;4)给出至少3条有效的死锁预防措施得2分。间隙锁范围分析错误扣1分,未提及 RC 隔离级别降低死锁扣1分。",
"explanation": "InnoDB 的锁机制围绕解决幻读问题而设计。在 RR 隔离级别下,临键锁(Next-Key Lock)是默认加锁方式,它结合了行锁和间隙锁的优点:既锁住已有记录防止被修改,又锁住间隙防止新记录插入。理解锁的范围对于死锁预防至关重要——很多死锁正是因为事务锁定了过大的范围(如范围查询无精确索引导致锁全表间隙),使两个事务的锁范围产生交叉。降低隔离级别到 RC 是一种激进但有效的策略,因为 RC 下只有行锁没有间隙锁,但代价是可能出现不可重复读。",
"source": null,
"related": []
},
{
"id": "sa-005",
"type": "short_answer",
"difficulty": 4,
"tags": [
"redis",
"sentinel",
"hybrid-persistence",
"slow-query"
],
"question": "请分别回答以下三部分内容:\n\n一、Redis 哨兵(Sentinel)模式:\n1. 哨兵节点之间如何选举 leader?采用什么算法?\n2. 故障转移(failover)的完整流程是什么?\n\n二、Redis 混合持久化(Hybrid Persistence):\n1. RDB 和 AOF 各自的优缺点是什么?\n2. 混合持久化是如何结合 RDB 和 AOF 的?AOF rewrite 的具体流程是什么?\n3. 故障恢复时,混合持久化的恢复流程是怎样的?\n\n三、Redis 内存优化:\n1. 如何排查 Redis 中的大 key 问题?大 key 有哪些危害?\n2. Redis 的内存淘汰策略有哪些?各自适用什么场景?",
"answer": "一、Redis 哨兵:\n1. Leader 选举采用 Raft 算法:\n- 每个 Sentinel 节点每秒向其他节点发送 PING。\n- 当某个 Sentinel 发现 master 在 down-after-milliseconds 时间内无响应,标记为 SDOWN(主观下线)。\n- 当 quorum(法定人数)个 Sentinel 都标记为 SDOWN 时,master 被标记为 ODOWN(客观下线)。\n- Sentinel 发起 leader 选举:每个 Sentinel 向其他 Sentinel 发送 Sentinel is-master-down-by-addr 命令请求投票。\n- 每个 Sentinel 在一个配置纪元(epoch)中只能投一票,先到先得。\n- 获得 quorum+1 票(即超过半数)的 Sentinel 成为 leader,执行故障转移。\n\n2. 故障转移流程:\na) Leader Sentinel 从 slave 列表中筛选新 master:\n - 排除已下线的、断线时间过长的(超过 down-after-milliseconds * 10)、slave-priority=0 的节点。\n - 优先选择 offset 最大(数据最新)的 slave。\nb) 对选中的 slave 执行 SLAVEOF NO ONE,使其成为 master。\nc) 向其他 slave 发送 SLAVEOF <new_master_ip> <port>,使它们复制新 master。\nd) 更新 sentinel.conf 中的 master 地址,通知客户端新的 master。\n\n二、Redis 混合持久化:\n1. RDB 优缺点:\n优点:文件紧凑,恢复速度快,适合备份和灾难恢复。\n缺点:两次快照之间的数据可能丢失(取决于 RDB 间隔);fork 子进程时可能占用较多内存。\n\nAOF 优缺点:\n优点:数据安全性高(everysec 策略最多丢1秒数据),可读性强。\n缺点:文件体积大,恢复速度慢(需重放所有写命令)。\n\n2. 混合持久化(Redis 4.0+,aof-use-rdb-preamble yes):\n在 AOF rewrite 时,不再是纯命令重放,而是:\na) 先以 RDB 格式将当前全量数据写入 AOF 文件头部(RDB preamble)。\nb) rewrite 期间的增量写命令追加在 RDB 数据之后(AOF 格式)。\nc) 最终 AOF 文件 = RDB 格式的全量数据 + AOF 格式的增量数据。\n\nAOF rewrite 流程:\n1) Redis 创建子进程执行 rewrite(类似 RDB 的 fork)。\n2) 子进程遍历内存数据,生成新的 AOF 文件(混合模式下先写 RDB 格式全量数据,再写增量命令)。\n3) rewrite 期间的写命令同时写入旧 AOF 文件的缓冲区和新 AOF 文件尾部。\n4) 子进程完成后,主进程将缓冲区中的增量数据追加到新 AOF 文件。\n5) 原子替换旧 AOF 文件为新 AOF 文件。\n\n3. 故障恢复流程:\na) 检查是否开启混合持久化(aof-use-rdb-preamble)。\nb) 如果开启:先以 RDB 方式恢复全量数据(快速),然后重放 RDB 之后的 AOF 命令恢复增量数据。\nc) 如果未开启纯 AOF 模式:重放所有 AOF 命令(较慢)。\nd) 如果只有 RDB:直接加载 RDB 文件(最快,但可能丢失最近数据)。\ne) 恢复优先级:AOF > RDB(如果两者都存在)。\n\n三、Redis 内存优化:\n1. 大 key 排查方法:\na) redis-cli --bigkeys:扫描集群,找出每个数据类型中最大的 key。\nb) redis-cli --memkeys:按内存使用量排序。\nc) SCAN + MEMORY USAGE:遍历所有 key,用 MEMORY USAGE 命令逐个检查。\nd) 使用 RDB 工具(如 rdb)离线分析 dump.rdb 文件。\ne) 在 Redis 6.0+ 使用 OBJECT HELP 和 DEBUG 检查。\n\n大 key 危害:\na) 操作延迟高:DEL 大 key 可能阻塞主线程(Redis 4.0+ 已改为异步删除)。\nb) 网络带宽占用:读写大 key 消耗大量网络资源。\nc) 内存不均:在 Cluster 中可能导致某个节点内存远大于其他节点。\nd) 持久化受影响:fork 时大 key 增加写时复制(COW)的内存开销。\ne) 过期删除开销:大 key 过期时可能造成短暂的性能抖动。\n\n2. 内存淘汰策略(8种):\na) noeviction(默认):内存满时拒绝写入,返回错误。适合不允许数据丢失的场景。\nb) allkeys-lru:所有 key 中淘汰最近最少使用的。适合热点数据明显的通用场景。\nc) volatile-lru:只在设置了过期时间的 key 中淘汰 LRU。适合有明确过期策略的缓存。\nd) allkeys-lfu:所有 key 中淘汰最不经常使用的(Redis 4.0+)。适合访问频率差异大的场景。\ne) volatile-lfu:只在有过期时间的 key 中淘汰 LFU。\nf) allkeys-random:所有 key 中随机淘汰。适合各 key 访问概率相近的场景。\ng) volatile-random:只在有过期时间的 key 中随机淘汰。\nh) volatile-ttl:只在有过期时间的 key 中淘汰 TTL 最小(即将过期)的。适合按时效性管理数据。\n\n选择建议:缓存场景首选 allkeys-lru;有明确过期需求用 volatile-lru 或 volatile-ttl;对数据准确性要求高用 noeviction。",
"keywords": [
"Sentinel",
"Raft",
"leader选举",
"SDOWN",
"ODOWN",
"quorum",
"failover",
"混合持久化",
"RDB",
"AOF",
"rewrite",
"fork",
"aof-use-rdb-preamble",
"大key",
"bigkeys",
"MEMORY USAGE",
"noeviction",
"allkeys-lru",
"allkeys-lfu",
"volatile-ttl"
],
"scoring_rubric": "满分15分:哨兵部分5分(leader选举算法2分 + 故障转移流程3分);混合持久化部分5分(RDB/AOF优缺点1分 + 混合原理2分 + 恢复流程2分);内存优化部分5分(大key排查2分 + 危害1分 + 淘汰策略2分)。每个部分答出核心要点即可得分,细节不完整酌情扣分。",
"explanation": "Redis 的高可用方案经历了主从→哨兵→Cluster 的演进。哨兵通过 Raft 算法选举 leader 来协调故障转移,避免了脑裂问题。混合持久化是 Redis 4.0 的重要改进,它结合了 RDB 的快速恢复和 AOF 的数据安全性,通过「RDB头 + AOF尾」的文件格式,使故障恢复既快又全。内存优化方面,大 key 是 Redis 运维中最常见的性能杀手之一,而内存淘汰策略的选择需要根据业务场景的数据重要性、访问模式和时效性来综合判断。这三部分共同构成了 Redis 生产环境运维的核心知识体系。",
"source": null,
"related": []
}
]
}
@@ -0,0 +1,217 @@
{
"topic": "database-advanced",
"type": "single_choice",
"schema_version": "1.0.0",
"generated": "2026-09-09T00:00:00+08:00",
"questions": [
{
"id": "sc-001",
"type": "single_choice",
"difficulty": 3,
"tags": [
"mysql",
"bplus-tree",
"index"
],
"question": "在 InnoDB 中,关于聚簇索引和非聚簇索引(二级索引)的区别,以下说法正确的是?",
"options": {
"A": "聚簇索引的叶子节点存储的是完整行数据,非聚簇索引的叶子节点存储的是主键值",
"B": "每个表可以有多个聚簇索引,但只能有一个非聚簇索引",
"C": "聚簇索引的查询效率一定比非聚簇索引高,因此应尽量为所有列都建立聚簇索引",
"D": "非聚簇索引查找数据时不需要回表,因为叶子节点直接指向磁盘上的行"
},
"answer": "A",
"explanation": "InnoDB 的聚簇索引(通常即主键索引)的叶子节点直接存储完整行数据,而非聚簇索引(二级索引)的叶子节点存储的是主键值。通过二级索引查找数据时,需要先拿到主键值,再回到聚簇索引中查找完整行,这个过程称为「回表」。每个 InnoDB 表只能有一个聚簇索引(数据物理上按聚簇索引排序存储),但可以有多个非聚簇索引。索引过多会增加写入开销和存储空间,不应盲目建立。",
"source": null,
"related": []
},
{
"id": "sc-002",
"type": "single_choice",
"difficulty": 2,
"tags": [
"mysql",
"bplus-tree",
"composite-index"
],
"question": "有一张表建立了联合索引 INDEX idx_abc(a, b, c),以下哪个查询能有效利用该索引?",
"options": {
"A": "SELECT * FROM t WHERE b = 1 AND c = 2",
"B": "SELECT * FROM t WHERE c = 2 AND a = 1",
"C": "SELECT * FROM t WHERE a = 1 AND c = 2",
"D": "SELECT * FROM t WHERE a = 1 AND b > 5 AND c = 2"
},
"answer": "B",
"explanation": "联合索引遵循最左前缀原则。MySQL 优化器会自动调整 WHERE 条件的顺序,所以选项 B(c=2 AND a=1)等价于 a=1 AND c=2,可以使用索引的 a 列。但注意 c=2 无法使用索引(中间跳过了 b)。选项 A 完全跳过了最左列 a,无法使用该索引。选项 C 使用了 a 但跳过了 b 直接用 c,只能用到 a 列。选项 D 中 b 是范围查询(b>5),根据最左前缀原则,范围查询后的列 c 无法使用索引,但 a 和 b 部分可以使用。不过选项 B 能让优化器识别出 a=1 可用索引,是本题最明确能利用索引的选项。",
"source": null,
"related": []
},
{
"id": "sc-003",
"type": "single_choice",
"difficulty": 3,
"tags": [
"mysql",
"bplus-tree",
"index",
"covering-index"
],
"question": "关于覆盖索引(Covering Index),以下描述正确的是?",
"options": {
"A": "覆盖索引是指索引包含了查询所需的所有列,从而避免回表操作",
"B": "覆盖索引要求索引列必须是主键,否则无法覆盖",
"C": "使用覆盖索引时,EXPLAIN 的 Extra 列会显示 Using filesort",
"D": "覆盖索引只适用于等值查询,范围查询无法使用覆盖索引"
},
"answer": "A",
"explanation": "覆盖索引是指查询所需的列都包含在索引中,无需回表读取聚簇索引中的完整行数据。EXPLAIN 中 Extra 列显示 Using index 表示使用了覆盖索引。覆盖索引不限于主键索引,任何二级索引只要包含了查询所需的列即可。范围查询同样可以利用覆盖索引,只要索引包含了所需列,例如 INDEX(name, age) 可以覆盖 SELECT name, age FROM t WHERE name > 'a'。Using filesort 表示需要额外排序,与覆盖索引无关。",
"source": null,
"related": []
},
{
"id": "sc-004",
"type": "single_choice",
"difficulty": 4,
"tags": [
"mysql",
"explain"
],
"question": "以下 EXPLAIN 输出中,type 列的值从最优到最差的正确排列顺序是?",
"options": {
"A": "ALL > index > range > ref > const",
"B": "const > ref > range > index > ALL",
"C": "const > range > ref > index > ALL",
"D": "range > const > ref > ALL > index"
},
"answer": "B",
"explanation": "EXPLAIN 的 type 列表示 MySQL 访问表的方式,从最优到最差的顺序为:system > const > eq_ref > ref > range > index > ALL。const 表示通过主键或唯一索引精确匹配一行;ref 表示非唯一索引等值匹配;range 表示索引范围扫描;index 表示全索引扫描(遍历索引树);ALL 表示全表扫描(最差)。选项 B 正确地将 const > ref > range > index > ALL 从最优到最差排列。",
"source": null,
"related": []
},
{
"id": "sc-005",
"type": "single_choice",
"difficulty": 3,
"tags": [
"mysql",
"explain"
],
"question": "EXPLAIN 的 Extra 列出现 Using temporary 和 Using filesort,通常意味着查询需要优化。以下哪种查询最容易同时出现这两个警告?",
"options": {
"A": "SELECT * FROM t WHERE id = 1",
"B": "SELECT DISTINCT col1 FROM t ORDER BY col2 LIMIT 10",
"C": "SELECT * FROM t FORCE INDEX(PRIMARY) WHERE id BETWEEN 1 AND 100",
"D": "SELECT count(*) FROM t WHERE col1 = 'value'"
},
"answer": "B",
"explanation": "选项 B 的查询同时涉及 DISTINCT(去重)和 ORDER BY(排序),且 col1 和 col2 不是同一个列,无法通过索引同时满足。MySQL 需要创建临时表来处理 DISTINCT,并且需要 filesort 来完成 ORDER BY,因此同时出现 Using temporary 和 Using filesort。选项 A 只是主键等值查询,type 为 const,无需临时表或排序。选项 C 使用 FORCE INDEX 做范围扫描,通常也不会触发临时表。选项 D 是简单的 WHERE 条件聚合,count(*) 不需要排序或去重。",
"source": null,
"related": []
},
{
"id": "sc-006",
"type": "single_choice",
"difficulty": 2,
"tags": [
"mysql",
"slow-query"
],
"question": "关于 MySQL 慢查询日志,以下配置和说法正确的是?",
"options": {
"A": "slow_query_log=ON 开启慢查询日志后,所有查询都会被记录到日志中",
"B": "long_query_time 默认值为 10 秒,表示执行时间超过 10 秒的 SQL 才会被记录",
"C": "慢查询日志只能记录 SELECT 语句,不包括 INSERT/UPDATE/DELETE",
"D": "开启慢查询日志会对数据库性能产生显著影响,生产环境不应开启"
},
"answer": "B",
"explanation": "MySQL 慢查询日志(slow_query_log)默认 long_query_time 为 10 秒,只有执行时间超过该阈值的 SQL 才会被记录。慢查询日志记录所有类型的语句(SELECT、INSERT、UPDATE、DELETE 等),只要执行时间超过阈值。默认情况下只有管理员可以查看慢查询日志,不会记录所有查询,因此对性能影响极小,生产环境强烈建议开启以便排查性能问题。也可以设置 log_queries_not_using_indexes=ON 记录未使用索引的查询。",
"source": null,
"related": []
},
{
"id": "sc-007",
"type": "single_choice",
"difficulty": 4,
"tags": [
"mysql",
"lock",
"next-key-lock",
"gap-lock"
],
"question": "在 InnoDB 的默认隔离级别(REPEATABLE READ)下,执行以下语句时,关于锁的行为描述正确的是?\n\nDELETE FROM orders WHERE order_id = 100;\n假设 order_id 是主键列。",
"options": {
"A": "会加行锁,锁定 order_id = 100 的那一行记录",
"B": "会加临键锁(Next-Key Lock),锁定 order_id = 100 前面的间隙",
"C": "会加间隙锁(Gap Lock),锁定 order_id = 100 周围的范围",
"D": "REPEATABLE READ 级别下不会加任何锁"
},
"answer": "A",
"explanation": "当使用主键进行精确等值匹配(WHERE order_id = 100)时,InnoDB 只需要加一个行锁(Record Lock)锁定该行即可,不需要间隙锁或临键锁。间隙锁和临键锁主要用于防止幻读,在非唯一索引的范围查询或主键的范围查询时才会出现。等值查询命中已存在的记录时,锁的粒度最小,只锁定匹配的那一行。DELETE 语句在 REPEATABLE READ 级别下会获取排他锁(X Lock),即该行在事务提交前不能被其他事务修改或读取(当前读)。",
"source": null,
"related": []
},
{
"id": "sc-008",
"type": "single_choice",
"difficulty": 3,
"tags": [
"redis",
"cluster",
"slot",
"consistent-hashing"
],
"question": "Redis Cluster 使用 16384 个 slot 分片来分布数据,关于 slot 的分配和路由机制,以下说法正确的是?",
"options": {
"A": "客户端发送任意 key 的命令时,Redis Cluster 会自动将 key 路由到任意一个节点处理",
"B": "使用 CRC16 对 key 计算哈希值后对 16384 取模来确定 key 所属的 slot",
"C": "每个 Redis 节点平均分配约 16384 个 slot,节点数固定后无法调整",
"D": "16384 个 slot 必须全部分配完毕才能正常提供服务,任何 slot 未分配都会导致集群不可用"
},
"answer": "B",
"explanation": "Redis Cluster 使用 CRC16(key) % 16384 来确定每个 key 所属的 slot。每个节点负责一部分 slot,客户端发送命令时,如果 key 不在当前节点,会收到 MOVED 重定向响应。slot 的分配是动态的,可以通过 reshard 操作在节点间迁移 slot。只要至少有 6470 个 slot 被分配(覆盖所有 slot 的一半以上 + 每个 master 至少一个),集群就可以正常工作,但通常建议 100% 覆盖。CRC16 是一种高效的哈希算法,16384 个 slot 使用 2KB 的位图在节点间通信,这是选择 16384 而非更大数字的原因之一。",
"source": null,
"related": []
},
{
"id": "sc-009",
"type": "single_choice",
"difficulty": 4,
"tags": [
"redis",
"sentinel"
],
"question": "关于 Redis Sentinel(哨兵)的故障转移流程,以下描述正确的是?",
"options": {
"A": "当 master 被标记为主观下线(SDOWN)后,Sentinel 会立即开始故障转移",
"B": "Sentinel 通过 Raft 协议选举 Leader,由 Leader 执行故障转移操作",
"C": "故障转移时,Sentinel 会优先选择 slave 的 offset 最大的节点作为新 master",
"D": "故障转移完成后,Sentinel 不会通知客户端新的 master 地址,客户端需要自行探测"
},
"answer": "B",
"explanation": "Redis Sentinel 的故障转移流程:首先,单个 Sentinel 发现 master 不响应,标记为 SDOWN(主观下线);当足够多的 Sentinel(quorum 数量)都认为 master SDOWN 后,标记为 ODOWN(客观下线);然后通过类似 Raft 的选举机制选出一个 Leader Sentinel;由 Leader 执行故障转移:从 slave 中选择合适的节点提升为新 master(优先级 > offset > runid),通知其他 slave 复制新 master,通知客户端新的 master 地址。注意不是 offset 最大就优先——还需要先看 slave-priority,只有 priority 相同时才比较 offset。",
"source": null,
"related": []
},
{
"id": "sc-010",
"type": "single_choice",
"difficulty": 3,
"tags": [
"redis",
"hybrid-persistence",
"lru"
],
"question": "关于 Redis 的持久化和内存管理,以下说法正确的是?",
"options": {
"A": "Redis 混合持久化(hybrid persistence)是在 RDB 快照的基础上追加 AOF 日志,重启时先加载 RDB 再重放增量 AOF,兼顾恢复速度和数据完整性",
"B": "Redis 混合持久化是将 RDB 和 AOF 数据同时写入同一个文件,恢复时随机读取两者的数据进行合并",
"C": "Redis 的 allkeys-lru 策略只会在键空间不足时淘汰设置了过期时间的 key",
"D": "Redis 开启内存碎片整理(activedefrag)会导致主线程完全阻塞,建议生产环境关闭"
},
"answer": "A",
"explanation": "Redis 混合持久化(Redis 4.0+)在 AOF 重写时,将当前 RDB 快照数据写入 AOF 文件头部,重写后的增量命令追加在 RDB 数据之后。重启时先加载 RDB 部分(快速恢复大部分数据),再重放后面的 AOF 增量命令(保证数据完整性)。这结合了 RDB 恢复速度快和 AOF 数据丢失少的优点。allkeys-lru 策略会在所有 key 中(不论是否设置了过期时间)淘汰最近最少使用的 key。Redis 4.0+ 的 activedefrag 在后台线程执行碎片整理,不会完全阻塞主线程,生产环境可以开启。",
"source": null,
"related": []
}
]
}
@@ -0,0 +1,80 @@
{
"topic": "database-advanced",
"type": "true_false",
"schema_version": "1.0.0",
"generated": "2026-09-09T00:00:00+08:00",
"questions": [
{
"id": "tf-001",
"type": "true_false",
"difficulty": 3,
"tags": [
"mysql",
"bplus-tree",
"composite-index"
],
"question": "在MySQL中,对于联合索引(a, b, c),当查询条件为 WHERE b = 1 AND c = 2 时,优化器可以自动调整条件顺序以匹配索引最左前缀,从而使用该索引。",
"answer": false,
"explanation": "联合索引(a, b, c)要求查询条件必须从最左列a开始才能命中索引。优化器虽然可以重排WHERE子句中条件的执行顺序(将 b=1 AND c=2 调整为 c=2 AND b=1),但无论怎么调整,条件中始终缺少最左列a,因此该联合索引完全无法被使用。最左前缀原则是B+树联合索引的物理存储结构决定的——索引按照(a, b, c)的顺序排序,没有a的值就无法在B+树上进行有效的范围定位。",
"source": null,
"related": []
},
{
"id": "tf-002",
"type": "true_false",
"difficulty": 2,
"tags": [
"mysql",
"explain"
],
"question": "在MySQL EXPLAIN输出中,type列的值为'index'表示全表扫描,而'ALL'表示使用了索引扫描。",
"answer": false,
"explanation": "这道题将两者搞反了。type='ALL'表示全表扫描(Full Table Scan),是最差的访问方式,需要逐行扫描聚簇索引中的所有记录。type='index'表示全索引扫描(Full Index Scan),虽然也需要扫描整个索引树,但至少使用了二级索引,在覆盖索引场景下甚至不需要回表,因此性能通常优于ALL。访问效率从好到差的典型顺序为:system > const > eq_ref > ref > range > index > ALL。",
"source": null,
"related": []
},
{
"id": "tf-003",
"type": "true_false",
"difficulty": 3,
"tags": [
"mysql",
"slow-query",
"index"
],
"question": "开启MySQL慢查询日志后,所有执行时间超过long_query_time阈值的SQL语句都会被完整记录到慢查询日志中。",
"answer": false,
"explanation": "超过long_query_time阈值的SQL并不一定会被记录,还受到min_examined_row_limit参数的影响——只有实际扫描行数超过该值的查询才会被记录。默认情况下min_examined_row_limit=0,此时所有超过阈值的查询都会被记录;但如果设置了非零值,那些虽然执行时间超标但扫描行数很少的SQL会被过滤掉。此外,慢查询日志的开启需要先设置slow_query_log=ON,且记录的是执行时间(而非锁等待时间),锁等待时间可通过log_slow_admin_statements等参数单独控制。",
"source": null,
"related": []
},
{
"id": "tf-004",
"type": "true_false",
"difficulty": 4,
"tags": [
"mysql",
"lock"
],
"question": "InnoDB的间隙锁(Gap Lock)和临键锁(Next-Key Lock)在读已提交(READ COMMITTED)隔离级别下仍然生效,用于防止幻读。",
"answer": false,
"explanation": "间隙锁和临键锁只在可重复读(REPEATABLE READ)及以上隔离级别下生效。在读已提交(READ COMMITTED)级别下,InnoDB仅使用记录锁(Record Lock)锁定索引记录本身,不会对索引记录之间的间隙加锁。这意味着RC级别下无法防止幻读——另一个事务可以在间隙中插入新记录。这是MySQL默认选择RR作为隔离级别的重要原因之一:通过间隙锁+临键锁在RR级别下实现幻读防护。注意意向锁(Intention Lock)和元数据锁(MDL)在所有隔离级别下都存在,它们与间隙锁是不同层面的锁机制。",
"source": null,
"related": []
},
{
"id": "tf-005",
"type": "true_false",
"difficulty": 3,
"tags": [
"redis",
"cluster"
],
"question": "在Redis Cluster中,当某个master节点负责的slot持续无法被访问时,该master的slave会自动发起投票选举并完成故障转移,客户端通过MOVED或ASK重定向自动感知拓扑变化。",
"answer": true,
"explanation": "Redis Cluster的故障转移机制如下:1) 每个master节点定期向其他节点发送PING,当某个master被标记为PFAIL(疑似下线)后,集群通过Gossip协议传播该状态;2) 当超过半数的master节点都将该节点标记为FAIL时,其slave会发起选举,通过类似Raft的投票机制竞争晋升为新master;3) 获得多数投票的slave被提升为master,接管原master负责的slot,并向集群广播配置更新。客户端收到MOVED响应后会更新本地路由表,后续请求直接发往新master,整个过程对应用层基本透明。值得注意的是,Redis Cluster默认需要至少3个master节点且每个master至少1个slave才能保证高可用。",
"source": null,
"related": []
}
]
}